Tilemaps

Storing, rendering, and doing collision against tile-based maps in Chasm.

Tilemaps are grids of integers. Each integer is a tile ID, 0 for empty, anything else for solid or typed terrain. Store the map in a script-lifetime fixed array.

Map Setup

# 20 x 15 grid, 300 tiles total
@map_w    :: script = 20
@map_h    :: script = 15
@tile_size :: script = 32
@tiles    :: script = array_fixed(300, 0)

Reading and Writing Tiles

defp tile_at(tx :: int, ty :: int) :: int do
  if tx < 0 do return 1 end
  if ty < 0 do return 1 end
  if tx >= @map_w do return 1 end
  if ty >= @map_h do return 1 end
  return @tiles.get(ty * @map_w + tx)
end

defp set_tile(tx :: int, ty :: int, id :: int) do
  if tx >= 0 and ty >= 0 and tx < @map_w and ty < @map_h do
    @tiles.set(ty * @map_w + tx, id)
  end
end

Out-of-bounds reads return 1 (solid), so the map is implicitly walled.

World-to-Tile Coordinates

defp world_to_tile_x(wx :: float) :: int do
  return to_int(wx) / @tile_size
end

defp world_to_tile_y(wy :: float) :: int do
  return to_int(wy) / @tile_size
end

Rendering the Map

Only draw tiles within the camera viewport:

defp draw_map(cam_x :: float, cam_y :: float) do
  ts    :: frame = to_float(@tile_size)
  start_tx :: frame = to_int(cam_x) / @tile_size
  start_ty :: frame = to_int(cam_y) / @tile_size
  sw    :: frame = to_float(screen_width())
  sh    :: frame = to_float(screen_height())
  tiles_x :: frame = to_int(sw / ts) + 2
  tiles_y :: frame = to_int(sh / ts) + 2
  ty = 0
  while ty < tiles_y do
    tx = 0
    while tx < tiles_x do
      wx :: frame = to_float((start_tx + tx) * @tile_size)
      wy :: frame = to_float((start_ty + ty) * @tile_size)
      id :: frame = tile_at(start_tx + tx, start_ty + ty)
      if id == 1 do
        draw_rectangle(wx - cam_x, wy - cam_y, ts, ts, :dark_gray)
      end
      tx = tx + 1
    end
    ty = ty + 1
  end
end

Tile Collision

Check the four corners of an AABB against the tile grid:

defp tile_solid(wx :: float, wy :: float) :: int do
  tx :: frame = world_to_tile_x(wx)
  ty :: frame = world_to_tile_y(wy)
  return tile_at(tx, ty)
end

defp aabb_in_solid(x :: float, y :: float, w :: float, h :: float) :: int do
  # check all four corners
  if tile_solid(x,         y        ) == 1 do return 1 end
  if tile_solid(x + w - 1.0, y      ) == 1 do return 1 end
  if tile_solid(x,         y + h - 1.0) == 1 do return 1 end
  if tile_solid(x + w - 1.0, y + h - 1.0) == 1 do return 1 end
  return 0
end

Swept Movement

Move in X and Y separately so the player slides along walls:

defp move_and_slide(x :: float, y :: float,
                    vx :: float, vy :: float,
                    w :: float, h :: float,
                    dt :: float) :: float do
  # try X
  nx :: frame = x + vx * dt
  if aabb_in_solid(nx, y, w, h) == 1 do
    nx = x
  end
  # try Y
  ny :: frame = y + vy * dt
  if aabb_in_solid(nx, ny, w, h) == 1 do
    ny = y
  end
  # return as encoded pair, caller unpacks
  return nx
end

For a clean API, split into separate move_x / move_y helpers so each can return a single float without needing tuples.

Loading a Map from Data

Hard-code a small level as a flat array of ints:

defp load_level() do
  data :: frame = array_fixed(300, 0)
  # perimeter walls
  i = 0
  while i < @map_w do
    set_tile(i, 0, 1)
    set_tile(i, @map_h - 1, 1)
    i = i + 1
  end
  i = 0
  while i < @map_h do
    set_tile(0, i, 1)
    set_tile(@map_w - 1, i, 1)
    i = i + 1
  end
  # platforms
  set_tile(5, 10, 1)
  set_tile(6, 10, 1)
  set_tile(7, 10, 1)
  set_tile(12, 7, 1)
  set_tile(13, 7, 1)
end