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);
}
}
}
}