namespace Zinnia.Tracking { using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Events; using Zinnia.Cast; using Zinnia.Data.Type; using Zinnia.Extension; using Zinnia.Process; using Zinnia.Process.Moment; using Zinnia.Rule; /// /// Casts a in a given direction and looks for the nearest valid surface. /// public class SurfaceLocator : MonoBehaviour, IProcessable { /// /// Compares two instances of . /// protected class RayCastHitComparer : IComparer { /// public virtual int Compare(RaycastHit x, RaycastHit y) { return x.distance.CompareTo(y.distance); } } /// /// Defines the event with the . /// [Serializable] public class UnityEvent : UnityEvent { } #region Search Settings [Header("Search Settings")] [Tooltip("The origin of where to begin the cast to locate the nearest surface.")] [SerializeField] private GameObject searchOrigin; /// /// The origin of where to begin the cast to locate the nearest surface. /// public GameObject SearchOrigin { get { return searchOrigin; } set { searchOrigin = value; } } [Tooltip("The direction in which to cast to locate the nearest surface.")] [SerializeField] private Vector3 searchDirection; /// /// The direction in which to cast to locate the nearest surface. /// public Vector3 SearchDirection { get { return searchDirection; } set { searchDirection = value; } } [Tooltip("The distance to move the origin backwards through the SearchDirection to ensure the origin isn't clipping a surface.")] [SerializeField] private float originOffset = -0.01f; /// /// The distance to move the origin backwards through the to ensure the origin isn't clipping a surface. /// public float OriginOffset { get { return originOffset; } set { originOffset = value; } } [Tooltip("The maximum distance to cast the Ray.")] [SerializeField] private float maximumDistance = 50f; /// /// The maximum distance to cast the . /// public float MaximumDistance { get { return maximumDistance; } set { maximumDistance = value; } } [Tooltip("The surface will only be located if the previous position has changed from the current position.")] [SerializeField] private bool mustChangePosition = true; /// /// The surface will only be located if the previous position has changed from the current position. /// public bool MustChangePosition { get { return mustChangePosition; } set { mustChangePosition = value; } } [Tooltip("The threshold difference between the previous point value and the current point value to be considered equal.")] [SerializeField] private float positionChangedEqualityThreshold = 0.0001f; /// /// The threshold difference between the previous point value and the current point value to be considered equal. /// public float PositionChangedEqualityThreshold { get { return positionChangedEqualityThreshold; } set { positionChangedEqualityThreshold = value; } } [Tooltip("The amount to offset the position of the destination point found on the located surface.")] [SerializeField] private Vector3 destinationOffset = Vector3.zero; /// /// The amount to offset the position of the destination point found on the located surface. /// public Vector3 DestinationOffset { get { return destinationOffset; } set { destinationOffset = value; } } #endregion #region Restriction Settings [Header("Restriction Settings")] [Tooltip("An optional RuleContainer to determine valid and invalid targets based on the set rules.")] [SerializeField] private RuleContainer targetValidity; /// /// An optional to determine valid and invalid targets based on the set rules. /// public RuleContainer TargetValidity { get { return targetValidity; } set { targetValidity = value; } } [Tooltip("An optional RuleContainer to determine specific target point based on the set rules.")] [SerializeField] private RuleContainer targetPointValidity; /// /// An optional to determine specific target point based on the set rules. /// public RuleContainer TargetPointValidity { get { return targetPointValidity; } set { targetPointValidity = value; } } [Tooltip("An optional RuleContainer to determine if the search for a valid surface should be terminated if the current found target matches the rule.")] [SerializeField] private RuleContainer locatorTermination; /// /// An optional to determine if the search for a valid surface should be terminated if the current found target matches the rule. /// public RuleContainer LocatorTermination { get { return locatorTermination; } set { locatorTermination = value; } } [Tooltip("An optional custom Cast.PhysicsCast object to affect the Ray.")] [SerializeField] private PhysicsCast physicsCast; /// /// An optional custom object to affect the . /// public PhysicsCast PhysicsCast { get { return physicsCast; } set { physicsCast = value; } } #endregion #region Location Events /// /// Emitted when a new surface is located. /// [Header("Location Events")] public UnityEvent SurfaceLocated = new UnityEvent(); #endregion /// /// The located surface. /// public readonly SurfaceData surfaceData = new SurfaceData(); /// /// A reused comparer instance. /// protected static readonly RayCastHitComparer Comparer = new RayCastHitComparer(); /// /// The comparison does. /// protected static readonly Comparison Comparison = Comparer.Compare; /// /// A reused data instance. /// protected readonly TransformData transformData = new TransformData(); /// /// Clears . /// public virtual void ClearSearchOrigin() { if (!this.IsValidState()) { return; } SearchOrigin = default; } /// /// Clears . /// public virtual void ClearTargetValidity() { if (!this.IsValidState()) { return; } TargetValidity = default; } /// /// Clears . /// public virtual void ClearTargetPointValidity() { if (!this.IsValidState()) { return; } TargetPointValidity = default; } /// /// Clears . /// public virtual void ClearLocatorTermination() { if (!this.IsValidState()) { return; } LocatorTermination = default; } /// /// Clears . /// public virtual void ClearPhysicsCast() { if (!this.IsValidState()) { return; } PhysicsCast = default; } /// /// Sets the x value. /// /// The value to set to. public virtual void SetSearchDirectionX(float value) { SearchDirection = new Vector3(value, SearchDirection.y, SearchDirection.z); } /// /// Sets the y value. /// /// The value to set to. public virtual void SetSearchDirectionY(float value) { SearchDirection = new Vector3(SearchDirection.x, value, SearchDirection.z); } /// /// Sets the z value. /// /// The value to set to. public virtual void SetSearchDirectionZ(float value) { SearchDirection = new Vector3(SearchDirection.x, SearchDirection.y, value); } /// /// Sets the x value. /// /// The value to set to. public virtual void SetDestinationOffsetX(float value) { DestinationOffset = new Vector3(value, DestinationOffset.y, DestinationOffset.z); } /// /// Sets the y value. /// /// The value to set to. public virtual void SetDestinationOffsetY(float value) { DestinationOffset = new Vector3(DestinationOffset.x, value, DestinationOffset.z); } /// /// Sets the z value. /// /// The value to set to. public virtual void SetDestinationOffsetZ(float value) { DestinationOffset = new Vector3(DestinationOffset.x, DestinationOffset.y, value); } /// /// Locates the nearest available surface upon a . /// public virtual void Process() { Locate(); } /// /// Locates the nearest available surface with the specified . /// public virtual void Locate() { transformData.Transform = SearchOrigin == null ? null : SearchOrigin.transform; Locate(transformData); } /// /// Locates the nearest available surface with the given . /// /// The object to use as the origin for the surface search. public virtual void Locate(TransformData givenOrigin) { if (!this.IsValidState() || givenOrigin == null || !givenOrigin.IsValid) { return; } if (CastRay(givenOrigin.Position, SearchDirection) && (!MustChangePosition || PositionChanged(PositionChangedEqualityThreshold))) { surfaceData.RotationOverride = givenOrigin.Rotation; surfaceData.ScaleOverride = givenOrigin.Scale; SurfaceLocated?.Invoke(surfaceData); } } protected virtual void OnEnable() { surfaceData.Clear(); } /// /// Checks to see if the surface position has changed between frames. /// /// The distance to consider a position change. /// if the surface position has changed or no previous surface data is found. protected virtual bool PositionChanged(float checkDistance) { return surfaceData.PreviousCollisionData.transform == null || !surfaceData.Position.ApproxEquals(surfaceData.PreviousCollisionData.point, checkDistance); } /// /// Casts a in the given direction from the given origin to search the nearest surface. /// /// The origin to begin the from. /// The direction in which to cast the . /// if a valid surface is located. protected virtual bool CastRay(Vector3 givenOrigin, Vector3 givenDirection) { givenOrigin += givenDirection.normalized * OriginOffset; surfaceData.Origin = givenOrigin; surfaceData.Direction = givenDirection; Ray tracerRaycast = new Ray(givenOrigin, givenDirection); return TargetValidity?.Interface == null && TargetPointValidity?.Interface == null ? FindFirstCollision(tracerRaycast) : FindAllCollisions(tracerRaycast); } /// /// Casts a ray to find the first collision. /// /// The ray to cast with. /// if a valid surface is located. protected virtual bool FindFirstCollision(Ray tracerRaycast) { if (PhysicsCast.Raycast(PhysicsCast, tracerRaycast, out RaycastHit collision, MaximumDistance, Physics.IgnoreRaycastLayer) && (LocatorTermination?.Interface == null || !CollisionMatchesRule(collision, LocatorTermination))) { SetSurfaceData(collision); return true; } return false; } /// /// Casts a ray to find all collisions. /// /// The ray to cast with. /// if a valid surface is located. protected virtual bool FindAllCollisions(Ray tracerRaycast) { ArraySegment raycastHits = PhysicsCast.RaycastAll( PhysicsCast, tracerRaycast, MaximumDistance, Physics.IgnoreRaycastLayer); ArraySortExtensions.Sort(raycastHits.Array, raycastHits.Offset, raycastHits.Count, Comparer, Comparison); foreach (RaycastHit collision in (HeapAllocationFreeReadOnlyList)raycastHits) { if (LocatorTermination?.Interface != null && CollisionMatchesRule(collision, LocatorTermination)) { break; } if (CollisionMatchesRule(collision, TargetValidity) && TargetPointValidity.Accepts(collision.point)) { SetSurfaceData(collision); return true; } } return false; } /// /// Sets the collision information. /// /// The data to use when setting . protected virtual void SetSurfaceData(RaycastHit collision) { surfaceData.CollisionData = collision; surfaceData.Transform = surfaceData.CollisionData.transform; surfaceData.PositionOverride = surfaceData.CollisionData.point + DestinationOffset; surfaceData.PositionalOffset = DestinationOffset; } /// /// Determines whether the data contains a valid collision against the given rule. /// /// The data to check for validity on. /// The to check the validity against. /// Whether the data contains a valid collision. protected virtual bool CollisionMatchesRule(RaycastHit collisionData, RuleContainer rule) { return collisionData.transform != null && rule.Accepts(collisionData.transform.gameObject); } } }