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.