Tutorial 2026-05-06

Building Custom Camera Systems for FiveM

OntelMonke

OntelMonke

Developer at Agency Scripts

Why Custom Camera Systems Matter

The default GTA V camera works well for general gameplay, but roleplay servers and custom game modes often need specialized camera behavior. Character creation screens require orbit cameras that let players rotate around their character. Cutscenes need scripted camera paths with smooth transitions. CCTV systems need fixed surveillance views. Property previews need flythrough cameras. Building a flexible camera system that handles all these use cases will dramatically improve the presentation quality of your server. This tutorial covers the FiveM camera API in depth with real-world examples you can use immediately.

Camera Fundamentals

FiveM cameras use the native CreateCam and CreateCamWithParams functions. A camera is an entity with a position, rotation, and field of view. You can create multiple cameras and switch between them, or interpolate smoothly from one camera position to another. The key concept is that rendering through a script camera disables the normal gameplay camera, so you need to handle the transition back carefully.

-- client/camera_core.lua
local CameraSystem = {
    activeCam = nil,
    isActive = false,
}

function CameraSystem.Create(coords, rot, fov)
    local cam = CreateCamWithParams(
        'DEFAULT_SCRIPTED_CAMERA',
        coords.x, coords.y, coords.z,
        rot.x, rot.y, rot.z,
        fov or 60.0,
        false, 0
    )
    return cam
end

function CameraSystem.Activate(cam, transitionTime)
    transitionTime = transitionTime or 1000

    SetCamActive(cam, true)
    RenderScriptCams(true, true, transitionTime, true, false)

    CameraSystem.activeCam = cam
    CameraSystem.isActive = true
end

function CameraSystem.Deactivate(transitionTime)
    transitionTime = transitionTime or 1000

    RenderScriptCams(false, true, transitionTime, true, false)

    if CameraSystem.activeCam then
        SetCamActive(CameraSystem.activeCam, false)
        DestroyCam(CameraSystem.activeCam, false)
        CameraSystem.activeCam = nil
    end

    CameraSystem.isActive = false
end

-- Clean up on resource stop
AddEventHandler('onResourceStop', function(resourceName)
    if GetCurrentResourceName() ~= resourceName then return end
    if CameraSystem.isActive then
        CameraSystem.Deactivate(0)
    end
end)

Orbit Camera for Character Creation

An orbit camera revolves around a central point, letting the player rotate the view by dragging the mouse. This is the standard camera for character creation, clothing stores, and barber shops. The camera maintains a fixed distance from the target and converts mouse movement into angular rotation around the center point.

-- client/orbit_camera.lua
local OrbitCam = {
    active = false,
    cam = nil,
    target = nil,
    distance = 2.0,
    angleH = 0.0,
    angleV = 20.0,
    minV = -30.0,
    maxV = 60.0,
    sensitivity = 0.3,
    fov = 45.0,
}

function OrbitCam.Start(targetEntity, distance, height)
    OrbitCam.target = targetEntity
    OrbitCam.distance = distance or 2.0
    OrbitCam.angleH = GetEntityHeading(targetEntity) + 180.0
    OrbitCam.angleV = 20.0

    local targetCoords = GetEntityCoords(targetEntity)
    local camPos = OrbitCam.CalculatePosition(targetCoords)

    OrbitCam.cam = CreateCamWithParams(
        'DEFAULT_SCRIPTED_CAMERA',
        camPos.x, camPos.y, camPos.z,
        0.0, 0.0, 0.0,
        OrbitCam.fov, false, 0
    )

    PointCamAtCoord(OrbitCam.cam, targetCoords.x, targetCoords.y, targetCoords.z + 0.5)
    SetCamActive(OrbitCam.cam, true)
    RenderScriptCams(true, true, 800, true, false)

    OrbitCam.active = true
    OrbitCam.UpdateLoop()
end

function OrbitCam.CalculatePosition(center)
    local hRad = math.rad(OrbitCam.angleH)
    local vRad = math.rad(OrbitCam.angleV)

    local x = center.x + OrbitCam.distance * math.cos(vRad) * math.sin(hRad)
    local y = center.y + OrbitCam.distance * math.cos(vRad) * math.cos(hRad)
    local z = center.z + 0.5 + OrbitCam.distance * math.sin(vRad)

    return vector3(x, y, z)
end

function OrbitCam.UpdateLoop()
    CreateThread(function()
        while OrbitCam.active do
            DisableAllControlActions(0)
            EnableControlAction(0, 1, true)   -- Mouse X
            EnableControlAction(0, 2, true)   -- Mouse Y
            EnableControlAction(0, 241, true)  -- Scroll Up
            EnableControlAction(0, 242, true)  -- Scroll Down

            -- Mouse rotation
            local mouseX = GetDisabledControlNormal(0, 1) * OrbitCam.sensitivity * 8.0
            local mouseY = GetDisabledControlNormal(0, 2) * OrbitCam.sensitivity * 8.0

            OrbitCam.angleH = OrbitCam.angleH - mouseX
            OrbitCam.angleV = math.max(OrbitCam.minV,
                math.min(OrbitCam.maxV, OrbitCam.angleV + mouseY))

            -- Scroll zoom
            if IsDisabledControlPressed(0, 241) then
                OrbitCam.distance = math.max(0.5, OrbitCam.distance - 0.1)
            elseif IsDisabledControlPressed(0, 242) then
                OrbitCam.distance = math.min(5.0, OrbitCam.distance + 0.1)
            end

            local targetCoords = GetEntityCoords(OrbitCam.target)
            local camPos = OrbitCam.CalculatePosition(targetCoords)

            SetCamCoord(OrbitCam.cam, camPos.x, camPos.y, camPos.z)
            PointCamAtCoord(OrbitCam.cam, targetCoords.x, targetCoords.y, targetCoords.z + 0.5)

            Wait(0)
        end
    end)
end

function OrbitCam.Stop()
    OrbitCam.active = false
    RenderScriptCams(false, true, 800, true, false)

    if OrbitCam.cam then
        SetCamActive(OrbitCam.cam, false)
        DestroyCam(OrbitCam.cam, false)
        OrbitCam.cam = nil
    end
end

Camera Interpolation and Transitions

Smooth camera transitions between two positions create cinematic effects for cutscenes, property tours, and loading screens. FiveM provides SetCamActiveWithInterp which handles the interpolation natively, but you can also build custom easing functions for more control over the transition curve.

-- client/camera_transition.lua
local function TransitionCamera(fromPos, fromRot, toPos, toRot, duration, fov)
    fov = fov or 50.0

    local camFrom = CreateCamWithParams('DEFAULT_SCRIPTED_CAMERA',
        fromPos.x, fromPos.y, fromPos.z,
        fromRot.x, fromRot.y, fromRot.z,
        fov, false, 0)

    local camTo = CreateCamWithParams('DEFAULT_SCRIPTED_CAMERA',
        toPos.x, toPos.y, toPos.z,
        toRot.x, toRot.y, toRot.z,
        fov, false, 0)

    SetCamActive(camFrom, true)
    RenderScriptCams(true, false, 0, true, false)

    -- Interpolate from first camera to second
    SetCamActiveWithInterp(camTo, camFrom, duration, 1, 1)

    Wait(duration)

    -- Clean up the first camera
    DestroyCam(camFrom, false)

    return camTo
end

-- Usage: Flythrough preview of a property
local function PropertyPreview(waypoints, duration)
    local perSegment = duration / (#waypoints - 1)
    local currentCam = nil

    for i = 1, #waypoints - 1 do
        local wp = waypoints[i]
        local nextWp = waypoints[i + 1]

        currentCam = TransitionCamera(
            wp.pos, wp.rot,
            nextWp.pos, nextWp.rot,
            perSegment, wp.fov or 60.0
        )
    end

    -- Return to gameplay camera
    Wait(500)
    RenderScriptCams(false, true, 1000, true, false)
    if currentCam then DestroyCam(currentCam, false) end
end

CCTV Surveillance Camera System

A CCTV system uses fixed cameras positioned around the map that players can cycle through. Each camera has a predefined position and rotation, and the system applies a post-processing effect to simulate the look of security footage. This is commonly used in police stations and business interiors.

-- client/cctv.lua
local CCTV = {
    cameras = {},
    currentIndex = 0,
    activeCam = nil,
    active = false,
}

function CCTV.AddCamera(name, coords, rot, fov)
    table.insert(CCTV.cameras, {
        name = name,
        coords = coords,
        rot = rot,
        fov = fov or 60.0,
    })
end

function CCTV.Start(startIndex)
    CCTV.currentIndex = startIndex or 1
    CCTV.active = true
    CCTV.SwitchTo(CCTV.currentIndex)

    CreateThread(function()
        while CCTV.active do
            -- Apply security camera filter
            SetTimecycleModifier('CAMERA_secuirity')
            SetTimecycleModifierStrength(1.0)

            DisableAllControlActions(0)
            EnableControlAction(0, 174, true) -- Left Arrow
            EnableControlAction(0, 175, true) -- Right Arrow
            EnableControlAction(0, 202, true) -- Escape

            -- Cycle cameras
            if IsDisabledControlJustPressed(0, 175) then
                local next = CCTV.currentIndex + 1
                if next > #CCTV.cameras then next = 1 end
                CCTV.SwitchTo(next)
            elseif IsDisabledControlJustPressed(0, 174) then
                local prev = CCTV.currentIndex - 1
                if prev < 1 then prev = #CCTV.cameras end
                CCTV.SwitchTo(prev)
            elseif IsDisabledControlJustPressed(0, 202) then
                CCTV.Stop()
            end

            Wait(0)
        end
    end)
end

function CCTV.SwitchTo(index)
    local data = CCTV.cameras[index]
    if not data then return end

    local newCam = CreateCamWithParams('DEFAULT_SCRIPTED_CAMERA',
        data.coords.x, data.coords.y, data.coords.z,
        data.rot.x, data.rot.y, data.rot.z,
        data.fov, false, 0)

    if CCTV.activeCam then
        SetCamActiveWithInterp(newCam, CCTV.activeCam, 500, 1, 1)
        Wait(500)
        DestroyCam(CCTV.activeCam, false)
    else
        SetCamActive(newCam, true)
        RenderScriptCams(true, true, 500, true, false)
    end

    CCTV.activeCam = newCam
    CCTV.currentIndex = index
end

function CCTV.Stop()
    CCTV.active = false
    ClearTimecycleModifier()
    RenderScriptCams(false, true, 800, true, false)

    if CCTV.activeCam then
        DestroyCam(CCTV.activeCam, false)
        CCTV.activeCam = nil
    end
end

Camera System Best Practices

  • Always destroy cameras when done. Undestroyed cameras are a memory leak. Track every camera handle and clean them up in onResourceStop.
  • Disable controls during camera sequences. Players should not be able to walk, shoot, or interact while a scripted camera is active unless intentionally allowed.
  • Use appropriate FOV values. Normal gameplay uses 50-60 FOV. Cinematic shots use 30-40 FOV for a telephoto look. Wide establishing shots use 70-90 FOV.
  • Hide the HUD during camera sequences. Use DisplayHud(false) and DisplayRadar(false) to remove gameplay elements during cinematic cameras.
  • Test transitions at different frame rates. Camera interpolation can look different at 30fps vs 144fps. Test on low-end hardware to ensure smooth behavior.

Share this article

Ready to upgrade your server?

Check out our premium FiveM scripts in the Agency Scripts store or join our Discord community for support and updates.