Categories
Mastering Development

Elixir: Looping through a map, comparing it to itself, and updating it

I have a map of a billiard ball table, as follows:

ball_map = %{
    "cue" => {"x":-15.0, "z": 0.0, "velocity_x": 0.0, "velocity_z": 0.0, "is_idle": true},
    "ball_1" => {"x":15.0, "z": 0.0, "velocity_x": 0.0, "velocity_z": 0.0, "is_idle": true},
    "ball_2" => {"x":17.0, "z": 1.1, "velocity_x": 0.0, "velocity_z": 0.0, "is_idle": true},
    "ball_3" => {"x":17.0, "z": -1.1, "velocity_x": 0.0, "velocity_z": 0.0, "is_idle": true}
}

I need to apply collisions to it. For that to work properly, each ball must be checked against the others, and the ball locations must be updated as it checks. In my previous attempts, I made a "collision map" and then applied the new locations after, but this had problems with subsequent collisions.

So what I’m trying to do is

  • Loop through each item in the map.
  • Compare that item with every other item, and run a check for collision on it (I have a function for that).
  • Update both items upon collision, while keeping the format of ball_map.

The code I’ve come up with for that is this:

collision_result = Enum.reduce(ball_map, fn {ball_a, body_a}, acc -> 
    new_result = Enum.map(ball_map, fn {ball_b, body_b} ->

        has_collided = checkStaticCollisions(ball_a, body_a, ball_b, body_b, const_physics["billiard_ball_radius"])
        # Takes in parameters (Key of Ball A, Value of Ball A, Key of Ball B, Value of Ball B, radius)
        # Returns true or false depending if Ball A != Ball B and collisions happen

        if has_collided == true do
            calc = calculatePosAfterCollision(ball_a, body_a, ball_b, body_b, const_physics["billiard_ball_radius"])
            # Takes in parameters (Key of Ball A, Value of Ball A, Key of Ball B, Value of Ball B, radius)
            # Returns {"id_of_ball_a" => {new X and Z pos}, "id_of_ball_b" => {new X and Z pos}}

            new_a_pos = calc[ball_a]
            new_b_pos = calc[ball_b]
            new_a = %{"x" => new_a_pos["x"], "z" => new_a_pos["z"], "velocity_x" => body_a["velocity_x"], "velocity_z" => body_a["velocity_z"], "is_idle" => body_a["is_idle"]}
            new_b = %{"x" => new_b_pos["x"], "z" => new_b_pos["z"], "velocity_x" => body_b["velocity_x"], "velocity_z" => body_b["velocity_z"], "is_idle" => body_b["is_idle"]}

            # Supposedly, Update both ball A and ball B, keep the rest
            Map.put(acc, ball_a, new_a) #?
            Map.put(acc, ball_b, new_b) #?

        else
            # Supposedly, No change for ball A or ball B
            %{ball_a => body_a, ball_b => body_b}
        end
    end)
    # Not sure what to output here, if any
end)

Now this doesn’t work, as I’m getting a BadMapError (I assume something to do with the accs on else’s), but I’m also positive this will result in a list within a list.

I’m still rather new to Elixir, and I’m not sure what else I can do. Is there anything else I can try to make this work? Are there non enum-approaches (does recursion count?)?

EDIT: here are the other functions in-use:

def checkStaticCollisions(key_a, body_a, key_b, body_b, ball_radius) do
    if (key_a == key_b) do
        false
    else
        pos_a = PGS.Vector.new(body_a["x"], 0.0, body_a["z"])
        pos_b = PGS.Vector.new(body_b["x"], 0.0, body_b["z"])
        circle_a = PGS.Circle.new(pos_a, ball_radius)
        circle_b = PGS.Circle.new(pos_b, ball_radius)
        checkCircleCollision(circle_a, circle_b)
    end
end

def checkCircleCollision(circleA, circleB) do 
    posA = circleA.position
    posB = circleB.position
    radiusA = circleA.radius
    radiusB = circleB.radius

    distance = PGS.Vector.distance(posA, posB)
    circleDistance = radiusA + radiusB

    if distance < circleDistance do 
        # collision occured
        true
    else
        false
    end
end

def calculatePosAfterCollision(id_a, ball_a, id_b, ball_b, radius) do
    pos_a = PGS.Vector.new(ball_a["x"], 0.0, ball_a["z"])
    pos_b = PGS.Vector.new(ball_b["x"], 0.0, ball_b["z"])
    radius_a = radius
    radius_b = radius       
    distance = PGS.Vector.distance(pos_a, pos_b)
    pos_delta = PGS.Vector.sub(pos_a, pos_b)
    overlap_dist = (distance - radius_a - radius_b) * 0.5
    
    vector_difference = PGS.Vector.new(overlap_dist * pos_delta.x / distance, overlap_dist * pos_delta.y / distance, overlap_dist * pos_delta.z / distance)

    new_pos_a = PGS.Vector.sub(pos_a, vector_difference)
    new_pos_b = PGS.Vector.add(pos_b, vector_difference)

    %{id_a => new_pos_a, id_b => new_pos_b}
end

For the Circle and Vector functions, please refer to this.

Leave a Reply

Your email address will not be published. Required fields are marked *