Scripted camera

From Wiki G1R-MP G1 Remake Multiplayer
Revision as of 00:07, 9 September 2026 by QCherry (talk | contribs) (Update 0.1.3 camera world placement, streaming safety and far camera test)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Scripted camera

AVAILABLE FROM UPDATE 0.1.3
The scripted-camera functions and client events described here are available in G1R:MP 0.1.3 and later.

Use a scripted camera for a login background, character presentation, a fixed shot, a travelling shot or an orbit. It controls the local view without teleporting the character or possessing a spectator pawn. Camera control does not freeze player input, make the character invulnerable, or change server gameplay rules.

Function reference

All functions support the documented lowerCamelCase name and an equivalent PascalCase name: for example, setCameraPos and SetCameraPos.

Purpose Functions Execution side
Position, rotation and view setCameraPos, setCameraRot, setCameraLookAt, setCameraFOV, setCameraPose Client and server
Movement moveCameraTo, playCameraPath, orbitCamera, stopCameraMovement Client and server
Restore gameplay view resetCameraPos Client and server
Cached read-back getCameraPos, getCameraRot, getCameraFOV, getCameraState Client only
Refresh cached state requestCameraState Client only

Client mutators return a positive request ID, or false when validation or queuing fails. The ID is not an execution acknowledgement. Server mutators prepend playerId and return a queued boolean instead:

-- Client-side, current resource and local player:
local requestId = setCameraFOV(70)

-- Server-side, a connected player:
local queued = setCameraFOV(playerId, 70)

Server-side true does not confirm the target client's camera is ready, accepts ownership, has finished world streaming or displays the requested view. There are no server camera getters or server camera-acknowledgement events. Use client scripting for sequences that must wait for execution results. Do not mix client and server ownership within one sequence.

Units, poses and limits

Value Contract
Position World-space metres; finite coordinates between -1,000,000 and 1,000,000
Distance from player No distance limit relative to the local character; positions, paths and orbits may target distant locations in the current world, subject to the coordinate limits
Rotation Pitch, yaw and roll in degrees; finite absolute values at most 360,000; shortest-angle interpolation
FOV 15 to 150 degrees inclusive
Timings Whole milliseconds; general movement duration 0 to 600,000; reset blend 0 to 10,000
Easing Case-sensitive linear, smooth, in, out; default smooth

A complete pose has x, y, z, pitch, yaw, roll, fov. Client-side setCameraPose, moveCameraTo and the first path point can inherit missing fields from the current resource's ready acknowledged cache. Without that cache, supply all seven fields. Later client path points inherit missing pose fields from the previous point.

On the server, setCameraPose, moveCameraTo and every path point independently require x,y,z. Missing angles default to 0 and FOV to 90. There is no server-side pose inheritance.

Position-only, rotation-only and FOV-only setters preserve the remaining current camera fields. Omitting roll in setCameraRot sets roll to 0. setCameraLookAt rotates towards a fixed world point, sets roll to 0, and fails if the target coincides with the camera position. It is not a persistent follow attachment.

Paths

playCameraPath(points, options) accepts a dense 1-based array of 1 to 32 poses. Each point's durationMs defaults to 2000 and must be at least 1; holdMs defaults to 0. The sum of all travel and hold times must not exceed 600,000 ms. Options are loop=false and easing="smooth" by default. A loop repeats the sequence until stopped, reset or interrupted; the 600,000 ms limit applies to one complete sequence, not the lifetime of a loop.

Orbits

orbitCamera(center, options) takes a fixed {x,y,z} centre, not a player ID. Defaults are radius 5 m, height 2 m, startAngle 0 degrees, sweepAngle 360 degrees, loop false and easing smooth. Radius must be 0.1 to 50 m, height -50 to 50 m, startAngle within +/-360,000 degrees and sweepAngle within +/-3600 degrees. A negative sweep reverses direction. The current FOV is preserved.

Client-side options must explicitly provide durationMs from 100 to 600,000. On the server, the options table is required but a missing durationMs defaults to 10,000. The centre does not follow a moving player. The centre and complete orbit extent must fit the finite coordinate range: center.x +/- radius, center.y +/- radius and center.z + height must each remain between -1,000,000 and 1,000,000 metres. This conservative check applies even to a partial sweep; it is not a distance limit from the character.

Acknowledgements and cached getters

The built-in local event onClientCameraCommandResult is attached to resourceRoot; no addEvent call is needed. Its callback is:

function(requestId, ok, phase, errorText, stateJson)
Command Successful phases
requestCameraState state
Position/rotation/LookAt/FOV/pose setters; stopCameraMovement applied
moveCameraTo; non-looping playCameraPath/orbitCamera applied, then completed using the same requestId
Looping path/orbit applied; no completed while looping
resetCameraPos reset after the restore transition

Execution rejection reports ok=false, phase="error". Replacing, stopping or resetting an active movement can report ok=false, phase="cancelled" for the earlier movement request. Interrupted reset requests can report an error. Match request IDs: a cancellation for an older movement is not a failure of the new command.

A validated state snapshot is cached before the callback, including an available valid snapshot attached to a failed result. A query can be submitted before the local camera/world is ready if IPC is connected, and can successfully return phase="state", ready=false; always check ready before using the pose or starting control. getCameraPos returns {x,y,z}, getCameraRot returns {pitch,yaw,roll}, and getCameraFOV returns a number. These return false when no ready snapshot exists. getCameraState returns false with no cache, or a table which may have ready=false.

The state table contains:

Fields Meaning
x, y, z; pitch, yaw, roll; fov Last acknowledged engine camera pose, in metres/degrees
playerX, playerY, playerZ Local character position in metres
ready Whether valid local-world/camera context is available for the snapshot
active Whether the scripted camera is active
moving Whether a scripted movement is active
sampleTimeMs Timestamp of the native sample; not a UTC wall-clock time

The event's JSON additionally includes version=1; getCameraState() does not expose that version field. Failed results may have empty stateJson, so parse defensively.

Getters are not synchronous engine queries. An applied acknowledgement confirms command handling; completed confirms the interpolation reached its end. Neither proves the new camera view has rendered. An immediate acknowledgement snapshot can still contain the previous frame's pose. If you need post-change values, wait another frame or a short timer, call requestCameraState, and handle its state result. Do not issue a new query unconditionally for every result, which would create a feedback loop.

Ownership, stopping and cleanup

Only one resource/origin can own scripted camera control at a time. A client resource and a server resource with the same name are still different origins. Another resource's mutating request is rejected while the owner is active. State queries do not take ownership and can be used by other client resources.

stopCameraMovement leaves the camera in place and keeps ownership. A non-looping movement reaching completed also leaves the scripted camera active. Use resetCameraPos to restore the player's gameplay camera and release your ownership. Reset is owner-scoped; it does not steal another resource's camera or forcefully replace a foreign cutscene.

Resource stop/start failure automatically cleans up camera ownership; do not depend on a camera-reset callback while the resource is closing. Use the normal resource-stop lifecycle to cancel your own timers. For a still-running resource, onClientCameraReset reports forced native lifecycle cleanup or IPC invalidation. Native forced cleanup targets the camera owner; IPC disconnect also notifies affected query-only resources with cached state or pending requests. The cache, pending requests and ownership bookkeeping are cleared before the callback. On IPC disconnect, stateJson can be empty; this notification alone does not prove a newly restored view has rendered. Wait for world/spawn readiness before starting again. Old request IDs and connection generations must not be replayed.

Distant views and world streaming

Camera positions, paths and orbits are not restricted by distance from the local character. They address locations in the current world; they do not switch maps or make coordinates outside the game's actual terrain meaningful.

Unreal's standard local PlayerController streaming source follows its active ViewTarget, which this API changes to the scripted camera.

Before taking that view, the native module adds its own WorldPartitionStreamingSourceComponent to the validated local pawn. The engine follows the component owner's position, preserving a streaming source around the character without teleporting it or freezing input. It uses the world's default grid loading range without changing global streaming settings or existing sources. If this safeguard cannot be prepared, camera takeover is rejected with camera-pawn-streaming-source-unavailable.

The pawn's source remains active throughout a reset blend until the return is confirmed. Deferred cleanup or a foreign cutscene keeps it until a safe return to the pawn or pawn/world teardown. stopCameraMovement and natural completed do not release it because scripted camera ownership remains active. Keeping both areas loaded can increase memory and streaming costs and affect frame rate.

World streaming is asynchronous: a large camera jump can initially show missing terrain, low-detail scenery or assets still loading. An applied, completed or state acknowledgement does not confirm streaming completion, and ready describes camera/world context rather than readiness of every visible asset. A fresh pose read-back checks where the camera is, not whether the remote scenery has finished loading. Allow time for streaming and manually check the distant scene and the return to the character; camera commands do not provide an all-visible-assets-loaded event.

Camera movement does not change the server's area of interest: distant players/NPCs/monsters are still replicated relative to the character. Streaming scenery around the camera does not request those remote multiplayer entities. This API is not long-range multiplayer spectator support. The older experimental setCameraTarget/resetCamera entry points are separate APIs, not aliases of these functions; remote-player target following is not implemented by this scripted-camera feature.

Example: a moving login background

This client-side example demonstrates the public APIs. Call beginLoginCamera() from your UI flow only after the local game world/player camera is ready. Call finishLoginCameraAfterSpawn() only after your resource has confirmed the actual character spawn has completed. Authentication success alone is not that confirmation. These two helper functions are defined by the example; they are not built-in events or engine functions.

local showingLogin = false
local queryRequest, fovRequest, orbitRequest, resetRequest
local orbitCenter

function beginLoginCamera()
    if showingLogin then return end
    showingLogin = true
    queryRequest = requestCameraState()
    if not queryRequest then showingLogin = false end
end

local function releaseLoginCamera()
    showingLogin = false
    queryRequest, fovRequest, orbitRequest = nil, nil, nil
    resetRequest = resetCameraPos(800)
    if not resetRequest then
        outputDebugString("Login camera reset was not queued", 2)
    end
end

function finishLoginCameraAfterSpawn()
    releaseLoginCamera()
end

addEventHandler("onClientCameraCommandResult", resourceRoot,
    function(requestId, ok, phase, errorText)
        if requestId == resetRequest then
            if not ok or phase == "reset" then resetRequest = nil end
            if not ok then outputDebugString("Camera reset: " .. tostring(errorText), 2) end
            return
        end
        if not showingLogin then return end
        if requestId ~= queryRequest and requestId ~= fovRequest
            and requestId ~= orbitRequest then return end
        if not ok then
            outputDebugString("Login camera: " .. tostring(errorText), 2)
            releaseLoginCamera() -- Abort this presentation and release our view.
            return
        end
        if requestId == queryRequest and phase == "state" then
            queryRequest = nil
            local state = getCameraState()
            if not state or not state.ready then
                showingLogin = false
                return
            end
            orbitCenter = {x=state.playerX, y=state.playerY, z=state.playerZ + 1.2}
            fovRequest = setCameraFOV(70)
            if not fovRequest then releaseLoginCamera() end
        elseif requestId == fovRequest and phase == "applied" then
            fovRequest = nil
            orbitRequest = orbitCamera(orbitCenter,
                {radius=5, height=2, durationMs=30000, loop=true, easing="linear"})
            if not orbitRequest then releaseLoginCamera() end
        elseif requestId == orbitRequest and phase == "applied" then
            outputDebugString("Login camera orbit started")
        end
    end)

addEventHandler("onClientCameraReset", resourceRoot, function(reason)
    showingLogin = false
    queryRequest, fovRequest, orbitRequest, resetRequest = nil, nil, nil, nil
    orbitCenter = nil
    outputDebugString("Login camera invalidated: " .. tostring(reason))
end)

For production UI, also bound the time spent waiting for acknowledgements and cancel your own timers when the resource stops or the camera is invalidated. Do not retry indefinitely or reacquire the camera on every frame. Freeze/unfreeze gameplay input separately only if your resource's login flow requires it; camera ownership itself does not manage input.

Manual test in the bundled GothicRP resource

After login and confirmed character spawn, run /cameratest in a safe place. Its original 19 stages remain local to the character and exercise position, rotation including roll, LookAt, FOV, cached getters, full poses, smooth and linear motion, paths/holds/loops, orbits, stop, movement replacement and reset.

The optional /cameratest far runs a separate 14-stage sequence around a fixed scene approximately 300 m from the starting character position. It tests a distant position and LookAt, smooth movement, a three-point path, a full orbit, a reverse looping orbit, stop and return to the player camera. Five fresh state queries, issued at least 350 ms after the corresponding acknowledgement, verify that the camera is more than 200 m from the starting character position, is stopped where expected, and reaches the requested endpoints within 1 m. The final reset is acknowledged and followed by a fresh inactive/not-moving check. The far test reports an error on the old distance-limited client or when an acknowledgement arrives without the expected position change.

/cameratest stop cancels either sequence and requests a reset. A missing native acknowledgement fails the current step after 20 seconds and requests a reset. Restarting a test retires the previous run's timers and pending request; delayed results from that run do not advance the new sequence. Chat messages are in English.

These are GothicRP resource commands, not built-in camera APIs available in every gamemode. They do not teleport the character, freeze input or modify progression. A success report confirms the tested commands/read-backs, not terrain loading or rendered image quality: a human must inspect the distant scenery, camera movement and final return to the player camera.