From 0edc59555a7045be2d7b5e8065aa7e99e8684a75 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Fri, 26 Jun 2026 22:01:43 +0200 Subject: [PATCH] Add per-pose camera target Y offset and route ceiling offset to camera Add cameraTargetYOffset to PlayerPose so each pose can offset the Cinemachine ThirdPersonCamera follow/look target independently of the body mesh. PlayerCharacterController gains a cameraTarget reference (auto-resolved via the "CameraTarget" child, base position captured in Awake) and a single-writer SetCameraTargetLocalYOffset. PlayerPoseController eases the per-pose camera offset and composes it with the shared ceiling offset, so the camera target follows the ceiling lift in lockstep with the mesh. Wire cameraTarget explicitly in SampleScene. Co-Authored-By: Claude Opus 4.8 --- Assets/Scenes/SampleScene.unity | 1 + Assets/Scripts/PlayerCharacterController.cs | 25 +++++++++++++++++++++ Assets/Scripts/PlayerPose.cs | 9 ++++++++ Assets/Scripts/PlayerPoseController.cs | 17 +++++++++++++- 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Assets/Scenes/SampleScene.unity b/Assets/Scenes/SampleScene.unity index 756b84c..e5abb87 100644 --- a/Assets/Scenes/SampleScene.unity +++ b/Assets/Scenes/SampleScene.unity @@ -4634,6 +4634,7 @@ MonoBehaviour: wallTopOutBoost: 0.3 cameraOrbit: {fileID: 0} meshOffsetTarget: {fileID: 0} + cameraTarget: {fileID: 1774267802} --- !u!114 &1141039643 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/PlayerCharacterController.cs b/Assets/Scripts/PlayerCharacterController.cs index 60bfc2d..42d5aad 100644 --- a/Assets/Scripts/PlayerCharacterController.cs +++ b/Assets/Scripts/PlayerCharacterController.cs @@ -138,6 +138,9 @@ public partial class PlayerCharacterController : MonoBehaviour [Tooltip("Visual model root moved by the wall-climb lean offset (into/out of the wall) while the capsule stays put. Must parent both the skinned mesh and its bones. Auto-resolved from the SkinnedMeshRenderer if empty.")] [SerializeField] private Transform meshOffsetTarget; + [Tooltip("Camera follow/look target (what the ThirdPersonCamera tracks). Moved vertically by the active pose's camera y-offset plus the ceiling offset, without moving the body. Auto-resolved from a child named \"CameraTarget\" if empty.")] + [SerializeField] private Transform cameraTarget; + /// /// When false, movement/jump input is ignored but gravity and ground contact /// keep running. Toggled by the GameModeManager. @@ -214,6 +217,8 @@ public partial class PlayerCharacterController : MonoBehaviour private bool _offsetDragging; // true while the offset/rotation is being set (pauses camera orbit) private bool _climbLeftGround; // true once the capsule has left the ground while climbing (enables ground-detach) private Vector3 _meshBaseLocalPos; // meshOffsetTarget's authored local position; the lean offset is applied relative to this + private Vector3 _cameraTargetBaseLocalPos; // cameraTarget's authored local position; the pose/ceiling camera offset is applied relative to this + private float _cameraYOffset; // last camera-target local-up offset written (cached so an unchanged value skips the transform write) private float _meshOffsetResidual; // leftover mesh lean still easing back to 0 after leaving the wall (no longer climbing) private Vector3 _meshOffsetExitNormal; // wall normal latched at exit; the residual eases along it (live _wallNormal may shift on stray contact) private readonly RaycastHit[] _wallProbe = new RaycastHit[8]; @@ -248,6 +253,13 @@ public partial class PlayerCharacterController : MonoBehaviour } } if (meshOffsetTarget) _meshBaseLocalPos = meshOffsetTarget.localPosition; + + if (!cameraTarget) + { + Transform t = transform.Find("CameraTarget"); + if (t) cameraTarget = t; + } + if (cameraTarget) _cameraTargetBaseLocalPos = cameraTarget.localPosition; } void Update() @@ -778,6 +790,19 @@ public partial class PlayerCharacterController : MonoBehaviour SetMeshLean(_meshOffsetExitNormal, _meshOffsetResidual); } + /// + /// Places the camera target (the ThirdPersonCamera's follow/look target) at the given local-up + /// offset relative to its authored local position, without moving the body. Driven by + /// with the active pose's camera y-offset plus the ceiling offset; + /// 0 restores the authored target height. Cached so an unchanged value skips the write. + /// + public void SetCameraTargetLocalYOffset(float y) + { + if (!cameraTarget || _cameraYOffset == y) return; + _cameraYOffset = y; + cameraTarget.localPosition = _cameraTargetBaseLocalPos + Vector3.up * y; + } + /// /// Raycasts from the character center toward the surface (along -_wallNormal) to find the /// current normal and the gap (surface clearance) to it. The capsule's support distance diff --git a/Assets/Scripts/PlayerPose.cs b/Assets/Scripts/PlayerPose.cs index 6a9ca23..fd1e68d 100644 --- a/Assets/Scripts/PlayerPose.cs +++ b/Assets/Scripts/PlayerPose.cs @@ -41,6 +41,12 @@ public class PlayerPose : ScriptableObject "hangs at the right height. Ignored on the ground and on walls. Meters.")] [SerializeField] private float ceilingYOffset; + [Tooltip("Local-up offset applied to the camera target (the ThirdPersonCamera's follow/look target) " + + "while this pose is active, in ANY state. Frames the pose by raising/lowering what the camera " + + "tracks, without moving the body. The ceiling offset is added on top of this while on the " + + "ceiling. Meters.")] + [SerializeField] private float cameraTargetYOffset; + /// Idle clip held while the player stands still in this pose (may be null). public AnimationClip Clip => clip; @@ -61,4 +67,7 @@ public class PlayerPose : ScriptableObject /// Local-up offset applied to the model only while on the ceiling. public float CeilingYOffset => ceilingYOffset; + + /// Local-up offset applied to the camera target while this pose is active (any state). + public float CameraTargetYOffset => cameraTargetYOffset; } diff --git a/Assets/Scripts/PlayerPoseController.cs b/Assets/Scripts/PlayerPoseController.cs index 0bde365..39d166f 100644 --- a/Assets/Scripts/PlayerPoseController.cs +++ b/Assets/Scripts/PlayerPoseController.cs @@ -24,6 +24,8 @@ using UnityEngine.InputSystem; /// crossfades the outgoing and incoming clips over instead of snapping. /// The per-pose ceiling y-offset is eased toward its target and routed through the controller (the /// single writer of the model-offset transform) so it composes with the wall-lean instead of fighting it. +/// A separate per-pose camera y-offset (plus that same ceiling offset) is likewise eased and routed to the +/// controller's camera target, framing each pose for the ThirdPersonCamera without moving the body. /// [RequireComponent(typeof(PlayerCharacterController))] [DisallowMultipleComponent] @@ -83,7 +85,8 @@ public partial class PlayerPoseController : MonoBehaviour private float _blend; // 0..1 weight of the "to" input; 1 = fully on the target pose private bool _blending; - private float _ceilingOffset; // eased local-up offset currently fed to the controller + private float _ceilingOffset; // eased local-up offset currently fed to the controller (mesh + camera) + private float _cameraPoseOffset; // eased per-pose camera-target offset; the ceiling offset is added on top when routed to the controller // Rest pose snapshot of the whole Animator subtree, taken in Awake before the Animator first // evaluates. An AnimationClipPlayable only writes the bones its curves cover, so a clip that @@ -149,6 +152,16 @@ public partial class PlayerPoseController : MonoBehaviour ? Mathf.MoveTowards(_ceilingOffset, ceilingTarget, ceilingOffsetSpeed * Time.deltaTime) : ceilingTarget; _player.SetPoseLocalYOffset(_ceilingOffset); + + // Camera target: a per-pose framing offset (applied in any state) plus the same ceiling offset, + // routed to the ThirdPersonCamera's follow target only (never the body). The pose part eases like + // the ceiling offset so cycling poses glides the camera; the ceiling part reuses the value above so + // camera and model stay in lockstep on the ceiling. + float cameraPoseTarget = pose ? pose.CameraTargetYOffset : 0f; + _cameraPoseOffset = ceilingOffsetSpeed > 0f + ? Mathf.MoveTowards(_cameraPoseOffset, cameraPoseTarget, ceilingOffsetSpeed * Time.deltaTime) + : cameraPoseTarget; + _player.SetCameraTargetLocalYOffset(_cameraPoseOffset + _ceilingOffset); } void OnDisable() @@ -157,9 +170,11 @@ public partial class PlayerPoseController : MonoBehaviour // next idle frame. Also clear any ceiling offset we were holding. ReleasePose(); _ceilingOffset = 0f; + _cameraPoseOffset = 0f; if (_player) { _player.SetPoseLocalYOffset(0f); + _player.SetCameraTargetLocalYOffset(0f); } }