//#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;
///
/// Maximum distance between two contacts for them to be considered near in relation to the height of the capsule.
///
private const float maxNearContactDistanceOverHeight = 0.03f;
///
/// How far it can move forward in the step down loop relative to the movement length.
///
private const float maxStepDownForwardDistanceOverMovement = 0.5f;
///
/// How far it can move sideways in the step down loop relative to the movement length.
///
private const float maxStepDownLateralDistanceOverMovement = 1.5f;
///
/// Minimum scaling that can be applied to the movement when overlapping other colliders.
///
private const float minMovementScalingOnOverlap = 0.05f;
private CharacterCapsule capsule;
private CapsuleCollider capsuleCollider;
#region Cached Values
///
/// Minimum up component of the ground normal for it to be considered floor.
///
private float minFloorUp;
///
/// Tangent of the max floor angle.
///
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
///
/// 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.
///
public static MoveContact[] NewMoveContactArray => new MoveContact[MaxContactsCount];
///
/// Maximum number of contacts which can occur during a Move call.
///
private static int MaxContactsCount => Mathf.Max(
maxMoveLoopIterationsSimpleSlide,
maxMoveLoopIterationsWalk + maxMoveLoopIterationsStepClimb + maxStepDownLoopIterations);
private void Awake()
{
capsule = GetComponent();
}
///
/// 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.
///
/// If true, the ground info is considered valid otherwise it is ignored.
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 _);
}
///
/// Adjusts the movement so that it doesn't move too much into the overlapping colliders.
///
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);
}
///
/// Main move loop which performs movement and handles collision and step climbing.
///
/// Direction which it can't move against on each iteration.
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;
}
///
/// 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.
///
/// Normal of the step contact, facing away from the step.
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;
}
///
/// 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.
///
/// Max reachable distance in the downward direction.
/// Max downward distance if at the end of the loop no floor has been found.
/// True if it has found floor, false otherwise.
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);
}
///
/// Checks if the contact point is on a floor surface or on the edge of a step with floor on top.
///
/// True if is on floor, false otherwise.
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 _);
}
///
/// Gets the resulting movement by resolving the given movement against the contact surface.
///
/// Direction which the resulting movement can't be against.
/// If true, it projects the movement on a floor surface if has a floor
/// surface or is present.
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;
}
///
/// Gets the resulting movement by resolving the given movement against the contact surface.
///
/// If true, it projects the movement on a floor surface if has a
/// floor surface.
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;
}
}
}