Skip to content

Arx 06 Resolving Collisions

noooway edited this page Jan 25, 2017 · 2 revisions

Ok, we can detect collisions. Now it's time to deal with how our objects react on them, i.e. with the collision resolution.

In our current setting we have 4 types of collisions: ball-brick, ball-wall, ball-platform, and platform-wall. Certainly, they should be resolved differently. Right now, in the resolve_collisions function we can query collider for collisions with the collider_shape of any game object. HC would return all other colliding shapes, but we have no way to tell, which game objects they belong to. To proceed, we need to identify not only colliding shapes, but also corresponding game objects.

Hopefully, the trick is simple. For each game object, we need to add another field into it's collider_shape table, referencing this game object itself. Here is an example for the Ball class:

function Ball:new( o )
   .....
   o.name = o.name or "ball"
   .....
   o.collider_shape = o.collider:circle( o.position.x,
                                         o.position.y,
                                         o.radius )
   o.collider_shape.game_object = o --(*1)
   return o
end

(*1): o is the new ball object we are constructing. o.collider_shape is the HC-shape object, used by HC to detect collisions. Internally, o.collider_shape is just a Lua table, so we can manually add the new field game_object to this table (as long as it doesn't mess with how HC works).

Same line, o.collider_shape.game_object = o should be added to the Brick, Wall and Platform classes.

This way, in the resolve_collision function we can get access to all properties of the second colliding object by referencing another_shape.game_object table.

function resolve_collisions( dt )
  for another_shape, delta in pairs( collider:collisions( ball.collider_shape ) ) do
     local toprint = string.format(
                          "Ball is colliding with %s. Separating vector = (%s,%s)", 
                          another_shape.game_object.name, delta.x, delta.y)
     print( toprint )     
  end
end

Note that another crucial ingredient for this trick to work is the field name in each of the Ball, Wall, Platform and Brick classes. It allows to identify the type of the other object, that participates in the collision.

Now that we know, what objects are colliding, we can call appropriate methods that describe objects' reaction on collisions:

function resolve_collisions( dt )
   -- Platform
   local collisions = collider:collisions( platform.collider_shape )
   for another_shape, separating_vector in pairs( collisions ) do
      if another_shape.game_object.name == "wall" then
         platform:react_on_wall_collision( another_shape, separating_vector ) --(*1)
      end
   end
   -- Ball
   local collisions = collider:collisions( ball.collider_shape )
   for another_shape, separating_vector in pairs( collisions ) do
      if another_shape.game_object.name == "wall" then
         ball:react_on_wall_collision( another_shape, separating_vector )     --(*2)
      elseif another_shape.game_object.name == "platform" then
         ball:react_on_platform_collision( another_shape, separating_vector ) --(*3)
      elseif another_shape.game_object.name == "brick" then
         ball:react_on_brick_collision( another_shape, separating_vector )    --(*4a)
         another_shape.game_object:react_on_ball_collision(                   --(*4b)
            ball.collider_shape,
            (-1) * vector( separating_vector.x, separating_vector.y )  )      --(*4c)
      end
   end
end

(*1): Platform-wall.
(*2): Ball-wall.
(*3): Ball-platform.
(*4): Ball-brick. In the ball-brick collision, we call both the ball's method to react on the brick (*4a), as well as the brick's method to react on the ball (*4b). Note, that we have to reverse separating_vector for react_on_ball_collision (*4c), since it is expected to point outside of the shape that experiences a collision with another one.

Speaking of actual collision effects, we need to deny platform pass the walls, change the ball speed on collision with anything, and destroy bricks on collision with the ball.

For the platform-wall, it is enough to shift the platform position by the separating vector. It is also necessary to synchronize it's new position with the HC shape.

function Platform:react_on_wall_collision( other, separating_vector )
   self.position = self.position + separating_vector
   self.collisions.shape:moveTo( self.position.x + self.width / 2,
                                 self.position.y + self.height / 2 )   
end

Since the platform is not affected by the ball, we do not need Platform:react_on_ball_collision for now.

For the ball, for we have 3 similar functions ( we'll make them different later ):

function Ball:react_on_wall_collision( another_shape, separating_vector )
   self.position = self.position + separating_vector
   self.collider_shape:moveTo( self.position:unpack() )
   self:normal_rebound( separating_vector )
end

function Ball:react_on_brick_collision( another_shape, separating_vector )
   self.position = self.position + separating_vector
   self.collider_shape:moveTo( self.position:unpack() )
   self:normal_rebound( separating_vector )
end

function Ball:react_on_platform_collision( another_shape, separating_vector )
   self.position = self.position + separating_vector
   self.collider_shape:moveTo( self.position:unpack() )
   self:normal_rebound( separating_vector )
end

Ball:normal_rebound function reverses the direction of the x or y speed component if the overlap over relevant axis is big enough (i.e. more than 0.5 pixel):

function Ball:normal_rebound( separating_vector )
   local big_enough_overlap = 0.5
   local vx, vy = self.speed:unpack()
   local dx, dy = separating_vector.x, separating_vector.y
   local new_vx, new_vy
   if math.abs( dx ) > big_enough_overlap then
          new_vx = -vx
   else
          new_vx = vx
   end
   if math.abs( dy ) > big_enough_overlap then
          new_vy = -vy
   else
          new_vy = vy
   end
   self.speed = vector( new_vx, new_vy )
end

It is also better to shift the ball's initial position in such a way, that it doesn't overlap with the bricks on the start of the level. This can be done by providing the position in the call to the constructor, or just by changing the default position value:

function Ball:new( o )
   .....
   o.position = o.position or vector( 500, 500 )
   .....
end

One last thing that is left for now, is to make bricks disappear on the collision with the ball. To annihilate a brick, we have to call the brick's destructor and then manually erase it from the bricks_container table.

The mechanism is following: we add a to_destroy flag into the Brick class, which we raise on collision with the ball:

function Brick:new( o )
   .....
   o.collider_shape.game_object = o
   o.to_destroy = o.to_destroy or false  --(*1)
   return o
end


function Brick:react_on_ball_collision(	another_shape, separating_vector )
   local big_enough_overlap = 0.5
   local dx, dy = separating_vector.x, separating_vector.y
   if ( math.abs( dx ) > big_enough_overlap ) or
      ( math.abs( dy ) > big_enough_overlap ) then
         self.to_destroy = true  --(*2)
   end
end

(*1): to_destroy flag in the constructor.
(*2): to_destroy set to true if collision happens.

Then, for each brick we check these flags in the bricks_container update() function. If the to_destroy flag is true, we call brick's destructor and then remove it from the bricks_container.bricks table.

function BricksContainer:update( dt )
   for i, brick_row in pairs( self.bricks ) do
      for j, brick in pairs( brick_row ) do
         brick:update( dt )
         if brick.to_destroy then
            brick:destroy()           --(*1)
            self.bricks[i][j] = nil   --(*2)
         end
      end
   end   
end

(*1): Brick's destructor call.
(*2): Brick is removed from the bricks_container.bricks table.

For now, there is no need for nontrivial destructor for the brick -- the garbage collector should handle it automatically. However, to facilitate the process, I believe (I'm not sure, whether it will have any effect or not) it is better to remove cyclic references and to delete the brick's collider_shape from the HC collider instance.

function Brick:destroy()
   self.collider_shape.game_object = nil
   self.collider:remove( self.collider_shape )
end

    Home
    Acknowledgements
    Todo

Chapter 1: Prototype

  1. The Ball, The Brick, The Platform
  2. Game Objects as Lua Tables
  3. Bricks and Walls
  4. Detecting Collisions
  5. Resolving Collisions
  6. Levels

    Appendix A: Storing Levels as Strings
    Appendix B: Optimized Collision Detection (draft)

Chapter 2: General Code Structure

  1. Splitting Code into Several Files
  2. Loading Levels from Files
  3. Straightforward Gamestates
  4. Advanced Gamestates
  5. Basic Tiles
  6. Different Brick Types
  7. Basic Sound
  8. Game Over

    Appendix C: Stricter Modules (draft)
    Appendix D-1: Intro to Classes (draft)
    Appendix D-2: Chapter 2 Using Classes.

Chapter 3 (deprecated): Details

  1. Improved Ball Rebounds
  2. Ball Launch From Platform (Two Objects Moving Together)
  3. Mouse Controls
  4. Spawning Bonuses
  5. Bonus Effects
  6. Glue Bonus
  7. Add New Ball Bonus
  8. Life and Next Level Bonuses
  9. Random Bonuses
  10. Menu Buttons
  11. Wall Tiles
  12. Side Panel
  13. Score
  14. Fonts
  15. More Sounds
  16. Final Screen
  17. Packaging

    Appendix D: GUI Layouts
    Appendix E: Love-release and Love.js

Beyond Programming:

  1. Game Design
  2. Minimal Marketing (draft)
  3. Finding a Team (draft)

Archive

Clone this wiki locally