Part 0: Common Ground^ top

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:

Prerequisites

You should already know how to program games with Lua and LÖVE. If not, this, this, this and also this, this and this and finally this will get you started.

Part 1: Hello, PONG!^ top

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

Holy Trinity and a Half

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.

Filling the Void

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

Rebound

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

Re-Rebound

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.

The Whole Picture

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

Room for Improvements

There is a lot of things you can do to not only make this game more fun but also practice using HardonCollider:

Part 2: Fly Me To The Moon^ top

This space intentionally left blank.

Part 3: Down To Earth^ top

This space intentionally left blank.