Lua for Game Development โ€” Chapter 9: Scenes, Levels, World Systems & Spawning

A complete and production-ready guide to scene flow, level structure, tilemaps, world systems, spawning, checkpoints, streaming, saving, and serialization in Lua.

Modern games have scenes, levels, maps, spawn waves, world streaming, checkpoints, and persistent game states.
Luaโ€™s lightweight and data-driven nature makes it perfect for orchestrating:

  • Level loading/unloading
  • Map systems (tile-based or object-based)
  • Trigger zones and events
  • Spawn systems (enemies, loot, NPCs)
  • World progression
  • Checkpoints/Save systems
  • Streaming large worlds
  • Scene transitions & layering

In this chapter, we build a full world architecture that can be extended to RPGs, shooters, platformers, adventure games, and SLG games.

1. Scene Architecture Review

Earlier we built:

  • A stack-based SceneManager
  • Update/render calls per scene
  • Push/pop/switch patterns (menus, pause menu, gameplay)

Now weโ€™ll extend scenes into level-aware world systems.

2. Level Structure

A typical Lua-friendly level folder:

levels/
level1/
map.lua
objects.lua
spawns.lua
scripts.lua
level2/
map.lua
objects.lua
spawns.lua

map.lua usually contains:

  • tile data
  • collision tiles
  • background decorations
  • tile-to-world mapping
  • tile properties

2.1 Example Tilemap Data (Lua table)

return {
  width = 20,
  height = 12,
  tilesize = 32,
  layers = {
    ground = {
      {1,1,1,1,1,...},
      {1,0,0,0,1,...},
      ...
    },
    collision = {
      {1,1,1,1,1,...},
      {1,0,0,0,1,...},
      ...
    },
  }
}

Lua table = JSON but faster and easily editable.

3. Loading a Level (LevelManager)

local LevelManager = {}
LevelManager.__index = LevelManager

function LevelManager.new(game)
  return setmetatable({
    game = game,
    current = nil,
    map = nil,
    objects = {},
  }, LevelManager)
end

function LevelManager:load(name)
  -- unload previous level
  self.objects = {}

  -- load new resources
  self.map = require("levels."..name..".map")
  local objs = require("levels."..name..".objects")
  local spawns = require("levels."..name..".spawns")

  -- instantiate objects
  for _, obj in ipairs(objs) do
    local e = self.game:create_entity(obj)
    table.insert(self.objects, e)
  end

  -- do spawn registration
  self.spawns = spawns

  self.current = name
end

function LevelManager:update(dt)
  -- update enemies, items, platforms, scripts
  for _, e in ipairs(self.objects) do
    e:update(dt)
  end
end

return LevelManager

This matches how many Lua-powered engines load levels.

4. Spawn System

Spawns usually map to events:

  • Player enters a region
  • Time-based waves
  • Triggered by scripts
  • Killed all enemies โ†’ next wave
  • Random spawns

4.1 Example spawn definitions

levels/level1/spawns.lua:

return {
  {
    type="enemy",
    pos={x=200, y=300},
    wave=1
  },
  {
    type="enemy",
    pos={x=450, y=300},
    wave=2
  },
  {
    type="item",
    pos={x=500, y=320},
    wave=1
  }
}

4.2 Spawn Manager

local Spawner = {}
Spawner.__index = Spawner

function Spawner.new(game)
  return setmetatable({
    pending = {},
    active_wave = 1
  }, Spawner)
end

function Spawner:load(spawns)
  self.pending = spawns
end

function Spawner:start_wave(wave)
  self.active_wave = wave
  for i = #self.pending, 1, -1 do
    local s = self.pending[i]
    if s.wave == wave then
      self.game:create_entity(s)
      table.remove(self.pending, i)
    end
  end
end

return Spawner

This gives:

  • wave-based spawns
  • progressive difficulty
  • scripted events

5. Trigger Zones (Event Regions)

Triggers activate scripts:

  • Area entry
  • Pickups
  • Boss room
  • Checkpoints
  • Cutscene starts
  • Dialogue triggers

5.1 Trigger Definition

levels/level1/objects.lua:

{
  type="trigger",
  shape={x=300, y=200, w=100, h=80},
  script="start_boss_cutscene"
}

5.2 Trigger Check

local function check_trigger(trigger, player)
  local a = trigger.shape
  local b = player.hitbox
  return a.x < b.x+b.w and b.x < a.x+a.w and
         a.y < b.y+b.h and b.y < a.y+a.h
end

5.3 Run Trigger Scripts

if check_trigger(t, player) then
  local scr = require("levels."..level_name..".scripts."..t.script)
  scr.run(player, level)
end

Triggers are the basis of:

  • level scripting
  • mission systems
  • cutscenes
  • boss phases
  • puzzle conditions
  • unlockable events

6. Level Script Example

start_boss_cutscene.lua:

local script = {}

function script.run(player, level)
  timeline:add(function()
    ui:fade_in()
    wait(0.5)

    show_dialogue({
      {speaker="Hero", text="I feel something..."},
      {speaker="Hero", text="A powerful enemy awaits."}
    })

    camera:pan_to(600, 200, 1.2)
    wait(1.2)

    level.spawner:start_wave("boss")

    ui:fade_out()
  end)
end

return script

Lua makes script-driven level design incredibly natural.

7. Tilemap Systems

Tilemaps give:

  • fast rendering
  • simple collision
  • low memory
  • data-driven design

7.1 Tile to world conversion

function tile_to_world(i, j, tilesize)
  return (i-1)*tilesize, (j-1)*tilesize
end

7.2 Tile collision checking

function solid(map, tx, ty)
  return map.layers.collision[ty] and map.layers.collision[ty][tx] == 1
end

7.3 Entity collision resolution

if solid(map, tx, ty) then
  entity.x = old_x
  entity.y = old_y
end

This allows grid-based platformers, RPGs, strategy SLGs, etc.

8. World Streaming (For Large Maps)

To simulate open worlds, only load nearby chunks.

8.1 Chunk-based world

world/
  chunk_0_0.lua
  chunk_0_1.lua
  chunk_1_0.lua

8.2 Stream chunks near player

local function get_chunk_id(x, y, size)
  return math.floor(x/size), math.floor(y/size)
end

function World:update(player)
  local cx, cy = get_chunk_id(player.x, player.y, self.chunk_size)

  for dx=-1,1 do
    for dy=-1,1 do
      self:load_chunk(cx+dx, cy+dy)
    end
  end
end

This is used in:

  • Terraria-like games
  • Survival sandboxes
  • Open-world RPGs

9. Checkpoints & Save States

Save system must preserve:

  • player position
  • hp, stats, buffs
  • inventory
  • quest progress
  • world state (enemies killed, items collected)
  • active triggers & wave states

9.1 Save Table

local save = {
  player = {
    x = player.x,
    y = player.y,
    hp = player.stats.hp.base,
  },
  world = {
    level = level.name,
    wave = spawner.active_wave,
    triggers = triggered_flags,
  }
}

9.2 Serialize (from Chapter 4)

file:write("return " .. serialize(save))

9.3 Load Save

local data = dofile("save.lua")
load_player(data.player)
level_manager:load(data.world.level)
spawner:start_wave(data.world.wave)

10. Scene and Level Transitions

Examples:

  • enter cave โ†’ new level
  • pass a door โ†’ next room
  • finish boss โ†’ outro scene
  • teleport โ†’ new map section

10.1 Transition Function

function goto_level(name)
  ui:fade_in()
  wait(0.4)

  level_manager:load(name)

  ui:fade_out()
end

Used everywhere in modern Lua-platform games.

11. Complete Example โ€” Full Level Script

Here is a complete level script that ties everything together.

timeline:add(function()
  ui:fade_in()
  wait(0.3)

  show_dialogue({
    {speaker="Guide", text="Welcome to the Forgotten Forest."}
  })

  ui:fade_out()

  -- spawn first wave
  spawner:start_wave(1)
  wait(5)

  show_dialogue({
    {speaker="Guide", text="More enemies approach!"}
  })

  spawner:start_wave(2)
  wait(6)

  -- final boss
  show_dialogue({
    {speaker="Guide", text="Prepare yourself..."}
  })

  spawner:start_wave("boss")
end)

This is a real production-style scripted level.

12. Summary of Chapter 9

You now know how to build:

  • Level structure & resource loading
  • Tilemap systems
  • Collision maps
  • Trigger zones & scripts
  • Wave & spawn management
  • Level transitions
  • World streaming
  • Checkpoints & save/load
  • Large-scale scene orchestration

Luaโ€™s data-driven structure makes world design incredibly efficient and readable.

Keep Reading

Follow the engineering thread

Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.

Join newsletter Browse articles