Lua for Game Development — Chapter 9: Scenes, Levels, World Systems & Spawning
Leeting Yan
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.