From 192e06618233a8686f98ba56255ce91fe79a8a90 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 12 Apr 2026 19:24:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(app+core):=20Phase=20B.3=20=E2=80=94=20play?= =?UTF-8?q?er=20cull-exempt,=20jump=20height,=20slope=20Z?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three user-reported movement fixes: 1. Player disappears when facing away: StaticMeshRenderer now accepts an alwaysVisibleEntityId. When a culled landblock contains the player entity, it is still drawn. Prevents the frustum culler from hiding the player character when they walk far from their spawn landblock. 2. Jump too high: JumpImpulse reduced from 10.0 to 3.5 (placeholder; retail scales by Jump skill value from the server). 3. Slope Z alignment: replaced the frame-delta slope bias with a foot-forward sampling approach — sample terrain Z at 1 unit ahead in the walk direction and use max(center, foot) as the ground Z. Handles multi-grade slopes where the terrain rises faster than a single-point sample tracks. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Input/PlayerMovementController.cs | 25 +++++++++-------- src/AcDream.App/Rendering/GameWindow.cs | 3 ++- .../Rendering/StaticMeshRenderer.cs | 27 ++++++++++++++++--- .../Input/PlayerMovementControllerTests.cs | 4 +-- 4 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/AcDream.App/Input/PlayerMovementController.cs b/src/AcDream.App/Input/PlayerMovementController.cs index b1df540..469a74f 100644 --- a/src/AcDream.App/Input/PlayerMovementController.cs +++ b/src/AcDream.App/Input/PlayerMovementController.cs @@ -64,7 +64,7 @@ public sealed class PlayerMovementController public float VerticalVelocity { get; private set; } public bool IsAirborne { get; private set; } - public float JumpImpulse { get; set; } = 10f; + public float JumpImpulse { get; set; } = 3.5f; // placeholder; retail scales by Jump skill public float GravityAccel { get; set; } = 20f; public float AirControlFactor { get; set; } = 0.2f; @@ -200,19 +200,22 @@ public sealed class PlayerMovementController } } - // Upward bias prevents feet from sinking into the terrain surface. - // On slopes the visual terrain mesh rises ahead of the physics sample - // point, so we add extra bias proportional to how fast the ground Z is - // changing (steeper slope → more bias). Only apply when grounded — during - // jumps/falls the bias would interfere with the ballistic arc. - float slopeBias = 0f; - if (!IsAirborne) + // Foot-forward Z sampling: on slopes the visual terrain at the + // character's feet (slightly forward) can be higher than at the center. + // Sample a point ~1 unit ahead in the walk direction and use the MAX + // of center and foot Z. Only when grounded and moving. + if (!IsAirborne && (dx != 0f || dy != 0f)) { - float slopeDelta = MathF.Max(0f, newZ - _prevGroundZ); - slopeBias = MathF.Min(slopeDelta * 3f, 0.8f); + float footX = result.Position.X + forwardX * 1.0f; + float footY = result.Position.Y + forwardY * 1.0f; + var footResult = _physics.Resolve( + new Vector3(footX, footY, newZ), CellId, + Vector3.Zero, StepUpHeight); + if (footResult.IsOnGround && footResult.Position.Z > newZ) + newZ = footResult.Position.Z; } _prevGroundZ = newZ; - Position = new Vector3(result.Position.X, result.Position.Y, newZ + 0.15f + slopeBias); + Position = new Vector3(result.Position.X, result.Position.Y, newZ + 0.1f); CellId = result.CellId; // 4. Determine current motion commands. diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index e298d7c..833e93c 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -1746,7 +1746,8 @@ public sealed class GameWindow : IDisposable var camera = _cameraController.Active; var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * camera.Projection); _terrain?.Draw(camera, frustum); - _staticMesh?.Draw(camera, _worldState.LandblockEntries, frustum); + _staticMesh?.Draw(camera, _worldState.LandblockEntries, frustum, + alwaysVisibleEntityId: _playerMode ? _playerServerGuid : null); // Count visible vs total for the perf overlay. foreach (var entry in _worldState.LandblockEntries) diff --git a/src/AcDream.App/Rendering/StaticMeshRenderer.cs b/src/AcDream.App/Rendering/StaticMeshRenderer.cs index 8ba7046..635e796 100644 --- a/src/AcDream.App/Rendering/StaticMeshRenderer.cs +++ b/src/AcDream.App/Rendering/StaticMeshRenderer.cs @@ -78,7 +78,8 @@ public sealed unsafe class StaticMeshRenderer : IDisposable public void Draw(ICamera camera, IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities)> landblockEntries, - FrustumPlanes? frustum = null) + FrustumPlanes? frustum = null, + uint? alwaysVisibleEntityId = null) { _shader.Use(); _shader.SetMatrix4("uView", camera.View); @@ -91,9 +92,19 @@ public sealed unsafe class StaticMeshRenderer : IDisposable { // Per-landblock frustum cull: one AABB test skips all entities in // this landblock if it is fully outside the view frustum. + // Exception: never cull a landblock containing the player entity. if (frustum is not null && !FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax)) - continue; + { + // Check if this landblock contains the always-visible entity. + bool hasAlwaysVisible = false; + if (alwaysVisibleEntityId is not null) + { + foreach (var e in entry.Entities) + if (e.Id == alwaysVisibleEntityId.Value) { hasAlwaysVisible = true; break; } + } + if (!hasAlwaysVisible) continue; + } foreach (var entity in entry.Entities) { @@ -168,10 +179,18 @@ public sealed unsafe class StaticMeshRenderer : IDisposable foreach (var entry in landblockEntries) { - // Same per-landblock frustum cull for the translucent pass. + // Same per-landblock frustum cull + always-visible exception for pass 2. if (frustum is not null && !FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax)) - continue; + { + bool hasAlwaysVisible = false; + if (alwaysVisibleEntityId is not null) + { + foreach (var e in entry.Entities) + if (e.Id == alwaysVisibleEntityId.Value) { hasAlwaysVisible = true; break; } + } + if (!hasAlwaysVisible) continue; + } foreach (var entity in entry.Entities) { diff --git a/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs b/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs index 530e366..fed7c26 100644 --- a/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs +++ b/tests/AcDream.Core.Tests/Input/PlayerMovementControllerTests.cs @@ -137,7 +137,7 @@ public class PlayerMovementControllerTests Assert.False(controller.IsAirborne, "Should have landed"); // +0.15 Z bias keeps feet above terrain surface (prevents z-fighting). - Assert.Equal(50.15f, controller.Position.Z, precision: 1); + Assert.Equal(50.1f, controller.Position.Z, precision: 1); } [Fact] @@ -177,6 +177,6 @@ public class PlayerMovementControllerTests controller.Update(0.05f, new MovementInput(Forward: true)); Assert.False(controller.IsAirborne, "Player should have landed"); - Assert.Equal(20.15f, controller.Position.Z, precision: 1); + Assert.Equal(20.1f, controller.Position.Z, precision: 1); } }