Skip to main content
    Courses/Lua/Lua for Game Development

    Lesson 6 • Final Lesson

    Lua for Game Development

    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.

    What You'll Learn

    • Name where Lua powers games (Roblox/Luau, LÖVE, Defold, WoW, Garry's Mod)
    • Describe the game loop: load, update, and draw, called every frame
    • Use delta time (dt) to make movement frame-rate-independent
    • Model entities and components as plain Lua tables
    • Apply position + velocity for simple, readable physics
    • Handle keyboard input and update a moving entity each frame

    💡 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.

    Worked example: the LÖVE game loop
    -- The GAME LOOP is the heartbeat of every game. Each frame it does
    -- three things in order: read input, UPDATE the world, then DRAW it.
    -- LÖVE (the popular Lua game framework, written "LÖVE2D") gives you
    -- three callbacks to fill in — it calls them for you ~60 times a second.
    
    function love.load()
      -- Runs ONCE at startup. Set up your starting state here.
      player = { x = 400, y = 300, speed = 200 }   -- a table = one entity
    end
    
    function love.update(dt)        -- dt = "delta time": seconds since the last frame
      -- Move right while the right arrow is held down.
      if love.keyboard.isDown("right") then
        player.x = player.x + player.speed * dt     -- * dt keeps speed frame-rate-independent
      end
      if love.keyboard.isDown("left") then
        player.x = player.x - player.speed * dt
      end
    end
    
    function love.draw()
      -- Draw a filled circle at the player's position, radius 20.
      love.graphics.circle("fill", player.x, player.y, 20)
    end
    
    -- Behaviour: a circle slides left/right at 200 pixels per second,
    -- moving the same real-world distance whether the game runs at 30 or 144 fps.
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    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.

    Worked example: position + velocity (with gravity)
    -- POSITION + VELOCITY is the simplest physics there is. A position is
    -- "where you are" (x, y); a velocity is "how fast x and y are changing".
    -- Each frame:  new position = old position + velocity * dt.
    
    -- A reusable 2D vector built from a plain table.
    local function vec(x, y) return { x = x, y = y } end
    
    local ball = {
      pos = vec(0, 100),     -- starts at the left edge, 100px down
      vel = vec(60, 0),      -- moving right at 60 px/s, not moving vertically
    }
    
    local gravity = vec(0, 90)   -- pulls 90 px/s DOWNWARD every second (y grows down)
    
    -- Simulate 3 frames of a 0.5-second step so you can read the numbers.
    local dt = 0.5
    for frame = 1, 3 do
      -- 1) gravity changes the velocity
      ball.vel.x = ball.vel.x + gravity.x * dt
      ball.vel.y = ball.vel.y + gravity.y * dt
      -- 2) velocity changes the position
      ball.pos.x = ball.pos.x + ball.vel.x * dt
      ball.pos.y = ball.pos.y + ball.vel.y * dt
      print(string.format("frame %d: pos=(%.1f, %.1f) vel=(%.1f, %.1f)",
            frame, ball.pos.x, ball.pos.y, ball.vel.x, ball.vel.y))
    end
    
    -- Behaviour (prints):
    -- frame 1: pos=(30.0, 122.5) vel=(60.0, 45.0)
    -- frame 2: pos=(60.0, 167.5) vel=(60.0, 90.0)
    -- frame 3: pos=(90.0, 235.0) vel=(60.0, 135.0)
    -- The ball drifts right at a steady 60 px/s while falling faster and faster.
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    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.

    Worked example: a list of entities
    -- ENTITIES AS TABLES: every game object is just a table of fields,
    -- and you keep them all in one list so the loop can update them together.
    -- Remember: Lua tables (lists) are 1-BASED — the first entity is [1].
    
    local entities = {}   -- our list of game objects
    
    -- A tiny factory: build an entity table and add it to the list.
    local function spawn(kind, x, y, hp)
      local e = { kind = kind, x = x, y = y, hp = hp, alive = true }
      table.insert(entities, e)             -- appends at position #entities + 1
      return e
    end
    
    spawn("Player", 0, 0, 100)
    local goblin = spawn("Goblin", 50, 0, 30)
    spawn("Goblin", 80, 10, 30)
    
    -- Apply damage to one entity; mark it dead at 0 hp.
    local function damage(e, amount)
      e.hp = e.hp - amount
      if e.hp <= 0 then e.alive = false end
    end
    
    damage(goblin, 30)        -- 30 - 30 = 0, so goblin.alive becomes false
    
    -- ipairs walks the list in order from index 1; count who is still alive.
    local alive = 0
    for index, e in ipairs(entities) do
      if e.alive then alive = alive + 1 end
    end
    print("Entities still alive:", alive)   -- prints: Entities still alive:	2
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    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.

    Worked example: a Roblox-style script
    -- Outside LÖVE, Lua powers ROBLOX (via "Luau", its dialect), Defold,
    -- Garry's Mod, and World of Warcraft addons. Here is a Roblox-style
    -- server Script: give every player who joins a coin counter.
    
    local Players = game:GetService("Players")
    
    -- PlayerAdded fires once per player; we connect a function to that event.
    Players.PlayerAdded:Connect(function(player)
      local stats = Instance.new("Folder")   -- a container object
      stats.Name = "leaderstats"             -- this exact name shows a leaderboard
      stats.Parent = player
    
      local coins = Instance.new("IntValue") -- holds a whole number
      coins.Name = "Coins"
      coins.Value = 0
      coins.Parent = stats
    
      print(player.Name .. " joined — gave them 0 coins")
    end)
    
    -- Luau also adds string interpolation with backticks and {} :
    -- local name = "Sam"
    -- print(`Welcome, {name}!`)   -- Luau only; prints: Welcome, Sam!
    
    -- Behaviour: the moment a player joins, a "Coins" value starting at 0
    -- appears on the in-game leaderboard, and a join message is printed.
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    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.

    🎯 Your turn: move with delta time
    -- 🎯 YOUR TURN — make movement frame-rate-INDEPENDENT.
    -- A player should move 150 pixels every second no matter the frame rate.
    -- That means: position changes by speed * dt each frame.
    
    local player = { x = 0, speed = 150 }
    
    function love.update(dt)
      if love.keyboard.isDown("right") then
        -- 1) Move the player right by speed scaled by delta time.
        player.x = player.x + ___          -- 👉 multiply player.speed by dt
      end
    end
    
    -- ✅ Expected: after holding "right" for exactly 1 second of real time,
    --    player.x has increased by 150 (because 150 * (sum of dt) = 150).
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Next, count the survivors in an entity list the idiomatic, 1-based way:

    🎯 Your turn: count living enemies
    -- 🎯 YOUR TURN — count living enemies the 1-based Lua way.
    local enemies = {
      { name = "Bat",     alive = true  },
      { name = "Slime",   alive = false },
      { name = "Skeleton", alive = true },
    }
    
    local alive = 0
    -- 1) Loop the list IN ORDER starting at index 1 (hint: ipairs).
    for _, e in ___(enemies) do            -- 👉 the iterator that starts at index 1
      -- 2) Only count the ones whose alive field is true.
      if ___ then                          -- 👉 e.alive
        alive = alive + 1
      end
    end
    
    print("Alive:", alive)
    -- ✅ Expected output:  Alive:	2
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    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

    TaskCode (LÖVE)When it runs
    Set up statefunction love.load() endonce
    Update worldfunction love.update(dt) endevery frame
    Draw screenfunction love.draw() endevery frame
    Key held?love.keyboard.isDown("up")in update
    Smooth movex = x + speed * dtin update
    Add entitytable.insert(list, e)any time
    Loop entitiesfor _, e in ipairs(list)1-based

    Frequently Asked Questions

    Q: Which game engines and games actually use Lua?

    A lot of them. Roblox runs Luau (its Lua dialect) for all game logic, LÖVE (Love2D) and Defold are full Lua game frameworks, World of Warcraft uses Lua for its UI addons and macros, and Garry's Mod is scripted in Lua. Learning Lua gives you a direct path into all of these.

    Q: What is delta time (dt) and why do I keep multiplying by it?

    Delta time is the number of seconds that passed since the previous frame. Frame rates vary between machines and even moment to moment, so if you move by a fixed amount each frame your game runs faster on faster computers. Multiplying movement by dt expresses speed in 'units per second', which stays constant at any frame rate.

    Q: How do I represent a game object like a player or enemy in Lua?

    As a table. Lua has no classes built in, so an entity is just a table of fields such as { x = 0, y = 0, hp = 100, alive = true }. You usually store all your entities in one list and loop over it each frame with ipairs to update and draw them together.

    Q: Why does my Lua entity loop skip the first item or behave oddly?

    Lua tables are 1-based: the first element is at index 1, not 0 like most languages. If you write 'for i = 0, #list' or assume index 0 exists, you read a nil and things break. Use 'for i = 1, #list' or, better, 'for _, e in ipairs(list)' which walks the list in order from index 1.

    Q: Is the Lua I learned in this course the same as Roblox's Luau?

    Yes — Luau is a superset of Lua 5.1. Everything you have learned (tables, functions, loops, metatables, the game loop pattern) works in Luau unchanged. Luau just adds extras on top, like optional type annotations and string interpolation with backticks, so your skills transfer straight across.

    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.

    🎯 Mini-Challenge: simulate a bouncing ball
    -- 🎯 MINI-CHALLENGE: a bouncing ball (no LÖVE needed — just print it)
    -- 1. Make a ball table with  x = 0, y = 0  and  vx = 50, vy = 30  (px/s).
    -- 2. Use a fixed step:  local dt = 0.1
    -- 3. Loop 5 times. Each step:
    --      - add vx * dt to x, and vy * dt to y   (move by velocity * dt)
    --      - if y goes above 10, bounce: set vy = -vy   (flip vertical speed)
    -- 4. print(x, y) each step so you can watch it travel and bounce.
    --
    -- ✅ Expected (first line): 5.0	3.0   then x keeps rising by 5 each step.
    
    -- your code here
    Lua runs outside the browser — try this code atonecompiler.com/luaor locally with the lua command.

    Where to Go From Here

    Free resources to build real games

    Project ideas to practise on

    LevelProject
    BeginnerPong, Snake, Flappy Bird clone
    IntermediateSide-scrolling platformer, top-down shooter
    AdvancedRoblox 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. 🌙

    Sign up for free to track which lessons you've completed and get learning reminders.

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service