Files
Project-M/Assets/_Project/Scripts/Simulation/Player/CharacterProcessor.cs
T
kronic c58a17b8af Fix: planar Y-lock in CharacterProcessor — no more climbing/floating on sloped colliders
Field report: walking onto hilly-looking colliders (the new convex rock
hulls are ramps) raised the character's Y, and with gravity zero +
SnapToGround off nothing ever brought it back down - the character
floated and its shadow drifted away.

Fix inside CharacterProcessor.PhysicsUpdate: capture the pre-update Y
and restore it (plus zero RelativeVelocity.y) after the CC sequence -
the character update itself can never change the movement plane, while
spawn/teleport writes made outside the update stay honored.
Deterministic + idempotent (rollback-safe), identical on server +
predicted client.

Verified live: player shoved into a cover-hull sloped edge decollided
~1 u horizontally with Y bit-exact at 1.0000 (the case that previously
rode up the slope); 466/466 EditMode, console clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 20:06:05 -07:00

223 lines
11 KiB
C#

using Unity.CharacterController;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
namespace ProjectM.Simulation
{
/// <summary>
/// Per-update "global" data the character processor needs (lookups, singletons). Empty for the minimal
/// top-down character — kept so the CC update signature is satisfied and future lookups have a home.
/// </summary>
public struct CharacterUpdateContext
{
public void OnSystemCreate(ref SystemState state) { }
public void OnSystemUpdate(ref SystemState state) { }
}
/// <summary>
/// Top-down kinematic character processor (CC 1.4.2 pattern: <see cref="IKinematicCharacterProcessor{T}"/>
/// + a <see cref="KinematicCharacterDataAccess"/> built in the job, driving the static
/// <see cref="KinematicCharacterUtilities"/> Update_* sequence). Stripped of all FPS features: no jump,
/// no air movement, no gravity, no view. <see cref="PhysicsUpdate"/> runs the canonical CC update
/// sequence; <see cref="HandleVelocityControl"/> lerps RelativeVelocity toward the desired planar
/// velocity <see cref="PlayerControlSystem"/> wrote. Rotation/facing is owned by <see cref="PlayerAimSystem"/>.
/// </summary>
public struct CharacterProcessor : IKinematicCharacterProcessor<CharacterUpdateContext>
{
public KinematicCharacterDataAccess CharacterDataAccess;
public RefRW<CharacterComponent> CharacterComponent;
public RefRW<CharacterControl> CharacterControl;
public void PhysicsUpdate(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
{
ref CharacterComponent characterComponent = ref CharacterComponent.ValueRW;
ref KinematicCharacterBody characterBody = ref CharacterDataAccess.CharacterBody.ValueRW;
ref float3 characterPosition = ref CharacterDataAccess.LocalTransform.ValueRW.Position;
float planeY = characterPosition.y; // authored movement plane — restored after the update (see below)
KinematicCharacterUtilities.Update_Initialize(
in this, ref context, ref baseContext,
ref characterBody,
CharacterDataAccess.CharacterHitsBuffer,
CharacterDataAccess.DeferredImpulsesBuffer,
CharacterDataAccess.VelocityProjectionHits,
baseContext.Time.DeltaTime);
KinematicCharacterUtilities.Update_ParentMovement(
in this, ref context, ref baseContext,
CharacterDataAccess.CharacterEntity,
ref characterBody,
CharacterDataAccess.CharacterProperties.ValueRO,
CharacterDataAccess.PhysicsCollider.ValueRO,
CharacterDataAccess.LocalTransform.ValueRO,
ref characterPosition,
characterBody.WasGroundedBeforeCharacterUpdate);
KinematicCharacterUtilities.Update_Grounding(
in this, ref context, ref baseContext,
ref characterBody,
CharacterDataAccess.CharacterEntity,
CharacterDataAccess.CharacterProperties.ValueRO,
CharacterDataAccess.PhysicsCollider.ValueRO,
CharacterDataAccess.LocalTransform.ValueRO,
CharacterDataAccess.VelocityProjectionHits,
CharacterDataAccess.CharacterHitsBuffer,
ref characterPosition);
HandleVelocityControl(ref context, ref baseContext);
KinematicCharacterUtilities.Update_PreventGroundingFromFutureSlopeChange(
in this, ref context, ref baseContext,
CharacterDataAccess.CharacterEntity,
ref characterBody,
CharacterDataAccess.CharacterProperties.ValueRO,
CharacterDataAccess.PhysicsCollider.ValueRO,
in characterComponent.StepAndSlopeHandling);
KinematicCharacterUtilities.Update_GroundPushing(
in this, ref context, ref baseContext,
ref characterBody,
CharacterDataAccess.CharacterProperties.ValueRO,
CharacterDataAccess.LocalTransform.ValueRO,
CharacterDataAccess.DeferredImpulsesBuffer,
float3.zero);
KinematicCharacterUtilities.Update_MovementAndDecollisions(
in this, ref context, ref baseContext,
CharacterDataAccess.CharacterEntity,
ref characterBody,
CharacterDataAccess.CharacterProperties.ValueRO,
CharacterDataAccess.PhysicsCollider.ValueRO,
CharacterDataAccess.LocalTransform.ValueRO,
CharacterDataAccess.VelocityProjectionHits,
CharacterDataAccess.CharacterHitsBuffer,
CharacterDataAccess.DeferredImpulsesBuffer,
ref characterPosition);
KinematicCharacterUtilities.Update_MovingPlatformDetection(ref baseContext, ref characterBody);
KinematicCharacterUtilities.Update_ParentMomentum(ref baseContext, ref characterBody,
CharacterDataAccess.LocalTransform.ValueRO.Position);
KinematicCharacterUtilities.Update_ProcessStatefulCharacterHits(
CharacterDataAccess.CharacterHitsBuffer,
CharacterDataAccess.StatefulHitsBuffer);
// Top-down planar lock: the world is one flat plane and gravity is zero, so any Y gained by
// step-handling/velocity-projection up a sloped collider (convex rock hulls etc.) would persist
// FOREVER — the character ends up floating and its shadow drifts away (07-09 field report).
// Restoring the pre-update Y forbids the CC update itself from ever changing the plane while
// spawn/teleport writes (made outside this update) stay honored. Deterministic + idempotent.
characterPosition.y = planeY;
characterBody.RelativeVelocity.y = 0f;
}
void HandleVelocityControl(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
{
float deltaTime = baseContext.Time.DeltaTime;
ref KinematicCharacterBody characterBody = ref CharacterDataAccess.CharacterBody.ValueRW;
ref CharacterComponent characterComponent = ref CharacterComponent.ValueRW;
CharacterControl characterControl = CharacterControl.ValueRO;
// Planar twin-stick: smoothly approach the desired world velocity (already speed-scaled, Y==0).
float3 targetVelocity = characterControl.MoveVelocity;
CharacterControlUtilities.StandardGroundMove_Interpolated(
ref characterBody.RelativeVelocity,
targetVelocity,
characterComponent.GroundedMovementSharpness,
deltaTime,
math.up(),
math.up());
}
public void VariableUpdate(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext) { }
public void UpdateGroundingUp(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext)
{
ref KinematicCharacterBody characterBody = ref CharacterDataAccess.CharacterBody.ValueRW;
KinematicCharacterUtilities.Default_UpdateGroundingUp(
ref characterBody,
CharacterDataAccess.LocalTransform.ValueRO.Rotation);
}
public bool CanCollideWithHit(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext, in BasicHit hit)
{
return PhysicsUtilities.IsCollidable(hit.Material);
}
public bool IsGroundedOnHit(ref CharacterUpdateContext context, ref KinematicCharacterUpdateContext baseContext, in BasicHit hit, int groundingEvaluationType)
{
CharacterComponent characterComponent = CharacterComponent.ValueRO;
return KinematicCharacterUtilities.Default_IsGroundedOnHit(
in this, ref context, ref baseContext,
CharacterDataAccess.CharacterEntity,
CharacterDataAccess.PhysicsCollider.ValueRO,
CharacterDataAccess.CharacterBody.ValueRO,
CharacterDataAccess.CharacterProperties.ValueRO,
in hit,
in characterComponent.StepAndSlopeHandling,
groundingEvaluationType);
}
public void OnMovementHit(
ref CharacterUpdateContext context,
ref KinematicCharacterUpdateContext baseContext,
ref KinematicCharacterHit hit,
ref float3 remainingMovementDirection,
ref float remainingMovementLength,
float3 originalVelocityDirection,
float hitDistance)
{
ref KinematicCharacterBody characterBody = ref CharacterDataAccess.CharacterBody.ValueRW;
ref float3 characterPosition = ref CharacterDataAccess.LocalTransform.ValueRW.Position;
CharacterComponent characterComponent = CharacterComponent.ValueRO;
KinematicCharacterUtilities.Default_OnMovementHit(
in this, ref context, ref baseContext,
ref characterBody,
CharacterDataAccess.CharacterEntity,
CharacterDataAccess.CharacterProperties.ValueRO,
CharacterDataAccess.PhysicsCollider.ValueRO,
CharacterDataAccess.LocalTransform.ValueRO,
ref characterPosition,
CharacterDataAccess.VelocityProjectionHits,
ref hit,
ref remainingMovementDirection,
ref remainingMovementLength,
originalVelocityDirection,
hitDistance,
characterComponent.StepAndSlopeHandling.StepHandling,
characterComponent.StepAndSlopeHandling.MaxStepHeight,
characterComponent.StepAndSlopeHandling.CharacterWidthForStepGroundingCheck);
}
public void OverrideDynamicHitMasses(
ref CharacterUpdateContext context,
ref KinematicCharacterUpdateContext baseContext,
ref PhysicsMass characterMass,
ref PhysicsMass otherMass,
BasicHit hit)
{
}
public void ProjectVelocityOnHits(
ref CharacterUpdateContext context,
ref KinematicCharacterUpdateContext baseContext,
ref float3 velocity,
ref bool characterIsGrounded,
ref BasicHit characterGroundHit,
in DynamicBuffer<KinematicVelocityProjectionHit> velocityProjectionHits,
float3 originalVelocityDirection)
{
CharacterComponent characterComponent = CharacterComponent.ValueRO;
KinematicCharacterUtilities.Default_ProjectVelocityOnHits(
ref velocity,
ref characterIsGrounded,
ref characterGroundHit,
in velocityProjectionHits,
originalVelocityDirection,
characterComponent.StepAndSlopeHandling.ConstrainVelocityToGroundPlane,
in CharacterDataAccess.CharacterBody.ValueRO);
}
}
}