Skip to content

Camera system

Assets/Scripts/Camera, namespace HoseBoy. Five components, plus the vendored HelloMarioFramework.FreeLookHelper that owns the third-person rig. Cinemachine 3.1.6.

For the design rationale see Camera.

Partly unwired

CameraZone, FirstPersonLook and PlayerCameraDirector are present in Scenes/demo.unity. FirstPersonBodyYaw, TrueFirstPersonBody and PerspectiveShiftTransition are referenced from nowhere. Without FirstPersonBodyYaw the body does not turn with the first-person view; without TrueFirstPersonBody the camera sits inside the character's head.

PlayerCameraDirector

The single owner of which camera the player is looking through. [DefaultExecutionOrder(-20)], ahead of the look and hose scripts that read the current mode. Exposes a static Instance and Mode CurrentMode (FirstPerson / ThirdPerson).

Mode resolution

flowchart TD
    Start[Refresh] --> D{default mode}
    D --> Z{zoneCount > 0?}
    Z -->|yes| TP[ThirdPerson]
    Z -->|no| Keep[keep default]
    TP --> Dlg{in dialogue and<br/>thirdPersonDuringDialogue?}
    Keep --> Dlg
    Dlg -->|yes| TP2[ThirdPerson]
    Dlg -->|no| Target[target mode]
    TP2 --> Target
    Target --> C{changed since<br/>last applied?}
    C -->|no| Stop[do nothing]
    C -->|yes| S{transition available<br/>and not first apply?}
    S -->|yes| Cover[shift.Play → Apply at cut point]
    S -->|no| Apply[Apply immediately]

The first application deliberately does not play a transition — spawning into a town should not wipe the screen at the player.

What Apply does

One mode switch drives six things:

Target First person Third person
firstPersonRig.enabled on off
thirdPersonRig.enabled off on
FirstPersonLook.SetActive true false
TrueFirstPersonBody.SetFirstPerson true false
HoseArmAim.SetWeight firstPersonArmWeight (1) thirdPersonArmWeight (0.7)
HoseAimReticle.SetSuppressed / crosshair reticle off, crosshair on reticle on, crosshair off

Leaving first person also calls AlignFreeLookBehindPlayer(), which parks the free-look rig's axes at m_XAxis = 0 / m_YAxis = 0.5 so it comes back directly behind the player instead of at whatever angle it was abandoned at.

Blend time

Awake reaches into Camera.main's CinemachineBrain and overrides DefaultBlend.Time — to switchBlend (0.35) normally, or to 0 when a PerspectiveShiftTransition is assigned, since a covered swap should be a hard cut. Cinemachine's default of 2 seconds is long enough to lose your bearings in.

Dialogue coupling

Subscribes to DialogueManager.OnDialogueStartEvent / OnDialogueEndEvent in OnEnable and forces third person for the duration when thirdPersonDuringDialogue is set. This is the only coupling between the camera and dialogue systems, and it goes through static events, so neither holds a reference to the other.

CameraZone

[RequireComponent(typeof(Collider))]. A trigger volume that requests third person while the player is inside.

Counted, not toggled. OnTriggerEnter calls EnterZone() and OnTriggerExit calls ExitZone(), and the director keeps a zoneCount, so a town built from several overlapping boxes has no seams. Each zone guards with its own holding flag so a re-entry cannot double-count, and OnDisable releases the count if the zone is switched off while occupied.

Player detection is other.GetComponentInParent<HelloMarioFramework.Player>() != nullmatched on the component, not a tag, because the player object in this project is untagged. Reset sets the collider to trigger; OnDrawGizmosSelected draws the volume.

FirstPersonLook

[DefaultExecutionOrder(-5)]. Owns yaw and pitch and writes them straight onto the first-person Cinemachine rig's transform.

The order is pinned between two things: BoneFollower poses the skeleton at −10, and CinemachineBrain samples the rigs at 0. Reading the head bone earlier gives a stale pose; writing the rig later costs a frame of lag.

  • Eye position rides the head bone: headBone.position + headBone.rotation * eyeOffset. Built from position and rotation only, never TransformPoint, because TrueFirstPersonBody collapses the head bone's scale to nearly zero and TransformPoint would drag the eye offset down with it.
  • Rotation is Quaternion.Euler(pitch, yaw, 0), slerped a little towards the head bone's own rotation by animationInfluence (0.12) so the walk has weight without being sickening.
  • One action, two devices. lookAction carries both a per-frame mouse pixel delta and a stick axis, which need completely different scaling. The component inspects action.activeControl to decide which the player last used, then applies either mouseSensitivity (0.08 deg/unit) or stickSensitivity (220 deg/s × deltaTime).
  • Handover continuity. SetActive(true) seeds yaw and pitch from Camera.main's current euler angles, so picking up first person never snaps to a different heading.

Public: IsActive, Yaw, Pitch, SetActive(bool).

FirstPersonBodyYaw

[DefaultExecutionOrder(200)], [RequireComponent(typeof(Rigidbody))]. Turns the body to match where first person is looking, with body.MoveRotation in FixedUpdate.

Split out from FirstPersonLook because the two halves need opposite execution orders: the camera must be written before CinemachineBrain reads it (−5), and the body must be turned after Player.FixedUpdate (200), which otherwise steers the body towards whatever direction it is walking.

TrueFirstPersonBody

[DefaultExecutionOrder(-3)] — after BoneFollower (−10), the look (−5) and the arm solve (−4).

Keeps the real animated character on screen in first person instead of swapping in a viewmodel. The head is removed by collapsing its bone to hiddenScale (0.0001), not by disabling a renderer, because the character is one skinned mesh and a renderer toggle would take the whole body with it. extraHiddenBones handles anything else that pokes into view.

The scale is re-stamped every LateUpdate, since the animation rewrites the pose first. Original scales are captured in Awake and restored on exit and on OnDisable.

It also disables the framework's HideNearCamera component while in first person — that shrinks the whole player near the camera, which fights this directly. HideNearCamera is added by Player.Start at runtime, so it is looked up lazily rather than serialised.

PerspectiveShiftTransition

A thin wrapper over the EasyTransitions plugin (Assets/Plugins/EasyTransitions).

public bool Play(System.Action atCutPoint)

Subscribes to TransitionManager.onTransitionCutPointReached, runs atCutPoint while the screen is covered, and clears its running flag on onTransitionEnd. Returns false if it is already running or there is no TransitionSettings asset / TransitionManager in the scene — which is exactly what makes the director fall back to an uncovered swap.

Assign any TransitionSettings asset, for example EasyTransitions › Transitions › Fade.

FreeLookHelper (vendored)

Assets/HelloMarioFramework/Script/FreeLookHelper.cs, MIT, part of Hello Mario Framework. Owns the CinemachineFreeLook third-person rig.

Relevant behaviour:

  • Awake forces every CinemachineBrain in the scene to UpdateMethods.LateUpdate. This is why the whole execution-order ladder is built around a brain at order 0 in LateUpdate.
  • Creates a CameraFollowTarget dummy transform that lerps towards the player at 15/s, and a CameraLookAt child positioned at the player's hat attach height.
  • Handles zoom (reshaping the three orbits along fitted parabolas), a centre-camera button, and recentering options loaded from OptionsSave.
  • WarpCameraFix and VictoryZoom are framework features not currently used by hoseboy.