Cameras & cinematics
Third person
Player preferences, server policy and camera ownership.
On this page10 sections
- Hide the local avatar
- The player's side
- Enable, disable or temporarily force a view (client)
- Server policy
- When the view hands itself back
- The crosshair
- Reading the aim from a resource
- Do not read perspective.state().aiming for this
- aiming is a latched mirror, and stateSequence is how you tell
- What aimedEntity() answers with
- Limitations
- See also
- aiming here is the older, looser read
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
