fix(camera): pin chase-camera Z to last-grounded while airborne
K-fix11 (150 ms exponential lag) wasn't aggressive enough — at 0.15 s time constant the camera catches up to ~96 % of the player's Z before peak, so the visible "rise on screen" was maybe ~0.5 m of the 3.11 m arc. User reported the jump still looked short. K-fix12: replace the lag with an explicit airborne-pin. The camera's tracked Z follows player Z directly while grounded, but stays PINNED while airborne and rising. Falling / dropping catches up immediately so we don't end up below ground when landing in a hole. Effect: during a jump the player visibly rises 3 m above the camera on screen, matching retail's "you can see yourself jump" feel. After landing the camera's tracked Z snaps back to the player Z so there's no lingering vertical offset. ChaseCamera.Update gains an isOnGround parameter; GameWindow passes result.IsOnGround from the per-frame movement controller. The look-at point still uses raw player Z so the camera tilts up to keep the airborne character framed. Tests stay 1222 green.
This commit is contained in:
parent
05ce090346
commit
4b6fcffa01
2 changed files with 41 additions and 35 deletions
|
|
@ -46,21 +46,15 @@ public sealed class ChaseCamera : ICamera
|
||||||
private float _playerYaw;
|
private float _playerYaw;
|
||||||
private Vector3 _lookAt;
|
private Vector3 _lookAt;
|
||||||
|
|
||||||
// K-fix11 (2026-04-26): smoothed Z follow. Without this the camera
|
// K-fix12 (2026-04-26): retail-feel jump camera. The camera Z is
|
||||||
// tracks player.Z 1:1 — during a 3 m jump the camera also rises 3 m
|
// pinned to the LAST GROUNDED Z while the player is airborne — the
|
||||||
// and the player's screen position never changes, making the jump
|
// character rises above the camera on screen, visually matching
|
||||||
// visually invisible. A retail observer sees ACdream's jump arc
|
// retail's "you can see yourself jump" feedback. Walking on the
|
||||||
// clearly because retail's chase camera lags the Z follow. Mirror
|
// ground tracks Z directly (no lag on hill transitions); falling
|
||||||
// that: the camera Z target uses an exponential low-pass filter
|
// catches up immediately so we don't end up below ground when
|
||||||
// toward player.Z with a ~150 ms time constant. Initialised on
|
// landing in a pit. Only the upward-while-airborne case is pinned.
|
||||||
// first Update so spawn doesn't snap.
|
private float _trackedZ;
|
||||||
private float _smoothedPlayerZ;
|
private bool _trackedZInitialised;
|
||||||
private bool _smoothedZInitialised;
|
|
||||||
/// <summary>Time constant for the camera-Z smoothing filter (seconds).
|
|
||||||
/// Smaller = camera catches up faster (less perceptible jump arc on
|
|
||||||
/// screen). Larger = jumps look bigger but camera lags on slope
|
|
||||||
/// transitions. 0.15 = retail-feel feedback for a ~0.5–1.0 s jump.</summary>
|
|
||||||
public float ZFollowTimeConstant { get; set; } = 0.15f;
|
|
||||||
|
|
||||||
public Matrix4x4 View =>
|
public Matrix4x4 View =>
|
||||||
Matrix4x4.CreateLookAt(Position, _lookAt, Vector3.UnitZ);
|
Matrix4x4.CreateLookAt(Position, _lookAt, Vector3.UnitZ);
|
||||||
|
|
@ -69,30 +63,37 @@ public sealed class ChaseCamera : ICamera
|
||||||
Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 1f, 5000f);
|
Matrix4x4.CreatePerspectiveFieldOfView(FovY, Aspect, 1f, 5000f);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update the camera position to follow the player. <paramref name="dt"/>
|
/// Update the camera position to follow the player. <paramref name="isOnGround"/>
|
||||||
/// drives the Z-smoothing filter.
|
/// drives the airborne-pin behavior: while airborne and rising, the
|
||||||
|
/// camera stays at last-grounded Z so the jump is visible on screen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Update(Vector3 playerPosition, float playerYaw, float dt = 1f / 60f)
|
public void Update(Vector3 playerPosition, float playerYaw, bool isOnGround = true, float dt = 1f / 60f)
|
||||||
{
|
{
|
||||||
_playerYaw = playerYaw;
|
_playerYaw = playerYaw;
|
||||||
|
|
||||||
// K-fix11: smooth the camera's Z target using an exponential
|
// K-fix12: track the camera's reference Z.
|
||||||
// low-pass filter. Camera Z lags player Z so airborne motion
|
// - On ground: snap directly to player.Z (smooth slope walking).
|
||||||
// visibly shows up on screen.
|
// - Airborne + rising: stay pinned (player visibly rises above camera).
|
||||||
if (!_smoothedZInitialised)
|
// - Airborne + falling below tracked Z: catch up so we don't lag below
|
||||||
|
// ground when landing somewhere lower (a cliff / hole).
|
||||||
|
if (!_trackedZInitialised)
|
||||||
{
|
{
|
||||||
_smoothedPlayerZ = playerPosition.Z;
|
_trackedZ = playerPosition.Z;
|
||||||
_smoothedZInitialised = true;
|
_trackedZInitialised = true;
|
||||||
}
|
}
|
||||||
else
|
else if (isOnGround)
|
||||||
{
|
{
|
||||||
float alpha = 1f - MathF.Exp(-MathF.Max(dt, 1e-4f) / MathF.Max(ZFollowTimeConstant, 1e-4f));
|
_trackedZ = playerPosition.Z;
|
||||||
_smoothedPlayerZ += (playerPosition.Z - _smoothedPlayerZ) * alpha;
|
|
||||||
}
|
}
|
||||||
|
else if (playerPosition.Z < _trackedZ)
|
||||||
|
{
|
||||||
|
_trackedZ = playerPosition.Z; // catch up to falls / drops
|
||||||
|
}
|
||||||
|
// else: airborne and rising — keep _trackedZ pinned.
|
||||||
|
|
||||||
// Look-at uses the actual player Z so the camera always points
|
// Look-at uses the actual player Z so the camera always points
|
||||||
// at the character — when player rises above the lagged camera
|
// at the character — when the player rises above the pinned
|
||||||
// the camera tilts up slightly to keep them in frame.
|
// camera the look-at tilts up to keep them centered in frame.
|
||||||
_lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
|
_lookAt = playerPosition + new Vector3(0f, 0f, EyeHeight);
|
||||||
|
|
||||||
// Camera offset: behind the player (-forward direction) plus any
|
// Camera offset: behind the player (-forward direction) plus any
|
||||||
|
|
@ -107,7 +108,7 @@ public sealed class ChaseCamera : ICamera
|
||||||
Position = new Vector3(
|
Position = new Vector3(
|
||||||
playerPosition.X - forwardX * horizontalDist,
|
playerPosition.X - forwardX * horizontalDist,
|
||||||
playerPosition.Y - forwardY * horizontalDist,
|
playerPosition.Y - forwardY * horizontalDist,
|
||||||
_smoothedPlayerZ + EyeHeight + verticalDist); // ← uses smoothed Z, not raw
|
_trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -3987,11 +3987,16 @@ public sealed class GameWindow : IDisposable
|
||||||
_worldState.RelocateEntity(pe, currentLb);
|
_worldState.RelocateEntity(pe, currentLb);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update chase camera. K-fix11 (2026-04-26): pass dt so the
|
// Update chase camera. K-fix12 (2026-04-26): pass isOnGround
|
||||||
// Z-smoothing low-pass filter tracks correctly on variable
|
// so the camera pins its Z to last-grounded while the
|
||||||
// frame rates — without this the camera tracks player.Z 1:1
|
// player is airborne — without this the camera follows
|
||||||
// and jumps look invisible (camera rises with player).
|
// player.Z 1:1 during a jump and the player's screen
|
||||||
_chaseCamera.Update(result.Position, _playerController.Yaw, (float)dt);
|
// position never changes. With the pin: player visibly
|
||||||
|
// rises above the camera, matching retail "you can see
|
||||||
|
// yourself jump" feedback.
|
||||||
|
_chaseCamera.Update(result.Position, _playerController.Yaw,
|
||||||
|
isOnGround: result.IsOnGround,
|
||||||
|
dt: (float)dt);
|
||||||
|
|
||||||
// Send outbound movement messages to the live server.
|
// Send outbound movement messages to the live server.
|
||||||
if (_liveSession is not null)
|
if (_liveSession is not null)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue