Game Dev
By the end of this lesson you'll understand the game loop that powers every game, how to move things smoothly with delta time, and how to model players and enemies as tables — the exact pattern Roblox and LÖVE developers use every day.
Part of the free Lua course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
💡 Real-World Analogy
A game is a flipbook animation . Each page is one still picture — a frame — and flipping through them fast enough (around 60 pages a second) tricks your eye into seeing smooth motion. The game loop is your hand flipping the pages: every flip it reads what the player pressed, nudges everything a little (the update ), and draws the new picture (the draw ). The catch: some hands flip faster than others. Delta time is how you account for that — instead of "move 5 pixels per page", you say "move 150 pixels per second ", so a fast flipper and a slow flipper end up in exactly the same place.
1. Why Lua Rules Game Scripting
Game studios write their heavy machinery — rendering, physics, audio — in fast languages like C++. But they need a small, friendly language to script the actual gameplay : what an enemy does when you get close, what a button does when you press it. Lua is the industry's favourite choice for that job because it's tiny, fast, and easy to embed. You've already learned the whole language; this is where it pays off.
Where Lua Powers Games
- Roblox — every experience is scripted in Luau , Roblox's Lua dialect (tens of millions of creators)
- LÖVE (Love2D) — a free framework for making 2D games entirely in Lua
- Defold — a complete cross-platform game engine scripted in Lua
- World of Warcraft — its UI addons and macros are written in Lua
- Garry's Mod — game modes and addons are all Lua
Different engines, one language. The game loop and table-based entities you're about to learn are the same idea in all of them — only the function names change.
2. The Game Loop: update and draw
Every game is an infinite loop that, each frame , reads input, updates the world, and redraws the screen. You don't write that loop yourself — the framework runs it and calls your functions at the right moments. In LÖVE there are three: love.load() runs once at startup, love.update(dt) runs every frame to change state, and love.draw() runs every frame to paint it. The dt handed to update is delta time — the seconds elapsed since the last frame — and it's the key to smooth movement.
The golden rule lives on the movement line: player.x = player.x + player.speed * dt . Because speed is measured in pixels per second and dt is seconds , multiplying them gives pixels this frame . Drop the * dt and your game speeds up on fast computers — the single most common beginner bug in game code.
3. Position, Velocity & Simple Physics
Most movement boils down to two pieces of data per object: its position (where it is, as x and y ) and its velocity (how fast that position changes, also as x and y ). Each frame you nudge the position by the velocity, scaled by dt . Add a constant downward velocity each frame and you've built gravity. A vector here is nothing fancy — just a little table with an x and a y .
4. Entities as Tables
In a real game you have many objects — a player, enemies, bullets. Each one is a table holding its own fields (its components : position, health, and so on), and you keep them all in one list so the game loop can update and draw them together. Watch the indexing carefully: Lua lists are 1-based , so the first entity is at index 1 , and ipairs walks them in order starting there.
5. Beyond LÖVE: a Roblox Script
The same skills carry to other engines — only the API differs. Roblox uses Luau and an event-driven style: instead of one big update , you connect functions to events like "a player joined". Here's a classic Roblox server script that hands every new player a coin counter. Notice it's the same Lua you know — tables, functions, string concatenation — wrapped around Roblox's objects.
6. 🎯 Your Turn
Now you fill in the gaps. Replace each ___ using the -- 👉 hint, then run the code and check it against the -- ✅ Expected comment. The first exercise nails the delta-time rule; the second nails 1-based entity loops — the two patterns this whole lesson rests on.
Next, count the survivors in an entity list the idiomatic, 1-based way:
Pro Tip
When you remove entities mid-loop (a dead enemy, an off-screen bullet), iterate the list backwards with for i = #list, 1, -1 do . Removing while going forwards shifts later items down and makes the loop skip one — going backwards sidesteps that entirely.
Common Errors (and the fix)
- Movement without delta time: player.x = player.x + speed moves a fixed amount per frame , so the game runs faster on a 144 Hz screen than a 30 Hz one. Always scale by dt : player.x = player.x + speed * dt .
- Treating lists as 0-based: Lua tables start at index 1 . Writing for i = 0, #entities do reads entities[0] , which is nil , and you get "attempt to index a nil value" . Use for i = 1, #entities or ipairs(entities) .
- Accidental globals as state: forgetting local ( score = 0 ) makes a global any script can overwrite, which causes baffling bugs across files. Keep game state local or inside a table you pass around on purpose.
- Setting up state in update instead of load : putting player = { x = 0 } inside love.update resets it 60 times a second, so nothing ever moves. Initialise once in love.load .
- Removing items while looping forwards: table.remove inside a forward ipairs loop shifts the rest down and skips the next item. Loop backwards ( for i = #list, 1, -1 do ) when deleting.
Quick Reference
Task
Code (LÖVE)
When it runs
Set up state
function love.load() end
once
Update world
function love.update(dt) end
every frame
Draw screen
function love.draw() end
Key held?
love.keyboard.isDown("up")
in update
Smooth move
x = x + speed * dt
Add entity
table.insert(list, e)
any time
Loop entities
for _, e in ipairs(list)
1-based
Frequently Asked Questions
Mini-Challenge: A Bouncing Ball
No blanks this time — just a brief and an outline. Write the whole thing yourself with plain Lua (so you can run it anywhere and print the positions), then check it against the expected first line. This is the same position + velocity loop a real game uses, minus the graphics.
Where to Go From Here
Free resources to build real games
- LÖVE (Love2D) — make 2D games in Lua, free and cross-platform
- Roblox Studio — build and publish multiplayer games with Luau, free
- Defold — a full engine scripted in Lua, free
- Programming in Lua — the official book, free online
Project ideas to practise on
Level
Project
Beginner
Pong, Snake, Flappy Bird clone
Intermediate
Side-scrolling platformer, top-down shooter
Advanced
Roblox obby, tower defence, procedural dungeon
🎉 Course Complete!
- ✅ Lua powers real games — Roblox/Luau, LÖVE, Defold, World of Warcraft, Garry's Mod
- ✅ The game loop is load once, then update and draw every frame
- ✅ Multiply movement by dt so speed is the same at any frame rate
- ✅ Entities are tables, kept in one list and iterated with ipairs (1-based!)
- ✅ Position + velocity (+ a constant for gravity) is all you need for simple physics
- ✅ You've finished the Lua course — from print() all the way to game development. Pick a project above, open LÖVE or Roblox Studio, and build it. 🌙
Practice quiz
Each frame, the game loop reads input, then does what next?
- Draws, then updates
- Updates the world, then draws
- Sleeps
- Saves the game
Answer: Updates the world, then draws. The loop updates the world, then draws it, every frame.
What does the dt passed to love.update mean?
- Draw time
- Display type
- Delta time: seconds since the last frame
- Data table
Answer: Delta time: seconds since the last frame. dt is delta time, the seconds elapsed since the previous frame.
Why multiply movement by dt ?
- To save memory
- To slow the game down
- To round positions
- To make movement frame-rate-independent
Answer: To make movement frame-rate-independent. Scaling by dt keeps speed in units-per-second regardless of frame rate.
Which LÖVE callback runs once at startup?
- love.load()
- love.start()
- love.update()
- love.init()
Answer: love.load(). love.load runs once to set up the starting state.
How is a game entity usually represented in Lua?
- A class instance only
- A table of fields
- A global variable
- A string
Answer: A table of fields. Entities are plain tables holding fields like x, y, and hp.
Which dialect does Roblox use for its game logic?
- LuaJIT
- MoonScript
- Luau
- Pico-8 Lua
Answer: Luau. Roblox scripts are written in Luau, its Lua dialect.
At what index does the first entity in a Lua list sit?
- 0
- -1
- Any
- 1
Answer: 1. Lua tables are 1-based, so the first entity is at index 1.
Which iterator walks an entity list in order from index 1?
- ipairs
- pairs
- next
- each
Answer: ipairs. ipairs visits integer keys 1, 2, 3... in order.
Where should you set up starting state like the player table?
- In love.update
- In love.draw
- In love.load
- In a global at the top
Answer: In love.load. Initialise once in love.load, not in update (which runs every frame).
When removing entities during iteration, you should loop...
- Forwards with ipairs
- Twice
- In any order
- Backwards with for i = #list, 1, -1
Answer: Backwards with for i = #list, 1, -1. Looping backwards avoids skipping items as later ones shift down.
Continue this course
- Previous: Metatables and Metamethods
- Next: Strings & Pattern Matching