Love2d Platformer Tutorial: Part 3 - Creating and Loading Levels

Posted in Tutorials

This tutorial should be more fun than the last one. With our framework in place we can get down to the business of making a game. In this section I'll teach you how to use Tiled to create levels for Love games. We'll have to write a little bit of code to use them.

Important: In the process of making this tutorial I noticed some problems with the logic in part 2. We'll be correcting this, but not until Part 4. I considered going back and writing the code right the first time, but that would have left people already following this tutorial series in a lurch. I also thought it might be good to show you all how I debug code.

With that out of the way, let's make our first level! Here are our steps.

  1. Make the level using Tiled.
  2. Create a class layer to load the level.

1. Make the level!

For this tutorial, I have decided to use Open Pixel Project for our assets. It's a fantastic project with some really cool art and because they follow a set of rules, you can choose whichever set of tiles you want for your level. I'm going to choose cave for my level.

You are free to choose any tileset you want, but be sure to move the tiles you'll be using to the /assets/ folder of our project to avoid weird loading bugs.

Tiled is very easy to use, but there are a few gotchas so I'm going to give you a rundown of exactly what I did to create the level.

First, open Tiled and choose New Map... You'll get a screen that looks a bit like this:

For this level I'm going to make a relatively small map. This makes debugging player behavior easier since you can transverse the entirety of the level quickly. The important values here are that is is an Orthagonal map with a height of 20 and a Tile size of 32 by 32. When you save, create a new folder in /assets called /levels. I saved mine as level_1.

Second, load the tileset by clicking the little New Tileset icon in the bottom right of the screen. You should get a popup. Fill out the settings so it looks like this:

Third, we need to add a custom property to some tiles so that we can handle our collisions. Choose the tileset you've just imported and click the document with a wrench on it. This will bring up the tileset editor. In this editor, select every tile you want to be impassable and create a new Custom Property on the left side called "collidable." Set the type to "bool" and the value to true. For the tiles you can pass through, just leave them alone. For me, the final product looks like this:

Note that I didn't make those little bits above the rocks collidable. The player will be ontop of these giving just the slightest illusion of depth.

Fourth, paint your level! For now, try a couple long platforms towards the bottom of the level. We need to fix our player in part 4 before we get too complicated.

Fifth, export your level. Go up to the File menu, choose Export As, make sure the file type is .lua and save the exported file into /assets/levels.

2. Coding our base class.

At some point we'll want a lot of level in our game and since they'll all be the levels of a platformer we'll want a way to separate out common code -- such as loading our Tiled maps. Create an empty file in /gamestates called LevelBase.lua. We'll move whatever common code we can to this file. Let's revisit gameLevel1.lua. In that file we have a couple of entities, ground and player, plus our bump library. In this tutorial we'll be adding level loading and a camera. Entities will change but the Entity system will not. So our common code will include the Entity system, bump library (collision handling), level loading, and our camera. This frees up gameLevel# files to include only the logic specific to that particular level.

We'll be using Simple Tiled Implementation (or STI) by karai17. Add that to your libs folder by whichever method you prefer.

Now we're ready for to program our LevelBase. In that file put:

-- Each level will inherit from this class which itself inherits from Gamestate. -- This class is Gamestate but with function for loading up Tiled maps. local bump = require 'libs.bump.bump' local Gamestate = require 'libs.hump.gamestate' local Class = require 'libs.hump.class' local sti = require 'libs.sti.sti' -- New addition here local Entities = require 'entities.Entities' local camera = require 'libs.camera' -- New addition here local LevelBase = Class{ __includes = Gamestate, init = function(self, mapFile) self.map = sti(mapFile, { 'bump' }) self.world = bump.newWorld(32) self.map:resize(love.graphics.getWidth(), love.graphics.getHeight()) self.map:bump_init(self.world) Entities:enter() end; Entities = Entities; camera = camera }

In this new block we tell the engine that level base inherits from Gamestate then we define a function to be run on LevelBase initialization. This function takes the location of a map file and sets up the player's environment. Now for the breakdown by line number:

  1. self.map = sti(mapFile, { 'bump' }) - Use sti to open our mapfile. We then tell sti that we are making use of the bump plugin for our collision handling.
  2. self.world = bump.newWorld(32) - Just as before, we declare a world for our collisions to occur in.
  3. self.map:resize(love.graphics.getWidth(), love.graphics.getHeight()) - Resize the map to fill our screen.
  4. self.map:bump_init(self.world) - Initialize the bump library for our map.
  5. Entities:enter() - Create the entities system.
  6. Entities = Entities; - Makes Entities which we required above a class variable for easy access from anyone that inherits this LevelBase.

This will allow us to simplify gameLevel1.lua, but before we do that let's setup our camera. Camera's in Love are super simple. We actually have a class already included but I found that it overcomplicated the process and actually fought with our level loader. Instead we're going to grab the camera from this nova-fusion tutorial: Cameras in Love2d Part 1 I recommend you read that tutorial as well. Understanding this bit of code isn't strictly necessary but it's always a good idea.

Take the code from that tutorial (which I have pared down and copied below) and add it to a file in /libs named camera.lua.

camera = {} camera.x = 0 camera.y = 0 function camera:set() love.graphics.push() love.graphics.translate(-self.x, -self.y) end function camera:unset() love.graphics.pop() end function camera:move(dx, dy) self.x = self.x + (dx or 0) self.y = self.y + (dy or 0) end function camera:setPosition(x, y) self.x = x or self.x self.y = y or self.y end return camera

We "set" the camera prior to drawing our graphics. This performs a translate operation that ensures we're drawing things in the correct position relative to our viewport. We'll use the setPosition function to make sure our viewport is in the proper position relative to the player.

Returning to LevelBase.lua we'll add our common procedures. Right now that's just pausing, but you could use this same concept for informational popups or other actions that appear in multiple levels.

function LevelBase:keypressed(key) -- All levels will have a pause menu if Gamestate.current() ~= pause and key == 'p' then Gamestate.push(pause) end end

Finally, let's write a little code to move the camera to match the player position. This is just a tiny bit tricky, but it boils down into very little code. First, we determine the width of the level by multiplying the number of tiles in the width by the width of the tiles (that was a bit of a mouthful), then we get the width of the screen and half that to center our player. Then we check if we're close to the starting edge or the ending edge and use Math.min and Math.max to set the position of the camera to the closest edge or to half a screen from our player character. The whole block looks like this:

function LevelBase:positionCamera(player, camera) local mapWidth = self.map.width * self.map.tilewidth -- get width in pixels local halfScreen = love.graphics.getWidth() / 2 if player.x < (mapWidth - halfScreen) then -- use this value until we're approaching the end. boundX = math.max(0, player.x - halfScreen) -- lock camera at the left side of the screen. else boundX = math.min(player.x - halfScreen, mapWidth - love.graphics.getWidth()) -- lock camera at the right side of the screen end camera:setPosition(boundX, 0) end

To finish up this file add the return statement at the bottom.

return LevelBase

Returning to gameLevel1.lua we remove some redundant code and add the LevelBase dependency. There is nothing new in this file other than the mounting and positioning of the camera - a few simple function calls. The final gameLevel1 file looks like this:

-- Import our libraries. local Gamestate = require 'libs.hump.gamestate' local Class = require 'libs.hump.class' -- Grab our base class local LevelBase = require 'gamestates.LevelBase' -- Import the Entities we will build. local Player = require 'entities.player' local camera = require 'libs.camera' -- Declare a couple immportant variables player = nil local gameLevel1 = Class{ __includes = LevelBase } function gameLevel1:init() LevelBase.init(self, 'assets/levels/level_1.lua') end function gameLevel1:enter() player = Player(self.world, 32, 64) LevelBase.Entities:add(player) end function gameLevel1:update(dt) self.map:update(dt) -- remember, we inherited map from LevelBase LevelBase.Entities:update(dt) -- this executes the update function for each individual Entity LevelBase.positionCamera(self, player, camera) end function gameLevel1:draw() -- Attach the camera before drawing the entities camera:set() self.map:draw() -- Remember that we inherited map from LevelBase LevelBase.Entities:draw() -- this executes the draw function for each individual Entity camera:unset() -- Be sure to detach after running to avoid weirdness end -- All levels will have a pause menu function gameLevel1:keypressed(key) LevelBase:keypressed(key) end return gameLevel1

That wraps up part 3! When you run the game you should see something like this:

In part 4 we'll make a better, more responsive player!

In this series:

  1. Part 1 - The Basics
  2. Part 2 - Plumbing a Game
  3. Part 3 - Creating and Loading Levels
  4. Part 4 - A Better Player (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
  4. Part 4 - Making a Better Player