Lua for Game Development — Chapter 8: Animation Systems, Cameras, Cutscenes & Timeline Scripting
Leeting Yan
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.