If you're new to programming, I highly suggest checking out my first tutorial Your First Love2d Game in 200 Lines. This tutorial will assume some programming knowledge. We'll start with moving that knowledge to game development in Love2d.
In part 1 we're going to make a small proof of concept. We'll build a little platformer with a player who can interact with platforms, move, and jump. In part 2 we'll abstract out that code and prepare to make our little protagonist into the big protagonist of a real game. We'll have plenty of time to talk about gamestates, building levels, animation, and more once our super simple platformer is done.
Let's start by making our basic Love engine functions:
function love.load()
end
function love.update(dt)
end
function love.keypressed(key)
if key == "escape" then
love.event.push("quit")
end
end
function love.draw(dt)
end
One of the first things I do when starting a new Love project is to add that love.keypressed function that listens for "escape." I don't have time to be exiting out in other ways. When our game is done we'll change that to be a pause button.
Next, let's think about our minimal platformer game. What is the least amount of work we can do to get a working demo? We call this the minimal viable product in the more professional development industry. You might be thinking, "well we need bosses and levels and score keeping and bad guys." Think simpler! A platformer needs a player character and platforms. That's it. Everything else, bosses and enemies, is just an expansion of that. We'll make a plan for dealing with all of that in the next part.
Let's go ahead and define a stub player above those functions.
-- Setup a player object to hold an image and attach a collision object
player = {
x = 16,
y = 16,
-- Here are some incidental storage areas
img = nil -- store the sprite we'll be drawing
}
So far, we've given the player a location, but we know from playing platformers that there are certain ways the player character interacts with the world. The player should be able to move backwards and forwards. Unlike my side-scrolling tutorial, the player doesn't move in set intervals. The player character needs to accelerate in a direction up to a given maximum and slow-down when the player stops giving the command. In other words, the player needs a velocity and friction with the ground. Let's store these values. Replace the player code block with:
player = {
x = 16,
y = 16,
-- The first set of values are for our rudimentary physics system
xVelocity = 0, -- current velocity on x, y axes
yVelocity = 0,
acc = 100, -- the acceleration of our player
maxSpeed = 600, -- the top speed
friction = 20, -- slow our player down - we could toggle this situationally to create icy or slick platforms
-- Here are some incidental storage areas
img = nil -- store the sprite we'll be drawing
}
We'll also need to jump. This is a more complex version of above. We'll set a jump acceleration as well as some maximums on how our character jumps. Finally, if we're jumping, we'll need gravity.
-- The finished player object
player = {
x = 16,
y = 16,
-- The first set of values are for our rudimentary physics system
xVelocity = 0, -- current velocity on x, y axes
yVelocity = 0,
acc = 100, -- the acceleration of our player
maxSpeed = 600, -- the top speed
friction = 20, -- slow our player down - we could toggle this situationally to create icy or slick platforms
gravity = 80, -- we will accelerate towards the bottom
-- These are values applying specifically to jumping
isJumping = false, -- are we in the process of jumping?
isGrounded = false, -- are we on the ground?
hasReachedMax = false, -- is this as high as we can go?
jumpAcc = 500, -- how fast do we accelerate towards the top
jumpMaxSpeed = 9.5, -- our speed limit while jumping
-- Here are some incidental storage areas
img = nil -- store the sprite we'll be drawing
}
Go ahead and finish our player out by adding player.img = love.graphics.newImage('assets/character_block.png')
to the love.load
function and love.graphics.draw(player.img, player.x, player.y)
to love.draw
.
Don't have a player character image? Here use mine:
If everything went as planned you should see a little rectangle on a black screen when you run the application.
The Player Character has a lot of variables to take in, but they'll make more sense as we use them. First, let's go ahead and write in our movement system. It's quite a bit more complex than the code in our scrollling shooter. We're rolling in some very rudimentary physics. The player will have a velocity and be acted on by gravity and friction.
Replace love.update
with this code block:
function love.update(dt)
player.x = player.x + player.xVelocity
player.y = player.y + player.yVelocity
-- Apply Friction
player.xVelocity = player.xVelocity * (1 - math.min(dt * player.friction, 1))
player.yVelocity = player.yVelocity * (1 - math.min(dt * player.friction, 1))
-- Apply gravity
player.yVelocity = player.yVelocity + player.gravity * dt
if love.keyboard.isDown("left", "a") and player.xVelocity > -player.maxSpeed then
player.xVelocity = player.xVelocity - player.acc * dt
elseif love.keyboard.isDown("right", "d") and player.xVelocity < player.maxSpeed then
player.xVelocity = player.xVelocity + player.acc * dt
end
-- The Jump code gets a lttle bit crazy. Bare with me.
if love.keyboard.isDown("up", "w") then
if -player.yVelocity < player.jumpMaxSpeed and not player.hasReachedMax then
player.yVelocity = player.yVelocity - player.jumpAcc * dt
elseif math.abs(player.yVelocity) > player.jumpMaxSpeed then
player.hasReachedMax = true
end
player.isGrounded = false -- we are no longer in contact with the ground
end
end
Let's walk through the code. There is a lot to see here so brace yourself. First, we move the player x, y position to their old position plus their velocity on the given axis.
player.x = player.x + player.xVelocity
player.y = player.y + player.yVelocity
It's odd that we're doing this first. Normally we'd process the input first then add the velocity results. However, when we write the code for collision handling we'll want collisions handled immediately to avoid shudders.
Next we add a small amount of decceleration and call it friction.
player.xVelocity = player.xVelocity * (1 - math.min(dt * player.friction, 1))
player.yVelocity = player.yVelocity * (1 - math.min(dt * player.friction, 1))
friction
was set in our player variables. All we are doing here is multiplying the current velocity by a number between 0 and 1. math.min
just chooses the smaller of dt * player.friction
and 1
. In the real-world the y axis doesn't have a whole lot of friction, but it ends up feeling better than turning the gravity down.
Gravity is just a constant downward acceleration. It will work the same way as our left / right movement code which looks like this:
if love.keyboard.isDown("left", "a") and player.xVelocity > -player.maxSpeed then
player.xVelocity = player.xVelocity - player.acc * dt
elseif love.keyboard.isDown("right", "d") and player.xVelocity < player.maxSpeed then
player.xVelocity = player.xVelocity + player.acc * dt
end
Pay attention to the signs here. For left we subtract and for right we add.
One of the fundamental elements of a platformer game is the player characters ability to interact with platforms. Obviously.
We have a few options here. We could use Love2d's built in Box2D physics engine. We could use a very simple rectangular collision check function like we did in the side scrolling tutorial. Or we could find a library mid-way inbetween.
Box2D feels like overkill. We don't need realistic physics. We need "game-y" physics. And a simple function isn't going to do enough when we have many collisions happening at once.
Instead, let's load up a library called bump.lua. You'll want to download the entire project then unzip it into /libs
. It should look like this:
We include the library by adding bump = require 'libs.bump.bump'
to the very top of the file. Next, we'll stub out a world and a couple of ground objects just below the require. The top of our file should now look like this:
world = nil -- storage place for bump
ground_0 = {}
ground_1 = {}
In bump our collidables are stored in a "world" which we create in our load event. Objects that have collisions are then inserted into the world. When we want to move an object we move it relative to the world we created so that bump can process our collisions for us.
We declare this world right at the top of love.load
. Our tiles will be 16 pixels wide and tall so we pass that into bump.newWorld which defaults to 64. After we load in our player object we'll add objects to our world so that our final love.load looks like this:
function love.load()
-- Setup bump
world = bump.newWorld(16) -- 16 is our tile size
-- Create our player.
player.img = love.graphics.newImage('assets/character_block.png')
world:add(player, player.x, player.y, player.img:getWidth(), player.img:getHeight())
-- Draw a level
world:add(ground_0, 120, 360, 640, 16)
world:add(ground_1, 0, 448, 640, 32)
end
Let's add our ground to love.draw so we can see it. Our final love.draw function will look like this:
function love.draw(dt)
love.graphics.draw(player.img, player.x, player.y)
love.graphics.rectangle('fill', world:getRect(ground_0))
love.graphics.rectangle('fill', world:getRect(ground_1))
end
We only need a couple more lines to start processing collisions. Our process will be to calculate where we want to go, a goalX and goalY, then attempt to move there. bump.lua will process our collisions and return the actual x and y of what was moved.
Let's change:
player.x = player.x + player.xVelocity
player.y = player.y + player.yVelocity
to
local goalX = player.x + player.xVelocity
local goalY = player.y + player.yVelocity
Then add our movement to the player object. world:move
returns the actual x and y coordinates after stopping our moving objects and a list of collisions that occurred.
player.x, player.y, collisions = world:move(player, goalX, goalY)
If we run the code now we'll see our little player character fall from the sky and stop when it hits the white platform. It should look a little like this:

This is actually complete and ready to go, but we will eventually need some collision filtering. For instance, we want our character to pass through the bottom of platforms but get stopped by landing on them. So we'll add a filter that looks like this:
player.filter = function(item, other)
local x, y, w, h = world:getRect(other)
local px, py, pw, ph = world:getRect(item)
local playerBottom = py + ph
local otherBottom = y + h
if playerBottom <= y then
return 'slide'
end
end
This function takes the item colliding, the player in this case, and what it is colliding with. From there we use a bump.lua world function world:getRect(item)
to return the coordinates of the object at the time of collision. If the bottom of our player is above (less than) the top of the platform we are colliding with the platform from above. We return 'slide' as the type of collision we'd like to see. You can read about collision types in bump.lua here. If we aren't above the platform we return an implicit nil value that means no collision is being processed.
Next we run our world:move
passing in player.filter as our last argument. player.x, player.y, collisions, len = world:move(player, goalX, goalY, player.filter)
Finally, we'll loop through the returned collisions and react appropriately:
for i, coll in ipairs(collisions) do
if coll.touch.y > goalY then
player.hasReachedMax = true
player.isGrounded = false
elseif coll.normal.y < 0 then
player.hasReachedMax = false
player.isGrounded = true
end
end
All we're doing is allowing the player to jump again once the character touches the ground.
That's all there is for this bit. By now main.lua
is looking a little cluttered. In part two we'll do some behind the scenes plumbing to make our game development quicker and cleaner.
In this series:
- Part 1 - The Basics
- Part 2 - Plumbing a Game
- Part 3 - Creating and Loading Levels
- Part 4 - A Better Player (Coming Soon!)