Love2d Platformer Tutorial: Part 2 - Plumbing A Game

Posted in Tutorials

The second part of this tutorial is going to be pretty heavy on general programming and a bit light on game making. If you're pretty familiar with OOP principles you'll probably just want to skim this tutorial and move onto part three.

This tutorial will seem a bit obtuse, but it's worth it to know. We're going to take what we did in the previous tutorial, add a few libraries, and split our code into several files.

Let's start by grabbing the libraries we need. I like to use git submodules to manage my dependencies, but downloading the source and adding it to your project manually will also work. We're going to use vlrd's fantasticHUMP library for classes and gamestates. We'll eventually use it for our camera as well. We'll use bump.lua again for our collisions.

If you're doing things manually, go to the links above, download the files and unzip them into the libs directory. If you're using git for managing things:

git init #if you haven't already.
git submodule add git@github.com:vrld/hump libs/hump
git submodule add git@github.com:kikito/bump.lua libs/bump

The new structure of our game will have a simple main.lua that sets up our gamestates, an entities folder that contains every object in the game that is drawn or updated, and a gamestate folder that contains our menus, levels, etc. It will look something like this:

root
- entities
- gamestates
- libs
 - hump
 - bump

Let's talk about Object Oriented Programming -- OOP for short. It's a programming paradigm that says code should be based around objects and data rather than actions. So far we've been discussing our game in terms of action. We have a load action, draw action, update action. Now we're going to transition to think of our game in terms of what is performing an action. So from now on we can say, "player draws x" and "enemy draws x" instead of "draw x and y."

Objects also give us the ability to share traits in what programming textbooks call inheritance and polymorphism. The idea is that the more code you can reuse the less code you need to write. So rather than defining a dog and a cat, you can first define a mammal and have dog and cat inherit from mammal. We can also inherit from multiple sources. So we could have a bit of code describing traits of a house pet that we can bring in.

Now for the catch, Lua is not an object oriented language. However, we can expand Lua with the features we need. The HUMP library adds a class system -- roughly speaking.

Create a file in the entities folder called "Entity.lua" and add the following code:

-- Represents a single drawable object local Class = require 'libs.hump.class' local Entity = Class{} function Entity:init(world, x, y, w, h) self.world = world self.x = x self.y = y self.w = w self.h = h end function Entity:getRect() return self.x, self.y, self.w, self.h end function Entity:draw() -- Do nothing by default, but we still have to have something to call end function Entity:update(dt) -- Do nothing by default, but we still have to have something to call end return Entity

That's the complete code for Entity.lua for now. We pull in Class from the HUMP library and use that to create Entity. Everything in our game is going to be an entity - from the ground to the background to our player character. If it can be drawn on the screen or needs to update per frame, it's an entity. Most of these objects only share a few properties - the world they inhabit for physics (potentially nil), an x and y coordinate, and a width and height. As the game expands we'll make note of other variables all Entities need and move the values into this file.

Next we have a simple getRect function. This is just a time saver for down the road. We'll need the shape and size of the object for use in our collision detector and this lets us fire it all back at once. It's just for convenience.

Finally, we have empty functions from draw and update. Each Entity will have both of these called from the gamestate they're a part of. Even if the functions do nothing, ground for instance does not have an update, it still needs a placeholder to avoid errors.

Let's use our new entity system to create our first entity. We'll start simple. Create a new file in the entities folder named ground.lua. The code is almost exactly the same as in Part 1 but we've abstracted a layer away.

local Class = require 'libs.hump.class' local Entity = require 'entities.Entity' local Ground = Class{ __includes = Entity -- Ground class inherits our Entity class } function Ground:init(world, x, y, w, h) Entity.init(self, world, x, y, w, h) self.world:add(self, self:getRect()) end function Ground:draw() love.graphics.rectangle('fill', self:getRect()) end return Ground

We'll do exactly the same thing for the player. Create a new file called player.lua in the entities folder.

local Class = require 'libs.hump.class' local Entity = require 'entities.Entity' local player = Class{ __includes = Entity -- Player class inherits our Entity class } function player:init(world, x, y) self.img = love.graphics.newImage('/assets/character_block.png') Entity.init(self, world, x, y, self.img:getWidth(), self.img:getHeight()) -- Add our unique player values self.xVelocity = 0 -- current velocity on x, y axes self.yVelocity = 0 self.acc = 100 -- the acceleration of our player self.maxSpeed = 600 -- the top speed self.friction = 20 -- slow our player down - we could toggle this situationally to create icy or slick platforms self.gravity = 80 -- we will accelerate towards the bottom -- These are values applying specifically to jumping self.isJumping = false -- are we in the process of jumping? self.isGrounded = false -- are we on the ground? self.hasReachedMax = false -- is this as high as we can go? self.jumpAcc = 500 -- how fast do we accelerate towards the top self.jumpMaxSpeed = 11 -- our speed limit while jumping self.world:add(self, self:getRect()) end function player:collisionFilter(other) local x, y, w, h = self.world:getRect(other) local playerBottom = self.y + self.h local otherBottom = y + h if playerBottom <= y then -- bottom of player collides with top of platform. return 'slide' end end function player:update(dt) local prevX, prevY = self.x, self.y -- Apply Friction self.xVelocity = self.xVelocity * (1 - math.min(dt * self.friction, 1)) self.yVelocity = self.yVelocity * (1 - math.min(dt * self.friction, 1)) -- Apply gravity self.yVelocity = self.yVelocity + self.gravity * dt if love.keyboard.isDown("left", "a") and self.xVelocity > -self.maxSpeed then self.xVelocity = self.xVelocity - self.acc * dt elseif love.keyboard.isDown("right", "d") and self.xVelocity < self.maxSpeed then self.xVelocity = self.xVelocity + self.acc * dt end -- The Jump code gets a lttle bit crazy. Bare with me. if love.keyboard.isDown("up", "w") then if -self.yVelocity < self.jumpMaxSpeed and not self.hasReachedMax then self.yVelocity = self.yVelocity - self.jumpAcc * dt elseif math.abs(self.yVelocity) > self.jumpMaxSpeed then self.hasReachedMax = true end self.isGrounded = false -- we are no longer in contact with the ground end -- these store the location the player will arrive at should local goalX = self.x + self.xVelocity local goalY = self.y + self.yVelocity -- Move the player while testing for collisions self.x, self.y, collisions, len = self.world:move(self, goalX, goalY, self.collisionFilter) -- Loop through those collisions to see if anything important is happening for i, coll in ipairs(collisions) do if coll.touch.y > goalY then -- We touched below (remember that higher locations have lower y values) our intended target. self.hasReachedMax = true -- this scenario does not occur in this demo self.isGrounded = false elseif coll.normal.y < 0 then self.hasReachedMax = false self.isGrounded = true end end end function player:draw() love.graphics.draw(self.img, self.x, self.y) end return player

Hopefully you're starting to see the advantage of this approach. If I need to make changes to the player I don't have to scroll through main.lua, I can just pop open player.lua and find what I need there. Big games become manageable. All we're missing now is the glue to hold it altogether.

For that we'll use Gamestate from the HUMP library. Each Gamestate is like a separate main.lua file. It has a load event, a draw event, and an update event. A Gamestate could represent a menu, a level, or a pause screen. We'll handle all three, but we'll build out the menu later.

The Gamestate documentation has a few examples of putting together your game with this pattern. Our main.lua is going to be heavily simplified in favor of moving code into the Gamestate objects. Here is the final code for main.lua.

-- Pull in Gamestate from the HUMP library Gamestate = require 'libs.hump.gamestate' -- Pull in each of our game states local mainMenu = require 'gamestates.mainmenu' local gameLevel1 = require 'gamestates.gameLevel1' local pause = require 'gamestates.pause' function love.load() Gamestate.registerEvents() Gamestate.switch(gameLevel1) end function love.keypressed(key) if key == "escape" then love.event.push("quit") end end

Quick breakdown: We load up the libraries we'll use, in this case just Gamestate from HUMP. Then we'll pull in three gamestate libraries. (We'll create these next.) Then we replace our love.load() function with two lines. You'll note that this file no longer has draw or update functions. Instead, you'll find those functions in the Gamestate files. What we do have is a keypressed function. This is just because I like to be able to get out of the game quickly while building. Eventually we'll tie this to a debug variable.

Before we build out our Gamestates we need to identify the code that will be common between them and we'll abstract that code out so that we don't repeat ourselves. Each Gamestate will be managing a list of entities and calling the individual draws and updates of those entities. I don't want to write those loops into each Gamestate so let's create a new file in our entities folder named -- appropriately -- Entities.lua.

-- Represents a collection of drawable entities. Each gamestate holds one of these. local Entities = { active = true, world = nil, entityList = {} } function Entities:enter(world) self:clear() self.world = world end function Entities:add(entity) table.insert(self.entityList, entity) end function Entities:addMany(entities) for k, entity in pairs(entities) do table.insert(self.entityList, entity) end end function Entities:remove(entity) for i, e in ipairs(self.entityList) do if e == entity then table.remove(self.entityList, i) return end end end function Entities:removeAt(index) table.remove(self.entityList, index) end function Entities:clear() self.world = nil self.entityList = {} end function Entities:draw() for i, e in ipairs(self.entityList) do e:draw(i) end end function Entities:update(dt) for i, e in ipairs(self.entityList) do e:update(dt, i) end end return Entities

This file wraps entityList -- an array of entities -- and provides us with handy methods for manipulation.

With DRY (don't repeat yourself) principles followed let's create our gameLevel1 Gamestate. Create a folder in the root (if you haven't already) named gamestates and add a file called gameLevel1.lua. Obviously we're naming it like this because we plan on having many levels.

-- Import our libraries. bump = require 'libs.bump.bump' Gamestate = require 'libs.hump.gamestate' -- Import our Entity system. local Entities = require 'entities.Entities' local Entity = require 'entities.Entity' -- Create our Gamestate local gameLevel1 = Gamestate.new() -- Import the Entities we build. local Player = require 'entities.player' local Ground = require 'entities.ground' -- Declare a couple immportant variables player = nil world = nil function gameLevel1:enter() -- Game Levels do need collisions. world = bump.newWorld(16) -- Create a world for bump to function in. -- Initialize our Entity System Entities:enter() player = Player(world, 16, 16) ground_0 = Ground(world, 120, 360, 640, 16) ground_1 = Ground(world, 0, 448, 640, 16) -- Add instances of our entities to the Entity List Entities:addMany({player, ground_0, ground_1}) end function gameLevel1:update(dt) Entities:update(dt) -- this executes the update function for each individual Entity end function gameLevel1:draw() Entities:draw() -- this executes the draw function for each individual Entity end return gameLevel1

The code here is pretty self-explanatory. Setup our physics, add our entities, update, draw. You might also notice that it looks a lot like main.lua used to look. Each Gamestate is a different main, a different set of events.

I'm also going to create a mainMenu.lua Gamestate, but I'm leaving it blank for now. We'll fill it out in a future tutorial.

local mainMenu = {} return mainMenu

Finally, we'll create one more Gamestate for a bit of magic. This is lifted directly from the HUMP libraries documentation, but I'll try to explain it here as well. This is our "pause" Gamestate. The HUMP library creates a list of Gamestates that we can manipulate by pushing (inserting a state) and popping (removing a state at the end). This means that as we switch Gamestates, we can still take advantage of code in previous Gamestates. In this case, we have a level Gamestate that we still want to draw but we don't want to update while paused.

pause = Gamestate.new() function pause:enter(from) self.from = from -- record previous state end function pause:draw() local w, h = love.graphics.getWidth(), love.graphics.getHeight() -- draw previous screen self.from:draw() -- overlay with pause message love.graphics.setColor(0,0,0, 100) love.graphics.rectangle('fill', 0,0, w, h) love.graphics.setColor(255,255,255) love.graphics.printf('PAUSE', 0, h/2, w, 'center') end function pause:keypressed(key) if key == 'p' then return Gamestate.pop() -- return to previous state end end return pause

When we enter this Gamestate (you'll remember the code to enter this state is in our main.lua) we record the previous state in self.from. Later we call the draw function of our previous state then overlay a black film over it with the word "PAUSE." We have no update function because nothing is moving, but we do need to catch the player unpausing so we have a keypressed event that looks for the letter "p" and returns us to the previous state.

That's it for Part 2. It's a bit painful since our finally product looks exactly the same as it did at the end of Part 1, but we've layed a solid foundation for all the features a real platforming game needs.

In Part 3 we'll create and load levels in with Tiled.

In this series:

  1. Part 1 - The Basics
  2. Part 2 - Plumbing a Game
  3. Part 3 - Creating and Loading Levels (Coming Soon!)

Source Code

In this series:

  1. Part 1 - The Basics
  2. Part 2 - Plumbing a Game
  3. Part 3 - Creating and Loading Levels (Coming Soon!)