Skip to content

Instantly share code, notes, and snippets.

@ReKylee
Created October 17, 2025 17:09
Show Gist options
  • Select an option

  • Save ReKylee/0c9aa1cfe4c2fe30528d88e96343a791 to your computer and use it in GitHub Desktop.

Select an option

Save ReKylee/0c9aa1cfe4c2fe30528d88e96343a791 to your computer and use it in GitHub Desktop.
/// <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);
}
}
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