Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Levels from files

In Tile maps the program built its level square by square. A real game draws its levels in a map editor and saves each one as a file, and the program only has to load it. Your game’s folder has one: level.map.json, the courtyard from my Adventures of Sir Blocksley demo, drawn with the tiles in greyscale.png.

One line

A map has layers of its own, and CREATE MAP puts them on the screen’s. It takes the file, which of the map’s layers to show, and with !! layer_to the screen layers they go on, one each. A map’s layers count up from 0, the lowest. This map has two, and this puts them on screen layers 1 and 2, which leaves screen layer 0 under them for anything else. The words go on layer 3, over the top, as in Tile maps:

SCREEN 320, 240
CREATE MAP "level.map.json", 0...1 !! layer_to(1...2)
SET LAYER 3
PRINT "the level is", mapwidth(1), "tiles across and", mapheight(1), "down"
the level is 20 tiles across and 23 down

The file says everything else: which tile set to use and how big a tile is, so there’s no LOADIMG and no TILESIZE. mapwidth and mapheight say how big a map is, in tiles, given the screen layer it’s on. This one is bigger than the screen, so you’d SET SCROLL down it the way you did in Tile maps.

One map layer goes on one screen layer: CREATE MAP "level.map.json", 1 !! layer_to(3) puts the map’s layer 1 on screen layer 3. Leave the map’s layers out and I start from 0, as many as layer_to has room for: CREATE MAP "level.map.json" !! layer_to(1...2) is the same as the line above.

From 1.x: That’s CREATE MAP("level.map.json", 0 TO 1) AS LAYER 1 TO 2.

What’s in the file

The file is JSON, and loadjson reads any JSON file into maps and lists. Here’s the level’s:

SCREEN 320, 240
level = loadjson("level.map.json")
PRINT "tiles from", level.tileset, "and", level.tileWidth, "pixels square"
PRINT level.mapWidth, "by", level.mapHeight
FOR layer = level.layers
    PRINT "a", layer.layerType, "layer called", layer.name
END FOR
tiles from greyscale.png and 32 pixels square
20 by 23
a tile layer called Layer 1
a tile layer called Layer 2
a metadata layer called Metadata

Two layers of tiles, and one more of metadata, which is the interesting one. It isn’t drawn. Each square of it holds a bit of text that says what’s there: "P" where the player starts, "C" for a coin, "X" for the exit, whatever your game needs. The map editor lets you write on it, and the game reads it back.

Crash-tan’s Tip: loadjson is for any data your game keeps in files: a list of levels, the enemies in each, what a shop sells. Build the file by hand or with a tool, and read it with one line.

Starting from the file

A TILEMAP can start from a file too. BASEMAP says which, and then the map’s size, tile set and layers all come from it. Each layer that says !! into(grid) fills that grid with the file’s tiles, so your program can look at the level square by square:

SCREEN 320, 240
ground = (23, 20):INTEGER
walls = (23, 20):INTEGER
TILEMAP "level"
    BASEMAP "level.map.json"
    TILES 0 !! name("ground") into(ground)
    TILES 1 !! name("walls") into(walls)
END TILEMAP
CREATE MAP "level", 0...1 !! layer_to(1...2)

count = 0
FOR row = 0 ... 22
    FOR column = 0 ... 19
        IF walls(row, column) <> 0 THEN count = count + 1
    NEXT
NEXT
SET LAYER 3
PRINT count, "squares have a wall on them"
PRINT "the top left is", ground(0, 0), "in the grid and", maptile(0, 0, 1), "on the map"
121 squares have a wall on them
the top left is 10 in the grid and 9 on the map

The grids are 23 rows of 20, the size of the map. A TILEMAP’s layers go on the screen the same way a file’s do, and when it has more than one TILES layer, each needs a name, which picks it too: CREATE MAP "level", "ground" !! layer_to(1). maptile(x, y, 1) asks about the map on screen layer 1, where its layer 0 went. The grid counts tiles from 1, with 0 for none, the same as the grids in Tile maps, while maptile counts them from 0.

Markers

The courtyard’s metadata layer is already written on. "P" is where Sir Blocksley starts, "K" is the king, "B" is someone standing about, "*" runs along every wall, and a number marks each way out.

mapmeta(x, y) reads the marker on one square of the map on the layer you’re drawing on, so SET LAYER one the map is on, and ask before going up to print. mapdata goes through a whole region and gives every square back, each with its x, its y and its meta, so finding the start and the people is one loop:

SCREEN 320, 240
CREATE MAP "level.map.json", 0...1 !! layer_to(1...2)
SET LAYER 1
start = mapmeta(9, 8)
cells = mapdata((0, 0) -> (19, 22))

SET LAYER 3
PRINT "on (9, 8):", start
FOR cell = cells
    IF cell.meta = "P" THEN PRINT "start at", cell.x, cell.y
    IF cell.meta = "K" THEN PRINT "the king at", cell.x, cell.y
    IF cell.meta = "B" THEN PRINT "someone at", cell.x, cell.y
END FOR
on (9, 8): P
someone at 3 4
the king at 9 5
someone at 15 6
start at 9 8

Your program can put markers down too. A META layer that says !! from(grid) is drawn from a grid of text you keep, the same way TILES is drawn from a grid of numbers:

SCREEN 320, 240
markers = (23, 20):STRING
markers(8, 9) = "P"
markers(7, 3) = "C"
markers(3, 14) = "C"

TILEMAP "level"
    BASEMAP "level.map.json"
    META "markers" !! from(markers)
END TILEMAP
CREATE MAP "level", 0...1 !! layer_to(1...2)
SET LAYER 1
cells = mapdata((0, 0) -> (19, 22))
start = mapmeta(9, 8)
gone = mapmeta(0, 1)

SET LAYER 3
FOR cell = cells
    IF cell.meta = "C" THEN PRINT "a coin at", cell.x, cell.y
END FOR
PRINT "on (9, 8):", start
PRINT "on (0, 1):", gone
a coin at 14 3
a coin at 3 7
on (9, 8): P
on (0, 1): 

Your grid takes the place of the file’s markers: (0, 1) had a "3" on it, and now it has nothing. That’s why the grid puts "P" back.

Change the grid and UPDATE MAP shows the change, for markers as for tiles: when a coin’s picked up, rub out its "C".

Crash-tan’s Tip: Most squares have no marker, so look for the ones you want. mapdata gives back every square in the region, marker or not.

Which square is that?

A sprite’s place, or the mouse’s, is in pixels, and the map is in squares. Four functions turn one into the other, and the difference between them is scrolling.

  • maptilex(x) and maptiley(y) are for a place on the screen, like mousex and mousey. They add on how far the map is scrolled.
  • mapworldtilex(x) and mapworldtiley(y) are for a place in the level, like a sprite’s x and y. They don’t.
SCREEN 320, 240
CREATE MAP "level.map.json", 0...1 !! layer_to(1...2)
FOR layer = 1 ... 2
    SET LAYER layer
    SET SCROLL 0, 160
NEXT
SET LAYER 1
on_screen = maptiley(20)
in_level = mapworldtiley(20)
SET LAYER 3
PRINT "20 pixels down the screen is row", on_screen
PRINT "20 pixels down the level is row", in_level
20 pixels down the screen is row 5
20 pixels down the level is row 0

With the map scrolled 160 pixels, five rows of 32, the top of the screen shows row 5 of the level.

A walk for coins

Here’s all of it together: the level from the file, the start and the coins from the markers, and the hero walking about picking them up. When the hero stands on a square marked "C", the marker’s rubbed out and the coin goes.

SCREEN 640, 480
LOADIMG "hero.png", "hero"
MEMORYBUFFER "coin", 10, 10
    CIRCLE (5, 5), 4, 14, 14
END MEMORYBUFFER

// The start and the coins, as the map editor would mark them.
markers = (23, 20):STRING
markers(8, 9) = "P"
FOR spot = [(3, 7), (14, 3), (9, 9), (15, 8), (3, 2)]
    markers(spot.y, spot.x) = "C"
END FOR

TILEMAP "level"
    BASEMAP "level.map.json"
    META "markers" !! from(markers)
END TILEMAP
CREATE MAP "level", 0...1 !! layer_to(1...2)
SET LAYER 1

// A coin sprite on every "C". coin_at remembers which sprite is where.
coin_at = (23, 20):INTEGER
coins = 0
FOR cell = mapdata((0, 0) -> (19, 14))
    IF cell.meta = "C"
        id = reserve_sprite()
        SET SPRITE id !! image("coin") x(cell.x * 32 + 11) y(cell.y * 32 + 11)
            !! layer(2)
        SHOW SPRITE id
        coin_at(cell.y, cell.x) = id
        coins = coins + 1
    ELSE IF cell.meta = "P"
        x = cell.x * 32 + 16
        y = cell.y * 32 + 16
    END IF
END FOR

hero = reserve_sprite()
SET SPRITE hero !! image("hero") x(x) y(y)
    !! origin((0.5, 0.5)) layer(2)
SHOW SPRITE hero

found = 0
DO WHILE found < coins
    IF COOLDOWN(1)
        IF keydown("Left") AND x > 8 THEN x = x - 2
        IF keydown("Right") AND x < 632 THEN x = x + 2
        IF keydown("Up") AND y > 8 THEN y = y - 2
        IF keydown("Down") AND y < 472 THEN y = y + 2
        SET SPRITE hero !! x(x) y(y)

        column = mapworldtilex(x)
        row = mapworldtiley(y)
        IF mapmeta(column, row) = "C"
            markers(row, column) = ""
            UPDATE MAP "level"
            DISABLE SPRITE coin_at(row, column)
            found = found + 1
        END IF
    END IF
LOOP
SOUND 660, 0.3

Click the screen and walk with the arrow keys. The level isn’t scrolled, so mapworldtilex and maptilex would agree here; use the world ones for a sprite anyway, and the game keeps working once the camera starts to move.