Fix: the combat "freeze on a kill" — camera shake channel + drop the position hold
This is what the operator's report was actually describing, and it was never a CPU hitch: median frame was 12.2 ms with a kill costing ~15 ms, wave respawn is not a spike, and enemies carry no PhysicsCollider so corpses never blocked. Two bugs in PrototypeCameraRig.LateUpdate, both measured with a STATIONARY player so the ideal camera motion is exactly zero and everything observed is artifact: 1. Shake was integrated by the follow filter. The smoothing ran as Lerp(transform.position, desired, k), and transform.position already contained last frame's random shake — so the filter treated shake as real positional error and corrected only ~9 %/frame. Shake was never subtracted, just slowly lerped out while new shake piled on, so in sustained combat the camera random-walked around its framing and never settled. One kill left it 0.447 units off-frame and still 0.199 off 24 frames later. The follow now smooths a _basePos that shake never touches; shake is applied only when writing the transform. 2. The position HOLD froze the follow for a fixed number of RENDER frames on every kill and heavy hit (2, or 7 on a finisher), making the freeze framerate-dependent — 49 ms at 144 fps, 233 ms at 30 fps — and it took its base from transform.position, permanently baking that frame's shake in as a 0.283-unit single-frame jump. A camera that stops while the world keeps moving reads as a hitch, not as crunch. Dropped; impact rides the FOV punch + shake alone (operator's call). HitStopMaxFrames / HitStopFreezeEnabled / FinisherHoldFrames retire with it, and the three saved feel profiles drop the dead keys. Worth recording: dropping the hold ALONE measured slightly worse (max deviation 0.653 vs 0.447) — the hold had been partly masking the shake integration. Both fixes together: deviation returns to 0.000 within a few frames of a kill, mean deviation 0.038 across the window. The 0.428 peak is now the shake impulse itself, a transient punch that immediately returns instead of a drift that lingers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,14 +39,10 @@ namespace ProjectM.Client
|
||||
public static float HitStopFovKickMin;
|
||||
/// <summary>Max FOV kick (deg) on a heavy player-dealt hit (delta >= HitStopRefDamage).</summary>
|
||||
public static float HitStopFovKickMax;
|
||||
/// <summary>Reserved cap for the (deferred) true freeze-frame, in fixed frames.</summary>
|
||||
public static int HitStopMaxFrames;
|
||||
/// <summary>Damage delta that saturates the player-dealt punch to HitStopFovKickMax.</summary>
|
||||
public static float HitStopRefDamage;
|
||||
/// <summary>Tint for the (deferred) enemy material hit-flash — exposed now, wired in the ShaderGraph slice.</summary>
|
||||
public static Color HitFlashColor;
|
||||
/// <summary>Master gate for the (deferred) true freeze-frame hit-stop. FALSE for v1 (camera-punch only).</summary>
|
||||
public static bool HitStopFreezeEnabled;
|
||||
|
||||
// ---- Feature 1/2: death camera punch ----
|
||||
/// <summary>Camera shake on LOCAL player death (loudest event by design).</summary>
|
||||
@@ -170,8 +166,6 @@ namespace ProjectM.Client
|
||||
public static float MeleeArcIntensity;
|
||||
/// <summary>Bubbles shed along a melee cleave (0 = off).</summary>
|
||||
public static int MeleeArcBubbles;
|
||||
/// <summary>Camera hold frames when the melee FINISHER lands (07-20 G5 impact ladder; light hits use HitStopMaxFrames).</summary>
|
||||
public static int FinisherHoldFrames;
|
||||
|
||||
// ---- 07-21 enemy hit-reacts (G5; the Rukhanka-safe flinch that replaced the cut vibrate) ----
|
||||
/// <summary>Seconds the light flinch pulse holds (0 = hit-reacts OFF).</summary>
|
||||
@@ -252,10 +246,8 @@ namespace ProjectM.Client
|
||||
HitStopDurationMs = 90f;
|
||||
HitStopFovKickMin = 0.6f;
|
||||
HitStopFovKickMax = 2.2f;
|
||||
HitStopMaxFrames = 2; // C4: a 2-frame (~33ms) camera hold reads as crunch, not a lag-y stutter
|
||||
HitStopRefDamage = 30f;
|
||||
HitFlashColor = new Color(1f, 0.85f, 0.55f, 1f);
|
||||
HitStopFreezeEnabled = true; // C4: enable the finisher hit-stop hold (presentation-only camera freeze, bounded below)
|
||||
|
||||
// Feature 1/2 death
|
||||
PlayerDeathShake = 0.50f;
|
||||
@@ -318,7 +310,6 @@ namespace ProjectM.Client
|
||||
CombatIdleHoldSec = 4f; // 07-18: Menacing01 combat-idle hold after the last swing
|
||||
MeleeArcIntensity = 0.8f; // 07-21 HEAVY LOCK: the weapon is the read, the arc recedes further
|
||||
MeleeArcBubbles = 6;
|
||||
FinisherHoldFrames = 7; // 07-21 HEAVY LOCK: a fatter finisher beat (~117ms, still under the 8-frame ladder cap)
|
||||
HitReactSeconds = 0.3f; // 07-21: light flinch pulse (clip plays at 1.4x -> the peak lands inside it)
|
||||
HitStaggerSeconds = 0.55f; // heavy stagger pulse
|
||||
HitReactStaggerDamage = 50f; // finisher (63 seeded) staggers, light (42) flinches -- tracks B2 poise
|
||||
|
||||
@@ -58,10 +58,6 @@ namespace ProjectM.Client
|
||||
s_fovLambda = 3f / durSec; // ~95% decayed after durSec (3 time constants)
|
||||
}
|
||||
|
||||
/// <summary>C4 hit-stop: hold the follow camera for a couple frames on a heavy hit (the combo finisher) so the
|
||||
/// impact lands with a beat of crunch. Presentation only — NEVER Time.timeScale (the deterministic sim keeps ticking).</summary>
|
||||
static int s_holdFrames;
|
||||
public static void Hold(int frames) { if (frames > s_holdFrames) s_holdFrames = frames; }
|
||||
|
||||
|
||||
[Header("Angle (degrees)")]
|
||||
@@ -95,6 +91,11 @@ namespace ProjectM.Client
|
||||
|
||||
Camera _cam;
|
||||
Vector3 _leadOffset; // smoothed look-ahead offset (world units), eased toward the desired lead each frame
|
||||
// The SMOOTHED follow position, kept free of shake. Reading the shaken transform back into the follow
|
||||
// filter is what made the camera wander in combat (see LateUpdate). Seeded from the transform on the
|
||||
// first frame so a scene-authored camera pose is honoured rather than snapped from the origin.
|
||||
Vector3 _basePos;
|
||||
bool _baseInit;
|
||||
|
||||
// 07-16c DEV ZOOM (LoL/SoD-style scroll-to-inspect; may not ship): the wheel moves a smoothed
|
||||
// distance target between ZoomMinDistance and the rig's authored Distance — zoom IN only, scroll
|
||||
@@ -156,15 +157,32 @@ namespace ProjectM.Client
|
||||
// uninitialized -> no change). Multiplies the serialized FollowSharpness so scenes stay untouched.
|
||||
float dragSharp = FollowSharpness * Mathf.Max(0.05f, FeelConfig.CameraDragMult <= 0f ? 1f : FeelConfig.CameraDragMult);
|
||||
float k = FollowSharpness <= 0f ? 1f : 1f - Mathf.Exp(-dragSharp * Time.deltaTime);
|
||||
Vector3 basePos;
|
||||
if (s_holdFrames > 0) { s_holdFrames--; basePos = transform.position; } // C4: brief hit-stop hold (freeze the follow a couple frames)
|
||||
else basePos = Vector3.Lerp(transform.position, desired, k);
|
||||
// 2026-08-13, TWO fixes here — they are the whole "combat freezes on a kill / doesn't feel smooth" bug.
|
||||
//
|
||||
// (1) SHAKE IS A TRANSIENT OFFSET, NOT PART OF THE FOLLOW STATE. The smoothing used to run as
|
||||
// Lerp(transform.position, desired, k) — and transform.position already contained LAST frame's
|
||||
// random shake, so the filter saw shake as real positional error and integrated it, correcting
|
||||
// only ~9 %/frame (FollowSharpness 8 at ~12 ms). Shake was never subtracted, only slowly lerped
|
||||
// out while new shake was added on top, so in sustained combat the camera random-walked around
|
||||
// its framing and never settled. Measured on a STATIONARY player (ideal motion = 0): one kill
|
||||
// left it 0.45 units off-frame and still 0.20 off 24 frames later. The follow now smooths
|
||||
// _basePos, which shake never touches; shake is added only when writing the transform.
|
||||
//
|
||||
// (2) The camera position-HOLD is gone. It froze the follow for a fixed number of RENDER frames on
|
||||
// every kill/heavy hit, so the freeze length was framerate-dependent (7 frames = 49 ms at
|
||||
// 144 fps, 233 ms at 30 fps), and it took its base from transform.position, permanently baking
|
||||
// that frame's shake in — a measured 0.28-unit single-frame jump. A camera that stops while the
|
||||
// world keeps moving reads as a hitch, not as crunch; impact rides the FOV punch + shake alone.
|
||||
if (!_baseInit) { _basePos = transform.position; _baseInit = true; }
|
||||
_basePos = Vector3.Lerp(_basePos, desired, k);
|
||||
|
||||
Vector3 writePos = _basePos;
|
||||
if (s_shake > 0.0001f)
|
||||
{
|
||||
basePos += UnityEngine.Random.insideUnitSphere * s_shake;
|
||||
writePos += UnityEngine.Random.insideUnitSphere * s_shake;
|
||||
s_shake = Mathf.Lerp(s_shake, 0f, 1f - Mathf.Exp(-12f * Time.deltaTime));
|
||||
}
|
||||
transform.SetPositionAndRotation(basePos, rot);
|
||||
transform.SetPositionAndRotation(writePos, rot);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"feel": [
|
||||
{ "k": "MeleeArcIntensity", "v": "0.8" },
|
||||
{ "k": "MeleeArcBubbles", "v": "6" },
|
||||
{ "k": "FinisherHoldFrames", "v": "7" },
|
||||
{ "k": "MeleeConnectFovKick", "v": "1.1" },
|
||||
{ "k": "CombatIdleHoldSec", "v": "4" }
|
||||
]
|
||||
|
||||
@@ -32,10 +32,6 @@
|
||||
"k": "MeleeArcIntensity",
|
||||
"v": "0.8"
|
||||
},
|
||||
{
|
||||
"k": "FinisherHoldFrames",
|
||||
"v": "7"
|
||||
},
|
||||
{
|
||||
"k": "MeleeConnectFovKick",
|
||||
"v": "1.1"
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
],
|
||||
"feel": [
|
||||
{ "k": "MeleeArcIntensity", "v": "1.1" },
|
||||
{ "k": "FinisherHoldFrames", "v": "4" },
|
||||
{ "k": "MeleeConnectFovKick", "v": "0.7" }
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user