Loading maps from the Tiled editor

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