//#define MB_DEBUG using UnityEngine; using MenteBacata.ScivoloCharacterController.Internal; using static MenteBacata.ScivoloCharacterController.Internal.OverlapResolver; namespace MenteBacata.ScivoloCharacterController { public class CharacterCapsule : MonoBehaviour { [SerializeField] [Tooltip("Height of the capsule.")] private float height = 2f; [SerializeField] [Tooltip("Raidus of the capsule.")] [Min(0f)] private float radius = 0.5f; [SerializeField] [Tooltip("Vertical offset from the game object position to the bottom of the capsule.")] private float verticalOffset = 0f; [Min(0f)] [Tooltip("Small distance from the surface of the capsule used as a safety margin to avoid that the capsule is directly in contact with other colliders.")] public float contactOffset = 0.01f; [Tooltip("Overlaps with colliders in layers excluded from this mask will be ignored if the attempt to resolve all overlaps fails.")] public LayerMask prioritizedOverlap = Physics.AllLayers; /// /// If true, it uses the transform position and rotation, otherwise it uses its own position and rotation. /// [System.NonSerialized] public bool useTransform = true; /// /// Small margin added to the radius when retrieving overlaps. /// internal const float overlapMargin = 0.001f; private Vector3 _position; private Quaternion _rotation; private CapsuleCollider capsuleCollider; private LayerMask collisionMask; /// /// Height of the capsule. /// public float Height { get => height; set { height = value; ValidateHeight(); SetColliderProperties(); } } /// /// Radius of the capsule. /// public float Radius { get => radius; set { radius = value; ValidateHeight(); SetColliderProperties(); } } /// /// Vertical offset from the game object position to the bottom of the capsule. /// public float VerticalOffset { get => verticalOffset; set { verticalOffset = value; SetColliderProperties(); } } /// /// Position of the capsule. /// public Vector3 Position { get => useTransform ? transform.position : _position; set { if (useTransform) transform.position = value; else _position = value; } } /// /// Rotation of the capsule. /// public Quaternion Rotation { get => useTransform ? transform.rotation : _rotation; set { if (useTransform) transform.rotation = value; else _rotation = value; } } /// /// Capsule up direction. /// public Vector3 UpDirection { get => Rotation * new Vector3(0f, 1f, 0f); set => Rotation = Quaternion.FromToRotation(UpDirection, value) * Rotation; } /// /// World space center of the capsule. /// public Vector3 Center => Position + Rotation * LocalCenter; /// /// World space center of the capsule lower hemisphere. /// public Vector3 LowerHemisphereCenter => Position + Rotation * LocalLowerHemisphereCenter; /// /// World space center of the capsule upper hemisphere. /// public Vector3 UpperHemisphereCenter => Position + Rotation * LocalUpperHemisphereCenter; /// /// Center of the capsule in the object's local space. /// public Vector3 LocalCenter => new Vector3(0f, verticalOffset + 0.5f * height, 0f); /// /// Center of the capsule lower hemisphere in the object's local space. /// public Vector3 LocalLowerHemisphereCenter => CapsuleUtils.GetLocalLowerCenter_YAxis(radius, height, LocalCenter); /// /// Center of the capsule upper hemisphere in the object's local space. /// public Vector3 LocalUpperHemisphereCenter => CapsuleUtils.GetLocalUpperCenter_YAxis(radius, height, LocalCenter); /// /// The collision layer mask. /// public LayerMask CollisionMask => collisionMask; /// /// The collider of the character capsule. /// public Collider Collider => capsuleCollider; /// /// The rigidbody of the character capsule. /// public Rigidbody Rigidbody { get; private set; } internal CapsuleCollider CapsuleCollider => capsuleCollider; private void Awake() { DoPreliminaryCheck(); InstantiateComponents(); collisionMask = gameObject.GetCollisionMask(); } /// /// Checks if the character capsule overlaps any other collider in the same layer mask of the character game object. /// /// True if it overlaps, false otherwise. public bool CheckOverlap() { return OverlapUtils.CheckCapsuleOverlap(LowerHemisphereCenter, UpperHemisphereCenter, radius + overlapMargin, collisionMask, capsuleCollider); } /// /// Checks if the character capsule overlaps any other colliders in the given layer mask. /// /// True if it overlaps, false otherwise. public bool CheckOverlap(LayerMask layerMask) { return OverlapUtils.CheckCapsuleOverlap(LowerHemisphereCenter, UpperHemisphereCenter, radius + overlapMargin, layerMask, capsuleCollider); } /// /// Collects the colliders that overlap the character capsule in the same layer mask. /// /// Overlapping colliders count. public int CollectOverlaps(Collider[] overlaps) { return OverlapUtils.OverlapCapsule(LowerHemisphereCenter, UpperHemisphereCenter, radius + overlapMargin, overlaps, collisionMask, capsuleCollider); } /// /// Collects the colliders that overlap the character capsule. /// /// Overlapping colliders count. public int CollectOverlaps(Collider[] overlaps, LayerMask layerMask) { return OverlapUtils.OverlapCapsule(LowerHemisphereCenter, UpperHemisphereCenter, radius + overlapMargin, overlaps, layerMask, capsuleCollider); } /// /// Tries to resolve overlaps with every colliders it is supposed to collide with. If the first attempt fails, it tries again /// considering only the high priority colliders. /// /// True if it managed to resolve all overlaps, false otherwise. public bool TryResolveOverlap() { Vector3 position = Position; Quaternion rotation = Rotation; if (TryResolveCapsuleOverlap(position, rotation, capsuleCollider, overlapMargin, contactOffset, collisionMask, out Vector3 newPosition)) { Position = newPosition; return true; } LayerMask prioritizedCollisionMask = collisionMask & prioritizedOverlap; if (prioritizedCollisionMask == collisionMask) { Position = newPosition; return false; } TryResolveCapsuleOverlap(position, rotation, capsuleCollider, overlapMargin, contactOffset, prioritizedCollisionMask, out newPosition); Position = newPosition; return false; } private void DoPreliminaryCheck() { if (!Mathf.Approximately(transform.lossyScale.x, 1f) || !Mathf.Approximately(transform.lossyScale.y, 1f) || !Mathf.Approximately(transform.lossyScale.z, 1f)) { Debug.LogWarning($"{nameof(CharacterCapsule)}: Object scale is not (1, 1, 1)."); } foreach (var col in gameObject.GetComponentsInChildren(true)) { if (col != capsuleCollider && !col.isTrigger && !Physics.GetIgnoreLayerCollision(gameObject.layer, col.gameObject.layer)) { Debug.LogWarning($"{nameof(CharacterCapsule)}: Found other colliders on this gameobject or in its childrens."); break; } } } private void InstantiateComponents() { capsuleCollider = gameObject.AddComponent(); SetColliderProperties(); Rigidbody = gameObject.AddComponent(); Rigidbody.isKinematic = true; } private void ValidateHeight() { if (height >= 2f * radius) return; height = 2f * radius; } private void SetColliderProperties() { if (capsuleCollider is null) return; capsuleCollider.center = LocalCenter; capsuleCollider.height = height; capsuleCollider.radius = radius; capsuleCollider.direction = 1; // Y-Axis } private void OnValidate() { ValidateHeight(); if (capsuleCollider != null) SetColliderProperties(); } private void OnDrawGizmosSelected() { if (Application.isPlaying == false) { Gizmos.color = GizmosUtility.defaultColliderColor; GizmosUtility.DrawWireCapsule(LowerHemisphereCenter, UpperHemisphereCenter, radius); } } } }