2689 lines
118 KiB
C#
2689 lines
118 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace KinematicCharacterController
|
|
{
|
|
public enum RigidbodyInteractionType
|
|
{
|
|
None,
|
|
Kinematic,
|
|
SimulatedDynamic
|
|
}
|
|
|
|
public enum StepHandlingMethod
|
|
{
|
|
None,
|
|
Standard,
|
|
Extra
|
|
}
|
|
|
|
public enum MovementSweepState
|
|
{
|
|
Initial,
|
|
AfterFirstHit,
|
|
FoundBlockingCrease,
|
|
FoundBlockingCorner,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the entire state of a character motor that is pertinent for simulation.
|
|
/// Use this to save state or revert to past state
|
|
/// </summary>
|
|
[System.Serializable]
|
|
public struct KinematicCharacterMotorState
|
|
{
|
|
public Vector3 Position;
|
|
public Quaternion Rotation;
|
|
public Vector3 BaseVelocity;
|
|
|
|
public bool MustUnground;
|
|
public float MustUngroundTime;
|
|
public bool LastMovementIterationFoundAnyGround;
|
|
public CharacterTransientGroundingReport GroundingStatus;
|
|
|
|
public Rigidbody AttachedRigidbody;
|
|
public Vector3 AttachedRigidbodyVelocity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Describes an overlap between the character capsule and another collider
|
|
/// </summary>
|
|
public struct OverlapResult
|
|
{
|
|
public Vector3 Normal;
|
|
public Collider Collider;
|
|
|
|
public OverlapResult(Vector3 normal, Collider collider)
|
|
{
|
|
Normal = normal;
|
|
Collider = collider;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains all the information for the motor's grounding status
|
|
/// </summary>
|
|
public struct CharacterGroundingReport
|
|
{
|
|
public bool FoundAnyGround;
|
|
public bool IsStableOnGround;
|
|
public bool SnappingPrevented;
|
|
public Vector3 GroundNormal;
|
|
public Vector3 InnerGroundNormal;
|
|
public Vector3 OuterGroundNormal;
|
|
|
|
public Collider GroundCollider;
|
|
public Vector3 GroundPoint;
|
|
|
|
public void CopyFrom(CharacterTransientGroundingReport transientGroundingReport)
|
|
{
|
|
FoundAnyGround = transientGroundingReport.FoundAnyGround;
|
|
IsStableOnGround = transientGroundingReport.IsStableOnGround;
|
|
SnappingPrevented = transientGroundingReport.SnappingPrevented;
|
|
GroundNormal = transientGroundingReport.GroundNormal;
|
|
InnerGroundNormal = transientGroundingReport.InnerGroundNormal;
|
|
OuterGroundNormal = transientGroundingReport.OuterGroundNormal;
|
|
|
|
GroundCollider = null;
|
|
GroundPoint = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains the simulation-relevant information for the motor's grounding status
|
|
/// </summary>
|
|
public struct CharacterTransientGroundingReport
|
|
{
|
|
public bool FoundAnyGround;
|
|
public bool IsStableOnGround;
|
|
public bool SnappingPrevented;
|
|
public Vector3 GroundNormal;
|
|
public Vector3 InnerGroundNormal;
|
|
public Vector3 OuterGroundNormal;
|
|
|
|
public void CopyFrom(CharacterGroundingReport groundingReport)
|
|
{
|
|
FoundAnyGround = groundingReport.FoundAnyGround;
|
|
IsStableOnGround = groundingReport.IsStableOnGround;
|
|
SnappingPrevented = groundingReport.SnappingPrevented;
|
|
GroundNormal = groundingReport.GroundNormal;
|
|
InnerGroundNormal = groundingReport.InnerGroundNormal;
|
|
OuterGroundNormal = groundingReport.OuterGroundNormal;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains all the information from a hit stability evaluation
|
|
/// </summary>
|
|
public struct HitStabilityReport
|
|
{
|
|
public bool IsStable;
|
|
|
|
public bool FoundInnerNormal;
|
|
public Vector3 InnerNormal;
|
|
public bool FoundOuterNormal;
|
|
public Vector3 OuterNormal;
|
|
|
|
public bool ValidStepDetected;
|
|
public Collider SteppedCollider;
|
|
|
|
public bool LedgeDetected;
|
|
public bool IsOnEmptySideOfLedge;
|
|
public float DistanceFromLedge;
|
|
public bool IsMovingTowardsEmptySideOfLedge;
|
|
public Vector3 LedgeGroundNormal;
|
|
public Vector3 LedgeRightDirection;
|
|
public Vector3 LedgeFacingDirection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Contains the information of hit rigidbodies during the movement phase, so they can be processed afterwards
|
|
/// </summary>
|
|
public struct RigidbodyProjectionHit
|
|
{
|
|
public Rigidbody Rigidbody;
|
|
public Vector3 HitPoint;
|
|
public Vector3 EffectiveHitNormal;
|
|
public Vector3 HitVelocity;
|
|
public bool StableOnHit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Component that manages character collisions and movement solving
|
|
/// </summary>
|
|
[RequireComponent(typeof(CapsuleCollider))]
|
|
public class KinematicCharacterMotor : MonoBehaviour
|
|
{
|
|
#pragma warning disable 0414
|
|
[Header("Components")]
|
|
/// <summary>
|
|
/// The capsule collider of this motor
|
|
/// </summary>
|
|
[ReadOnly]
|
|
public CapsuleCollider Capsule;
|
|
|
|
[Header("Capsule Settings")]
|
|
/// <summary>
|
|
/// Radius of the character's capsule
|
|
/// </summary>
|
|
[SerializeField]
|
|
[Tooltip("Radius of the Character Capsule")]
|
|
private float CapsuleRadius = 0.5f;
|
|
/// <summary>
|
|
/// Height of the character's capsule
|
|
/// </summary>
|
|
[SerializeField]
|
|
[Tooltip("Height of the Character Capsule")]
|
|
private float CapsuleHeight = 2f;
|
|
/// <summary>
|
|
/// Local y position of the character's capsule center
|
|
/// </summary>
|
|
[SerializeField]
|
|
[Tooltip("Height of the Character Capsule")]
|
|
private float CapsuleYOffset = 1f;
|
|
/// <summary>
|
|
/// Physics material of the character's capsule
|
|
/// </summary>
|
|
[SerializeField]
|
|
[Tooltip("Physics material of the Character Capsule (Does not affect character movement. Only affects things colliding with it)")]
|
|
#pragma warning disable 0649
|
|
private PhysicMaterial CapsulePhysicsMaterial;
|
|
#pragma warning restore 0649
|
|
|
|
|
|
[Header("Grounding settings")]
|
|
/// <summary>
|
|
/// Increases the range of ground detection, to allow snapping to ground at very high speeds
|
|
/// </summary>
|
|
[Tooltip("Increases the range of ground detection, to allow snapping to ground at very high speeds")]
|
|
public float GroundDetectionExtraDistance = 0f;
|
|
/// <summary>
|
|
/// Maximum slope angle on which the character can be stable
|
|
/// </summary>
|
|
[Range(0f, 89f)]
|
|
[Tooltip("Maximum slope angle on which the character can be stable")]
|
|
public float MaxStableSlopeAngle = 60f;
|
|
/// <summary>
|
|
/// Which layers can the character be considered stable on
|
|
/// </summary>
|
|
[Tooltip("Which layers can the character be considered stable on")]
|
|
public LayerMask StableGroundLayers = -1;
|
|
/// <summary>
|
|
/// Notifies the Character Controller when discrete collisions are detected
|
|
/// </summary>
|
|
[Tooltip("Notifies the Character Controller when discrete collisions are detected")]
|
|
public bool DiscreteCollisionEvents = false;
|
|
|
|
|
|
[Header("Step settings")]
|
|
/// <summary>
|
|
/// Handles properly detecting grounding status on steps, but has a performance cost.
|
|
/// </summary>
|
|
[Tooltip("Handles properly detecting grounding status on steps, but has a performance cost.")]
|
|
public StepHandlingMethod StepHandling = StepHandlingMethod.Standard;
|
|
/// <summary>
|
|
/// Maximum height of a step which the character can climb
|
|
/// </summary>
|
|
[Tooltip("Maximum height of a step which the character can climb")]
|
|
public float MaxStepHeight = 0.5f;
|
|
/// <summary>
|
|
/// Can the character step up obstacles even if it is not currently stable?
|
|
/// </summary>
|
|
[Tooltip("Can the character step up obstacles even if it is not currently stable?")]
|
|
public bool AllowSteppingWithoutStableGrounding = false;
|
|
/// <summary>
|
|
/// Minimum length of a step that the character can step on (used in Extra stepping method. Use this to let the character step on steps that are smaller that its radius
|
|
/// </summary>
|
|
[Tooltip("Minimum length of a step that the character can step on (used in Extra stepping method). Use this to let the character step on steps that are smaller that its radius")]
|
|
public float MinRequiredStepDepth = 0.1f;
|
|
|
|
|
|
[Header("Ledge settings")]
|
|
/// <summary>
|
|
/// Handles properly detecting ledge information and grounding status, but has a performance cost.
|
|
/// </summary>
|
|
[Tooltip("Handles properly detecting ledge information and grounding status, but has a performance cost.")]
|
|
public bool LedgeAndDenivelationHandling = true;
|
|
/// <summary>
|
|
/// The distance from the capsule central axis at which the character can stand on a ledge and still be stable
|
|
/// </summary>
|
|
[Tooltip("The distance from the capsule central axis at which the character can stand on a ledge and still be stable")]
|
|
public float MaxStableDistanceFromLedge = 0.5f;
|
|
/// <summary>
|
|
/// Prevents snapping to ground on ledges beyond a certain velocity
|
|
/// </summary>
|
|
[Tooltip("Prevents snapping to ground on ledges beyond a certain velocity")]
|
|
public float MaxVelocityForLedgeSnap = 0f;
|
|
/// <summary>
|
|
/// The maximun downward slope angle change that the character can be subjected to and still be snapping to the ground
|
|
/// </summary>
|
|
[Tooltip("The maximun downward slope angle change that the character can be subjected to and still be snapping to the ground")]
|
|
[Range(1f, 180f)]
|
|
public float MaxStableDenivelationAngle = 180f;
|
|
|
|
|
|
[Header("Rigidbody interaction settings")]
|
|
/// <summary>
|
|
/// Handles properly being pushed by and standing on PhysicsMovers or dynamic rigidbodies. Also handles pushing dynamic rigidbodies
|
|
/// </summary>
|
|
[Tooltip("Handles properly being pushed by and standing on PhysicsMovers or dynamic rigidbodies. Also handles pushing dynamic rigidbodies")]
|
|
public bool InteractiveRigidbodyHandling = true;
|
|
/// <summary>
|
|
/// How the character interacts with non-kinematic rigidbodies. \"Kinematic\" mode means the character pushes the rigidbodies with infinite force (as a kinematic body would). \"SimulatedDynamic\" pushes the rigidbodies with a simulated mass value.
|
|
/// </summary>
|
|
[Tooltip("How the character interacts with non-kinematic rigidbodies. \"Kinematic\" mode means the character pushes the rigidbodies with infinite force (as a kinematic body would). \"SimulatedDynamic\" pushes the rigidbodies with a simulated mass value.")]
|
|
public RigidbodyInteractionType RigidbodyInteractionType;
|
|
[Tooltip("Mass used for pushing bodies")]
|
|
public float SimulatedCharacterMass = 1f;
|
|
/// <summary>
|
|
/// Determines if the character preserves moving platform velocities when de-grounding from them
|
|
/// </summary>
|
|
[Tooltip("Determines if the character preserves moving platform velocities when de-grounding from them")]
|
|
public bool PreserveAttachedRigidbodyMomentum = true;
|
|
|
|
|
|
[Header("Constraints settings")]
|
|
/// <summary>
|
|
/// Determines if the character's movement uses the planar constraint
|
|
/// </summary>
|
|
[Tooltip("Determines if the character's movement uses the planar constraint")]
|
|
public bool HasPlanarConstraint = false;
|
|
/// <summary>
|
|
/// Defines the plane that the character's movement is constrained on, if HasMovementConstraintPlane is active
|
|
/// </summary>
|
|
[Tooltip("Defines the plane that the character's movement is constrained on, if HasMovementConstraintPlane is active")]
|
|
public Vector3 PlanarConstraintAxis = Vector3.forward;
|
|
|
|
[Header("Other settings")]
|
|
/// <summary>
|
|
/// How many times can we sweep for movement per update
|
|
/// </summary>
|
|
[Tooltip("How many times can we sweep for movement per update")]
|
|
public int MaxMovementIterations = 5;
|
|
/// <summary>
|
|
/// How many times can we check for decollision per update
|
|
/// </summary>
|
|
[Tooltip("How many times can we check for decollision per update")]
|
|
public int MaxDecollisionIterations = 1;
|
|
/// <summary>
|
|
/// Checks for overlaps before casting movement, making sure all collisions are detected even when already intersecting geometry (has a performance cost, but provides safety against tunneling through colliders)
|
|
/// </summary>
|
|
[Tooltip("Checks for overlaps before casting movement, making sure all collisions are detected even when already intersecting geometry (has a performance cost, but provides safety against tunneling through colliders)")]
|
|
public bool CheckMovementInitialOverlaps = true;
|
|
/// <summary>
|
|
/// Sets the velocity to zero if exceed max movement iterations
|
|
/// </summary>
|
|
[Tooltip("Sets the velocity to zero if exceed max movement iterations")]
|
|
public bool KillVelocityWhenExceedMaxMovementIterations = true;
|
|
/// <summary>
|
|
/// Sets the remaining movement to zero if exceed max movement iterations
|
|
/// </summary>
|
|
[Tooltip("Sets the remaining movement to zero if exceed max movement iterations")]
|
|
public bool KillRemainingMovementWhenExceedMaxMovementIterations = true;
|
|
|
|
/// <summary>
|
|
/// Contains the current grounding information
|
|
/// </summary>
|
|
[System.NonSerialized]
|
|
public CharacterGroundingReport GroundingStatus = new CharacterGroundingReport();
|
|
/// <summary>
|
|
/// Contains the previous grounding information
|
|
/// </summary>
|
|
[System.NonSerialized]
|
|
public CharacterTransientGroundingReport LastGroundingStatus = new CharacterTransientGroundingReport();
|
|
/// <summary>
|
|
/// Specifies the LayerMask that the character's movement algorithm can detect collisions with. By default, this uses the rigidbody's layer's collision matrix
|
|
/// </summary>
|
|
[System.NonSerialized]
|
|
public LayerMask CollidableLayers = -1;
|
|
|
|
/// <summary>
|
|
/// The Transform of the character motor
|
|
/// </summary>
|
|
public Transform Transform { get { return _transform; } }
|
|
private Transform _transform;
|
|
/// <summary>
|
|
/// The character's goal position in its movement calculations (always up-to-date during the character update phase)
|
|
/// </summary>
|
|
public Vector3 TransientPosition { get { return _transientPosition; } }
|
|
private Vector3 _transientPosition;
|
|
/// <summary>
|
|
/// The character's up direction (always up-to-date during the character update phase)
|
|
/// </summary>
|
|
public Vector3 CharacterUp { get { return _characterUp; } }
|
|
private Vector3 _characterUp;
|
|
/// <summary>
|
|
/// The character's forward direction (always up-to-date during the character update phase)
|
|
/// </summary>
|
|
public Vector3 CharacterForward { get { return _characterForward; } }
|
|
private Vector3 _characterForward;
|
|
/// <summary>
|
|
/// The character's right direction (always up-to-date during the character update phase)
|
|
/// </summary>
|
|
public Vector3 CharacterRight { get { return _characterRight; } }
|
|
private Vector3 _characterRight;
|
|
/// <summary>
|
|
/// The character's position before the movement calculations began
|
|
/// </summary>
|
|
public Vector3 InitialSimulationPosition { get { return _initialSimulationPosition; } }
|
|
private Vector3 _initialSimulationPosition;
|
|
/// <summary>
|
|
/// The character's rotation before the movement calculations began
|
|
/// </summary>
|
|
public Quaternion InitialSimulationRotation { get { return _initialSimulationRotation; } }
|
|
private Quaternion _initialSimulationRotation;
|
|
/// <summary>
|
|
/// Represents the Rigidbody to stay attached to
|
|
/// </summary>
|
|
public Rigidbody AttachedRigidbody { get { return _attachedRigidbody; } }
|
|
private Rigidbody _attachedRigidbody;
|
|
/// <summary>
|
|
/// Vector3 from the character transform position to the capsule center
|
|
/// </summary>
|
|
public Vector3 CharacterTransformToCapsuleCenter { get { return _characterTransformToCapsuleCenter; } }
|
|
private Vector3 _characterTransformToCapsuleCenter;
|
|
/// <summary>
|
|
/// Vector3 from the character transform position to the capsule bottom
|
|
/// </summary>
|
|
public Vector3 CharacterTransformToCapsuleBottom { get { return _characterTransformToCapsuleBottom; } }
|
|
private Vector3 _characterTransformToCapsuleBottom;
|
|
/// <summary>
|
|
/// Vector3 from the character transform position to the capsule top
|
|
/// </summary>
|
|
public Vector3 CharacterTransformToCapsuleTop { get { return _characterTransformToCapsuleTop; } }
|
|
private Vector3 _characterTransformToCapsuleTop;
|
|
/// <summary>
|
|
/// Vector3 from the character transform position to the capsule bottom hemi center
|
|
/// </summary>
|
|
public Vector3 CharacterTransformToCapsuleBottomHemi { get { return _characterTransformToCapsuleBottomHemi; } }
|
|
private Vector3 _characterTransformToCapsuleBottomHemi;
|
|
/// <summary>
|
|
/// Vector3 from the character transform position to the capsule top hemi center
|
|
/// </summary>
|
|
public Vector3 CharacterTransformToCapsuleTopHemi { get { return _characterTransformToCapsuleTopHemi; } }
|
|
private Vector3 _characterTransformToCapsuleTopHemi;
|
|
/// <summary>
|
|
/// The character's velocity resulting from standing on rigidbodies or PhysicsMover
|
|
/// </summary>
|
|
public Vector3 AttachedRigidbodyVelocity { get { return _attachedRigidbodyVelocity; } }
|
|
private Vector3 _attachedRigidbodyVelocity;
|
|
/// <summary>
|
|
/// The number of overlaps detected so far during character update (is reset at the beginning of the update)
|
|
/// </summary>
|
|
public int OverlapsCount { get { return _overlapsCount; } }
|
|
private int _overlapsCount;
|
|
/// <summary>
|
|
/// The overlaps detected so far during character update
|
|
/// </summary>
|
|
public OverlapResult[] Overlaps { get { return _overlaps; } }
|
|
private OverlapResult[] _overlaps = new OverlapResult[MaxRigidbodyOverlapsCount];
|
|
|
|
/// <summary>
|
|
/// The motor's assigned controller
|
|
/// </summary>
|
|
[NonSerialized]
|
|
public ICharacterController CharacterController;
|
|
/// <summary>
|
|
/// Did the motor's last swept collision detection find a ground?
|
|
/// </summary>
|
|
[NonSerialized]
|
|
public bool LastMovementIterationFoundAnyGround;
|
|
/// <summary>
|
|
/// Index of this motor in KinematicCharacterSystem arrays
|
|
/// </summary>
|
|
[NonSerialized]
|
|
public int IndexInCharacterSystem;
|
|
/// <summary>
|
|
/// Remembers initial position before all simulation are done
|
|
/// </summary>
|
|
[NonSerialized]
|
|
public Vector3 InitialTickPosition;
|
|
/// <summary>
|
|
/// Remembers initial rotation before all simulation are done
|
|
/// </summary>
|
|
[NonSerialized]
|
|
public Quaternion InitialTickRotation;
|
|
/// <summary>
|
|
/// Specifies a Rigidbody to stay attached to
|
|
/// </summary>
|
|
[NonSerialized]
|
|
public Rigidbody AttachedRigidbodyOverride;
|
|
/// <summary>
|
|
/// The character's velocity resulting from direct movement
|
|
/// </summary>
|
|
[NonSerialized]
|
|
public Vector3 BaseVelocity;
|
|
|
|
// Private
|
|
private RaycastHit[] _internalCharacterHits = new RaycastHit[MaxHitsBudget];
|
|
private Collider[] _internalProbedColliders = new Collider[MaxCollisionBudget];
|
|
private List<Rigidbody> _rigidbodiesPushedThisMove = new List<Rigidbody>(16);
|
|
private RigidbodyProjectionHit[] _internalRigidbodyProjectionHits = new RigidbodyProjectionHit[MaxRigidbodyOverlapsCount];
|
|
private Rigidbody _lastAttachedRigidbody;
|
|
private bool _solveMovementCollisions = true;
|
|
private bool _solveGrounding = true;
|
|
private bool _movePositionDirty = false;
|
|
private Vector3 _movePositionTarget = Vector3.zero;
|
|
private bool _moveRotationDirty = false;
|
|
private Quaternion _moveRotationTarget = Quaternion.identity;
|
|
private bool _lastSolvedOverlapNormalDirty = false;
|
|
private Vector3 _lastSolvedOverlapNormal = Vector3.forward;
|
|
private int _rigidbodyProjectionHitCount = 0;
|
|
private bool _isMovingFromAttachedRigidbody = false;
|
|
private bool _mustUnground = false;
|
|
private float _mustUngroundTimeCounter = 0f;
|
|
private Vector3 _cachedWorldUp = Vector3.up;
|
|
private Vector3 _cachedWorldForward = Vector3.forward;
|
|
private Vector3 _cachedWorldRight = Vector3.right;
|
|
private Vector3 _cachedZeroVector = Vector3.zero;
|
|
|
|
private Quaternion _transientRotation;
|
|
/// <summary>
|
|
/// The character's goal rotation in its movement calculations (always up-to-date during the character update phase)
|
|
/// </summary>
|
|
public Quaternion TransientRotation
|
|
{
|
|
get
|
|
{
|
|
return _transientRotation;
|
|
}
|
|
private set
|
|
{
|
|
_transientRotation = value;
|
|
_characterUp = _transientRotation * _cachedWorldUp;
|
|
_characterForward = _transientRotation * _cachedWorldForward;
|
|
_characterRight = _transientRotation * _cachedWorldRight;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The character's total velocity, including velocity from standing on rigidbodies or PhysicsMover
|
|
/// </summary>
|
|
public Vector3 Velocity
|
|
{
|
|
get
|
|
{
|
|
return BaseVelocity + _attachedRigidbodyVelocity;
|
|
}
|
|
}
|
|
|
|
// Warning: Don't touch these constants unless you know exactly what you're doing!
|
|
public const int MaxHitsBudget = 16;
|
|
public const int MaxCollisionBudget = 16;
|
|
public const int MaxGroundingSweepIterations = 2;
|
|
public const int MaxSteppingSweepIterations = 3;
|
|
public const int MaxRigidbodyOverlapsCount = 16;
|
|
public const float CollisionOffset = 0.01f;
|
|
public const float GroundProbeReboundDistance = 0.02f;
|
|
public const float MinimumGroundProbingDistance = 0.005f;
|
|
public const float GroundProbingBackstepDistance = 0.1f;
|
|
public const float SweepProbingBackstepDistance = 0.002f;
|
|
public const float SecondaryProbesVertical = 0.02f;
|
|
public const float SecondaryProbesHorizontal = 0.001f;
|
|
public const float MinVelocityMagnitude = 0.01f;
|
|
public const float SteppingForwardDistance = 0.03f;
|
|
public const float MinDistanceForLedge = 0.05f;
|
|
public const float CorrelationForVerticalObstruction = 0.01f;
|
|
public const float ExtraSteppingForwardDistance = 0.01f;
|
|
public const float ExtraStepHeightPadding = 0.01f;
|
|
#pragma warning restore 0414
|
|
|
|
private void OnEnable()
|
|
{
|
|
KinematicCharacterSystem.EnsureCreation();
|
|
KinematicCharacterSystem.RegisterCharacterMotor(this);
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
KinematicCharacterSystem.UnregisterCharacterMotor(this);
|
|
}
|
|
|
|
private void Reset()
|
|
{
|
|
ValidateData();
|
|
}
|
|
|
|
private void OnValidate()
|
|
{
|
|
ValidateData();
|
|
}
|
|
|
|
[ContextMenu("Remove Component")]
|
|
private void HandleRemoveComponent()
|
|
{
|
|
CapsuleCollider tmpCapsule = gameObject.GetComponent<CapsuleCollider>();
|
|
DestroyImmediate(this);
|
|
DestroyImmediate(tmpCapsule);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle validating all required values
|
|
/// </summary>
|
|
public void ValidateData()
|
|
{
|
|
Capsule = GetComponent<CapsuleCollider>();
|
|
CapsuleRadius = Mathf.Clamp(CapsuleRadius, 0f, CapsuleHeight * 0.5f);
|
|
Capsule.direction = 1;
|
|
Capsule.sharedMaterial = CapsulePhysicsMaterial;
|
|
SetCapsuleDimensions(CapsuleRadius, CapsuleHeight, CapsuleYOffset);
|
|
|
|
MaxStepHeight = Mathf.Clamp(MaxStepHeight, 0f, Mathf.Infinity);
|
|
MinRequiredStepDepth = Mathf.Clamp(MinRequiredStepDepth, 0f, CapsuleRadius);
|
|
MaxStableDistanceFromLedge = Mathf.Clamp(MaxStableDistanceFromLedge, 0f, CapsuleRadius);
|
|
|
|
transform.localScale = Vector3.one;
|
|
|
|
#if UNITY_EDITOR
|
|
Capsule.hideFlags = HideFlags.NotEditable;
|
|
if (!Mathf.Approximately(transform.lossyScale.x, 1f) || !Mathf.Approximately(transform.lossyScale.y, 1f) || !Mathf.Approximately(transform.lossyScale.z, 1f))
|
|
{
|
|
Debug.LogError("Character's lossy scale is not (1,1,1). This is not allowed. Make sure the character's transform and all of its parents have a (1,1,1) scale.", this.gameObject);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets whether or not the capsule collider will detect collisions
|
|
/// </summary>
|
|
public void SetCapsuleCollisionsActivation(bool collisionsActive)
|
|
{
|
|
Capsule.isTrigger = !collisionsActive;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets whether or not the motor will solve collisions when moving (or moved onto)
|
|
/// </summary>
|
|
public void SetMovementCollisionsSolvingActivation(bool movementCollisionsSolvingActive)
|
|
{
|
|
_solveMovementCollisions = movementCollisionsSolvingActive;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets whether or not grounding will be evaluated for all hits
|
|
/// </summary>
|
|
public void SetGroundSolvingActivation(bool stabilitySolvingActive)
|
|
{
|
|
_solveGrounding = stabilitySolvingActive;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the character's position directly
|
|
/// </summary>
|
|
public void SetPosition(Vector3 position, bool bypassInterpolation = true)
|
|
{
|
|
_transform.position = position;
|
|
_initialSimulationPosition = position;
|
|
_transientPosition = position;
|
|
|
|
if (bypassInterpolation)
|
|
{
|
|
InitialTickPosition = position;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the character's rotation directly
|
|
/// </summary>
|
|
public void SetRotation(Quaternion rotation, bool bypassInterpolation = true)
|
|
{
|
|
_transform.rotation = rotation;
|
|
_initialSimulationRotation = rotation;
|
|
TransientRotation = rotation;
|
|
|
|
if (bypassInterpolation)
|
|
{
|
|
InitialTickRotation = rotation;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the character's position and rotation directly
|
|
/// </summary>
|
|
public void SetPositionAndRotation(Vector3 position, Quaternion rotation, bool bypassInterpolation = true)
|
|
{
|
|
_transform.SetPositionAndRotation(position, rotation);
|
|
_initialSimulationPosition = position;
|
|
_initialSimulationRotation = rotation;
|
|
_transientPosition = position;
|
|
TransientRotation = rotation;
|
|
|
|
if (bypassInterpolation)
|
|
{
|
|
InitialTickPosition = position;
|
|
InitialTickRotation = rotation;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the character position, taking all movement collision solving int account. The actual move is done the next time the motor updates are called
|
|
/// </summary>
|
|
public void MoveCharacter(Vector3 toPosition)
|
|
{
|
|
_movePositionDirty = true;
|
|
_movePositionTarget = toPosition;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the character rotation. The actual move is done the next time the motor updates are called
|
|
/// </summary>
|
|
public void RotateCharacter(Quaternion toRotation)
|
|
{
|
|
_moveRotationDirty = true;
|
|
_moveRotationTarget = toRotation;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all the state information of the motor that is pertinent for simulation
|
|
/// </summary>
|
|
public KinematicCharacterMotorState GetState()
|
|
{
|
|
KinematicCharacterMotorState state = new KinematicCharacterMotorState();
|
|
|
|
state.Position = _transientPosition;
|
|
state.Rotation = _transientRotation;
|
|
|
|
state.BaseVelocity = BaseVelocity;
|
|
state.AttachedRigidbodyVelocity = _attachedRigidbodyVelocity;
|
|
|
|
state.MustUnground = _mustUnground;
|
|
state.MustUngroundTime = _mustUngroundTimeCounter;
|
|
state.LastMovementIterationFoundAnyGround = LastMovementIterationFoundAnyGround;
|
|
state.GroundingStatus.CopyFrom(GroundingStatus);
|
|
state.AttachedRigidbody = _attachedRigidbody;
|
|
|
|
return state;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies a motor state instantly
|
|
/// </summary>
|
|
public void ApplyState(KinematicCharacterMotorState state, bool bypassInterpolation = true)
|
|
{
|
|
SetPositionAndRotation(state.Position, state.Rotation, bypassInterpolation);
|
|
|
|
BaseVelocity = state.BaseVelocity;
|
|
_attachedRigidbodyVelocity = state.AttachedRigidbodyVelocity;
|
|
|
|
_mustUnground = state.MustUnground;
|
|
_mustUngroundTimeCounter = state.MustUngroundTime;
|
|
LastMovementIterationFoundAnyGround = state.LastMovementIterationFoundAnyGround;
|
|
GroundingStatus.CopyFrom(state.GroundingStatus);
|
|
_attachedRigidbody = state.AttachedRigidbody;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resizes capsule. ALso caches importand capsule size data
|
|
/// </summary>
|
|
public void SetCapsuleDimensions(float radius, float height, float yOffset)
|
|
{
|
|
height = Mathf.Max(height, (radius * 2f) + 0.01f); // Safety to prevent invalid capsule geometries
|
|
|
|
CapsuleRadius = radius;
|
|
CapsuleHeight = height;
|
|
CapsuleYOffset = yOffset;
|
|
|
|
Capsule.radius = CapsuleRadius;
|
|
Capsule.height = Mathf.Clamp(CapsuleHeight, CapsuleRadius * 2f, CapsuleHeight);
|
|
Capsule.center = new Vector3(0f, CapsuleYOffset, 0f);
|
|
|
|
_characterTransformToCapsuleCenter = Capsule.center;
|
|
_characterTransformToCapsuleBottom = Capsule.center + (-_cachedWorldUp * (Capsule.height * 0.5f));
|
|
_characterTransformToCapsuleTop = Capsule.center + (_cachedWorldUp * (Capsule.height * 0.5f));
|
|
_characterTransformToCapsuleBottomHemi = Capsule.center + (-_cachedWorldUp * (Capsule.height * 0.5f)) + (_cachedWorldUp * Capsule.radius);
|
|
_characterTransformToCapsuleTopHemi = Capsule.center + (_cachedWorldUp * (Capsule.height * 0.5f)) + (-_cachedWorldUp * Capsule.radius);
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
_transform = this.transform;
|
|
ValidateData();
|
|
|
|
_transientPosition = _transform.position;
|
|
TransientRotation = _transform.rotation;
|
|
|
|
// Build CollidableLayers mask
|
|
CollidableLayers = 0;
|
|
for (int i = 0; i < 32; i++)
|
|
{
|
|
if (!Physics.GetIgnoreLayerCollision(this.gameObject.layer, i))
|
|
{
|
|
CollidableLayers |= (1 << i);
|
|
}
|
|
}
|
|
|
|
SetCapsuleDimensions(CapsuleRadius, CapsuleHeight, CapsuleYOffset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update phase 1 is meant to be called after physics movers have calculated their velocities, but
|
|
/// before they have simulated their goal positions/rotations. It is responsible for:
|
|
/// - Initializing all values for update
|
|
/// - Handling MovePosition calls
|
|
/// - Solving initial collision overlaps
|
|
/// - Ground probing
|
|
/// - Handle detecting potential interactable rigidbodies
|
|
/// </summary>
|
|
public void UpdatePhase1(float deltaTime)
|
|
{
|
|
// NaN propagation safety stop
|
|
if (float.IsNaN(BaseVelocity.x) || float.IsNaN(BaseVelocity.y) || float.IsNaN(BaseVelocity.z))
|
|
{
|
|
BaseVelocity = Vector3.zero;
|
|
}
|
|
if (float.IsNaN(_attachedRigidbodyVelocity.x) || float.IsNaN(_attachedRigidbodyVelocity.y) || float.IsNaN(_attachedRigidbodyVelocity.z))
|
|
{
|
|
_attachedRigidbodyVelocity = Vector3.zero;
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
if (!Mathf.Approximately(_transform.lossyScale.x, 1f) || !Mathf.Approximately(_transform.lossyScale.y, 1f) || !Mathf.Approximately(_transform.lossyScale.z, 1f))
|
|
{
|
|
Debug.LogError("Character's lossy scale is not (1,1,1). This is not allowed. Make sure the character's transform and all of its parents have a (1,1,1) scale.", this.gameObject);
|
|
}
|
|
#endif
|
|
|
|
_rigidbodiesPushedThisMove.Clear();
|
|
|
|
// Before update
|
|
CharacterController.BeforeCharacterUpdate(deltaTime);
|
|
|
|
_transientPosition = _transform.position;
|
|
TransientRotation = _transform.rotation;
|
|
_initialSimulationPosition = _transientPosition;
|
|
_initialSimulationRotation = _transientRotation;
|
|
_rigidbodyProjectionHitCount = 0;
|
|
_overlapsCount = 0;
|
|
_lastSolvedOverlapNormalDirty = false;
|
|
|
|
#region Handle Move Position
|
|
if (_movePositionDirty)
|
|
{
|
|
if (_solveMovementCollisions)
|
|
{
|
|
Vector3 tmpVelocity = GetVelocityFromMovement(_movePositionTarget - _transientPosition, deltaTime);
|
|
if (InternalCharacterMove(ref tmpVelocity, deltaTime))
|
|
{
|
|
if (InteractiveRigidbodyHandling)
|
|
{
|
|
ProcessVelocityForRigidbodyHits(ref tmpVelocity, deltaTime);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_transientPosition = _movePositionTarget;
|
|
}
|
|
|
|
_movePositionDirty = false;
|
|
}
|
|
#endregion
|
|
|
|
LastGroundingStatus.CopyFrom(GroundingStatus);
|
|
GroundingStatus = new CharacterGroundingReport();
|
|
GroundingStatus.GroundNormal = _characterUp;
|
|
|
|
if (_solveMovementCollisions)
|
|
{
|
|
#region Resolve initial overlaps
|
|
Vector3 resolutionDirection = _cachedWorldUp;
|
|
float resolutionDistance = 0f;
|
|
int iterationsMade = 0;
|
|
bool overlapSolved = false;
|
|
while (iterationsMade < MaxDecollisionIterations && !overlapSolved)
|
|
{
|
|
int nbOverlaps = CharacterCollisionsOverlap(_transientPosition, _transientRotation, _internalProbedColliders);
|
|
|
|
if (nbOverlaps > 0)
|
|
{
|
|
// Solve overlaps that aren't against dynamic rigidbodies or physics movers
|
|
for (int i = 0; i < nbOverlaps; i++)
|
|
{
|
|
if (GetInteractiveRigidbody(_internalProbedColliders[i]) == null)
|
|
{
|
|
// Process overlap
|
|
Transform overlappedTransform = _internalProbedColliders[i].GetComponent<Transform>();
|
|
if (Physics.ComputePenetration(
|
|
Capsule,
|
|
_transientPosition,
|
|
_transientRotation,
|
|
_internalProbedColliders[i],
|
|
overlappedTransform.position,
|
|
overlappedTransform.rotation,
|
|
out resolutionDirection,
|
|
out resolutionDistance))
|
|
{
|
|
// Resolve along obstruction direction
|
|
HitStabilityReport mockReport = new HitStabilityReport();
|
|
mockReport.IsStable = IsStableOnNormal(resolutionDirection);
|
|
resolutionDirection = GetObstructionNormal(resolutionDirection, mockReport.IsStable);
|
|
|
|
// Solve overlap
|
|
Vector3 resolutionMovement = resolutionDirection * (resolutionDistance + CollisionOffset);
|
|
_transientPosition += resolutionMovement;
|
|
|
|
// Remember overlaps
|
|
if (_overlapsCount < _overlaps.Length)
|
|
{
|
|
_overlaps[_overlapsCount] = new OverlapResult(resolutionDirection, _internalProbedColliders[i]);
|
|
_overlapsCount++;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
overlapSolved = true;
|
|
}
|
|
|
|
iterationsMade++;
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
#region Ground Probing and Snapping
|
|
// Handle ungrounding
|
|
if (_solveGrounding)
|
|
{
|
|
if (MustUnground())
|
|
{
|
|
_transientPosition += _characterUp * (MinimumGroundProbingDistance * 1.5f);
|
|
}
|
|
else
|
|
{
|
|
// Choose the appropriate ground probing distance
|
|
float selectedGroundProbingDistance = MinimumGroundProbingDistance;
|
|
if (!LastGroundingStatus.SnappingPrevented && (LastGroundingStatus.IsStableOnGround || LastMovementIterationFoundAnyGround))
|
|
{
|
|
if (StepHandling != StepHandlingMethod.None)
|
|
{
|
|
selectedGroundProbingDistance = Mathf.Max(CapsuleRadius, MaxStepHeight);
|
|
}
|
|
else
|
|
{
|
|
selectedGroundProbingDistance = CapsuleRadius;
|
|
}
|
|
|
|
selectedGroundProbingDistance += GroundDetectionExtraDistance;
|
|
}
|
|
|
|
ProbeGround(ref _transientPosition, _transientRotation, selectedGroundProbingDistance, ref GroundingStatus);
|
|
|
|
if (!LastGroundingStatus.IsStableOnGround && GroundingStatus.IsStableOnGround)
|
|
{
|
|
// Handle stable landing
|
|
BaseVelocity = Vector3.ProjectOnPlane(BaseVelocity, CharacterUp);
|
|
BaseVelocity = GetDirectionTangentToSurface(BaseVelocity, GroundingStatus.GroundNormal) * BaseVelocity.magnitude;
|
|
}
|
|
}
|
|
}
|
|
|
|
LastMovementIterationFoundAnyGround = false;
|
|
|
|
if (_mustUngroundTimeCounter > 0f)
|
|
{
|
|
_mustUngroundTimeCounter -= deltaTime;
|
|
}
|
|
_mustUnground = false;
|
|
#endregion
|
|
|
|
if (_solveGrounding)
|
|
{
|
|
CharacterController.PostGroundingUpdate(deltaTime);
|
|
}
|
|
|
|
if (InteractiveRigidbodyHandling)
|
|
{
|
|
#region Interactive Rigidbody Handling
|
|
_lastAttachedRigidbody = _attachedRigidbody;
|
|
if (AttachedRigidbodyOverride)
|
|
{
|
|
_attachedRigidbody = AttachedRigidbodyOverride;
|
|
}
|
|
else
|
|
{
|
|
// Detect interactive rigidbodies from grounding
|
|
if (GroundingStatus.IsStableOnGround && GroundingStatus.GroundCollider.attachedRigidbody)
|
|
{
|
|
Rigidbody interactiveRigidbody = GetInteractiveRigidbody(GroundingStatus.GroundCollider);
|
|
if (interactiveRigidbody)
|
|
{
|
|
_attachedRigidbody = interactiveRigidbody;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
_attachedRigidbody = null;
|
|
}
|
|
}
|
|
|
|
Vector3 tmpVelocityFromCurrentAttachedRigidbody = Vector3.zero;
|
|
Vector3 tmpAngularVelocityFromCurrentAttachedRigidbody = Vector3.zero;
|
|
if (_attachedRigidbody)
|
|
{
|
|
GetVelocityFromRigidbodyMovement(_attachedRigidbody, _transientPosition, deltaTime, out tmpVelocityFromCurrentAttachedRigidbody, out tmpAngularVelocityFromCurrentAttachedRigidbody);
|
|
}
|
|
|
|
// Conserve momentum when de-stabilized from an attached rigidbody
|
|
if (PreserveAttachedRigidbodyMomentum && _lastAttachedRigidbody != null && _attachedRigidbody != _lastAttachedRigidbody)
|
|
{
|
|
BaseVelocity += _attachedRigidbodyVelocity;
|
|
BaseVelocity -= tmpVelocityFromCurrentAttachedRigidbody;
|
|
}
|
|
|
|
// Process additionnal Velocity from attached rigidbody
|
|
_attachedRigidbodyVelocity = _cachedZeroVector;
|
|
if (_attachedRigidbody)
|
|
{
|
|
_attachedRigidbodyVelocity = tmpVelocityFromCurrentAttachedRigidbody;
|
|
|
|
// Rotation from attached rigidbody
|
|
Vector3 newForward = Vector3.ProjectOnPlane(Quaternion.Euler(Mathf.Rad2Deg * tmpAngularVelocityFromCurrentAttachedRigidbody * deltaTime) * _characterForward, _characterUp).normalized;
|
|
TransientRotation = Quaternion.LookRotation(newForward, _characterUp);
|
|
}
|
|
|
|
// Cancel out horizontal velocity upon landing on an attached rigidbody
|
|
if (GroundingStatus.GroundCollider &&
|
|
GroundingStatus.GroundCollider.attachedRigidbody &&
|
|
GroundingStatus.GroundCollider.attachedRigidbody == _attachedRigidbody &&
|
|
_attachedRigidbody != null &&
|
|
_lastAttachedRigidbody == null)
|
|
{
|
|
BaseVelocity -= Vector3.ProjectOnPlane(_attachedRigidbodyVelocity, _characterUp);
|
|
}
|
|
|
|
// Movement from Attached Rigidbody
|
|
if (_attachedRigidbodyVelocity.sqrMagnitude > 0f)
|
|
{
|
|
_isMovingFromAttachedRigidbody = true;
|
|
|
|
if (_solveMovementCollisions)
|
|
{
|
|
// Perform the move from rgdbdy velocity
|
|
InternalCharacterMove(ref _attachedRigidbodyVelocity, deltaTime);
|
|
}
|
|
else
|
|
{
|
|
_transientPosition += _attachedRigidbodyVelocity * deltaTime;
|
|
}
|
|
|
|
_isMovingFromAttachedRigidbody = false;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update phase 2 is meant to be called after physics movers have simulated their goal positions/rotations.
|
|
/// At the end of this, the TransientPosition/Rotation values will be up-to-date with where the motor should be at the end of its move.
|
|
/// It is responsible for:
|
|
/// - Solving Rotation
|
|
/// - Handle MoveRotation calls
|
|
/// - Solving potential attached rigidbody overlaps
|
|
/// - Solving Velocity
|
|
/// - Applying planar constraint
|
|
/// </summary>
|
|
public void UpdatePhase2(float deltaTime)
|
|
{
|
|
// Handle rotation
|
|
CharacterController.UpdateRotation(ref _transientRotation, deltaTime);
|
|
TransientRotation = _transientRotation;
|
|
|
|
// Handle move rotation
|
|
if (_moveRotationDirty)
|
|
{
|
|
TransientRotation = _moveRotationTarget;
|
|
_moveRotationDirty = false;
|
|
}
|
|
|
|
if (_solveMovementCollisions && InteractiveRigidbodyHandling)
|
|
{
|
|
if (InteractiveRigidbodyHandling)
|
|
{
|
|
#region Solve potential attached rigidbody overlap
|
|
if (_attachedRigidbody)
|
|
{
|
|
float upwardsOffset = Capsule.radius;
|
|
|
|
RaycastHit closestHit;
|
|
if (CharacterGroundSweep(
|
|
_transientPosition + (_characterUp * upwardsOffset),
|
|
_transientRotation,
|
|
-_characterUp,
|
|
upwardsOffset,
|
|
out closestHit))
|
|
{
|
|
if (closestHit.collider.attachedRigidbody == _attachedRigidbody && IsStableOnNormal(closestHit.normal))
|
|
{
|
|
float distanceMovedUp = (upwardsOffset - closestHit.distance);
|
|
_transientPosition = _transientPosition + (_characterUp * distanceMovedUp) + (_characterUp * CollisionOffset);
|
|
}
|
|
}
|
|
}
|
|
#endregion
|
|
}
|
|
|
|
if (InteractiveRigidbodyHandling)
|
|
{
|
|
#region Resolve overlaps that could've been caused by rotation or physics movers simulation pushing the character
|
|
Vector3 resolutionDirection = _cachedWorldUp;
|
|
float resolutionDistance = 0f;
|
|
int iterationsMade = 0;
|
|
bool overlapSolved = false;
|
|
while (iterationsMade < MaxDecollisionIterations && !overlapSolved)
|
|
{
|
|
int nbOverlaps = CharacterCollisionsOverlap(_transientPosition, _transientRotation, _internalProbedColliders);
|
|
if (nbOverlaps > 0)
|
|
{
|
|
for (int i = 0; i < nbOverlaps; i++)
|
|
{
|
|
// Process overlap
|
|
Transform overlappedTransform = _internalProbedColliders[i].GetComponent<Transform>();
|
|
if (Physics.ComputePenetration(
|
|
Capsule,
|
|
_transientPosition,
|
|
_transientRotation,
|
|
_internalProbedColliders[i],
|
|
overlappedTransform.position,
|
|
overlappedTransform.rotation,
|
|
out resolutionDirection,
|
|
out resolutionDistance))
|
|
{
|
|
// Resolve along obstruction direction
|
|
HitStabilityReport mockReport = new HitStabilityReport();
|
|
mockReport.IsStable = IsStableOnNormal(resolutionDirection);
|
|
resolutionDirection = GetObstructionNormal(resolutionDirection, mockReport.IsStable);
|
|
|
|
// Solve overlap
|
|
Vector3 resolutionMovement = resolutionDirection * (resolutionDistance + CollisionOffset);
|
|
_transientPosition += resolutionMovement;
|
|
|
|
// If interactiveRigidbody, register as rigidbody hit for velocity
|
|
if (InteractiveRigidbodyHandling)
|
|
{
|
|
Rigidbody probedRigidbody = GetInteractiveRigidbody(_internalProbedColliders[i]);
|
|
if (probedRigidbody != null)
|
|
{
|
|
HitStabilityReport tmpReport = new HitStabilityReport();
|
|
tmpReport.IsStable = IsStableOnNormal(resolutionDirection);
|
|
if (tmpReport.IsStable)
|
|
{
|
|
LastMovementIterationFoundAnyGround = tmpReport.IsStable;
|
|
}
|
|
if (probedRigidbody != _attachedRigidbody)
|
|
{
|
|
Vector3 characterCenter = _transientPosition + (_transientRotation * _characterTransformToCapsuleCenter);
|
|
Vector3 estimatedCollisionPoint = _transientPosition;
|
|
|
|
|
|
StoreRigidbodyHit(
|
|
probedRigidbody,
|
|
Velocity,
|
|
estimatedCollisionPoint,
|
|
resolutionDirection,
|
|
tmpReport);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remember overlaps
|
|
if (_overlapsCount < _overlaps.Length)
|
|
{
|
|
_overlaps[_overlapsCount] = new OverlapResult(resolutionDirection, _internalProbedColliders[i]);
|
|
_overlapsCount++;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
overlapSolved = true;
|
|
}
|
|
|
|
iterationsMade++;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|
|
|
|
// Handle velocity
|
|
CharacterController.UpdateVelocity(ref BaseVelocity, deltaTime);
|
|
|
|
//this.CharacterController.UpdateVelocity(ref BaseVelocity, deltaTime);
|
|
if (BaseVelocity.magnitude < MinVelocityMagnitude)
|
|
{
|
|
BaseVelocity = Vector3.zero;
|
|
}
|
|
|
|
#region Calculate Character movement from base velocity
|
|
// Perform the move from base velocity
|
|
if (BaseVelocity.sqrMagnitude > 0f)
|
|
{
|
|
if (_solveMovementCollisions)
|
|
{
|
|
InternalCharacterMove(ref BaseVelocity, deltaTime);
|
|
}
|
|
else
|
|
{
|
|
_transientPosition += BaseVelocity * deltaTime;
|
|
}
|
|
}
|
|
|
|
// Process rigidbody hits/overlaps to affect velocity
|
|
if (InteractiveRigidbodyHandling)
|
|
{
|
|
ProcessVelocityForRigidbodyHits(ref BaseVelocity, deltaTime);
|
|
}
|
|
#endregion
|
|
|
|
// Handle planar constraint
|
|
if (HasPlanarConstraint)
|
|
{
|
|
_transientPosition = _initialSimulationPosition + Vector3.ProjectOnPlane(_transientPosition - _initialSimulationPosition, PlanarConstraintAxis.normalized);
|
|
}
|
|
|
|
// Discrete collision detection
|
|
if (DiscreteCollisionEvents)
|
|
{
|
|
int nbOverlaps = CharacterCollisionsOverlap(_transientPosition, _transientRotation, _internalProbedColliders, CollisionOffset * 2f);
|
|
for (int i = 0; i < nbOverlaps; i++)
|
|
{
|
|
CharacterController.OnDiscreteCollisionDetected(_internalProbedColliders[i]);
|
|
}
|
|
}
|
|
|
|
CharacterController.AfterCharacterUpdate(deltaTime);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if motor can be considered stable on given slope normal
|
|
/// </summary>
|
|
private bool IsStableOnNormal(Vector3 normal)
|
|
{
|
|
return Vector3.Angle(_characterUp, normal) <= MaxStableSlopeAngle;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if motor can be considered stable on given slope normal
|
|
/// </summary>
|
|
private bool IsStableWithSpecialCases(ref HitStabilityReport stabilityReport, Vector3 velocity)
|
|
{
|
|
if (LedgeAndDenivelationHandling)
|
|
{
|
|
if (stabilityReport.LedgeDetected)
|
|
{
|
|
if (stabilityReport.IsMovingTowardsEmptySideOfLedge)
|
|
{
|
|
// Max snap vel
|
|
Vector3 velocityOnLedgeNormal = Vector3.Project(velocity, stabilityReport.LedgeFacingDirection);
|
|
if (velocityOnLedgeNormal.magnitude >= MaxVelocityForLedgeSnap)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Distance from ledge
|
|
if (stabilityReport.IsOnEmptySideOfLedge && stabilityReport.DistanceFromLedge > MaxStableDistanceFromLedge)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// "Launching" off of slopes of a certain denivelation angle
|
|
if (LastGroundingStatus.FoundAnyGround && stabilityReport.InnerNormal.sqrMagnitude != 0f && stabilityReport.OuterNormal.sqrMagnitude != 0f)
|
|
{
|
|
float denivelationAngle = Vector3.Angle(stabilityReport.InnerNormal, stabilityReport.OuterNormal);
|
|
if (denivelationAngle > MaxStableDenivelationAngle)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
denivelationAngle = Vector3.Angle(LastGroundingStatus.InnerGroundNormal, stabilityReport.OuterNormal);
|
|
if (denivelationAngle > MaxStableDenivelationAngle)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Probes for valid ground and midifies the input transientPosition if ground snapping occurs
|
|
/// </summary>
|
|
public void ProbeGround(ref Vector3 probingPosition, Quaternion atRotation, float probingDistance, ref CharacterGroundingReport groundingReport)
|
|
{
|
|
if (probingDistance < MinimumGroundProbingDistance)
|
|
{
|
|
probingDistance = MinimumGroundProbingDistance;
|
|
}
|
|
|
|
int groundSweepsMade = 0;
|
|
RaycastHit groundSweepHit = new RaycastHit();
|
|
bool groundSweepingIsOver = false;
|
|
Vector3 groundSweepPosition = probingPosition;
|
|
Vector3 groundSweepDirection = (atRotation * -_cachedWorldUp);
|
|
float groundProbeDistanceRemaining = probingDistance;
|
|
while (groundProbeDistanceRemaining > 0 && (groundSweepsMade <= MaxGroundingSweepIterations) && !groundSweepingIsOver)
|
|
{
|
|
// Sweep for ground detection
|
|
if (CharacterGroundSweep(
|
|
groundSweepPosition, // position
|
|
atRotation, // rotation
|
|
groundSweepDirection, // direction
|
|
groundProbeDistanceRemaining, // distance
|
|
out groundSweepHit)) // hit
|
|
{
|
|
Vector3 targetPosition = groundSweepPosition + (groundSweepDirection * groundSweepHit.distance);
|
|
HitStabilityReport groundHitStabilityReport = new HitStabilityReport();
|
|
EvaluateHitStability(groundSweepHit.collider, groundSweepHit.normal, groundSweepHit.point, targetPosition, _transientRotation, BaseVelocity, ref groundHitStabilityReport);
|
|
|
|
groundingReport.FoundAnyGround = true;
|
|
groundingReport.GroundNormal = groundSweepHit.normal;
|
|
groundingReport.InnerGroundNormal = groundHitStabilityReport.InnerNormal;
|
|
groundingReport.OuterGroundNormal = groundHitStabilityReport.OuterNormal;
|
|
groundingReport.GroundCollider = groundSweepHit.collider;
|
|
groundingReport.GroundPoint = groundSweepHit.point;
|
|
groundingReport.SnappingPrevented = false;
|
|
|
|
// Found stable ground
|
|
if (groundHitStabilityReport.IsStable)
|
|
{
|
|
// Find all scenarios where ground snapping should be canceled
|
|
groundingReport.SnappingPrevented = !IsStableWithSpecialCases(ref groundHitStabilityReport, BaseVelocity);
|
|
|
|
groundingReport.IsStableOnGround = true;
|
|
|
|
// Ground snapping
|
|
if (!groundingReport.SnappingPrevented)
|
|
{
|
|
probingPosition = groundSweepPosition + (groundSweepDirection * (groundSweepHit.distance - CollisionOffset));
|
|
}
|
|
|
|
CharacterController.OnGroundHit(groundSweepHit.collider, groundSweepHit.normal, groundSweepHit.point, ref groundHitStabilityReport);
|
|
groundSweepingIsOver = true;
|
|
}
|
|
else
|
|
{
|
|
// Calculate movement from this iteration and advance position
|
|
Vector3 sweepMovement = (groundSweepDirection * groundSweepHit.distance) + ((atRotation * _cachedWorldUp) * Mathf.Max(CollisionOffset, groundSweepHit.distance));
|
|
groundSweepPosition = groundSweepPosition + sweepMovement;
|
|
|
|
// Set remaining distance
|
|
groundProbeDistanceRemaining = Mathf.Min(GroundProbeReboundDistance, Mathf.Max(groundProbeDistanceRemaining - sweepMovement.magnitude, 0f));
|
|
|
|
// Reorient direction
|
|
groundSweepDirection = Vector3.ProjectOnPlane(groundSweepDirection, groundSweepHit.normal).normalized;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
groundSweepingIsOver = true;
|
|
}
|
|
|
|
groundSweepsMade++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Forces the character to unground itself on its next grounding update
|
|
/// </summary>
|
|
public void ForceUnground(float time = 0.1f)
|
|
{
|
|
_mustUnground = true;
|
|
_mustUngroundTimeCounter = time;
|
|
}
|
|
|
|
public bool MustUnground()
|
|
{
|
|
return _mustUnground || _mustUngroundTimeCounter > 0f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the direction adjusted to be tangent to a specified surface normal relatively to the character's up direction.
|
|
/// Useful for reorienting a direction on a slope without any lateral deviation in trajectory
|
|
/// </summary>
|
|
public Vector3 GetDirectionTangentToSurface(Vector3 direction, Vector3 surfaceNormal)
|
|
{
|
|
Vector3 directionRight = Vector3.Cross(direction, _characterUp);
|
|
return Vector3.Cross(surfaceNormal, directionRight).normalized;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves the character's position by given movement while taking into account all physics simulation, step-handling and
|
|
/// velocity projection rules that affect the character motor
|
|
/// </summary>
|
|
/// <returns> Returns false if movement could not be solved until the end </returns>
|
|
private bool InternalCharacterMove(ref Vector3 transientVelocity, float deltaTime)
|
|
{
|
|
if (deltaTime <= 0f)
|
|
return false;
|
|
|
|
// Planar constraint
|
|
if (HasPlanarConstraint)
|
|
{
|
|
transientVelocity = Vector3.ProjectOnPlane(transientVelocity, PlanarConstraintAxis.normalized);
|
|
}
|
|
|
|
bool wasCompleted = true;
|
|
Vector3 remainingMovementDirection = transientVelocity.normalized;
|
|
float remainingMovementMagnitude = transientVelocity.magnitude * deltaTime;
|
|
Vector3 originalVelocityDirection = remainingMovementDirection;
|
|
int sweepsMade = 0;
|
|
bool hitSomethingThisSweepIteration = true;
|
|
Vector3 tmpMovedPosition = _transientPosition;
|
|
bool previousHitIsStable = false;
|
|
Vector3 previousVelocity = _cachedZeroVector;
|
|
Vector3 previousObstructionNormal = _cachedZeroVector;
|
|
MovementSweepState sweepState = MovementSweepState.Initial;
|
|
|
|
// Project movement against current overlaps before doing the sweeps
|
|
for (int i = 0; i < _overlapsCount; i++)
|
|
{
|
|
Vector3 overlapNormal = _overlaps[i].Normal;
|
|
if (Vector3.Dot(remainingMovementDirection, overlapNormal) < 0f)
|
|
{
|
|
bool stableOnHit = IsStableOnNormal(overlapNormal) && !MustUnground();
|
|
Vector3 velocityBeforeProjection = transientVelocity;
|
|
Vector3 obstructionNormal = GetObstructionNormal(overlapNormal, stableOnHit);
|
|
|
|
InternalHandleVelocityProjection(
|
|
stableOnHit,
|
|
overlapNormal,
|
|
obstructionNormal,
|
|
originalVelocityDirection,
|
|
ref sweepState,
|
|
previousHitIsStable,
|
|
previousVelocity,
|
|
previousObstructionNormal,
|
|
ref transientVelocity,
|
|
ref remainingMovementMagnitude,
|
|
ref remainingMovementDirection);
|
|
|
|
previousHitIsStable = stableOnHit;
|
|
previousVelocity = velocityBeforeProjection;
|
|
previousObstructionNormal = obstructionNormal;
|
|
}
|
|
}
|
|
|
|
// Sweep the desired movement to detect collisions
|
|
while (remainingMovementMagnitude > 0f &&
|
|
(sweepsMade <= MaxMovementIterations) &&
|
|
hitSomethingThisSweepIteration)
|
|
{
|
|
bool foundClosestHit = false;
|
|
Vector3 closestSweepHitPoint = default;
|
|
Vector3 closestSweepHitNormal = default;
|
|
float closestSweepHitDistance = 0f;
|
|
Collider closestSweepHitCollider = null;
|
|
|
|
if (CheckMovementInitialOverlaps)
|
|
{
|
|
int numOverlaps = CharacterCollisionsOverlap(
|
|
tmpMovedPosition,
|
|
_transientRotation,
|
|
_internalProbedColliders,
|
|
0f,
|
|
false);
|
|
if (numOverlaps > 0)
|
|
{
|
|
closestSweepHitDistance = 0f;
|
|
|
|
float mostObstructingOverlapNormalDotProduct = 2f;
|
|
|
|
for (int i = 0; i < numOverlaps; i++)
|
|
{
|
|
Collider tmpCollider = _internalProbedColliders[i];
|
|
|
|
if (Physics.ComputePenetration(
|
|
Capsule,
|
|
tmpMovedPosition,
|
|
_transientRotation,
|
|
tmpCollider,
|
|
tmpCollider.transform.position,
|
|
tmpCollider.transform.rotation,
|
|
out Vector3 resolutionDirection,
|
|
out float resolutionDistance))
|
|
{
|
|
float dotProduct = Vector3.Dot(remainingMovementDirection, resolutionDirection);
|
|
if (dotProduct < 0f && dotProduct < mostObstructingOverlapNormalDotProduct)
|
|
{
|
|
mostObstructingOverlapNormalDotProduct = dotProduct;
|
|
|
|
closestSweepHitNormal = resolutionDirection;
|
|
closestSweepHitCollider = tmpCollider;
|
|
closestSweepHitPoint = tmpMovedPosition + (_transientRotation * CharacterTransformToCapsuleCenter) + (resolutionDirection * resolutionDistance);
|
|
|
|
foundClosestHit = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!foundClosestHit && CharacterCollisionsSweep(
|
|
tmpMovedPosition, // position
|
|
_transientRotation, // rotation
|
|
remainingMovementDirection, // direction
|
|
remainingMovementMagnitude + CollisionOffset, // distance
|
|
out RaycastHit closestSweepHit, // closest hit
|
|
_internalCharacterHits) // all hits
|
|
> 0)
|
|
{
|
|
closestSweepHitNormal = closestSweepHit.normal;
|
|
closestSweepHitDistance = closestSweepHit.distance;
|
|
closestSweepHitCollider = closestSweepHit.collider;
|
|
closestSweepHitPoint = closestSweepHit.point;
|
|
|
|
foundClosestHit = true;
|
|
}
|
|
|
|
if (foundClosestHit)
|
|
{
|
|
// Calculate movement from this iteration
|
|
Vector3 sweepMovement = (remainingMovementDirection * (Mathf.Max(0f, closestSweepHitDistance - CollisionOffset)));
|
|
tmpMovedPosition += sweepMovement;
|
|
remainingMovementMagnitude -= sweepMovement.magnitude;
|
|
|
|
// Evaluate if hit is stable
|
|
HitStabilityReport moveHitStabilityReport = new HitStabilityReport();
|
|
EvaluateHitStability(closestSweepHitCollider, closestSweepHitNormal, closestSweepHitPoint, tmpMovedPosition, _transientRotation, transientVelocity, ref moveHitStabilityReport);
|
|
|
|
// Handle stepping up steps points higher than bottom capsule radius
|
|
bool foundValidStepHit = false;
|
|
if (_solveGrounding && StepHandling != StepHandlingMethod.None && moveHitStabilityReport.ValidStepDetected)
|
|
{
|
|
float obstructionCorrelation = Mathf.Abs(Vector3.Dot(closestSweepHitNormal, _characterUp));
|
|
if (obstructionCorrelation <= CorrelationForVerticalObstruction)
|
|
{
|
|
Vector3 stepForwardDirection = Vector3.ProjectOnPlane(-closestSweepHitNormal, _characterUp).normalized;
|
|
Vector3 stepCastStartPoint = (tmpMovedPosition + (stepForwardDirection * SteppingForwardDistance)) +
|
|
(_characterUp * MaxStepHeight);
|
|
|
|
// Cast downward from the top of the stepping height
|
|
int nbStepHits = CharacterCollisionsSweep(
|
|
stepCastStartPoint, // position
|
|
_transientRotation, // rotation
|
|
-_characterUp, // direction
|
|
MaxStepHeight, // distance
|
|
out RaycastHit closestStepHit, // closest hit
|
|
_internalCharacterHits,
|
|
0f,
|
|
true); // all hits
|
|
|
|
// Check for hit corresponding to stepped collider
|
|
for (int i = 0; i < nbStepHits; i++)
|
|
{
|
|
if (_internalCharacterHits[i].collider == moveHitStabilityReport.SteppedCollider)
|
|
{
|
|
Vector3 endStepPosition = stepCastStartPoint + (-_characterUp * (_internalCharacterHits[i].distance - CollisionOffset));
|
|
tmpMovedPosition = endStepPosition;
|
|
foundValidStepHit = true;
|
|
|
|
// Project velocity on ground normal at step
|
|
transientVelocity = Vector3.ProjectOnPlane(transientVelocity, CharacterUp);
|
|
remainingMovementDirection = transientVelocity.normalized;
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle movement solving
|
|
if (!foundValidStepHit)
|
|
{
|
|
Vector3 obstructionNormal = GetObstructionNormal(closestSweepHitNormal, moveHitStabilityReport.IsStable);
|
|
|
|
// Movement hit callback
|
|
CharacterController.OnMovementHit(closestSweepHitCollider, closestSweepHitNormal, closestSweepHitPoint, ref moveHitStabilityReport);
|
|
|
|
// Handle remembering rigidbody hits
|
|
if (InteractiveRigidbodyHandling && closestSweepHitCollider.attachedRigidbody)
|
|
{
|
|
StoreRigidbodyHit(
|
|
closestSweepHitCollider.attachedRigidbody,
|
|
transientVelocity,
|
|
closestSweepHitPoint,
|
|
obstructionNormal,
|
|
moveHitStabilityReport);
|
|
}
|
|
|
|
bool stableOnHit = moveHitStabilityReport.IsStable && !MustUnground();
|
|
Vector3 velocityBeforeProj = transientVelocity;
|
|
|
|
// Project velocity for next iteration
|
|
InternalHandleVelocityProjection(
|
|
stableOnHit,
|
|
closestSweepHitNormal,
|
|
obstructionNormal,
|
|
originalVelocityDirection,
|
|
ref sweepState,
|
|
previousHitIsStable,
|
|
previousVelocity,
|
|
previousObstructionNormal,
|
|
ref transientVelocity,
|
|
ref remainingMovementMagnitude,
|
|
ref remainingMovementDirection);
|
|
|
|
previousHitIsStable = stableOnHit;
|
|
previousVelocity = velocityBeforeProj;
|
|
previousObstructionNormal = obstructionNormal;
|
|
}
|
|
}
|
|
// If we hit nothing...
|
|
else
|
|
{
|
|
hitSomethingThisSweepIteration = false;
|
|
}
|
|
|
|
// Safety for exceeding max sweeps allowed
|
|
sweepsMade++;
|
|
if (sweepsMade > MaxMovementIterations)
|
|
{
|
|
if (KillRemainingMovementWhenExceedMaxMovementIterations)
|
|
{
|
|
remainingMovementMagnitude = 0f;
|
|
}
|
|
|
|
if (KillVelocityWhenExceedMaxMovementIterations)
|
|
{
|
|
transientVelocity = Vector3.zero;
|
|
}
|
|
wasCompleted = false;
|
|
}
|
|
}
|
|
|
|
// Move position for the remainder of the movement
|
|
tmpMovedPosition += (remainingMovementDirection * remainingMovementMagnitude);
|
|
_transientPosition = tmpMovedPosition;
|
|
|
|
return wasCompleted;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the effective normal for movement obstruction depending on current grounding status
|
|
/// </summary>
|
|
private Vector3 GetObstructionNormal(Vector3 hitNormal, bool stableOnHit)
|
|
{
|
|
// Find hit/obstruction/offset normal
|
|
Vector3 obstructionNormal = hitNormal;
|
|
if (GroundingStatus.IsStableOnGround && !MustUnground() && !stableOnHit)
|
|
{
|
|
Vector3 obstructionLeftAlongGround = Vector3.Cross(GroundingStatus.GroundNormal, obstructionNormal).normalized;
|
|
obstructionNormal = Vector3.Cross(obstructionLeftAlongGround, _characterUp).normalized;
|
|
}
|
|
|
|
// Catch cases where cross product between parallel normals returned 0
|
|
if (obstructionNormal.sqrMagnitude == 0f)
|
|
{
|
|
obstructionNormal = hitNormal;
|
|
}
|
|
|
|
return obstructionNormal;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remembers a rigidbody hit for processing later
|
|
/// </summary>
|
|
private void StoreRigidbodyHit(Rigidbody hitRigidbody, Vector3 hitVelocity, Vector3 hitPoint, Vector3 obstructionNormal, HitStabilityReport hitStabilityReport)
|
|
{
|
|
if (_rigidbodyProjectionHitCount < _internalRigidbodyProjectionHits.Length)
|
|
{
|
|
if (!hitRigidbody.GetComponent<KinematicCharacterMotor>())
|
|
{
|
|
RigidbodyProjectionHit rph = new RigidbodyProjectionHit();
|
|
rph.Rigidbody = hitRigidbody;
|
|
rph.HitPoint = hitPoint;
|
|
rph.EffectiveHitNormal = obstructionNormal;
|
|
rph.HitVelocity = hitVelocity;
|
|
rph.StableOnHit = hitStabilityReport.IsStable;
|
|
|
|
_internalRigidbodyProjectionHits[_rigidbodyProjectionHitCount] = rph;
|
|
_rigidbodyProjectionHitCount++;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SetTransientPosition(Vector3 newPos)
|
|
{
|
|
_transientPosition = newPos;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Processes movement projection upon detecting a hit
|
|
/// </summary>
|
|
private void InternalHandleVelocityProjection(bool stableOnHit, Vector3 hitNormal, Vector3 obstructionNormal, Vector3 originalDirection,
|
|
ref MovementSweepState sweepState, bool previousHitIsStable, Vector3 previousVelocity, Vector3 previousObstructionNormal,
|
|
ref Vector3 transientVelocity, ref float remainingMovementMagnitude, ref Vector3 remainingMovementDirection)
|
|
{
|
|
if (transientVelocity.sqrMagnitude <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Vector3 velocityBeforeProjection = transientVelocity;
|
|
|
|
if (stableOnHit)
|
|
{
|
|
LastMovementIterationFoundAnyGround = true;
|
|
HandleVelocityProjection(ref transientVelocity, obstructionNormal, stableOnHit);
|
|
}
|
|
else
|
|
{
|
|
// Handle projection
|
|
if (sweepState == MovementSweepState.Initial)
|
|
{
|
|
HandleVelocityProjection(ref transientVelocity, obstructionNormal, stableOnHit);
|
|
sweepState = MovementSweepState.AfterFirstHit;
|
|
}
|
|
// Blocking crease handling
|
|
else if (sweepState == MovementSweepState.AfterFirstHit)
|
|
{
|
|
EvaluateCrease(
|
|
transientVelocity,
|
|
previousVelocity,
|
|
obstructionNormal,
|
|
previousObstructionNormal,
|
|
stableOnHit,
|
|
previousHitIsStable,
|
|
GroundingStatus.IsStableOnGround && !MustUnground(),
|
|
out bool foundCrease,
|
|
out Vector3 creaseDirection);
|
|
|
|
if (foundCrease)
|
|
{
|
|
if (GroundingStatus.IsStableOnGround && !MustUnground())
|
|
{
|
|
transientVelocity = Vector3.zero;
|
|
sweepState = MovementSweepState.FoundBlockingCorner;
|
|
}
|
|
else
|
|
{
|
|
transientVelocity = Vector3.Project(transientVelocity, creaseDirection);
|
|
sweepState = MovementSweepState.FoundBlockingCrease;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
HandleVelocityProjection(ref transientVelocity, obstructionNormal, stableOnHit);
|
|
}
|
|
}
|
|
// Blocking corner handling
|
|
else if (sweepState == MovementSweepState.FoundBlockingCrease)
|
|
{
|
|
transientVelocity = Vector3.zero;
|
|
sweepState = MovementSweepState.FoundBlockingCorner;
|
|
}
|
|
}
|
|
|
|
if (HasPlanarConstraint)
|
|
{
|
|
transientVelocity = Vector3.ProjectOnPlane(transientVelocity, PlanarConstraintAxis.normalized);
|
|
}
|
|
|
|
float newVelocityFactor = transientVelocity.magnitude / velocityBeforeProjection.magnitude;
|
|
remainingMovementMagnitude *= newVelocityFactor;
|
|
remainingMovementDirection = transientVelocity.normalized;
|
|
}
|
|
|
|
private void EvaluateCrease(
|
|
Vector3 currentCharacterVelocity,
|
|
Vector3 previousCharacterVelocity,
|
|
Vector3 currentHitNormal,
|
|
Vector3 previousHitNormal,
|
|
bool currentHitIsStable,
|
|
bool previousHitIsStable,
|
|
bool characterIsStable,
|
|
out bool isValidCrease,
|
|
out Vector3 creaseDirection)
|
|
{
|
|
isValidCrease = false;
|
|
creaseDirection = default;
|
|
|
|
if (!characterIsStable || !currentHitIsStable || !previousHitIsStable)
|
|
{
|
|
Vector3 tmpBlockingCreaseDirection = Vector3.Cross(currentHitNormal, previousHitNormal).normalized;
|
|
float dotPlanes = Vector3.Dot(currentHitNormal, previousHitNormal);
|
|
bool isVelocityConstrainedByCrease = false;
|
|
|
|
// Avoid calculations if the two planes are the same
|
|
if (dotPlanes < 0.999f)
|
|
{
|
|
// TODO: can this whole part be made simpler? (with 2d projections, etc)
|
|
Vector3 normalAOnCreasePlane = Vector3.ProjectOnPlane(currentHitNormal, tmpBlockingCreaseDirection).normalized;
|
|
Vector3 normalBOnCreasePlane = Vector3.ProjectOnPlane(previousHitNormal, tmpBlockingCreaseDirection).normalized;
|
|
float dotPlanesOnCreasePlane = Vector3.Dot(normalAOnCreasePlane, normalBOnCreasePlane);
|
|
|
|
Vector3 enteringVelocityDirectionOnCreasePlane = Vector3.ProjectOnPlane(previousCharacterVelocity, tmpBlockingCreaseDirection).normalized;
|
|
|
|
if (dotPlanesOnCreasePlane <= (Vector3.Dot(-enteringVelocityDirectionOnCreasePlane, normalAOnCreasePlane) + 0.001f) &&
|
|
dotPlanesOnCreasePlane <= (Vector3.Dot(-enteringVelocityDirectionOnCreasePlane, normalBOnCreasePlane) + 0.001f))
|
|
{
|
|
isVelocityConstrainedByCrease = true;
|
|
}
|
|
}
|
|
|
|
if (isVelocityConstrainedByCrease)
|
|
{
|
|
// Flip crease direction to make it representative of the real direction our velocity would be projected to
|
|
if (Vector3.Dot(tmpBlockingCreaseDirection, currentCharacterVelocity) < 0f)
|
|
{
|
|
tmpBlockingCreaseDirection = -tmpBlockingCreaseDirection;
|
|
}
|
|
|
|
isValidCrease = true;
|
|
creaseDirection = tmpBlockingCreaseDirection;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows you to override the way velocity is projected on an obstruction
|
|
/// </summary>
|
|
public virtual void HandleVelocityProjection(ref Vector3 velocity, Vector3 obstructionNormal, bool stableOnHit)
|
|
{
|
|
if (GroundingStatus.IsStableOnGround && !MustUnground())
|
|
{
|
|
// On stable slopes, simply reorient the movement without any loss
|
|
if (stableOnHit)
|
|
{
|
|
velocity = GetDirectionTangentToSurface(velocity, obstructionNormal) * velocity.magnitude;
|
|
}
|
|
// On blocking hits, project the movement on the obstruction while following the grounding plane
|
|
else
|
|
{
|
|
Vector3 obstructionRightAlongGround = Vector3.Cross(obstructionNormal, GroundingStatus.GroundNormal).normalized;
|
|
Vector3 obstructionUpAlongGround = Vector3.Cross(obstructionRightAlongGround, obstructionNormal).normalized;
|
|
velocity = GetDirectionTangentToSurface(velocity, obstructionUpAlongGround) * velocity.magnitude;
|
|
velocity = Vector3.ProjectOnPlane(velocity, obstructionNormal);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (stableOnHit)
|
|
{
|
|
// Handle stable landing
|
|
velocity = Vector3.ProjectOnPlane(velocity, CharacterUp);
|
|
velocity = GetDirectionTangentToSurface(velocity, obstructionNormal) * velocity.magnitude;
|
|
}
|
|
// Handle generic obstruction
|
|
else
|
|
{
|
|
velocity = Vector3.ProjectOnPlane(velocity, obstructionNormal);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows you to override the way hit rigidbodies are pushed / interacted with.
|
|
/// ProcessedVelocity is what must be modified if this interaction affects the character's velocity.
|
|
/// </summary>
|
|
public virtual void HandleSimulatedRigidbodyInteraction(ref Vector3 processedVelocity, RigidbodyProjectionHit hit, float deltaTime)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Takes into account rigidbody hits for adding to the velocity
|
|
/// </summary>
|
|
private void ProcessVelocityForRigidbodyHits(ref Vector3 processedVelocity, float deltaTime)
|
|
{
|
|
for (int i = 0; i < _rigidbodyProjectionHitCount; i++)
|
|
{
|
|
RigidbodyProjectionHit bodyHit = _internalRigidbodyProjectionHits[i];
|
|
|
|
if (bodyHit.Rigidbody && !_rigidbodiesPushedThisMove.Contains(bodyHit.Rigidbody))
|
|
{
|
|
if (_internalRigidbodyProjectionHits[i].Rigidbody != _attachedRigidbody)
|
|
{
|
|
// Remember we hit this rigidbody
|
|
_rigidbodiesPushedThisMove.Add(bodyHit.Rigidbody);
|
|
|
|
float characterMass = SimulatedCharacterMass;
|
|
Vector3 characterVelocity = bodyHit.HitVelocity;
|
|
|
|
KinematicCharacterMotor hitCharacterMotor = bodyHit.Rigidbody.GetComponent<KinematicCharacterMotor>();
|
|
bool hitBodyIsCharacter = hitCharacterMotor != null;
|
|
bool hitBodyIsDynamic = !bodyHit.Rigidbody.isKinematic;
|
|
float hitBodyMass = bodyHit.Rigidbody.mass;
|
|
float hitBodyMassAtPoint = bodyHit.Rigidbody.mass; // todo
|
|
Vector3 hitBodyVelocity = bodyHit.Rigidbody.velocity;
|
|
if (hitBodyIsCharacter)
|
|
{
|
|
hitBodyMass = hitCharacterMotor.SimulatedCharacterMass;
|
|
hitBodyMassAtPoint = hitCharacterMotor.SimulatedCharacterMass; // todo
|
|
hitBodyVelocity = hitCharacterMotor.BaseVelocity;
|
|
}
|
|
else if (!hitBodyIsDynamic)
|
|
{
|
|
PhysicsMover physicsMover = bodyHit.Rigidbody.GetComponent<PhysicsMover>();
|
|
if(physicsMover)
|
|
{
|
|
hitBodyVelocity = physicsMover.Velocity;
|
|
}
|
|
}
|
|
|
|
// Calculate the ratio of the total mass that the character mass represents
|
|
float characterToBodyMassRatio = 1f;
|
|
{
|
|
if (characterMass + hitBodyMassAtPoint > 0f)
|
|
{
|
|
characterToBodyMassRatio = characterMass / (characterMass + hitBodyMassAtPoint);
|
|
}
|
|
else
|
|
{
|
|
characterToBodyMassRatio = 0.5f;
|
|
}
|
|
|
|
// Hitting a non-dynamic body
|
|
if (!hitBodyIsDynamic)
|
|
{
|
|
characterToBodyMassRatio = 0f;
|
|
}
|
|
// Emulate kinematic body interaction
|
|
else if (RigidbodyInteractionType == RigidbodyInteractionType.Kinematic && !hitBodyIsCharacter)
|
|
{
|
|
characterToBodyMassRatio = 1f;
|
|
}
|
|
}
|
|
|
|
ComputeCollisionResolutionForHitBody(
|
|
bodyHit.EffectiveHitNormal,
|
|
characterVelocity,
|
|
hitBodyVelocity,
|
|
characterToBodyMassRatio,
|
|
out Vector3 velocityChangeOnCharacter,
|
|
out Vector3 velocityChangeOnBody);
|
|
|
|
processedVelocity += velocityChangeOnCharacter;
|
|
|
|
if (hitBodyIsCharacter)
|
|
{
|
|
hitCharacterMotor.BaseVelocity += velocityChangeOnCharacter;
|
|
}
|
|
else if (hitBodyIsDynamic)
|
|
{
|
|
bodyHit.Rigidbody.AddForceAtPosition(velocityChangeOnBody, bodyHit.HitPoint, ForceMode.VelocityChange);
|
|
}
|
|
|
|
if (RigidbodyInteractionType == RigidbodyInteractionType.SimulatedDynamic)
|
|
{
|
|
HandleSimulatedRigidbodyInteraction(ref processedVelocity, bodyHit, deltaTime);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public void ComputeCollisionResolutionForHitBody(
|
|
Vector3 hitNormal,
|
|
Vector3 characterVelocity,
|
|
Vector3 bodyVelocity,
|
|
float characterToBodyMassRatio,
|
|
out Vector3 velocityChangeOnCharacter,
|
|
out Vector3 velocityChangeOnBody)
|
|
{
|
|
velocityChangeOnCharacter = default;
|
|
velocityChangeOnBody = default;
|
|
|
|
float bodyToCharacterMassRatio = 1f - characterToBodyMassRatio;
|
|
float characterVelocityMagnitudeOnHitNormal = Vector3.Dot(characterVelocity, hitNormal);
|
|
float bodyVelocityMagnitudeOnHitNormal = Vector3.Dot(bodyVelocity, hitNormal);
|
|
|
|
// if character velocity was going against the obstruction, restore the portion of the velocity that got projected during the movement phase
|
|
if (characterVelocityMagnitudeOnHitNormal < 0f)
|
|
{
|
|
Vector3 restoredCharacterVelocity = hitNormal * characterVelocityMagnitudeOnHitNormal;
|
|
velocityChangeOnCharacter += restoredCharacterVelocity;
|
|
}
|
|
|
|
// solve impulse velocities on both bodies, but only if the body velocity would be giving resistance to the character in any way
|
|
if (bodyVelocityMagnitudeOnHitNormal > characterVelocityMagnitudeOnHitNormal)
|
|
{
|
|
Vector3 relativeImpactVelocity = hitNormal * (bodyVelocityMagnitudeOnHitNormal - characterVelocityMagnitudeOnHitNormal);
|
|
velocityChangeOnCharacter += relativeImpactVelocity * bodyToCharacterMassRatio;
|
|
velocityChangeOnBody += -relativeImpactVelocity * characterToBodyMassRatio;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if the input collider is valid for collision processing
|
|
/// </summary>
|
|
/// <returns> Returns true if the collider is valid </returns>
|
|
private bool CheckIfColliderValidForCollisions(Collider coll)
|
|
{
|
|
// Ignore self
|
|
if (coll == Capsule)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!InternalIsColliderValidForCollisions(coll))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if the input collider is valid for collision processing
|
|
/// </summary>
|
|
private bool InternalIsColliderValidForCollisions(Collider coll)
|
|
{
|
|
Rigidbody colliderAttachedRigidbody = coll.attachedRigidbody;
|
|
if (colliderAttachedRigidbody)
|
|
{
|
|
bool isRigidbodyKinematic = colliderAttachedRigidbody.isKinematic;
|
|
|
|
// If movement is made from AttachedRigidbody, ignore the AttachedRigidbody
|
|
if (_isMovingFromAttachedRigidbody && (!isRigidbodyKinematic || colliderAttachedRigidbody == _attachedRigidbody))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// don't collide with dynamic rigidbodies if our RigidbodyInteractionType is kinematic
|
|
if (RigidbodyInteractionType == RigidbodyInteractionType.Kinematic && !isRigidbodyKinematic)
|
|
{
|
|
// wake up rigidbody
|
|
if (coll.attachedRigidbody)
|
|
{
|
|
coll.attachedRigidbody.WakeUp();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Custom checks
|
|
bool colliderValid = CharacterController.IsColliderValidForCollisions(coll);
|
|
if (!colliderValid)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if the motor is considered stable on a given hit
|
|
/// </summary>
|
|
public void EvaluateHitStability(Collider hitCollider, Vector3 hitNormal, Vector3 hitPoint, Vector3 atCharacterPosition, Quaternion atCharacterRotation, Vector3 withCharacterVelocity, ref HitStabilityReport stabilityReport)
|
|
{
|
|
if (!_solveGrounding)
|
|
{
|
|
stabilityReport.IsStable = false;
|
|
return;
|
|
}
|
|
|
|
Vector3 atCharacterUp = atCharacterRotation * _cachedWorldUp;
|
|
Vector3 innerHitDirection = Vector3.ProjectOnPlane(hitNormal, atCharacterUp).normalized;
|
|
|
|
stabilityReport.IsStable = this.IsStableOnNormal(hitNormal);
|
|
|
|
stabilityReport.FoundInnerNormal = false;
|
|
stabilityReport.FoundOuterNormal = false;
|
|
stabilityReport.InnerNormal = hitNormal;
|
|
stabilityReport.OuterNormal = hitNormal;
|
|
|
|
// Ledge handling
|
|
if (LedgeAndDenivelationHandling)
|
|
{
|
|
float ledgeCheckHeight = MinDistanceForLedge;
|
|
if (StepHandling != StepHandlingMethod.None)
|
|
{
|
|
ledgeCheckHeight = MaxStepHeight;
|
|
}
|
|
|
|
bool isStableLedgeInner = false;
|
|
bool isStableLedgeOuter = false;
|
|
|
|
if (CharacterCollisionsRaycast(
|
|
hitPoint + (atCharacterUp * SecondaryProbesVertical) + (innerHitDirection * SecondaryProbesHorizontal),
|
|
-atCharacterUp,
|
|
ledgeCheckHeight + SecondaryProbesVertical,
|
|
out RaycastHit innerLedgeHit,
|
|
_internalCharacterHits) > 0)
|
|
{
|
|
Vector3 innerLedgeNormal = innerLedgeHit.normal;
|
|
stabilityReport.InnerNormal = innerLedgeNormal;
|
|
stabilityReport.FoundInnerNormal = true;
|
|
isStableLedgeInner = IsStableOnNormal(innerLedgeNormal);
|
|
}
|
|
|
|
if (CharacterCollisionsRaycast(
|
|
hitPoint + (atCharacterUp * SecondaryProbesVertical) + (-innerHitDirection * SecondaryProbesHorizontal),
|
|
-atCharacterUp,
|
|
ledgeCheckHeight + SecondaryProbesVertical,
|
|
out RaycastHit outerLedgeHit,
|
|
_internalCharacterHits) > 0)
|
|
{
|
|
Vector3 outerLedgeNormal = outerLedgeHit.normal;
|
|
stabilityReport.OuterNormal = outerLedgeNormal;
|
|
stabilityReport.FoundOuterNormal = true;
|
|
isStableLedgeOuter = IsStableOnNormal(outerLedgeNormal);
|
|
}
|
|
|
|
stabilityReport.LedgeDetected = (isStableLedgeInner != isStableLedgeOuter);
|
|
if (stabilityReport.LedgeDetected)
|
|
{
|
|
stabilityReport.IsOnEmptySideOfLedge = isStableLedgeOuter && !isStableLedgeInner;
|
|
stabilityReport.LedgeGroundNormal = isStableLedgeOuter ? stabilityReport.OuterNormal : stabilityReport.InnerNormal;
|
|
stabilityReport.LedgeRightDirection = Vector3.Cross(hitNormal, stabilityReport.LedgeGroundNormal).normalized;
|
|
stabilityReport.LedgeFacingDirection = Vector3.ProjectOnPlane(Vector3.Cross(stabilityReport.LedgeGroundNormal, stabilityReport.LedgeRightDirection), CharacterUp).normalized;
|
|
stabilityReport.DistanceFromLedge = Vector3.ProjectOnPlane((hitPoint - (atCharacterPosition + (atCharacterRotation * _characterTransformToCapsuleBottom))), atCharacterUp).magnitude;
|
|
stabilityReport.IsMovingTowardsEmptySideOfLedge = Vector3.Dot(withCharacterVelocity.normalized, stabilityReport.LedgeFacingDirection) > 0f;
|
|
}
|
|
|
|
if (stabilityReport.IsStable)
|
|
{
|
|
stabilityReport.IsStable = IsStableWithSpecialCases(ref stabilityReport, withCharacterVelocity);
|
|
}
|
|
}
|
|
|
|
// Step handling
|
|
if (StepHandling != StepHandlingMethod.None && !stabilityReport.IsStable)
|
|
{
|
|
// Stepping not supported on dynamic rigidbodies
|
|
Rigidbody hitRigidbody = hitCollider.attachedRigidbody;
|
|
if (!(hitRigidbody && !hitRigidbody.isKinematic))
|
|
{
|
|
DetectSteps(atCharacterPosition, atCharacterRotation, hitPoint, innerHitDirection, ref stabilityReport);
|
|
|
|
if (stabilityReport.ValidStepDetected)
|
|
{
|
|
stabilityReport.IsStable = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
CharacterController.ProcessHitStabilityReport(hitCollider, hitNormal, hitPoint, atCharacterPosition, atCharacterRotation, ref stabilityReport);
|
|
}
|
|
|
|
private void DetectSteps(Vector3 characterPosition, Quaternion characterRotation, Vector3 hitPoint, Vector3 innerHitDirection, ref HitStabilityReport stabilityReport)
|
|
{
|
|
int nbStepHits = 0;
|
|
Collider tmpCollider;
|
|
RaycastHit outerStepHit;
|
|
Vector3 characterUp = characterRotation * _cachedWorldUp;
|
|
Vector3 verticalCharToHit = Vector3.Project((hitPoint - characterPosition), characterUp);
|
|
Vector3 horizontalCharToHitDirection = Vector3.ProjectOnPlane((hitPoint - characterPosition), characterUp).normalized;
|
|
Vector3 stepCheckStartPos = (hitPoint - verticalCharToHit) + (characterUp * MaxStepHeight) + (horizontalCharToHitDirection * CollisionOffset * 3f);
|
|
|
|
// Do outer step check with capsule cast on hit point
|
|
nbStepHits = CharacterCollisionsSweep(
|
|
stepCheckStartPos,
|
|
characterRotation,
|
|
-characterUp,
|
|
MaxStepHeight + CollisionOffset,
|
|
out outerStepHit,
|
|
_internalCharacterHits,
|
|
0f,
|
|
true);
|
|
|
|
// Check for overlaps and obstructions at the hit position
|
|
if (CheckStepValidity(nbStepHits, characterPosition, characterRotation, innerHitDirection, stepCheckStartPos, out tmpCollider))
|
|
{
|
|
stabilityReport.ValidStepDetected = true;
|
|
stabilityReport.SteppedCollider = tmpCollider;
|
|
}
|
|
|
|
if (StepHandling == StepHandlingMethod.Extra && !stabilityReport.ValidStepDetected)
|
|
{
|
|
// Do min reach step check with capsule cast on hit point
|
|
stepCheckStartPos = characterPosition + (characterUp * MaxStepHeight) + (-innerHitDirection * MinRequiredStepDepth);
|
|
nbStepHits = CharacterCollisionsSweep(
|
|
stepCheckStartPos,
|
|
characterRotation,
|
|
-characterUp,
|
|
MaxStepHeight - CollisionOffset,
|
|
out outerStepHit,
|
|
_internalCharacterHits,
|
|
0f,
|
|
true);
|
|
|
|
// Check for overlaps and obstructions at the hit position
|
|
if (CheckStepValidity(nbStepHits, characterPosition, characterRotation, innerHitDirection, stepCheckStartPos, out tmpCollider))
|
|
{
|
|
stabilityReport.ValidStepDetected = true;
|
|
stabilityReport.SteppedCollider = tmpCollider;
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool CheckStepValidity(int nbStepHits, Vector3 characterPosition, Quaternion characterRotation, Vector3 innerHitDirection, Vector3 stepCheckStartPos, out Collider hitCollider)
|
|
{
|
|
hitCollider = null;
|
|
Vector3 characterUp = characterRotation * Vector3.up;
|
|
|
|
// Find the farthest valid hit for stepping
|
|
bool foundValidStepPosition = false;
|
|
|
|
while (nbStepHits > 0 && !foundValidStepPosition)
|
|
{
|
|
// Get farthest hit among the remaining hits
|
|
RaycastHit farthestHit = new RaycastHit();
|
|
float farthestDistance = 0f;
|
|
int farthestIndex = 0;
|
|
for (int i = 0; i < nbStepHits; i++)
|
|
{
|
|
float hitDistance = _internalCharacterHits[i].distance;
|
|
if (hitDistance > farthestDistance)
|
|
{
|
|
farthestDistance = hitDistance;
|
|
farthestHit = _internalCharacterHits[i];
|
|
farthestIndex = i;
|
|
}
|
|
}
|
|
|
|
Vector3 characterPositionAtHit = stepCheckStartPos + (-characterUp * (farthestHit.distance - CollisionOffset));
|
|
|
|
int atStepOverlaps = CharacterCollisionsOverlap(characterPositionAtHit, characterRotation, _internalProbedColliders);
|
|
if (atStepOverlaps <= 0)
|
|
{
|
|
// Check for outer hit slope normal stability at the step position
|
|
if (CharacterCollisionsRaycast(
|
|
farthestHit.point + (characterUp * SecondaryProbesVertical) + (-innerHitDirection * SecondaryProbesHorizontal),
|
|
-characterUp,
|
|
MaxStepHeight + SecondaryProbesVertical,
|
|
out RaycastHit outerSlopeHit,
|
|
_internalCharacterHits,
|
|
true) > 0)
|
|
{
|
|
if (IsStableOnNormal(outerSlopeHit.normal))
|
|
{
|
|
// Cast upward to detect any obstructions to moving there
|
|
if (CharacterCollisionsSweep(
|
|
characterPosition, // position
|
|
characterRotation, // rotation
|
|
characterUp, // direction
|
|
MaxStepHeight - farthestHit.distance, // distance
|
|
out RaycastHit tmpUpObstructionHit, // closest hit
|
|
_internalCharacterHits) // all hits
|
|
<= 0)
|
|
{
|
|
// Do inner step check...
|
|
bool innerStepValid = false;
|
|
RaycastHit innerStepHit;
|
|
|
|
if (AllowSteppingWithoutStableGrounding)
|
|
{
|
|
innerStepValid = true;
|
|
}
|
|
else
|
|
{
|
|
// At the capsule center at the step height
|
|
if (CharacterCollisionsRaycast(
|
|
characterPosition + Vector3.Project((characterPositionAtHit - characterPosition), characterUp),
|
|
-characterUp,
|
|
MaxStepHeight,
|
|
out innerStepHit,
|
|
_internalCharacterHits,
|
|
true) > 0)
|
|
{
|
|
if (IsStableOnNormal(innerStepHit.normal))
|
|
{
|
|
innerStepValid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!innerStepValid)
|
|
{
|
|
// At inner step of the step point
|
|
if (CharacterCollisionsRaycast(
|
|
farthestHit.point + (innerHitDirection * SecondaryProbesHorizontal),
|
|
-characterUp,
|
|
MaxStepHeight,
|
|
out innerStepHit,
|
|
_internalCharacterHits,
|
|
true) > 0)
|
|
{
|
|
if (IsStableOnNormal(innerStepHit.normal))
|
|
{
|
|
innerStepValid = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Final validation of step
|
|
if (innerStepValid)
|
|
{
|
|
hitCollider = farthestHit.collider;
|
|
foundValidStepPosition = true;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Discard hit if not valid step
|
|
if (!foundValidStepPosition)
|
|
{
|
|
nbStepHits--;
|
|
if (farthestIndex < nbStepHits)
|
|
{
|
|
_internalCharacterHits[farthestIndex] = _internalCharacterHits[nbStepHits];
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get true linear velocity (taking into account rotational velocity) on a given point of a rigidbody
|
|
/// </summary>
|
|
public void GetVelocityFromRigidbodyMovement(Rigidbody interactiveRigidbody, Vector3 atPoint, float deltaTime, out Vector3 linearVelocity, out Vector3 angularVelocity)
|
|
{
|
|
if (deltaTime > 0f)
|
|
{
|
|
linearVelocity = interactiveRigidbody.velocity;
|
|
angularVelocity = interactiveRigidbody.angularVelocity;
|
|
if(interactiveRigidbody.isKinematic)
|
|
{
|
|
PhysicsMover physicsMover = interactiveRigidbody.GetComponent<PhysicsMover>();
|
|
if (physicsMover)
|
|
{
|
|
linearVelocity = physicsMover.Velocity;
|
|
angularVelocity = physicsMover.AngularVelocity;
|
|
}
|
|
}
|
|
|
|
if (angularVelocity != Vector3.zero)
|
|
{
|
|
Vector3 centerOfRotation = interactiveRigidbody.transform.TransformPoint(interactiveRigidbody.centerOfMass);
|
|
|
|
Vector3 centerOfRotationToPoint = atPoint - centerOfRotation;
|
|
Quaternion rotationFromInteractiveRigidbody = Quaternion.Euler(Mathf.Rad2Deg * angularVelocity * deltaTime);
|
|
Vector3 finalPointPosition = centerOfRotation + (rotationFromInteractiveRigidbody * centerOfRotationToPoint);
|
|
linearVelocity += (finalPointPosition - atPoint) / deltaTime;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
linearVelocity = default;
|
|
angularVelocity = default;
|
|
return;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a collider has an attached interactive rigidbody
|
|
/// </summary>
|
|
private Rigidbody GetInteractiveRigidbody(Collider onCollider)
|
|
{
|
|
Rigidbody colliderAttachedRigidbody = onCollider.attachedRigidbody;
|
|
if (colliderAttachedRigidbody)
|
|
{
|
|
if (colliderAttachedRigidbody.gameObject.GetComponent<PhysicsMover>())
|
|
{
|
|
return colliderAttachedRigidbody;
|
|
}
|
|
|
|
if (!colliderAttachedRigidbody.isKinematic)
|
|
{
|
|
return colliderAttachedRigidbody;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the velocity required to move the character to the target position over a specific deltaTime.
|
|
/// Useful for when you wish to work with positions rather than velocities in the UpdateVelocity callback
|
|
/// </summary>
|
|
public Vector3 GetVelocityForMovePosition(Vector3 fromPosition, Vector3 toPosition, float deltaTime)
|
|
{
|
|
return GetVelocityFromMovement(toPosition - fromPosition, deltaTime);
|
|
}
|
|
|
|
public Vector3 GetVelocityFromMovement(Vector3 movement, float deltaTime)
|
|
{
|
|
if (deltaTime <= 0f)
|
|
return Vector3.zero;
|
|
|
|
return movement / deltaTime;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trims a vector to make it restricted against a plane
|
|
/// </summary>
|
|
private void RestrictVectorToPlane(ref Vector3 vector, Vector3 toPlane)
|
|
{
|
|
if (vector.x > 0 != toPlane.x > 0)
|
|
{
|
|
vector.x = 0;
|
|
}
|
|
if (vector.y > 0 != toPlane.y > 0)
|
|
{
|
|
vector.y = 0;
|
|
}
|
|
if (vector.z > 0 != toPlane.z > 0)
|
|
{
|
|
vector.z = 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detect if the character capsule is overlapping with anything collidable
|
|
/// </summary>
|
|
/// <returns> Returns number of overlaps </returns>
|
|
public int CharacterCollisionsOverlap(Vector3 position, Quaternion rotation, Collider[] overlappedColliders, float inflate = 0f, bool acceptOnlyStableGroundLayer = false)
|
|
{
|
|
int queryLayers = CollidableLayers;
|
|
if (acceptOnlyStableGroundLayer)
|
|
{
|
|
queryLayers = CollidableLayers & StableGroundLayers;
|
|
}
|
|
|
|
Vector3 bottom = position + (rotation * _characterTransformToCapsuleBottomHemi);
|
|
Vector3 top = position + (rotation * _characterTransformToCapsuleTopHemi);
|
|
if (inflate != 0f)
|
|
{
|
|
bottom += (rotation * Vector3.down * inflate);
|
|
top += (rotation * Vector3.up * inflate);
|
|
}
|
|
|
|
int nbHits = 0;
|
|
int nbUnfilteredHits = Physics.OverlapCapsuleNonAlloc(
|
|
bottom,
|
|
top,
|
|
Capsule.radius + inflate,
|
|
overlappedColliders,
|
|
queryLayers,
|
|
QueryTriggerInteraction.Ignore);
|
|
|
|
// Filter out invalid colliders
|
|
nbHits = nbUnfilteredHits;
|
|
for (int i = nbUnfilteredHits - 1; i >= 0; i--)
|
|
{
|
|
if (!CheckIfColliderValidForCollisions(overlappedColliders[i]))
|
|
{
|
|
nbHits--;
|
|
if (i < nbHits)
|
|
{
|
|
overlappedColliders[i] = overlappedColliders[nbHits];
|
|
}
|
|
}
|
|
}
|
|
|
|
return nbHits;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Detect if the character capsule is overlapping with anything
|
|
/// </summary>
|
|
/// <returns> Returns number of overlaps </returns>
|
|
public int CharacterOverlap(Vector3 position, Quaternion rotation, Collider[] overlappedColliders, LayerMask layers, QueryTriggerInteraction triggerInteraction, float inflate = 0f)
|
|
{
|
|
Vector3 bottom = position + (rotation * _characterTransformToCapsuleBottomHemi);
|
|
Vector3 top = position + (rotation * _characterTransformToCapsuleTopHemi);
|
|
if (inflate != 0f)
|
|
{
|
|
bottom += (rotation * Vector3.down * inflate);
|
|
top += (rotation * Vector3.up * inflate);
|
|
}
|
|
|
|
int nbHits = 0;
|
|
int nbUnfilteredHits = Physics.OverlapCapsuleNonAlloc(
|
|
bottom,
|
|
top,
|
|
Capsule.radius + inflate,
|
|
overlappedColliders,
|
|
layers,
|
|
triggerInteraction);
|
|
|
|
// Filter out the character capsule itself
|
|
nbHits = nbUnfilteredHits;
|
|
for (int i = nbUnfilteredHits - 1; i >= 0; i--)
|
|
{
|
|
if (overlappedColliders[i] == Capsule)
|
|
{
|
|
nbHits--;
|
|
if (i < nbHits)
|
|
{
|
|
overlappedColliders[i] = overlappedColliders[nbHits];
|
|
}
|
|
}
|
|
}
|
|
|
|
return nbHits;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sweeps the capsule's volume to detect collision hits
|
|
/// </summary>
|
|
/// <returns> Returns the number of hits </returns>
|
|
public int CharacterCollisionsSweep(Vector3 position, Quaternion rotation, Vector3 direction, float distance, out RaycastHit closestHit, RaycastHit[] hits, float inflate = 0f, bool acceptOnlyStableGroundLayer = false)
|
|
{
|
|
int queryLayers = CollidableLayers;
|
|
if (acceptOnlyStableGroundLayer)
|
|
{
|
|
queryLayers = CollidableLayers & StableGroundLayers;
|
|
}
|
|
|
|
Vector3 bottom = position + (rotation * _characterTransformToCapsuleBottomHemi) - (direction * SweepProbingBackstepDistance);
|
|
Vector3 top = position + (rotation * _characterTransformToCapsuleTopHemi) - (direction * SweepProbingBackstepDistance);
|
|
if (inflate != 0f)
|
|
{
|
|
bottom += (rotation * Vector3.down * inflate);
|
|
top += (rotation * Vector3.up * inflate);
|
|
}
|
|
|
|
// Capsule cast
|
|
int nbHits = 0;
|
|
int nbUnfilteredHits = Physics.CapsuleCastNonAlloc(
|
|
bottom,
|
|
top,
|
|
Capsule.radius + inflate,
|
|
direction,
|
|
hits,
|
|
distance + SweepProbingBackstepDistance,
|
|
queryLayers,
|
|
QueryTriggerInteraction.Ignore);
|
|
|
|
// Hits filter
|
|
closestHit = new RaycastHit();
|
|
float closestDistance = Mathf.Infinity;
|
|
nbHits = nbUnfilteredHits;
|
|
for (int i = nbUnfilteredHits - 1; i >= 0; i--)
|
|
{
|
|
hits[i].distance -= SweepProbingBackstepDistance;
|
|
|
|
RaycastHit hit = hits[i];
|
|
float hitDistance = hit.distance;
|
|
|
|
// Filter out the invalid hits
|
|
if (hitDistance <= 0f || !CheckIfColliderValidForCollisions(hit.collider))
|
|
{
|
|
nbHits--;
|
|
if (i < nbHits)
|
|
{
|
|
hits[i] = hits[nbHits];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Remember closest valid hit
|
|
if (hitDistance < closestDistance)
|
|
{
|
|
closestHit = hit;
|
|
closestDistance = hitDistance;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nbHits;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sweeps the capsule's volume to detect hits
|
|
/// </summary>
|
|
/// <returns> Returns the number of hits </returns>
|
|
public int CharacterSweep(Vector3 position, Quaternion rotation, Vector3 direction, float distance, out RaycastHit closestHit, RaycastHit[] hits, LayerMask layers, QueryTriggerInteraction triggerInteraction, float inflate = 0f)
|
|
{
|
|
closestHit = new RaycastHit();
|
|
|
|
Vector3 bottom = position + (rotation * _characterTransformToCapsuleBottomHemi);
|
|
Vector3 top = position + (rotation * _characterTransformToCapsuleTopHemi);
|
|
if (inflate != 0f)
|
|
{
|
|
bottom += (rotation * Vector3.down * inflate);
|
|
top += (rotation * Vector3.up * inflate);
|
|
}
|
|
|
|
// Capsule cast
|
|
int nbHits = 0;
|
|
int nbUnfilteredHits = Physics.CapsuleCastNonAlloc(
|
|
bottom,
|
|
top,
|
|
Capsule.radius + inflate,
|
|
direction,
|
|
hits,
|
|
distance,
|
|
layers,
|
|
triggerInteraction);
|
|
|
|
// Hits filter
|
|
float closestDistance = Mathf.Infinity;
|
|
nbHits = nbUnfilteredHits;
|
|
for (int i = nbUnfilteredHits - 1; i >= 0; i--)
|
|
{
|
|
RaycastHit hit = hits[i];
|
|
|
|
// Filter out the character capsule
|
|
if (hit.distance <= 0f || hit.collider == Capsule)
|
|
{
|
|
nbHits--;
|
|
if (i < nbHits)
|
|
{
|
|
hits[i] = hits[nbHits];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Remember closest valid hit
|
|
float hitDistance = hit.distance;
|
|
if (hitDistance < closestDistance)
|
|
{
|
|
closestHit = hit;
|
|
closestDistance = hitDistance;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nbHits;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Casts the character volume in the character's downward direction to detect ground
|
|
/// </summary>
|
|
/// <returns> Returns the number of hits </returns>
|
|
private bool CharacterGroundSweep(Vector3 position, Quaternion rotation, Vector3 direction, float distance, out RaycastHit closestHit)
|
|
{
|
|
closestHit = new RaycastHit();
|
|
|
|
// Capsule cast
|
|
int nbUnfilteredHits = Physics.CapsuleCastNonAlloc(
|
|
position + (rotation * _characterTransformToCapsuleBottomHemi) - (direction * GroundProbingBackstepDistance),
|
|
position + (rotation * _characterTransformToCapsuleTopHemi) - (direction * GroundProbingBackstepDistance),
|
|
Capsule.radius,
|
|
direction,
|
|
_internalCharacterHits,
|
|
distance + GroundProbingBackstepDistance,
|
|
CollidableLayers & StableGroundLayers,
|
|
QueryTriggerInteraction.Ignore);
|
|
|
|
// Hits filter
|
|
bool foundValidHit = false;
|
|
float closestDistance = Mathf.Infinity;
|
|
for (int i = 0; i < nbUnfilteredHits; i++)
|
|
{
|
|
RaycastHit hit = _internalCharacterHits[i];
|
|
float hitDistance = hit.distance;
|
|
|
|
// Find the closest valid hit
|
|
if (hitDistance > 0f && CheckIfColliderValidForCollisions(hit.collider))
|
|
{
|
|
if (hitDistance < closestDistance)
|
|
{
|
|
closestHit = hit;
|
|
closestHit.distance -= GroundProbingBackstepDistance;
|
|
closestDistance = hitDistance;
|
|
|
|
foundValidHit = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return foundValidHit;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Raycasts to detect collision hits
|
|
/// </summary>
|
|
/// <returns> Returns the number of hits </returns>
|
|
public int CharacterCollisionsRaycast(Vector3 position, Vector3 direction, float distance, out RaycastHit closestHit, RaycastHit[] hits, bool acceptOnlyStableGroundLayer = false)
|
|
{
|
|
int queryLayers = CollidableLayers;
|
|
if (acceptOnlyStableGroundLayer)
|
|
{
|
|
queryLayers = CollidableLayers & StableGroundLayers;
|
|
}
|
|
|
|
// Raycast
|
|
int nbHits = 0;
|
|
int nbUnfilteredHits = Physics.RaycastNonAlloc(
|
|
position,
|
|
direction,
|
|
hits,
|
|
distance,
|
|
queryLayers,
|
|
QueryTriggerInteraction.Ignore);
|
|
|
|
// Hits filter
|
|
closestHit = new RaycastHit();
|
|
float closestDistance = Mathf.Infinity;
|
|
nbHits = nbUnfilteredHits;
|
|
for (int i = nbUnfilteredHits - 1; i >= 0; i--)
|
|
{
|
|
RaycastHit hit = hits[i];
|
|
float hitDistance = hit.distance;
|
|
|
|
// Filter out the invalid hits
|
|
if (hitDistance <= 0f ||
|
|
!CheckIfColliderValidForCollisions(hit.collider))
|
|
{
|
|
nbHits--;
|
|
if (i < nbHits)
|
|
{
|
|
hits[i] = hits[nbHits];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Remember closest valid hit
|
|
if (hitDistance < closestDistance)
|
|
{
|
|
closestHit = hit;
|
|
closestDistance = hitDistance;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nbHits;
|
|
}
|
|
}
|
|
} |