Created
October 17, 2025 17:09
-
-
Save ReKylee/0c9aa1cfe4c2fe30528d88e96343a791 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /// <summary> | |
| /// Custom Cinemachine extension that provides spring-based camera following with lag. | |
| /// </summary> | |
| [SaveDuringPlay] | |
| public class CinemachineSpringFollow : CinemachineExtension | |
| { | |
| [BoxGroup("Target")] | |
| [Required("Follow target is required")] | |
| [Tooltip("The transform to follow with spring lag")] | |
| [SerializeField] private Transform followTarget; | |
| [BoxGroup("Target")] | |
| [Tooltip("Offset from target in local space")] | |
| [SerializeField] private Vector3 followOffset = Vector3.zero; | |
| [BoxGroup("Position Spring")] | |
| [Tooltip("Spring damping ratio. 0=undamped, 0.7=slight wobble, 1=critically damped, >1=overdamped.")] | |
| [Range(0.1f, 3f)] | |
| [SerializeField] private float positionDampingRatio = 1.2f; // Overdamped - no bounce, just smooth lag | |
| [BoxGroup("Position Spring")] | |
| [Tooltip("Spring frequency. Lower = slower/floatier. (3-8 for horror, 10-20 for responsive)")] | |
| [Range(1f, 30f)] | |
| [SerializeField] private float positionAngularFrequency = 12f; // Faster to match player acceleration | |
| [BoxGroup("Rotation Spring")] | |
| [Tooltip("Spring damping for rotation matching.")] | |
| [Range(0.1f, 3f)] | |
| [SerializeField] private float rotationDampingRatio = 1.5f; // Very damped - smooth rotation follow | |
| [BoxGroup("Rotation Spring")] | |
| [Tooltip("Spring frequency for rotation. Lower = more lag.")] | |
| [Range(1f, 30f)] | |
| [SerializeField] private float rotationAngularFrequency = 15f; // Tight rotation follow | |
| [BoxGroup("Movement Sway")] | |
| [Tooltip("Adds perpendicular sway based on movement speed")] | |
| [Range(0f, 1f)] | |
| [SerializeField] private float swayAmount = 0.08f; // Reduced - subtle unease, not nausea | |
| [BoxGroup("Movement Sway")] | |
| [SerializeField] private Vector3 swayScale = new Vector3(0.015f, 0.008f, 0f); // Much subtler sway | |
| // Spring state | |
| private Vector3 _currentPosition; | |
| private Vector3 _currentVelocity; | |
| private Quaternion _currentRotation; | |
| private Vector3 _rotationVelocity; | |
| private Vector3 _previousTargetPosition; | |
| private bool _isInitialized; | |
| protected override void PostPipelineStageCallback(CinemachineVirtualCameraBase vcam, CinemachineCore.Stage stage, ref CameraState state, float deltaTime) | |
| { | |
| // Run at the Body stage to control position/rotation | |
| if (stage != CinemachineCore.Stage.Body || followTarget == null) return; | |
| // Get target position | |
| Vector3 targetPosition = followTarget.position + followTarget.TransformDirection(followOffset); | |
| Quaternion targetRotation = followTarget.rotation; | |
| // Initialize on first frame | |
| if (!_isInitialized) | |
| { | |
| _currentPosition = targetPosition; | |
| _currentVelocity = Vector3.zero; | |
| _currentRotation = targetRotation; | |
| _rotationVelocity = Vector3.zero; | |
| _previousTargetPosition = targetPosition; | |
| _isInitialized = true; | |
| } | |
| // Calculate target velocity for sway | |
| Vector3 targetVelocity = deltaTime > 0.0001f ? (targetPosition - _previousTargetPosition) / deltaTime : Vector3.zero; | |
| Vector3 horizontalVelocity = new Vector3(targetVelocity.x, 0f, targetVelocity.z); | |
| float speed = horizontalVelocity.magnitude; | |
| // Add perpendicular sway to target | |
| Vector3 swayOffset = Vector3.zero; | |
| if (speed > 0.1f && swayAmount > 0.001f) | |
| { | |
| Vector3 movementDir = horizontalVelocity.normalized; | |
| Vector3 perpendicular = new Vector3(-movementDir.z, 0f, movementDir.x); | |
| float swayPhase = Mathf.Sin(Time.time * speed * 2f); | |
| swayOffset = perpendicular * swayPhase * swayAmount; | |
| swayOffset = Vector3.Scale(swayOffset, swayScale); | |
| } | |
| Vector3 targetWithSway = targetPosition + swayOffset; | |
| // Apply spring to position using utility | |
| SpringUtility.Spring( | |
| ref _currentPosition, | |
| ref _currentVelocity, | |
| targetWithSway, | |
| positionDampingRatio, | |
| positionAngularFrequency, | |
| deltaTime | |
| ); | |
| // Apply spring to rotation using utility | |
| SpringUtility.Spring( | |
| ref _currentRotation, | |
| ref _rotationVelocity, | |
| targetRotation, | |
| rotationDampingRatio, | |
| rotationAngularFrequency, | |
| deltaTime | |
| ); | |
| // Apply to camera state (override position and rotation) | |
| state.RawPosition = _currentPosition; | |
| state.RawOrientation = _currentRotation; | |
| _previousTargetPosition = targetPosition; | |
| } | |
| private void OnValidate() | |
| { | |
| positionDampingRatio = Mathf.Max(0.1f, positionDampingRatio); | |
| positionAngularFrequency = Mathf.Max(0.1f, positionAngularFrequency); | |
| rotationDampingRatio = Mathf.Max(0.1f, rotationDampingRatio); | |
| rotationAngularFrequency = Mathf.Max(0.1f, rotationAngularFrequency); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| public static class SpringUtility | |
| { | |
| /// <summary> | |
| /// Apply spring physics to a float value. | |
| /// </summary> | |
| /// <param name="current">Current value (modified in place)</param> | |
| /// <param name="velocity">Current velocity (modified in place)</param> | |
| /// <param name="target">Target value to spring towards</param> | |
| /// <param name="dampingRatio">Damping ratio (0=undamped, 1=critically damped, >1=overdamped)</param> | |
| /// <param name="angularFrequency">Angular frequency (higher = faster response)</param> | |
| /// <param name="deltaTime">Time step (usually Time.deltaTime)</param> | |
| public static void Spring( | |
| ref float current, | |
| ref float velocity, | |
| float target, | |
| float dampingRatio, | |
| float angularFrequency, | |
| float deltaTime) | |
| { | |
| if (deltaTime < 0.0001f) return; | |
| float f = 1.0f + 2.0f * deltaTime * dampingRatio * angularFrequency; | |
| float oo = angularFrequency * angularFrequency; | |
| float hoo = deltaTime * oo; | |
| float hhoo = deltaTime * hoo; | |
| float detInv = 1.0f / (f + hhoo); | |
| float detX = f * current + deltaTime * velocity + hhoo * target; | |
| float detV = velocity + hoo * (target - current); | |
| current = detX * detInv; | |
| velocity = detV * detInv; | |
| } | |
| /// <summary> | |
| /// Apply spring physics to a Vector2. | |
| /// </summary> | |
| public static void Spring( | |
| ref Vector2 current, | |
| ref Vector2 velocity, | |
| Vector2 target, | |
| float dampingRatio, | |
| float angularFrequency, | |
| float deltaTime) | |
| { | |
| if (deltaTime < 0.0001f) return; | |
| float f = 1.0f + 2.0f * deltaTime * dampingRatio * angularFrequency; | |
| float oo = angularFrequency * angularFrequency; | |
| float hoo = deltaTime * oo; | |
| float hhoo = deltaTime * hoo; | |
| float detInv = 1.0f / (f + hhoo); | |
| Vector2 detX = f * current + deltaTime * velocity + hhoo * target; | |
| Vector2 detV = velocity + hoo * (target - current); | |
| current = detX * detInv; | |
| velocity = detV * detInv; | |
| } | |
| /// <summary> | |
| /// Apply spring physics to a Vector3. | |
| /// </summary> | |
| public static void Spring( | |
| ref Vector3 current, | |
| ref Vector3 velocity, | |
| Vector3 target, | |
| float dampingRatio, | |
| float angularFrequency, | |
| float deltaTime) | |
| { | |
| if (deltaTime < 0.0001f) return; | |
| float f = 1.0f + 2.0f * deltaTime * dampingRatio * angularFrequency; | |
| float oo = angularFrequency * angularFrequency; | |
| float hoo = deltaTime * oo; | |
| float hhoo = deltaTime * hoo; | |
| float detInv = 1.0f / (f + hhoo); | |
| Vector3 detX = f * current + deltaTime * velocity + hhoo * target; | |
| Vector3 detV = velocity + hoo * (target - current); | |
| current = detX * detInv; | |
| velocity = detV * detInv; | |
| } | |
| /// <summary> | |
| /// Apply spring physics to a Quaternion rotation. | |
| /// Note: This converts to euler angles internally for the spring calculation. | |
| /// </summary> | |
| public static void Spring( | |
| ref Quaternion current, | |
| ref Vector3 angularVelocity, | |
| Quaternion target, | |
| float dampingRatio, | |
| float angularFrequency, | |
| float deltaTime) | |
| { | |
| if (deltaTime < 0.0001f) return; | |
| Vector3 currentEuler = current.eulerAngles; | |
| Vector3 targetEuler = target.eulerAngles; | |
| // Handle euler angle wrapping (find shortest path) | |
| targetEuler = new Vector3( | |
| Mathf.DeltaAngle(0, targetEuler.x - currentEuler.x) + currentEuler.x, | |
| Mathf.DeltaAngle(0, targetEuler.y - currentEuler.y) + currentEuler.y, | |
| Mathf.DeltaAngle(0, targetEuler.z - currentEuler.z) + currentEuler.z | |
| ); | |
| Spring(ref currentEuler, ref angularVelocity, targetEuler, dampingRatio, angularFrequency, deltaTime); | |
| current = Quaternion.Euler(currentEuler); | |
| } | |
| /// <summary> | |
| /// Apply spring physics to a Color. | |
| /// </summary> | |
| public static void Spring( | |
| ref Color current, | |
| ref Vector4 velocity, | |
| Color target, | |
| float dampingRatio, | |
| float angularFrequency, | |
| float deltaTime) | |
| { | |
| if (deltaTime < 0.0001f) return; | |
| float f = 1.0f + 2.0f * deltaTime * dampingRatio * angularFrequency; | |
| float oo = angularFrequency * angularFrequency; | |
| float hoo = deltaTime * oo; | |
| float hhoo = deltaTime * hoo; | |
| float detInv = 1.0f / (f + hhoo); | |
| Vector4 currentVec = new Vector4(current.r, current.g, current.b, current.a); | |
| Vector4 targetVec = new Vector4(target.r, target.g, target.b, target.a); | |
| Vector4 detX = f * currentVec + deltaTime * velocity + hhoo * targetVec; | |
| Vector4 detV = velocity + hoo * (targetVec - currentVec); | |
| currentVec = detX * detInv; | |
| velocity = detV * detInv; | |
| current = new Color(currentVec.x, currentVec.y, currentVec.z, currentVec.w); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment