namespace Zinnia.Pointer
{
using System;
using UnityEngine;
using UnityEngine.Events;
using Zinnia.Cast;
using Zinnia.Data.Type;
using Zinnia.Extension;
using Zinnia.Process;
using Zinnia.Visual;
///
/// Allows pointing at objects and notifies when a target is hit, continues to be hit or stops being hit by listening to a .
///
public class ObjectPointer : MonoBehaviour, IProcessable
{
///
/// Holds data about a event.
///
[Serializable]
public class EventData : SurfaceData
{
[Tooltip("Whether the ObjectPointer is currently activated.")]
[SerializeField]
private bool isCurrentlyActive;
///
/// Whether the is currently activated.
///
public bool IsCurrentlyActive
{
get
{
return isCurrentlyActive;
}
set
{
isCurrentlyActive = value;
}
}
[Tooltip("Whether the ObjectPointer is currently hovering over a target.")]
[SerializeField]
private bool isCurrentlyHovering;
///
/// Whether the is currently hovering over a target.
///
public bool IsCurrentlyHovering
{
get
{
return isCurrentlyHovering;
}
set
{
isCurrentlyHovering = value;
}
}
[Tooltip("The duration that the ObjectPointer has been hovering over it's current target.")]
[SerializeField]
private float currentHoverDuration;
///
/// The duration that the has been hovering over it's current target.
///
public float CurrentHoverDuration
{
get
{
return currentHoverDuration;
}
set
{
currentHoverDuration = value;
}
}
[Tooltip("The points cast data given to the ObjectPointer.")]
[SerializeField]
private PointsCast.EventData currentPointsCastData;
///
/// The points cast data given to the .
///
public PointsCast.EventData CurrentPointsCastData
{
get
{
return currentPointsCastData;
}
set
{
currentPointsCastData = value;
}
}
public EventData() { }
public EventData(Transform transform) : base(transform) { }
public EventData Set(EventData source)
{
return Set(source.IsCurrentlyActive, source.IsCurrentlyHovering, source.CurrentHoverDuration, source.CurrentPointsCastData);
}
public EventData Set(bool isActive, bool isHovering, float hoverDuration, PointsCast.EventData pointsCastData)
{
IsCurrentlyActive = isActive;
IsCurrentlyHovering = isHovering;
CurrentHoverDuration = hoverDuration;
CurrentPointsCastData = pointsCastData;
return this;
}
///
public override string ToString()
{
string[] titles = new string[]
{
"IsCurrentlyActive",
"IsCurrentlyHovering",
"CurrentHoverDuration",
"CurrentPointsCastData"
};
object[] values = new object[]
{
IsCurrentlyActive,
IsCurrentlyHovering,
CurrentHoverDuration,
CurrentPointsCastData
};
return StringExtensions.FormatForToString(titles, values, base.ToString());
}
public override void Clear()
{
base.Clear();
Set(default, default, default, default);
}
public override bool Equals(object other)
{
if (other == null)
{
return false;
}
EventData data = other as EventData;
return Equals(data);
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public virtual bool Equals(EventData other)
{
if (other == null || !GetType().Equals(other.GetType()))
{
return false;
}
return base.Equals(other) &&
IsCurrentlyActive == other.IsCurrentlyActive &&
IsCurrentlyHovering == other.IsCurrentlyHovering &&
CurrentHoverDuration.ApproxEquals(other.CurrentHoverDuration) &&
CurrentPointsCastData == other.CurrentPointsCastData;
}
}
///
/// Defines the event with the .
///
/// The data is in case the isn't hitting any target.
[Serializable]
public class UnityEvent : UnityEvent { }
///
/// Defines the event with the state.
///
[Serializable]
public class PointsRendererUnityEvent : UnityEvent { }
[Header("Pointer Element Settings")]
[Tooltip("Represents the origin, i.e. the first rendered point.")]
[SerializeField]
private PointerElement origin;
///
/// Represents the origin, i.e. the first rendered point.
///
public PointerElement Origin
{
get
{
return origin;
}
set
{
origin = value;
}
}
[Tooltip("Represents the segments between Origin and Destination. This will get cloned to create all the segments.")]
[SerializeField]
private PointerElement repeatedSegment;
///
/// Represents the segments between and . This will get cloned to create all the segments.
///
public PointerElement RepeatedSegment
{
get
{
return repeatedSegment;
}
set
{
repeatedSegment = value;
}
}
[Tooltip("Represents the destination, i.e. the last rendered point.")]
[SerializeField]
private PointerElement destination;
///
/// Represents the destination, i.e. the last rendered point.
///
public PointerElement Destination
{
get
{
return destination;
}
set
{
destination = value;
}
}
[Tooltip("Whether the Destination will be enabled if the raycast does not collide with anything and contains no PointsCast.EventData.HitData.")]
[SerializeField]
private bool enableDestinationOnNoCollision = true;
///
/// Whether the will be enabled if the raycast does not collide with anything and contains no .
///
public bool EnableDestinationOnNoCollision
{
get
{
return enableDestinationOnNoCollision;
}
set
{
enableDestinationOnNoCollision = value;
}
}
[Tooltip("Provides an alternative as the pointer origin in the events.")]
[SerializeField]
private GameObject eventDataOriginTransformOverride;
///
/// Provides an alternative as the pointer origin in the events.
///
public GameObject EventDataOriginTransformOverride
{
get
{
return eventDataOriginTransformOverride;
}
set
{
eventDataOriginTransformOverride = value;
}
}
///
/// Emitted when the becomes active.
///
[Header("Pointer Events")]
public UnityEvent Activated = new UnityEvent();
///
/// Emitted when the becomes inactive.
///
public UnityEvent Deactivated = new UnityEvent();
///
/// Emitted when the collides with a new target.
///
public UnityEvent Entered = new UnityEvent();
///
/// Emitted when the stops colliding with an existing target.
///
public UnityEvent Exited = new UnityEvent();
///
/// Emitted when the changes its hovering position over an existing target.
///
public UnityEvent Hovering = new UnityEvent();
///
/// Emitted whenever is called.
///
public UnityEvent Selected = new UnityEvent();
///
/// Emitted when the appears and becomes visible.
///
public UnityEvent Appeared = new UnityEvent();
///
/// Emitted when the disappears and becomes hidden.
///
public UnityEvent Disappeared = new UnityEvent();
///
/// Emitted when the elements change.
///
public PointsRendererUnityEvent RenderDataChanged = new PointsRendererUnityEvent();
///
/// Whether the is currently activated.
///
public virtual bool IsActivated { get; protected set; }
///
/// Whether the is currently hovering over a target.
///
public virtual bool IsHovering { get; protected set; }
///
/// The duration that the is hovering over the current target.
///
public virtual float HoverDuration { get; protected set; }
///
/// The target that the is currently hovering over. If there is no target then it is .
///
public virtual EventData HoverTarget => IsHovering ? GetEventData(activePointsCastData) : null;
///
/// The target that the has most recently selected.
///
public virtual EventData SelectedTarget { get; protected set; }
///
/// Whether any of the is currently visible.
///
public virtual bool IsVisible
{
get
{
if (!enabled)
{
return false;
}
if (Origin.ElementVisibility == PointerElement.Visibility.AlwaysOff &&
RepeatedSegment.ElementVisibility == PointerElement.Visibility.AlwaysOff &&
Destination.ElementVisibility == PointerElement.Visibility.AlwaysOff)
{
return false;
}
if (Origin.ElementVisibility == PointerElement.Visibility.AlwaysOn ||
RepeatedSegment.ElementVisibility == PointerElement.Visibility.AlwaysOn ||
Destination.ElementVisibility == PointerElement.Visibility.AlwaysOn)
{
return true;
}
return IsActivated;
}
}
///
/// The current active points in the cast.
///
protected readonly PointsCast.EventData activePointsCastData = new PointsCast.EventData();
///
/// The previous active points in the cast.
///
protected readonly PointsCast.EventData previousPointsCastData = new PointsCast.EventData();
///
/// Whether the pointer was visible in the previous frame.
///
protected bool? wasPreviouslyVisible;
///
/// The event data to emit on pointer events.
///
protected readonly EventData eventData = new EventData();
///
/// The points data to render the pointer with.
///
protected readonly PointsRenderer.PointsData pointsData = new PointsRenderer.PointsData();
///
/// Clears .
///
public virtual void ClearOrigin()
{
if (!this.IsValidState())
{
return;
}
Origin = default;
}
///
/// Clears .
///
public virtual void ClearRepeatedSegment()
{
if (!this.IsValidState())
{
return;
}
RepeatedSegment = default;
}
///
/// Clears .
///
public virtual void ClearDestination()
{
if (!this.IsValidState())
{
return;
}
Destination = default;
}
///
/// Clears .
///
public virtual void ClearEventDataOriginTransformOverride()
{
if (!this.IsValidState())
{
return;
}
EventDataOriginTransformOverride = default;
}
///
/// The Activate method turns on the .
///
public virtual void Activate()
{
if (!this.IsValidState() || IsActivated)
{
return;
}
IsActivated = true;
UpdateRenderData();
Activated?.Invoke(HoverTarget);
}
///
/// The Deactivate method turns off the .
///
public virtual void Deactivate()
{
TryDeactivate(false);
}
///
/// Gets the current state and emits it through .
///
public virtual void Select()
{
if (!this.IsValidState())
{
return;
}
SelectedTarget = IsActivated && activePointsCastData.IsValid ? HoverTarget : null;
Selected?.Invoke(SelectedTarget);
}
///
/// Handles the provided data to transition state and emit the events.
///
/// The data describing the results of the most recent cast.
public virtual void HandleData(PointsCast.EventData data)
{
if (!this.IsValidState())
{
return;
}
if (IsVisible)
{
previousPointsCastData.Set(activePointsCastData);
if (data.HitData != null)
{
Transform targetTransform = data.HitData.Value.transform;
if (targetTransform != null && targetTransform != activePointsCastData?.HitData?.transform)
{
TryEmitExit(previousPointsCastData);
Entered?.Invoke(GetEventData(data));
}
HoverDuration += Time.deltaTime;
IsHovering = true;
Hovering?.Invoke(GetEventData(data));
}
else
{
TryEmitExit(previousPointsCastData);
}
activePointsCastData.Set(data);
}
else
{
activePointsCastData.Clear();
previousPointsCastData.Clear();
}
UpdateRenderData();
}
///
/// Handles the previous provided once more.
///
public virtual void RehandleData()
{
if (!this.IsValidState())
{
return;
}
HandleData(activePointsCastData);
}
///
/// Processes the appearance of the pointer.
///
public void Process()
{
if (!this.IsValidState())
{
return;
}
TryEmitVisibilityEvent();
}
protected virtual void OnEnable()
{
TryEmitVisibilityEvent();
UpdateRenderData();
}
protected virtual void OnDisable()
{
TryDeactivate(true);
}
///
/// Updates the 's 's visibility and emits the event with the s used for the s.
///
protected virtual void UpdateRenderData()
{
pointsData.Points = activePointsCastData.Points;
pointsData.StartPoint = GetElementRepresentation(Origin);
pointsData.IsStartPointVisible = Origin.IsVisible;
pointsData.RepeatedSegmentPoint = GetElementRepresentation(RepeatedSegment);
pointsData.IsRepeatedSegmentPointVisible = RepeatedSegment.IsVisible;
pointsData.EndPoint = GetElementRepresentation(Destination);
pointsData.IsEndPointVisible = Destination.IsVisible;
TryDeactivateElementObject(Origin.ValidElementContainer);
TryDeactivateElementObject(Origin.InvalidElementContainer);
TryDeactivateElementObject(RepeatedSegment.ValidElementContainer);
TryDeactivateElementObject(RepeatedSegment.InvalidElementContainer);
TryDeactivateElementObject(Destination.ValidElementContainer);
TryDeactivateElementObject(Destination.InvalidElementContainer);
pointsData.StartPoint.TrySetActive(true);
pointsData.RepeatedSegmentPoint.TrySetActive(true);
pointsData.EndPoint.TrySetActive(activePointsCastData.HitData != null || EnableDestinationOnNoCollision);
RenderDataChanged?.Invoke(pointsData);
TryEmitVisibilityEvent();
}
///
/// Attempts to deactivate the .
///
/// Determines if the events should be emitted based on the state.
protected virtual void TryDeactivate(bool ignoreBehaviourState)
{
if ((!this.CheckIsActiveAndEnabled() && !ignoreBehaviourState) || !IsActivated)
{
return;
}
UpdateRenderData();
TryEmitExit(previousPointsCastData);
Deactivated?.Invoke(HoverTarget);
IsActivated = false;
activePointsCastData.Clear();
previousPointsCastData.Clear();
UpdateRenderData();
}
///
/// Checks to see if the is not currently colliding with a valid target and emits the event.
///
/// The current points cast data.
protected virtual void TryEmitExit(PointsCast.EventData data)
{
if (activePointsCastData.HitData?.transform != null)
{
Exited?.Invoke(GetEventData(data));
IsHovering = false;
HoverDuration = 0f;
}
}
///
/// Emits the or event for the current state in case that state changed.
///
protected virtual void TryEmitVisibilityEvent()
{
if (IsVisible == wasPreviouslyVisible)
{
return;
}
wasPreviouslyVisible = IsVisible;
if (IsVisible)
{
Appeared?.Invoke(HoverTarget);
}
else
{
Disappeared?.Invoke(HoverTarget);
}
}
///
/// Returns the used for a specific based on the current state.
///
/// The to return a for.
/// A to represent .
protected virtual GameObject GetElementRepresentation(PointerElement element)
{
bool isValid = activePointsCastData.HitData != null && activePointsCastData.IsValid;
bool showValidMesh = false;
bool showInvalidMesh = false;
switch (element.ElementVisibility)
{
case PointerElement.Visibility.OnWhenPointerActivated:
if (!IsActivated)
{
break;
}
showValidMesh = isValid;
showInvalidMesh = !isValid;
break;
case PointerElement.Visibility.AlwaysOn:
showValidMesh = true;
showInvalidMesh = true;
break;
case PointerElement.Visibility.AlwaysOff:
showValidMesh = false;
showInvalidMesh = false;
break;
default:
throw new ArgumentOutOfRangeException(nameof(element.ElementVisibility), element.ElementVisibility, null);
}
element.ValidMeshContainer.SetActive(showValidMesh);
element.InvalidMeshContainer.SetActive(showInvalidMesh);
element.IsVisible = showValidMesh || showInvalidMesh;
return isValid ? element.ValidElementContainer : element.InvalidElementContainer;
}
///
/// Attempts to deactivate an object that is an element of this if it's not one of the elements that should stay activated.
///
/// The object to check and deactivate if needed.
protected virtual void TryDeactivateElementObject(GameObject elementObject)
{
if (elementObject != null
&& elementObject != pointsData.StartPoint
&& elementObject != pointsData.RepeatedSegmentPoint
&& elementObject != pointsData.EndPoint)
{
elementObject.SetActive(false);
}
}
///
/// Builds a valid payload to use in the events.
///
/// A object of the 's current state.
protected virtual EventData GetEventData(PointsCast.EventData data)
{
Transform validDestinationTransform = Destination == null || Destination.ValidElementContainer == null ? null : Destination.ValidElementContainer.transform;
Transform pointerTransform = EventDataOriginTransformOverride != null ? EventDataOriginTransformOverride.transform : transform;
eventData.Transform = pointerTransform;
eventData.PositionOverride = validDestinationTransform == null ? data.HitData?.point : validDestinationTransform.position;
eventData.RotationOverride = validDestinationTransform == null ? Quaternion.identity : validDestinationTransform.localRotation;
eventData.ScaleOverride = validDestinationTransform == null ? Vector3.one : validDestinationTransform.lossyScale;
eventData.Origin = pointerTransform.position;
eventData.Direction = pointerTransform.forward;
eventData.CollisionData = data?.HitData ?? default;
return eventData.Set(IsActivated, IsHovering, HoverDuration, data);
}
}
}