Animation and cinematic systems transform raw gameplay into a polished, emotional experience.
Lua is ideal for sequencing, orchestration, animation management, and camera scripting because:
- Coroutine syntax is perfect for timelines.
- Hot reload accelerates iteration.
- Data-driven tables make timelines readable.
- Engines expose animation/camera APIs easily.
In this chapter we build:
- Animation state machines
- Sprite animation system
- Camera follow/zoom/lerp
- Timeline scripting engine
- Dialogue system
- Cutscenes (with coroutines)
- Screen/camera effects (shake, flash, fade)
- Sequencers for complex cinematic flows
1. Animation State Machines (ASM)
Most games rely on an animation finite-state-machine:
idle โ run โ jump โ fall โ land โ idle
Lua makes this trivial.
1.1 Animation State Machine
local ASM = {}
ASM.__index = ASM
function ASM.new(initial)
return setmetatable({
state = initial,
states = {},
sprite = nil
}, ASM)
end
function ASM:set_sprite(sprite)
self.sprite = sprite
end
function ASM:add(name, cfg)
self.states[name] = cfg
end
function ASM:update(entity, dt)
local cfg = self.states[self.state]
if cfg and cfg.update then
local new = cfg.update(entity, dt)
if new and new ~= self.state then
self.state = new
if self.states[new].enter then
self.states[new].enter(entity)
end
end
end
if self.sprite then
self.sprite:play(cfg.anim)
end
end
return ASM
1.2 Example ASM Setup
local asm = ASM.new("idle")
asm:add("idle", {
anim="idle",
update=function(e, dt)
if e.vel.x ~= 0 then return "run" end
end
})
asm:add("run", {
anim="run",
update=function(e, dt)
if e.vel.x == 0 then return "idle" end
end
})
This powers any sprite-based character.
2. Sprite Animation System (Frame Timings)
A simple sprite animator using a Lua table:
local Sprite = {}
Sprite.__index = Sprite
function Sprite.new(frames)
return setmetatable({
anims = frames,
current = nil,
time = 0,
frame_index = 1,
}, Sprite)
end
function Sprite:play(name)
if self.current ~= name then
self.current = name
self.time = 0
self.frame_index = 1
end
end
function Sprite:update(dt)
local anim = self.anims[self.current]
if not anim then return end
self.time = self.time + dt
if self.time > anim.rate then
self.time = self.time - anim.rate
self.frame_index = self.frame_index + 1
if self.frame_index > #anim.frames then
self.frame_index = 1
end
end
end
function Sprite:draw(x, y)
local anim = self.anims[self.current]
if anim then
draw_quad(anim.frames[self.frame_index], x, y)
end
end
return Sprite
2.1 Animation definitions
local frames = {
idle = {frames={1,2,3}, rate=0.25},
run = {frames={4,5,6,7}, rate=0.15},
}
3. Camera Systems (Follow, Smooth, Shake, Zoom)
Camera controls visibility and drama.
3.1 Basic Camera
local Camera = {}
Camera.__index = Camera
function Camera.new()
return setmetatable({
x=0, y=0,
smooth=0.15,
shake_time=0,
shake_mag=0,
zoom=1
}, Camera)
end
function Camera:follow(target, dt)
self.x = self.x + (target.x - self.x) * self.smooth
self.y = self.y + (target.y - self.y) * self.smooth
end
function Camera:update(dt)
if self.shake_time > 0 then
self.shake_time = self.shake_time - dt
end
end
function Camera:get_offset()
local ox, oy = 0, 0
if self.shake_time > 0 then
ox = (math.random() - 0.5) * self.shake_mag
oy = (math.random() - 0.5) * self.shake_mag
end
return self.x + ox, self.y + oy
end
return Camera
3.2 Camera Shake
function Camera:shake(mag, time)
self.shake_mag = mag
self.shake_time = time
end
Used for:
- explosions
- hits
- cinematic moments
4. Timeline Scripting (Key Feature)
Luaโs coroutines make timeline scripting extremely clean.
4.1 Timeline Engine
local function wait(sec)
local start = os.clock()
while os.clock() - start < sec do
coroutine.yield()
end
end
local Timeline = {}
Timeline.__index = Timeline
function Timeline.new()
return setmetatable({tasks={}}, Timeline)
end
function Timeline:add(fn)
table.insert(self.tasks, coroutine.create(fn))
end
function Timeline:update()
local alive = {}
for _, t in ipairs(self.tasks) do
if coroutine.status(t) ~= "dead" then
coroutine.resume(t)
if coroutine.status(t) ~= "dead" then
table.insert(alive, t)
end
end
end
self.tasks = alive
end
return Timeline
4.2 Timeline Scripting Example
timeline:add(function()
camera:shake(5, 0.5)
wait(0.5)
play_animation("boss_roar")
wait(1.2)
camera:shake(3, 0.3)
spawn_effect("dust", boss.x, boss.y)
wait(0.5)
print("Cutscene finished.")
end)
This is exactly how many AAA engines script cutscenes.
5. Dialogue System
Lua tables + timeline = perfect dialogue scripting.
5.1 Dialogue Data
local dialogue = {
{speaker="Alice", text="What is that sound?"},
{speaker="Bob", text="...Something big is coming."},
{speaker="Alice", text="Ready your weapons!"},
}
5.2 Dialogue Runner
local function show_dialogue(d)
for _, line in ipairs(d) do
ui:show_dialog(line.speaker, line.text)
wait(1.6)
end
ui:hide_dialog()
end
6. Cutscene System
Combine:
- timeline
- camera
- dialogue
- animation
- SFX
6.1 Cutscene Script
timeline:add(function()
ui:fade_in()
show_dialogue(dialogue)
camera:shake(8, 1.2)
enemy:play("roar")
wait(1)
camera:zoom_to(1.5, 0.5)
wait(0.5)
ui:fade_out()
end)
This script is readable and designer-friendly.
7. Cinematic Effects
These small effects provide huge impact.
7.1 Screen Flash
local flash = {alpha=0}
function flash:play()
self.alpha = 1
end
function flash:update(dt)
self.alpha = math.max(0, self.alpha - dt * 2)
end
function flash:render()
if self.alpha > 0 then
draw_rect(0,0,800,600,1,1,1,self.alpha)
end
end
7.2 Camera Zoom Tween
function Camera:zoom_to(z, t)
local start = self.zoom
local elapsed = 0
while elapsed < t do
elapsed = elapsed + (1/60)
local p = elapsed / t
self.zoom = start + (z - start) * p
coroutine.yield()
end
end
7.3 Camera Pan
function Camera:pan_to(tx, ty, t)
local sx, sy = self.x, self.y
local elapsed = 0
while elapsed < t do
elapsed = elapsed + (1/60)
local p = elapsed / t
self.x = sx + (tx - sx) * p
self.y = sy + (ty - sy) * p
coroutine.yield()
end
end
8. Putting It Together: A Full Cutscene Example
timeline:add(function()
ui:fade_in()
wait(0.6)
show_dialogue({
{speaker="Hero", text="This is it..."},
{speaker="Hero", text="The final battle begins."},
})
wait(0.5)
camera:pan_to(boss.x, boss.y - 200, 1.5)
camera:zoom_to(1.3, 1.0)
boss.sprite:play("roar")
camera:shake(10, 1)
spawn_effect("roar_effect", boss.x, boss.y)
wait(1)
ui:fade_out()
end)
This is 100% production-ready.
9. Summary of Chapter 8
You learned how to implement:
- Animation state machines
- Frame-based sprite animation
- Smooth camera follow
- Camera shake, zoom, pan
- Coroutine-based timeline engine
- Dialogue scripting
- Cutscene orchestration
- Cinematic effects
- Tween-based UI/camera transitions
Lua is one of the best languages in the world for timeline and cinematic scripting.
Keep Reading
Follow the engineering thread
Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.