Aller au contenu
Cheap native calls

Cheap native calls

Backtick hashes, joaat, vector distance instead of GetDistanceBetweenCoords, caching ped handles safely, and keeping per frame natives behind a gate.

Cette documentation est en anglais pour l'instant.

Last updated

A native call leaves the Lua interpreter, crosses into the game or the server core, and comes back. It is not expensive on its own, and it is not free either. In a loop that runs every frame the number of native calls per pass is usually the biggest thing you control.

This page is about the cost. For what natives are and how to find them, see Natives: talking to the game.

Hash strings at compile time

GTA identifies models, weapons, anim sets and much more by a 32 bit joaat hash of the name. You have three ways to produce one, and they are not equal:

Lua
local a = GetHashKey('adder')   -- a native call, at runtime, every time
local b = joaat('adder')        -- a CfxLua function, at runtime, no native call
local c = `adder`               -- hashed while the script compiles, nothing at runtime

All three give the same number. The backtick form is the one to use for a name you type in the source: the Cfx docs describe it as compile time generation of hash keys with zero runtime overhead, so `adder` is a literal number by the time the script runs.

When the name comes from a variable you cannot use backticks, and then joaat(name) is the right call. It hashes in the Lua runtime instead of going out to a native. Note that joaat lowercases the input before hashing, which is also why spawn names work in any case.

Lua
-- In a loop, over a config full of names
for i = 1, #Config.Blacklist do
    blacklist[joaat(Config.Blacklist[i])] = true   -- hashed once, at load
end

local function isBlacklisted(model)
    return blacklist[model] == true                 -- no hashing per call
end

GetHashKey is not broken and thousands of resources use it. There is just no case left where it is the best of the three.

Distance without a native

Vectors are real types in CfxLua, and #v is the magnitude of a vector, so the distance between two points is one operator:

Lua
-- A native call with seven arguments
local d = GetDistanceBetweenCoords(x1, y1, z1, x2, y2, z2, true)

-- Vector maths in the VM
local d = #(coordsA - coordsB)

Most position natives already hand you a vector3, so the second form usually saves you unpacking as well. When you are only comparing against a radius and do not need the number itself, you can skip the square root by comparing the squared lengths:

Lua
local diff = coordsA - coordsB
if (diff.x * diff.x + diff.y * diff.y + diff.z * diff.z) < (20.0 * 20.0) then

That is worth it over a few thousand comparisons and not worth the loss of clarity over forty. Use #(a - b) < 20.0 unless you have measured a reason not to.

For a flat check that ignores height, build the difference and leave z out of the sum, or compare vector2(a.x, a.y) against vector2(b.x, b.y).

The local ped

Lua
local ped = GetPlayerPed(-1)   -- old, magic number
local ped = PlayerPedId()      -- the current way

Both are natives. The important part is not which one you call, it is how often. Inside a loop, call it once per pass and reuse the handle:

Lua
CreateThread(function()
    while true do
        local ped = PlayerPedId()          -- once per pass
        local pos = GetEntityCoords(ped)   -- once per pass
        ...
        Wait(sleep)
    end
end)

Warning

Do not cache a ped handle forever. The handle changes when the player respawns or the model is switched, and code holding the old one will silently do nothing. Cache it for the body of a loop pass, or use ox_lib’s cache.ped, which refreshes itself when the ped changes.

The same applies to any handle: vehicles, objects and blips are local numbers that can become invalid. Check with DoesEntityExist(entity) before you use one you stored.

Drop the Citizen. prefix

Wait, CreateThread, SetTimeout and ClearTimeout are set up as plain global aliases of the Citizen.* functions in the FiveM Lua scheduler. Wait is Citizen.Wait, the same function object.

Lua
Citizen.CreateThread(function()
    Citizen.Wait(1000)
end)

CreateThread(function()
    Wait(1000)
end)

So this is a readability change, not a speed one. The prefixed form costs one extra table lookup per call and nothing else, which is nothing you will ever measure. Write the short form because every current example does, and be suspicious of a tutorial that still uses the long one: it is usually old enough that other things in it are out of date too.

Per frame natives belong behind a gate

Some natives only do anything for the frame you call them in. DrawMarker, DrawText, DisableControlAction, HideHudComponentThisFrame and friends all have to run every frame to stay on screen, which means a Wait(0) loop, which means you must make sure that loop is not running when there is nothing to draw:

Lua
CreateThread(function()
    while true do
        local sleep = 500
        if nearMarker then
            sleep = 0
            DrawMarker(...)
        end
        Wait(sleep)
    end
end)

That pattern, and the ox_lib points and zones that replace it, are covered in Threads and performance.

Hoist what cannot change

Anything constant for the life of the resource should be read once, not per frame:

Lua
-- Called every frame, always the same answer
CreateThread(function()
    while true do
        Wait(0)
        if GetCurrentResourceName() == 'my_resource' then ... end
    end
end)

-- Read once at load
local RESOURCE <const> = GetCurrentResourceName()

Convars, screen resolution, config values and model hashes all fall in this group. Resolution is the one exception worth a note: it can change when the player alt tabs or edits settings, so read it on a timer or on the relevant event rather than every frame.

Load and release streamed assets

Models, anim dicts, particle assets, texture dicts and scaleforms have to be requested and waited for. Always put a timeout on the wait so a missing asset does not hang a thread forever, and release what you loaded:

Lua
local function loadModel(model)
    local hash = type(model) == 'number' and model or joaat(model)
    if not IsModelInCdimage(hash) then return nil end

    RequestModel(hash)
    local deadline = GetGameTimer() + 5000
    while not HasModelLoaded(hash) do
        if GetGameTimer() > deadline then return nil end
        Wait(0)
    end
    return hash
end

-- when you are done
SetModelAsNoLongerNeeded(hash)

ox_lib ships lib.requestModel, lib.requestAnimDict and lib.requestNamedPtfxAsset with the timeout already handled.

Next: Events that stay cheap and safe.