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