Files
BABA_YAGA/Assets/Third Parties/Opsive/UltimateCharacterController/Scripts/Character/CapsuleColliderPositioner.cs
2026-06-09 09:18:17 +07:00

345 lines
18 KiB
C#

/// ---------------------------------------------
/// 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;
/// <summary>
/// Rotates and sets the CapsuleCollider height so it always matches the same relative location/size of the character.
/// </summary>
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;
/// <summary>
/// Initialize the default values.
/// </summary>
protected override void Awake()
{
base.Awake();
m_Transform = transform;
m_CapsuleCollider = GetComponent<CapsuleCollider>();
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<UltimateCharacterLocomotion>();
m_CharacterGameObject = m_CharacterLocomotion.gameObject;
m_CharacterTransform = m_CharacterLocomotion.transform;
m_CharacterLayerManager = gameObject.GetCachedParentComponent<CharacterLayerManager>();
m_OverlapColliders = new Collider[1];
EventHandler.RegisterEvent(m_CharacterGameObject, "OnAnimatorSnapped", AnimatorSnapped);
EventHandler.RegisterEvent<bool>(m_CharacterGameObject, "OnCharacterImmediateTransformChange", OnImmediateTransformChange);
EventHandler.RegisterEvent<Vector3, Vector3, GameObject>(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<Networking.INetworkInfo>();
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
}
/// <summary>
/// The animator has snapped into position. Reinitialized.
/// </summary>
private void AnimatorSnapped()
{
// The positioner only needs to be initialized once.
EventHandler.UnregisterEvent(m_CharacterGameObject, "OnAnimatorSnapped", AnimatorSnapped);
Initialize();
}
/// <summary>
/// Sets the initial end cap offsets.
/// </summary>
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<float>(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;
}
/// <summary>
/// Perform the rotation and height changes.
/// </summary>
private void FixedUpdate()
{
UpdateRotationHeight();
}
/// <summary>
/// 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.
/// </summary>
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);
}
/// <summary>
/// Adjusts the collider height by the given amount.
/// </summary>
/// <param name="amount">The amount to adjust the collider height by.</param>
private void AdjustCapsuleColliderHeight(float amount)
{
var localPosition = m_SecondEndCapTarget.localPosition;
localPosition.y = m_CapsuleCollider.height + amount;
m_SecondEndCapTarget.localPosition = localPosition;
}
/// <summary>
/// Adjusts the collider's center position to the specified value.
/// </summary>
/// <param name="targetOffset">The desired offset value.</param>
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);
}
/// <summary>
/// The character has died.
/// </summary>
/// <param name="position">The position of the force.</param>
/// <param name="force">The amount of force which killed the character.</param>
/// <param name="attacker">The GameObject that killed the character.</param>
private void OnDeath(Vector3 position, Vector3 force, GameObject attacker)
{
enabled = false;
}
/// <summary>
/// The character has respawned.
/// </summary>
private void OnRespawn()
{
enabled = true;
}
/// <summary>
/// The character's position or rotation has been teleported.
/// </summary>
/// <param name="snapAnimator">Should the animator be snapped?</param>
private void OnImmediateTransformChange(bool snapAnimator)
{
if (!enabled) {
return;
}
UpdateRotationHeight();
}
/// <summary>
/// The object has been destroyed.
/// </summary>
private void OnDestroy()
{
Scheduler.Cancel(m_ColliderOffsetEvent);
EventHandler.UnregisterEvent(m_CharacterGameObject, "OnAnimatorSnapped", Initialize);
EventHandler.UnregisterEvent<float>(m_CharacterGameObject, "OnHeightChangeAdjustHeight", AdjustCapsuleColliderHeight);
EventHandler.UnregisterEvent<bool>(m_CharacterGameObject, "OnCharacterImmediateTransformChange", OnImmediateTransformChange);
EventHandler.UnregisterEvent<Vector3, Vector3, GameObject>(m_CharacterGameObject, "OnDeath", OnDeath);
EventHandler.UnregisterEvent(m_CharacterGameObject, "OnRespawn", OnRespawn);
}
}
}