-
Notifications
You must be signed in to change notification settings - Fork 17
Arx 06 Resolving Collisions
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
Feedback is crucial to improve the tutorial!
Let me know if you have any questions, critique, suggestions or just any other ideas.
Chapter 1: Prototype
- The Ball, The Brick, The Platform
- Game Objects as Lua Tables
- Bricks and Walls
- Detecting Collisions
- Resolving Collisions
- Levels
Appendix A: Storing Levels as Strings
Appendix B: Optimized Collision Detection (draft)
Chapter 2: General Code Structure
- Splitting Code into Several Files
- Loading Levels from Files
- Straightforward Gamestates
- Advanced Gamestates
- Basic Tiles
- Different Brick Types
- Basic Sound
- 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
- Improved Ball Rebounds
- Ball Launch From Platform (Two Objects Moving Together)
- Mouse Controls
- Spawning Bonuses
- Bonus Effects
- Glue Bonus
- Add New Ball Bonus
- Life and Next Level Bonuses
- Random Bonuses
- Menu Buttons
- Wall Tiles
- Side Panel
- Score
- Fonts
- More Sounds
- Final Screen
- Packaging
Appendix D: GUI Layouts
Appendix E: Love-release and Love.js
Beyond Programming: