-
Notifications
You must be signed in to change notification settings - Fork 17
Arx 05 Detecting Collisions
Now it is time to deal with collisions. A collision happens when two objects overlap. First we need to detect an overlap, then to react on it (i.e. resolve collision). Collisions are the part of the game, where a good piece of it's logic resigns. I'm going to start from some basic things and deal with collision detection first.
It is possible to code an overlap-checking from scratch.
However, I'm going to use an external library, called HardonCollider (HC).
Unlike love.physics
module, HC
only serves to detect collisions, but does not resolve them.
The resolution of the collisions will be done purely manually.
So, how does HC work? It maintains it's own world, so to say, populated by different geometrical primitives, called shapes. Those shapes are added to the world manually, and each time you add one, you receive a reference to this shape. Then you can query this HC world by this shape-reference and it will tell you, what are the other shapes, that overlap with the one you specified. Here is a slightly modified code from the official example:
HC = require 'HC' -- (*1)
.....
function love.load()
--(*2)
rect = HC.rectangle(200,400,400,20)
circ = HC.circle(400,300,20)
.....
end
function love.update(dt)
-- (*3)
circ:moveTo( love.mouse.getPosition() )
rect:rotate(dt)
-- (*4)
for another_shape, delta in pairs( HC.collisions( circ ) ) do
local toprint = string.format("Colliding. Separating vector = (%s,%s)",
delta.x, delta.y)
end
.....
end
(*1) - first, it is necessary to load this library.
(*2) - rectangle and circle are added to the HC 'world' to check collisions between them.
(*3) - both shapes positions are updated: circ is moved under the mouse coursor, and rect is rotated.
(*4) - we query HC on which shapes collide with circ. For each collision, the query returns two things: the first one is another shape, that circ collides with; the second one is separation vector delta. The separating vector points outside of the circ and we need to displace another shape by this vector to make sure that it will no longer overlap with the circ.
How to use this library in our project?
First we need to create an HC world and add there all our objects.
Obviously, creation should be done in love.load()
( don't forget to download the library and require it first. I've renamed the folder simply to HC
).
local Platform = require "Platform"
local Ball = require "Ball"
local BricksContainer = require "BricksContainer"
local WallsContainer = require "WallsContainer"
local HC = require "HC"
function love.load()
collider = HC.new()
.....
end
After the creation, it is necessary to pass the collider
instance to the constructors of the other objects, so we can later add appropriate shapes into it:
function love.load()
collider = HC.new()
ball = Ball:new( { collider = collider } )
platform = Platform:new( { collider = collider } )
bricks_container = BricksContainer:new( { collider = collider } )
walls_container = WallsContainer:new( { collider = collider } )
end
Now at each constructor we need to add the appropriate shape to the collider
.
Let's start from the Ball.
function Ball:new( o )
.....
o.speed = o.speed or vector( 300, 300 )
o.collider = o.collider or {} --(*1)
o.collider_shape = o.collider:circle( o.position.x, --(*2)
o.position.y,
o.radius )
.....
return o
end
(*1): First, inside each ball object we are going to store a reference to the collider
object.
(*2): When each new ball object is constructed, we are going to add an appropriate shape to the collider
.
We need to keep the ball.position
and the position of the ball.collider_shape
in sync.
Therefore, during each update we are going to call a moveTo
method of the ball.collider_shape
to make it coincide with the ball.position
.
function Ball:update( dt )
self.position = self.position + self.speed * dt
self.collider_shape:moveTo( self.position:unpack() )
end
This might seem like an unnecessary complication to have to duplicating variables, however for now it is better to leave it that way.
HC
also has a number of debugging methods that allow to draw each shape added to the collider
.
It is good to use it for now to check that everything works as expected.
I'm going to draw the shape of the ball in the transparent green.
function Ball:draw()
.....
local r, g, b, a = love.graphics.getColor( )
love.graphics.setColor( 0, 255, 0, 100 )
self.collider_shape:draw( 'fill' )
love.graphics.setColor( r, g, b, a )
end
It is necessary to make the same changes in the Wall, Brick and Platform classes.
They are similar in nature, so I won't stop on them.
Some care should be taken regarding rectangle:moveTo
.
It positions the center of the rectangle shape to the provided coordinates, so a shift is necessary.
function Platform:update( dt )
.....
self.collider_shape:moveTo( self.position.x + self.width / 2,
self.position.y + self.height / 2 )
end
Walls and bricks do not move, so there is no need to call moveTo
method for their collider_shape
.
Still, it is useful to draw appropriate shapes.
function Brick:draw()
love.graphics.rectangle( 'line',
self.position.x,
self.position.y,
self.width,
self.height )
local r, g, b, a = love.graphics.getColor( )
love.graphics.setColor( 255, 0, 0, 100 )
self.collider_shape:draw( 'fill' )
love.graphics.setColor( r, g, b, a )
end
We do not construct bricks and walls directly - this is done by bricks_container
and walls_container
.
We have to make sure that reference to collider
is passed to bricks and walls.
function BricksContainer:new( o )
.....
o.name = o.name or "bricks_container"
o.bricks = o.bricks or {}
o.collider = o.collider or {}
.....
local new_brick = Brick:new{
width = o.brick_width,
height = o.brick_height,
position = new_brick_position,
collider = o.collider
}
new_row[ col ] = new_brick
.....
return o
end
A final touch for this chapter is to display some information when actual collision happens.
This is done by resolve_collisions
function, placed in love.update
.
function love.update( dt )
.....
resolve_collisions( dt )
end
For now, it's definition is simple.
As suggested by the HC
example, it prints a displacement vector when the ball collides with something
function resolve_collisions( dt )
for another_shape, delta in pairs( collider:collisions( ball.collider_shape ) ) do
local toprint = string.format("Ball is colliding. Separating vector = (%s,%s)",
delta.x, delta.y)
print( toprint )
end
end
For now we do not know what is ball colliding with exactly - a brick, a wall or the platform. Obviously, we need to know this information to resolve collisions properly. This is done in the next chapter.
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: