Por qué importan los sistemas de cámara personalizados
La cámara por defecto de GTA V funciona bien para el juego general, pero los servidores de rol y los modos personalizados a menudo necesitan comportamiento especializado. Las pantallas de creación de personaje requieren cámaras orbitales que dejen rotar alrededor del personaje. Las cinemáticas necesitan rutas scripteadas con transiciones suaves. Los sistemas CCTV necesitan vistas fijas de vigilancia. Las presentaciones de propiedades necesitan cámaras de recorrido. Construir un sistema de cámara flexible que cubra todos estos casos mejorará radicalmente la calidad de presentación de tu servidor. Este tutorial cubre la API de cámara de FiveM en profundidad con ejemplos reales que puedes usar al momento.
Fundamentos de cámara
Las cámaras de FiveM usan las funciones native CreateCam y CreateCamWithParams. Una cámara es una entidad con posición, rotación y campo de visión. Puedes crear varias cámaras y alternar entre ellas, o interpolar suavemente de una posición a otra. El concepto clave es que renderizar a través de una cámara scripteada desactiva la cámara normal del juego, por lo que hay que gestionar con cuidado la transición de vuelta.
-- 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
-- Limpia al detener el recurso
AddEventHandler('onResourceStop', function(resourceName)
if GetCurrentResourceName() ~= resourceName then return end
if CameraSystem.isActive then
CameraSystem.Deactivate(0)
end
end)
Cámara orbital para creación de personajes
Una cámara orbital gira alrededor de un punto central y deja al jugador rotar la vista arrastrando el ratón. Es la cámara estándar para creación de personaje, tiendas de ropa y barberías. La cámara mantiene una distancia fija respecto al objetivo y convierte el movimiento del ratón en rotación angular alrededor del punto central.
-- 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 arriba
EnableControlAction(0, 242, true) -- Scroll abajo
-- Rotación con el ratón
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))
-- Zoom con la rueda
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
Interpolación y transiciones de cámara
Las transiciones suaves entre dos posiciones de cámara crean efectos cinemáticos para cinemáticas, tours de propiedades y pantallas de carga. FiveM ofrece SetCamActiveWithInterp, que gestiona la interpolación de forma nativa, pero también puedes construir funciones de easing personalizadas para un mayor control sobre la curva de transición.
-- 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)
-- Interpola de la primera cámara a la segunda
SetCamActiveWithInterp(camTo, camFrom, duration, 1, 1)
Wait(duration)
-- Libera la primera cámara
DestroyCam(camFrom, false)
return camTo
end
-- Uso: recorrido de presentación de una propiedad
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
-- Vuelve a la cámara de gameplay
Wait(500)
RenderScriptCams(false, true, 1000, true, false)
if currentCam then DestroyCam(currentCam, false) end
end
Sistema de cámaras CCTV
Un sistema CCTV utiliza cámaras fijas colocadas por el mapa entre las que los jugadores pueden ir alternando. Cada cámara tiene una posición y rotación predefinidas y el sistema aplica un efecto de postprocesado para simular el aspecto del circuito cerrado. Se usa habitualmente en comisarías e interiores de negocios.
-- 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
-- Aplica el filtro de cámara de seguridad
SetTimecycleModifier('CAMERA_secuirity')
SetTimecycleModifierStrength(1.0)
DisableAllControlActions(0)
EnableControlAction(0, 174, true) -- Flecha izquierda
EnableControlAction(0, 175, true) -- Flecha derecha
EnableControlAction(0, 202, true) -- Escape
-- Cambia de cámara
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
Buenas prácticas con sistemas de cámara
- Destruye siempre las cámaras al terminar. Las cámaras sin destruir son una fuga de memoria. Controla cada handle de cámara y libéralas en
onResourceStop. - Desactiva los controles durante las secuencias de cámara. El jugador no debería poder caminar, disparar ni interactuar mientras haya una cámara scripteada activa, salvo que lo permitas a propósito.
- Usa valores de FOV adecuados. El juego normal usa 50-60 de FOV. Las tomas cinemáticas emplean 30-40 para un efecto teleobjetivo. Los planos generales amplios usan 70-90.
- Oculta el HUD durante las secuencias. Usa
DisplayHud(false)yDisplayRadar(false)para quitar elementos de gameplay durante cámaras cinemáticas. - Prueba las transiciones a distintas tasas de FPS. La interpolación puede verse distinta a 30fps frente a 144fps. Prueba en hardware modesto para asegurarte de un comportamiento fluido.
