Loading maps from the Tiled editor

I'm liking Tiled as an editor. Is there a lua library for loading maps made with Tiled? I like that it can export directly to lua.

From reading the Playdate docs, it seems that Tilemaps could be used to load the tiles, map, and even to render it.

Perhaps there's someone working on this already?

Right, I haven't programmed games in ages. I forgot.

Turns out you can use Tilemaps and ImageTables: they are quite nice indeed.

Tilemaps are great for straight maps (orthogonal). They don't support isometric maps, because you can't offset rows the way you need to for an isometric perspective.

I'll be working (slowly) on finishing a Tiled map loader in Lua. It would be interesting to see when it can load infinite maps and scroll.

Do you use any other editors like Tiled?
Any other suggestions?

2 Likes

I would LOVE a Tiled map loader! It's a blocker for me at the moment.

Before the Playdate SDK was released I started learning Lua using Love2D. There is a great library called Simple Tiled Implementation (STI) . It would be awesome to have something like that for Playdate.

1 Like

Cool! I've started on it, it's definitely fun!

@polyphonicboy What sort of game are you making? I'd love to collaborate on this. I've started doing isometric maps, but I can switch it needed. Let me know, it will be much more productive to work on real example maps.

Ideally, it would support all the features from Tiled, you would point it to a lua export and that would be it.

2 Likes

I'm working on a little platformer, so would be simple orthogonal maps.

I'm very much a novice programmer, but I would love to help as much as I can. I've made a test map in Tiled with 3 layers (sprites, walls, background).

Here's how the game is looking just now. Currently the background is one image, and I've manually positioned a few block sprites. The elevator is controlled with the crank.

playdate

2 Likes

This looks really cool! Loving the idea of using the crank for elevators. I bet you're thinking of other puzzles to involve the crank.

How do you intend to scroll it? Both axes, Mario-like? Would be a good use for Tiled, you would make a whole big level.

PS Are you on the discord? We should connect!

https://www.reddit.com/r/PanicPlaydate/comments/f1683n/join_the_playdate_discord_server/

Yeah I'm Yoshimi on the discord. I couldn't find you there, but send me a friend request and we can chat.

I'm planning on both axis scrolling. Initially thinking of starting at the bottom of the level and working up to the goal at the top, using elevators etc to reach the end. Using the crank to interact with various contraptions to progress or take out enemies.

There's a LDtk loader here: Importer for LDtk level editor

@matt this is great, thanks. I'm new at this so I don't really know about tools. LDtk seems great. I'll give it a spin.

In case anyone stumbles on this thread while searching for "Playdate isometric Tiled loader" here is code I'm using in my game to do that.

This is loading an isometric Tiled map saved as a .tmj with embedded tilesets. The tileset is created using images named properly for use as an imagetable on the Playdate.

This is a minimal class which doesn't support multiple layers or tilesets. The device also doesn't have the performance to scroll the tilemap very well without a fair amount of extra work which isn't shown here (my game has a whole chunk load/unload system which keeps about 1 screenful of tiles in place at a time and even that just barely hits 30fps).

What's most useful here, I think, is the math in addSprites() which shows how to do the coordinate calculations.

This isn't a drop-in "just works" thing but it is really small and not too opinionated so it can be a reasonable place to start if someone wants to use an isometric Tiled map. From here one could add things to support various Tiled features as needed.

Note: that the tiled format has some relatively involved mapping from the tile id numbers in the tmj files to tile id numbers in tilesets. However, if you only have one tileset the mapping can be simplified to a -1 operation on the tile id which is what I do here.

import 'CoreLibs/graphics'
import 'CoreLibs/object'
import 'CoreLibs/sprites'

import 'TileType'

local gfx <const> = playdate.graphics
local geo <const> = playdate.geometry

--
-- Tilemap
--
-- Loads Tiled map files (.tmj) and displays them with sprites.
-- Currently only supports iso maps.
--
class('Tilemap').extends(Object)

function Tilemap:init()
end

function Tilemap:load(filename)
  self.mapData = json.decodeFile(filename)
  assert(self.mapData)

  for _, layer in ipairs(self.mapData.layers) do
    print('Layer: '..layer.name..' ('..layer.type..')')
  end

  -- Set this to whatever your tileset name is.
  self.tileset = self:getTileset('ground')

  print('Tileset: '..self.tileset.name)

  local path = 'assets/images/'..self.tileset.name ..'/'..self.tileset.name

  print('  '..path)

  self.tilesetImages = gfx.imagetable.new(path)
  assert(self.tilesetImages,  'Couldn\'t load tileset images')

  print('  '..#self.tilesetImages..' images loaded')

  self.tileWidth = self.mapData.tilewidth
  self.tileHeight = self.mapData.tileheight

  self.scaleX = 2
  self.scaleY = 1

  self:addSprites(self.mapData.layers[1].data, self.mapData.layers[1].width, self.mapData.layers[1].height)
end

function Tilemap:getTileset(name)
  for _, tileset in ipairs(self.mapData.tilesets) do
    if tileset.name == name then
      return tileset
    end
  end
  return nil
end

function Tilemap:getLayer(name)
  for _, layer in ipairs(self.mapData.layers) do
    if layer.name == name then
      return layer
    end
  end
  return nil
end

function Tilemap:addSprites(tiles, width, height)
  assert(tiles)
  for i = 0, height - 1 do -- Loop through rows
    for j = 0, width - 1 do -- Loop through cols in the rows
      local tileIndex = (i * width) + j + 1

      if tiles[tileIndex] ~= 0 then -- If there is a tile to draw
        local x =
          (j * (self.tileWidth / 2)) -- The width on rows
          - (i * (self.tileWidth / 2)) -- The width on cols
        local y =
          (i * (math.floor(self.tileHeight / 2))) -- The height on rows
          + (j * (math.floor(self.tileHeight / 2))) -- The width on cols

        local tile = tiles[tileIndex] - 1

        local image, error = self.tilesetImages:getImage(tile)
        assert(image, tile)

        local sprite = gfx.sprite.new(image)
        assert(sprite)

        -- If anything has high Z bump them up to a higher layer so that overlapping works properly.
        -- I'm using an enum but you could just keep a table of wall-like tiles.
        local z = 0
        if tile == TileType.Wall or
           tile == TileType.Spikes or
           tile == TileType.Bumper then
          z = 1
        end

       -- Your player character and other moving objects should be at the same layer as the walls. You will need to set their Z index every time they move using a call similar to the one below.

        -- The `setCenter()` offset here accounts for the size of tile graphics.
        -- We want the origin to be right at the top center pixel of a ground tile.
       -- You will likely need to adjust this based on the visual "thickness" of your tiles.
        sprite:setCenter(0.5, 0.393939)
        sprite:moveTo(x, y)
        sprite:setZIndex(y + 34 + z * 1000) -- `The setZindex()` call also takes into account the tile graphic height.
        sprite:add()
      end
    end
  end
end

3 Likes

what does TileType do here? I'm having issues getting this working

TileType is just an enum that looks like this:

TileType = {
  FloorDark = 1,
  FloorLight = 2,
  Wall = 3,
  Spikes = 4,
  Bumper = 5,
}

The ints here match up with tile ids in the Tilemap file. You don't have to use the enum and, in fact, not using it will be a bit faster but I use it because it's easier to read.

The main thing that's happening here is that tile types which have art that extends above the flat terrain should have in increased Z index to ensure proper overlapping behavior. So, in my example, Walls, Spikes, and Bumpers get pushed up into the next "layer". For your game you should identify which of your tiles ore like this this and give them a Z-boost. If all of your tiles are flat against the ground then you can skip this.