Wróć do bloga
Tutorial5 min czytania

System kamer FiveM: własne kamery, CCTV i ujęcia filmowe

Opanuj tworzenie skryptów kamer w FiveM. Freecam, kamery monitoringu dla policji, ujęcia filmowe i niestandardowe UI kamer z praktycznym kodem oraz zestawieniem najlepszych skryptów.

Agency Scripts

Założyciel i główny deweloper w Agency Scripts

Dlaczego własne systemy kamer mają znaczenie

Domyślna kamera w GTA V sprawdza się w standardowej rozgrywce, ale serwery roleplay i niestandardowe tryby gry często wymagają specjalnego zachowania kamery. Ekrany tworzenia postaci wymagają kamer orbitalnych, które pozwalają obracać widok wokół postaci. Scenki przerywnikowe potrzebują oskryptowanych ścieżek kamery z płynnymi przejściami. Systemy CCTV wymagają stałych widoków monitoringu. Podglądy nieruchomości potrzebują kamer przelatujących przez wnętrza. Zbudowanie elastycznego systemu kamer obsługującego wszystkie te przypadki diametralnie poprawi jakość prezentacji twojego serwera. W tym poradniku szczegółowo omawiamy FiveM camera API wraz z praktycznymi przykładami do natychmiastowego wykorzystania.

Podstawy kamery

Kamery FiveM używają natywnej funkcji CreateCam i CreateCamWithParams funkcje. Kamera to encja posiadająca pozycję, rotację i pole widzenia. Możesz utworzyć wiele kamer i przełączać się między nimi lub płynnie interpolować z jednej pozycji kamery do drugiej. Kluczową kwestią jest to, że renderowanie przez kamerę skryptową wyłącza zwykłą kamerę rozgrywki, więc musisz ostrożnie obsłużyć przejście powrotne.

-- 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)

Kamera orbitalna do tworzenia postaci

Kamera orbitalna obraca się wokół centralnego punktu, pozwalając graczowi obracać widok poprzez przeciąganie myszą. To standardowa kamera do tworzenia postaci, sklepów odzieżowych i salonów fryzjerskich. Kamera utrzymuje stałą odległość od celu i przekształca ruch myszy w obrót kątowy wokół punktu środkowego.

-- 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

Interpolacja i przejścia kamery

Płynne przejścia kamery między dwiema pozycjami tworzą kinowe efekty w cutscenkach, prezentacjach nieruchomości i na ekranach ładowania. FiveM oferuje SetCamActiveWithInterp co natywnie obsługuje interpolację, ale możesz także zbudować własne funkcje easing dla większej kontroli nad krzywą przejścia.

-- 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

System kamer monitoringu CCTV

System CCTV wykorzystuje stałe kamery rozmieszczone na mapie, które gracze mogą przełączać. Każda kamera ma z góry określoną pozycję i rotację, a system nakłada efekt postprocessingu, aby symulować obraz z kamer monitoringu. Jest to powszechnie stosowane na komisariatach policji i we wnętrzach firm.

-- 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

Dobre praktyki systemu kamery

  • Zawsze niszcz kamery po zakończeniu. Nieusunięte kamery powodują wyciek pamięci. Śledź każdy uchwyt kamery i czyść je w onResourceStop.
  • Wyłącz sterowanie podczas sekwencji kamery. Gracze nie powinni mieć możliwości chodzenia, strzelania ani interakcji, gdy aktywna jest oskryptowana kamera, chyba że zostało to celowo dozwolone.
  • Używaj odpowiednich wartości FOV. Normalna rozgrywka używa FOV 50-60. Ujęcia filmowe używają FOV 30-40 dla efektu teleobiektywu. Szerokie ujęcia wprowadzające używają FOV 70-90.
  • Ukryj HUD podczas sekwencji kamery. Użyj DisplayHud(false) i DisplayRadar(false) aby usuwać elementy rozgrywki podczas ujęć z kamery filmowej.
  • Przetestuj przejścia przy różnych wartościach klatek na sekundę. Interpolacja kamery może wyglądać inaczej przy 30 fps a 144 fps. Przetestuj na słabszym sprzęcie, aby upewnić się o płynnym działaniu.

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.