Working with Tilemaps and STI

· 7 min read · Updated March 20, 2026 · intermediate
love2d tilemap sti love2d-tilemap box2d tiled

STI (Simple Tiled Implementation) is a library that bridges Tiled Map Editor and LÖVE. It loads maps exported from Tiled and gives you a clean Lua API to draw them, query tiles, manage layers, and integrate physics. If you are building a 2D game in LÖVE and want to handcraft your levels in a visual editor, STI is the standard solution.

This tutorial covers STI from loading a map to working with tile properties, custom layers, and the Box2D physics plugin.

What You’ll Need

  • LÖVE 11.x — STI is tested and works well with LÖVE 11. Avoid older versions unless you have a specific reason.
  • Tiled Map Editor 1.2.x — The recommended version for compatibility. Newer Tiled releases may export formats that require map re-exporting.
  • STI library — Grab the latest release from the STI GitHub repository. Drop the sti folder into your LÖVE project so you can require it.

No package manager needed — STI is a single Lua file with no external dependencies beyond LÖVE itself.

Setting Up Tiled

Before writing any code, create a map in Tiled. Install Tiled, then:

  1. Create a new map — use an orthogonal grid. Set your tile size (for example 32×32 pixels) and map dimensions (for example 20 tiles wide × 15 tiles tall).
  2. Add a tileset — import an image containing your tiles. Tiled calls this an image tileset. Place your tile image in your project’s assets folder so LÖVE can find it at runtime.
  3. Draw your level — paint tiles onto the map using the Stamp tool. You can add multiple layers (Background, Ground, Walls, etc.) to separate visual elements.
  4. Set tile properties — right-click a tile in the tileset panel and choose Tile Properties. Add keys like solid = true or damage = 10. These become available in STI.
  5. Export as Lua — go to File → Export As, pick Lua files (*.lua), and save the file inside your LÖVE project. The path matters: it must be relative to love.filesystem.

A typical project layout looks like this:

mygame/
├── main.lua
├── conf.lua
├── map.lua          ← exported from Tiled
└── assets/
    └── tiles.png    ← tileset image

Keep your tileset image in a folder accessible to LÖVE and use the relative path when Tiled asks for it during tileset creation. STI resolves tileset image paths relative to the map file location.

Loading and Drawing the Map

In your LÖVE game, require STI and load the map:

local sti = require("sti")

function love.load()
    -- Path is relative to love.filesystem (inside your .love archive or folder)
    map = sti("map.lua")
end

Draw the map every frame with map:draw():

function love.draw()
    map:draw()
end

The draw() method accepts optional translation and scale arguments:

function love.draw()
    -- Draw the map with 320px offset and 2× scale
    map:draw(320, 320, 2, 2)
end

To draw a specific layer instead of the whole map, use map:drawLayer():

function love.draw()
    map:drawLayer(map.layers["Ground"])
    map:drawLayer(map.layers["Walls"])
end

Drawing layers individually gives you control over render order, which matters when you draw sprites between layers.

Animations

Tile animations in Tiled (such as animated water or blinking lights) are handled automatically by STI. Call map:update(dt) inside your love.update function:

function love.update(dt)
    map:update(dt)
end

STI tracks animation timers and swaps tile IDs as time passes. You do not need to manually trigger or manage animations — just call update each frame and pass the delta time.

Working with Tile Properties

Tile properties let you attach data to individual tiles in Tiled. Access them with map:getTileProperties():

-- Pass pixel coordinates (tile col 5, row 3 at 32px/tile = 160px, 96px)
local props = map:getTileProperties("Walls", {x = 160, y = 96})
if props.solid then
    -- this tile blocks movement
end

Important: Tiled uses 1-indexed coordinates. Passing 0 for either x or y does not raise an error — it silently returns nothing useful. Always verify your tile coordinates start at 1 when iterating.

To change a tile at runtime, use map:setLayerTile():

-- Set tile at column 5, row 3 on the "Ground" layer to tile ID 12
map:setLayerTile("Ground", 5, 3, 12)

Tile IDs (gid) correspond to the global tile ID assigned by Tiled. When you query a tile, the returned table includes this gid. You can read it back and reuse it elsewhere.

Object Layers and Custom Properties

Tiled object layers store shapes like rectangles, ellipses, and polygons. STI exposes them through map.layers["Objects"].objects:

local objects = map.layers["Objects"].objects
for _, obj in ipairs(objects) do
    print(obj.name, obj.x, obj.y, obj.width, obj.height)
    if obj.properties.spawn then
        -- spawn an enemy here
    end
end

Each object has x, y, width, height, and a properties table populated from Tiled. Use objects to place spawn points, define trigger zones, or annotate level design without rendering them as tiles.

Custom Layers for Sprites

STI lets you attach your own update and draw logic to the map via custom layers. Call map:addCustomLayer() with a name and optional insert position:

map:addCustomLayer("Sprites", 3)

map.layers["Sprites"].update = function(self, dt)
    -- move sprites, check collisions, etc.
end

map.layers["Sprites"].draw = function(self)
    for _, sprite in ipairs(self.sprites) do
        sprite:draw()
    end
end

Custom layers live inside map.layers alongside regular tile layers, so you can draw them in the correct order by calling map:drawLayer(map.layers["Sprites"]) if needed, or let the layer’s own draw callback handle it. Remember that you must define both update and draw yourself — STI does not provide defaults.

Collision Detection with Box2D Plugin

STI includes a Box2D plugin that generates collision shapes from your tile map automatically. Load it by passing the plugin name during initialization:

local sti = require("sti")

function love.load()
    map = sti("map.lua", { "box2d" })

    local world = love.physics.newWorld(0, 0)
    map:box2d_init(world)

    -- optional: set collision categories per layer
    map:box2d_setLayer("Walls", "wall", world)
end

The map:box2d_init(world) call creates collision bodies for every tile that has the solid property set to true in Tiled. Your player or enemy can then collide against these bodies using standard Box2D queries.

One thing to watch out for: scaling the map with map:draw(tx, ty, sx, sy) does not update the Box2D collision bodies automatically. If you need a scaled world, you must scale your Box2D world coordinates manually or create a separate collision setup at the desired scale.

Common Pitfalls

Here are the most frequent issues you will encounter with STI:

Wrong path. STI resolves map paths relative to love.filesystem. If you run your game as a .love archive, the map file must be inside the archive. If you get a black screen with no errors, double-check the path.

Tile Collections are not supported. Tiled supports two tileset types: image tilesets and tile collections. STI only works with image tilesets — the ones backed by a single sprite sheet. If your tileset shows individual files instead of a grid, switch to an image tileset in Tiled.

Coordinates are 1-indexed. Tiled numbers tiles starting at 1. Passing 0 for an x or y coordinate silently fails. Use map:convertPixelToTile(x, y) and map:convertTileToPixel(tx, ty) when converting between pixel space and tile space to avoid off-by-one errors.

Tiled version upgrades. If you upgrade Tiled, re-export your maps. Format changes between versions can break loading. Keep a known-good version of Tiled around if you have a mature map file.

Scale and Box2D are independent. Drawing a scaled map with map:draw(tx, ty, sx, sy) does not scale the collision bodies. Your physics simulation runs in tile-pixel coordinates; handle scaling at render time or adjust your world gravity and body sizes to match.

Custom layer callbacks need manual definition. When you call map:addCustomLayer, STI creates the layer entry but does not populate update or draw. You must assign these yourself or the layer does nothing.

See Also

With those issues in mind, STI gives you a fast path from a Tiled map to a rendered, queryable, physics-enabled game world. Start with a simple static map, get it drawing, then layer in animations, tile queries, and Box2D collisions one step at a time.