What It Replaces#
Hundreds of FiveM scripts ship their own DrawText3D: a function that draws a line of white text at a coordinate, called every frame from a loop while the player stands nearby.
[E] Open door[E] Talk to the clerk[G] Lockpick
It is drawn with the game's text natives, so it cannot be styled, it has to be redrawn on every frame, and each script carries a slightly different copy. Text UI keeps the function name and the call, and turns every label into a proper prompt: a marker from a distance, the key badge and the text up close.
Only the function is replaced. The script keeps its own loop and its own keypress check, so its behaviour does not change — only the way the label looks.
Replacing the Function#
Add the wrapper
Find the
DrawText3Dfunction in the script's client files — usually inclient/main.luaorclient/functions.lua— and replace the whole function with this:lua local texts = {} function DrawText3D(x, y, z, text, id, key) id = id or text exports['qs-textui']:drawText3D(x, y, z, text, id, key or 'E') if texts[id] then texts[id] = 5 return end texts[id] = 5 CreateThread(function() while texts[id] > 0 do texts[id] = texts[id] - 1 Wait(0) end texts[id] = nil exports['qs-textui']:DeleteDrawText3D(id) end) endEvery call is forwarded to Text UI, so a label that moves or changes its text follows along. When the script stops calling it for a few frames — the player walked away — the label is removed.
Keep a fallback (optional)
For a script that must also run on servers without Text UI, use this version instead. It picks Text UI when
qs-textuiis started, and the classic white text when it is not.lua local texts = {} if GetResourceState('qs-textui') == 'started' then function DrawText3D(x, y, z, text, id, key) id = id or text exports['qs-textui']:drawText3D(x, y, z, text, id, key or 'E') if texts[id] then texts[id] = 5 return end texts[id] = 5 CreateThread(function() while texts[id] > 0 do texts[id] = texts[id] - 1 Wait(0) end texts[id] = nil exports['qs-textui']:DeleteDrawText3D(id) end) end else function DrawText3D(x, y, z, text) SetTextScale(0.35, 0.35) SetTextFont(4) SetTextProportional(1) SetTextColour(255, 255, 255, 215) SetTextEntry('STRING') SetTextCentre(true) AddTextComponentString(text) SetDrawOrigin(x, y, z, 0) DrawText(0.0, 0.0) local factor = text:len() / 370 DrawRect(0.0, 0.0 + 0.0125, 0.017 + factor, 0.03, 0, 0, 0, 75) ClearDrawOrigin() end endThe check runs once, when the script starts — so
qs-textuimust start before it inserver.cfg.Give each label an id and a key
Old scripts call the function with four values, and put the key inside the text:
lua DrawText3D(x, y, z, '[E] Open door')Add a unique id and the key, and take the key out of the text — the badge already shows it:
lua DrawText3D(x, y, z, 'Open door', 'motel_door_' .. doorId, 'E')Without an id, the text itself is used as one. That is fine for most scripts, but a script that draws the same text at several places at once needs an id per place, or the labels merge into one.
A Complete Example#
A typical door loop after the change. Only the DrawText3D line is different from the original:
CreateThread(function()
while true do
local sleep = 1000
local coords = GetEntityCoords(PlayerPedId())
for doorId, door in pairs(Doors) do
if #(coords - door.coords) < 2.0 then
sleep = 0
DrawText3D(door.coords.x, door.coords.y, door.coords.z, 'Open door', 'door_' .. doorId, 'E')
if IsControlJustReleased(0, 38) then
OpenDoor(doorId)
end
end
end
Wait(sleep)
end
end)How the Labels Behave#
Labels drawn through DrawText3D use fixed distances, measured from the player to the label:
Distance | What shows |
|---|---|
Over 5 m | Nothing. |
2 to 5 m | A small marker at the point. |
Under 2 m | The key badge and the text. |
The script still decides when the label exists: a script that only draws within 1.5 m shows the label from 1.5 m and removes it beyond that. For your own distances, a keypress handled by Text UI, or a job filter, create the prompt with create3DTextUI instead — see Commands and Exports.
GTA codes left in the text — ~g~, ~r~, ~h~, ~n~ — become colours, bold and line breaks, and codes such as ~INPUT_CONTEXT~ are removed. See Text Formatting.
Labels belong to the script that drew them. When that script stops or restarts, its labels are removed with it.



