Every docs URL answers with Markdown when you add .md. Site map: llms.txt
Functions that stay small
One job per function, names that say what happens, parameters that do not grow, the cost of a function literal in a hot path, and annotating exports.
Tato dokumentace je zatím v angličtině.
A function that does one thing is easy to name, easy to test, and easy to delete. A function that does four things has a name like handlePlayer and you have to read all of it before you can change any of it.
One job, one level
Two rules cover most of it. A function should do one thing, and it should stay at one level of detail. Mixing “decide what happens” with “poke the game” is what turns a 20 line function into a 200 line one:
local function useItem(item)
if item.type == 'food' then
local ped = PlayerPedId()
RequestAnimDict('mp_player_inteat@burger')
while not HasAnimDictLoaded('mp_player_inteat@burger') do Wait(0) end
TaskPlayAnim(ped, 'mp_player_inteat@burger', 'mp_player_int_eat_burger', 8.0, -8.0, 3000, 49, 0, false, false, false)
Wait(3000)
ClearPedTasks(ped)
TriggerServerEvent('food:server:ateItem', item.name)
elseif item.type == 'drink' then
-- another 10 lines of the same
end
endlocal function playAnim(dict, clip, duration)
lib.requestAnimDict(dict)
TaskPlayAnim(PlayerPedId(), dict, clip, 8.0, -8.0, duration, 49, 0, false, false, false)
Wait(duration)
ClearPedTasks(PlayerPedId())
end
local function eat(item)
playAnim('mp_player_inteat@burger', 'mp_player_int_eat_burger', 3000)
TriggerServerEvent('food:server:ateItem', item.name)
end
local function useItem(item)
if item.type == 'food' then return eat(item) end
if item.type == 'drink' then return drink(item) end
enduseItem now reads as a routing table, eat reads as one action, and playAnim is the only place that knows the native signature. When TaskPlayAnim gains another argument, you change one line.
Names
| Kind | Style | Example |
|---|---|---|
| Local function | camelCase | getShopStock, playAnim |
| Resource global | PascalCase | GetItemPrice |
| Export | PascalCase | exports('GetItemPrice', ...) |
Start with a verb. player() tells you nothing, getPlayerData() tells you what comes back and that nothing changes. dropPlayer() tells you something happens. A function whose name needs “and” in it is two functions.
Parameters
Once you are past about three parameters, nobody remembers the order:
-- What is the fourth argument again?
createCharacter('Ali', 'Demir', 34, true, false, 'tr')
-- Self describing, and adding a field breaks nothing
createCharacter({
first = 'Ali',
last = 'Demir',
age = 34,
male = true,
nationality = 'tr',
})The table form also lets the caller leave things out without counting nils. The cost is one table allocation per call, so in a function you call thousands of times per frame keep the positional form. Everywhere else the readability is worth more.
Required parameters come first, optional ones last. Lua has no way to skip an argument, so doThing(nil, nil, value) is what happens when you get this wrong.
A boolean parameter usually means two functions. notify(text, isError) has two bodies inside one name, and at the call site notify('Saved', false) tells the reader nothing. notifySuccess(text) and notifyError(text) are clearer and each one is shorter. The exception is a flag that really is an option on one behaviour, like alive on a native.
Function literals in hot paths
In Lua 5.4 a function expression creates a new closure value every time that line runs. There is no cache reusing an identical one, so a literal inside a loop or a per frame callback allocates every pass and gives the garbage collector something to clean up.
-- A new closure every frame
CreateThread(function()
while true do
Wait(0)
drawSomething(function(x) return x * 2 end)
end
end)
-- One closure, created once
local function double(x) return x * 2 end
CreateThread(function()
while true do
Wait(0)
drawSomething(double)
end
end)This matters in loops and callbacks that fire constantly. Passing a literal to something that runs once, like a command handler or a one off timer, is fine and clearer than naming it.
The same applies to closures that capture upvalues you do not need. If the inner function only uses its arguments, lift it out of the enclosing function so it is created once when the file loads.
Return values
Returning several values is normal in Lua and cheaper than building a table for them:
local function getSpawn()
return vector3(-269.4, -955.3, 31.2), 205.0
end
local coords, heading = getSpawn()Inside one resource, returning a table does not copy it, it hands back a reference, so returning a big table to your own code is not expensive. That changes the moment the value crosses into another resource: an export call packs and unpacks its arguments and returns. See Exports and resource boundaries.
Annotate what other people call
Any function other resources call should carry Lua Language Server annotations. With the CfxLua IntelliSense setup from Start developing for FiveM, these turn into autocomplete and warnings for whoever uses your resource:
---Price of one item for one player, including job discount and zone tax.
---@param item string Item name, e.g. 'water'
---@param job string Job name, or 'unemployed'
---@param zone string Zone id from the config
---@return number price Final price, never below zero
function GetItemPrice(item, job, zone)
...
end
exports('GetItemPrice', GetItemPrice)It costs four lines and removes an entire category of support questions.
Guard clauses
The best tool for keeping a function small is refusing to continue. That has its own page: Conditionals and early returns.
