Quick navigation

Go to

Cameras & cinematics

Third person

Player preferences, server policy and camera ownership.

10 min read2,285 wordsMarkdown
On this page10 sections

Open77's third-person view renders a local character body using the replication system. The player entity retains gameplay authority: aiming, hit detection and network state are unchanged.

Nothing changes for other players. The stand-in body is local: nobody else ever sees it, and the same bytes leave your client whether you are in first or third person.

permissions { "perspective.policy" }   -- server policy, or a client-side forced view

Hide the local avatar

Use setLocalPuppetVisible to hide the body and held weapon without leaving third person. The same request keeps the native body hidden when switching to first person. Server Open77.players.setVisible controls observer visibility separately.

The player's side

Key f6 by default, reassignable, and remembered. f7 also works and always will: it lives in the client itself, so it toggles the view even on a server that ships no perspective resource at all. Rebinding f7 is not possible without a new build.
Look around without turning Hold the middle mouse button. The body freezes its facing where it stood and the camera swings around it, so you can look behind yourself while still walking forward. Let go and the body catches up. Your aim is unaffected β€” the character keeps facing wherever the mouse points, so the shot goes where the reticle is, not where the body appears to look.
Remembered per server The preference is stored per connection address, so two servers keep two independent choices.
Survives Reconnecting, a resource reload, restarting the game.
While a panel has the keyboard The key does nothing while chat, the pause menu or an admin surface owns input, so it can never fire underneath a text field.

A player who has never asked gets first person.

Enable, disable or temporarily force a view (client)

Requires the client update introducing setThirdPerson and clearThirdPersonOverride. On older clients, check that the function exists before calling it. Configuring a camera style does not enable or lock the perspective by itself.

-- Ordinary switch: the player can still change the view with F7.
Open77.perspective.setThirdPerson(true)         -- request third person
Open77.perspective.setThirdPerson(false)        -- request first person

-- Requires permissions { "perspective.policy" } in the resource manifest.
Open77.perspective.setThirdPerson(true, true)   -- force third person, block F7 -> FPP
Open77.perspective.setThirdPerson(false, true)  -- force first person, block F7 -> TPP

-- Release only this resource's lock, keeping the player's previous preference.
Open77.perspective.clearThirdPersonOverride()

setThirdPerson(enabled[, force]) takes strict booleans (force=false by default). It returns true when the request is accepted, or false, reason. force=false also releases this resource's previous lock before requesting the new view. It does not unlock another resource's lock or override server policy. To forbid third person rather than merely switch away, use setThirdPerson(false, true).

A forced view is temporary and exclusive to the requesting resource instance. Another resource's forced request returns perspective_owned. Ordinary opposing requests (including F7) are refused without changing the pre-lock preference. clearThirdPersonOverride() is idempotent, needs no capability, and cannot clear someone else's lock. Stop/reload, coroutine error and host teardown release the owner's lock automatically; the selected camera style and the server policy are separate and are not changed by this API.

World safety and native vehicle/cutscene cameras, then the server's disabled/forced policy, take priority over this local override. A conflicting server policy rejects a new lock with refused_by_policy. If the server policy changes while a lock exists, the policy wins; the still-owned lock resumes when the policy permits it again. No Lua per-frame loop is needed.

Open77.perspective.state() includes resourceForced and resourcePerspective ("tps", "fpp", or "none"). These describe the requested lock, not a guarantee that it currently owns a vehicle/cutscene camera; inspect mode, camera, reason and settled for the actual view. An active local lock reports reason="resource_forced" and allowed=false.

Other errors include invalid_perspective_arguments, permission_denied:perspective.policy (forced calls only), and perspective_unavailable_on_this_host. The API is client-local: use the existing server setPolicy API below for all players, or a server event to ask a selected client resource to apply a local lock. It is not a networked player animation.

Server policy

Four values. The platform default is allowed, and a server whose resources declare nothing sends nothing β€” every client simply keeps allowed. That is the intended behaviour, not a fallback.

Policy Meaning
disabled Third person is not available here.
allowed The player chooses. Unasked players get first person.
default The player chooses. As shipped this behaves exactly like allowed β€” the unasked default it is supposed to change is a build-time constant nothing writes yet, so it is still first person. Pass default if you want to state the intent; do not rely on it to put anyone in third person.
forced The server pins a view. The player's own request is still remembered, so lifting the pin gives them back what they wanted rather than dropping them somewhere arbitrary.
-- server_script; the manifest needs "network.events" and "perspective.policy"
AddEventHandler("onResourceStart", function(name)
    if name ~= GetCurrentResourceName() then return end
    Open77.perspective.setPolicy("allowed")
end)

-- A stealth round where the camera must not see around corners:
Open77.perspective.setPolicy("forced", "fpp")

-- Back to the players' choice, third person for anyone who never asked:
Open77.perspective.setPolicy("default", "tps")

Call it from the resource's start path. A reload replaces the VM and the policy lives in that VM, so a resource that declared its policy from somewhere else comes back declaring nothing.

Let one resource own the policy. If two declare one, both answer a joining client and the last packet wins β€” the same caveat Open77.notifications carries.

When the view hands itself back

Third person is not a mode that overrides everything. Several world states take the view back automatically, and give it back afterwards. This is by design: the vanilla body and the native cameras are the fallback, and they are always allowed to win.

State What happens
In a vehicle The engine owns the view, and it obeys the player's own vanilla preference.
Scenes and scripted sequences Native camera.
Photo mode Native camera.
Death and ragdoll Native camera.
Sliding, and vaulting First person for the length of the move, then straight back.
The pre-game UI, including character creation The engine owns the screen for as long as it needs it.

Each handover returns the player's real body and every effect that belongs to it β€” muzzle flash, shells, footsteps, audio β€” without the player doing anything.

Sliding can hand the view back to first person when the third-person rig has no compatible slide clip. This changes presentation only, not movement or player state.

Vaulting is on the list for the same kind of reason and has never come off it.

The search for a real slide clip is closed rather than merely unfinished: the shipped game has no slide animation authored for this body, in its data, in its animation catalogue, or in its scripts. If one is ever added, the handover comes off again β€” the machinery that drives it is still in place and still running.

The crosshair

Third person draws its own. The vanilla reticle belongs to the weapon's HUD and is anchored to the first-person aiming camera, so with a third-person camera it has nothing to anchor to and is simply not drawn β€” which is why aiming with a gun used to leave the screen with no crosshair at all. The client bootstrap open77_reticle draws a centred overlay while aiming with a weapon in third person, including native F7 on servers without open77_perspective. It follows the game's aiming state, not a hard-coded mouse button. It hides when aim is released, the weapon is holstered, a menu captures input, or an engine/first-person camera takes over. Vanilla hip-fire and first-person HUD behaviour are unchanged. The overlay takes no clicks and cannot swallow input.

Reading the aim from a resource

Open77.character answers what the player is doing with a weapon, and what is in front of it. It works the same in both perspectives.

Call Answers
Open77.character.isAiming() Aiming down sights. boolean, or nil, reason.
Open77.character.isFiring() The trigger is held β€” the IsPedShooting equivalent, not a shot event.
Open77.character.aimState() aiming, firing, weaponDrawn and stateSequence, in one snapshot.
Open77.character.aimedEntity() What is under the crosshair, or a bare nil for scenery.

The first three require the player.aim.read permission. aimedEntity() requires both player.aim.read and world.query: where the player is looking is a fact about the player, and what is there is a world entity read, so granting the first must not quietly hand out the second.

permissions { "player.aim.read", "world.query" }

Do not read perspective.state().aiming for this

That field exists and is not the same thing. It has exactly two writers: a trigger pull, and the third-person self-view's per-frame gate β€” and the second is skipped entirely whenever the self-view session is not active, which is to say in first person nothing writes it at all. A resource reading it in FPP sees whatever the last shot said, for as long as the player does not shoot again. It was published so a player refused a hip-fire shot could see why; it is not an aim poll, and it never was one.

Open77.character.isAiming() reads the player's own weapon animation graph instead. The call costs no raycast, advances no sequence and disturbs no ballistics.

aiming is a latched mirror, and stateSequence is how you tell

Nothing samples the aim on a timer. The observer records whatever the animation graph last pushed, and between two pushes the bit simply keeps its previous value. So aiming == true on its own cannot distinguish aiming right now from the last thing this feature ever said was aim, some minutes ago β€” and the second reads exactly like the defect where an aim pose never comes down.

aimState().stateSequence rises on every observed change. Difference it across two reads and the question is settled:

local before = Open77.character.aimState()
Wait(500)
local after = Open77.character.aimState()
if after.aiming and after.stateSequence == before.stateSequence then
    -- held for at least half a second, or the graph has gone quiet
end

It is evidence, never a gate. Nothing times an aim out, because a player may hold one as long as they like.

What aimedEntity() answers with

The engine's own look-at target, not a physics ray β€” a physics ray answers with geometry and carries no entity reference at all. Three outcomes, kept apart on purpose:

Result Meaning
a table Something is under the crosshair.
a bare nil, no second value Pointing at scenery, or at nothing. An answer.
nil, reason The question could not be asked β€” permission, or no target system.

The table carries engineEntity, className, kind, family, position and distance, plus entity and one of playerId / vehicleId / npcId when the thing is an Open77 body. family is the raw class family and kind is that family refined by ownership β€” the same refinement Open77.camera.aimRay({ entities = true }) applies, from the same classifier, so a vanilla crowd ped reads populationNpc through both and a server-spawned one reads npc. A resource branching on kind == "npc" therefore never acts on a body the server does not own.

local target = Open77.character.aimedEntity()
if target == nil then return end                     -- nothing there
if target.kind == "player" then
    reportAimedAt(target.playerId)
elseif target.kind == "trafficVehicle" then
    -- vanilla traffic: no vehicleId, and no server write will reach it
end

Limitations

Known presentation limitations on Cyberpunk 2077 2.31:

The firing weapon and the weapon drawn on the third-person body can have different muzzle positions. Damage and native effects use the actual firing weapon; the displayed weapon can appear offset.

You have to be aiming for the body to raise its weapon. Press the trigger from the hip and the stand-in keeps the gun down. This is a rule now, and it was not always one: for a while the firing pose was withheld because the stand-in could not resolve which weapon family it was holding, and a withheld pose looks exactly like a forbidden shot. Repairing the family resolution took the block away with the defect -- which is how you learn a behaviour was an accident. It was asked for back, so it is written down where it will survive the next repair. What it withholds is the pose: the shot still fires, still costs ammo, and still cannot cross cover that blocks the barrel -- that gate is separate and unchanged.

Animations without a compatible clip are suppressed on the third-person rig to avoid displaying its bind pose.

The body does not crouch. The camera anchor does dip, so the framing is right while the body is not.

Interaction prompts follow the camera, not the body. The engine decides which interaction to offer from where the camera is looking, and in third person the camera sits about three metres behind you. Standing against a car door does not necessarily offer it. A body-anchored prompt for vehicle entry exists and ships disabled; the engine's own prompt is unchanged.

Two things this view will not do. Cinematics, braindance, finishers, cyberware animations, photo mode and mirrors are out of scope by design and fall back cleanly to the native camera. And the toggle is client-local: a server can set a policy, but it cannot watch a specific player's current view.

Steep downward aim can displace the camera framing, particularly near -80 degrees. The body remains visible, but the requested framing is not guaranteed.

Your character wears what your character owns. A body that owns no clothing appears in underwear, because that is what it is wearing β€” a V straight out of the creator wears exactly that, and first person never shows it. Dress the character and the third-person body follows.

See also

Vehicles Seats, authority and the vehicle APIs.
Interactions Prompts, choices and layers.
Chat Input focus, which the perspective key shares.

aiming here is the older, looser read

perspective.state() carries an aiming field and costs no capability, while Open77.character.isAiming() answers the same question behind player.aim.read. Both spellings ship, and which one you should use is not a matter of taste.

Prefer Open77.character.isAiming(). The field here has two writers -- a trigger pull, and the third-person self-view's per-frame gate -- and the second is short-circuited away whenever that session is inactive, so in first person nothing writes it. A resource reading it in first person sees whatever the last shot said until the player shoots again. The gated read observes combat directly and does not have that hole.

The older field stays ungated because it already shipped that way, and tightening a surface underneath resources that already call it would break installs to fix an inconsistency nobody is exploiting. The newer read is gated because a new surface starts closed: relaxing a capability later costs nothing, and adding one later breaks every manifest that was written without it.

// For agents & LLMsthis page as Markdownllms.txtllms-full.txt

// Guides Β· Lua API

Search the docs

1,905 guides and functions indexed

Try