namespace VRTK.Prefabs.Locomotion.BodyRepresentation { using UnityEngine; using System; using System.Collections.Generic; using Malimbe.BehaviourStateRequirementMethod; using Malimbe.MemberChangeMethod; using Malimbe.PropertySerializationAttribute; using Malimbe.XmlDocumentationAttribute; using Zinnia.Cast; using Zinnia.Data.Attribute; using Zinnia.Data.Type; using Zinnia.Extension; using Zinnia.Process; using Zinnia.Tracking.Collision; using Zinnia.Tracking.Follow; using VRTK.Prefabs.Interactions.Interactables; using VRTK.Prefabs.Interactions.Interactors; /// /// Sets up the BodyRepresentation prefab based on the provided user settings and implements the logic to represent a body. /// public class BodyRepresentationProcessor : MonoBehaviour, IProcessable { /// /// The object that defines the main source of truth for movement. /// public enum MovementInterest { /// /// The source of truth for movement comes from . /// CharacterController, /// /// The source of truth for movement comes from until is in the air, then is the new source of truth. /// CharacterControllerUntilAirborne, /// /// The source of truth for movement comes from . /// Rigidbody, /// /// The source of truth for movement comes from until hits the ground, then is the new source of truth. /// RigidbodyUntilGrounded } #region Facade Settings /// /// The public interface facade. /// [Serialized] [field: Header("Facade Settings"), DocumentedByXml, Restricted] public BodyRepresentationFacade Facade { get; protected set; } #endregion #region Reference Settings /// /// The that acts as the main representation of the body. /// [Serialized] [field: Header("Reference Settings"), DocumentedByXml, Restricted] public CharacterController Character { get; protected set; } /// /// The that acts as the physical representation of the body. /// [Serialized] [field: DocumentedByXml, Restricted] public Rigidbody PhysicsBody { get; protected set; } /// /// The that acts as the physical collider representation of the body. /// [Serialized] [field: DocumentedByXml, Restricted] public CapsuleCollider RigidbodyCollider { get; protected set; } /// /// A to manage ignoring collisions with the BodyRepresentation colliders. /// [Serialized] [field: DocumentedByXml, Restricted] public CollisionIgnorer CollisionsToIgnore { get; protected set; } #endregion /// /// The object that defines the main source of truth for movement. /// public MovementInterest Interest { get; set; } = MovementInterest.CharacterControllerUntilAirborne; /// /// Whether touches ground. /// public bool IsCharacterControllerGrounded => wasCharacterControllerGrounded == true; /// /// Movement to apply to to resolve collisions. /// protected static readonly Vector3 collisionResolutionMovement = new Vector3(0.001f, 0f, 0f); /// /// The colliders to ignore body collisions with. /// protected readonly HashSet ignoredColliders = new HashSet(); /// /// The colliders to restore after an ungrab. /// protected readonly HashSet restoreColliders = new HashSet(); /// /// The previous position of . /// protected Vector3 previousRigidbodyPosition; /// /// Whether was grounded previously. /// protected bool? wasCharacterControllerGrounded; /// /// The frame count of the last time was set to or . /// protected int rigidbodySetFrameCount; /// /// Stores the routine for ignoring interactor collisions. /// protected Coroutine ignoreInteractorCollisions; /// /// An optional follower of . /// protected ObjectFollower offsetObjectFollower; /// /// An optional follower of . /// protected ObjectFollower sourceObjectFollower; /// /// Positions, sizes and controls all variables necessary to make a body representation follow the given . /// [RequiresBehaviourState] public virtual void Process() { if (Interest != MovementInterest.CharacterController && Facade.Offset != null) { Vector3 offsetPosition = Facade.Offset.transform.position; Vector3 previousPosition = offsetPosition; offsetPosition.y = PhysicsBody.position.y - Character.skinWidth; Facade.Offset.transform.position = offsetPosition; Facade.Source.transform.position += offsetPosition - previousPosition; } Vector3 previousCharacterControllerPosition; // Handle walking down stairs/slopes and physics affecting the Rigidbody in general. Vector3 rigidbodyPhysicsMovement = PhysicsBody.position - previousRigidbodyPosition; if (Interest == MovementInterest.Rigidbody || Interest == MovementInterest.RigidbodyUntilGrounded) { previousCharacterControllerPosition = Character.transform.position; Character.Move(rigidbodyPhysicsMovement); if (Facade.Offset != null) { Vector3 movement = Character.transform.position - previousCharacterControllerPosition; Facade.Offset.transform.position += movement; Facade.Source.transform.position += movement; } } // Position the CharacterController and handle moving the source relative to the offset. Vector3 characterControllerPosition = Character.transform.position; previousCharacterControllerPosition = characterControllerPosition; MatchCharacterControllerWithSource(false); Vector3 characterControllerSourceMovement = characterControllerPosition - previousCharacterControllerPosition; bool isGrounded = CheckIfCharacterControllerIsGrounded(); // Allow moving the Rigidbody via physics. if (Interest == MovementInterest.CharacterControllerUntilAirborne && !isGrounded) { Interest = MovementInterest.RigidbodyUntilGrounded; } else if (Interest == MovementInterest.RigidbodyUntilGrounded && isGrounded && rigidbodyPhysicsMovement.sqrMagnitude <= 1E-06F && rigidbodySetFrameCount > 0 && rigidbodySetFrameCount + 1 < Time.frameCount) { Interest = MovementInterest.CharacterControllerUntilAirborne; } // Handle walking up stairs/slopes via the CharacterController. if (isGrounded && Facade.Offset != null && characterControllerSourceMovement.y > 0f) { Facade.Offset.transform.position += Vector3.up * characterControllerSourceMovement.y; } MatchRigidbodyAndColliderWithCharacterController(); RememberCurrentPositions(); EmitIsGroundedChangedEvent(isGrounded); } /// /// Solves body collisions by not moving the body in case it can't go to its current position. /// /// /// If body collisions should be prevented this method needs to be called right before or right after applying any form of movement to the body. /// [RequiresBehaviourState] public virtual void SolveBodyCollisions() { if (Facade.Source == null) { return; } if (offsetObjectFollower != null) { offsetObjectFollower.Process(); } if (sourceObjectFollower != null) { sourceObjectFollower.Process(); } Process(); Vector3 characterControllerPosition = Character.transform.position + Character.center; Vector3 difference = Facade.Source.transform.position - characterControllerPosition; difference.y = 0f; float minimumDistanceToColliders = Character.radius - Facade.SourceThickness; if (difference.magnitude < minimumDistanceToColliders) { return; } float newDistance = difference.magnitude - minimumDistanceToColliders; (Facade.Offset == null ? Facade.Source : Facade.Offset).transform.position -= difference.normalized * newDistance; Process(); } /// /// Configures the source object follower based on the facade settings. /// public virtual void ConfigureSourceObjectFollower() { if (Facade.Source != null) { sourceObjectFollower = Facade.Source.GetComponent(); } } /// /// Configures the offset object follower based on the facade settings. /// public virtual void ConfigureOffsetObjectFollower() { if (Facade.Offset != null) { offsetObjectFollower = Facade.Offset.GetComponent(); } } /// /// Ignores all of the colliders on the interactor collection. /// public virtual void IgnoreInteractorsCollisions(InteractorFacade interactor) { CollisionsToIgnore.RunWhenActiveAndEnabled(() => CollisionsToIgnore.Targets.AddUnique(interactor.gameObject)); interactor.Grabbed.AddListener(IgnoreInteractorGrabbedCollision); interactor.Ungrabbed.AddListener(ResumeInteractorUngrabbedCollision); } /// /// Resumes all of the colliders on the interactor collection. /// public virtual void ResumeInteractorsCollisions(InteractorFacade interactor) { CollisionsToIgnore.RunWhenActiveAndEnabled(() => CollisionsToIgnore.Targets.Remove(interactor.gameObject)); interactor.Grabbed.RemoveListener(IgnoreInteractorGrabbedCollision); interactor.Ungrabbed.RemoveListener(ResumeInteractorUngrabbedCollision); } protected virtual void Awake() { Physics.IgnoreCollision(Character, RigidbodyCollider, true); } protected virtual void OnEnable() { ConfigureSourceObjectFollower(); ConfigureOffsetObjectFollower(); Interest = MovementInterest.CharacterControllerUntilAirborne; MatchCharacterControllerWithSource(true); MatchRigidbodyAndColliderWithCharacterController(); RememberCurrentPositions(); } protected virtual void OnDisable() { sourceObjectFollower = null; offsetObjectFollower = null; } /// /// Ignores the interactable grabbed by the interactor. /// /// The interactable to ignore. protected virtual void IgnoreInteractorGrabbedCollision(InteractableFacade interactable) { CollisionsToIgnore.RunWhenActiveAndEnabled(() => CollisionsToIgnore.Targets.AddUnique(interactable.gameObject)); } /// /// Resumes the interactable ungrabbed by the interactor. /// /// The interactable to resume. protected virtual void ResumeInteractorUngrabbedCollision(InteractableFacade interactable) { CollisionsToIgnore.RunWhenActiveAndEnabled(() => CollisionsToIgnore.Targets.Remove(interactable.gameObject)); } /// /// Changes the height and position of to match . /// /// Whether to set the position directly or tell to move to it. protected virtual void MatchCharacterControllerWithSource(bool setPositionDirectly) { Vector3 sourcePosition = Facade.Source.transform.position; float height = Facade.Offset == null ? sourcePosition.y : Facade.Offset.transform.InverseTransformPoint(sourcePosition).y; height -= Character.skinWidth; // CharacterController enforces a minimum height of twice its radius, so let's match that here. height = Mathf.Max(height, 2f * Character.radius); Vector3 position = sourcePosition; position.y -= height; if (Facade.Offset != null) { // The offset defines the source's "floor". position.y = Mathf.Max(position.y, Facade.Offset.transform.position.y + Character.skinWidth); } if (setPositionDirectly) { Character.transform.position = position; } else { Vector3 movement = position - Character.transform.position; // The CharacterController doesn't resolve any potential collisions in case we don't move it. Character.Move(movement == Vector3.zero ? movement + collisionResolutionMovement : movement); if (movement == Vector3.zero) { Character.Move(movement - collisionResolutionMovement); } } Character.height = height; Vector3 center = Character.center; center.y = height / 2f; Character.center = center; } /// /// Changes to match the collider settings of and moves to match . /// protected virtual void MatchRigidbodyAndColliderWithCharacterController() { RigidbodyCollider.radius = Character.radius; RigidbodyCollider.height = Character.height + Character.skinWidth; Vector3 center = Character.center; center.y = (Character.height - Character.skinWidth) / 2f; RigidbodyCollider.center = center; PhysicsBody.position = Character.transform.position; } /// /// Checks whether is grounded. /// /// /// isn't accurate so this method does an additional check using . /// /// Whether is grounded. protected virtual bool CheckIfCharacterControllerIsGrounded() { if (Character.isGrounded) { return true; } HeapAllocationFreeReadOnlyList hitColliders = PhysicsCast.OverlapSphereAll( null, Character.transform.position + (Vector3.up * (Character.radius - Character.skinWidth - 0.001f)), Character.radius, 1 << Character.gameObject.layer); foreach (Collider hitCollider in hitColliders) { if (hitCollider != Character && hitCollider != RigidbodyCollider && !ignoredColliders.Contains(hitCollider) && !Physics.GetIgnoreLayerCollision( hitCollider.gameObject.layer, Character.gameObject.layer) && !Physics.GetIgnoreLayerCollision(hitCollider.gameObject.layer, PhysicsBody.gameObject.layer)) { return true; } } return false; } /// /// Updates the previous position variables to remember the current state. /// protected virtual void RememberCurrentPositions() { previousRigidbodyPosition = PhysicsBody.position; } /// /// Emits or . /// /// The current state. protected virtual void EmitIsGroundedChangedEvent(bool isCharacterControllerGrounded) { if (wasCharacterControllerGrounded == isCharacterControllerGrounded) { return; } wasCharacterControllerGrounded = isCharacterControllerGrounded; if (isCharacterControllerGrounded) { Facade.BecameGrounded?.Invoke(); } else { Facade.BecameAirborne?.Invoke(); } } /// /// Called after has been changed. /// [CalledAfterChangeOf(nameof(Interest))] protected virtual void OnAfterInterestChange() { switch (Interest) { case MovementInterest.CharacterController: case MovementInterest.CharacterControllerUntilAirborne: PhysicsBody.isKinematic = true; rigidbodySetFrameCount = 0; break; case MovementInterest.Rigidbody: case MovementInterest.RigidbodyUntilGrounded: PhysicsBody.isKinematic = false; rigidbodySetFrameCount = Time.frameCount; break; default: throw new ArgumentOutOfRangeException(nameof(Interest), Interest, null); } } } }