2

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.

zack_falcon
  • 4,186
  • 20
  • 62
  • 108
  • A couple potential problems I can spot before attempting an official "answer": you are using the `acc` variable twice, so that's confusing if not problematic. Remember for your calls to `Map.put/3`, you have to re-assign the result, you can't just call `Map.put(acc, ball_a, new_a)` and call it a day unless it's the very last part of the function and you are benefiting from Elixir's implicit return. – Everett Apr 06 '21 at 16:02
  • I kinda based that on this answer here: https://stackoverflow.com/questions/29924170/elixir-looping-through-and-adding-to-map But I probably didn't do it right or didn't understand it right. – zack_falcon Apr 07 '21 at 03:05
  • I think the `BadMapError` problem might be due to the fact you are using `Enum.reduce/2` (uses the first value as initial acc) and not `Enum.reduce/3` (explicit initial acc). `Enum.reduce(results, %{}, fn ...` should fix this, but you probably have other errors as well (e.g there is probably no `acc` in `Enum.map(result, fn {ball_b, body_b} acc ->`) – sabiwara Apr 07 '21 at 11:37
  • 1
    This feels snarled in a way that makes it difficult to posit a solution, and perhaps this indicates that the problem isn't framed correctly (or at least cleanly)? For instance, wouldn't the result of `checkStaticCollisions/3` potentially return a list of all balls that are in a state of collision (based on their current coordinates and their radius)? Are 3-way collisions possible? You may be able to `checkStaticCollisions` _before_ any enumeration, and then get the new positions, and only then would you need to apply this to `ball_map`? – Everett Apr 07 '21 at 13:35
  • @Everett, sorry for the confusion, `checkStaticCollisions` just checks if two bodies collided, and returns `true` or `false`. `calculatePosAfterCollision` calculates the collision and returns two new updated bodies. Three way collisions are indeed possible, but it should be handled as what collides with what first, hence why I need to update the new positions immediately after detecting collisions. – zack_falcon Apr 07 '21 at 14:58
  • 1
    Thanks for the clarifications. If 3-way collisions are possible, but you are handling the collisions sequentially, isn't that equivalent with handling 2-ball collisions ONLY, and iterating over those? Maybe it would help if you clarified what `result` contains. You could simplify things a bit if you updated `checkStaticCollisions` to return `false` when ball_a == ball_b. – Everett Apr 07 '21 at 15:57
  • @Everett, `result` is `ball_map`, thanks for that catch. I've updated the code in question. So sorry, I was copy-paste-changing this without much mind. I've also taken your suggestion and added the check for `ball_a == ball_b` in `checkStaticCollisions` (great suggestion!). – zack_falcon Apr 07 '21 at 16:16
  • 1
    It's helpful to update the code. I would encourage you include all of the relevant code in your question, including the other functions you are calling or at least include the function and have it return a sample result so we can follow the flow of the pieces. It's difficult to make mental leaps and imagine what maybe is happening in functions we can't see. – Everett Apr 08 '21 at 00:15
  • @Everett, I've added the other functions `checkStaticCollisions`, `checkCircleCollision`, and `calculatePosAfterCollision`. The `circle` and `vector` functions are in other def modules, so I put it on a pastebin here: https://pastebin.com/zX6L5zS8 (also in the question above). – zack_falcon Apr 08 '21 at 02:38

0 Answers0