namespace VRTK.Prefabs.Interactions.InteractableSnapZone { using UnityEngine; using UnityEngine.Events; using System; using System.Collections.Generic; using Malimbe.XmlDocumentationAttribute; using Malimbe.PropertySerializationAttribute; using Zinnia.Extension; using Zinnia.Data.Attribute; using VRTK.Prefabs.Interactions.Interactables; /// /// Determines if the collided SnapZone is valid for activation based on whether another snap zone is already holding the activated state. /// public class ActivationValidator : MonoBehaviour { /// /// Defines the event with the . /// [Serializable] public class UnityEvent : UnityEvent { } #region Facade Settings /// /// The public interface facade. /// [Serialized] [field: Header("Facade Settings"), DocumentedByXml, Restricted] public SnapZoneFacade Facade { get; protected set; } #endregion /// /// Emitted when the SnapZone activation is validated. /// [DocumentedByXml] public UnityEvent Validated = new UnityEvent(); /// /// Determines if the SnapZone is currently activated. /// public bool IsActivated { get; protected set; } /// /// A unique reference for a listener based upon the interactable and SnapZone being activated. /// protected struct ListenerKey { private readonly GameObject interactable; private readonly SnapZoneActivator zone; public ListenerKey(GameObject interactable, SnapZoneActivator zone) { this.interactable = interactable; this.zone = zone; } } /// /// The that is currently activating this SnapZone. /// protected GameObject currentActivatingGameObject; /// /// The associated to the that is currently activating this SnapZone. /// protected InteractableFacade currentActivatingInteractable; /// /// A collection of listeners registered with the SnapZone that is being activated by a given interactable . /// protected Dictionary> activatingZoneListeners = new Dictionary>(); /// /// A collection of listeners registered with this SnapZone. /// protected Dictionary> currentZoneListeners = new Dictionary>(); /// /// A collection of found snap zone collisions that are actually invalid and not colliding. /// List invalidSnapZoneCollisions = new List(); /// /// Attempts to activate the SnapZone if the colliding is not already activating another SnapZone. /// /// The colliding interactable. public virtual void Activate(GameObject activator) { IsActivated = false; TrySetInteractableFacade(activator); if (currentActivatingInteractable == null || Facade.Configuration.ActivationArea == null || !Facade.Configuration.CollidingObjectsList.Contains(currentActivatingInteractable.gameObject)) { return; } currentActivatingInteractable.ActiveCollisions.AddUnique(Facade.Configuration.ActivationArea.gameObject); invalidSnapZoneCollisions.Clear(); foreach (GameObject collidingObject in currentActivatingInteractable.ActiveCollisions.SubscribableElements) { if (collidingObject == null) { continue; } SnapZoneActivator activatingZone = collidingObject.GetComponent(); if (activatingZone == null) { continue; } if (!activatingZone.Facade.Configuration.CollidingObjectsList.Contains(currentActivatingInteractable.gameObject)) { invalidSnapZoneCollisions.Add(activatingZone.Facade.Configuration.ActivationArea.gameObject); continue; } if (activatingZone == Facade.Configuration.ActivationArea) { if (!IsActivated) { Facade.Configuration.EmitActivated(activator); } IsActivated = true; Validated?.Invoke(activator); ClearInvalidSnapZoneCollisions(currentActivatingInteractable, ref invalidSnapZoneCollisions); break; } else { ListenerKey listenerKey = new ListenerKey(activator, activatingZone); if (!activatingZoneListeners.ContainsKey(listenerKey)) { UnityAction onExitActivatingZoneListener = activatingInteractable => AttemptReactivation(activatingInteractable, activatingZone); activatingZone.Facade.Exited.AddListener(onExitActivatingZoneListener); activatingZoneListeners.Add(listenerKey, onExitActivatingZoneListener); } if (!currentZoneListeners.ContainsKey(listenerKey)) { UnityAction onExitCurrentZoneListener = activatingInteractable => CancelAttemptReactivation(activatingInteractable, activatingZone); Facade.Exited.AddListener(onExitCurrentZoneListener); currentZoneListeners.Add(listenerKey, onExitCurrentZoneListener); } ClearInvalidSnapZoneCollisions(currentActivatingInteractable, ref invalidSnapZoneCollisions); break; } } ClearInvalidSnapZoneCollisions(currentActivatingInteractable, ref invalidSnapZoneCollisions); } /// /// Attempts to Deactivate the SnapZone if it is already activated. /// /// The interactable that is no longer colliding with the SnapZone. public virtual void Deactivate(GameObject deactivator) { if (IsActivated) { IsActivated = false; Facade.Deactivated?.Invoke(deactivator); } InteractableFacade deactivatingInteractable = TryGetInteractable(deactivator); if (deactivatingInteractable != null) { deactivatingInteractable.ActiveCollisions.Remove(Facade.Configuration.ActivationArea.gameObject); } } /// /// Clears any invalid SnapZone collision that still may be stored on the activating Interactable. /// /// The activating interactable. /// The collection of invalid SnapZone collisions. protected virtual void ClearInvalidSnapZoneCollisions(InteractableFacade interactable, ref List invalidCollisions) { foreach (GameObject invalidCollision in invalidCollisions) { interactable.ActiveCollisions.Remove(invalidCollision); } invalidCollisions.Clear(); } /// /// Attempts activate this SnapZone with the given . /// /// The colliding interactable. /// The SnapZone that was previously being activated by the colliding interactable. protected virtual void AttemptReactivation(GameObject activator, SnapZoneActivator activatingZone) { activator.TryGetComponent(true, true).ActiveCollisions.Remove(activatingZone.Facade.Configuration.ActivationArea.gameObject); ListenerKey listenerKey = new ListenerKey(activator, activatingZone); activatingZoneListeners.TryGetValue(listenerKey, out UnityAction activatingZoneListener); if (activatingZoneListener != null) { activatingZone.Facade.Exited.RemoveListener(activatingZoneListener); } activatingZoneListeners.Remove(listenerKey); Activate(activator); } /// /// Cancels the attempt to activate the SnapZone upon the previous activating SnapZone becoming deactivated. /// /// The colliding interactable. /// The SnapZone that was previously being activated by the colliding interactable. protected virtual void CancelAttemptReactivation(GameObject activator, SnapZoneActivator activatingZone) { ListenerKey listenerKey = new ListenerKey(activator, activatingZone); activatingZoneListeners.TryGetValue(listenerKey, out UnityAction onExitActivatingZoneListener); if (onExitActivatingZoneListener != null) { activatingZone.Facade.Exited.RemoveListener(onExitActivatingZoneListener); activatingZoneListeners.Remove(listenerKey); } currentZoneListeners.TryGetValue(listenerKey, out UnityAction onExitCurrentZoneListener); if (onExitCurrentZoneListener != null) { Facade.Exited.RemoveListener(onExitCurrentZoneListener); currentZoneListeners.Remove(listenerKey); } } /// /// Attempts to set the cache for the associated with the given valid snappable . /// /// The colliding interactable. protected virtual void TrySetInteractableFacade(GameObject container) { if ((container != null && container != currentActivatingGameObject) || currentActivatingInteractable == null) { currentActivatingGameObject = container; currentActivatingInteractable = TryGetInteractable(currentActivatingGameObject); } if (currentActivatingInteractable == null) { throw new NullReferenceException("The given container must contain an InteractableFacade."); } } /// /// Attempts to retrieve the associated with the given valid snappable . /// /// The colliding interactable. /// The interactable associated with the snappable object. protected virtual InteractableFacade TryGetInteractable(GameObject container) { return container != null ? container.TryGetComponent(true, true) : null; } } }