diff --git a/Assets/Main Camera Custom Blends.asset b/Assets/Main Camera Custom Blends.asset
new file mode 100644
index 0000000..d9e377d
--- /dev/null
+++ b/Assets/Main Camera Custom Blends.asset
@@ -0,0 +1,26 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!114 &11400000
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 36baaa8bdcb9d8b49b9199833965d2c3, type: 3}
+ m_Name: Main Camera Custom Blends
+ m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineBlenderSettings
+ CustomBlends:
+ - From: '**ANY CAMERA**'
+ To: '**ANY CAMERA**'
+ Blend:
+ Style: 1
+ Time: 0.4
+ CustomCurve:
+ serializedVersion: 2
+ m_Curve: []
+ m_PreInfinity: 0
+ m_PostInfinity: 0
+ m_RotationOrder: 0
diff --git a/Assets/Main Camera Custom Blends.asset.meta b/Assets/Main Camera Custom Blends.asset.meta
new file mode 100644
index 0000000..935c16c
--- /dev/null
+++ b/Assets/Main Camera Custom Blends.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 47b57a7fe51fd154e975311ad293878a
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 11400000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Poses/Pose_Ball.asset b/Assets/Poses/Pose_Ball.asset
index f9b44de..698b2d6 100644
--- a/Assets/Poses/Pose_Ball.asset
+++ b/Assets/Poses/Pose_Ball.asset
@@ -19,3 +19,4 @@ MonoBehaviour:
runClip: {fileID: 0}
runClipSpeed: 1
ceilingYOffset: 0.84
+ cameraTargetYOffset: -0.5
diff --git a/Assets/Poses/Pose_Star.asset b/Assets/Poses/Pose_Star.asset
index b623bf2..a405109 100644
--- a/Assets/Poses/Pose_Star.asset
+++ b/Assets/Poses/Pose_Star.asset
@@ -19,3 +19,4 @@ MonoBehaviour:
runClip: {fileID: 0}
runClipSpeed: 1
ceilingYOffset: 1
+ cameraTargetYOffset: -0.55
diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity
index e5abb87..fd9cedb 100644
--- a/Assets/Scenes/SampleScene.unity
+++ b/Assets/Scenes/SampleScene.unity
@@ -1561,7 +1561,7 @@ MonoBehaviour:
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
- CustomBlends: {fileID: 0}
+ CustomBlends: {fileID: 11400000, guid: 47b57a7fe51fd154e975311ad293878a, type: 2}
--- !u!1 &347639556
GameObject:
m_ObjectHideFlags: 0
@@ -1750,7 +1750,7 @@ Light:
m_Type: 2
m_Resolution: -1
m_CustomResolution: -1
- m_Strength: 1
+ m_Strength: 0.9
m_Bias: 0.05
m_NormalBias: 0.4
m_NearPlane: 0.2
@@ -2559,6 +2559,19 @@ PrefabInstance:
m_AddedGameObjects: []
m_AddedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: ac1cc01cb5dbf5d4f869c403b38f8a21, type: 3}
+--- !u!134 &610463647
+PhysicsMaterial:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name:
+ serializedVersion: 2
+ m_DynamicFriction: 0.6
+ m_StaticFriction: 0.6
+ m_Bounciness: 0
+ m_FrictionCombine: 0
+ m_BounceCombine: 0
--- !u!1001 &626957352
PrefabInstance:
m_ObjectHideFlags: 0
@@ -3020,19 +3033,6 @@ Transform:
m_CorrespondingSourceObject: {fileID: 4922444305696846, guid: ac1cc01cb5dbf5d4f869c403b38f8a21, type: 3}
m_PrefabInstance: {fileID: 696901729}
m_PrefabAsset: {fileID: 0}
---- !u!134 &699178043
-PhysicsMaterial:
- m_ObjectHideFlags: 0
- m_CorrespondingSourceObject: {fileID: 0}
- m_PrefabInstance: {fileID: 0}
- m_PrefabAsset: {fileID: 0}
- m_Name:
- serializedVersion: 2
- m_DynamicFriction: 0.6
- m_StaticFriction: 0.6
- m_Bounciness: 0
- m_FrictionCombine: 0
- m_BounceCombine: 0
--- !u!1001 &724408611
PrefabInstance:
m_ObjectHideFlags: 0
@@ -4571,7 +4571,7 @@ CharacterController:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1141039638}
- m_Material: {fileID: 699178043}
+ m_Material: {fileID: 1671101580}
m_IncludeLayers:
serializedVersion: 2
m_Bits: 0
@@ -4979,6 +4979,155 @@ Transform:
m_CorrespondingSourceObject: {fileID: 415460, guid: ab99da1b8f76c44549a399bcdd9c8caa, type: 3}
m_PrefabInstance: {fileID: 1253165018}
m_PrefabAsset: {fileID: 0}
+--- !u!1 &1260370184 stripped
+GameObject:
+ m_CorrespondingSourceObject: {fileID: 133438, guid: 6f95ff39b42c3414d9a7ced2eea58274, type: 3}
+ m_PrefabInstance: {fileID: 1860487067}
+ m_PrefabAsset: {fileID: 0}
+--- !u!64 &1260370189
+MeshCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1260370184}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 5
+ m_Convex: 0
+ m_CookingOptions: 30
+ m_Mesh: {fileID: 4300000, guid: 6bb1e412df9cd4efd8941377ebabb1be, type: 3}
+--- !u!1 &1282734303
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 1282734307}
+ - component: {fileID: 1282734306}
+ - component: {fileID: 1282734305}
+ - component: {fileID: 1282734304}
+ m_Layer: 0
+ m_Name: FreeCamera
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!114 &1282734304
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1282734303}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 5ca7cff7542b53b498f0a9834786b9e0, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::FreeCamController
+ moveSpeed: 6
+ sprintMultiplier: 3
+ horizontalSensitivity: 0.1
+ verticalSensitivity: 0.07
+ invertY: 0
+ maxPitch: 89
+ activePriority: 100
+ inactivePriority: -100
+--- !u!143 &1282734305
+CharacterController:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1282734303}
+ m_Material: {fileID: 610463647}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Height: 1.3
+ m_Radius: 0.1
+ m_SlopeLimit: 59
+ m_StepOffset: 0.15
+ m_SkinWidth: 0.01
+ m_MinMoveDistance: 0.001
+ m_Center: {x: 0, y: -0.09, z: 0}
+--- !u!114 &1282734306
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1282734303}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: f9dfa5b682dcd46bda6128250e975f58, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.Cinemachine::Unity.Cinemachine.CinemachineCamera
+ Priority:
+ Enabled: 1
+ m_Value: -100
+ OutputChannel: 1
+ StandbyUpdate: 2
+ m_StreamingVersion: 20241001
+ m_LegacyPriority: 0
+ Target:
+ TrackingTarget: {fileID: 0}
+ LookAtTarget: {fileID: 0}
+ CustomLookAtTarget: 0
+ Lens:
+ FieldOfView: 40
+ OrthographicSize: 10
+ NearClipPlane: 0.1
+ FarClipPlane: 5000
+ Dutch: 0
+ ModeOverride: 0
+ PhysicalProperties:
+ GateFit: 2
+ SensorSize: {x: 21.946, y: 16.002}
+ LensShift: {x: 0, y: 0}
+ FocusDistance: 10
+ Iso: 200
+ ShutterSpeed: 0.005
+ Aperture: 16
+ BladeCount: 5
+ Curvature: {x: 2, y: 11}
+ BarrelClipping: 0.25
+ Anamorphism: 0
+ BlendHint: 0
+--- !u!4 &1282734307
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 1282734303}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 1, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1001 &1317768121
PrefabInstance:
m_ObjectHideFlags: 0
@@ -5768,6 +5917,7 @@ MonoBehaviour:
player: {fileID: 1141039642}
cameraInput: {fileID: 172071431}
painter: {fileID: 0}
+ freeCam: {fileID: 1282734304}
switchKey: 3
startModeIndex: 0
--- !u!4 &1516578028
@@ -6264,6 +6414,19 @@ MeshCollider:
m_Convex: 0
m_CookingOptions: 30
m_Mesh: {fileID: 4300000, guid: 0727ba69b779d42a0978a362226fad3e, type: 3}
+--- !u!134 &1671101580
+PhysicsMaterial:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: ' (Instance)'
+ serializedVersion: 2
+ m_DynamicFriction: 0.6
+ m_StaticFriction: 0.6
+ m_Bounciness: 0
+ m_FrictionCombine: 0
+ m_BounceCombine: 0
--- !u!1 &1680244256
GameObject:
m_ObjectHideFlags: 0
@@ -7029,10 +7192,14 @@ PrefabInstance:
propertyPath: m_LocalRotation.z
value: 0
objectReference: {fileID: 0}
- m_RemovedComponents: []
+ m_RemovedComponents:
+ - {fileID: 6582054, guid: 6f95ff39b42c3414d9a7ced2eea58274, type: 3}
m_RemovedGameObjects: []
m_AddedGameObjects: []
- m_AddedComponents: []
+ m_AddedComponents:
+ - targetCorrespondingSourceObject: {fileID: 133438, guid: 6f95ff39b42c3414d9a7ced2eea58274, type: 3}
+ insertIndex: -1
+ addedObject: {fileID: 1260370189}
m_SourcePrefab: {fileID: 100100000, guid: 6f95ff39b42c3414d9a7ced2eea58274, type: 3}
--- !u!4 &1860487068 stripped
Transform:
@@ -7836,3 +8003,4 @@ SceneRoots:
- {fileID: 686187287}
- {fileID: 489277934}
- {fileID: 347639557}
+ - {fileID: 1282734307}
diff --git a/Assets/Scripts/FreeCamController.cs b/Assets/Scripts/FreeCamController.cs
new file mode 100644
index 0000000..725b0d3
--- /dev/null
+++ b/Assets/Scripts/FreeCamController.cs
@@ -0,0 +1,194 @@
+using UnityEngine;
+using Unity.Cinemachine;
+#if ENABLE_INPUT_SYSTEM
+using UnityEngine.InputSystem;
+#endif
+
+///
+/// Free-flying spectator camera. Drives its OWN dedicated by moving
+/// this transform with WASD (relative to where it looks), Q/E (or Space/Left Ctrl) for down/up
+/// and the mouse for look. Collision is a capsule sized like the
+/// player's, so the freecam slides along world geometry instead of clipping through walls/floors.
+///
+/// Activation is driven by via : it raises the vcam
+/// priority so the CinemachineBrain blends to it, snaps the rig to the current view (no jump) and
+/// locks the cursor. While inactive the vcam rests at a low priority and this component does nothing.
+///
+[RequireComponent(typeof(CharacterController))]
+[DisallowMultipleComponent]
+public class FreeCamController : MonoBehaviour
+{
+ [Header("Flight")]
+ [Tooltip("Base flight speed (units/second).")]
+ public float moveSpeed = 6f;
+
+ [Tooltip("Speed multiplier while the sprint key (Left Shift) is held.")]
+ public float sprintMultiplier = 3f;
+
+ [Header("Look")]
+ [Tooltip("Horizontal look sensitivity (degrees of yaw per pixel).")]
+ public float horizontalSensitivity = 0.2f;
+
+ [Tooltip("Vertical look sensitivity (degrees of pitch per pixel).")]
+ public float verticalSensitivity = 0.15f;
+
+ [Tooltip("Invert vertical look (mouse up tilts the camera down).")]
+ public bool invertY = false;
+
+ [Tooltip("Pitch is clamped to +/- this angle (degrees) so the camera never flips over.")]
+ [Range(1f, 89.9f)]
+ public float maxPitch = 89f;
+
+ [Header("Camera")]
+ [Tooltip("Priority the dedicated CinemachineCamera takes while active (must beat the gameplay camera).")]
+ public int activePriority = 100;
+
+ [Tooltip("Priority the dedicated CinemachineCamera rests at while inactive (must lose to the gameplay camera).")]
+ public int inactivePriority = -100;
+
+ CharacterController _controller;
+ CinemachineCamera _vcam;
+ float _yaw;
+ float _pitch;
+ bool _ignoredPlayer;
+
+ void Awake()
+ {
+ _controller = GetComponent();
+ _vcam = GetComponent();
+ if (!_vcam)
+ Debug.LogError("[FreeCamController] No CinemachineCamera on this GameObject; freecam cannot go live.", this);
+ else
+ _vcam.Priority = inactivePriority;
+
+ // Dormant until FreeCamMode enters; nothing reads input or moves the rig meanwhile.
+ enabled = false;
+ }
+
+ /// Enter/leave freecam. Raises/lowers the vcam priority, snaps to the live view and
+ /// manages the cursor. Called by .
+ public void SetActive(bool active)
+ {
+ if (_vcam)
+ _vcam.Priority = active ? activePriority : inactivePriority;
+
+ if (active)
+ {
+ IgnorePlayerCollision();
+ SyncToCurrentView();
+ SetCursorLocked(true);
+ }
+ else
+ {
+ SetCursorLocked(false);
+ }
+
+ enabled = active;
+ }
+
+ void Update()
+ {
+ LookTick();
+ MoveTick();
+ }
+
+ void LookTick()
+ {
+ Vector2 delta = ReadMouseDelta();
+ _yaw += delta.x * horizontalSensitivity;
+ float vs = verticalSensitivity * (invertY ? 1f : -1f);
+ _pitch = Mathf.Clamp(_pitch + delta.y * vs, -maxPitch, maxPitch);
+ // Rotation is unconstrained by the CharacterController; only position is.
+ transform.rotation = Quaternion.Euler(_pitch, _yaw, 0f);
+ }
+
+ void MoveTick()
+ {
+ Vector3 dir = ReadMoveDirection();
+ if (dir.sqrMagnitude > 1f)
+ dir.Normalize();
+
+ float speed = moveSpeed * (SprintHeld() ? sprintMultiplier : 1f);
+ _controller.Move(dir * (speed * Time.deltaTime));
+ }
+
+ /// Place the rig at the currently rendered view so going live produces no visible jump.
+ void SyncToCurrentView()
+ {
+ var cam = Camera.main;
+ if (!cam) return;
+
+ // CharacterController forbids direct transform writes while enabled, so toggle it for the teleport.
+ bool wasEnabled = _controller.enabled;
+ _controller.enabled = false;
+ transform.SetPositionAndRotation(cam.transform.position, cam.transform.rotation);
+ _controller.enabled = wasEnabled;
+
+ Vector3 e = transform.rotation.eulerAngles;
+ _yaw = e.y;
+ float p = e.x > 180f ? e.x - 360f : e.x;
+ _pitch = Mathf.Clamp(p, -maxPitch, maxPitch);
+ }
+
+ /// Stop the freecam capsule from being blocked by the (frozen) player's capsule.
+ void IgnorePlayerCollision()
+ {
+ if (_ignoredPlayer) return;
+ _ignoredPlayer = true;
+
+ var player = FindFirstObjectByType();
+ if (player)
+ {
+ var playerCC = player.GetComponent();
+ if (playerCC)
+ Physics.IgnoreCollision(_controller, playerCC, true);
+ }
+ }
+
+ Vector3 ReadMoveDirection()
+ {
+ Vector3 dir = Vector3.zero;
+#if ENABLE_INPUT_SYSTEM
+ var kb = Keyboard.current;
+ if (kb != null)
+ {
+ if (kb.wKey.isPressed) dir += transform.forward;
+ if (kb.sKey.isPressed) dir -= transform.forward;
+ if (kb.dKey.isPressed) dir += transform.right;
+ if (kb.aKey.isPressed) dir -= transform.right;
+ if (kb.eKey.isPressed || kb.spaceKey.isPressed) dir += Vector3.up;
+ if (kb.qKey.isPressed || kb.leftCtrlKey.isPressed) dir -= Vector3.up;
+ }
+#else
+ dir += transform.forward * Input.GetAxisRaw("Vertical");
+ dir += transform.right * Input.GetAxisRaw("Horizontal");
+ if (Input.GetKey(KeyCode.E) || Input.GetKey(KeyCode.Space)) dir += Vector3.up;
+ if (Input.GetKey(KeyCode.Q) || Input.GetKey(KeyCode.LeftControl)) dir -= Vector3.up;
+#endif
+ return dir;
+ }
+
+ bool SprintHeld()
+ {
+#if ENABLE_INPUT_SYSTEM
+ return Keyboard.current != null && Keyboard.current.leftShiftKey.isPressed;
+#else
+ return Input.GetKey(KeyCode.LeftShift);
+#endif
+ }
+
+ Vector2 ReadMouseDelta()
+ {
+#if ENABLE_INPUT_SYSTEM
+ return Mouse.current != null ? Mouse.current.delta.ReadValue() : Vector2.zero;
+#else
+ return new Vector2(Input.GetAxisRaw("Mouse X"), Input.GetAxisRaw("Mouse Y")) * 10f;
+#endif
+ }
+
+ static void SetCursorLocked(bool locked)
+ {
+ Cursor.lockState = locked ? CursorLockMode.Locked : CursorLockMode.None;
+ Cursor.visible = !locked;
+ }
+}
diff --git a/Assets/Scripts/FreeCamController.cs.meta b/Assets/Scripts/FreeCamController.cs.meta
new file mode 100644
index 0000000..5d89b38
--- /dev/null
+++ b/Assets/Scripts/FreeCamController.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5ca7cff7542b53b498f0a9834786b9e0
\ No newline at end of file
diff --git a/Assets/Scripts/FreeCamMode.cs b/Assets/Scripts/FreeCamMode.cs
new file mode 100644
index 0000000..7d2b62f
--- /dev/null
+++ b/Assets/Scripts/FreeCamMode.cs
@@ -0,0 +1,41 @@
+///
+/// Free-fly camera mode. The player is frozen in place, painting is off, and a dedicated
+/// freecam ( + its own CinemachineCamera) takes over: fly the
+/// camera through the map with WASD/Q/E and the mouse, colliding with world geometry like the
+/// player. The orbit camera input is disabled while here so it doesn't fight for the cursor.
+///
+public class FreeCamMode : GameMode
+{
+ public override string DisplayName => "Free Cam";
+
+ public override void Enter()
+ {
+ // Keep the player component alive (gravity/grounding) but reject input, like Paint mode.
+ if (Manager.Player)
+ {
+ Manager.Player.enabled = true;
+ Manager.Player.ControlEnabled = false;
+ }
+
+ if (Manager.Painter)
+ Manager.Painter.PaintingEnabled = false;
+
+ // The orbit rig would otherwise keep grabbing the cursor and reading the mouse every frame.
+ if (Manager.CameraInput)
+ Manager.CameraInput.enabled = false;
+
+ if (Manager.FreeCam)
+ Manager.FreeCam.SetActive(true);
+ }
+
+ public override void Exit()
+ {
+ // Modes are Enter-only, so freecam must undo its own camera takeover here; the next mode's
+ // Enter restores the player/painter/look-mode state.
+ if (Manager.FreeCam)
+ Manager.FreeCam.SetActive(false);
+
+ if (Manager.CameraInput)
+ Manager.CameraInput.enabled = true;
+ }
+}
diff --git a/Assets/Scripts/FreeCamMode.cs.meta b/Assets/Scripts/FreeCamMode.cs.meta
new file mode 100644
index 0000000..4dfa4bb
--- /dev/null
+++ b/Assets/Scripts/FreeCamMode.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a221ef0b363824540993c714c0fc7385
\ No newline at end of file
diff --git a/Assets/Scripts/GameModeManager.cs b/Assets/Scripts/GameModeManager.cs
index 6713edd..b136b2d 100644
--- a/Assets/Scripts/GameModeManager.cs
+++ b/Assets/Scripts/GameModeManager.cs
@@ -5,7 +5,7 @@ using UnityEngine.InputSystem;
#endif
///
-/// Switches the game between high-level modes (Player Control, Paint). The switch
+/// Switches the game between high-level modes (Player Control, Paint, Free Cam). The switch
/// key (Tab) cycles to the next registered mode. Register additional modes in
/// . Attach to a single, persistent scene GameObject.
///
@@ -22,6 +22,9 @@ public class GameModeManager : MonoBehaviour
[Tooltip("Mesh painter. Empty = found automatically.")]
public MeshPainter painter;
+ [Tooltip("Free-fly spectator camera. Empty = found automatically. Absent = Free Cam is left out of the cycle.")]
+ public FreeCamController freeCam;
+
//[Tooltip("Insect / surface-walk controller. Empty = found automatically.")]
//public InsectController insect;
@@ -39,6 +42,7 @@ public class GameModeManager : MonoBehaviour
public PlayerCharacterController Player => player;
public ThirdPersonOrbitInput CameraInput => cameraInput;
public MeshPainter Painter => painter;
+ public FreeCamController FreeCam => freeCam;
//public InsectController Insect => insect;
/// Raised after a mode switch finishes (the new mode's Enter has already run).
@@ -57,6 +61,8 @@ public class GameModeManager : MonoBehaviour
if (!player) player = FindFirstObjectByType();
if (!cameraInput) cameraInput = FindFirstObjectByType();
if (!painter) painter = FindFirstObjectByType();
+ // Include inactive so a freecam rig that starts disabled is still found and wired.
+ if (!freeCam) freeCam = FindFirstObjectByType(FindObjectsInactive.Include);
//if (!insect) insect = FindFirstObjectByType();
BuildModes();
@@ -80,6 +86,10 @@ public class GameModeManager : MonoBehaviour
_modes.Add(new PaintMode());
+ // Free Cam only joins the cycle when a freecam rig is present, so scenes without one are unchanged.
+ if (freeCam)
+ _modes.Add(new FreeCamMode());
+
// Insect mode only joins the Tab cycle when a surface walker is present, so a scene
// without one behaves exactly as before (no dead, player-freezing mode in the cycle).
//if (insect)