/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.Character { using Opsive.Shared.Utility; using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.UltimateCharacterController.StateSystem; using Opsive.UltimateCharacterController.Utility; using UnityEngine; /// /// Rotates and sets the CapsuleCollider height so it always matches the same relative location/size of the character. /// public class CapsuleColliderPositioner : StateBehavior { [Tooltip("Should the positioner rotate the collider to match the targets?")] [SerializeField] protected bool m_RotateCollider; [Tooltip("If Rotate Collider is enabled, should the collider be rotated based on the end caps? If false the collider will be rotated based on the specified bone.")] [SerializeField] protected bool m_EndCapRotation = true; [Tooltip("Specifies the bone that the collider should rotate with if not using End Cap Rotation.")] [SerializeField] protected Transform m_RotationBone; [Tooltip("An offset to apply to the rotation bone.")] [SerializeField] protected Vector3 m_RotationBoneOffset; [Tooltip("Should the positioner adjust the height of the collider?")] [SerializeField] protected bool m_AdjustHeight = true; [Tooltip("A reference to the target that is near the first end cap.")] [SerializeField] protected Transform m_FirstEndCapTarget; [Tooltip("A reference to the target that is near the second end cap.")] [SerializeField] protected Transform m_SecondEndCapTarget; [Tooltip("The padding on top of the second end cap target.")] [SerializeField] protected float m_SecondEndCapPadding; [Tooltip("Specifies the bone that the collider should positioned to if not using Adjust Height.")] [SerializeField] protected Transform m_PositionBone; [Tooltip("The overridden height if the height is not being adjusted. Set to -1 to disable.")] [SerializeField] protected float m_HeightOverride = -1; [Tooltip("The offset to apply to the collider's center position.")] [SerializeField] protected Vector3 m_CenterOffset; public bool RotateCollider { get { return m_RotateCollider; } set { if (m_RotateCollider != value) { // Remember the original rotation so the value can be restored if the collider is no longer being rotated. if (value) { m_PrevLocalRotation = m_Transform.localRotation; UpdateRotationHeight(); } else { m_Transform.localRotation = m_PrevLocalRotation; } m_RotateCollider = value; } } } public bool EndCapRotation { get { return m_EndCapRotation; } set { m_EndCapRotation = value; } } [NonSerialized] public Transform RotationBone { get { return m_RotationBone; } set { m_RotationBone = value; } } public Vector3 RotationBoneOffset { get { return m_RotationBoneOffset; } set { m_RotationBoneOffset = value; } } public bool AdjustHeight { get { return m_AdjustHeight; } set { if (m_AdjustHeight != value) { m_AdjustHeight = value; if (m_AdjustHeight) { m_Transform.localPosition = Vector3.zero; UpdateRotationHeight(); } } } } [NonSerialized] public Transform FirstEndCapTarget { get { return m_FirstEndCapTarget; } set { m_FirstEndCapTarget = value; } } [NonSerialized] public Transform SecondEndCapTarget { get { return m_SecondEndCapTarget; } set { m_SecondEndCapTarget = value; Initialize(); } } [NonSerialized] public float SecondEndCapPadding { get { return m_SecondEndCapPadding; } set { m_SecondEndCapPadding = value; } } [NonSerialized] public Transform PositionBone { get { return m_PositionBone; } set { m_PositionBone = value; } } public float HeightOverride { get { return m_HeightOverride; } set { m_HeightOverride = value; } } public Vector3 CenterOffset { get { return m_CenterOffset; } set { if (m_ColliderOffsetEvent != null) { Scheduler.Cancel(m_ColliderOffsetEvent); m_ColliderOffsetEvent = null; } AdjustCenterOffset(value); } } private Transform m_Transform; private CapsuleCollider m_CapsuleCollider; private UltimateCharacterLocomotion m_CharacterLocomotion; private GameObject m_CharacterGameObject; private Transform m_CharacterTransform; private CharacterLayerManager m_CharacterLayerManager; private Collider[] m_OverlapColliders; private Vector3 m_FirstEndCapOffset; private Vector3 m_SecondEndCapOffset; private Vector3 m_FirstEndCapLocalPosition; private Quaternion m_PrevLocalRotation; private ScheduledEventBase m_ColliderOffsetEvent; /// /// Initialize the default values. /// protected override void Awake() { base.Awake(); m_Transform = transform; m_CapsuleCollider = GetComponent(); if (m_CapsuleCollider.direction != 1) { Debug.LogError("Error: The CapsuleColliderPositioner only works with capsule colliders that are in the Y-axis direction."); enabled = false; return; } m_CharacterLocomotion = gameObject.GetCachedParentComponent(); m_CharacterGameObject = m_CharacterLocomotion.gameObject; m_CharacterTransform = m_CharacterLocomotion.transform; m_CharacterLayerManager = gameObject.GetCachedParentComponent(); m_OverlapColliders = new Collider[1]; EventHandler.RegisterEvent(m_CharacterGameObject, "OnAnimatorSnapped", AnimatorSnapped); EventHandler.RegisterEvent(m_CharacterGameObject, "OnCharacterImmediateTransformChange", OnImmediateTransformChange); EventHandler.RegisterEvent(m_CharacterGameObject, "OnDeath", OnDeath); EventHandler.RegisterEvent(m_CharacterGameObject, "OnRespawn", OnRespawn); Initialize(); #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER // The positioner cannot be used with server autoritative implementations. var networkInfo = m_CharacterGameObject.GetCachedComponent(); if (networkInfo != null && networkInfo.IsServerAuthoritative()) { enabled = false; Debug.LogWarning("Warning: The CapsuleColliderPositioner has been disabled. Unity bug 985643 needs to be fixed for it to work over a server authoritative network."); } #endif } /// /// The animator has snapped into position. Reinitialized. /// private void AnimatorSnapped() { // The positioner only needs to be initialized once. EventHandler.UnregisterEvent(m_CharacterGameObject, "OnAnimatorSnapped", AnimatorSnapped); Initialize(); } /// /// Sets the initial end cap offsets. /// private void Initialize() { // If the first and second end cap targets are null then the component is likely added at runtime. if (!Application.isPlaying || (m_FirstEndCapTarget == null && m_SecondEndCapTarget == null)) { return; } // If the CapsuleCollider doesn't have a second end cap then the character doesn't have an animator. The end cap should be // a child GameObject so abilities (such as HeightChange) can adjust the collider height. if (m_SecondEndCapTarget == null) { m_SecondEndCapTarget = new GameObject("EndCap").transform; m_SecondEndCapTarget.SetParentOrigin(m_Transform); var localPosition = m_SecondEndCapTarget.localPosition; localPosition.y = m_CapsuleCollider.height; m_SecondEndCapTarget.localPosition = localPosition; EventHandler.RegisterEvent(m_CharacterGameObject, "OnHeightChangeAdjustHeight", AdjustCapsuleColliderHeight); } Vector3 firstEndCap, secondEndCap; MathUtility.CapsuleColliderEndCaps(m_CapsuleCollider, m_Transform.position, m_Transform.rotation, out firstEndCap, out secondEndCap); if ((m_FirstEndCapTarget.position - firstEndCap).sqrMagnitude > (m_SecondEndCapTarget.position - firstEndCap).sqrMagnitude) { // The second target may be closer to the first end cap than the first target is. Switch the targets. var target = m_FirstEndCapTarget; m_FirstEndCapTarget = m_SecondEndCapTarget; m_SecondEndCapTarget = target; } m_Transform.position = m_FirstEndCapTarget.position; m_FirstEndCapLocalPosition = m_FirstEndCapTarget.localPosition; m_FirstEndCapOffset = m_Transform.InverseTransformDirection(m_FirstEndCapTarget.position - firstEndCap); m_SecondEndCapOffset = m_Transform.InverseTransformDirection(m_SecondEndCapTarget.position - secondEndCap) - m_CharacterTransform.up * m_SecondEndCapPadding; } /// /// Perform the rotation and height changes. /// private void FixedUpdate() { UpdateRotationHeight(); } /// /// Does the actual rotation and height changes. This method is separate from FixedUpdate so the rotation/height can be determined /// during server reconciliation on the network. /// public void UpdateRotationHeight() { m_CharacterLocomotion.EnableColliderCollisionLayer(false); // The first end cap may change positions. if (m_FirstEndCapTarget.localPosition != m_FirstEndCapLocalPosition) { m_Transform.position = m_FirstEndCapTarget.position; m_FirstEndCapLocalPosition = m_FirstEndCapTarget.localPosition; } Vector3 localDirection; if (m_RotateCollider) { if (m_EndCapRotation) { // Update the rotation of the CapsuleCollider so it is rotated in the same direction as the end cap targets. var direction = m_SecondEndCapTarget.position - m_FirstEndCapTarget.position; m_Transform.rotation = Quaternion.LookRotation(Vector3.Cross(m_CharacterTransform.forward, direction.normalized), direction.normalized); } else { m_Transform.rotation = m_RotationBone.rotation * Quaternion.Euler(m_RotationBoneOffset); } } if (m_AdjustHeight) { // After the CapsuleCollider has rotated determine the new height of the CapsuleCollider. This can be done by determining the current // end cap locations and then getting the offset from the start end cap offsets. Vector3 firstEndCap, secondEndCap; MathUtility.CapsuleColliderEndCaps(m_CapsuleCollider, m_Transform.position, m_Transform.rotation, out firstEndCap, out secondEndCap); var firstEndCapOffset = m_Transform.InverseTransformDirection(m_FirstEndCapTarget.position - firstEndCap); var secondEndCapOffset = m_Transform.InverseTransformDirection(m_SecondEndCapTarget.position - secondEndCap); var offset = m_SecondEndCapOffset - m_FirstEndCapOffset; localDirection = ((secondEndCapOffset - firstEndCapOffset) - offset); // Determine if the new height would cause any collisions. If it does not then apply the height changes. A negative height change will never cause any // collisions so the OverlapCapsule does not need to be checked. A valid capsule collider height is always greater than 2 times the radius of the collider. var heightMultiplier = MathUtility.CapsuleColliderHeightMultiplier(m_CapsuleCollider); var targetHeight = m_CapsuleCollider.height + localDirection.y / heightMultiplier; if (targetHeight >= m_CapsuleCollider.radius * 2 && (localDirection.y < 0 || !m_CharacterLocomotion.UsingVerticalCollisionDetection || Physics.OverlapCapsuleNonAlloc(firstEndCap, secondEndCap + m_CharacterLocomotion.Up * localDirection.y, m_CapsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(m_CapsuleCollider), m_OverlapColliders, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore) == 0)) { // Adjust the CapsuleCollider height and center to account for the new offset. m_CapsuleCollider.height = targetHeight; var center = m_CapsuleCollider.center; center.y += localDirection.y / (heightMultiplier * 2); m_CapsuleCollider.center = center; } } else { if (m_PositionBone != null) { m_Transform.position = MathUtility.TransformPoint(m_PositionBone.position, m_Transform.rotation, -m_CapsuleCollider.center); } if (m_HeightOverride != -1) { m_CapsuleCollider.height = m_HeightOverride; var center = m_CapsuleCollider.center; center.y = m_CapsuleCollider.height / 2 + m_CenterOffset.y; m_CapsuleCollider.center = center; } } m_CharacterLocomotion.EnableColliderCollisionLayer(true); } /// /// Adjusts the collider height by the given amount. /// /// The amount to adjust the collider height by. private void AdjustCapsuleColliderHeight(float amount) { var localPosition = m_SecondEndCapTarget.localPosition; localPosition.y = m_CapsuleCollider.height + amount; m_SecondEndCapTarget.localPosition = localPosition; } /// /// Adjusts the collider's center position to the specified value. /// /// The desired offset value. private void AdjustCenterOffset(Vector3 targetOffset) { var delta = targetOffset - m_CenterOffset; m_CapsuleCollider.center += delta; m_CapsuleCollider.height += delta.y / 2; if (!m_CharacterLocomotion.UsingHorizontalCollisionDetection) { m_CenterOffset = targetOffset; return; } // Apply the offset if there are no collisions. var collisionEnabled = m_CharacterLocomotion.CollisionLayerEnabled; m_CharacterLocomotion.EnableColliderCollisionLayer(false); Vector3 firstEndCap, secondEndCap; MathUtility.CapsuleColliderEndCaps(m_CapsuleCollider, m_Transform.position, m_Transform.rotation, out firstEndCap, out secondEndCap); if (Physics.OverlapCapsuleNonAlloc(firstEndCap, secondEndCap, m_CapsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(m_CapsuleCollider), m_OverlapColliders, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore) > 0) { m_CapsuleCollider.center -= delta; m_CapsuleCollider.height -= delta.y / 2; m_ColliderOffsetEvent = Scheduler.Schedule(Time.fixedDeltaTime, AdjustCenterOffset, targetOffset); } else { m_CenterOffset = targetOffset; } m_CharacterLocomotion.EnableColliderCollisionLayer(collisionEnabled); } /// /// The character has died. /// /// The position of the force. /// The amount of force which killed the character. /// The GameObject that killed the character. private void OnDeath(Vector3 position, Vector3 force, GameObject attacker) { enabled = false; } /// /// The character has respawned. /// private void OnRespawn() { enabled = true; } /// /// The character's position or rotation has been teleported. /// /// Should the animator be snapped? private void OnImmediateTransformChange(bool snapAnimator) { if (!enabled) { return; } UpdateRotationHeight(); } /// /// The object has been destroyed. /// private void OnDestroy() { Scheduler.Cancel(m_ColliderOffsetEvent); EventHandler.UnregisterEvent(m_CharacterGameObject, "OnAnimatorSnapped", Initialize); EventHandler.UnregisterEvent(m_CharacterGameObject, "OnHeightChangeAdjustHeight", AdjustCapsuleColliderHeight); EventHandler.UnregisterEvent(m_CharacterGameObject, "OnCharacterImmediateTransformChange", OnImmediateTransformChange); EventHandler.UnregisterEvent(m_CharacterGameObject, "OnDeath", OnDeath); EventHandler.UnregisterEvent(m_CharacterGameObject, "OnRespawn", OnRespawn); } } }