296 lines
11 KiB
C#
296 lines
11 KiB
C#
|
namespace Fusion.Addons.KCC
|
||
|
{
|
||
|
using UnityEngine;
|
||
|
|
||
|
/// <summary>
|
||
|
/// This processor detect steps (obstacles which block the character moving forward) and reflects blocked movement upwards.
|
||
|
/// </summary>
|
||
|
public class StepUpProcessor : KCCProcessor, IAfterMoveStep
|
||
|
{
|
||
|
// CONSTANTS
|
||
|
|
||
|
public static readonly int DefaultPriority = -1000;
|
||
|
|
||
|
// PRIVATE MEMBERS
|
||
|
|
||
|
[SerializeField][Tooltip("Maximum obstacle height to step on it.")]
|
||
|
private float _stepHeight = 0.5f;
|
||
|
[SerializeField][Tooltip("Maximum depth of the step check.")]
|
||
|
private float _stepDepth = 0.2f;
|
||
|
[SerializeField][Tooltip("Multiplier of unapplied movement projected to step up. This helps traversing obstacles faster.")]
|
||
|
private float _stepSpeed = 1.0f;
|
||
|
[SerializeField][Tooltip("Minimum proportional penetration push-back distance to activate step-up. A value of 0.5f means the KCC must be pushed back from colliding geometry by at least 50% of desired movement.")]
|
||
|
[Range(0.25f, 0.75f)]
|
||
|
private float _minPushBack = 0.5f;
|
||
|
[SerializeField][Tooltip("Radius multiplier used for last sphere-cast (ground surface detection). Lower value work better with shorter step depth.")]
|
||
|
[Range(0.25f, 1.0f)]
|
||
|
private float _groundCheckRadiusScale = 0.5f;
|
||
|
[SerializeField][Tooltip("Clears dynamic velocity when the step up is over. This eliminates bumps when dynamic up velocity is positive (for example after triggering jump).")]
|
||
|
private bool _clearDynamicVelocityOnEnd = true;
|
||
|
[SerializeField][Tooltip("Step-up starts only if the target surface is walkable (angle <= KCCData.MaxGroundAngle).")]
|
||
|
private bool _requireGroundTarget = false;
|
||
|
[SerializeField][Tooltip("Force extra update of collision hits if the step-up is active and moves the KCC.")]
|
||
|
private bool _forceUpdateHits = false;
|
||
|
|
||
|
private KCCOverlapInfo _overlapInfo = new KCCOverlapInfo();
|
||
|
private KCCShapeCastInfo _shapeCastInfo = new KCCShapeCastInfo();
|
||
|
|
||
|
// StepUpProcessor INTERFACE
|
||
|
|
||
|
protected virtual void OnStepUpBegin(KCC kcc, KCCData data) {}
|
||
|
protected virtual void OnStepUpEnd(KCC kcc, KCCData data) {}
|
||
|
|
||
|
// KCCProcessor INTERFACE
|
||
|
|
||
|
public override float GetPriority(KCC kcc) => DefaultPriority;
|
||
|
|
||
|
// IAfterMoveStep INTERFACE
|
||
|
|
||
|
public void Execute(AfterMoveStep stage, KCC kcc, KCCData data)
|
||
|
{
|
||
|
if (_stepHeight <= 0.0f || _stepDepth <= 0.0f || _stepSpeed <= 0.0f)
|
||
|
return;
|
||
|
|
||
|
// Ignore step-up after jump and teleport.
|
||
|
if (data.JumpFrames > 0 || data.HasTeleported == true)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Vector3 checkDesiredDeltaXZ = (data.DesiredPosition - data.BasePosition).OnlyXZ();
|
||
|
float checkDesiredDistanceXZ = checkDesiredDeltaXZ.magnitude;
|
||
|
|
||
|
// No horizontal movement, stopping step-up.
|
||
|
if (checkDesiredDistanceXZ < 0.001f)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
bool tryStepUp = false;
|
||
|
|
||
|
if (HasCollisionsWithinExtent(stage.OverlapInfo, ECollisionType.Slope | ECollisionType.Wall | ECollisionType.Hang) == true)
|
||
|
{
|
||
|
tryStepUp = true;
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
// Following check compares desired distance with real distance traveled by KCC and triggers step-up
|
||
|
// if something is pushing KCC back on horizontal plane, lowering the distance traveled by more than min push back.
|
||
|
|
||
|
Vector3 targetDeltaXZ = (data.TargetPosition - data.BasePosition).OnlyXZ();
|
||
|
float targetDistanceXZ = targetDeltaXZ.magnitude;
|
||
|
|
||
|
if (targetDistanceXZ / checkDesiredDistanceXZ < _minPushBack)
|
||
|
{
|
||
|
tryStepUp = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (tryStepUp == false)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Vector3 basePosition = data.BasePosition;
|
||
|
Vector3 desiredPosition = data.DesiredPosition;
|
||
|
Vector3 targetPosition = data.TargetPosition;
|
||
|
|
||
|
Vector3 desiredDelta = desiredPosition - basePosition;
|
||
|
Vector3 desiredDirection = Vector3.Normalize(desiredDelta);
|
||
|
|
||
|
// The step-up is not triggered if there is no pending movement from the player or external sources.
|
||
|
if (desiredDirection == default)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Ignore step-up while moving upwards or downwards (with ~25° deviation).
|
||
|
float desiredDirectionUpDot = Vector3.Dot(desiredDirection, Vector3.up);
|
||
|
if (Mathf.Abs(desiredDirectionUpDot) >= 0.9f)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Vector3 correctionDelta = targetPosition - desiredPosition;
|
||
|
float correctionDistance = correctionDelta.magnitude;
|
||
|
Vector3 correctionDirection = correctionDistance > 0.001f ? correctionDelta / correctionDistance : -desiredDirection;
|
||
|
|
||
|
// The step-up is not triggered if the correction vector overlaps desired movement hemisphere.
|
||
|
if (Vector3.Dot(desiredDirection, correctionDirection) >= 0.0f)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
Vector3 desiredCheckDirectionXZ = Vector3.Normalize(desiredDirection.OnlyXZ());
|
||
|
Vector3 correctionCheckDirectionXZ = Vector3.Normalize(-correctionDirection.OnlyXZ());
|
||
|
Vector3 combinedCheckDirectionXZ = Vector3.Normalize(desiredCheckDirectionXZ + correctionCheckDirectionXZ);
|
||
|
|
||
|
// Additional XZ comparison of desired direction and correction direction with deviation of ~85°.
|
||
|
if (Vector3.Dot(desiredCheckDirectionXZ, correctionCheckDirectionXZ) < 0.1f)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Following image shows movement step and collision with a geometry from top-down perspective.
|
||
|
// P = Player position before movement.
|
||
|
// D = Desired position before depenetration.
|
||
|
// T = Target position after depenetration.
|
||
|
// I = Impact position (intersection of P->D and collider plane).
|
||
|
//
|
||
|
// D
|
||
|
// | \
|
||
|
// --------T---I------- <- Obstacle
|
||
|
// \
|
||
|
// P
|
||
|
//
|
||
|
// Following code recalculates base step-up position from target position (T) to impact position (I).
|
||
|
// This eliminates sliding along the collider when stepping up.
|
||
|
if (correctionDirection.IsZero() == false && HasCollisionsWithinExtent(stage.OverlapInfo, ECollisionType.Slope) == false)
|
||
|
{
|
||
|
Ray ray = new Ray(basePosition - desiredDelta * 2.0f, desiredDirection);
|
||
|
Plane plane = new Plane(correctionDirection, targetPosition);
|
||
|
|
||
|
if (plane.Raycast(ray, out float distance) == true)
|
||
|
{
|
||
|
targetPosition = ray.GetPoint(distance);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
float checkRadius = kcc.Settings.Radius - kcc.Settings.Extent;
|
||
|
Vector3 checkPosition = targetPosition + new Vector3(0.0f, _stepHeight, 0.0f);
|
||
|
|
||
|
// 1. Upward collision check.
|
||
|
if (kcc.CapsuleOverlap(_overlapInfo, checkPosition, checkRadius, kcc.Settings.Height, QueryTriggerInteraction.Ignore) == true)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
checkPosition += combinedCheckDirectionXZ * _stepDepth;
|
||
|
|
||
|
// 2. Forward collision check. Forward = Combination of desired XZ direction and negative correction XZ direction - to eliminate stepping up along collider surface.
|
||
|
if (kcc.CapsuleOverlap(_overlapInfo, checkPosition, checkRadius, kcc.Settings.Height, QueryTriggerInteraction.Ignore) == true)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
float maxStepHeight = _stepHeight;
|
||
|
bool highestPointFound = default;
|
||
|
Vector3 highestPointNormal = default;
|
||
|
|
||
|
if (_groundCheckRadiusScale < 1.0f)
|
||
|
{
|
||
|
// Ground check can be done with smaller radius to compensate normal on edges.
|
||
|
checkRadius = kcc.Settings.Radius * _groundCheckRadiusScale;
|
||
|
checkPosition += combinedCheckDirectionXZ * (kcc.Settings.Radius - kcc.Settings.Extent - checkRadius);
|
||
|
}
|
||
|
|
||
|
// 3. Downward collision check to get step height.
|
||
|
if (kcc.SphereCast(_shapeCastInfo, checkPosition + new Vector3(0.0f, kcc.Settings.Radius, 0.0f), checkRadius, Vector3.down, maxStepHeight + kcc.Settings.Radius, QueryTriggerInteraction.Ignore, false) == true)
|
||
|
{
|
||
|
Vector3 highestPoint = new Vector3(0.0f, float.MinValue, 0.0f);
|
||
|
|
||
|
for (int i = 0, count = _shapeCastInfo.ColliderHitCount; i < count; ++i)
|
||
|
{
|
||
|
RaycastHit raycastHit = _shapeCastInfo.ColliderHits[i].RaycastHit;
|
||
|
Vector3 raycastHitPoint = raycastHit.point;
|
||
|
|
||
|
if (raycastHitPoint.y > targetPosition.y && raycastHitPoint.y > highestPoint.y)
|
||
|
{
|
||
|
highestPoint = raycastHitPoint;
|
||
|
highestPointNormal = raycastHit.normal;
|
||
|
highestPointFound = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (highestPointFound == true)
|
||
|
{
|
||
|
maxStepHeight = Mathf.Clamp(highestPoint.y - targetPosition.y, 0.0f, _stepHeight);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// For initial attempt, do not try to step up on non-ground surfaces.
|
||
|
if (_requireGroundTarget == true && data.IsSteppingUp == false && data.WasSteppingUp == false)
|
||
|
{
|
||
|
if (highestPointFound == true)
|
||
|
{
|
||
|
float minGroundDot = Mathf.Cos(Mathf.Clamp(data.MaxGroundAngle, 0.0f, 90.0f) * Mathf.Deg2Rad);
|
||
|
float highestPointUpDot = Vector3.Dot(highestPointNormal, Vector3.up);
|
||
|
|
||
|
if (highestPointUpDot < minGroundDot)
|
||
|
{
|
||
|
ProcessStepUpResult(kcc, data, false);
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Project unapplied movement as step-up movement.
|
||
|
float desiredDistance = Vector3.Distance(basePosition, desiredPosition);
|
||
|
float traveledDistance = Vector3.Distance(basePosition, targetPosition);
|
||
|
float remainingDistance = Mathf.Clamp((desiredDistance - traveledDistance) * _stepSpeed, 0.0f, maxStepHeight);
|
||
|
|
||
|
remainingDistance *= Mathf.Clamp01(Vector3.Dot(desiredDirection, -correctionDirection));
|
||
|
|
||
|
data.TargetPosition = targetPosition + new Vector3(0.0f, remainingDistance, 0.0f);
|
||
|
|
||
|
// KCC remains grounded state while stepping up.
|
||
|
data.IsGrounded = true;
|
||
|
data.GroundNormal = Vector3.up;
|
||
|
data.GroundDistance = kcc.Settings.Extent;
|
||
|
data.GroundPosition = data.TargetPosition;
|
||
|
data.GroundTangent = data.TransformDirection;
|
||
|
|
||
|
if (_forceUpdateHits == true)
|
||
|
{
|
||
|
// New position is set, refresh collision hits after the stage.
|
||
|
stage.RequestUpdateHits(true);
|
||
|
}
|
||
|
|
||
|
ProcessStepUpResult(kcc, data, true);
|
||
|
}
|
||
|
|
||
|
// PRIVATE METHODS
|
||
|
|
||
|
private void ProcessStepUpResult(KCC kcc, KCCData data, bool isSteppingUp)
|
||
|
{
|
||
|
data.IsSteppingUp = isSteppingUp;
|
||
|
|
||
|
if (data.IsSteppingUp == true && data.WasSteppingUp == false)
|
||
|
{
|
||
|
OnStepUpBegin(kcc, data);
|
||
|
}
|
||
|
else if (data.IsSteppingUp == false && data.WasSteppingUp == true)
|
||
|
{
|
||
|
if (_clearDynamicVelocityOnEnd == true)
|
||
|
{
|
||
|
// Clearing dynamic velocity ensures that the movement from external sources and jump won't continue after hiting the edge.
|
||
|
data.DynamicVelocity = default;
|
||
|
}
|
||
|
|
||
|
OnStepUpEnd(kcc, data);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
private static bool HasCollisionsWithinExtent(KCCOverlapInfo overlapInfo, ECollisionType collisionTypes)
|
||
|
{
|
||
|
for (int i = 0; i < overlapInfo.ColliderHitCount; ++i)
|
||
|
{
|
||
|
KCCOverlapHit hit = overlapInfo.ColliderHits[i];
|
||
|
if (hit.IsWithinExtent == true && (collisionTypes & hit.CollisionType) != default)
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|