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)andDisplayRadar(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.