Skip to main content

GridMap module

A GridMap is a grid of cells in which each cell holds a number from 0 to 255. It is the map of a game: which tile is drawn in each cell (with draw.tilemap), which cells are walls for the sprites, how to get from one place to another and where each thing is.

JARU GridMap module screenshot for creating tilemaps, levels, and paths in games

This module is especially useful for developing arcade-style games, grid-based puzzles, map simulations, and any application that needs intelligent navigation between points.

use GridMap

What a GridMap is​

Cells and values​

Each cell is named by its column x and its row y, counted from 0: in a 20 × 15 map, x goes from 0 to 19 and y from 0 to 14. Each cell holds a value from 0 to 255, which means whatever you decide:

ValueTypical use
0Empty floor / walkable
1Wall / obstacle
2-254Different terrain types or states
255In a tilemap, an empty cell that is not drawn; in a dMap distance map, unreachable

To draw the map and to make it collide with sprites, the map also needs the pixel size of its tile (tSize) and its position on the screen (x, y). A map you only use to find paths does not need them.

Solids and platforms​

The solids and platforms lists say which cell values are walls and which are platforms that only stop from above. They are used by:

  • collisions with sprites: hits and sprite.hitsGrid;
  • pathfinding: path, dMap and nearest go through the cells that are not in solids.

Toroidal movement (wrap)​

When wrapX or wrapY are true, the grid's edges connect:

  • wrapX: moving right from the last column leads to the first column.
  • wrapY: moving down from the last row leads to the first row.

This is useful for maze games where the character can go through the map edges. It is taken into account by get, collisions, draw.tilemap (which repeats the map) and pathfinding.

map.wrapX = true
map.wrapY = true
var route = map.path(0, 5, 19, 5) // Can wrap around the edges
Wrap on both axes

path, dMap and nearest do not tell the axes apart: as soon as wrapX or wrapY is true, they connect the edges horizontally and vertically. The allowWrap parameter of dMap and nearest does the same. If you only want one axis, close the other one with a row or a column of walls.

Cost​

  • A grid takes one byte per cell: a 100 × 100 one, about 10 KB.
  • path, dMap and nearest go through the whole map (breadth-first search, cost O(width × height)); keep the map size in mind in a game that calls them often.
  • To send several enemies towards the same thing, compute dMap once and call dir once per enemy every frame.

Functions​

FunctionWhat it doesReturns
GridMap.new(width, height)Creates a map with all the cells at 0A GridMap
GridMap.load(file)Loads a map from a .gmap fileA GridMap

The methods and functions of GridMap throw an exception when something fails (an argument of another type, a cell outside the map…), which you catch with try (see Errors and exceptions).

new​

The GridMap.new(width, height) function creates a new grid with the specified dimensions. All cells are initialized to 0.

use GridMap

// Create a 20x15 cell grid
var map = GridMap.new(20, 15)
ParameterTypeDescription
widthintegerNumber of columns in the grid
heightintegerNumber of rows in the grid

The new map has no tile size (tSize() is [0, 0]), no tileset and no solids. With a width or a height of 0 it throws ERR_INVALID_VALUE; with decimals, ERR_TYPE_MISMATCH.

load​

The GridMap.load(file) function loads a map from a .gmap file.

var map = GridMap.load("Maps/Level1_cells.gmap")
ParameterTypeDescription
filestringPath to the .gmap file

It returns a GridMap with the file's cells and its tile size. It brings neither the tileset, nor solids, nor platforms: set them yourself.

The IDE creates the .gmap files when you build, from the maps of the tilemap editor: Maps/<map>_tiles.gmap with the tiles and Maps/<map>_cells.gmap with the cell values. What each one holds and what the file looks like is explained in What it saves and what it generates.

ErrorWhen
ERR_FILE_NOT_FOUNDThe file is not there.
ERR_INVALID_FORMATIt is not a .gmap, it is from another version or it lacks cells.
ERR_INVALID_VALUEThe map in the file is 0 wide or 0 high.
ERR_TYPE_MISMATCHThe path is not a string.

Properties​

Properties are accessed and assigned directly as fields of the object.

PropertyTypeDefaultWhat for
x, ydecimal0Where cell (0, 0) goes, in pixels
wrapX, wrapYyes/nofalseConnecting the edges
solidslist or nilnilThe wall values
platformslist or nilnilThe platform values
tilesetBitmap or nilnilThe image of the tiles
bouncyyes/nofalseBouncing against the map
elasticitydecimal, from 0 to 10How much it bounces

x / y​

Position of the map in screen space, in pixels: where the top left corner of cell (0, 0) goes. It is used by draw.tilemap, draw.scene and collisions with the map; draw.gridBitmap does not use it, since it receives its own position.

map.x = 32
map.y = 16
println(map.x) // 32
PropertyTypeDescription
xfloatHorizontal position of the map
yfloatVertical position of the map

To scroll without a scene, change them: with negative values, the map moves left and up. Assigning them something that is not a number throws ERR_INVALID_VALUE.

wrapX / wrapY​

Enable toroidal movement (the map's edges connect). When enabled, pathfinding and collision operations take them into account automatically (see Toroidal movement).

map.wrapX = true   // Left and right edges connect
map.wrapY = false
println(map.wrapX) // true
PropertyTypeDescription
wrapXbooleanConnects the right edge with the left one
wrapYbooleanConnects the bottom edge with the top one

solids​

List of cell values considered solid (obstacles). The path, dMap, nearest, and hits functions use this list automatically when no additional parameters are given.

map.solids = [1, 2, 3]   // Tiles 1, 2 and 3 are solid
println(map.solids) // [1, 2, 3]
map.solids = nil // No solids defined
PropertyTypeDescription
solidslist | nilList of solid cell values

The map keeps the same list you give it, not a copy: if you change it afterwards, the solids change. With something that is neither a list nor nil (an array, for example), it throws ERR_TYPE_MISMATCH.

platforms​

List of cell values that act as one-way platforms (they only block from above). Used by hits and by sprite.hitsGrid.

map.platforms = [4]
PropertyTypeDescription
platformslist | nilList of cell values for one-way platforms

tileset​

Bitmap linked to the map, used by draw.tilemap to automatically render the tiles, and by draw.scene for the scene's map.

var ts = Bitmap.load("Images/tileset.bmp")
map.tileset = ts
println(map.tileset) // <Obj Bitmap>
map.tileset = nil // Unlink
PropertyTypeDescription
tilesetBitmap | nilTileset associated with the map

bouncy / elasticity​

They control how sprites bounce when their collisions with the map are resolved through hits.

map.bouncy = true
map.elasticity = 0.8
PropertyTypeDescription
bouncybooleanEnables bouncing on collision
elasticityfloatBounce factor (0.0 = no bounce, 1.0 = full bounce)

With bouncy = false, the sprite does not bounce: it stops against the wall. With true, the bounce combines the map's elasticity and the sprite's. A value outside 0–1 stays at the limit.

Map methods​

MethodWhat it doesReturns
set(x, y, value)Changes the value of a cellnil
get(x, y)Reads the value of a cellAn integer
width(), height()The size in cellsAn integer
clear(value)Sets all the cells to a valuenil
clone()Makes a copyA GridMap
tSize() / tSize(width, height)Reads or changes the tile size[width, height] / nil
tEmpty() / tEmpty(value)Reads or changes the tile that is not drawnAn integer / true
count(value)How many cells have a valueAn integer
has(value)Whether any cell has a valuetrue or false
find(value)Where the cells with a value areA list [x, y, …]
around(x, y [, use8 [, allowWrap]])The values of the neighboring cellsA list
see(x1, y1, x2, y2, blockers)Whether there is a line of sight between two cellstrue or false
path(sx, sy, gx, gy)The shortest path between two cellsA list [x, y, …], or nil
dMap(targets [, walkable [, allowWrap]])A map of distances to some cellsA GridMap
nearest(targets, sx, sy [, walkable [, allowWrap]])The path to the nearest target cellA list [x, y, …], or nil
dir(x, y [, use8])On a distance map, which way to goAn integer from 0 to 8
hits(sprite)Pushes one or more sprites out of the wallsA list of values

set​

The set(x, y, value) method sets the value of a specific cell.

map.set(5, 3, 1)   // Wall
map.set(0, 0, 0) // Empty floor
map.set(2, 0, 5) // Point of interest
ParameterTypeDescription
xintegerX coordinate (column)
yintegerY coordinate (row)
valueintegerValue to assign (0-255)
ErrorWhen
ERR_INVALID_VALUEvalue is not between 0 and 255.
ERR_INDEX_OUT_OF_RANGEThe cell is outside the map. set does not use wrap.
ERR_TYPE_MISMATCHAn argument is not an integer (decimals too).

get​

The get(x, y) method gets the value of a specific cell.

var value = map.get(5, 3)
println("Value at (5,3): ", value)
ParameterTypeDescription
xnumberX coordinate (column)
ynumberY coordinate (row)

Returns the integer value (0-255) stored in the cell.

  • It accepts decimals, and rounds them down: get(1.9, 0.2) is cell (1, 0), and -0.5 is -1.
  • With wrapX or wrapY, a coordinate outside the map wraps around on that axis: in a map 4 wide with wrapX, get(5, 0) is get(1, 0).
  • Outside the map, without wrap on that axis, it throws ERR_INDEX_OUT_OF_RANGE.

width / height​

The width() and height() methods return the grid dimensions.

println("Width:  ", map.width())
println("Height: ", map.height())

clear​

The clear(value) method sets all cells to the specified value.

map.clear(0)   // Clear the entire map
map.clear(1) // Fill everything with walls
ParameterTypeDescription
valueintegerValue to assign to all cells (0-255)

A value outside 0–255 throws ERR_INVALID_VALUE.

On Windows, draw.tilemap does not see the clear

On Windows, after clear, draw.tilemap keeps drawing the old map until you change a cell with set. Do a set of any cell after the clear, for example map.set(0, 0, map.get(0, 0)).

clone​

The clone() method creates a deep copy of the grid.

var copy = map.clone()
copy.set(0, 0, 99)
println(map.get(0, 0)) // Original value unchanged
println(copy.get(0, 0)) // 99

It returns a new GridMap with the same values as the original, and also its position, its tile size, wrap, bounce, empty tile and its tileset (the same image). solids and platforms are copied: changing the copy's does not touch the original's.

tSize​

The tSize() / tSize(width, height) method works as a getter/setter for the size in pixels of each tile. It is used by draw.tilemap, draw.scene, hits and sprite.hitsGrid: without it, they neither draw nor collide.

map.tSize(16, 16)            // Set 16x16 px tiles
var size = map.tSize() // Get -> [16, 16]
println(size[0], "x", size[1])
UsageParametersReturn
Getternone[tileWidth, tileHeight] (list)
Setterwidth:integer, height:integer (1-255)nil

A new map has [0, 0], and one loaded from a .gmap, the file's. Outside 1–255 it throws ERR_INVALID_VALUE.

tEmpty​

The tEmpty() / tEmpty(value) method works as a getter/setter for the empty tile index. Cells with that value are not drawn when rendering the map.

map.tEmpty(0)           // Tile 0 is the "empty" one (default: 255)
println(map.tEmpty()) // 0
UsageParametersReturn
Getternoneinteger (0-255)
Settervalue:integertrue

Cells with 255 are never drawn, whatever tEmpty is.

count​

The count(value) method counts how many cells have a specific value.

var wallCount = map.count(1)
println("Walls: ", wallCount)
ParameterTypeDescription
valueintegerCell value to search for (0-255)

Returns an integer with the number of cells that have that value.

has​

The has(value) method checks whether at least one cell has the given value.

if (map.has(5)) then
println("There are still dots on the map")
end
ParameterTypeDescription
valueintegerCell value to search for (0-255)

Returns true if at least one cell has that value, false otherwise.

find​

The find(value) method searches for all cells with a specific value and returns their coordinates.

var positions = map.find(5)   // All cells with value 5
for (var i = 0; i < positions.size(); i += 2)
var x = positions[i]
var y = positions[i + 1]
println("Dot at (", x, ", ", y, ")")
end
ParameterTypeDescription
valueintegerCell value to search for (0-255)

Returns a flat list [x1, y1, x2, y2, ...] with the coordinates of all matches (in row-by-row order). Empty list if none found. It is useful, for example, to place the enemies where the tilemap editor marked their cells.

around​

The around(x, y [, use8 [, allowWrap]]) method gets the values of the cells neighboring a position.

var neighbors4 = map.around(5, 3)              // 4 cardinal neighbors
var neighbors8 = map.around(5, 3, true) // 8 neighbors (+ diagonals)
var neighborsW = map.around(0, 0, false, true) // 4 neighbors with toroidal wrap
ParameterTypeDescription
xintegerX coordinate of the center cell
yintegerY coordinate of the center cell
use8boolean(Optional) If true, includes the 4 diagonals. Default: false
allowWrapboolean(Optional) If true, edges connect. Default: false

Returns a list with the values of the valid neighboring cells, in this order: up, down, left, right and, with use8, up-left, up-right, down-left and down-right. Without wrap, the cells outside the bounds are skipped (the list may have fewer than 4/8 elements), so on an edge the position in the list does not tell which neighbor it is. It uses the allowWrap argument, not wrapX or wrapY.

see​

The see(x1, y1, x2, y2, blockers) method checks whether there is a clear line of sight between two cells using Bresenham's algorithm.

// Can the player see the ghost? Walls (value 1) block the view
if (map.see(playerX, playerY, ghostX, ghostY, 1)) then
println("The ghost can see you")
end

// Multiple blocking values
if (map.see(playerX, playerY, ghostX, ghostY, [1, 2, 3])) then
println("Line of sight is clear")
end
ParameterTypeDescription
x1, y1integerOrigin coordinates
x2, y2integerDestination coordinates
blockersinteger | list | arrayCell values that block the line of sight

Returns true if the path between origin and destination does not pass through any blocking cell, false otherwise. The origin and destination cells do not count: even if they are walls, they do not block. It does not use wrap.

path​

The path(sx, sy, gx, gy) method finds the shortest path between two points, moving in the four directions (BFS).

Pathfinding methods automatically derive walkable cells from the map's solids property (walkable = NOT solid). Toroidal wrap is taken from the wrapX and wrapY properties. If different behavior is needed, dMap and nearest accept optional parameters to manually specify walkable cells and wrap.

use GridMap

var map = GridMap.new(8, 5)
map.solids = [1]

// A small map with two walls
// . . . . . . . .
// . # # # # # . .
// . . . . . . . .
// . . # # # # # .
// . . . . . . . .
for (var x = 1; x <= 5; x++) map.set(x, 1, 1) end
for (var x = 2; x <= 6; x++) map.set(x, 3, 1) end

var route = map.path(0, 0, 7, 4)
if (route != nil) then
println(len(route) div 2, " cells")
println(route)
else
println("No path")
end
12 cells
[0, 0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 7, 1, 7, 2, 7, 3, 7, 4]
ParameterTypeDescription
sxintegerStarting X coordinate
syintegerStarting Y coordinate
gxintegerGoal X coordinate
gyintegerGoal Y coordinate

Returns a flat list [x0, y0, x1, y1, ..., gx, gy] with the path coordinates, starting with the origin, or nil if no path exists or if the start/goal are not walkable. If the origin is already the goal, the path is that cell: [x, y]. With an origin or a goal outside the map it throws ERR_INDEX_OUT_OF_RANGE, and with decimals, ERR_TYPE_MISMATCH.

Walkable cells

Walkable cells are automatically calculated as the complement of map.solids. Make sure to assign map.solids before calling path. Wrap is taken from map.wrapX and map.wrapY.

dMap​

The dMap(targets [, walkable [, allowWrap]]) method builds a BFS distance map from all cells that have the target values.

// Distance map to cell type 5 (dots/pellets)
map.solids = [1]
var distances = map.dMap(5)

// Query distance from any position
var dist = distances.get(playerX, playerY)
println("Distance to nearest dot: ", dist)

// With multiple target types
var distances2 = map.dMap([5, 6])

// With manual walkable list and wrap
var distances3 = map.dMap(5, [0, 5, 6], true)
ParameterTypeDescription
targetsinteger | list | arrayCell value(s) from which distance is measured
walkableinteger | list | array(Optional) Cell values that can be traversed
allowWrapboolean(Optional) Enables toroidal movement

Returns a new GridMap where each cell contains the minimum distance to the nearest target, in steps of one cell. Target cells are 0 and unreachable ones, 255. Distances of 254 or more stay at 254. The new map is only a distance map: it has no tile size, tileset or solids.

Automatic mode

With a single argument, dMap derives walkable cells from map.solids (walkable = NOT solid) and wrap from map.wrapX/map.wrapY.

nearest​

The nearest(targets, sx, sy [, walkable [, allowWrap]]) method finds the shortest path from a position to the nearest cell that has one of the target values.

// Player position: which dot is closest?
map.solids = [1]
var route = map.nearest(5, playerX, playerY)

if (route != nil) then
// The last pair of values is the position of the found target
var n = route.size()
var tx = route[n - 2]
var ty = route[n - 1]
println("Nearest dot at (", tx, ", ", ty, ")")
println("Steps: ", n div 2 - 1)
else
println("No reachable dots")
end
ParameterTypeDescription
targetsinteger | list | arrayCell value(s) to search for
sxintegerStarting X coordinate
syintegerStarting Y coordinate
walkableinteger | list | array(Optional) Walkable cell values
allowWrapboolean(Optional) Enables toroidal movement

Returns a flat list [sx, sy, ..., tx, ty] with the path from origin to the nearest target cell, or nil if no target is reachable (also if the origin is a wall). If the origin is already a target, it returns [sx, sy].

dir​

The dir(x, y [, use8]) method gets the best direction to follow from a position on a distance map (the result of dMap). It lets you move an agent toward the nearest target without recalculating the full path every frame.

map.solids = [1]
var distances = map.dMap(5) // Distance map to the dots

// Ghost AI: move toward the nearest dot
var dir = distances.dir(ghostX, ghostY)
if (dir == 1) then ghostY -= 1 // Up
elsif (dir == 2) then ghostY += 1 // Down
elsif (dir == 3) then ghostX -= 1 // Left
elsif (dir == 4) then ghostX += 1 // Right
end
ParameterTypeDescription
xintegerCurrent X coordinate
yintegerCurrent Y coordinate
use8boolean(Optional) If true, also considers diagonals. Default: false

Returns an integer indicating the optimal direction:

ValueDirection
0No movement (already at target or unreachable cell)
1Up (y-1)
2Down (y+1)
3Left (x-1)
4Right (x+1)
5Up-left (x-1, y-1) — 8 directions only
6Up-right (x+1, y-1) — 8 directions only
7Down-left (x-1, y+1) — 8 directions only
8Down-right (x+1, y+1) — 8 directions only

It picks the neighbor with the lowest distance; on a tie, the first one in that order. It does not use wrap: at the edge of the map it does not wrap around. With 8 directions it only looks at the distance of the diagonal cell, not at whether the two cells beside it are walls: it can cut a corner.

hits​

The hits(sprite_or_list) method resolves the physical collision between one or more sprites and the map. It reads the solids and platforms properties directly from the GridMap to determine which tiles block and which ones are one-way platforms.

map.solids = [1, 2]
map.platforms = [3]

// Collision with one sprite
var hitTiles = map.hits(player)

// Collision with several sprites at once
var all = [player, enemy1, enemy2]
var hitList = map.hits(all)
ParameterTypeDescription
sprite_or_listSprite | listSingle sprite or list of sprites to resolve

Returns a list with the values of the tiles it collided with, empty if there are none.

Unlike sprite.hitsGrid, which only asks, hits moves the sprites: it pushes out of the walls those that got into them and takes away their velocity against the wall (or makes them bounce, with bouncy). For that:

  • It only works with DYNAMIC bodies. Sprites of other types in the list are skipped; that is why you can pass it the whole list.
  • The map needs its tile size (tSize). Without it, it does nothing.
  • It uses the path from prevX, prevY: a fast sprite does not go through a thin wall. In a scene, scene.update() keeps them up to date.
  • Call it every frame, after scene.update(): the scene's physics moves the bodies, but it does not look at the map (the Scene module explains it).
  • It sets onGround() to true on the sprites the map holds up (sprite.onGround).

It accepts a sprite or a list; with an array or anything else, it throws ERR_TYPE_MISMATCH.

Complete example​

A chaser (red) goes after the player (green) through a maze: every six frames it computes with dMap the distances to the player's cell and takes a step with dir. The walls are drawn with rectangles, so it needs no images.

use Display, GridMap

const CELL = 20

// The maze: 1 is wall and 0 is corridor
var rows = [
"111111111111",
"100000010001",
"101111010101",
"100001000101",
"111101111101",
"100000000001",
"101111011101",
"100000010001",
"111111111111"
]
var map = GridMap.new(12, 9)
for (var y = 0; y < 9; y++)
for (var x = 0; x < 12; x++)
if (rows[y][x] == "1") then map.set(x, y, 1) end
end
end
map.solids = [1]

var playerX = 10 // the player, standing still
var playerY = 7
var chaserX = 1 // the chaser
var chaserY = 1
var route = map.path(chaserX, chaserY, playerX, playerY)
println("Shortest path: ", len(route) div 2 - 1, " steps") // 15 steps

// dir() returns 0 (still), 1 up, 2 down, 3 left or 4 right
var dx = [0, 0, 0, -1, 1]
var dy = [0, -1, 1, 0, 0]

Display.open(240, 180)
Display.fps = 30
var draw = Display.draw
for (var f = 0; f < 300; f++) // 10 seconds
if (f % 6 == 0) then // one step every 6 frames
map.set(playerX, playerY, 9) // the player's cell, the dMap target
var dist = map.dMap(9)
map.set(playerX, playerY, 0)
var d = dist.dir(chaserX, chaserY)
chaserX = chaserX + dx[d]
chaserY = chaserY + dy[d]
end
draw.color = 0x3050A0
for (var y = 0; y < 9; y++)
for (var x = 0; x < 12; x++)
if (map.get(x, y) == 1) then
draw.rect(x * CELL, y * CELL, CELL, CELL, true)
end
end
end
draw.color = 0x40FF40
draw.rect(playerX * CELL + 4, playerY * CELL + 4, 12, 12, true)
draw.color = 0xFF4040
draw.rect(chaserX * CELL + 4, chaserY * CELL + 4, 12, 12, true)
Display.update()
end
println("The chaser is at (", chaserX, ", ", chaserY, ")") // (10, 7)
Display.close()

To draw the map with images instead of rectangles, use draw.tilemap (one tile per cell, with a tileset) or draw.gridBitmap (one image on the cells with a value).

Pitfalls and known issues​

  • path, dMap and nearest connect both axes as soon as one has wrap (see Toroidal movement).
  • On Windows, draw.tilemap does not see what clear changes until the next set (see clear).
  • set does not accept decimals, and get does. If you work out the cell by dividing pixels by the tile size, use div for set (map.set(px div 16, py div 16, 1)).
  • solids and platforms keep your list, not a copy: if you change the list afterwards, it changes in the map too.
  • count, has and find do not check the value: one outside 0–255 wraps around in a byte (count(256) counts the 0s).
  • hits only moves DYNAMIC bodies, and only with the tile size set (see hits).

All the known issues of JARU, together, are in Known issues.