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