Performance: Tilemaps in Löve with spritebatch

A usual pattern in 2D games is the use of tilemaps. In Löve you can implement those in a variety of ways. In this article I want to compare tilemap implementations in Löve 0.9.1 using spritebatches.

I will use this to create an ascii display.

Screenshot 2014-10-30 15.01.44

Timing

I will use the following code to measure the performance. It is an extended example from kikitos time function. It uses os.clock to measure the time before the execution and again after the execution.

time.lua:

   local last

    return function(title, f, n)
        if title == "clear" then last = nil; return end
        collectgarbage()
        local n = n or 100000
        local startTime = os.clock()
        for i = 1, n do
            f()
        end
        local endTime = os.clock()
        local delta = endTime - startTime
        if not last then last = delta end
        print(title, delta, (delta) - last)
        last = delta
    end

Spritebatch

We will generate a spritebatch with a tilesheet image(aka texture atlas) and use the spritebatches add/set method to change the content. The spritebatch will have NxM size. I used Alloy_curses_12x12 from the dwarf fortress tileset repository.

   local img = love.graphics.newImage("Alloy_curses_12x12.png")
    local spritebatch = love.graphics.newSpriteBatch(img, 87*25, "stream")
    local quads = {}
    for x = 0, 255 do
        local i,j = 1+(x%16), math.floor(x/16)
        quads[x] = love.graphics.newQuad(i*12, j*12, 12, 12, img:getWidth(), img:getHeight())
    end

We can also use the bind and unbind function on the spritebatch to bind the spritebatch into memory before we loop through the whole display grid.

Luajit Tables

We need two arrays: one to store the id and one to store the ascii value of the cell. Map is our display grid and idmap will store the id returned by adding the cells.

   local map = {}
    local idmap = {}
    spritebatch:bind()
    for i=0,86 do
        map[i] = {}
        idmap[i] = {}
        for j=0,24 do
            map[i][j] = 68
            spritebatch:setColor(255,255,255)
            idmap[i][j] = spritebatch:add(quads[map[i][j]], i*12, j*12)
        end 
    end
    spritebatch:unbind()

   local random = math.random
    time("spritebatch, lua table", function()
        spritebatch:bind()
        for i=0,86 do
            for j=0,24 do
                map[i][j] = random(255)
                spritebatch:setColor(0,255,255)
                spritebatch:set(idmap[i][j], quads[map[i][j]], i*12, j*12)
            end
        end
        spritebatch:unbind()
        love.graphics.setColor(255,255,255,255)
        love.graphics.draw(spritebatch)
    end, 1000)

FFI Arrays

We use the ffi.new functions to generate a 2 Dimensional uint8_t array for the character display grid and uint16_t for the id grid.

   local ffi = require "ffi"
    local map = ffi.new("uint8_t[87][25]")
    local idmap = ffi.new("uint16_t[87][25]")

    spritebatch:bind()
    for i=0,86 do
        for j=0,24 do
            map[i][j] = 68
            spritebatch:setColor(255,255,255)
            idmap[i][j] = spritebatch:add(quads[map[i][j]], i*12, j*12)
        end 
    end
    spritebatch:unbind()

    local time = require "time"

    time("spritebatch, ffi array", function()
        local random = math.random
        spritebatch:bind()
        for i=0,86 do
            for j=0,24 do
                map[i][j] = random(255)
                spritebatch:setColor(0,255,255)
                spritebatch:set(idmap[i][j], quads[map[i][j]], i*12, j*12)
            end
        end
        spritebatch:unbind()
        love.graphics.setColor(255,255,255,255)
        love.graphics.draw(spritebatch)
    end, 1000)

Performance

Using Luajit tables is a lot faster than using FFI arrays:

spritebatch, ffi array 2.13035 0 spritebatch, lua table 0.577013 -1.553337

image (2)

FFI Arrays are a lot slower, so you should stick to tables if you want to draw tiles fast.

Slime explains: “The FFI array is probably slower because the loop it’s used in can’t be compiled by the JIT (since C API functions – all of LÖVE’s API, for example – prevent JIT compilation for the block of code they’re called in.)”

One more thing for spritebatch

It’s probably a good idea to only use spritebatch:set when something has changed. You can do this by having two map tables. Check if the map table is different than the cache table and only then set the spritebatch tile.

   spritebatch:bind()
    for i=0,86 do
        for j=0,24 do
            if map[i][j] ~= old[i][j] then
                old[i][j] = map[i][j]
                spritebatch:setColor(255,255,255)
                spritebatch:set(smap[i][j], quads[map[i][j]], i*12, j*12)
            end
        end
    end
    spritebatch:unbind()

I created an example lua file that can be used as a very simple base to render roguelike ascii games:gist. But of course this can be used for any other tile based game too.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.