>
Tutorial 2026-05-06

Cómo crear sistemas de cámaras personalizadas para FiveM

OntelMonke

OntelMonke

Desarrollador de Agency Scripts

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) y DisplayRadar(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.

Compartir este artículo

¿Listo para mejorar tu servidor?

Echa un vistazo a nuestros scripts premium de FiveM en la tienda de Agency Scripts o únete a nuestra comunidad de Discord para soporte y novedades.