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