namespace Zinnia.Tracking.Collision { using System.Collections; using System.Collections.Generic; using UnityEngine; using Zinnia.Extension; using Zinnia.Rule; /// /// Tracks collisions on the this component is on. /// public class CollisionTracker : CollisionNotifier { #region Tracker Settings [Header("Tracker Settings")] [Tooltip("Causes collisions to stop if the GameObject on either side of the collision is disabled.")] [SerializeField] private bool stopCollisionsOnDisable = true; /// /// Causes collisions to stop if the on either side of the collision is disabled. /// public bool StopCollisionsOnDisable { get { return stopCollisionsOnDisable; } set { stopCollisionsOnDisable = value; } } [Tooltip("Allows to optionally determine which colliders to allow collisions against.")] [SerializeField] private RuleContainer colliderValidity; /// /// Allows to optionally determine which colliders to allow collisions against. /// public RuleContainer ColliderValidity { get { return colliderValidity; } set { colliderValidity = value; } } [Tooltip("Allows to optionally determine which collider containing transforms to allow collisions against.")] [SerializeField] private RuleContainer containingTransformValidity; /// /// Allows to optionally determine which collider containing transforms to allow collisions against. /// public RuleContainer ContainingTransformValidity { get { return containingTransformValidity; } set { containingTransformValidity = value; } } [Tooltip("The delay interval in seconds defining how long to pause between processing the `Stay` method of the collision process. Negative values will be clamped to zero.")] [SerializeField] private float stayDelayInterval; /// /// The delay interval in seconds defining how long to pause between processing the `Stay` method of the collision process. Negative values will be clamped to zero. /// public float StayDelayInterval { get { return stayDelayInterval; } set { stayDelayInterval = value; if (this.IsMemberChangeAllowed()) { OnAfterStayDelayIntervalChange(); } } } /// /// When to process the `Stay` method the next time. Updated automatically based on after a `Stay` method has been called. /// public float NextStayProcessTime { get; protected set; } #endregion /// /// Determines whether to apply the fix for the PhysX 4.11 issue where when a kinematic state is changed it force calls a and a subsequent for the collision even though nothing has changed with the collision. /// /// /// This is set to in Unity 2019.3 and above when the PhysX version was updated to 4.11. /// public bool ApplyKinematicChangeTriggerEventFix { get; set; } /// /// A collection of current existing collisions. /// protected List trackedCollisions = new List(); /// /// A collection to track the kinematic state changes in for the PhysX 4.11 trigger exit/enter issue. /// protected HashSet trackedStateChangers = new HashSet(); /// /// A collection to track which colliders have caused the Trigger Exit within the same frame count to avoid duplicate kinematic change event fixes. /// Dictionary exitColliderTimeStamps = new Dictionary(); /// /// A collection to track which collider containers have caused the Trigger Exit within the same frame count to avoid duplicate kinematic change event fixes. /// Dictionary exitColliderContainerTimeStamps = new Dictionary(); /// /// An instruction to wait for the next FixedUpdate process in the life-cycle. /// protected WaitForFixedUpdate waitForFixedUpdateInstruction = new WaitForFixedUpdate(); /// /// The coroutine for deferring the call to the next FixedUpdate process in the life-cycle. /// protected Coroutine deferredTriggerExit; /// /// The containing the current checked collider. /// protected Transform colliderContainingTransform; /// /// Clears . /// public virtual void ClearColliderValidity() { if (!this.IsValidState()) { return; } ColliderValidity = default; } /// /// Clears . /// public virtual void ClearContainingTransformValidity() { if (!this.IsValidState()) { return; } ContainingTransformValidity = default; } /// /// Prepares the collision states for a kinematic state change on the given . /// /// /// This is a requirement for Unity 2019.3 and above and the PhysX 4.11 handling of kinematic changes on a . /// /// The that is to have the kinematic state changed on imminently. public virtual void PrepareKinematicStateChange(Rigidbody aboutToChange) { if (!ApplyKinematicChangeTriggerEventFix || aboutToChange == null) { return; } trackedStateChangers.Add(aboutToChange); } /// /// Stops the collision between this and the given . If there is still a physical intersection, then the collision will start again in the next physics frame. /// /// The to stop the collision with. public virtual void StopCollision(Collider collider) { RemoveDisabledObserver(collider); if (collider == null || (StatesToProcess & CollisionStates.Exit) == 0) { return; } OnCollisionStopped(eventData.Set(this, collider.isTrigger, null, collider)); } protected virtual void Awake() { #if UNITY_2019_3_OR_NEWER ApplyKinematicChangeTriggerEventFix = true; #endif NextStayProcessTime = Time.time; } protected virtual void OnEnable() { OnAfterStayDelayIntervalChange(); } protected virtual void OnDisable() { if (!StopCollisionsOnDisable) { return; } foreach (Collider collider in trackedCollisions.ToArray()) { StopCollision(collider); } StopDeferredTriggerExitRoutine(); trackedStateChangers.Clear(); exitColliderTimeStamps.Clear(); exitColliderContainerTimeStamps.Clear(); } protected virtual void OnCollisionEnter(Collision collision) { AddDisabledObserver(collision.collider); if ((StatesToProcess & CollisionStates.Enter) == 0) { return; } OnCollisionStarted(eventData.Set(this, false, collision, collision.collider)); } protected virtual void OnCollisionStay(Collision collision) { if (ShouldIgnoreStay()) { return; } OnCollisionChanged(eventData.Set(this, false, collision, collision.collider)); NextStayProcessTime = Time.time + StayDelayInterval; } protected virtual void OnCollisionExit(Collision collision) { RemoveDisabledObserver(collision.collider); if ((StatesToProcess & CollisionStates.Exit) == 0) { return; } OnCollisionStopped(eventData.Set(this, false, collision, collision.collider)); } protected virtual void OnTriggerEnter(Collider collider) { if (HasKinematicStateChanged(collider, true)) { exitColliderContainerTimeStamps.Remove(collider.GetContainingTransform()); if (exitColliderTimeStamps.TryGetValue(collider, out int colliderFrame) && colliderFrame == Time.frameCount) { StopDeferredTriggerExitRoutine(); exitColliderTimeStamps.Remove(collider); return; } } AddDisabledObserver(collider); if ((StatesToProcess & CollisionStates.Enter) == 0) { return; } OnCollisionStarted(eventData.Set(this, true, null, collider)); } protected virtual void OnTriggerStay(Collider collider) { if (ShouldIgnoreStay()) { return; } OnCollisionChanged(eventData.Set(this, true, null, collider)); NextStayProcessTime = Time.time + StayDelayInterval; } protected virtual void OnTriggerExit(Collider collider) { colliderContainingTransform = collider.GetContainingTransform(); if (HasKinematicStateChanged(collider, false) && (!exitColliderContainerTimeStamps.TryGetValue(colliderContainingTransform, out int colliderFrame) || colliderFrame != Time.frameCount)) { exitColliderTimeStamps[collider] = Time.frameCount; exitColliderContainerTimeStamps[colliderContainingTransform] = Time.frameCount; StopDeferredTriggerExitRoutine(); deferredTriggerExit = StartCoroutine(RunTriggerExitAfterNextFixedUpdate(collider)); return; } RemoveDisabledObserver(collider); if ((StatesToProcess & CollisionStates.Exit) == 0) { return; } OnCollisionStopped(eventData.Set(this, true, null, collider)); } /// protected override bool CanEmit(EventData data) { return base.CanEmit(data) && (data.ColliderData == null || (ColliderValidity.Accepts(data.ColliderData.gameObject) && ContainingTransformValidity.Accepts(data.ColliderData.GetContainingTransform().gameObject))); } /// /// Determines whether to ignore the collision/trigger stay state. /// /// Whether to ignore the state. protected virtual bool ShouldIgnoreStay() { return (StatesToProcess & CollisionStates.Stay) == 0 || NextStayProcessTime > Time.time; } /// /// Determines whether the kinematic state of the given has been flagged as it has just changed. /// /// The to get the kinematic state from. /// Whether to remove the from the collection. /// Whether the kinematic state has changed on the given . protected virtual bool HasKinematicStateChanged(Collider collider, bool remove) { if (!ApplyKinematicChangeTriggerEventFix || collider.attachedRigidbody == null || !trackedStateChangers.Contains(collider.attachedRigidbody)) { return false; } if (remove) { trackedStateChangers.Remove(collider.attachedRigidbody); } return true; } /// /// Executes the after the next FixedUpdate process in the life-cycle. /// /// The to run with. /// An Enumerator to manage the running of the Coroutine. protected IEnumerator RunTriggerExitAfterNextFixedUpdate(Collider collider) { yield return waitForFixedUpdateInstruction; trackedStateChangers.Remove(collider.attachedRigidbody); OnTriggerExit(collider); deferredTriggerExit = null; } /// /// Stops the coroutine from running. /// protected virtual void StopDeferredTriggerExitRoutine() { if (deferredTriggerExit != null) { StopCoroutine(deferredTriggerExit); } } /// /// Adds a to the that contains the causing the collision. /// /// The target to add the component to. protected virtual void AddDisabledObserver(Collider target) { if (target == null || !StopCollisionsOnDisable || ContainsDisabledObserver(target)) { return; } trackedCollisions.Add(target); CollisionTrackerDisabledObserver observer = target.gameObject.AddComponent(); observer.Source = this; observer.Target = target; } /// /// Removes the from the that contains the ceasing the collision. /// /// The target to remove the component from. protected virtual void RemoveDisabledObserver(Collider target) { if (target == null || !StopCollisionsOnDisable) { return; } foreach (CollisionTrackerDisabledObserver observer in target.gameObject.GetComponents()) { if (observer.Source == this && observer.Target == target) { observer.Destroy(); trackedCollisions.Remove(target); } } } /// /// Checks to see if the target already contains a for this source. /// /// The target to check the existence of a on. /// Whether the target already contains a . protected virtual bool ContainsDisabledObserver(Collider target) { foreach (CollisionTrackerDisabledObserver observer in target.gameObject.GetComponents()) { if (observer.Source == this && observer.Target == target) { return true; } } return false; } /// /// Called after has been changed. /// protected virtual void OnAfterStayDelayIntervalChange() { stayDelayInterval = Mathf.Max(0f, StayDelayInterval); NextStayProcessTime = Time.time; } } /// /// Observes the disabled state of any that the is currently colliding with. /// public class CollisionTrackerDisabledObserver : MonoBehaviour { [Tooltip("The CollisionTracker that is causing the collision.")] [SerializeField] private CollisionTracker source; /// /// The that is causing the collision. /// public CollisionTracker Source { get { return source; } set { source = value; } } [Tooltip("The Collider that is being collided with.")] [SerializeField] private Collider target; /// /// The that is being collided with. /// public Collider Target { get { return target; } set { target = value; } } /// /// Whether is being destroyed. /// protected bool isDestroyed; /// /// Clears . /// public virtual void ClearSource() { if (!this.IsValidState()) { return; } Source = default; } /// /// Clears . /// public virtual void ClearTarget() { if (!this.IsValidState()) { return; } Target = default; } /// /// Destroys from the scene. /// public virtual void Destroy() { isDestroyed = true; Destroy(this); } protected virtual void OnDisable() { if (Source != null && !isDestroyed) { Source.StopCollision(Target); } } } }