FixFX

Client-Side Development

Complete guide to developing client-side scripts for FiveM resources.

Client-side scripts run on each player's game client and handle user interface, player input, local game state, and communication with the server.

Client Script Basics

Script Structure

-- client.lua
local isMenuOpen = false
local playerData = {}
 
-- Event handlers
RegisterNetEvent('myresource:updateData')
AddEventHandler('myresource:updateData', function(data)
    playerData = data
end)
 
-- Main thread
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)
        -- Main loop logic
    end
end)

Player Management

-- Get local player
local playerId = PlayerId()
local playerPed = PlayerPedId()
 
-- Player state
local isPlayerDead = IsEntityDead(playerPed)
local playerCoords = GetEntityCoords(playerPed)
local playerHeading = GetEntityHeading(playerPed)
 
-- Player info
local playerName = GetPlayerName(playerId)
local playerServerId = GetPlayerServerId(playerId)

Vehicle Handling

-- Current vehicle
local vehicle = GetVehiclePedIsIn(playerPed, false)
local lastVehicle = GetVehiclePedIsIn(playerPed, true)
 
-- Vehicle state
local isInVehicle = IsPedInAnyVehicle(playerPed, false)
local isDriver = GetPedInVehicleSeat(vehicle, -1) == playerPed
 
-- Vehicle info
local vehicleModel = GetEntityModel(vehicle)
local vehicleSpeed = GetEntitySpeed(vehicle)
local vehicleHealth = GetEntityHealth(vehicle)

Event System

Registering Events

-- Server to client events
RegisterNetEvent('myresource:notify')
AddEventHandler('myresource:notify', function(message, type)
    ShowNotification(message, type)
end)
 
-- Client to client events (local)
AddEventHandler('myresource:localEvent', function(data)
    print('Local event triggered:', json.encode(data))
end)

Triggering Events

-- Trigger server event
TriggerServerEvent('myresource:saveData', playerData)
 
-- Trigger client event (local)
TriggerEvent('myresource:localEvent', {key = 'value'})
 
-- Trigger event for all clients (from client)
TriggerServerEvent('myresource:broadcastToAll', message)

Event Best Practices

-- Use meaningful event names
RegisterNetEvent('banking:updateBalance')  -- Good
RegisterNetEvent('event1')                 -- Bad
 
-- Validate event data
AddEventHandler('banking:updateBalance', function(newBalance)
    if type(newBalance) == 'number' and newBalance >= 0 then
        balance = newBalance
        UpdateUI()
    end
end)
 
-- Clean up event handlers
AddEventHandler('onResourceStop', function(resourceName)
    if resourceName == GetCurrentResourceName() then
        -- Cleanup code here
    end
end)

User Interface

Key Input Handling

local Keys = {
    ['E'] = 38,
    ['F'] = 23,
    ['G'] = 47,
    ['H'] = 74
}
 
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)
        
        if IsControlJustPressed(0, Keys['E']) then
            -- E key pressed
            HandleInteraction()
        end
        
        if IsControlPressed(0, Keys['F']) then
            -- F key held down
            HandleContinuousAction()
        end
    end
end)

Drawing Text and UI

-- Draw text on screen
function DrawText3D(x, y, z, text)
    local onScreen, _x, _y = World3dToScreen2d(x, y, z)
    local pX, pY, pZ = table.unpack(GetGameplayCamCoords())
    
    SetTextScale(0.35, 0.35)
    SetTextFont(4)
    SetTextProportional(1)
    SetTextColour(255, 255, 255, 215)
    SetTextEntry("STRING")
    SetTextCentre(1)
    AddTextComponentString(text)
    DrawText(_x, _y)
end
 
-- Show notification
function ShowNotification(message, type)
    SetNotificationTextEntry("STRING")
    AddTextComponentString(message)
    DrawNotification(false, false)
end
 
-- Help text
function ShowHelpText(text)
    SetTextComponentFormat("STRING")
    AddTextComponentString(text)
    DisplayHelpTextFromStringLabel(0, 0, 1, -1)
end

Markers and Blips

-- Create marker
function CreateMarker(type, x, y, z, r, g, b, a)
    DrawMarker(type, x, y, z, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 
               1.0, 1.0, 1.0, r, g, b, a, false, true, 2, false, false, false, false)
end
 
-- Create blip
function CreateBlipForCoord(x, y, z, sprite, color, text)
    local blip = AddBlipForCoord(x, y, z)
    SetBlipSprite(blip, sprite)
    SetBlipDisplay(blip, 4)
    SetBlipScale(blip, 1.0)
    SetBlipColour(blip, color)
    SetBlipAsShortRange(blip, true)
    BeginTextCommandSetBlipName("STRING")
    AddTextComponentString(text)
    EndTextCommandSetBlipName(blip)
    return blip
end

NUI Integration

Basic NUI Setup

-- Open NUI
function OpenUI()
    SetNuiFocus(true, true)
    SendNUIMessage({
        action = "show",
        data = playerData
    })
end
 
-- Close NUI
function CloseUI()
    SetNuiFocus(false, false)
    SendNUIMessage({
        action = "hide"
    })
end
 
-- NUI Callbacks
RegisterNUICallback('close', function(data, cb)
    CloseUI()
    cb('ok')
end)
 
RegisterNUICallback('saveSettings', function(data, cb)
    TriggerServerEvent('myresource:saveSettings', data)
    cb('ok')
end)

Advanced NUI Communication

-- Send data to NUI
function UpdateNUIData(dataType, data)
    SendNUIMessage({
        action = "update",
        type = dataType,
        data = data
    })
end
 
-- Handle multiple NUI callbacks
local nuiCallbacks = {
    ['getUserData'] = function(data, cb)
        cb({
            name = playerData.name,
            money = playerData.money,
            job = playerData.job
        })
    end,
    
    ['performAction'] = function(data, cb)
        if data.action == 'withdraw' then
            TriggerServerEvent('banking:withdraw', data.amount)
        elseif data.action == 'deposit' then
            TriggerServerEvent('banking:deposit', data.amount)
        end
        cb('ok')
    end
}
 
-- Register all callbacks
for name, callback in pairs(nuiCallbacks) do
    RegisterNUICallback(name, callback)
end

Performance Optimization

Efficient Loops

-- Bad: Runs every frame
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)  -- Every frame
        DoExpensiveOperation()
    end
end)
 
-- Good: Runs every 100ms
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(100)  -- Every 100ms
        DoExpensiveOperation()
    end
end)
 
-- Best: Only when needed
local shouldUpdate = false
 
RegisterNetEvent('myresource:triggerUpdate')
AddEventHandler('myresource:triggerUpdate', function()
    shouldUpdate = true
end)
 
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(100)
        if shouldUpdate then
            DoExpensiveOperation()
            shouldUpdate = false
        end
    end
end)

Conditional Processing

-- Check conditions before expensive operations
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(1000)
        
        local playerPed = PlayerPedId()
        if DoesEntityExist(playerPed) and not IsEntityDead(playerPed) then
            local coords = GetEntityCoords(playerPed)
            
            -- Only process if player is in specific area
            if GetDistanceBetweenCoords(coords, bankCoords, true) < 50.0 then
                ProcessBankingLogic()
            end
        end
    end
end)

Resource Cleanup

local createdObjects = {}
local activeBlips = {}
local runningThreads = {}
 
-- Clean up on resource stop
AddEventHandler('onResourceStop', function(resourceName)
    if resourceName == GetCurrentResourceName() then
        -- Delete created objects
        for _, obj in pairs(createdObjects) do
            if DoesEntityExist(obj) then
                DeleteEntity(obj)
            end
        end
        
        -- Remove blips
        for _, blip in pairs(activeBlips) do
            if DoesBlipExist(blip) then
                RemoveBlip(blip)
            end
        end
        
        -- Stop threads
        for _, thread in pairs(runningThreads) do
            if thread then
                -- Thread cleanup logic
            end
        end
    end
end)

Common Patterns

Distance-Based Interactions

local interactionPoints = {
    {coords = vector3(0, 0, 0), text = "Press E to interact", action = function() end},
    {coords = vector3(10, 10, 10), text = "Press F to use", action = function() end}
}
 
Citizen.CreateThread(function()
    while true do
        Citizen.Wait(0)
        
        local playerCoords = GetEntityCoords(PlayerPedId())
        local nearbyPoint = nil
        
        for _, point in pairs(interactionPoints) do
            local distance = GetDistanceBetweenCoords(playerCoords, point.coords, true)
            if distance < 2.0 then
                nearbyPoint = point
                break
            end
        end
        
        if nearbyPoint then
            ShowHelpText(nearbyPoint.text)
            if IsControlJustPressed(0, 38) then -- E key
                nearbyPoint.action()
            end
        end
    end
end)

State Management

local PlayerState = {
    isInMenu = false,
    isDead = false,
    currentJob = nil,
    money = 0
}
 
function UpdatePlayerState(key, value)
    PlayerState[key] = value
    TriggerEvent('playerState:updated', key, value)
end
 
-- Listen for state changes
AddEventHandler('playerState:updated', function(key, value)
    if key == 'isInMenu' then
        SetNuiFocus(value, value)
    elseif key == 'isDead' then
        if value then
            -- Handle death
        else
            -- Handle respawn
        end
    end
end)

Animation Handling

function PlayAnimation(dict, anim, duration, flag)
    duration = duration or -1
    flag = flag or 49
    
    RequestAnimDict(dict)
    while not HasAnimDictLoaded(dict) do
        Citizen.Wait(1)
    end
    
    TaskPlayAnim(PlayerPedId(), dict, anim, 8.0, 8.0, duration, flag, 0, false, false, false)
    RemoveAnimDict(dict)
end
 
-- Usage
PlayAnimation('mp_common', 'givetake1_a', 3000, 0)

Debugging

Debug Functions

local DEBUG_MODE = true
 
function DebugPrint(...)
    if DEBUG_MODE then
        print('[DEBUG]', ...)
    end
end
 
function DrawDebugText(text, x, y)
    if DEBUG_MODE then
        SetTextFont(0)
        SetTextProportional(1)
        SetTextScale(0.0, 0.55)
        SetTextColour(255, 255, 255, 255)
        SetTextDropshadow(0, 0, 0, 0, 255)
        SetTextEdge(2, 0, 0, 0, 150)
        SetTextDropShadow()
        SetTextOutline()
        SetTextEntry("STRING")
        AddTextComponentString(text)
        DrawText(x, y)
    end
end
 
-- Show player coordinates
Citizen.CreateThread(function()
    while DEBUG_MODE do
        Citizen.Wait(0)
        local coords = GetEntityCoords(PlayerPedId())
        DrawDebugText(string.format("X: %.2f Y: %.2f Z: %.2f", coords.x, coords.y, coords.z), 0.0, 0.0)
    end
end)

Error Handling

function SafeExecute(func, ...)
    local success, result = pcall(func, ...)
    if not success then
        print('[ERROR]', result)
        return false
    end
    return result
end
 
-- Usage
SafeExecute(function()
    -- Code that might error
    local data = json.decode(someJsonString)
    ProcessData(data)
end)