Files
BABA_YAGA/Assets/Opsive/UltimateCharacterController/Scripts/Character/CharacterLocomotion.cs
2026-06-09 02:05:00 +07:00

2120 lines
126 KiB
C#

/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Character
{
using Opsive.Shared.Game;
using Opsive.Shared.Utility;
using Opsive.UltimateCharacterController.Game;
using Opsive.UltimateCharacterController.Objects;
using Opsive.UltimateCharacterController.Utility;
using Opsive.UltimateCharacterController.StateSystem;
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// The CharacterLocomotion class serves as the base character controller class. It handles the base locomotion with the following features:
/// - Movement
/// - Collision Detection
/// - Root Motion
/// - Wall Bouncing
/// - Wall Gliding
/// - Slopes
/// - Stairs
/// - Push Rigidbodies
/// - Gravity Direction
/// - Variable Time Scale
/// - CapsuleCollider and SphereCollider support (for generic characters)
/// - Moving Platforms
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public abstract class CharacterLocomotion : StateBehavior, IForceObject
{
// Padding value used to prevent the character's collider from overlapping the environment collider. Overlapped colliders don't work well with ray casts.
private const float c_ColliderSpacing = 0.01f;
private const float c_ColliderSpacingCubed = c_ColliderSpacing * c_ColliderSpacing * c_ColliderSpacing * c_ColliderSpacing;
// Padding value used to add a bit of spacing for the slope limit.
private const float c_SlopeLimitSpacing = 0.3f;
[Tooltip("Should root motion be used to move the character?")]
[SerializeField] protected bool m_UseRootMotionPosition;
[Tooltip("If using root motion, applies a multiplier to the root motion delta position while on the ground.")]
[SerializeField] protected float m_RootMotionSpeedMultiplier = 1;
[Tooltip("If using root motion, applies a multiplier to the root motion delta position while in the air.")]
[SerializeField] protected float m_RootMotionAirForceMultiplier = 1;
[Tooltip("Should root motion be used to rotate the character?")]
[SerializeField] protected bool m_UseRootMotionRotation;
[Tooltip("If using root motion, applies a multiplier to the root motion delta rotation.")]
[SerializeField] protected float m_RootMotionRotationMultiplier = 1;
[Tooltip("The rate at which the character can rotate. Only used by non-root motion characters.")]
[SerializeField] protected float m_MotorRotationSpeed = 10;
[Tooltip("The mass of the character.")]
[SerializeField] protected float m_Mass = 100;
[Tooltip("Specifies the width of the characters skin, use for ground detection.")]
[SerializeField] protected float m_SkinWidth = 0.08f;
[Tooltip("The maximum object slope angle that the character can traverse (in degrees).")]
[SerializeField] protected float m_SlopeLimit = 40f;
[Tooltip("The maximum height that the character can step on top of.")]
[SerializeField] protected float m_MaxStepHeight = 0.35f;
[Tooltip("The rate at which the character's motor force accelerates while on the ground. Only used by non-root motion characters.")]
[SerializeField] protected Vector3 m_MotorAcceleration = new Vector3(0.18f, 0, 0.18f);
[Tooltip("The rate at which the character's motor force decelerates while on the ground. Only used by non-root motion characters.")]
[SerializeField] protected float m_MotorDamping = 0.27f;
[Tooltip("The rate at which the character's motor force accelerates while in the air. Only used by non-root motion characters.")]
[SerializeField] protected Vector3 m_MotorAirborneAcceleration = new Vector3(0.18f, 0, 0.18f);
[Tooltip("The rate at which the character's motor force decelerates while in the air. Only used by non-root motion characters.")]
[SerializeField] protected float m_MotorAirborneDamping = 0.01f;
[Tooltip("A multiplier which is applied to the motor while moving backwards.")]
[SerializeField] protected float m_MotorBackwardsMultiplier = 0.7f;
[Tooltip("A (0-1) value specifying the amount of influence the previous acceleration direction has on the current velocity.")]
[SerializeField] protected float m_PreviousAccelerationInfluence = 1;
[Tooltip("Should the motor force be adjusted while on a slope?")]
[SerializeField] protected bool m_AdjustMotorForceOnSlope = true;
[Tooltip("If adjusting the motor force on a slope, the force multiplier when on an upward slope.")]
[SerializeField] protected float m_MotorSlopeForceUp = 1f;
[Tooltip("If adjusting the motor force on a slope, the force multiplier when on a downward slope.")]
[SerializeField] protected float m_MotorSlopeForceDown = 1.25f;
[Tooltip("The rate at which the character's external force decelerates.")]
[SerializeField] protected float m_ExternalForceDamping = 0.1f;
[Tooltip("The rate at which the character's external force decelerates while in the air.")]
[SerializeField] protected float m_ExternalForceAirDamping = 0.05f;
[Tooltip("Should the character stick to the ground?")]
[SerializeField] protected bool m_StickToGround = true;
[Tooltip("If the character is sticking to the ground, specifies how sticky the ground is. A higher value means the ground is more sticky.")]
[SerializeField] protected float m_Stickiness = 0.2f;
[Tooltip("The local time scale of the character.")]
[SerializeField] protected float m_TimeScale = 1;
[Tooltip("Should gravity be applied?")]
[SerializeField] protected bool m_UseGravity = true;
[Tooltip("The normalized direction of the gravity force.")]
[SerializeField] protected Vector3 m_GravityDirection = new Vector3(0, -1, 0);
[Tooltip("A amount of gravity force to apply.")]
[SerializeField] protected float m_GravityMagnitude = 3.92f;
[Tooltip("Can the character detect horizontal collisions?")]
[SerializeField] protected bool m_DetectHorizontalCollisions = true;
[Tooltip("Can the character detect vertical collisions?")]
[SerializeField] protected bool m_DetectVerticalCollisions = true;
[Tooltip("The layers that can act as colliders for the character.")]
[SerializeField] protected LayerMask m_ColliderLayerMask = ~(1 << LayerManager.IgnoreRaycast | 1 << LayerManager.SubCharacter |
1 << LayerManager.Overlay | 1 << LayerManager.VisualEffect);
[Tooltip("The maximum number of colliders that the character can detect.")]
[SerializeField] protected int m_MaxCollisionCount = 100;
[Tooltip("The maximum number of frames that the soft force can be distributed by.")]
[SerializeField] protected int m_MaxSoftForceFrames = 100;
[Tooltip("The maximum number of collision checks that should be performed when rotating.")]
[SerializeField] protected int m_RotationCollisionCheckCount = 10;
[Tooltip("The maximum number of iterations to detect collision overlaps.")]
[SerializeField] protected int m_MaxOverlapIterations = 6;
[Tooltip("A curve specifying the amount to move when gliding along a wall. The x variable represents the dot product between the character look direction and wall normal. " +
"An x value of 0 means the character is looking directly at the wall. An x value of 1 indicates the character is looking parallel to the wall.")]
[SerializeField] protected AnimationCurve m_WallGlideCurve = new AnimationCurve(new Keyframe[] { new Keyframe(0, 0), new Keyframe(0.1f, 1.5f, 0, 0), new Keyframe(1, 1.5f) });
[Tooltip("A multiplier to apply when hitting a wall. Allows for the character to bounce off of a wall.")]
[SerializeField] protected float m_WallBounceModifier = 1;
[Tooltip("Should the character stick to the moving platform? If false the character will inherit the moving platform's momentum when the platform stops quickly.")]
[SerializeField] protected bool m_StickToMovingPlatform = true;
[Tooltip("The velocity magnitude required for the character to separate from the moving platform due to a sudden moving platform stop.")]
[SerializeField] protected float m_MovingPlatformSeperationVelocity = 5;
[Tooltip("The maximum speed of the platform that the character should stick when the platform collides with the character from a horizontal position.")]
[SerializeField] protected float m_MinHorizontalMovingPlatformStickSpeed = 2;
[Tooltip("The rate at which the character's moving platform force decelerates when the character is no longer on the platform.")]
[SerializeField] protected float m_MovingPlatformForceDamping;
[Tooltip("An array of bones that should be smoothed by the Kinematic Object Manager.")]
[SerializeField] protected Transform[] m_SmoothedBones;
public float ColliderSpacing { get { return c_ColliderSpacing; } }
public float ColliderSpacingCubed { get { return c_ColliderSpacingCubed; } }
public float SlopeLimitSpacing { get { return c_SlopeLimitSpacing; } }
public bool UseRootMotionPosition { get { return m_UseRootMotionPosition; } set { m_UseRootMotionPosition = value; } }
public float RootMotionSpeedMultiplier { get { return m_RootMotionSpeedMultiplier; } set { m_RootMotionSpeedMultiplier = value; } }
public float RootMotionAirForceMultiplier { get { return m_RootMotionAirForceMultiplier; } set { m_RootMotionAirForceMultiplier = value; } }
public bool UseRootMotionRotation { get { return m_UseRootMotionRotation; } set { m_UseRootMotionRotation = value; } }
public float RootMotionRotationMultiplier { get { return m_RootMotionRotationMultiplier; } set { m_RootMotionRotationMultiplier = value; } }
public float MotorRotationSpeed { get { return m_MotorRotationSpeed; } set { m_MotorRotationSpeed = value; } }
public float Mass { get { return m_Mass; } set { m_Mass = value; } }
public float SkinWidth { get { return m_SkinWidth; } set { m_SkinWidth = value; } }
public float SlopeLimit { get { return m_SlopeLimit; } set { m_SlopeLimit = value; } }
public float MaxStepHeight { get { return m_MaxStepHeight; } set { m_MaxStepHeight = value; } }
public Vector3 MotorAcceleration { get { return m_MotorAcceleration; } set { m_MotorAcceleration = value; } }
public float MotorDamping { get { return m_MotorDamping; } set { m_MotorDamping = value; } }
public Vector3 MotorAirborneAcceleration { get { return m_MotorAirborneAcceleration; } set { m_MotorAirborneAcceleration = value; } }
public float MotorAirborneDamping { get { return m_MotorAirborneDamping; } set { m_MotorAirborneDamping = value; } }
public float MotorBackwardsMultiplier { get { return m_MotorBackwardsMultiplier; } set { m_MotorBackwardsMultiplier = value; } }
public float PreviousAccelerationInfluence { get { return m_PreviousAccelerationInfluence; } set { m_PreviousAccelerationInfluence = value; } }
public bool AdjustMotorForceOnSlope { get { return m_AdjustMotorForceOnSlope; } set { m_AdjustMotorForceOnSlope = value; } }
public float MotorSlopeForceUp { get { return m_MotorSlopeForceUp; } set { m_MotorSlopeForceUp = value; } }
public float MotorSlopeForceDown { get { return m_MotorSlopeForceDown; } set { m_MotorSlopeForceDown = value; } }
public float ExternalForceDamping { get { return m_ExternalForceDamping; } set { m_ExternalForceDamping = value; } }
public float ExternalForceAirDamping { get { return m_ExternalForceAirDamping; } set { m_ExternalForceAirDamping = value; } }
public bool StickToGround { get { return m_StickToGround; } set { m_StickToGround = value; } }
public float Stickiness { get { return m_Stickiness; } set { m_Stickiness = value; } }
public virtual float TimeScale { get { return m_TimeScale; } set { m_TimeScale = value; } }
public bool UseGravity { get { return m_UseGravity; } set { m_UseGravity = value; } }
public Vector3 GravityDirection { get { return m_GravityDirection; } set { m_GravityDirection = value; m_GravityDirection.Normalize(); } }
public float GravityMagnitude { get { return m_GravityMagnitude; } set { m_GravityMagnitude = value; } }
public bool DetectHorizontalCollisions { get { return m_DetectHorizontalCollisions; } set { m_DetectHorizontalCollisions = value; } }
public bool DetectVerticalCollisions { get { return m_DetectVerticalCollisions; } set { m_DetectVerticalCollisions = value; } }
public LayerMask ColliderLayerMask { get { return m_ColliderLayerMask; } set { m_ColliderLayerMask = value; } }
public int MaxCollisionCount { get { return m_MaxCollisionCount; } }
public int MaxSoftForceFrames { get { return m_MaxSoftForceFrames; } }
public int RotationCollisionCheckCount { get { return m_RotationCollisionCheckCount; } set { m_RotationCollisionCheckCount = value; } }
public int MaxOverlapIterations { get { return m_MaxOverlapIterations; } set { m_MaxOverlapIterations = value; } }
public AnimationCurve WallGlideCurve { get { return m_WallGlideCurve; } set { m_WallGlideCurve = value; } }
public float WallBounceModifier { get { return m_WallBounceModifier; } set { m_WallBounceModifier = value; } }
public bool StickToMovingPlatform { get { return m_StickToMovingPlatform; } set { m_StickToMovingPlatform = value; } }
public float MovingPlatformSeperationVelocity { get { return m_MovingPlatformSeperationVelocity; } set { m_MovingPlatformSeperationVelocity = value; } }
public float MinHorizontalMovingPlatformStickSpeed { get { return m_MinHorizontalMovingPlatformStickSpeed; } set { m_MinHorizontalMovingPlatformStickSpeed = value; } }
public float MovingPlatformForceDamping { get { return m_MovingPlatformForceDamping; } set { m_MovingPlatformForceDamping = value; } }
public Transform[] SmoothedBones { get { return m_SmoothedBones; } }
protected Transform m_Transform;
private Rigidbody m_Rigidbody;
protected AnimatorMonitor m_AnimatorMonitor;
protected Collider[] m_Colliders;
private Collider[] m_IgnoredColliders;
protected int m_ColliderCount;
private int m_IgnoredColliderCount;
private GameObject[] m_ColliderGameObjects;
private GameObject[] m_IgnoredColliderGameObjects;
protected CharacterLayerManager m_CharacterLayerManager;
private RaycastHit m_GroundRaycastHit;
private Vector3 m_GroundRaycastOrigin;
private Transform m_GroundHitTransform;
private RaycastHit m_HorizontalRaycastHit;
protected RaycastHit[] m_RaycastHits;
private RaycastHit[] m_CombinedRaycastHits;
private Collider[] m_OverlapColliderHit;
private RaycastHit m_BlankRaycastHit = new RaycastHit();
private Ray m_SlopeRay = new Ray();
private RaycastHit m_RaycastHit;
private Vector3 m_Up;
private float m_Height;
private float m_Radius = float.MaxValue;
private Vector3 m_MotorThrottle;
private Quaternion m_MotorRotation;
private Quaternion m_PrevMotorRotation;
private Vector3 m_MoveDirection;
protected Quaternion m_Torque = Quaternion.identity;
private Vector3 m_PrevPosition;
private Vector3 m_Velocity;
private bool m_CheckRotationCollision;
private bool m_AllowUseGravity = true;
private bool m_ForceUseGravity;
private bool m_AllowRootMotionPosition = true;
private bool m_AllowRootMotionRotation = true;
private bool m_ForceRootMotionPosition;
private bool m_ForceRootMotionRotation;
private bool m_AllowHorizontalCollisionDetection = true;
private bool m_ForceHorizontalCollisionDetection;
private bool m_AllowVerticalCollisionDetection = true;
private bool m_ForceVerticalCollisionDetection;
protected Vector3 m_AnimatorDeltaPosition;
protected Quaternion m_AnimatorDeltaRotation;
private Vector3 m_LocalRootMotionForce;
private Quaternion m_LocalRootMotionRotation = Quaternion.identity;
private float m_SlopeFactor = 1;
private Vector3 m_ExternalForce;
private float m_GravityAmount;
private Vector3[] m_SoftForceFrames;
protected UnityEngineUtility.RaycastHitComparer m_RaycastHitComparer = new UnityEngineUtility.RaycastHitComparer();
private Dictionary<RaycastHit, int> m_ColliderIndexMap;
private int[] m_ColliderLayers;
private int[] m_IgnoredColliderLayers;
protected Vector2 m_InputVector;
protected Vector3 m_DeltaRotation;
protected bool m_Grounded = true;
private bool m_CollisionLayerEnabled = true;
private bool m_AlignToGravity;
private bool m_SmoothGravityYawDelta = true;
private bool m_ManualMove;
protected Transform m_Platform;
private Vector3 m_PlatformRelativePosition;
private Quaternion m_PlatformRotationOffset;
protected Quaternion m_PlatformTorque = Quaternion.identity;
private Vector3 m_PlatformMovement;
private Vector3 m_PlatformVelocity;
private bool m_GroundedMovingPlatform;
private bool m_PlatformOverride;
public Collider[] Colliders { get { return m_Colliders; } }
public Collider[] IgnoredColliders { get { return m_IgnoredColliders; } }
public int ColliderCount { get { return m_ColliderCount; } }
public bool AllowUseGravity { set { m_AllowUseGravity = value; } }
public bool ForceUseGravity { set { m_ForceUseGravity = value; } }
public bool UsingGravity { get { return (m_UseGravity || m_ForceUseGravity) && m_AllowUseGravity; } }
public bool AllowRootMotionPosition { set { m_AllowRootMotionPosition = value; } }
public bool ForceRootMotionPosition { set { m_ForceRootMotionPosition = value; } }
public bool UsingRootMotionPosition { get { return (m_UseRootMotionPosition || m_ForceRootMotionPosition) && m_AllowRootMotionPosition; } }
public bool AllowRootMotionRotation { set { m_AllowRootMotionRotation = value; } }
public bool ForceRootMotionRotation { set { m_ForceRootMotionRotation = value; } }
public bool UsingRootMotionRotation { get { return (m_UseRootMotionRotation || m_ForceRootMotionRotation) && m_AllowRootMotionRotation; } }
public Vector3 AnimatorDeltaPosition { get { return m_AnimatorDeltaPosition; } set { m_AnimatorDeltaPosition = value; } }
public Quaternion AnimatorDeltaRotation { get { return m_AnimatorDeltaRotation; } set { m_AnimatorDeltaRotation = value; } }
public bool AllowHorizontalCollisionDetection { set { m_AllowHorizontalCollisionDetection = value; } }
public bool ForceHorizontalCollisionDetection { set { m_ForceHorizontalCollisionDetection = value; } }
public bool UsingHorizontalCollisionDetection { get { return (m_DetectHorizontalCollisions || m_ForceHorizontalCollisionDetection) && m_AllowHorizontalCollisionDetection; } }
public bool AllowVerticalCollisionDetection { set { m_AllowVerticalCollisionDetection = value; } }
public bool ForceVerticalCollisionDetection { set { m_ForceVerticalCollisionDetection = value; } }
public bool UsingVerticalCollisionDetection { get { return (m_DetectVerticalCollisions || m_ForceVerticalCollisionDetection) && m_AllowVerticalCollisionDetection; } }
[Shared.Utility.NonSerialized] public Vector3 MoveDirection { get { return m_MoveDirection; } set { m_MoveDirection = value; } }
[Shared.Utility.NonSerialized] public Quaternion Torque { get { return m_Torque; } set { m_Torque = value; } }
public Vector3 LocomotionVelocity { get { return m_Velocity - m_PlatformVelocity; } protected set { m_Velocity = value; } }
public Vector3 LocalLocomotionVelocity { get { return m_Transform.InverseTransformDirection(m_Velocity - m_PlatformVelocity); } }
[Snapshot] public Vector3 Up { get { return m_Up; } protected set { m_Up = value; } }
public float Height { get { return m_Height; } }
public float Radius { get { return m_Radius; } }
public Vector3 Center { get { return m_Transform.InverseTransformPoint(m_Transform.position + (m_Up * m_Height / 2)); } }
[Snapshot] public bool Grounded { get { return m_Grounded; } protected set { m_Grounded = value; } }
public Vector3 LocalExternalForce { get { return m_Transform.InverseTransformDirection(m_ExternalForce); } }
public RaycastHit GroundRaycastHit { get { return m_GroundRaycastHit; } }
public RaycastHit HorizontalRaycastHit { get { return m_HorizontalRaycastHit; } }
public float TimeScaleSquared { get { return m_TimeScale * m_TimeScale; } }
public Transform Platform { get { return m_Platform; } }
public Vector3 PlatformMovement { get { return m_PlatformMovement; } }
public Quaternion PlatformTorque { get { return m_PlatformTorque; } }
public bool CollisionLayerEnabled { get { return m_CollisionLayerEnabled; } }
[Opsive.Shared.Utility.NonSerialized] public bool AlignToGravity { get { return m_AlignToGravity; } set { m_AlignToGravity = value; } }
[Opsive.Shared.Utility.NonSerialized] public bool SmoothGravityYawDelta { get { return m_SmoothGravityYawDelta; } set { m_SmoothGravityYawDelta = value; } }
[Opsive.Shared.Utility.NonSerialized] public bool ManualMove { get { return m_ManualMove; } set { m_ManualMove = value; } }
[Snapshot] public Vector3 PlatformVelocity { get { return m_PlatformVelocity; } set { m_PlatformVelocity = value; } }
[Snapshot] protected float SlopeFactor { get { return m_SlopeFactor; } set { m_SlopeFactor = value; } }
// The Velocity property will return the combined character locomotion and platform velocity.
// For only the character's locmotion velocity use the LocomotionVelocity property.
[Opsive.Shared.Utility.NonSerialized] public Vector3 Velocity { get { return m_Velocity; } set { m_Velocity = value; } }
public Vector3 LocalVelocity { get { return m_Transform.InverseTransformDirection(m_Velocity); } }
[Snapshot] public Vector3 MotorThrottle { get { return m_MotorThrottle; } set { m_MotorThrottle = value; } }
[Snapshot] public Quaternion MotorRotation { get { return m_MotorRotation; } set { m_MotorRotation = value; } }
[Snapshot] public Quaternion PrevMotorRotation { get { return m_PrevMotorRotation; } set { m_PrevMotorRotation = value; } }
[Snapshot] public Vector3 ExternalForce { get { return m_ExternalForce; } protected set { m_ExternalForce = value; } }
[Snapshot] public float GravityAmount { get { return m_GravityAmount; } set { m_GravityAmount = value; } }
[Snapshot] protected Vector3 PrevPosition { get { return m_PrevPosition; } set { m_PrevPosition = value; } }
[Snapshot] protected Vector3 PlatformRelativePosition { get { return m_PlatformRelativePosition; } set { m_PlatformRelativePosition = value; } }
/// <summary>
/// Cache the component references and initialize the default values.
/// </summary>
protected override void Awake()
{
m_Transform = transform;
m_CharacterLayerManager = gameObject.GetCachedComponent<CharacterLayerManager>();
m_AnimatorMonitor = gameObject.GetCachedComponent<AnimatorMonitor>();
base.Awake();
m_Up = m_Transform.up;
m_PrevPosition = m_Transform.position;
m_MotorRotation = m_PrevMotorRotation = m_Transform.rotation;
m_GravityDirection = -m_Up;
// The Rigidbody is only used to notify Unity that the character isn't static. The Rigidbody doesn't control any movement.
m_Rigidbody = GetComponent<Rigidbody>();
m_Rigidbody.mass = m_Mass;
m_Rigidbody.isKinematic = true;
m_Rigidbody.constraints = RigidbodyConstraints.FreezeAll;
var colliders = GetComponentsInChildren<Collider>();
m_Colliders = new Collider[colliders.Length];
m_IgnoredColliders = new Collider[colliders.Length];
m_CheckRotationCollision = GetComponentInChildren<CapsuleColliderPositioner>(true) != null;
for (int i = 0; i < colliders.Length; ++i) {
// There are a variety of colliders which should be ignored. Only CapsuleCollider and SphereColliders are supported.
if (!colliders[i].enabled || colliders[i].isTrigger) {
continue;
}
// Sub colliders are parented to the character but they are not used for collision detection.
if (!MathUtility.InLayerMask(colliders[i].gameObject.layer, m_ColliderLayerMask) || !(colliders[i] is CapsuleCollider || colliders[i] is SphereCollider)) {
m_IgnoredColliders[m_IgnoredColliderCount] = colliders[i];
m_IgnoredColliderCount++;
continue;
}
m_Colliders[m_ColliderCount] = colliders[i];
m_ColliderCount++;
// Determine the max height of the character based on the collider.
var height = MathUtility.LocalColliderHeight(m_Transform, colliders[i]);
if (height > m_Height) {
m_Height = height;
}
// Determine the mim radius of the character.
var radius = float.MaxValue;
if (colliders[i] is CapsuleCollider) {
radius = (colliders[i] as CapsuleCollider).radius;
} else { // SphereCollider.
radius = (colliders[i] as SphereCollider).radius;
}
if (radius < m_Radius) {
m_Radius = radius;
}
// The rotation collider check only needs to be checked if the collider rotates on an axis other than the relative-y axis.
if (!m_CheckRotationCollision) {
m_CheckRotationCollision = CanCauseRotationCollision(colliders[i]);
}
}
// Resize the array depending on the number of valid colliders.
if (m_Colliders.Length != m_ColliderCount) {
Array.Resize(ref m_Colliders, m_ColliderCount);
}
if (m_Colliders.Length == 0) {
Debug.LogWarning($"Warning: The character {name} doesn't contain any colliders. Only capsule and sphere colliders are supported.", this);
}
if (m_IgnoredColliders.Length != m_IgnoredColliderCount) {
Array.Resize(ref m_IgnoredColliders, m_IgnoredColliderCount);
}
// Cache the collider GameObjects for best performance.
m_ColliderGameObjects = new GameObject[m_Colliders.Length];
for (int i = 0; i < m_ColliderGameObjects.Length; ++i) {
m_ColliderGameObjects[i] = m_Colliders[i].gameObject;
}
m_IgnoredColliderGameObjects = new GameObject[m_IgnoredColliders.Length];
for (int i = 0; i < m_IgnoredColliderGameObjects.Length; ++i) {
m_IgnoredColliderGameObjects[i] = m_IgnoredColliders[i].gameObject;
}
m_RaycastHits = new RaycastHit[m_MaxCollisionCount];
// If there are multiple colliders then save a mapping between the raycast hit and the collider index.
if (m_Colliders.Length > 1) {
m_ColliderIndexMap = new Dictionary<RaycastHit, int>(new UnityEngineUtility.RaycastHitEqualityComparer());
m_CombinedRaycastHits = new RaycastHit[m_RaycastHits.Length * m_Colliders.Length];
}
m_ColliderLayers = new int[m_Colliders.Length];
m_IgnoredColliderLayers = new int[m_IgnoredColliders.Length];
m_OverlapColliderHit = new Collider[m_MaxCollisionCount];
m_SoftForceFrames = new Vector3[m_MaxSoftForceFrames];
}
/// <summary>
/// The character has been enabled.
/// </summary>
protected virtual void OnEnable()
{
ResetRotationPosition();
}
/// <summary>
/// Can the specified collider cause a collision when the character is rotating? The collider can cause a rotation collision when it would rotate on
/// an axis other than the relative-y axis.
/// </summary>
/// <param name="collider">The collider to check against.</param>
/// <returns>True if the collider could cause a rotation collision.</returns>
private bool CanCauseRotationCollision(Collider collider)
{
Vector3 direction;
if (collider is CapsuleCollider) {
Vector3 startEndCap, endEndCap;
var capsuleCollider = collider as CapsuleCollider;
// The CapsuleCollider's end caps and the center position must be on the same relative-y axis.
direction = m_Transform.InverseTransformDirection(collider.transform.TransformPoint((collider as CapsuleCollider).center) - m_Transform.position);
if (Mathf.Abs(direction.x) > 0.0001f || Mathf.Abs(direction.z) > 0.0001f) {
return true;
}
MathUtility.CapsuleColliderEndCaps(capsuleCollider, capsuleCollider.transform.position, capsuleCollider.transform.rotation, out startEndCap, out endEndCap);
direction = m_Transform.InverseTransformDirection(startEndCap - endEndCap);
} else { // SphereCollider.
direction = m_Transform.InverseTransformDirection(collider.transform.TransformPoint((collider as SphereCollider).center) - m_Transform.position);
}
if (Mathf.Abs(direction.x) > 0.0001f || Mathf.Abs(direction.z) > 0.0001f) {
return true;
}
return false;
}
/// <summary>
/// Determine if the character is on the ground when the game begins.
/// </summary>
public virtual void Start()
{
// Disable the colliders to prevent the grounded check from hitting the character.
EnableColliderCollisionLayer(false);
// Update the grounded state so it is accurate on the first run.
UpdateGroundState(CheckGround(), false);
// CheckGround updates the MoveDirection. This value should be reset because it's not applied until Move.
m_MoveDirection = Vector3.zero;
// Reenable the disabled colliders.
EnableColliderCollisionLayer(true);
}
/// <summary>
/// Moves the character according to the input. This method exists to allow AI to easily move the character instead of having to go through
/// the ControllerHandler.
/// </summary>
/// <param name="horizontalMovement">-1 to 1 value specifying the amount of horizontal movement.</param>
/// <param name="forwardMovement">-1 to 1 value specifying the amount of forward movement.</param>
/// <param name="targetRotation">Value specifying the amount of yaw rotation change.</param>
public virtual void Move(float horizontalMovement, float forwardMovement, float deltaYawRotation)
{
if (m_TimeScale == 0 || Time.deltaTime == 0) {
return;
}
// Assign the inputs.
m_InputVector.x = horizontalMovement;
m_InputVector.y = forwardMovement;
m_DeltaRotation.Set(0, deltaYawRotation, 0);
//m_InputVector.x = 1;
//m_InputVector.y = 1;
//m_DeltaYawRotation = 5;
// Disable the colliders to prevent any subsequent raycasts from hitting the character.
EnableColliderCollisionLayer(false);
// Provide a callback for when the UltimateCharacterLocomotion subclass should perform its updates (such as updating the abilities).
UpdateUltimateLocomotion();
// Update the animator so the correct animations will play.
UpdateAnimator();
// Update the position and rotation after the animator has updated.
UpdatePositionAndRotation(false);
// Update the external forces after the movement has been applied.
// This should be done after the movement is applied so the full force value has been applied within UpdateMovemnet.
UpdateExternalForces();
// Reenable the disabled colliders.
EnableColliderCollisionLayer(true);
}
/// <summary>
/// Provides a virtual method for the UltimateCharacterLocomotion to perform its updates (such as updating the abilities).
/// </summary>
protected virtual void UpdateUltimateLocomotion() { }
/// <summary>
/// Provides a virtual method to update the animator parameters.
/// </summary>
protected virtual void UpdateAnimator() { }
/// <summary>
/// Updates the position and rotation. This should be done after the animator has updated so the root motion is accurate for the current frame.
/// </summary>
/// <param name="fromAnimatorMove">Is the position and rotation being updated from the animator move method?</param>
/// <returns>True if the position and rotation were updated.</returns>
private void UpdatePositionAndRotation(bool fromAnimatorMove)
{
if (m_AnimatorMonitor != null && m_AnimatorMonitor.AnimatorEnabled) {
// When the character is being moved manually it should always be updated within the main Move loop.
if (fromAnimatorMove == m_ManualMove) {
return;
}
}
UpdatePositionAndRotation();
}
/// <summary>
/// Updates the position and rotation. This should be done after the animator has updated so the root motion is accurate for the current frame.
/// </summary>
protected virtual void UpdatePositionAndRotation()
{
// Depending on when OnAnimatorMove is called the collision layer may not already be disabled.
var collisionLayerEnabled = m_CollisionLayerEnabled;
EnableColliderCollisionLayer(false);
// Before any other movements are done the character should first stay aligned to any moving platforms.
UpdatePlatformMovement();
// Update the rotation before the position so the forces will be applied in the correct direction.
UpdateRotation();
// Apply the resulting rotation changes.
ApplyRotation();
// Update all forces and check for collisions.
UpdatePosition();
// Apply the resulting position changes.
ApplyPosition();
// Rever the collision layer to the previous value.
EnableColliderCollisionLayer(collisionLayerEnabled);
}
/// <summary>
/// Updates the position and rotation changes while on a moving platform.
/// </summary>
private void UpdatePlatformMovement()
{
if (m_Platform == null) {
if (m_Grounded) {
return;
}
// The character may have previously been on a platform and should inherit the platform movement.
var damping = m_MovingPlatformForceDamping * (m_TimeScale * TimeUtility.FramerateDeltaTime);
m_PlatformMovement /= (1 + damping);
m_MoveDirection += m_PlatformMovement;
m_PlatformTorque = Quaternion.RotateTowards(m_PlatformTorque, Quaternion.identity, damping);
m_Torque *= m_PlatformTorque;
return;
}
// The character may not need to be attached to the horizontal platform anymore.
if (!m_GroundedMovingPlatform && !m_PlatformOverride) {
var moveDirection = m_Transform.position - m_Platform.TransformPoint(m_PlatformRelativePosition);
// The move direction has to be in the same direction as the character's current position.
var hitCount = NonAllocCast(moveDirection, Vector3.zero);
var platformCollision = false;
for (int i = 0; i < hitCount; ++i) {
var closestRaycastHit = QuickSelect.SmallestK(m_CombinedRaycastHits, hitCount, i, m_RaycastHitComparer);
if (closestRaycastHit.transform == m_Platform) {
platformCollision = true;
break;
}
}
if (!platformCollision) {
UpdateMovingPlatformTransform(null, false);
return;
}
}
// Update the position changes.
m_PlatformMovement = m_Platform.TransformPoint(m_PlatformRelativePosition) - m_Transform.position;
m_MoveDirection += m_PlatformMovement;
// If the character doesn't stick to the moving platform and the platform slows down more than the separation velocity then the
// moving platform momentum should be transferred to the character.
var platformVelocity = m_PlatformMovement / (m_TimeScale * Time.deltaTime);
if (!m_StickToMovingPlatform) {
if (platformVelocity.sqrMagnitude < m_PlatformVelocity.sqrMagnitude && (platformVelocity - m_PlatformVelocity).magnitude > m_MovingPlatformSeperationVelocity) {
AddForce(m_PlatformMovement, 1, false, true);
}
}
m_PlatformVelocity = platformVelocity;
// Update the rotation changes.
m_PlatformTorque = MathUtility.InverseTransformQuaternion(m_Platform.rotation, m_PlatformRotationOffset) *
Quaternion.Inverse(MathUtility.InverseTransformQuaternion(m_Platform.rotation, m_Transform.rotation * Quaternion.Inverse(m_Platform.rotation)));
if (!m_AlignToGravity) {
// Only the local y rotation should affect the character's rotation.
var localPlatformTorque = MathUtility.InverseTransformQuaternion(m_Transform.rotation, m_PlatformTorque).eulerAngles;
localPlatformTorque.x = localPlatformTorque.z = 0;
m_PlatformTorque = MathUtility.TransformQuaternion(m_Transform.rotation, Quaternion.Euler(localPlatformTorque));
}
m_Torque *= m_PlatformTorque;
}
/// <summary>
/// Update the rotation forces.
/// </summary>
protected virtual void UpdateRotation()
{
// Rotate according to the root motion rotation or target rotation.
Quaternion rotationDelta, targetRotation;
var rotation = m_Transform.rotation * m_Torque;
if (UsingRootMotionRotation) {
targetRotation = rotation * m_LocalRootMotionRotation;
if (m_AlignToGravity) {
targetRotation *= Quaternion.Euler(m_DeltaRotation);
}
} else {
if (m_AlignToGravity) {
// When aligning to gravity the character should rotate immediately to the up direction.
if (m_SmoothGravityYawDelta) {
m_DeltaRotation.y = Mathf.LerpAngle(0, MathUtility.ClampInnerAngle(m_DeltaRotation.y), m_MotorRotationSpeed * m_TimeScale * TimeUtility.DeltaTimeScaled);
}
targetRotation = rotation * Quaternion.Euler(m_DeltaRotation);
} else {
targetRotation = Quaternion.Slerp(rotation, rotation * Quaternion.Euler(m_DeltaRotation), m_MotorRotationSpeed * m_TimeScale * TimeUtility.DeltaTimeScaled);
}
}
rotationDelta = Quaternion.Inverse(rotation) * targetRotation;
m_LocalRootMotionRotation = Quaternion.identity;
rotationDelta = CheckRotation(rotationDelta, false);
// Apply the delta rotation.
m_Torque *= rotationDelta;
}
/// <summary>
/// Checks the rotation to ensure the character's colliders won't collide with any other objects.
/// </summary>
/// <param name="rotationDelta">The delta to apply to the rotation.</param>
/// <param name="forceCheck">Should the rotation be force checked? This is used when the character is aligning to the ground.</param>
/// <returns>A valid rotation delta.</returns>
public Quaternion CheckRotation(Quaternion rotationDelta, bool forceCheck)
{
// A rotation change can cause horizontal collisions. If horizontal collisions are not checked then the rotation does not need to be checked.
if (!UsingHorizontalCollisionDetection) {
return rotationDelta;
}
// The rotation only needs to be checked if a collider could cause a collision when rotating. For example, a vertical CapsuleCollider centered
// in the origin doesn't need to be checked because it can't collide with anything else when rotating.
if (m_CheckRotationCollision && rotationDelta != Quaternion.identity) {
if (m_ColliderCount > 1) {
// Clear the index map to start it off fresh.
m_ColliderIndexMap.Clear();
}
// There is no "rotation sphere/capsule cast" so the collisions must be detected manually. Loop through all of the colliders checking for a collision using
// the Physics Overlap method. If there is a collision then the penetration must be determined to detect slopes. If the collision is not on a slope then
// the rotation would overlap another collider so a smaller rotation must be used. Do this for the maximum number of collision checks, with the last one
// being no rotation at all.
for (int i = 0; i < m_ColliderCount; ++i) {
// The collider doesn't need to be checked if the rotation doesn't cause a change on the relative x or z axis. It will always be checked if the character
// is being realigned to the ground because the colliders will always change on the relative x or z axis.
if (!forceCheck && !CanCauseRotationCollision(m_Colliders[i])) {
continue;
}
// Prevent the character from intersecting with another object while rotating.
var targetRotationDelta = rotationDelta;
var hitCount = 0;
for (int k = 0; k < m_RotationCollisionCheckCount; ++k) {
// Slerp towards Quaternion.identity which will not add any rotation at all.
rotationDelta = Quaternion.Slerp(targetRotationDelta, Quaternion.identity, k / (float)(m_RotationCollisionCheckCount - 1));
// Calculate what the matrix for the child collider will be based on the rotation delta. This is done instead of setting the rotation
// direction on the Transform to reduce the calls to the Unity API.
var matrix = MathUtility.ApplyRotationToChildMatrices(m_Colliders[i].transform, m_Transform, rotationDelta);
// Store the position and rotation from the matrix for future use.
var targetPosition = MathUtility.PositionFromMatrix(matrix);
var targetRotation = MathUtility.QuaternionFromMatrix(matrix);
if (m_Colliders[i] is CapsuleCollider) {
Vector3 startEndCap, endEndCap;
var capsuleCollider = m_Colliders[i] as CapsuleCollider;
capsuleCollider.radius += c_ColliderSpacing;
MathUtility.CapsuleColliderEndCaps(capsuleCollider, targetPosition, targetRotation, out startEndCap, out endEndCap);
hitCount = Physics.OverlapCapsuleNonAlloc(startEndCap, endEndCap, capsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(capsuleCollider),
m_OverlapColliderHit, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
} else { // SphereCollider.
var sphereCollider = m_Colliders[i] as SphereCollider;
sphereCollider.radius += c_ColliderSpacing;
hitCount = Physics.OverlapSphereNonAlloc(targetPosition, sphereCollider.radius * MathUtility.ColliderRadiusMultiplier(sphereCollider),
m_OverlapColliderHit, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
}
var overlap = false;
if (hitCount > 0) {
Vector3 direction;
float distance;
// If there is a collision ensure that collision is with a non-sloped object.
for (int j = 0; j < hitCount; ++j) {
if (Physics.ComputePenetration(m_Colliders[i], targetPosition, targetRotation,
m_OverlapColliderHit[j], m_OverlapColliderHit[j].transform.position, m_OverlapColliderHit[j].transform.rotation, out direction, out distance)) {
// If the hit object is less then the slope limit then the character should rotate with the slope rather then stopping immediately.
var slope = Vector3.Angle((m_Transform.rotation * rotationDelta) * Vector3.up, direction);
if (slope <= m_SlopeLimit + c_SlopeLimitSpacing) {
continue;
}
overlap = true;
break;
}
}
}
// The size of the collider has been increased ever so slightly so the collider bounds won't overlap other colliders. If this was not done
// the character would be able to rotate in a position that would allow them to move through other colliders.
if (m_Colliders[i] is CapsuleCollider) {
(m_Colliders[i] as CapsuleCollider).radius -= c_ColliderSpacing;
} else { // SphereCollider.
(m_Colliders[i] as SphereCollider).radius -= c_ColliderSpacing;
}
// If there is no overlap then the rotation is valid and can be used.
if (!overlap) {
break;
}
}
}
}
return rotationDelta;
}
/// <summary>
/// Applies the final rotation to the transform.
/// </summary>
protected virtual void ApplyRotation()
{
// Apply the rotation.
m_Transform.rotation = MathUtility.Round(m_Transform.rotation * m_Torque, 1000000);
m_Up = m_Transform.up;
m_MotorRotation = m_Transform.rotation;
m_Torque = Quaternion.identity;
if (m_Platform != null) {
m_PlatformRotationOffset = m_Transform.rotation * Quaternion.Inverse(m_Platform.rotation);
}
}
/// <summary>
/// Move according to the forces.
/// </summary>
protected virtual void UpdatePosition()
{
// Update any base movement forces.
UpdateMotorThrottle();
var deltaTime = TimeScaleSquared * Time.timeScale * TimeUtility.FramerateDeltaTime;
m_MoveDirection += m_ExternalForce * deltaTime + (m_MotorThrottle * (UsingRootMotionPosition ? 1 : deltaTime)) - m_GravityDirection * m_GravityAmount * deltaTime;
// After the character has moved update the collisions. This will prevent the character from moving through solid objects.
DeflectHorizontalCollisions();
DeflectVerticalCollisions();
}
/// <summary>
/// Updates the motor forces.
/// </summary>
protected virtual void UpdateMotorThrottle()
{
if (UsingRootMotionPosition || m_LocalRootMotionForce.sqrMagnitude > 0) {
m_MotorThrottle = MathUtility.TransformDirection(m_LocalRootMotionForce, m_MotorRotation) * (m_Grounded ? 1 : m_RootMotionAirForceMultiplier) * m_SlopeFactor;
} else {
// Apply a multiplier if the character is moving backwards.
var backwardsMultiplier = 1f;
if (m_InputVector.y < 0) {
backwardsMultiplier *= Mathf.Lerp(1, m_MotorBackwardsMultiplier, Mathf.Abs(m_InputVector.y));
}
// As the character changes rotation the same local motor throttle force should be applied. This is most apparent when the character is being aligned to the ground
// and the local y direction changes.
var prevLocalMotorThrottle = MathUtility.InverseTransformDirection(m_MotorThrottle, m_PrevMotorRotation) * m_PreviousAccelerationInfluence;
var rotation = Quaternion.Slerp(m_PrevMotorRotation, m_MotorRotation, m_PreviousAccelerationInfluence);
var acceleration = (m_Grounded ? m_MotorAcceleration : m_MotorAirborneAcceleration) * m_SlopeFactor * backwardsMultiplier * 0.1f;
// Convert input into motor forces. Normalize the input vector to prevent the diagonal from moving faster.
var normalizedInputVector = m_InputVector.normalized * Mathf.Max(Mathf.Abs(m_InputVector.x), Mathf.Abs(m_InputVector.y));
m_MotorThrottle = MathUtility.TransformDirection(new Vector3(prevLocalMotorThrottle.x + normalizedInputVector.x * acceleration.x,
prevLocalMotorThrottle.y, prevLocalMotorThrottle.z + normalizedInputVector.y * acceleration.z), rotation);
// Dampen motor forces.
m_MotorThrottle /= (1 + ((m_Grounded ? m_MotorDamping : m_MotorAirborneDamping) * m_TimeScale * Time.timeScale));
}
m_PrevMotorRotation = m_MotorRotation;
m_LocalRootMotionForce = Vector3.zero;
}
/// <summary>
/// Deflects any collisions in the horizontal direction.
/// </summary>
private void DeflectHorizontalCollisions()
{
m_HorizontalRaycastHit = m_BlankRaycastHit;
// No horizontal collision checks are necessary if no collisions are being detected.
if (!UsingHorizontalCollisionDetection) {
return;
}
// While on a platform the move direction will contain the movement caused by the platform. When performing collision detection this value should be ignored
// because the character is going to move with the platform.
var platformIndependentMoveDirection = (m_MoveDirection - m_PlatformMovement);
var horizontalDirection = Vector3.ProjectOnPlane(platformIndependentMoveDirection, m_Up);
// No casts are necessary if there is no movement.
if (horizontalDirection.sqrMagnitude < c_ColliderSpacingCubed && Vector3.ProjectOnPlane(m_PlatformMovement, m_Up).sqrMagnitude < c_ColliderSpacingCubed) {
var localDirection = m_Transform.InverseTransformDirection(platformIndependentMoveDirection);
localDirection.x = localDirection.z = 0;
m_MoveDirection = m_Transform.TransformDirection(localDirection) + m_PlatformMovement;
return;
}
var hitCount = NonAllocCast(horizontalDirection, m_PlatformMovement);
if (hitCount == 0) {
return;
}
var localPlatformIndependentMoveDirection = m_Transform.InverseTransformDirection(platformIndependentMoveDirection);
var moveDistance = 0f;
var hitStrength = 0f;
var hitMoveDirection = Vector3.zero;
for (int i = 0; i < hitCount; ++i) {
var closestRaycastHit = QuickSelect.SmallestK(m_CombinedRaycastHits, hitCount, i, m_RaycastHitComparer);
// Determine which collider caused the intersection.
var activeCollider = m_ColliderCount > 1 ? m_Colliders[m_ColliderIndexMap[closestRaycastHit]] : m_Colliders[0];
// If the distance is 0 then the colliders are overlapping. Use ComputePenetaration to seperate the objects.
if (closestRaycastHit.distance == 0) {
var offset = Vector3.zero;
ComputePenetration(activeCollider, closestRaycastHit.collider, horizontalDirection, false, out offset);
if (offset.sqrMagnitude >= c_ColliderSpacingCubed) {
moveDistance = Mathf.Max(0, horizontalDirection.magnitude - offset.magnitude - c_ColliderSpacing);
platformIndependentMoveDirection = Vector3.ProjectOnPlane(horizontalDirection.normalized * moveDistance, m_Up) +
m_Up * localPlatformIndependentMoveDirection.y;
} else {
platformIndependentMoveDirection = Vector3.zero;
}
break;
}
// Push any hit Rigidbodies that can be pushed.
var hitGameObject = closestRaycastHit.transform.gameObject;
var hitRigidbody = hitGameObject.GetCachedParentComponent<Rigidbody>();
var canStep = true;
if (hitRigidbody != null) {
var radius = (activeCollider is CapsuleCollider ?
((activeCollider as CapsuleCollider).radius * MathUtility.ColliderRadiusMultiplier(activeCollider as CapsuleCollider)) :
((activeCollider as SphereCollider).radius * MathUtility.ColliderRadiusMultiplier(activeCollider)));
canStep = !PushRigidbody(hitRigidbody, horizontalDirection, closestRaycastHit.point, radius);
}
// Determine if the character should step over the object. If the object is a rigidbody that was pushed then the character shouldn't attempt to step over
// the object since the object will be moving.
if (m_Grounded && canStep) {
// Only check for slope/steps if the hit point is lower than the max step height.
var groundPoint = m_Transform.InverseTransformPoint(closestRaycastHit.point);
if (groundPoint.y <= m_MaxStepHeight + c_ColliderSpacing) {
// A CapsuleCast/SphereCast normal isn't always the true normal: http://answers.unity3d.com/questions/50825/raycasthitnormal-what-does-it-really-return.html.
// Cast a regular raycast in order to determine the true normal.
m_SlopeRay.direction = horizontalDirection.normalized;
m_SlopeRay.origin = closestRaycastHit.point - m_SlopeRay.direction * (c_ColliderSpacing + 0.1f);
if (!Physics.Raycast(m_SlopeRay, out m_RaycastHit, (c_ColliderSpacing + 0.11f), 1 << hitGameObject.layer, QueryTriggerInteraction.Ignore)) {
m_RaycastHit = closestRaycastHit;
}
var slope = Vector3.Angle(m_Up, m_RaycastHit.normal);
if (slope <= m_SlopeLimit + c_SlopeLimitSpacing) {
continue;
}
// Cast a ray directly in front of the character. If it doesn't hit an object then the object is shorter than the step height and should be stepped on.
// Continue out of the loop to prevent the character from stopping in front of the object.
if (SingleCast(activeCollider, horizontalDirection, m_PlatformMovement + m_Up * (m_MaxStepHeight - c_ColliderSpacing))) {
if ((m_RaycastHit.distance - c_ColliderSpacing) < horizontalDirection.magnitude) {
// An object was hit. The character can step over the object if the slope is less than the limit.
m_SlopeRay.direction = horizontalDirection.normalized;
m_SlopeRay.origin = closestRaycastHit.point - m_SlopeRay.direction * (c_ColliderSpacing + 0.1f);
var normal = m_RaycastHit.normal;
if (Physics.Raycast(m_SlopeRay, out m_RaycastHit, (c_ColliderSpacing + 0.11f), 1 << hitGameObject.layer, QueryTriggerInteraction.Ignore)) {
normal = m_RaycastHit.normal;
}
slope = Vector3.Angle(m_Up, normal);
if (slope <= m_SlopeLimit + c_SlopeLimitSpacing) {
continue;
}
}
} else {
groundPoint.y = 0;
groundPoint = m_Transform.TransformPoint(groundPoint);
var direction = groundPoint - m_Transform.position;
if (OverlapCount(activeCollider, (direction.normalized * (direction.magnitude + m_Radius * 0.5f)) + m_PlatformMovement + m_Up * (m_MaxStepHeight - c_ColliderSpacing)) == 0) {
// Step over the object if there are no objects in the way.
continue;
}
}
}
}
#if UNITY_EDITOR
Debug.DrawRay(closestRaycastHit.point, closestRaycastHit.normal, Color.red);
#endif
// Determine the direction that the character should move based off of the angle of the hit object.
var hitNormal = Vector3.ProjectOnPlane(closestRaycastHit.normal, m_Up).normalized;
var targetDirection = Vector3.Cross(hitNormal, m_Up).normalized;
var closestPoint = MathUtility.ClosestPointOnCollider(m_Transform, activeCollider, closestRaycastHit.point, platformIndependentMoveDirection, true, false);
if ((Vector3.Dot(Vector3.Cross(Vector3.ProjectOnPlane(m_Transform.position - closestPoint, m_Up).normalized, horizontalDirection).normalized, m_Up)) > 0) {
targetDirection = -targetDirection;
}
// The hit distance may be more than the horizontal direction magnitude if the hit object is within the radius distance.
// This could happen if there is a small object directly in front of the character but under the collider's concave curve.
// Subtract the collider spacing constant to prevent the character's collider from overlapping the hit collider.
moveDistance = Mathf.Min(closestRaycastHit.distance - c_ColliderSpacing, horizontalDirection.magnitude);
if (moveDistance < 0.001f || Vector3.Angle(m_Up, m_GroundRaycastHit.normal) > m_SlopeLimit + c_SlopeLimitSpacing) {
moveDistance = 0;
}
// Glide across the wall if it's at an angle relative to the character. Evaluate how much to glide based off a curve. Don't glide faster
// then the horizontal direction magnitude unless the curve specifies to.
var dynamicFrictionValue = Mathf.Clamp01(1 - MathUtility.FrictionValue(activeCollider.material, closestRaycastHit.collider.material, true));
hitStrength = 1 - Vector3.Dot(horizontalDirection.normalized, -hitNormal);
hitMoveDirection = targetDirection * (horizontalDirection.magnitude - moveDistance) * m_WallGlideCurve.Evaluate(hitStrength) * dynamicFrictionValue;
if (hitMoveDirection.magnitude <= c_ColliderSpacing) {
hitMoveDirection = Vector3.zero;
hitStrength = 0;
m_HorizontalRaycastHit = closestRaycastHit;
}
platformIndependentMoveDirection = (horizontalDirection.normalized * moveDistance) + hitMoveDirection + m_Up * localPlatformIndependentMoveDirection.y;
// The character may bounce away from the object. This bounce is applied to the external force so it'll be checked next frame.
var bouncinessValue = MathUtility.BouncinessValue(activeCollider.material, closestRaycastHit.collider.material);
if (bouncinessValue > 0.0f) {
var magnitude = Mathf.Max(m_ExternalForce.magnitude, bouncinessValue * m_WallBounceModifier);
AddForce(Vector3.Reflect(horizontalDirection, hitNormal).normalized * magnitude, 1, false, true);
}
break;
}
ResetCombinedRaycastHits();
// Do another cast in the desired direction to ensure the position is valid.
if (hitStrength > 0.0001f) {
hitCount = NonAllocCast(hitMoveDirection, m_PlatformMovement);
for (int i = 0; i < hitCount; ++i) {
var closestRaycastHit = QuickSelect.SmallestK(m_CombinedRaycastHits, hitCount, i, m_RaycastHitComparer);
// Determine if the character should step over the object. The character must be on the ground with a slope less than the limit in order to step over.
var activeCollider = m_ColliderCount > 1 ? m_Colliders[m_ColliderIndexMap[closestRaycastHit]] : m_Colliders[0];
if (m_Grounded) {
var groundPoint = m_Transform.InverseTransformPoint(closestRaycastHit.point);
if (groundPoint.y > c_ColliderSpacing && groundPoint.y <= m_MaxStepHeight + c_ColliderSpacing) {
var hitGameObject = closestRaycastHit.transform.gameObject;
// A CapsuleCast/SphereCast normal isn't always the true normal: http://answers.unity3d.com/questions/50825/raycasthitnormal-what-does-it-really-return.html.
// Cast a regular raycast in order to determine the true normal.
m_SlopeRay.direction = hitMoveDirection.normalized;
m_SlopeRay.origin = closestRaycastHit.point - m_SlopeRay.direction * (c_ColliderSpacing + 0.1f);
if (!Physics.Raycast(m_SlopeRay, out m_RaycastHit, (c_ColliderSpacing + 0.11f), 1 << hitGameObject.layer, QueryTriggerInteraction.Ignore)) {
m_RaycastHit = closestRaycastHit;
}
var slope = Vector3.Angle(m_Up, m_RaycastHit.normal);
if (slope <= m_SlopeLimit + c_SlopeLimitSpacing) {
continue;
}
// Cast a ray directly in front of the character. If it doesn't hit an object then the object is shorter than the step height and should be stepped on.
// Continue out of the loop to prevent the character from stopping in front of the object.
if (SingleCast(activeCollider, hitMoveDirection, m_PlatformMovement + m_Up * (m_MaxStepHeight - c_ColliderSpacing))) {
if ((m_RaycastHit.distance - c_ColliderSpacing) < hitMoveDirection.magnitude) {
// An object was hit. The character can step over the object if the slope is less than the limit.
m_SlopeRay.direction = hitMoveDirection.normalized;
m_SlopeRay.origin = closestRaycastHit.point - m_SlopeRay.direction * (c_ColliderSpacing + 0.1f);
var normal = m_RaycastHit.normal;
if (Physics.Raycast(m_SlopeRay, out m_RaycastHit, (c_ColliderSpacing + 0.11f), 1 << hitGameObject.layer, QueryTriggerInteraction.Ignore)) {
normal = m_RaycastHit.normal;
}
slope = Vector3.Angle(m_Up, normal);
if (slope <= m_SlopeLimit + c_SlopeLimitSpacing) {
continue;
}
}
} else {
groundPoint.y = 0;
groundPoint = m_Transform.TransformPoint(groundPoint);
var direction = groundPoint - m_Transform.position;
if (OverlapCount(activeCollider, (direction.normalized * (direction.magnitude + m_Radius * 0.5f)) + m_PlatformMovement + m_Up * (m_MaxStepHeight - c_ColliderSpacing)) == 0) {
// Step over the object if there are no objects in the way.
continue;
}
}
}
}
#if UNITY_EDITOR
Debug.DrawRay(closestRaycastHit.point, closestRaycastHit.normal, Color.yellow);
#endif
// Another collider is in the way of the hit move direction. Do not apply the full magnitude of the move direction.
// Subtract the collider spacing constant to prevent the character's collider from overlapping the hit collider.
// In addition to subtracting the collider spacing, also subtract the distance that the move distance is contributing to the hit move direction.
// This will prevent the character from moving too far because the hit point direction is similar to the horizontal direction.
// This last condition is especially apprant with -extremely- fast moving objects.
var moveDistanceContribution = moveDistance * Mathf.Cos(Vector3.Angle(horizontalDirection.normalized, hitMoveDirection.normalized) * Mathf.Deg2Rad);
var hitMoveDistance = Mathf.Min(closestRaycastHit.distance - moveDistanceContribution - hitMoveDirection.magnitude - c_ColliderSpacing, hitMoveDirection.magnitude);
if (hitMoveDistance < 0.001f || Vector3.Angle(m_Up, m_GroundRaycastHit.normal) > m_SlopeLimit + c_SlopeLimitSpacing) {
hitMoveDistance = 0;
m_HorizontalRaycastHit = closestRaycastHit;
}
platformIndependentMoveDirection = (horizontalDirection.normalized * moveDistance) + hitMoveDirection.normalized * hitMoveDistance + m_Up * localPlatformIndependentMoveDirection.y;
break;
}
ResetCombinedRaycastHits();
}
m_MoveDirection = platformIndependentMoveDirection + m_PlatformMovement;
}
/// <summary>
/// Deflects any collisions in the vertical direction.
/// </summary>
private void DeflectVerticalCollisions()
{
// Ensure the character doesn't collide with any objects above them.
var localMoveDirection = m_Transform.InverseTransformDirection(m_MoveDirection);
if (UsingVerticalCollisionDetection && localMoveDirection.y > 0) {
var horizontalDirection = Vector3.ProjectOnPlane(m_MoveDirection - m_PlatformMovement, m_Up);
var hitCount = NonAllocCast(m_Up * (localMoveDirection.y + c_ColliderSpacing), horizontalDirection);
if (hitCount > 0) {
var closestRaycastHit = QuickSelect.SmallestK(m_CombinedRaycastHits, hitCount, 0, m_RaycastHitComparer);
if (closestRaycastHit.distance == 0) {
// Use ComputePenetration to determine a direction that doesn't overlap with other objects.
var activeCollider = m_ColliderCount > 1 ? m_Colliders[m_ColliderIndexMap[closestRaycastHit]] : m_Colliders[0];
var offset = Vector3.zero;
var overlap = ComputePenetration(activeCollider, closestRaycastHit.collider, horizontalDirection, true, out offset);
if (overlap) {
overlap = ComputePenetration(activeCollider, closestRaycastHit.collider, horizontalDirection, false, out offset);
}
if (!overlap) {
// The offset is valid.
localMoveDirection.y = m_Transform.InverseTransformDirection(offset).y;
} else {
// The offset still causes an overlap. Set the vertical move direction to 0.
localMoveDirection.y = 0;
}
} else {
// The character would hit an object above it. Use the hit object to determine the max distance that the character can move.
localMoveDirection.y = Mathf.Max(0, closestRaycastHit.distance - c_ColliderSpacing);
}
// The character shouldn't move up anymore.
if (localMoveDirection.y == 0) {
var localExternalForce = LocalExternalForce;
localExternalForce.y = 0;
m_ExternalForce = m_Transform.TransformDirection(localExternalForce);
}
// Convert back to the global direction for CheckGround.
m_MoveDirection = m_Transform.TransformDirection(localMoveDirection);
}
}
// Is the character on the ground?
var grounded = CheckGround();
localMoveDirection = m_Transform.InverseTransformDirection(m_MoveDirection);
var accumulateGravity = UsingGravity;
var verticalOffset = 0f;
if (UsingVerticalCollisionDetection && m_GroundRaycastHit.distance != 0) {
verticalOffset = m_Transform.InverseTransformDirection(m_GroundRaycastHit.point - m_GroundRaycastOrigin).y + c_ColliderSpacing;
if (Mathf.Abs(verticalOffset) < 0.0001f) {
verticalOffset = 0;
}
// Staying in local space, determine if the vertical offset or the gravity force should be used. The lesser value should be used to prevent the character
// from moving through the floor. Add the collider spacing constant to prevent the character's collider from overlapping the ground.
// Use the verticalOffset if the local move direction is negative (ignoring the platform) and less than the vertical offset (plus an offset if sticking to the ground).
var localPlatformVerticalDirection = m_Platform != null ? m_Transform.InverseTransformDirection(m_PlatformMovement).y : 0;
if ((m_Grounded || grounded) && (localMoveDirection.y - localPlatformVerticalDirection) < m_SkinWidth && verticalOffset > -c_ColliderSpacing -
((m_Grounded && m_StickToGround) ? m_Stickiness : 0)) {
localMoveDirection.y += verticalOffset;
// Don't allow the character to go through the ground.
if (localMoveDirection.y < -m_GroundRaycastHit.distance + localPlatformVerticalDirection) {
localMoveDirection.y = -m_GroundRaycastHit.distance + localPlatformVerticalDirection + c_ColliderSpacing;
}
accumulateGravity = false;
// If the character wasn't previously grounded they are now. This allows the character to stick to the ground without the grounded state updating.
grounded = true;
}
}
// Convert the local move direction back to world position.
m_MoveDirection = m_Transform.TransformDirection(localMoveDirection);
if (accumulateGravity) { // The character is in the air.
// Accumulate gravity.
m_GravityAmount += (m_GravityMagnitude * -0.001f) / Time.timeScale;
} else if (grounded) { // The character is on the ground.
// If the character is standing on a rigidbody then a downward force should be applied.
if (m_GroundRaycastHit.rigidbody != null) {
m_GroundRaycastHit.rigidbody.AddForceAtPosition(-m_Up * ((m_Mass / m_GroundRaycastHit.rigidbody.mass) + m_GravityAmount) / Time.deltaTime,
m_Transform.position + m_MoveDirection, ForceMode.Force);
}
// No gravity is applied when the character is grounded.
m_GravityAmount = 0;
}
// Update the grounded status. The vertical collisions may change the grounded state so this value is not updated immediately (such as if the character is sticking to the ground).
UpdateGroundState(grounded, true);
}
/// <summary>
/// Determines if the character is grounded.
/// </summary>
/// <returns>True if the character is grounded.</returns>
private bool CheckGround()
{
// Reset the ground raycast hit for the next run.
m_GroundRaycastHit = m_BlankRaycastHit;
var verticalMoveDirection = m_Transform.InverseTransformDirection(m_MoveDirection).y;
var horizontalDirection = Vector3.ProjectOnPlane(m_MoveDirection, -m_GravityDirection);
var localPlatformVerticalDirection = m_Platform != null ? m_Transform.InverseTransformDirection(m_PlatformMovement).y : 0;
var horizontalCastOffset = horizontalDirection + (-m_GravityDirection * localPlatformVerticalDirection);
var hitCount = NonAllocCast(m_GravityDirection * (Mathf.Abs(verticalMoveDirection) + m_MaxStepHeight + (m_Grounded && m_StickToGround ? m_Stickiness : 0) + c_ColliderSpacing),
horizontalCastOffset);
var grounded = false;
// The character hit the ground if any hit points are below the collider.
for (int i = 0; i < hitCount; ++i) {
var closestRaycastHit = QuickSelect.SmallestK(m_CombinedRaycastHits, hitCount, i, m_RaycastHitComparer);
var activeCollider = m_ColliderCount > 1 ? m_Colliders[m_ColliderIndexMap[closestRaycastHit]] : m_Colliders[0];
// When DeflectHorizontalCollisions runs it will not do any vertical placement. If the character is on a slope or a step there will be a collision
// because the move direction hasn't changed vertically. The RaycastHit distance will then be 0 indicating there are objects overlapping it.
if (closestRaycastHit.distance == 0) {
// If vertical collisions are not being detected then don't try to readjust the collider position. The hit collider should not count as being grounded
// because it overlaps the character collider.
if (!UsingVerticalCollisionDetection) {
continue;
}
// Do not run ComputeGroundPenetration if the character should step over the object. DeflectVerticalForces will account for the vertical step difference.
var horizontalStep = false;
if (UsingHorizontalCollisionDetection && horizontalDirection.sqrMagnitude >= 0.000001f) {
var horizontalPlatformMovement = Vector3.ProjectOnPlane(m_PlatformMovement, m_Up);
if (SingleCast(activeCollider, horizontalDirection, horizontalPlatformMovement)) {
var groundPoint = m_Transform.InverseTransformPoint(m_RaycastHit.point);
if (groundPoint.y <= m_MaxStepHeight + c_ColliderSpacing) {
// If there are no objects in front of the character then any objects that is in front is shorter than the step height and should be stepped on.
if (OverlapCount(activeCollider, horizontalDirection + horizontalPlatformMovement + m_Up * (m_MaxStepHeight - c_ColliderSpacing)) == 0) {
horizontalStep = true;
}
}
}
}
// ComputeGroundPenetration has to potential of running twice. The first time it will try to keep the velocity at a constant speed while also preventing
// any overlap. If it can't do that then ComputeGroundPenetration will be run a second time to which does not try to keep a constant speed.
var offset = Vector3.zero;
var overlap = !horizontalStep && UsingHorizontalCollisionDetection && ComputePenetration(activeCollider, closestRaycastHit.collider, horizontalCastOffset, true, out offset);
if (overlap) {
overlap = ComputePenetration(activeCollider, closestRaycastHit.collider, horizontalCastOffset, false, out offset);
}
// If there is no more overlap then perform a final cast to determine the location of the ground.
if (!overlap) {
var localOffset = m_Transform.InverseTransformDirection(offset);
// Prevent ComputeGroundPenetration from trying to reposition the character because of a collision.
if (!UsingHorizontalCollisionDetection && Mathf.Abs(localOffset.y) <= 0.001f) {
if (!SingleCast(activeCollider, m_GravityDirection * (m_MaxStepHeight + c_ColliderSpacing), Vector3.ProjectOnPlane(m_MoveDirection, -m_GravityDirection) + m_Transform.up * (m_MaxStepHeight + c_ColliderSpacing))) {
continue;
}
}
// Remove any external force on the y direction once the character has hit the collider above them.
if (localOffset.y < 0) {
var localExternalForce = m_Transform.InverseTransformDirection(m_ExternalForce);
localExternalForce.y = 0;
m_ExternalForce = m_Transform.TransformDirection(localExternalForce);
}
// Reset any upwards offset to 0. It will later be added back in after the gravity forces have been detected.
if (localOffset.y > 0) { localOffset.y = 0; }
// The move direction should update based on the ComputePenetration value.
m_MoveDirection += m_Transform.TransformDirection(localOffset);
// One more cast should be performed to determine the ground object now that the character is in a non-overlapping position.
if (SingleCast(activeCollider, m_GravityDirection * (m_MaxStepHeight + c_ColliderSpacing), Vector3.ProjectOnPlane(m_MoveDirection, -m_GravityDirection) + m_Transform.up * (m_MaxStepHeight + c_ColliderSpacing))) {
closestRaycastHit = m_RaycastHit;
} else {
// The character is not grounded after resolving the collision. Continue the grounded check next update.
break;
}
} else {
m_MoveDirection = Vector3.zero;
break;
}
}
// The raycast position is determined by the location that the raycast hit. This is required because the capsule collider is long and if the hit point is within the
// center length of the capsule collider then the character should not account for that hit distance.
m_GroundRaycastHit = closestRaycastHit;
m_GroundRaycastOrigin = MathUtility.ClosestPointOnCollider(m_Transform, activeCollider, m_GroundRaycastHit.point, m_MoveDirection, false, true);
// The character is grounded when the ground contact point is within the skin width. The ground raycast hit and origin still need to be set to detect vertical collisions.
var localDirection = m_Transform.InverseTransformDirection(m_GroundRaycastHit.point - m_GroundRaycastOrigin);
var deltaTime = m_TimeScale * Time.timeScale * TimeUtility.FramerateDeltaTime;
grounded = (localDirection.y - (LocalExternalForce.y * deltaTime)) >= -m_SkinWidth;
break;
}
ResetCombinedRaycastHits();
// Update the moving platform if the character lands on a new platform or moves off of the platform.
if (grounded != m_Grounded || m_GroundRaycastHit.transform != m_GroundHitTransform) {
var target = grounded ? m_GroundRaycastHit.transform : null;
if (target != m_GroundHitTransform) {
m_GroundHitTransform = target;
if (!m_PlatformOverride) {
UpdateMovingPlatformTransform(m_GroundHitTransform, true);
}
}
}
UpdateSlopeFactor();
return grounded;
}
/// <summary>
/// Use ComputePenetration to prevent the collider from overlapping with another collider.
/// </summary>
/// <param name="activeCollider">The collider which may is being checked to ensure it doesn't cause a collision.</param>
/// <param name="hitCollider">The original collider that caused a collision with the active collider.</param>
/// <param name="horizontalDirection">The horizontal direction that the character is moving.</param>
/// <param name="constantVelocity">Should a constant velocity be used?</param>
/// <param name="offset">The offset returned by Physics.ComputePenetration.</param>
/// <returns>True if an overlap still occurs even after using the offset.</returns>
private bool ComputePenetration(Collider activeCollider, Collider hitCollider, Vector3 horizontalDirection, bool constantVelocity, out Vector3 offset)
{
var iterations = m_MaxOverlapIterations;
float distance;
Vector3 direction;
offset = Vector3.zero;
var overlap = true;
m_OverlapColliderHit[0] = hitCollider;
while (iterations > 0) {
if (Physics.ComputePenetration(activeCollider, activeCollider.transform.position + horizontalDirection + offset,
activeCollider.transform.rotation, m_OverlapColliderHit[0], m_OverlapColliderHit[0].transform.position, m_OverlapColliderHit[0].transform.rotation, out direction, out distance)) {
offset += direction.normalized * (distance + c_ColliderSpacing);
} else {
// End early - no need to keep trying.
offset = Vector3.zero;
overlap = false;
break;
}
if (constantVelocity) {
// Keep the same velocity magnitude as before the overlap. This will prevent slopes from causing the character to change velocities.
offset = (horizontalDirection + offset).normalized * horizontalDirection.magnitude - horizontalDirection;
}
// Determine if the offset resolved the overlap.
if (OverlapCount(activeCollider, horizontalDirection + offset) == 0) {
overlap = false;
break;
}
iterations--;
}
return overlap;
}
/// <summary>
/// When the character changes grounded state the moving platform should also be updated. This
/// allows the character to always reference the correct moving platform (if one exists at all).
/// </summary>
/// <param name="hitTransform">The name of the possible moving platform transform.</param>
/// <param name="groundUpdate">Is the moving platform update being called from a grounded check?</param>
/// <returns>True if the platform changed.</returns>
private bool UpdateMovingPlatformTransform(Transform hitTransform, bool groundUpdate)
{
// If the platform is not null then the platform should only update if the method is being called from the grounded check.
// This will prevent the moving platform from being updated more than once during a single move event.
if (m_Platform != null && m_GroundedMovingPlatform != groundUpdate) {
return false;
}
// Update the moving platform if on the ground and the ground transform is a moving platform.
if (hitTransform != null) {
// The character may not be on the ground if the character is teleported to a location that overlaps the moving platform.
if (hitTransform.gameObject.layer == LayerManager.MovingPlatform) {
if (hitTransform != m_Platform) {
SetPlatform(hitTransform, false);
}
m_GroundedMovingPlatform = groundUpdate;
return true;
} else if (m_Platform != null) {
TransferPlatformMovement();
SetPlatform(null, false);
return true;
}
} else if (m_Platform != null && hitTransform == null) { // The character is no longer on a moving platform.
if (m_ExternalForce.sqrMagnitude > 0.01f) {
m_PlatformMovement = Vector3.zero;
m_PlatformTorque = Quaternion.identity;
}
SetPlatform(null, true);
return true;
}
return false;
}
/// <summary>
/// Sets the moving platform to the specified transform.
/// </summary>
/// <param name="platform">The platform transform that should be set. Can be null.</param>
public void SetPlatform(Transform platform)
{
SetPlatform(platform, true);
}
/// <summary>
/// Sets the moving platform to the specified transform.
/// </summary>
/// <param name="platform">The platform transform that should be set. Can be null.</param>
/// <param name="platformOverride">Is the default moving platform logic being overridden?</param>
/// <returns>True if the platform was changed.</returns>
public virtual bool SetPlatform(Transform platform, bool platformOverride)
{
if (m_Platform == platform) {
return false;
}
m_Platform = platform;
m_PlatformOverride = m_Platform != null && platformOverride;
if (m_Platform != null) {
var localDirection = (m_Grounded ?
m_Transform.InverseTransformDirection(m_GroundRaycastHit.point - m_GroundRaycastOrigin + m_Up * c_ColliderSpacing) :
Vector3.zero);
m_PlatformRelativePosition = m_Platform.InverseTransformPoint(m_Transform.position) + localDirection;
m_PlatformRotationOffset = m_Transform.rotation * Quaternion.Inverse(m_Platform.rotation);
} else if (platformOverride) {
TransferPlatformMovement();
}
return true;
}
/// <summary>
/// Transfers the platform movement from the platform onto the character.
/// </summary>
private void TransferPlatformMovement()
{
m_MotorThrottle += m_PlatformMovement;
m_Torque *= m_PlatformTorque;
m_PlatformVelocity = m_PlatformMovement = Vector3.zero;
m_PlatformTorque = Quaternion.identity;
}
/// <summary>
/// Updates the slope factor. This gives the option of slowing the character down while moving up a slope or increasing the speed while moving down.
/// </summary>
protected virtual void UpdateSlopeFactor()
{
// The character isn't on a slope while in the air.
if (!m_Grounded || !UsingVerticalCollisionDetection) {
m_SlopeFactor = 1;
return;
}
// Determine if the slope is uphill or downhill.
m_SlopeFactor = 1 + (1 - (Vector3.Angle(m_GroundRaycastHit.normal, m_MotorThrottle) / 90));
if (Mathf.Abs(1 - m_SlopeFactor) < 0.01f) { // Standing still, moving on flat ground, or moving perpendicular to a slope.
m_SlopeFactor = 1;
} else if (m_SlopeFactor > 1) { // Downhill.
m_SlopeFactor = m_MotorSlopeForceDown / m_SlopeFactor;
} else { // Uphill.
m_SlopeFactor *= m_MotorSlopeForceUp;
}
}
/// <summary>
/// Updates the grounded state.
/// </summary>
/// <param name="grounded">Is the character grounded?</param>
/// <param name="eventUpdate">Should the events be sent if the grounded status changes?</param>
/// <returns>True if the grounded state changed.</returns>
protected virtual bool UpdateGroundState(bool grounded, bool sendEvents)
{
// Update the grounded state. Allows for cleanup when the character hits the ground or moves into the air.
if (m_Grounded != grounded) {
m_Grounded = grounded;
return true;
}
return false;
}
/// <summary>
/// Applies the final move direction to the transform.
/// </summary>
protected virtual void ApplyPosition()
{
// Apply the position.
m_Transform.position = m_Transform.position + m_MoveDirection;
m_Velocity = (m_Transform.position - m_PrevPosition) / (m_TimeScale * Time.deltaTime);
m_PrevPosition = m_Transform.position;
m_MoveDirection = Vector3.zero;
// Update the platform variables.
if (m_Platform != null) {
m_PlatformRelativePosition = m_Platform.InverseTransformPoint(m_Transform.position);
}
}
/// <summary>
/// Updates any external forces.
/// </summary>
private void UpdateExternalForces()
{
// Apply a soft force (forces applied over several frames).
if (m_SoftForceFrames[0] != Vector3.zero) {
AddExternalForce(m_SoftForceFrames[0], false);
for (int v = 0; v < m_MaxSoftForceFrames; v++) {
m_SoftForceFrames[v] = (v < m_MaxSoftForceFrames - 1) ? m_SoftForceFrames[v + 1] : Vector3.zero;
if (m_SoftForceFrames[v] == Vector3.zero) {
break;
}
}
}
// Dampen external forces.
var deltaTime = m_TimeScale * TimeUtility.FramerateDeltaTime;
m_ExternalForce /= (1 + (m_Grounded ? m_ExternalForceDamping : m_ExternalForceAirDamping) * deltaTime);
}
/// <summary>
/// Callback from the animator when root motion has updated.
/// </summary>
protected virtual void OnAnimatorMove()
{
// The delta position will be NaN after the first respawn frame. The TimeScale must also be positive.
if (float.IsNaN(m_AnimatorDeltaPosition.x) || m_TimeScale == 0 || Time.timeScale == 0 || Time.deltaTime == 0) {
return;
}
if (m_AnimatorDeltaPosition.sqrMagnitude > 0) {
m_LocalRootMotionForce += m_Transform.InverseTransformDirection(m_AnimatorDeltaPosition) * m_RootMotionSpeedMultiplier;
m_AnimatorDeltaPosition = Vector3.zero;
}
if (m_AnimatorDeltaRotation != Quaternion.identity) {
float angle; Vector3 axis;
m_AnimatorDeltaRotation.ToAngleAxis(out angle, out axis);
angle *= m_RootMotionRotationMultiplier;
m_LocalRootMotionRotation *= Quaternion.AngleAxis(angle, axis);
m_AnimatorDeltaRotation = Quaternion.identity;
}
// Root motion has been retrieved from the animator. The rotation and position should now be applied based on the root motion data. This should be done within
// OnAnimatorMove so the rotation and position will be applied before the animator does its IK pass.
UpdatePositionAndRotation(true);
}
/// <summary>
/// Casts a ray in the specified direction. If the character has multiple colliders added then a ray will be cast from each collider.
/// A CapsuleCast or SphereCast is used depending on the type of collider that has been added. The result is stored in the m_CombinedRaycastHits array.
/// </summary>
/// <param name="direction">The direction to perform the cast.</param>
/// <param name="offset">Any offset to apply to the cast.</param>
/// <returns>The number of objects hit from the cast.</returns>
private int NonAllocCast(Vector3 direction, Vector3 offset)
{
if (m_ColliderCount > 1) {
// Clear the index map to start it off fresh.
m_ColliderIndexMap.Clear();
}
var hitCount = 0;
for (int i = 0; i < m_ColliderCount; ++i) {
int localHitCount;
// Determine if the collider would intersect any objects.
if (m_Colliders[i] is CapsuleCollider) {
Vector3 startEndCap, endEndCap;
var capsuleCollider = m_Colliders[i] as CapsuleCollider;
MathUtility.CapsuleColliderEndCaps(capsuleCollider, capsuleCollider.transform.position + offset, capsuleCollider.transform.rotation, out startEndCap, out endEndCap);
var radius = capsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(capsuleCollider) - c_ColliderSpacing;
localHitCount = Physics.CapsuleCastNonAlloc(startEndCap, endEndCap, radius, direction.normalized, m_RaycastHits, direction.magnitude + c_ColliderSpacing, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
} else { // SphereCollider.
var sphereCollider = m_Colliders[i] as SphereCollider;
var radius = sphereCollider.radius * MathUtility.ColliderRadiusMultiplier(sphereCollider) - c_ColliderSpacing;
localHitCount = Physics.SphereCastNonAlloc(sphereCollider.transform.TransformPoint(sphereCollider.center) + offset, radius, direction.normalized,
m_RaycastHits, direction.magnitude + c_ColliderSpacing, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
}
if (localHitCount > 0) {
// The mapping needs to be saved if there are multiple colliders.
if (m_ColliderCount > 1) {
int validHitCount = 0;
for (int j = 0; j < localHitCount; ++j) {
if (m_ColliderIndexMap.ContainsKey(m_RaycastHits[j])) {
continue;
}
// Ensure the array is large enough.
if (hitCount + j >= m_CombinedRaycastHits.Length) {
Debug.LogWarning("Warning: The maximum number of collisions has been reached. Consider increasing the CharacterLocomotion MaxCollisionCount value.");
continue;
}
m_ColliderIndexMap.Add(m_RaycastHits[j], i);
m_CombinedRaycastHits[hitCount + j] = m_RaycastHits[j];
validHitCount += 1;
}
hitCount += validHitCount;
} else {
m_CombinedRaycastHits = m_RaycastHits;
hitCount += localHitCount;
}
}
}
return hitCount;
}
/// <summary>
/// Casts a ray using in the specified direction. A CapsuleCast or SphereCast is used depending on the type of collider that has been added.
/// The result is stored in the m_RaycastHit object.
/// </summary>
/// <param name="collider">The collider which is performing the cast.</param>
/// <param name="direction">The direction to perform the cast.</param>
/// <param name="offset">Any offset to apply to the cast.</param>
/// <returns>Did the cast hit an object?</returns>
private bool SingleCast(Collider collider, Vector3 direction, Vector3 offset)
{
// Determine if the collider would intersect any objects.
if (collider is CapsuleCollider) {
Vector3 startEndCap, endEndCap;
var capsuleCollider = collider as CapsuleCollider;
MathUtility.CapsuleColliderEndCaps(capsuleCollider, capsuleCollider.transform.position + offset, capsuleCollider.transform.rotation, out startEndCap, out endEndCap);
var radius = capsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(capsuleCollider) - c_ColliderSpacing;
return Physics.CapsuleCast(startEndCap, endEndCap, radius, direction.normalized, out m_RaycastHit, direction.magnitude + c_ColliderSpacing, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
} else { // SphereCollider.
var sphereCollider = collider as SphereCollider;
var radius = sphereCollider.radius * MathUtility.ColliderRadiusMultiplier(sphereCollider) - c_ColliderSpacing;
return Physics.SphereCast(sphereCollider.transform.TransformPoint(sphereCollider.center) + offset, radius, direction.normalized,
out m_RaycastHit, direction.magnitude + c_ColliderSpacing, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
}
}
/// <summary>
/// Returns the number of colliders that are overlapping the character's collider.
/// </summary>
/// <param name="collider">The collider to check against.</param>
/// <param name="offset">The offset to apply to the character's collider position.</param>
/// <returns>The number of objects which overlap the collider. These objects will be populated within m_OverlapColliderHit.</returns>
protected int OverlapCount(Collider collider, Vector3 offset)
{
if (collider is CapsuleCollider) {
Vector3 startEndCap, endEndCap;
var capsuleCollider = collider as CapsuleCollider;
MathUtility.CapsuleColliderEndCaps(capsuleCollider, collider.transform.position + offset, collider.transform.rotation, out startEndCap, out endEndCap);
return Physics.OverlapCapsuleNonAlloc(startEndCap, endEndCap, capsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(capsuleCollider) - c_ColliderSpacing,
m_OverlapColliderHit, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
} else { // SphereCollider.
var sphereCollider = collider as SphereCollider;
return Physics.OverlapSphereNonAlloc(sphereCollider.transform.TransformPoint(sphereCollider.center) + offset,
sphereCollider.radius * MathUtility.ColliderRadiusMultiplier(sphereCollider) - c_ColliderSpacing,
m_OverlapColliderHit, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
}
}
/// <summary>
/// Resets the m_CombinedRaycastHits array to blank RaycastHit objects. This will prevent old hit objects from being used during the current frame.
/// </summary>
private void ResetCombinedRaycastHits()
{
if (m_CombinedRaycastHits == null) {
return;
}
for (int i = 0; i < m_CombinedRaycastHits.Length; ++i) {
if (m_CombinedRaycastHits[i].collider == null) {
break;
}
m_CombinedRaycastHits[i] = m_BlankRaycastHit;
}
}
/// <summary>
/// If the collision layer is disabled then all of the character's colliders will be set to an IgnoreRaycast layer. This
/// prevents any CapsuleCast or SphereCasts from returning a collider added to the character itself.
/// </summary>
/// <param name="enable"></param>
public void EnableColliderCollisionLayer(bool enable)
{
// Protect against duplicate enabled values changing the collider layer.
if (m_CollisionLayerEnabled == enable) {
return;
}
m_CollisionLayerEnabled = enable;
if (enable) {
for (int i = 0; i < m_ColliderCount; ++i) {
m_ColliderGameObjects[i].layer = m_ColliderLayers[i];
}
for (int i = 0; i < m_IgnoredColliderCount; ++i) {
m_IgnoredColliderGameObjects[i].layer = m_IgnoredColliderLayers[i];
}
} else {
for (int i = 0; i < m_ColliderCount; ++i) {
m_ColliderLayers[i] = m_ColliderGameObjects[i].layer;
m_ColliderGameObjects[i].layer = LayerManager.IgnoreRaycast;
}
for (int i = 0; i < m_IgnoredColliderCount; ++i) {
m_IgnoredColliderLayers[i] = m_IgnoredColliderGameObjects[i].layer;
m_IgnoredColliderGameObjects[i].layer = LayerManager.IgnoreRaycast;
}
}
}
/// <summary>
/// Adds a collider to the existing collider array.
/// </summary>
/// <param name="collider">The collider that should be added to the array.</param>
public void AddCollider(Collider collider)
{
m_ColliderCount = AddCollider(collider, ref m_Colliders, ref m_ColliderLayers, ref m_ColliderGameObjects, m_ColliderCount);
if (m_ColliderCount > 0 && m_ColliderIndexMap == null && m_Colliders.Length > 1) {
m_ColliderIndexMap = new Dictionary<RaycastHit, int>(new UnityEngineUtility.RaycastHitEqualityComparer());
}
}
/// <summary>
/// Adds a collider to the existing collider array.
/// </summary>
/// <param name="collider">The collider that should be added to the array.</param>
/// <param name="existingColliders">An array of colliders that the colliders array should be added to.</param>
/// <param name="existingColliderLayers">An array of existing collider layers that may need to be resized.</param>
/// <param name="existingColliderGameObjects">An array of existing collider GameObjects that should be updated.</param>
/// <param name="existingColliderCount">The count of the existing colliders array.</param>
/// <returns>The new count of the existing colliders array.</returns>
private int AddCollider(Collider collider, ref Collider[] existingColliders, ref int[] existingColliderLayers, ref GameObject[] existingColliderGameObjects, int existingColliderCount)
{
// Don't add an already added collider.
for (int i = 0; i < existingColliderCount; ++i) {
if (existingColliders[i] == collider) {
return existingColliderCount;
}
}
// The collider should be added.
if (existingColliderCount == existingColliders.Length) {
Array.Resize(ref existingColliders, existingColliders.Length + 1);
Array.Resize(ref existingColliderLayers, existingColliderLayers.Length + 1);
Array.Resize(ref existingColliderGameObjects, existingColliderGameObjects.Length + 1);
}
existingColliders[existingColliderCount] = collider;
existingColliderGameObjects[existingColliderCount] = collider.gameObject;
if (!m_CollisionLayerEnabled) {
existingColliderLayers[existingColliderCount] = existingColliderGameObjects[existingColliderCount].layer;
existingColliderGameObjects[existingColliderCount].layer = LayerManager.IgnoreRaycast;
}
return existingColliderCount + 1;
}
/// <summary>
/// Removes the specified collider from the collider array.
/// </summary>
/// <param name="collider">The collider which should be removed from the array.</param>
public void RemoveCollider(Collider collider)
{
m_ColliderCount = RemoveCollider(collider, ref m_Colliders, ref m_ColliderLayers, ref m_ColliderGameObjects, m_ColliderCount);
}
/// <summary>
/// Removes the specified collider from the collider array.
/// </summary>
/// <param name="collider">The collider which should be removed from the array.</param>
/// <param name="existingColliders">An array of colliders that the colliders array should be removed from.</param>
/// <param name="existingColliderLayers">An array of existing collider layers that may need to be resized.</param>
/// <param name="existingColliderGameObjects">An array of existing collider GameObjects that should be updated.</param>
/// <param name="existingColliderCount">The count of the existing colliders array.</param>
/// <returns>The number of colliders within the existing colliders array.</returns>
public int RemoveCollider(Collider collider, ref Collider[] existingColliders, ref int[] existingColliderLayers, ref GameObject[] existingColliderGameObjects, int existingColliderCount)
{
for (int i = existingColliders.Length - 1; i > -1; --i) {
if (existingColliders[i] != collider) {
continue;
}
// The collider may be removed when collisions are disabled. The layer should be reverted back to its original.
if (!m_CollisionLayerEnabled) {
existingColliderGameObjects[i].layer = existingColliderLayers[i];
}
// Do not resize the array for performance reasons. Move all of the next colliders back a slot instead.
for (int j = i; j < existingColliderCount - 1; ++j) {
existingColliders[j] = existingColliders[j + 1];
existingColliderLayers[j] = existingColliderLayers[j + 1];
existingColliderGameObjects[j] = existingColliderGameObjects[j + 1];
}
existingColliderCount--;
existingColliders[i] = null;
existingColliderGameObjects[i] = null;
}
return existingColliderCount;
}
/// <summary>
/// Adds an array to the collider array.
/// </summary>
/// <param name="colliders">The colliders which should be added to the array.</param>
public void AddColliders(Collider[] colliders)
{
m_ColliderCount = AddColliders(colliders, ref m_Colliders, ref m_ColliderLayers, ref m_ColliderGameObjects, m_ColliderCount);
if (m_ColliderIndexMap == null && m_Colliders.Length > 1) {
m_ColliderIndexMap = new Dictionary<RaycastHit, int>(new UnityEngineUtility.RaycastHitEqualityComparer());
}
}
/// <summary>
/// Adds the colliders array to the existing colliders array. The existing colliders array length will be resized if the new
/// set of colliders won't fit.
/// </summary>
/// <param name="colliders">The colliders that should be added to the existing colliders.</param>
/// <param name="existingColliders">An array of colliders that the colliders array should be added to.</param>
/// <param name="existingColliderLayers">An array of existing collider layers that may need to be resized.</param>
/// <param name="existingColliderGameObjects">An array of existing collider GameObjects that should be updated.</param>
/// <param name="existingColliderCount">The count of the existing colliders array.</param>
/// <returns>The new count of the existing colliders array.</returns>
private int AddColliders(Collider[] colliders, ref Collider[] existingColliders, ref int[] existingColliderLayers, ref GameObject[] existingColliderGameObjects, int existingColliderCount)
{
// The array may need to be increased with the new colliders.
if (existingColliders.Length < existingColliderCount + colliders.Length) {
var diff = (existingColliderCount + colliders.Length) - existingColliders.Length;
Array.Resize(ref existingColliders, existingColliders.Length + diff);
Array.Resize(ref existingColliderLayers, existingColliderLayers.Length + diff);
Array.Resize(ref existingColliderGameObjects, existingColliderGameObjects.Length + diff);
}
var startCount = existingColliderCount;
for (int i = 0; i < colliders.Length; ++i) {
if (colliders[i] == null) {
continue;
}
// Don't add an already added collider.
var addCollider = true;
for (int j = 0; j < startCount; ++j) {
if (colliders[i] == existingColliders[j]) {
addCollider = false;
break;
}
}
// The collider is new - add it to the array.
if (addCollider) {
existingColliders[existingColliderCount] = colliders[i];
existingColliderGameObjects[existingColliderCount] = colliders[i].gameObject;
if (!m_CollisionLayerEnabled) {
existingColliderLayers[existingColliderCount] = existingColliderGameObjects[existingColliderCount].layer;
existingColliderGameObjects[existingColliderCount].layer = LayerManager.IgnoreRaycast;
}
existingColliderCount++;
}
}
// Return the new collider count.
return existingColliderCount;
}
/// <summary>
/// Removes the specified colliders from the collider array.
/// </summary>
/// <param name="colliders">The colliders that should be removed.</param>
public void RemoveColliders(Collider[] colliders)
{
m_ColliderCount = RemoveColliders(colliders, ref m_Colliders, ref m_ColliderLayers, ref m_ColliderGameObjects, m_ColliderCount);
}
/// <summary>
/// Removes the colliders from the collider array.
/// </summary>
/// <param name="colliders">The colliders that should be removed.</param>
/// <param name="existingColliders">An array of colliders that the colliders array should be removed from.</param>
/// <param name="existingColliderLayers">An array of existing collider layers that may need to be resized.</param>
/// <param name="existingColliderGameObjects">An array of existing collider GameObjects that should be updated.</param>
/// <param name="existingColliderCount">The count of the existing colliders array.</param>
/// <returns>The number of colliders within the existing colliders array.</returns>
private int RemoveColliders(Collider[] colliders, ref Collider[] existingColliders, ref int[] existingColliderLayers, ref GameObject[] existingColliderGameObjects, int existingColliderCount)
{
for (int i = existingColliderCount - 1; i > -1; --i) {
for (int j = colliders.Length - 1; j > -1; --j) {
if (existingColliders[i] != colliders[j]) {
continue;
}
// The collider may be removed when collisions are disabled. The layer should be reverted back to its original.
if (!m_CollisionLayerEnabled) {
existingColliderGameObjects[i].layer = existingColliderLayers[i];
}
// Do not resize the array for performance reasons. Move all of the next colliders back a slot instead.
for (int k = i; k < existingColliderCount - 1; ++k) {
existingColliders[k] = existingColliders[k + 1];
existingColliderLayers[k] = existingColliderLayers[k + 1];
existingColliderGameObjects[k] = existingColliderGameObjects[k + 1];
}
existingColliderCount--;
existingColliders[existingColliderCount] = null;
existingColliderGameObjects[existingColliderCount] = null;
break;
}
}
return existingColliderCount;
}
/// <summary>
/// Adds an element to the ignored collider array.
/// </summary>
/// <param name="collider">The collider which should be added to the array.</param>
public void AddIgnoredCollider(Collider collider)
{
m_IgnoredColliderCount = AddCollider(collider, ref m_IgnoredColliders, ref m_IgnoredColliderLayers, ref m_IgnoredColliderGameObjects, m_IgnoredColliderCount);
}
/// <summary>
/// Removes the specified collider from the ignored collider array.
/// </summary>
/// <param name="collider">The collider which should be removed from the array.</param>
public void RemoveIgnoredCollider(Collider collider)
{
m_IgnoredColliderCount = RemoveCollider(collider, ref m_IgnoredColliders, ref m_IgnoredColliderLayers, ref m_IgnoredColliderGameObjects, m_IgnoredColliderCount);
}
/// <summary>
/// Adds an array to the ignored collider array.
/// </summary>
/// <param name="colliders">The colliders which should be added to the array.</param>
public void AddIgnoredColliders(Collider[] colliders)
{
m_IgnoredColliderCount = AddColliders(colliders, ref m_IgnoredColliders, ref m_IgnoredColliderLayers, ref m_IgnoredColliderGameObjects, m_IgnoredColliderCount);
}
/// <summary>
/// Removes the specified colliders from the ignored collider array.
/// </summary>
/// <param name="colliders">The colliders that should be removed.</param>
public void RemoveIgnoredColliders(Collider[] colliders)
{
m_IgnoredColliderCount = RemoveColliders(colliders, ref m_IgnoredColliders, ref m_IgnoredColliderLayers, ref m_IgnoredColliderGameObjects, m_IgnoredColliderCount);
}
/// <summary>
/// Adds a force to the character. The force will either be an external or soft force.
/// </summary>
/// <param name="force">The force to add.</param>
public void AddForce(Vector3 force)
{
AddForce(force, 1, true, true);
}
/// <summary>
/// Adds a force to the character in the specified number of frames. The force will either be an external or soft force.
/// </summary>
/// <param name="force">The force to add.</param>
/// <param name="frames">The number of frames to add the force to.</param>
public void AddForce(Vector3 force, int frames)
{
AddForce(force, frames, true, true);
}
/// <summary>
/// Adds a force to the character in the specified number of frames. The force will either be an external or soft force.
/// </summary>
/// <param name="force">The force to add.</param>
/// <param name="frames">The number of frames to add the force to.</param>
/// <param name="scaleByTime">Should the force be scaled by the timescale?</param>
/// <param name="scaleByMass">Should the force be scaled by the character's mass?</param>
public void AddForce(Vector3 force, int frames, bool scaleByMass, bool scaleByTime)
{
if (scaleByMass) {
force /= m_Mass;
}
if (frames > 1) {
AddSoftForce(force, frames, scaleByTime);
} else {
AddExternalForce(force, scaleByTime);
}
}
/// <summary>
/// Adds an external force to add.
/// </summary>
/// <param name="force">The force to add.</param>
private void AddExternalForce(Vector3 force)
{
AddExternalForce(force, true);
}
/// <summary>
/// Adds an external force to add.
/// </summary>
/// <param name="force">The force to add.</param>
/// <param name="scaleByTime">Should the force be scaled by the timescale?</param>
private void AddExternalForce(Vector3 force, bool scaleByTime)
{
// The force may already account for a variable time scale in which case the force should not be scaled by time. For example, the jump damping force.
if (scaleByTime) {
var timeScale = m_TimeScale * Time.timeScale;
if (timeScale < 1) {
force /= timeScale;
}
}
m_ExternalForce += force;
}
/// <summary>
/// Adds a soft force to the character. A soft force is spread out through up to c_MaxSoftForceFrames frames.
/// </summary>
/// <param name="force">The force to add.</param>
/// <param name="frames">The number of frames to add the force to.</param>
private void AddSoftForce(Vector3 force, float frames)
{
AddSoftForce(force, frames, true);
}
/// <summary>
/// Adds a soft force to the character. A soft force is spread out through up to c_MaxSoftForceFrames frames.
/// </summary>
/// <param name="force">The force to add.</param>
/// <param name="frames">The number of frames to add the force to.</param>
/// <param name="scaleByTime">Should the force be scaled by the timescale?</param>
private void AddSoftForce(Vector3 force, float frames, bool scaleByTime)
{
frames = Mathf.Clamp(frames, 1, m_MaxSoftForceFrames);
AddExternalForce(force / frames, scaleByTime);
var timeScale = (scaleByTime ? m_TimeScale * Time.timeScale : 1f);
for (int v = 0; v < (Mathf.RoundToInt(frames) - 1); v++) {
m_SoftForceFrames[v] += (force / (frames * timeScale));
}
}
/// <summary>
/// Adds a force relative to the character. The force will either be an external or soft force.
/// </summary>
/// <param name="force">The force to add.</param>
public void AddRelativeForce(Vector3 force)
{
AddRelativeForce(force, 1);
}
/// <summary>
/// Adds a force relative to the character in the specified number of frames. The force will either be an external or soft force.
/// </summary>
/// <param name="force">The force to add.</param>
/// <param name="frames">The number of frames to add the force to.</param>
public void AddRelativeForce(Vector3 force, int frames)
{
AddRelativeForce(force, frames, true, true);
}
/// <summary>
/// Adds a force relative to the character in the specified number of frames. The force will either be an external or soft force.
/// </summary>
/// <param name="force">The force to add.</param>
/// <param name="frames">The number of frames to add the force to.</param>
/// <param name="scaleByMass">Should the force be scaled by the character's mass?</param>
/// <param name="scaleByTime">Should the force be scaled by the timescale?</param>
public void AddRelativeForce(Vector3 force, int frames, bool scaleByMass, bool scaleByTime)
{
// Convert the force into a relative force.
force = m_Transform.InverseTransformVector(force);
AddForce(force, frames, scaleByMass, scaleByTime);
}
/// <summary>
/// Pushes the target Rigidbody in the specified direction.
/// </summary>
/// <param name="targetRigidbody">The Rigidbody to push.</param>
/// <param name="moveDirection">The direction that the character is moving.</param>
/// <param name="point">The point at which to apply the push force.</param>
/// <param name="radius">The radius of the pushing collider.</param>
/// <returns>Was the rigidbody pushed?</returns>
protected virtual bool PushRigidbody(Rigidbody targetRigidbody, Vector3 moveDirection, Vector3 point, float radius)
{
if (targetRigidbody.isKinematic) {
return false;
}
targetRigidbody.AddForceAtPosition((moveDirection / Time.deltaTime) * (m_Mass / targetRigidbody.mass) * 0.01f, point, ForceMode.VelocityChange);
return targetRigidbody.linearVelocity.sqrMagnitude > 0.1f;
}
/// <summary>
/// Returns the collider which contains the point within its bounding box.
/// </summary>
/// <param name="point">The point to determine if it is within the bounding box of the character.</param>
/// <returns>The collider which contains the point within its bounding box. Can be null.</returns>
public Collider BoundsCountains(Vector3 point)
{
for (int i = 0; i < m_ColliderCount; ++i) {
if (m_Colliders[i].bounds.Contains(point)) {
return m_Colliders[i];
}
}
return null;
}
/// <summary>
/// Sets the rotation of the character.
/// </summary>
/// <param name="rotation">The rotation to set.</param>
public virtual void SetRotation(Quaternion rotation)
{
m_Transform.rotation = m_MotorRotation = m_PrevMotorRotation = rotation;
m_LocalRootMotionRotation = m_Torque = Quaternion.identity;
m_Up = m_Transform.up;
if (m_AlignToGravity) {
m_GravityDirection = -m_Up;
}
}
/// <summary>
/// Sets the position of the character.
/// </summary>
/// <param name="position">The position to set.</param>
public virtual void SetPosition(Vector3 position)
{
m_Transform.position = m_PrevPosition = position;
m_MotorThrottle = m_LocalRootMotionForce = m_MoveDirection = m_Velocity = m_ExternalForce = Vector3.zero;
m_GravityAmount = 0;
m_GroundHitTransform = null;
if (m_Platform != null) {
if (m_PlatformOverride) {
m_PlatformRelativePosition = m_Platform.InverseTransformPoint(m_Transform.position);
} else {
// Remove the moving platform if the character changes position. It will be set again on the next update if the character is on a moving platform.
UpdateMovingPlatformTransform(null, true);
}
}
}
/// <summary>
/// Resets the rotation and position to their default values.
/// </summary>
public virtual void ResetRotationPosition()
{
m_MotorRotation = m_PrevMotorRotation = m_Transform.rotation;
m_Up = m_Transform.up;
m_LocalRootMotionRotation = m_Torque = Quaternion.identity;
if (m_AlignToGravity) {
m_GravityDirection = -m_Up;
}
m_PrevPosition = m_Transform.position;
m_MotorThrottle = m_LocalRootMotionForce = m_MoveDirection = m_Velocity = m_ExternalForce = Vector3.zero;
}
/// <summary>
/// Resets the variables to the default values.
/// </summary>
private void Reset()
{
AddDefaultSmoothedBones();
}
/// <summary>
/// Adds the default humanoid smoothed bones.
/// </summary>
public void AddDefaultSmoothedBones()
{
var animator = gameObject.GetComponent<Animator>();
if (animator == null || !animator.isHuman) {
return;
}
// The smoothed bone variable should be populated with all of the humanoid bones.
var bones = new List<Transform>();
AddBone(bones, animator, HumanBodyBones.Spine);
AddBone(bones, animator, HumanBodyBones.Chest);
AddBone(bones, animator, HumanBodyBones.Neck);
AddBone(bones, animator, HumanBodyBones.Head);
AddBone(bones, animator, HumanBodyBones.LeftShoulder);
AddBone(bones, animator, HumanBodyBones.LeftUpperArm);
AddBone(bones, animator, HumanBodyBones.LeftLowerArm);
AddBone(bones, animator, HumanBodyBones.LeftHand);
AddBone(bones, animator, HumanBodyBones.RightShoulder);
AddBone(bones, animator, HumanBodyBones.RightUpperArm);
AddBone(bones, animator, HumanBodyBones.RightLowerArm);
AddBone(bones, animator, HumanBodyBones.RightHand);
if (bones.Count > 0) {
m_SmoothedBones = bones.ToArray();
}
}
/// <summary>
/// Adds the bone to the list if the bone exists.
/// </summary>
/// <param name="bones">The list of current bones.</param>
/// <param name="animator">A reference to the character's animator.</param>
/// <param name="bone">The humanoid bone that should be added if it exists.</param>
private void AddBone(List<Transform> bones, Animator animator, HumanBodyBones bone)
{
var boneTransform = animator.GetBoneTransform(bone);
if (boneTransform != null) {
bones.Add(boneTransform);
}
}
}
}