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)