Lua for Game Development β Chapter 10: Audio, Particles, VFX & Feedback Systems
Leeting Yan
Great gameplay is not only logic, animation, and UI.
It is FEEL.
Games become satisfying through:
- impactful audio
- juicy hit feedback
- particles & sparks
- camera shake
- screen flashes
- slow motion
- dynamic music
Lua excels at orchestrating these effects.
This chapter will show how to design a complete feedback system using Lua.
1. Audio Architecture
Audio usually comes in three categories:
β Sound Effects (SFX)
Attacks, footsteps, explosions.
β Background Music (BGM)
Looping tracks.
β Environmental Audio
Wind, rain, ambience layers.
β Dynamic Music
Boss phases, intensity-driven music.
1.1 Audio Manager
local Audio = {}
Audio.__index = Audio
function Audio.new()
return setmetatable({
sfx = {},
bgm = nil,
volume = 1.0
}, Audio)
end
function Audio:play_sfx(name, vol)
engine_play_sound(name, (vol or 1) * self.volume)
end
function Audio:play_bgm(name, volume)
self.bgm = name
engine_play_music(name, volume or 1)
end
function Audio:set_volume(v)
self.volume = v
end
return Audio
This abstracts away the engine-specific functions.
1.2 Dynamic Music System
Dynamic intensity-driven music:
local function update_music_intensity(game)
local hp_ratio = game.player.hp / game.player.max_hp
local enemies = #game.level.enemies
local intensity = 0
if hp_ratio < 0.3 then intensity = intensity + 0.4 end
if enemies > 5 then intensity = intensity + 0.6 end
engine_set_music_parameter("intensity", intensity)
end
Useful for:
- boss fights
- stealth detection
- stressful chase scenes
2. Particle Systems (Lua-Controlled)
Particles make hits, magic, fire, smoke, dust, and explosions more exciting.
2.1 Particle Object
local Particle = {}
Particle.__index = Particle
function Particle.new(x, y, vx, vy, life)
return setmetatable({
x=x, y=y,
vx=vx, vy=vy,
life=life,
age=0
}, Particle)
end
function Particle:update(dt)
self.age = self.age + dt
self.x = self.x + self.vx * dt
self.y = self.y + self.vy * dt
return self.age >= self.life
end
function Particle:draw()
draw_sprite("particle", self.x, self.y, 1 - self.age / self.life)
end
return Particle
2.2 Particle System
local ParticleSystem = {}
ParticleSystem.__index = ParticleSystem
function ParticleSystem.new()
return setmetatable({list={}}, ParticleSystem)
end
function ParticleSystem:spawn(x, y, n)
for i=1,n do
local vx = (math.random() - 0.5) * 100
local vy = (math.random() - 0.5) * 100
table.insert(self.list, Particle.new(x, y, vx, vy, 0.4))
end
end
function ParticleSystem:update(dt)
for i=#self.list,1,-1 do
if self.list[i]:update(dt) then
table.remove(self.list, i)
end
end
end
function ParticleSystem:draw()
for _, p in ipairs(self.list) do
p:draw()
end
end
return ParticleSystem
3. VFX (Visual Effects)
VFX systems combine:
- particles
- flashes
- shader effects
- screen overlays
- camera shake
- timeline sequence
Lua orchestrates these easily.
3.1 VFX Manager
local VFX = {}
VFX.__index = VFX
function VFX.new()
return setmetatable({
flashes = {},
screenshake = nil,
particles = ParticleSystem.new()
}, VFX)
end
function VFX:flash(color, duration)
table.insert(self.flashes, {color=color, dur=duration, t=0})
end
function VFX:shake(mag, dur)
camera:shake(mag, dur)
end
function VFX:spark(x, y)
self.particles:spawn(x, y, 12)
end
function VFX:update(dt)
for i=#self.flashes,1,-1 do
local f = self.flashes[i]
f.t = f.t + dt
if f.t >= f.dur then table.remove(self.flashes, i) end
end
self.particles:update(dt)
end
function VFX:draw()
for _, f in ipairs(self.flashes) do
draw_rect(0,0,800,600, f.color.r, f.color.g, f.color.b, 1 - f.t/f.dur)
end
self.particles:draw()
end
return VFX
4. Hit Feedback (Game Feel)
βGame feelβ is what makes combat impactful.
Components:
- Hit pause (freeze frames)
- Enemy knockback
- Camera shake
- Loud SFX
- Flash overlay
- Particles/sparks
- Screen jolt
- Slow motion
4.1 Hit Pause (Freeze Frames)
function hit_pause(dur)
local t = 0
while t < dur do
t = t + (1/60)
coroutine.yield()
end
end
4.2 Knockback
function apply_knockback(entity, dir, force)
entity.vx = entity.vx + dir.x * force
entity.vy = entity.vy + dir.y * force
end
4.3 Combined Hit Event
local function on_hit(attacker, target, dmg)
Audio:play_sfx("hit")
VFX:spark(target.x, target.y)
VFX:flash({r=1,g=1,b=1}, 0.1)
VFX:shake(8, 0.2)
timeline:add(function()
hit_pause(0.03)
end)
apply_knockback(target, attacker.dir, 200)
end
This integrates SFX, particles, camera shake, hit pause, and knockback.
5. Slow Motion Effects
Slow-motion is used in:
- finishing moves
- boss intros
- dramatic kills
- parries
- last-hit sequences
5.1 Global Time Scale
local Time = {scale=1}
function Time:set_scale(s)
self.scale = s
end
function Time:update(dt)
return dt * self.scale
end
return Time
5.2 Slow Motion Timeline
timeline:add(function()
Time:set_scale(0.2)
wait(0.2)
Time:set_scale(1)
end)
6. Event-Driven Audio & VFX
Use event bus for feedback.
event:on("enemy_killed", function(pos)
VFX:spark(pos.x, pos.y)
Audio:play_sfx("enemy_die")
end)
7. Background Effects (Weather, Ambience)
Examples:
- rain particle layer
- snow
- dust motes
- fog
- waterfalls
- glowing lights
- radial gradients
7.1 Continuous Weather Particles
function Weather:update(dt)
if math.random() < 0.3 then
self.particles:spawn(math.random(0,800), -10, 5)
end
self.particles:update(dt)
end
8. Putting It All Together β A Complete SFX/VFX Feedback Loop
function player_attack()
Audio:play_sfx("slash")
timeline:add(function()
hit_pause(0.02)
end)
VFX:flash({1,1,1}, 0.05)
VFX:spark(target.x, target.y)
camera:shake(6, 0.2)
end
This is essentially what powers action games like:
- Dead Cells
- Hades
- Hollow Knight
- Streets of Rage
- Slay the Spire (SFX/juice for cards)
Lua is perfect for this orchestration.
9. Summary of Chapter 10
You now understand:
- Audio management (SFX, BGM, dynamic mixing)
- Particle systems
- VFX manager
- Flash overlays
- Camera shake
- Knockback & hit pause
- Slow motion systems
- Timeline integration
- Weather & ambience layers
- Dynamic boss fight music and dramatic sequences
Lua excels at creating juice, timing, and feedback loops for modern games.