Lua Fundamentals for FiveM Roleplay Scripting
Every FiveM script whether it targets ESX, QBCore, or QBox is written in Lua, a small, fast, beginner-friendly scripting language. You do not need to master computer science to script roleplay features, but you do need a working grasp of a handful of Lua ideas: variables and scope, tables (Lua's one all-purpose data structure), functions, loops, and the FiveM-specific concept of threads. Get these right and the framework functions you call will start to make sense instead of feeling like magic incantations.
This guide is deliberately focused on the Lua you will actually use in roleplay scripting, with examples shaped like real FiveM code. It is not an exhaustive language reference; it is the 20% of Lua that covers 80% of scripts. PlayDeck teaches this with AI alongside the framework: you can have the AI write the Lua while you learn to read it, spot bugs, and steer the logic which is the fastest way to go from copy-pasting to genuinely understanding your own scripts.
Variables and scope: always use local
Lua is dynamically typed, so you never declare a type you just assign. The one rule that matters most in FiveM is to declare variables with local: local playerName = 'Mike'. A local variable is scoped to its block; a variable without local is global, shared across your entire resource, slower to access, and a frequent source of subtle bugs where two scripts clobber the same name.
The practical takeaway: put local in front of nearly every variable you create. Reserve globals for things that genuinely need to be shared, like your framework object (ESX or QBCore) declared once at the top of a file. New scripters who forget local end up with mysterious cross-script interference that is painful to track down.
Tables: arrays, dictionaries, and objects in one
Tables are the heart of Lua. The same table type acts as an array (a numbered list), a dictionary (key-value pairs), or an object, depending on how you use it. As a list: local fruits = {'apple', 'banana'} accessed by fruits[1] (Lua arrays start at 1, not 0 a common gotcha). As a dictionary: local player = { name = 'Mike', cash = 500 } accessed by player.name.
You iterate tables two ways. ipairs walks a numbered list in order: for i, fruit in ipairs(fruits) do print(fruit) end. pairs walks every key in a dictionary: for key, value in pairs(player) do print(key, value) end. Knowing which to use ipairs for ordered lists, pairs for key-value data is essential, because nearly every framework function hands you data as a table you'll need to loop over.
- Arrays are 1-indexed in Lua: myList[1] is the first item
- ipairs: iterate a numbered list in order
- pairs: iterate all key-value entries (order not guaranteed)
- #myList: the length of an array-style table
Functions and commands
Functions package reusable logic. local function giveReward(amount) return amount * 2 end defines one; giveReward(100) calls it and returns 200. In FiveM, functions are first-class values, which is why so many natives take a function as an argument for example, RegisterCommand('hello', function(source, args) print('Hello from', source) end) registers a /hello chat command whose body is an inline function.
RegisterCommand is one of the first natives every scripter learns: it ties a slash command to a function that receives the source (who ran it) and args (what they typed after it). Combined with a framework's GetPlayer function, RegisterCommand is enough to build dozens of small roleplay utilities. Most beginner scripts are just a command, a player lookup, and one framework call.
Threads and Citizen.Wait
FiveM runs your script on a shared game thread, which means you can never block it with an infinite loop doing so freezes the whole game. Instead, long-running or repeating logic goes inside a thread created with Citizen.CreateThread (or the modern CreateThread). Inside it, you yield control back to the game with Citizen.Wait(ms), which pauses your loop for that many milliseconds without freezing anything.
A classic pattern is a loop that checks something every frame or every second: CreateThread(function() while true do Wait(1000) /* check player status here */ end end). The Wait(1000) is mandatory a while true loop with no Wait will crash the client. Picking the right Wait value matters for performance: Wait(0) runs every frame (use sparingly), while Wait(1000) or higher is plenty for periodic checks like hunger or zone detection.
Nil, conditionals, and safe code
nil is Lua's 'nothing' value, and the error you will see most often as a beginner is 'attempt to index a nil value' you tried to read a field on something that was nil. This is why framework code constantly checks if Player then ... end before using a player object: GetPlayer can return nil, and guarding against it prevents crashes.
Conditionals use if/elseif/else/end, and Lua treats only nil and false as falsy every number (including 0) and every string (including an empty one) is truthy, which surprises people coming from other languages. Build the habit of validating inputs and checking for nil before you use a value; it is the difference between a script that crashes your server and one that fails gracefully.
Frequently asked questions
Is Lua hard to learn for FiveM?
No. Lua is one of the simplest mainstream languages, and FiveM scripts follow predictable patterns. If you learn variables, tables, functions, loops, and threads, you can read and write the majority of roleplay scripts. The framework handles the hard parts.
Why do Lua arrays start at 1?
It is a deliberate language design choice in Lua. It trips up programmers from other languages, so remember: the first element is myTable[1], and the length operator #myTable gives the count. Off-by-one bugs almost always trace back to forgetting this.
What does Citizen.Wait actually do?
It yields your thread back to the game engine for the given number of milliseconds, so other code (and the game itself) can run. Every repeating loop needs at least one Wait or it will hang the client. Higher Wait values use less CPU.
Do ESX, QBCore, and QBox all use the same Lua?
Yes, the language is identical; only the framework functions differ. The Lua fundamentals on this page apply to every FiveM framework. Once you know Lua, switching frameworks is mostly learning new function names.