Has anyone posted a good tutorial for tilemaps?

Hey, I've been messing around the playdate SDK (programming in Lua), and I love the concept of tilemaps, but I have no clue how to use them. I've been searching around the internet, and I haven't been able to find any good tutorials, so I thought I would post here. Has anyone made or seen any good tilemap tutorials for the playdate SDK? Thanks!

4 Likes

I would love a tilemap tutorial too!

Before the Playdate SDK was released I started learning Love2D Lua. There is a great library called Simple Tiled Implementation (STI) that makes loading tilemaps very easy. It would be great if something like this was created for Playdate.

I've tried reading the level loader code in the 'Level 1-1' example, but I'm struggling to understand/adapt it.

I would recommend tiled documentation here as good place to start: Tiled Documentation β€” Tiled 1.10.2 documentation

First you need the image that will serve as the spritesheet

It must be named somethinganything-table-[ x ]-[ y ].theextension
[ x ] = the width of a TILE in pixels
[ y ] = the height of a TILE in pixels
ie: sprites-table-16-16.gif

local spritesheet = nil -- this will be the image table
local map = nil -- this is the tile map

function init() -- needs to be called before update
    spritesheet = gfx.imagetable.new("images/sprites") -- this will automatically load sprites-table-16-16.gif if it exists, so try not to reuse the part before -table in multiple images or you'll have to specify the entire filename
    map = gfx.tilemap.new()
    map:setImageTable(spritesheet)
    map:setSize(width of the map in TILES, height of the map in TILES)
end

function playdate.update()
	gfx.sprite.update() -- whatever drawing code you have first
	map:draw(x you want it to draw, y you want it to draw) -- ie: map:draw(0,0)
end 

you can use: (x and y start at 1 like all lua arrays)
map:getTileAtPosition(x,y) -- will return nil if there is no tile, otherwise the tile INDEX NUMBER
map:setTileAtPosition(x,y, INDEX NUMBER) -- if the INDEX NUMBER is out of bounds from the spritesheet (ie: below 1, or above the total number of tiles) it'll be set to blank
spritesheet:drawImage(INDEX NUMBER,x,y) -- draw a sprite from the spritesheet

as for the INDEX NUMBER, it starts at 1 which is the top-left-most sprite, and goes right
ie:
1,2,3,4,5
6,7,8,9,10
11,12,13,14,15

2 Likes

Hey thanks for the tip, but I'm still not clear on how to draw a specific tile at a specific location. For example, I've imported the tilemap like so:

	bgTileMap = gfx.tilemap.new()
	bgTileMap:setImageTable(bgImageTable)
	bgTileMap:setSize(4,4)

But then how would I draw, let's say, the tile at 3,4 in the tilemap at position 100,100 on the screen?

Edit: I just read Yoshimi's comment and decided to take a look at Level 1-1. After that and nibuen's comment, I'm starting to realize that Playdate's tilemap implementation is assuming you're using something like Tiled to generate levels. My assumption was that it was more like a spritesheet where you import a series of sprites in a single image, then draw them by specifying which cell you want.

1 Like

I think you might be confusing tile maps with image tables (what some other platforms call atlases). If you want to draw just one image, it'll be much easier to get that image from the image table; e.g. if the tile is at 3,4 in the original atlas image, you can use bgImageTable:getImage(3,4):draw(100,100). A tile map is for making a large image, usually a background, out of tiles.

3 Likes

Hi Tanya, I was doing some research in this topic and your code example is all what I need! Thanks a lot. That being said, I'm getting an error (attempt to index a nil value) in the map:draw function and I'm not sure why, I'm using the screen coordinates as parameters map:draw(100, 50) but still getting the same error, I'm afraid is something else... Any help? Thanks!

1 Like

You're welcome.

You'd have to show your code and the error message for us to be of any help

I'd have replied sooner but I was visiting the in-laws for thanksgiving

1 Like

Hi, thank you so much Tanya, no worries. This is the code I'm using, where the image is a png of 32x32 pixels containing a tile. I got no more errors, but the image does not show on the screen... I have no idea what I'm missing. Thanks for your help!

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

local geo = playdate.geometry
local pd = playdate
local gfx = pd.graphics

-- Creating the grid

local i = 1

local spritesheet = nil -- this will be the image table

local map = nil -- this is the tile map

function init() -- needs to be called before update

spritesheet = gfx.imagetable.new("images/tile")
map = gfx.tilemap.new()
map:setImageTable(spritesheet)
map:setSize(32,32)

end

init ()

function playdate.update()

gfx.sprite.update()
map:draw(40,40)

end

This should be:.............

1 Like

oops... Thanks a lot! I have made the change but still the image is not showing on the screen, there's something I'm still missing here...

1 Like

I'm not seeing any code to set the actual tiles in the map

You've set it to use a tileset, but then you need to make the map itself

Finally I figure out a way to implement tiles, not sure if this is the intended way from the SDK but it works fine to me.
It uses a png containing all the isometric tiles (here only 3) with a ration 2:1 that the compiler separates into 3 different kind of tiles. I used a one dimensional table or array to set up the way I want combine them and the draw function to put into the screen.
As I said, probably not the best implementation, so any comments are welcomed!

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

local geo = playdate.geometry
local pd = playdate
local gfx = pd.graphics

-- Creating the grid

local grid = {}

local spritesheet = nil -- this will be the image table

local map = nil -- this is the tile map

function init() -- needs to be called before update

-- Grid 'hand made' configuration with 3 different types of setTiles

grid [1] = 1 -- tile type 1
grid [2] = 2 -- tile type 1
grid [3] = 1
grid [4] = 3 -- tile type 3
grid [5] = 2
grid [6] = 3

-- Tiles are in one same png image as a tiletable of 300 x 50 pixels
-- In this case it contains 3 tiles of 100 x 50, you can put as much as you need

spritesheet = gfx.imagetable.new("images/tile-table-100-50")

-- The image is separated into 3 tiles of 100 x 50 px each 

map = gfx.tilemap.new()

map:setImageTable(spritesheet)

-- Tile matrix. Grid is a one dimensional table array

map: setTiles(grid, 2) 

end

init ()

function playdate.update()

gfx.sprite.update()

-- Show the tile map starting at X = 10, Y = 10 on screen
map:draw(10,10)

end

tile-table-100-50
tile-table-100-50.png

2 Likes

I'm working my way up to using tilemaps (want to port my UI code from Love2d first), but I noticed you had a couple of assign nil statements, such as:
local map = nil
iirc this is how you delete entries in Lua, so if you are trying to force the map variable to be locally scoped you should probably write something like
local map = false
Since later map assignments will then overwrite the local variable instead of creating a global map variable. A false value will still let you check if you have "built" the map or not, if this is something you need to check for later.

Also that dither looks rad! Nice going on isometric, too

local map = nil works fine, it’s just a bit redundant because local map assigns nil to map (and scopes it local). Local variables are only deleted when they go out of scope. Global variables can be deleted with = nil.

local function test()
    local scope = nil
    
    local function setScope()
        scope = "local"
    end
    
    setScope()
    
    print("scope is " .. scope .. " to function test()")
end

test()

if scope ~= nil then
    print("scope is global")
else
    print("scope is not global")
end
scope is local to function test()
scope is not global
2 Likes

Some time has pass by, and I learn and discover quite a few things... if anyone is interested on isometric projections please let me know an I'll add some post on this :smile:
ezgif.com-gif-maker

9 Likes

Yes please. I would be interested.

2 Likes

Hi there, I got a bit confused with the examples above and still couldn't managed to draw tiles on the screen. So I kept at it and made it work in my own way. The part that my smoothbrain forgot is that duh, once you have a tilemap you need to put some actual data in it.
Here's the simplest, most basic way to put some tiles on screen that I found, I hope it helps someone!

-- Core libraries
import "CoreLibs/graphics"
import "CoreLibs/math"
-- Constants
local pd <const> = playdate
local gfx <const>  = playdate.graphics

-- Setup
local tileset = gfx.imagetable.new("images/tilemap_packed-table-32-32.png")
local tilemap = gfx.tilemap.new()
tilemap:setImageTable(tileset)

--[[ 
Our tilemap has been created but it's still EMPTY! We need to feed it some actual level date with tilemap:setTiles().
Now this is the part that should be done with a level editor such as LTdk, but here I'm just gonna fill it with random tile indexes.
]]
local myLevel = {}
for i = 1, 100 do
	table.insert(myLevel, math.random(20))
end

-- tilemap:setTiles(data, width) : where "data" is a 1-D array and "width" is its width on screen IN TILES.
-- For example for a level that just fits the screen (400 px wide), if your tiles are 16 px wide, you wanna set the width to 400/16 = 25 

tilemap:setTiles(myLevel, 13) -- My tiles are 32 pixels, so it takes 13 of them to cover the screen's width.
tilemap:draw(0,0) -- x, y in PIXELS where you wanna start drawing the level

function playdate.update()
end

This successfully displays a "level" made of random tile indexes. But I do have some questions: Oddly, it seems that using tilemap:setSize() isn't even necessary. Is it because tilemap:setTiles() overrides it?

Also, it seems that if I remove the import lines at the beginning, math and graphics function still work. Why is that? Are they implicitly imported at compile time if I call them somewhere, rendering importing them useless?

1 Like