Lua in Game Development (Roblox / LÖVE2D)
Lua is widely adopted in game development. It is used in many environments including Roblox game logic, World of Warcraft add-ons, and LÖVE2D game engine scripts. Its lightweight, fast, and embeddable nature makes it an excellent fit for game engines.
Main Use Cases
| Environment | Description |
|---|---|
| LÖVE2D | An open-source framework for building 2D games entirely in Lua. Write the game loop in main.lua. |
| Roblox | Roblox Studio uses Luau (a Lua derivative) as its internal scripting language. All game behavior, UI, and physics control are written in Lua. |
| World of Warcraft | Add-ons (extensions) can be written in Lua. Used for UI control, command additions, and chat automation. |
| Defold | Game logic in the 2D/3D engine Defold is written in Lua. Define functions in component lifecycle hooks. |
| Corona SDK / Solar2D | A mobile-oriented 2D framework. Physics, animation, and networking can all be implemented in Lua alone. |
LÖVE2D Basic Syntax
-- -----------------------------------------------
-- Basic structure of the LÖVE2D game loop
-- -----------------------------------------------
-- love.load(): called once when the game starts
function love.load()
-- load resources and perform initialization here
end
-- love.update(dt): called every frame (dt is seconds elapsed since the previous frame)
function love.update(dt)
-- update game logic here
end
-- love.draw(): called every frame for rendering
function love.draw()
-- write drawing code here
end
-- love.keypressed(key): called the moment a key is pressed
function love.keypressed(key)
if key == "escape" then
love.event.quit() -- quit the game on Escape key
end
end
Sample Code
main.lua
-- main.lua — character selection demo using LÖVE2D
-- Manages Steins;Gate characters in a table and implements
-- a simple menu where you can select with arrow keys and confirm with Enter
--
-- Run: love . (main.lua must be in the current directory)
-- -----------------------------------------------
-- Define the character table
-- -----------------------------------------------
local characters = {
{ name = "Rintaro Okabe", title = "Mad Scientist", hp = 100, sp = 80 },
{ name = "Kurisu Makise", title = "Genius Physicist", hp = 90, sp = 120 },
{ name = "Mayuri Shiina", title = "Cosplayer", hp = 110, sp = 60 },
{ name = "Itaru Hashida", title = "Hacker and Otaku", hp = 95, sp = 100 },
{ name = "Moeka Kiryu", title = "Phone-Dependent Writer", hp = 85, sp = 90 },
}
-- -----------------------------------------------
-- Table holding the game state
-- -----------------------------------------------
local state = {
selected_index = 1, -- currently selected index
confirmed = false, -- whether a selection has been confirmed
confirmed_name = "", -- name of the confirmed character
blink_timer = 0, -- timer for blink animation
blink_visible = true, -- whether the blinking text is visible
}
-- -----------------------------------------------
-- Color constants (LÖVE2D uses 0-1 range)
-- -----------------------------------------------
local COLOR = {
bg = { 0.08, 0.08, 0.15, 1 }, -- background color (dark navy)
title = { 0.90, 0.75, 0.20, 1 }, -- title color (gold)
selected = { 0.30, 0.80, 1.00, 1 }, -- selected item color (cyan)
normal = { 0.85, 0.85, 0.85, 1 }, -- normal text color
sub = { 0.60, 0.60, 0.70, 1 }, -- subtext color
hp_bar = { 0.20, 0.80, 0.40, 1 }, -- HP bar color (green)
sp_bar = { 0.20, 0.50, 1.00, 1 }, -- SP bar color (blue)
bar_bg = { 0.20, 0.20, 0.25, 1 }, -- bar background color
confirmed = { 1.00, 0.40, 0.40, 1 }, -- highlight color after confirmation
}
-- -----------------------------------------------
-- Helper function: draw a filled rectangle
-- -----------------------------------------------
local function draw_rect(x, y, w, h, color)
love.graphics.setColor(color)
love.graphics.rectangle("fill", x, y, w, h)
end
-- -----------------------------------------------
-- Helper function: draw an HP/SP bar
-- -----------------------------------------------
local function draw_bar(x, y, w, h, value, max_value, color)
-- draw the bar background
draw_rect(x, y, w, h, COLOR.bar_bg)
-- draw the fill proportional to the value
local fill_w = math.floor(w * (value / max_value))
draw_rect(x, y, fill_w, h, color)
end
-- -----------------------------------------------
-- love.load(): initialization
-- -----------------------------------------------
function love.load()
love.window.setTitle("Character Selection — Steins;Gate")
love.window.setMode(640, 480)
love.graphics.setBackgroundColor(COLOR.bg)
end
-- -----------------------------------------------
-- love.update(dt): per-frame update
-- -----------------------------------------------
function love.update(dt)
-- update the blink timer (toggles every 0.5 seconds)
state.blink_timer = state.blink_timer + dt
if state.blink_timer >= 0.5 then
state.blink_timer = 0
state.blink_visible = not state.blink_visible
end
end
-- -----------------------------------------------
-- love.draw(): rendering
-- -----------------------------------------------
function love.draw()
local gfx = love.graphics
-- draw title
gfx.setColor(COLOR.title)
gfx.print("=== Character Selection ===", 180, 20)
-- draw character list
for i, chara in ipairs(characters) do
local y = 60 + (i - 1) * 60
if i == state.selected_index then
-- highlight the selected row
draw_rect(40, y - 4, 560, 52, { 0.15, 0.25, 0.35, 1 })
gfx.setColor(COLOR.selected)
gfx.print("> " .. chara.name, 52, y)
else
gfx.setColor(COLOR.normal)
gfx.print(" " .. chara.name, 52, y)
end
-- display the title
gfx.setColor(COLOR.sub)
gfx.print(chara.title, 72, y + 18)
-- draw HP bar
draw_bar(280, y + 6, 120, 10, chara.hp, 120, COLOR.hp_bar)
gfx.setColor(COLOR.sub)
gfx.print("HP", 258, y + 4)
-- draw SP bar
draw_bar(440, y + 6, 120, 10, chara.sp, 120, COLOR.sp_bar)
gfx.setColor(COLOR.sub)
gfx.print("SP", 418, y + 4)
end
-- draw instructions
gfx.setColor(COLOR.sub)
gfx.print("Up/Down: select Enter: confirm Escape: quit", 140, 430)
-- if confirmed, display the message with blinking
if state.confirmed and state.blink_visible then
gfx.setColor(COLOR.confirmed)
gfx.print(state.confirmed_name .. " selected!", 180, 390)
end
end
-- -----------------------------------------------
-- love.keypressed(key): key input handling
-- -----------------------------------------------
function love.keypressed(key)
if key == "up" then
-- move cursor up (wrap to end when passing the first item)
state.selected_index = state.selected_index - 1
if state.selected_index < 1 then
state.selected_index = #characters
end
state.confirmed = false
elseif key == "down" then
-- move cursor down (wrap to start when passing the last item)
state.selected_index = state.selected_index + 1
if state.selected_index > #characters then
state.selected_index = 1
end
state.confirmed = false
elseif key == "return" or key == "kpenter" then
-- confirm the selection on Enter key
state.confirmed = true
state.confirmed_name = characters[state.selected_index].name
elseif key == "escape" then
love.event.quit() -- quit the game on Escape key
end
end
(Run in LÖVE2D: love .) -- A window opens and the character selection screen is displayed. === Character Selection === > Rintaro Okabe Mad Scientist HP [======== ] SP [====== ] Kurisu Makise Genius Physicist HP [======== ] SP [==========] Mayuri Shiina Cosplayer HP [=========] SP [===== ] Itaru Hashida Hacker and Otaku HP [======== ] SP [======== ] Moeka Kiryu Phone-Dependent Writer HP [======= ] SP [======= ] Up/Down: select Enter: confirm Escape: quit -- After pressing Enter: Rintaro Okabe selected! (blinking)
Roblox (Luau) Script Example
CharacterStats.lua
-- CharacterStats.lua — server script for Roblox Studio
-- Manages Steins;Gate character stats and sends data
-- to the client via a RemoteEvent
--
-- Place the script in ServerScriptService in Roblox and run it
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
-- Create a RemoteEvent (used to send data to the client)
local stats_event = Instance.new("RemoteEvent")
stats_event.Name = "StatsEvent"
stats_event.Parent = ReplicatedStorage
-- -----------------------------------------------
-- Character stats table
-- -----------------------------------------------
local CHARACTER_STATS = {
["Rintaro Okabe"] = { hp = 100, sp = 80, ability = "Future Gadget" },
["Kurisu Makise"] = { hp = 90, sp = 120, ability = "Time Leap Theory" },
["Mayuri Shiina"] = { hp = 110, sp = 60, ability = "Steins Gate's Choice" },
["Itaru Hashida"] = { hp = 95, sp = 100, ability = "Hacking" },
["Moeka Kiryu"] = { hp = 85, sp = 90, ability = "Blind Faith in FB" },
}
-- -----------------------------------------------
-- Event called when a player joins
-- -----------------------------------------------
Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined the game.")
-- Randomly assign a character
local names = {}
for name in pairs(CHARACTER_STATS) do
table.insert(names, name)
end
local assigned_name = names[math.random(1, #names)]
local assigned_stats = CHARACTER_STATS[assigned_name]
-- Send stats to the client
stats_event:FireClient(player, {
character = assigned_name,
hp = assigned_stats.hp,
sp = assigned_stats.sp,
ability = assigned_stats.ability,
})
print("Assignment complete: " .. player.Name .. " -> " .. assigned_name)
end)
-- -----------------------------------------------
-- Log stats for all characters
-- -----------------------------------------------
print("=== Character Stats List ===")
for name, stats in pairs(CHARACTER_STATS) do
print(string.format(
" %-20s HP:%3d SP:%3d Ability: %s",
name, stats.hp, stats.sp, stats.ability
))
end
print("Initialization complete")
(Displayed in the Roblox Studio output window) === Character Stats List === Rintaro Okabe HP:100 SP: 80 Ability: Future Gadget Kurisu Makise HP: 90 SP:120 Ability: Time Leap Theory Mayuri Shiina HP:110 SP: 60 Ability: Steins Gate's Choice Itaru Hashida HP: 95 SP:100 Ability: Hacking Moeka Kiryu HP: 85 SP: 90 Ability: Blind Faith in FB Initialization complete
Common Mistakes
Creating global variables inside the game loop every frame in LÖVE2D causes frequent GC pauses
Creating tables or strings every frame inside the game loop (love.update / love.draw) increases garbage collector (GC) load and causes unstable frame rates. Values that need to persist across frames should be created only once in love.load() or an outer scope.
-- NG (a new table is created every frame)
function love.update(dt)
local pos = { x = player.x + dt * 100, y = player.y } -- becomes GC target every frame
player.x = pos.x
end
-- OK (update fields directly to avoid creating a table)
function love.update(dt)
player.x = player.x + dt * 100
end
Not using love.update(dt)'s dt for speed calculations makes behavior environment-dependent
dt (delta time) is the number of seconds elapsed since the previous frame. Multiplying by it ensures the same speed regardless of frame rate differences between environments. Using a fixed value causes speed to change when the frame rate changes.
-- NG (adding a fixed value makes speed frame-rate-dependent)
function love.update(dt)
player.x = player.x + 5 -- speed doubles between 60fps and 30fps
end
-- OK (multiply by dt to define movement per second)
local SPEED = 300 -- 300 pixels per second
function love.update(dt)
player.x = player.x + SPEED * dt -- same speed at any frame rate
end
Trying to get LocalPlayer from a server script in Roblox
game.Players.LocalPlayer is a property only valid on the client side (LocalScript). Referencing it from a server script (Script) returns nil and causes unexpected errors.
-- NG (using LocalPlayer from a script placed in ServerScriptService) local player = game.Players.LocalPlayer -- nil is returned print(player.Name) -- attempt to index nil value
-- OK (in a server script, receive the player from the PlayerAdded event)
game.Players.PlayerAdded:Connect(function(player)
print(player.Name .. " joined")
end)
-- Code using LocalPlayer should be written in a LocalScript (placed in StarterPlayerScripts, etc.)
local player = game.Players.LocalPlayer -- valid in a LocalScript
Overview
The reasons Lua is widely used in game engines come down to three points: lightweight, easy to embed, and flexible data representation. A single table can serve as an array, dictionary, or object, making it well-suited for game entity management and configuration data. In LÖVE2D, you can quickly get a prototype running by simply defining three functions as the game loop: love.load, love.update, and love.draw. Roblox (Luau) adds its own extensions to Lua 5.x including type annotations and bitwise operators, making it suitable for large-scale game logic. World of Warcraft add-ons operate UI frames and event hooks through an API based on Lua 5.1. In all of these environments, table-centric data modeling and declarative event handling via callback registration are the fundamental patterns. For Lua embedded in C, see C API Integration as well.
If you find any errors or copyright issues, please contact us.