741 lines
31 KiB
C#
741 lines
31 KiB
C#
//#define MB_DEBUG
|
||
|
||
using UnityEngine;
|
||
using MenteBacata.ScivoloCharacterController.Internal;
|
||
using static MenteBacata.ScivoloCharacterController.Internal.Math;
|
||
using static MenteBacata.ScivoloCharacterController.Internal.MathUtils;
|
||
using static MenteBacata.ScivoloCharacterController.Internal.StepDetectionUtils;
|
||
using static MenteBacata.ScivoloCharacterController.Internal.MovementSurfaceUtils;
|
||
using static MenteBacata.ScivoloCharacterController.Internal.GeometricTests;
|
||
using static MenteBacata.ScivoloCharacterController.Internal.OverlapUtils;
|
||
using static MenteBacata.ScivoloCharacterController.Internal.SweepTestsWithPadding;
|
||
using Plane = MenteBacata.ScivoloCharacterController.Internal.Plane;
|
||
|
||
namespace MenteBacata.ScivoloCharacterController
|
||
{
|
||
[RequireComponent(typeof(CharacterCapsule))]
|
||
public partial class CharacterMover : MonoBehaviour
|
||
{
|
||
[Tooltip("Simple Slide: it moves doing simple collide and slide. Suitable for air/water movement.\nWalk: it climbs steps, clamps to floor and prevents climbing steep slope. Suitable for walking/running or any movement on feet.")]
|
||
public Mode mode = Mode.SimpleSlide;
|
||
|
||
[Range(30f, 75f)]
|
||
[Tooltip("Maximum angle a slope can have in order to be considered floor.")]
|
||
public float maxFloorAngle = 45f;
|
||
|
||
[Header("Walk Mode Only")]
|
||
[Min(0f)]
|
||
[Tooltip("Maximum height of climbable step.")]
|
||
public float maxStepHeight = 0.3f;
|
||
|
||
[Header("Simple Slide Mode Only")]
|
||
[Tooltip("Allows the character to climb slopes which exceed the maximum floor angle.")]
|
||
public bool canClimbSteepSlope = true;
|
||
|
||
private const int maxMoveLoopIterationsSimpleSlide = 5;
|
||
|
||
private const int maxMoveLoopIterationsWalk = 3;
|
||
|
||
private const int maxMoveLoopIterationsStepClimb = 2;
|
||
|
||
private const int maxStepDownLoopIterations = 2;
|
||
|
||
private const int maxStepClimbAttempts = 1;
|
||
|
||
/// <summary>
|
||
/// Maximum distance between two contacts for them to be considered near in relation to the height of the capsule.
|
||
/// </summary>
|
||
private const float maxNearContactDistanceOverHeight = 0.03f;
|
||
|
||
/// <summary>
|
||
/// How far it can move forward in the step down loop relative to the movement length.
|
||
/// </summary>
|
||
private const float maxStepDownForwardDistanceOverMovement = 0.5f;
|
||
|
||
/// <summary>
|
||
/// How far it can move sideways in the step down loop relative to the movement length.
|
||
/// </summary>
|
||
private const float maxStepDownLateralDistanceOverMovement = 1.5f;
|
||
|
||
/// <summary>
|
||
/// Minimum scaling that can be applied to the movement when overlapping other colliders.
|
||
/// </summary>
|
||
private const float minMovementScalingOnOverlap = 0.05f;
|
||
|
||
private CharacterCapsule capsule;
|
||
|
||
private CapsuleCollider capsuleCollider;
|
||
|
||
#region Cached Values
|
||
/// <summary>
|
||
/// Minimum up component of the ground normal for it to be considered floor.
|
||
/// </summary>
|
||
private float minFloorUp;
|
||
|
||
/// <summary>
|
||
/// Tangent of the max floor angle.
|
||
/// </summary>
|
||
private float tanMaxFloorAngle;
|
||
|
||
private LayerMask collisionMask;
|
||
|
||
private float capsuleHeight;
|
||
|
||
private float capsuleRadius;
|
||
|
||
private float capsuleContactOffset;
|
||
|
||
private float capsuleVerticalOffset;
|
||
|
||
private Vector3 upDirection;
|
||
|
||
private Vector3 toCapsuleLowerCenter;
|
||
|
||
private Vector3 toCapsuleUpperCenter;
|
||
|
||
private Quaternion capsuleRotation;
|
||
#endregion
|
||
|
||
|
||
/// <summary>
|
||
/// Returns a new instance of a MoveContact array that is of optimal length to contain all possible contacts that can occur during a
|
||
/// single call to the Move method.
|
||
/// </summary>
|
||
public static MoveContact[] NewMoveContactArray => new MoveContact[MaxContactsCount];
|
||
|
||
/// <summary>
|
||
/// Maximum number of contacts which can occur during a Move call.
|
||
/// </summary>
|
||
private static int MaxContactsCount => Mathf.Max(
|
||
maxMoveLoopIterationsSimpleSlide,
|
||
maxMoveLoopIterationsWalk + maxMoveLoopIterationsStepClimb + maxStepDownLoopIterations);
|
||
|
||
private void Awake()
|
||
{
|
||
capsule = GetComponent<CharacterCapsule>();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Moves the character according to the movement settings trying to fulfill the desired movement as close as it can, if a
|
||
/// MoveContact array is provided it populates it with the contact informations it has found during the movement.
|
||
/// </summary>
|
||
/// <param name="hasGroundInfo">If true, the ground info is considered valid otherwise it is ignored.</param>
|
||
public void Move(Vector3 desiredMovement, bool hasGroundInfo, in GroundInfo groundInfo, int overlapCount, Collider[] overlaps, MoveContact[] moveContacts, out int contactCount)
|
||
{
|
||
UpdateCachedValues();
|
||
|
||
Vector3 position = capsule.Position;
|
||
|
||
if (mode == Mode.Walk)
|
||
{
|
||
bool hasInitialFloor = hasGroundInfo && GetMovementSurface(groundInfo.tangentNormal, upDirection, minFloorUp) == MovementSurface.Floor;
|
||
Vector3? initialFloorNormal = hasInitialFloor ? groundInfo.tangentNormal : (Vector3?)null;
|
||
|
||
MoveForWalkMode(ref position, desiredMovement, initialFloorNormal, overlapCount, overlaps, moveContacts, out contactCount);
|
||
}
|
||
else
|
||
{
|
||
MoveForSimpleSlide(ref position, desiredMovement, overlapCount, overlaps, moveContacts, out contactCount);
|
||
}
|
||
|
||
capsule.Position = position;
|
||
}
|
||
|
||
private void UpdateCachedValues()
|
||
{
|
||
maxFloorAngle = Mathf.Clamp(maxFloorAngle, 25f, 75f);
|
||
minFloorUp = Mathf.Cos(Mathf.Deg2Rad * maxFloorAngle);
|
||
tanMaxFloorAngle = Mathf.Tan(Mathf.Deg2Rad * maxFloorAngle);
|
||
|
||
capsuleCollider = capsule.CapsuleCollider;
|
||
collisionMask = capsule.CollisionMask;
|
||
capsuleHeight = capsule.Height;
|
||
capsuleRadius = capsule.Radius;
|
||
capsuleContactOffset = capsule.contactOffset;
|
||
capsuleVerticalOffset = capsule.VerticalOffset;
|
||
upDirection = capsule.UpDirection;
|
||
capsuleRotation = capsule.Rotation;
|
||
toCapsuleLowerCenter = capsuleRotation * capsule.LocalLowerHemisphereCenter;
|
||
toCapsuleUpperCenter = capsuleRotation * capsule.LocalUpperHemisphereCenter;
|
||
}
|
||
|
||
private void MoveForWalkMode(ref Vector3 position, Vector3 desiredMovement, Vector3? initialFloorNormal, int overlapCount, Collider[] overlaps, MoveContact[] moveContacts, out int contactCount)
|
||
{
|
||
Vector3 initialPosition = position;
|
||
Vector3 desiredMovementHorizontal = ProjectOnPlane(desiredMovement, upDirection);
|
||
Vector3 progressDirection = Normalized(desiredMovementHorizontal);
|
||
float maxFloorDescent = tanMaxFloorAngle * Magnitude(desiredMovementHorizontal);
|
||
bool hasInitialOverlap = overlapCount > 0;
|
||
|
||
if (initialFloorNormal.HasValue)
|
||
{
|
||
MovementResolver movementResolver = new MovementResolver(upDirection, minFloorUp);
|
||
desiredMovement = movementResolver.GetMovementOneSurface(desiredMovement, initialFloorNormal.Value, false, true);
|
||
}
|
||
else
|
||
{
|
||
// Makes desired movement to go down at max floor angle.
|
||
desiredMovement = desiredMovementHorizontal - maxFloorDescent * upDirection;
|
||
}
|
||
|
||
if (hasInitialOverlap && overlaps != null)
|
||
{
|
||
AdjustMovementToOverlaps(ref desiredMovement, position, overlapCount, overlaps, false);
|
||
}
|
||
|
||
contactCount = 0;
|
||
|
||
MoveLoopOptions moveLoopOptions = new MoveLoopOptions
|
||
{
|
||
canClimbSteepSlope = false,
|
||
canClampToFloor = true,
|
||
canClimbStep = !hasInitialOverlap,
|
||
breakOnSweepOverlap = !hasInitialOverlap
|
||
};
|
||
|
||
DoMoveLoop(ref position, desiredMovement, progressDirection, initialFloorNormal, moveContacts, ref contactCount, moveLoopOptions, maxMoveLoopIterationsWalk, out LoopBreakInfo breakInfo);
|
||
|
||
if (hasInitialOverlap)
|
||
{
|
||
// Skips step down because it could push even deeper into an overlapping collider.
|
||
return;
|
||
}
|
||
|
||
if (breakInfo != LoopBreakInfo.None)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Vector3 movementMade = position - initialPosition;
|
||
|
||
float maxStepDownDistanceNoFloor = Mathf.Max(Dot(movementMade, upDirection) + maxFloorDescent, 0f);
|
||
float maxStepDownDistance = maxStepDownDistanceNoFloor + maxStepHeight;
|
||
|
||
BoundingRectangle stepDownBounds = GetStepDownBounds(position, desiredMovement, movementMade, progressDirection);
|
||
|
||
DoStepDownLoop(ref position, maxStepDownDistance, maxStepDownDistanceNoFloor, stepDownBounds, moveContacts, ref contactCount, !hasInitialOverlap, maxStepDownLoopIterations, out _);
|
||
}
|
||
|
||
private BoundingRectangle GetStepDownBounds(Vector3 position, Vector3 desiredMovement, Vector3 movementMade, Vector3 progressDirection)
|
||
{
|
||
// The length of the horizontal desired movement is used as a reference for the size of the bounds.
|
||
float desiredLength = Magnitude(ProjectOnPlane(desiredMovement, upDirection));
|
||
|
||
// Distance from position to the bounds front side.
|
||
float frontSideDistance = maxStepDownForwardDistanceOverMovement * desiredLength;
|
||
|
||
// Distance from position to the bounds back side. It should pass through the initial position to prevent moving behind it.
|
||
float backSideDistance = Mathf.Max(Dot(movementMade, progressDirection), 0f);
|
||
|
||
BoundingRectangle bounds = new BoundingRectangle
|
||
{
|
||
center = position + 0.5f * (frontSideDistance - backSideDistance) * progressDirection,
|
||
forwardDirection = progressDirection,
|
||
forwardExtent = 0.5f * (frontSideDistance + backSideDistance),
|
||
rightDirection = Cross(upDirection, progressDirection),
|
||
rightExtent = maxStepDownLateralDistanceOverMovement * desiredLength
|
||
};
|
||
|
||
return bounds;
|
||
}
|
||
|
||
private void MoveForSimpleSlide(ref Vector3 position, Vector3 desiredMovement, int overlapCount, Collider[] overlaps, MoveContact[] moveContacts, out int contactCount)
|
||
{
|
||
Vector3 progressDirection = Normalized(desiredMovement);
|
||
|
||
if (overlapCount > 0 && overlaps != null)
|
||
{
|
||
AdjustMovementToOverlaps(ref desiredMovement, position, overlapCount, overlaps, canClimbSteepSlope);
|
||
}
|
||
|
||
contactCount = 0;
|
||
|
||
MoveLoopOptions moveLoopOptions = new MoveLoopOptions
|
||
{
|
||
canClimbSteepSlope = canClimbSteepSlope,
|
||
canClampToFloor = false,
|
||
canClimbStep = false,
|
||
breakOnSweepOverlap = overlapCount == 0
|
||
};
|
||
|
||
DoMoveLoop(ref position, desiredMovement, progressDirection, null, moveContacts, ref contactCount, moveLoopOptions, maxMoveLoopIterationsSimpleSlide, out _);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Adjusts the movement so that it doesn't move too much into the overlapping colliders.
|
||
/// </summary>
|
||
private void AdjustMovementToOverlaps(ref Vector3 movement, Vector3 position, int overlapCount, Collider[] overlaps, bool canClimbSteepSlope)
|
||
{
|
||
Vector3 directionAverage = GetSeparationDirectionAverage(position, capsuleRotation, capsuleCollider, CharacterCapsule.overlapMargin, overlapCount, overlaps);
|
||
|
||
MagnitudeAndDirection(directionAverage, out float magnitude, out Vector3 direction);
|
||
|
||
// The direction average has magnitude between 0 and 1, it is 1 when all the separations have the same direction. Then its
|
||
// magnitude can be used to modulate the movement.
|
||
movement *= Mathf.Max(minMovementScalingOnOverlap, magnitude);
|
||
|
||
if (IsCircaZero(magnitude))
|
||
{
|
||
return;
|
||
}
|
||
|
||
MovementResolver movementResolver = new MovementResolver(upDirection, minFloorUp);
|
||
|
||
// The direction average is treated as a surface normal. It’s not an exact solution as it doesn’t prevent moving into the
|
||
// overlapping colliders, but it helps to limit the movement into them.
|
||
movement = movementResolver.GetMovementOneSurface(movement, direction, canClimbSteepSlope, false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Main move loop which performs movement and handles collision and step climbing.
|
||
/// </summary>
|
||
/// <param name="progressDirection">Direction which it can't move against on each iteration.</param>
|
||
private void DoMoveLoop(ref Vector3 position, Vector3 desiredMovement, Vector3 progressDirection, Vector3? initialFloorNormal, MoveContact[] moveContacts, ref int contactCount, MoveLoopOptions options, int maxIterations, out LoopBreakInfo breakInfo)
|
||
{
|
||
breakInfo = LoopBreakInfo.None;
|
||
|
||
if (Dot(desiredMovement, progressDirection) < 0f)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Vector3 movementToMake = desiredMovement;
|
||
|
||
bool hasCurrentContact = false;
|
||
ContactInfo currentContact = default;
|
||
|
||
Vector3? lastFloorNormal = initialFloorNormal;
|
||
|
||
int stepClimbAttempts = 0;
|
||
|
||
for (int i = 0; i < maxIterations; i++)
|
||
{
|
||
if (IsCircaZero(movementToMake))
|
||
{
|
||
break;
|
||
}
|
||
|
||
SweepCapsuleAndUpdateContact(position, movementToMake, ref hasCurrentContact, ref currentContact, out Vector3 sweepMovement, out SweepResult sweepResult);
|
||
|
||
if (sweepResult.startedWithOverlap && options.breakOnSweepOverlap)
|
||
{
|
||
break;
|
||
}
|
||
|
||
position += sweepMovement;
|
||
|
||
if (!hasCurrentContact)
|
||
{
|
||
break;
|
||
}
|
||
|
||
AddMoveContact(new MoveContact(currentContact.position, currentContact.normal, currentContact.collider), moveContacts, ref contactCount);
|
||
|
||
if (currentContact.surface == MovementSurface.Floor)
|
||
lastFloorNormal = currentContact.normal;
|
||
|
||
movementToMake -= sweepMovement;
|
||
|
||
if (options.canClimbStep && stepClimbAttempts < maxStepClimbAttempts && CanTryClimbStep(position, movementToMake, currentContact))
|
||
{
|
||
stepClimbAttempts++;
|
||
|
||
if (TryClimbStep(ref position, movementToMake, progressDirection, currentContact.normal, moveContacts, ref contactCount))
|
||
{
|
||
breakInfo = LoopBreakInfo.ClimbedStep;
|
||
break;
|
||
}
|
||
}
|
||
|
||
movementToMake = GetMovementOnContact(movementToMake, currentContact, progressDirection, lastFloorNormal, options.canClimbSteepSlope, options.canClampToFloor);
|
||
}
|
||
}
|
||
|
||
private bool CanTryClimbStep(Vector3 position, Vector3 movement, in ContactInfo contact)
|
||
{
|
||
if (IsCircaZero(movement))
|
||
return false;
|
||
|
||
Vector3 movementHorizontal = ProjectOnPlane(movement, upDirection);
|
||
|
||
// The movement is to actually climb up the step, not descending it.
|
||
if (Dot(movementHorizontal, contact.normal) >= 0)
|
||
return false;
|
||
|
||
return IsClimbableStepCandidate(position, contact.position, contact.surface);
|
||
}
|
||
|
||
private bool IsClimbableStepCandidate(Vector3 position, Vector3 contactPosition, in MovementSurface contactSurface)
|
||
{
|
||
if (contactSurface != MovementSurface.SteepSlope && contactSurface != MovementSurface.Wall)
|
||
return false;
|
||
|
||
if (GetPointHeightFromCapsuleBottom(contactPosition, position) >= maxStepHeight)
|
||
return false;
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Tries to climb a step by first stepping up the max step height, then moving horizontally and lastly stepping down. If it steps
|
||
/// down on floor it returns true and updates the position.
|
||
/// </summary>
|
||
/// <param name="stepNormal">Normal of the step contact, facing away from the step.</param>
|
||
private bool TryClimbStep(ref Vector3 position, Vector3 desiredMovement, Vector3 progressDirection, Vector3 stepNormal, MoveContact[] moveContacts, ref int contactCount)
|
||
{
|
||
Vector3 tempPosition = position;
|
||
float maxStepUpDistance = maxStepHeight + capsuleContactOffset;
|
||
|
||
// Step up.
|
||
SweepTest(tempPosition, upDirection, maxStepUpDistance, out SweepResult stepUpResult);
|
||
|
||
tempPosition += stepUpResult.safeDistance * upDirection;
|
||
|
||
if (stepUpResult.startedWithOverlap)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (stepUpResult.safeDistance < epsilon)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
int tempContactCount = contactCount;
|
||
|
||
Vector3 horizontalMovement = ProjectOnPlane(desiredMovement, upDirection);
|
||
|
||
MoveLoopOptions moveLoopOptions = new MoveLoopOptions
|
||
{
|
||
canClimbSteepSlope = false,
|
||
canClampToFloor = false,
|
||
canClimbStep = false,
|
||
breakOnSweepOverlap = true
|
||
};
|
||
|
||
DoMoveLoop(ref tempPosition, horizontalMovement, progressDirection, null, moveContacts, ref tempContactCount, moveLoopOptions, maxMoveLoopIterationsStepClimb, out LoopBreakInfo breakInfo);
|
||
|
||
if (breakInfo == LoopBreakInfo.SweepOverlap)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
Vector3 movementMade = tempPosition - position;
|
||
|
||
if (IsCircaZero(ProjectOnPlane(movementMade, upDirection)))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
float maxStepDownDistance = Mathf.Max(Dot(movementMade, upDirection), 0f) + capsuleContactOffset;
|
||
|
||
BoundingRectangle stepDownBounds = GetStepDownBounds(tempPosition, desiredMovement, movementMade, progressDirection);
|
||
|
||
DoStepDownLoop(ref tempPosition, maxStepDownDistance, 0f, stepDownBounds, moveContacts, ref tempContactCount, true, maxStepDownLoopIterations, out bool hasFoundFloor);
|
||
|
||
if (!hasFoundFloor)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
movementMade = tempPosition - position;
|
||
|
||
// Checks if it has actually moved past the step and not slided back down to the floor where it started. It’s not perfect because
|
||
// it assumes the edge of the step is horizontal, but it helps to prevent many common cases and in general it limits the damage.
|
||
if (Dot(ProjectOnPlane(movementMade, upDirection), stepNormal) > 0f)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
position = tempPosition;
|
||
contactCount = tempContactCount;
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Iteratively slides down until it completes the movement or it reaches the floor. If at the end it hasn't reached the floor then
|
||
/// it goes down using at most the max no floor distance.
|
||
/// </summary>
|
||
/// <param name="maxDistance">Max reachable distance in the downward direction.</param>
|
||
/// <param name="maxDistanceNoFloor">Max downward distance if at the end of the loop no floor has been found.</param>
|
||
/// <param name="hasFoundFloor">True if it has found floor, false otherwise.</param>
|
||
private void DoStepDownLoop(ref Vector3 position, float maxDistance, float maxDistanceNoFloor, BoundingRectangle bounds, MoveContact[] moveContacts, ref int contactCount, bool breakOnSweepOverlap, int maxIterations, out bool hasFoundFloor)
|
||
{
|
||
Vector3 initialPosition = position;
|
||
Vector3 desiredMovement = -maxDistance * upDirection;
|
||
Vector3 remainingMovement = desiredMovement;
|
||
Vector3 movementToMake = remainingMovement;
|
||
|
||
// It can happen when the desired movement passed to the Move method is zero.
|
||
bool hasValidBounds = bounds.forwardDirection != new Vector3(0f, 0f, 0f);
|
||
bool hasCrossedBounds = false;
|
||
|
||
bool hasCurrentContact = false;
|
||
ContactInfo currentContact = default;
|
||
|
||
hasFoundFloor = false;
|
||
|
||
Plane maxDistanceNoFloorPlane = new Plane(upDirection, initialPosition - maxDistanceNoFloor * upDirection, true);
|
||
bool isPastMaxDistanceNoFloor = false;
|
||
Vector3 positionAtMaxDistanceNoFloor = default;
|
||
int contactCountAtMaxDistanceNoFloor = 0;
|
||
for (int i = 0; i < maxIterations; i++)
|
||
{
|
||
if (!CheckMovementForStepDownLoop(movementToMake, remainingMovement))
|
||
{
|
||
break;
|
||
}
|
||
|
||
if (i > 0)
|
||
{
|
||
if (!hasValidBounds || hasCrossedBounds || !bounds.IsPointInside(position))
|
||
{
|
||
break;
|
||
}
|
||
}
|
||
|
||
Vector3 positionBeforeSweep = position;
|
||
|
||
SweepCapsuleAndUpdateContact(position, movementToMake, ref hasCurrentContact, ref currentContact, out Vector3 movementMadeSweep, out SweepResult sweepResult);
|
||
|
||
if (sweepResult.startedWithOverlap && breakOnSweepOverlap)
|
||
{
|
||
break;
|
||
}
|
||
|
||
position += movementMadeSweep;
|
||
|
||
// It checks the intersection with the bounds only after the first iteration, so it at least does the vertical movement.
|
||
if (i > 0)
|
||
{
|
||
if (bounds.CheckLineIntersectionFromInsideToOutside(positionBeforeSweep, position, out Vector3 boundsIntersection))
|
||
{
|
||
position = boundsIntersection;
|
||
hasCurrentContact = false;
|
||
hasCrossedBounds = true;
|
||
}
|
||
}
|
||
|
||
if (!isPastMaxDistanceNoFloor)
|
||
{
|
||
if (CheckLineAndPlaneIntersection(positionBeforeSweep, position, maxDistanceNoFloorPlane, out positionAtMaxDistanceNoFloor))
|
||
{
|
||
contactCountAtMaxDistanceNoFloor = contactCount;
|
||
isPastMaxDistanceNoFloor = true;
|
||
}
|
||
}
|
||
|
||
movementToMake = remainingMovement = desiredMovement - (position - initialPosition);
|
||
|
||
if (hasCurrentContact)
|
||
{
|
||
AddMoveContact(new MoveContact(currentContact.position, currentContact.normal, currentContact.collider), moveContacts, ref contactCount);
|
||
|
||
// After the first iteration, the contact could be generated by a non vertical sweep so it has to check that the point
|
||
// is right below the capsule.
|
||
if (i == 0 || IsPointWithinDistanceFromLine(currentContact.position, position, upDirection, capsuleRadius))
|
||
{
|
||
if (CheckFloorOnContact(currentContact))
|
||
{
|
||
hasFoundFloor = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
movementToMake = GetMovementOnContact(remainingMovement, currentContact, false, false);
|
||
|
||
// Resizes movement so that it has the same downward component as remaining movement.
|
||
if (TryResizeToTargetVerticalComponent(movementToMake, remainingMovement, upDirection, out Vector3 resizedMovement))
|
||
movementToMake = resizedMovement;
|
||
}
|
||
}
|
||
|
||
if (isPastMaxDistanceNoFloor && !hasFoundFloor)
|
||
{
|
||
// Reverts back to the state it was in when it reached the max no floor distance.
|
||
position = positionAtMaxDistanceNoFloor;
|
||
contactCount = contactCountAtMaxDistanceNoFloor;
|
||
}
|
||
}
|
||
|
||
private bool CheckMovementForStepDownLoop(Vector3 movementToMake, Vector3 remainingMovement)
|
||
{
|
||
if (IsCircaZero(movementToMake))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Dot(movementToMake, upDirection) > 0f)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (Dot(remainingMovement, upDirection) > 0f)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private void SweepCapsuleAndUpdateContact(Vector3 position, Vector3 movement, ref bool hasContact, ref ContactInfo contact, out Vector3 sweepMovement, out SweepResult sweepResult)
|
||
{
|
||
MagnitudeAndDirection(movement, out float maxDistance, out Vector3 direction);
|
||
|
||
SweepTest(position, direction, maxDistance, out sweepResult);
|
||
|
||
if (sweepResult.hasHit)
|
||
{
|
||
bool hasPreviousContact = hasContact;
|
||
Vector3 previousNormal = contact.normal;
|
||
MovementSurface previousSurface = contact.surface;
|
||
|
||
ref RaycastHit hit = ref sweepResult.hit;
|
||
|
||
contact = new ContactInfo
|
||
{
|
||
position = hit.point,
|
||
normal = hit.normal,
|
||
surface = GetMovementSurface(hit.normal, upDirection, minFloorUp),
|
||
collider = hit.collider,
|
||
hasNear = hasPreviousContact && sweepResult.safeDistance < maxNearContactDistanceOverHeight * capsuleHeight,
|
||
nearNormal = previousNormal,
|
||
nearSurface = previousSurface
|
||
};
|
||
|
||
hasContact = true;
|
||
}
|
||
else
|
||
{
|
||
hasContact = false;
|
||
}
|
||
|
||
sweepMovement = sweepResult.safeDistance * direction;
|
||
}
|
||
|
||
private void SweepTest(Vector3 position, Vector3 direction, float maxDistance, out SweepResult sweepResult)
|
||
{
|
||
SweepTestCapsule(position + toCapsuleLowerCenter, position + toCapsuleUpperCenter, capsuleRadius, direction, maxDistance, capsuleContactOffset, collisionMask, capsuleCollider, out sweepResult);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Checks if the contact point is on a floor surface or on the edge of a step with floor on top.
|
||
/// </summary>
|
||
/// <returns>True if is on floor, false otherwise.</returns>
|
||
private bool CheckFloorOnContact(in ContactInfo contact)
|
||
{
|
||
if (contact.surface == MovementSurface.Floor)
|
||
return true;
|
||
|
||
if (Dot(contact.normal, upDirection) < 0f)
|
||
return false;
|
||
|
||
return CheckFloorAbovePoint(contact.position, capsuleRadius, minFloorUp, upDirection, collisionMask, capsuleCollider, out _);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the resulting movement by resolving the given movement against the contact surface.
|
||
/// </summary>
|
||
/// <param name="progressDirection">Direction which the resulting movement can't be against.</param>
|
||
/// <param name="canClampToFloor">If true, it projects the movement on a floor surface if <paramref name="contact"/> has a floor
|
||
/// surface or <paramref name="floorNormal"/> is present.</param>
|
||
private Vector3 GetMovementOnContact(Vector3 movement, in ContactInfo contact, Vector3 progressDirection, Vector3? floorNormal, bool canClimbSteepSlope, bool canClampToFloor)
|
||
{
|
||
Vector3 result;
|
||
|
||
MovementResolver movementResolver = new MovementResolver(upDirection, minFloorUp);
|
||
|
||
if (canClampToFloor && floorNormal.HasValue && !HasFloorSurface(contact))
|
||
{
|
||
// Uses the floor normal as the second normal, ignoring the near normal if present.
|
||
result = movementResolver.GetMovementTwoSurfaces(movement, contact.normal, floorNormal.Value, canClimbSteepSlope, true);
|
||
}
|
||
else if (contact.hasNear)
|
||
{
|
||
result = movementResolver.GetMovementTwoSurfaces(movement, contact.normal, contact.nearNormal, canClimbSteepSlope, canClampToFloor);
|
||
}
|
||
else
|
||
{
|
||
result = movementResolver.GetMovementOneSurface(movement, contact.normal, canClimbSteepSlope, canClampToFloor);
|
||
}
|
||
|
||
if (IsCircaZero(result))
|
||
return new Vector3(0f, 0f, 0f);
|
||
|
||
if (Dot(result, progressDirection) >= 0f)
|
||
return result;
|
||
|
||
// Uses the progress direction as a fake surface normal to enforce the constraint.
|
||
result = movementResolver.GetMovementTwoSurfaces(movement, contact.normal, progressDirection, canClimbSteepSlope, canClampToFloor);
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Gets the resulting movement by resolving the given movement against the contact surface.
|
||
/// </summary>
|
||
/// <param name = "canClampToFloor" > If true, it projects the movement on a floor surface if <paramref name = "contact" /> has a
|
||
/// floor surface.</param>
|
||
private Vector3 GetMovementOnContact(Vector3 movement, in ContactInfo contact, bool canClimbSteepSlope, bool canClampToFloor)
|
||
{
|
||
MovementResolver movementResolver = new MovementResolver(upDirection, minFloorUp);
|
||
|
||
if (contact.hasNear)
|
||
{
|
||
return movementResolver.GetMovementTwoSurfaces(movement, contact.normal, contact.nearNormal, canClimbSteepSlope, canClampToFloor);
|
||
}
|
||
|
||
return movementResolver.GetMovementOneSurface(movement, contact.normal, canClimbSteepSlope, canClampToFloor);
|
||
}
|
||
|
||
private bool HasFloorSurface(in ContactInfo contact)
|
||
{
|
||
return contact.surface == MovementSurface.Floor || (contact.hasNear && contact.nearSurface == MovementSurface.Floor);
|
||
}
|
||
|
||
private float GetPointHeightFromCapsuleBottom(Vector3 point, Vector3 capsulePosition)
|
||
{
|
||
return Dot((point - capsulePosition), upDirection) - capsuleVerticalOffset;
|
||
}
|
||
|
||
private void AddMoveContact(in MoveContact moveContact, MoveContact[] moveContacts, ref int contactCount)
|
||
{
|
||
if (moveContacts != null && contactCount < moveContacts.Length)
|
||
moveContacts[contactCount++] = moveContact;
|
||
}
|
||
|
||
public enum Mode : byte
|
||
{
|
||
SimpleSlide = 0,
|
||
Walk = 1
|
||
}
|
||
|
||
private struct MoveLoopOptions
|
||
{
|
||
public bool canClimbSteepSlope;
|
||
public bool canClampToFloor;
|
||
public bool canClimbStep;
|
||
public bool breakOnSweepOverlap;
|
||
}
|
||
|
||
private enum LoopBreakInfo : byte
|
||
{
|
||
None,
|
||
ClimbedStep,
|
||
SweepOverlap
|
||
}
|
||
|
||
private struct ContactInfo
|
||
{
|
||
public Vector3 position;
|
||
public Vector3 normal;
|
||
public MovementSurface surface;
|
||
public Collider collider;
|
||
public bool hasNear;
|
||
public Vector3 nearNormal;
|
||
public MovementSurface nearSurface;
|
||
|
||
}
|
||
}
|
||
}
|