Tutorial: Creating a platformer with Löve

In this tutorial I want to show how easy it is to create a small 2D sideview platformer with the use of Löve, Tiled Map Editor, HardonCollider.

You can download the Löve project for this tutorial here: Download

The Tools

A lot of work is done by the librarys and tools we use here. I’ll explain what each component is supposed to do briefly but if you want to go into more details you should check out the projects individually.

First of all, a platformer consists of  a main character whose movement through a world or level is oftentimes controlled through keyboard input. The character can move in horizontal direction as long as he stands on solid ground and you can apply a force that let’s him temporarily float in the air(= jumping).

The Level: In tradition of well known platformer – like super mario – we will use tiles to create our maps(if you are unfamiliar with tilemaps you could for example check THIS out). A good and free tool to edit and create tilemaps is Tiled. In order to load the maps in Löve that were build in Tiled we have to use a library. You can find a few but my tests have shown that the AdvancedTiledMapLoader from Kadoba is the most sophisticated out there.

For the sake of this tutorial I will provide a tilesheet with a tilewidth and tileheight of 16. Feel free to use, copy, modify it as you please.



The second picture is a small 32×32 map. Loading a map and displaying it in Löve is really easy.  Be sure to require the AdvancedTiledMapLoader. Set the loader path and load the map in Love.load() with loader.load(), bind the map to a variable and draw via map:draw().

 


--
--  Tutorial-Platform
--

local loader = require "AdvTiledLoader/Loader"
-- set the path to the Tiled map files
loader.path = "maps/"

function love.load()
    -- load the level and bind to variable map
    map = loader.load("level.tmx")
end

function love.update(dt)
    -- nothing to do (yet!)
end

function love.draw()
    -- draw the level
    map:draw()
end

If we execute the above code we see that the level is nicely drawn. I will write a love.graphics.scale(2,2) to zoom into the scene a bit.

The collision: We are lazy and do not want to do the collision detection by ourself so we use HardonCollider for this. Note that we still have to do the collision response. We need the collider elsewhere so we also need a file local variable collider. But first require the library to a variable(because the HardonCollider module it returns a value) and set up love.load as the following:


--
--  Platformer Tutorial
--

local loader = require "AdvTiledLoader/Loader"
-- set the path to the Tiled map files
loader.path = "maps/"

local HC = require "HardonCollider"

local hero
local collider
local allSolidTiles

function love.load()

    -- load the level and bind to variable map
    map = loader.load("level.tmx")

    -- load HardonCollider, set callback to on_collide and size of 100
    collider = HC(100, on_collide)

    -- find all the tiles that we can collide with
    allSolidTiles = findSolidTiles(map)

    -- set up the hero object, set him to position 32, 32
    setupHero(32,32)

end

The tiles from our level don’t just magically appear as HardonCollider shapes so we have to add them to the collision engine. We do this in the findSolidTiles() function. We just loop through all tiles in the level and check if they are solid. This solid property must be set via the Tiled Map Editor. Just Rightclick in the tilesets view on the desired tile click Tile Properties and add the name Solid. You don’t even need a value. Back in our loop we check all tiles and if they are solid we create a rectangular shape. The function looks like this:



function findSolidTiles(map)

    local collidable_tiles = {}

    local layer = map.tl["ground"]

    for tileX=1,map.width do
        for tileY=1,map.height do

            local tile

            if layer.tileData[tileY] then
                tile = map.tiles[layer.tileData[tileY][tileX]]
            end

            if tile and tile.properties.solid then
                local ctile = collider:addRectangle((tileX-1)*16,(tileY-1)*16,16,16)
                ctile.type = "tile"
                collider:addToGroup("tiles", ctile)
                collider:setPassive(ctile)
                table.insert(collidable_tiles, ctile)
            end

        end
    end

    return collidable_tiles
end

Tileshapes get placed in a group and are set to passive so they do not collide with each other and eat up all your cpu. Next thing we set up in the love.load() was the hero. This is relativly easy because we just create a HC shape and attach alle the other custom attributes and methods to it. I also declared the the hero local to the main.lua file right above love.load. The initializing of the hero looks like this:


function setupHero(x,y)

    hero = collider:addRectangle(x,y,16,16)
    hero.speed = 50

end

The speed of the hero is needed in the input handling method. This also is standard stuff and should look like this:



function handleInput(dt)

    if love.keyboard.isDown("left") then
        hero:move(-hero.speed*dt, 0)
    end
    if love.keyboard.isDown("right") then
        hero:move(hero.speed*dt, 0)
    end
    if love.keyboard.isDown("up") then

    end

end

 

As you might have guessed from the delta time argument this function is called from love.update. Because HardonCollider already provides move and moveTo functions for the shapes we don’t even have to this by manually. We also want the hero to fall down so we write up a small update function for the hero:

 


function updateHero(dt)

    -- apply a downward force to the hero (=gravity)
    hero:move(0,dt*50)

end

 

Now we update the code for love.update() and love.load() to the current status:



function love.update(dt)

    -- do all the input and movement

    handleInput(dt)
    updateHero(dt)

    -- update the collision detection

    collider:update(dt)

end

function love.draw()

    -- scale everything 2x
    love.graphics.scale(2,2)

    -- draw the level
    map:draw()

    -- draw the hero as a rectangle
    hero:draw("fill")
end

At last we need to handle the actual collision response. For now we just use the minimum translation vector to push away the hero from the colliding tile.


function on_collide(dt, shape_a, shape_b, mtv_x, mtv_y)

    collideHeroWithTile(dt, shape_a, shape_b, mtv_x, mtv_y)

end

function collideHeroWithTile(dt, shape_a, shape_b, mtv_x, mtv_y)

    -- sort out which one our hero shape is
    local hero_shape, tileshape
    if shape_a == hero and shape_b.type == "tile" then
        hero_shape = shape_a
    elseif shape_b == hero and shape_a.type == "tile" then
        hero_shape = shape_b
    else
        -- none of the two shapes is a tile, return to upper function
        return
    end

    -- why not in one function call? because we will need to differentiate between the axis later
    hero_shape:move(mtv_x, 0)
    hero_shape:move(0, mtv_y)

end

 

Ok, now this was quite a a lot of code. Time for a recap:

We load and draw the level via the AdvTiledLoader, setup HardonCollider, find the solid tiles and add them as shapes to hardoncollider. We introduced a playerobject with movement and simple gravity and handled the collision with solid tiles.

There is still a lot to do. Next steps would be include the crucial part of a jump and run game: the jumping and proper movement. I hope this helps to start your platformer project. Lets me know what you think in the comments.

20 thoughts on “Tutorial: Creating a platformer with Löve

  1. Hey i got all your code but in the findSolidTiles() function you make a call to a nil value, map.tl

    local layer = map.tl["ground"]

    It gives me this error: http://i.imgur.com/2521i.png

    Any clue whats wrong with it? I looked through some of the code included in AdvTileLoader and i could only find references to the thing but i could never find its function. I did delete a few files i thought were unnecessary so if i was being stupid and deleted something i wasnt suppose to than please tell me?

  2. thanks for pointing that out in love.load it should be:
    allSolidTiles = findSolidTiles(map)

    fixed that in post

  3. I ran into the same problem and the fix you provided didn’t help. What did though was going unto the Tiled map file and changing the name of the Tile layer to “ground”. Now my play just falls through the ground so I think I forgot to type something or I just missed something out.

  4. @JaydenB
    In Tiled the layer has to be called “ground”! Or change the following to your tiled layer name:

    local layer = map.tl["ground"]
  5. Haha I need part 2 too :P but I solved it. I found out the layer name thing myself, but then I re-read through the tut, and figured out I needed to change the Tile Properties…

  6. Thanks for this, it’s really helping me learn Love2D and Lua. The only issue i’m trying to figure out based on this tutorial is finding the x and y position of the hero at all times i.e. Im try to attach the basic camera.lua class to these values. I’ve attempted to set up hero.x and hero.y but I just can’t get it to work. Any help for a beginner like me would be much appreciated :)

  7. @headchant

    Damn it, I knew it was something as simple as that. Thank you very much. I look forward to part 2! :D

  8. This really helped me get started, thanks!

    I do have one issue though. It’s quite hard to explain, but it’s easily recreated by running the Love project that you provided and simply holding down the left arrow key before the hero hits the ground. The hero will get stuck on the wall because the wall is made up of multiple tiles. I’m sure you can much better understand what’s going on than I can describe it.

    Any insight towards a solution?

  9. Hey, i am currently authoring a book on Love2d video game programming. i found your tutorials easy and straight to the point, got some ideas from your tutorials and i would like to give you credits for it in my book. Hope you don’t mind?

  10. Really helpfull! But can anyone please tell me what should I do if i needed my hero to be an actual image. I’ve tryed everything I could think of but it won’t work:(

  11. I’m still struggling with the “attempt to index field ‘tl’”

    Tiled has the layer set as ground (even tried changing it to something else, and even setting a layer property called ground)

Leave a Reply

Your email address will not be published. Required fields are marked *


three × = 18


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

Comment moderation is enabled. Your comment may take some time to appear.