using System; using UnityEngine; namespace Framework.Actors { /// Checks whether an actor is grounded public class GroundChecker { /// Maximum number of colliders the actor can touch at the same time /// /// If this number is exceeded, undefined behavior results. /// private const int MaximumIntersectionCount = 16; /// /// Normalized angle around the bottom of the capsule/sphere colliders that is considered /// a ground collision. A value of 1.0 would count collisions with the sides as grounded, /// a value approaching 0.0 would only allow for a very tight spot on the bottom to count /// as grounded (with the potential to cause issues if the actor is standing on a slope). /// private const float CapsuleAndSphereGroundDot = 0.5f; /// Padding used around any collision checks /// /// Due to the limits of floating point accuracy, the physics engine or Unity's /// character controller may leave the actor slightly above the ground, thus resulting /// in an unreliable ground contact state. The is added to /// the size of the actor's collider when checking its grounded state. /// public float SkinWidth = 0.03f; /// Vector that is downwards relative to the actor /// /// This only needs to be touched if your game does crazy things with gravity, /// such as having characters walk on walls :) /// public Vector3 Down = Vector3.down; /// Initialized a new collision tracker public GroundChecker() { this.intersectingColliders = new Collider[MaximumIntersectionCount]; } /// Checks if a sphere collider is grounded /// Collider that will be checked /// Layers against which the sphere collider will be checked /// True if the sphere collider is grounded public bool CheckIfGrounded(Collider collider, int layerMask) { var characterController = collider as CharacterController; if(characterController != null) { return CheckIfGrounded(characterController, layerMask); } var capsuleCollider = collider as CapsuleCollider; if(capsuleCollider != null) { return CheckIfGrounded(capsuleCollider, layerMask); } var sphereCollider = collider as SphereCollider; if(sphereCollider != null) { return CheckIfGrounded(sphereCollider, layerMask); } var boxCollider = collider as BoxCollider; if(boxCollider != null) { return CheckIfGrounded(boxCollider, layerMask); } throw new ArgumentException("Unsupport collider type in grounded check", "collider"); } /// Checks if a sphere collider is grounded /// Sphere collider that will be checked /// Layers against which the sphere collider will be checked /// True if the sphere collider is grounded public bool CheckIfGrounded(SphereCollider sphereCollider, int layerMask) { return CheckIfSphereGrounded( sphereCollider.transform.position + sphereCollider.center, sphereCollider.radius, layerMask ); } /// Checks if a capsule collider is grounded /// Capsule collider that will be checked /// Layers against which the capsule collider will be checked /// True if the capsule collider is grounded public bool CheckIfGrounded(CapsuleCollider capsuleCollider, int layerMask) { Vector3 sphereCenter; float sphereRadius; { Transform capsuleColliderTransform = capsuleCollider.transform; Vector3 downVector = capsuleColliderTransform.rotation * Vector3.down; sphereRadius = capsuleCollider.radius; float bottomSphereOffset = (capsuleCollider.height / 2.0f) - sphereRadius; sphereCenter = ( capsuleColliderTransform.position + capsuleCollider.center + (bottomSphereOffset * downVector) ); } return CheckIfSphereGrounded(sphereCenter, sphereRadius, layerMask); } /// Checks if a box collider is grounded /// Box collider that will be checked /// Layers against which the box collider will be checked /// True if the box collider is grounded public bool CheckIfGrounded(BoxCollider boxCollider, int layerMask) { return CheckIfBoxGrounded( boxCollider.transform.rotation, boxCollider.transform.position + boxCollider.center, boxCollider.size / 2.0f, layerMask ); } /// Checks if the actor is grounded /// Actor that will be checked /// True if the actor is grounded, false otherwise public bool CheckIfGrounded(ActorPhysics actorPhysics) { return CheckIfGrounded(actorPhysics.CharacterController, actorPhysics.SolidLayerMask); } /// Checks if a character is grounded /// Character controller that will be checked /// Layers against which the character will be checked /// True if the character is grounded, false otherwise /// /// This figures out where the bottom sphere of the character controller's /// capsule collider is, enlarges it minimally and then looks for any colliders /// that are touching its bottom side (in a 60 degree cone). /// public bool CheckIfGrounded(CharacterController characterController, int layerMask) { Vector3 sphereCenter; float sphereRadius; { Transform characterControllerTransform = characterController.transform; Vector3 downVector = characterControllerTransform.rotation * Vector3.down; sphereRadius = characterController.radius; float bottomSphereOffset = (characterController.height / 2.0f) - sphereRadius; sphereCenter = ( characterControllerTransform.position + characterController.center + (bottomSphereOffset * downVector) ); } return CheckIfSphereGrounded(sphereCenter, sphereRadius, layerMask); } /// Checks if a box is grounded /// Orientation of the box /// Center of the box /// Extents of the box (half of its side lengths / size) /// Lyers on which to check for collisions /// True if the box is touching the ground, false otherwise public bool CheckIfBoxGrounded( Quaternion orientation, Vector3 center, Vector3 extents, int layerMask ) { this.intersectingColliderCount = Physics.OverlapBoxNonAlloc( center, extents + (Vector3.one * this.SkinWidth), this.intersectingColliders, orientation, layerMask, QueryTriggerInteraction.Ignore ); for(int intersectionIndex = 0; intersectionIndex < this.intersectingColliderCount;) { Vector3 closest = ( this.intersectingColliders[intersectionIndex].ClosestPoint(center) ); closest -= center; // Get the contact point as an interval along a line going through the center // of the box towards its bottom float y = Vector2.Dot(closest, this.Down); // If the collider is being touched by the box' lower half, we're grounded. if(y > this.SkinWidth) { ++intersectionIndex; } else { // Contact is not on the bottom half of the box, discard this intersection this.intersectingColliders[intersectionIndex] = ( this.intersectingColliders[this.intersectingColliderCount - 1] ); --this.intersectingColliderCount; } } // We're grounded if one or more colliders are touching the box' bottom return (this.intersectingColliderCount > 0); } /// Checks if a sphere is grounded /// Center of the sphere /// Radius of the sphere /// Lyers on which to check for collisions /// True if the box is touching the ground, false otherwise public bool CheckIfSphereGrounded(Vector3 center, float radius, int layerMask) { this.intersectingColliderCount = Physics.OverlapSphereNonAlloc( center, radius + this.SkinWidth, this.intersectingColliders, layerMask, QueryTriggerInteraction.Ignore ); for(int intersectionIndex = 0; intersectionIndex < this.intersectingColliderCount;) { Vector3 closest; { // Wasting a lot of CPU power here doing dynamic casts :-( // Terrain and mesh collisions aren't trivial and can have multiple answers, // but forcing the user to write special case code still sucks. var terrainCollider = this.intersectingColliders[intersectionIndex] as TerrainCollider; if(terrainCollider != null) { closest = center; } else { var meshCollider = this.intersectingColliders[intersectionIndex] as MeshCollider; if(meshCollider != null) { closest = center; } else { closest = ( this.intersectingColliders[intersectionIndex].ClosestPoint(center) ); } } closest -= center; } // If we're inside the collider, we're grounded. Oh well. if(closest.sqrMagnitude < this.SkinWidth * this.SkinWidth) { ++intersectionIndex; continue; } // If the sphere's bottom is in contact, consider this collider float dot = Vector3.Dot(this.Down, Vector3.Normalize(closest)); if(dot > CapsuleAndSphereGroundDot) { ++intersectionIndex; } else { // Contact is not on the bottom of the sphere, discard this intersection this.intersectingColliders[intersectionIndex] = ( this.intersectingColliders[this.intersectingColliderCount - 1] ); --this.intersectingColliderCount; } } // We're grounded if one or more colliders are touching the sphere's bottom return (this.intersectingColliderCount > 0); } /// /// Colliders the actor was intersecting with at the most recent call to one of /// the Check...() methods /// public ArraySegment IntersectingCollidersFromMostRecentCheck { get { return new ArraySegment( this.intersectingColliders, 0, this.intersectingColliderCount ); } } /// Whether the actor was grounded during the most recent check public bool WasGroundedInMostRecentCheck { get { return (this.intersectingColliderCount > 0); } } /// Checks the velocity of the ground the actor is standing upon /// The velocity of the ground the actor is standing upon public Vector3 GetMostRecentGroundVelocity() { Vector3 result = Vector3.zero; // Find the intersecting collider which has the highest upwards velocity float? highestUpwardVelocity = null; for(int index = 0; index < this.intersectingColliderCount; ++index) { Vector3 velocity = getColliderVelocity(this.intersectingColliders[index]); float y = Vector2.Dot(velocity, this.Down); if(highestUpwardVelocity.HasValue) { if(y < highestUpwardVelocity.Value) { // Check: Shouldn't this be greater-than? highestUpwardVelocity = y; result = velocity; } } else { highestUpwardVelocity = y; result = velocity; } } return result; } /// Determines the velocity of a collider /// Collider whose velocity will be determined /// The velocity of the collider private static Vector3 getColliderVelocity(Collider collider) { // If it has a Rigidbody component or is the child of a Rigidbody, return that velocity Rigidbody rigidbody = collider.GetComponentInParent(); if(rigidbody != null) { return rigidbody.velocity; } // If it has a component implementing IVelocityProvider or is the child of such // a game object (complex moving geometry), return that velocity IVelocityProvider velocityProvider = collider.GetComponentInParent(); if(velocityProvider != null) { return velocityProvider.Velocity; } // No physics component means its velocity is zero (and not undefined!) return Vector3.zero; } /// Reused array to store colliders the actor is intersecting with private Collider[/*MaximumIntersectionCount*/] intersectingColliders; /// Number of intersecting colliders private int intersectingColliderCount; } } // namespace Framework.Actors