I suck at writing Introductions, so let's dive right in:
This tutorial will teach you how to use HardonCollider. It is divided into three parts:
In our first steps with HardonCollider we will write (yet another) clone of Pong. Using a collision detection library for this task is probably a little over the top, but Pong serves well to introduce some of the libraries core concepts. Let's begin:
Create a new folder called hardonpong
. Download
the latest version
of HardonCollider and put it into that directory. Create an empty file named
main.lua
. The directory content should look like this before we go on:
hardonpong |-- hardoncollider | |-- class.lua | |-- init.lua | |-- polygon.lua | |-- shapes.lua | |-- spatialhash.lua | `-- vector-light.lua `-- main.lua
Fill your main.lua with the following:
HC = require 'hardoncollider'
function on_collide(dt, shape_a, shape_b)
end
function love.load()
Collider = HC(100, on_collide)
end
function love.update(dt)
Collider:update(dt)
end
function love.draw()
end
This is the basic code skeleton for using HardonCollider.
HC = require 'hardoncollider'
loads the module and puts it into the variable
HC
.
The next lines defines an empty function called on_collide
in which we will handle what should happen when two shapes collide. We will cover
the different parameters later.
Collider = HC(100, on_collide)
creates a new collision detection instance, setting on_collide
as callback function.
The first argument defines a cell size which is used internally to speed things
up. You don't have to worry about that at the moment.
Finally, Collider:update(dt)
will check for collisions and run the callback functions if needed.
Pong wouldn't be Pong without two paddles and a ball. In fact, it wouldn't be anything. So let's add them by creating two rectangles and a circle shape:
function love.load()
Collider = HC(100, on_collide)
ball = Collider:addCircle(400,300, 10)
paddleLeft = Collider:addRectangle(10,250, 20,100)
paddleRight = Collider:addRectangle(770,250, 20,100)
ball.velocity = {x = -100, y = 0}
end
We now have defined three shapes that are subject to collision detection. Because
shapes are just Lua tables with some functions
and properties in them, we can add anythin we like to them. We exploit this by adding
a table named velocity
to the ball, which will be used to move the it.
To see what we've created, we need to draw the shapes. Mostly for debugging purposes, shapes have functions for drawing themselves. We, however, will not use these functions for debugging, but as our only means to show what's going on:
function love.draw()
-- we can also use 'line' instead of 'fill'
ball:draw('fill', 16) -- approximated circle with 16 edges
paddleLeft:draw('fill')
paddleRight:draw('fill')
end
To put things in motion (haha, get it?), we change the position of the ball according to it's velocity:
function love.update(dt)
ball:move(ball.velocity.x * dt, ball.velocity.y * dt)
-- check for collisions
Collider:update(dt)
end
If you run what we have done so far, you will see the ball moving very, very slowly from the screen
center to the left paddle. And then passing it. That's not Pong! The ball needs to
bounce off the paddle. This is where on_collide(dt, shape_a, shape_b)
comes in handy.
on_collide()
will be called whenever two shapes collide (actually only
when they first collide, more about that in Part 3). It's first argument dt
is the same as to love.update()
: the time passed since
the last call of love.update()
. The other two parameters
are the shapes that are involved in the collision.
If the ball collides with a paddle, it should bounce off depending where on the paddle it hit it. We can get the current (center-)position of a shape by using shape:center(). Thus, by getting the difference between the ball's and the paddle's center's y-component, we can determine in what direction the ball should bounce off:
function on_collide(dt, shape_a, shape_b)
-- determine which shape is the ball and which the paddle
local paddle
if shape_a == ball then
paddle = shape_b
else
paddle = shape_a
end
-- reflect the ball on the paddle
local px,py = paddle:center()
local bx,by = ball:center()
ball.velocity.x = -ball.velocity.x
ball.velocity.y = by - py
-- keep the ball at the same speed as before
local len = math.sqrt(ball.velocity.x^2 + ball.velocity.y^2)
ball.velocity.x = ball.velocity.x / len * 100
ball.velocity.y = ball.velocity.y / len * 100
end
Although being a hypnotic, this has little to no entertainment value: We cannot move the paddles. Let's fix that:
function love.update(dt)
ball:move(ball.velocity.x * dt, ball.velocity.y * dt)
-- left player movement
if love.keyboard.isDown('w') then
paddleLeft:move(0, -100 * dt)
elseif love.keyboard.isDown('s') then
paddleLeft:move(0, 100 * dt)
end
-- right player movement
if love.keyboard.isDown('up') then
paddleRight:move(0, -100 * dt)
elseif love.keyboard.isDown('down') then
paddleRight:move(0, 100 * dt)
end
-- check for collisions
Collider:update(dt)
end
Hooray, we've done it! Or have we? As it turns out, we have missed two very important parts: The ball won't bounce off the upper and lower screen border and it's not reset to the screen center once a player scores a goal. We can do both by introducing more shapes defining special zones. These shapes won't be drawn - they are off screen anyway.
function love.load()
-- ... as before ...
borderTop = Collider:addRectangle(0,-100, 800,100)
borderBottom = Collider:addRectangle(0,600, 800,100)
goalLeft = Collider:addRectangle(-100,0, 100,600)
goalRight = Collider:addRectangle(800,0, 100,600)
end
We also need to define what happens when the ball touches a border or a goal. Because there are now more things that can collide, the collision callback has to change:
function on_collide(dt, shape_a, shape_b)
-- determine which shape is the ball and which is not
local other
if shape_a == ball then
other = shape_b
elseif shape_b == ball then
other = shape_a
else -- neither shape is the ball. exit
return
end
-- reset on goal
if other == goalLeft then
ball.velocity = {x = 100, y = 0}
ball:moveTo(400,300)
elseif other == goalRight then
ball.velocity = {x = -100, y = 0}
ball:moveTo(400,300)
elseif other == borderTop or other == borderBottom then
-- bounce off top and bottom
ball.velocity.y = -ball.velocity.y
else
-- bounce of paddle
local px,py = other:center()
local bx,by = ball:center()
local dy = by - py
ball.velocity.x = -ball.velocity.x
ball.velocity.y = dy
-- keep the ball at the same speed
local len = math.sqrt(ball.velocity.x^2 + ball.velocity.y^2)
ball.velocity.x = ball.velocity.x / len * 100
ball.velocity.y = ball.velocity.y / len * 100
end
end
Because we want to react on collision only when the ball is involved, we return early if neither one of the shapes is the ball - can you tell what these shapes are in this case? (answer). The rest is relatively straightforward: If the ball hits a goal, it is reset to the screen center and will move to the scoring player's paddle. If the ball hit's either the top or bottom screen edge, it's y-direction is inverted. If it hit's one of the paddles, it is bounced off as before.
And there you have it: Your very first and very feature lacking Pong-clone using HardonCollider:
HC = require 'hardoncollider'
function on_collide(dt, shape_a, shape_b)
-- determine which shape is the ball and which is not
local other
if shape_a == ball then
other = shape_b
elseif shape_b == ball then
other = shape_a
else -- no shape is the ball. exit
return
end
-- reset on goal
if other == goalLeft then
ball.velocity = {x = 100, y = 0}
ball:moveTo(400,300)
elseif other == goalRight then
ball.velocity = {x = -100, y = 0}
ball:moveTo(400,300)
elseif other == borderTop or other == borderBottom then
-- bounce off top and bottom
ball.velocity.y = -ball.velocity.y
else
-- bounce of paddle
local px,py = other:center()
local bx,by = ball:center()
local dy = by - py
ball.velocity.x = -ball.velocity.x
ball.velocity.y = dy
-- keep the ball at the same speed
local len = math.sqrt(ball.velocity.x^2 + ball.velocity.y^2)
ball.velocity.x = ball.velocity.x / len * 100
ball.velocity.y = ball.velocity.y / len * 100
end
end
function love.load()
Collider = HC(100, on_collide)
ball = Collider:addCircle(400,300, 10)
leftPaddle = Collider:addRectangle(10,250, 20,100)
rightPaddle = Collider:addRectangle(770,250, 20,100)
ball.velocity = {x = -100, y = 0}
borderTop = Collider:addRectangle(0,-100, 800,100)
borderBottom = Collider:addRectangle(0,600, 800,100)
goalLeft = Collider:addRectangle(-100,0, 100,600)
goalRight = Collider:addRectangle(800,0, 100,600)
end
function love.update(dt)
ball:move(ball.velocity.x * dt, ball.velocity.y * dt)
-- left player movement
if love.keyboard.isDown('w') then
leftPaddle:move(0, -100 * dt)
elseif love.keyboard.isDown('s') then
leftPaddle:move(0, 100 * dt)
end
-- right player movement
if love.keyboard.isDown('up') then
rightPaddle:move(0, -100 * dt)
elseif love.keyboard.isDown('down') then
rightPaddle:move(0, 100 * dt)
end
Collider:update(dt)
end
function love.draw()
ball:draw('fill', 16)
leftPaddle:draw('fill')
rightPaddle:draw('fill')
end
There is a lot of things you can do to not only make this game more fun but also practice using HardonCollider:
paddleLeft:center()
and
paddleRight:center()
).