This commit is contained in:
PC-20230316NUNE\Administrator
2024-02-01 19:06:51 +08:00
parent aa4d6c3ce2
commit 877dca3b43
7518 changed files with 653768 additions and 162059 deletions

View File

@@ -0,0 +1,729 @@
using UnityEngine;
using System.Collections.Generic;
using Pathfinding.Util;
using Pathfinding.Serialization;
namespace Pathfinding {
/// <summary>
/// Exposes internal methods for graphs.
/// This is used to hide methods that should not be used by any user code
/// but still have to be 'public' or 'internal' (which is pretty much the same as 'public'
/// as this library is distributed with source code).
///
/// Hiding the internal methods cleans up the documentation and IntelliSense suggestions.
/// </summary>
public interface IGraphInternals {
string SerializedEditorSettings { get; set; }
void OnDestroy();
void DestroyAllNodes();
IEnumerable<Progress> ScanInternal();
void SerializeExtraInfo(GraphSerializationContext ctx);
void DeserializeExtraInfo(GraphSerializationContext ctx);
void PostDeserialization(GraphSerializationContext ctx);
void DeserializeSettingsCompatibility(GraphSerializationContext ctx);
}
/// <summary>Base class for all graphs</summary>
public abstract class NavGraph : IGraphInternals {
/// <summary>Reference to the AstarPath object in the scene</summary>
public AstarPath active;
/// <summary>
/// Used as an ID of the graph, considered to be unique.
/// Note: This is Pathfinding.Util.Guid not System.Guid. A replacement for System.Guid was coded for better compatibility with iOS
/// </summary>
[JsonMember]
public Guid guid;
/// <summary>Default penalty to apply to all nodes</summary>
[JsonMember]
public uint initialPenalty;
/// <summary>Is the graph open in the editor</summary>
[JsonMember]
public bool open;
/// <summary>Index of the graph, used for identification purposes</summary>
public uint graphIndex;
/// <summary>
/// Name of the graph.
/// Can be set in the unity editor
/// </summary>
[JsonMember]
public string name;
/// <summary>
/// Enable to draw gizmos in the Unity scene view.
/// In the inspector this value corresponds to the state of
/// the 'eye' icon in the top left corner of every graph inspector.
/// </summary>
[JsonMember]
public bool drawGizmos = true;
/// <summary>
/// Used in the editor to check if the info screen is open.
/// Should be inside UNITY_EDITOR only \<see cref="ifs"/> but just in case anyone tries to serialize a NavGraph instance using Unity, I have left it like this as it would otherwise cause a crash when building.
/// Version 3.0.8.1 was released because of this bug only
/// </summary>
[JsonMember]
public bool infoScreenOpen;
/// <summary>Used in the Unity editor to store serialized settings for graph inspectors</summary>
[JsonMember]
string serializedEditorSettings;
/// <summary>True if the graph exists, false if it has been destroyed</summary>
internal bool exists { get { return active != null; } }
/// <summary>
/// True if the graph has been scanned and contains nodes.
///
/// Graphs are typically scanned when the game starts, but they can also be scanned manually.
///
/// If a graph has not been scanned, it does not contain any nodes and it not possible to use it for pathfinding.
///
/// See: <see cref="AstarPath.Scan(NavGraph)"/>
/// </summary>
public abstract bool isScanned { get; }
/// <summary>
/// Number of nodes in the graph.
/// Note that this is, unless the graph type has overriden it, an O(n) operation.
///
/// This is an O(1) operation for grid graphs and point graphs.
/// For layered grid graphs it is an O(n) operation.
/// </summary>
public virtual int CountNodes () {
int count = 0;
GetNodes(node => count++);
return count;
}
/// <summary>Calls a delegate with all nodes in the graph until the delegate returns false</summary>
public void GetNodes (System.Func<GraphNode, bool> action) {
bool cont = true;
GetNodes(node => {
if (cont) cont &= action(node);
});
}
/// <summary>
/// Calls a delegate with all nodes in the graph.
/// This is the primary way of iterating through all nodes in a graph.
///
/// Do not change the graph structure inside the delegate.
///
/// <code>
/// var gg = AstarPath.active.data.gridGraph;
///
/// gg.GetNodes(node => {
/// // Here is a node
/// Debug.Log("I found a node at position " + (Vector3)node.position);
/// });
/// </code>
///
/// If you want to store all nodes in a list you can do this
///
/// <code>
/// var gg = AstarPath.active.data.gridGraph;
///
/// List<GraphNode> nodes = new List<GraphNode>();
///
/// gg.GetNodes((System.Action<GraphNode>)nodes.Add);
/// </code>
///
/// See: <see cref="Pathfinding.AstarData.GetNodes"/>
/// </summary>
public abstract void GetNodes(System.Action<GraphNode> action);
/// <summary>True if the point is inside the bounding box of this graph</summary>
public virtual bool IsInsideBounds (Vector3 point) {
return true;
}
/// <summary>
/// A matrix for translating/rotating/scaling the graph.
/// Deprecated: Use the transform field (only available on some graph types) instead
/// </summary>
[System.Obsolete("Use the transform field (only available on some graph types) instead", true)]
public Matrix4x4 matrix = Matrix4x4.identity;
/// <summary>
/// Inverse of matrix.
/// Deprecated: Use the transform field (only available on some graph types) instead
/// </summary>
[System.Obsolete("Use the transform field (only available on some graph types) instead", true)]
public Matrix4x4 inverseMatrix = Matrix4x4.identity;
/// <summary>
/// Use to set both matrix and inverseMatrix at the same time.
/// Deprecated: Use the transform field (only available on some graph types) instead
/// </summary>
[System.Obsolete("Use the transform field (only available on some graph types) instead", true)]
public void SetMatrix (Matrix4x4 m) {
matrix = m;
inverseMatrix = m.inverse;
}
/// <summary>
/// Moves nodes in this graph.
/// Deprecated: Use RelocateNodes(Matrix4x4) instead.
/// To keep the same behavior you can call RelocateNodes(newMatrix * oldMatrix.inverse).
/// </summary>
[System.Obsolete("Use RelocateNodes(Matrix4x4) instead. To keep the same behavior you can call RelocateNodes(newMatrix * oldMatrix.inverse).")]
public void RelocateNodes (Matrix4x4 oldMatrix, Matrix4x4 newMatrix) {
RelocateNodes(newMatrix * oldMatrix.inverse);
}
/// <summary>
/// Throws an exception if it is not safe to update internal graph data right now.
///
/// It is safe to update graphs when graphs are being scanned, or inside a work item.
/// In other cases pathfinding could be running at the same time, which would not appreciate graph data changing under its feet.
///
/// See: <see cref="AstarPath.AddWorkItem"/>
/// </summary>
protected void AssertSafeToUpdateGraph () {
if (!active.IsAnyWorkItemInProgress && !active.isScanning) {
throw new System.Exception("Trying to update graphs when it is not safe to do so. Graph updates must be done inside a work item or when a graph is being scanned. See AstarPath.AddWorkItem");
}
}
/// <summary>
/// Moves the nodes in this graph.
/// Multiplies all node positions by deltaMatrix.
///
/// For example if you want to move all your nodes in e.g a point graph 10 units along the X axis from the initial position
/// <code>
/// var graph = AstarPath.data.pointGraph;
/// var m = Matrix4x4.TRS (new Vector3(10,0,0), Quaternion.identity, Vector3.one);
/// graph.RelocateNodes (m);
/// </code>
///
/// Note: For grid graphs, navmesh graphs and recast graphs it is recommended to
/// use their custom overloads of the RelocateNodes method which take parameters
/// for e.g center and nodeSize (and additional parameters) instead since
/// they are both easier to use and are less likely to mess up pathfinding.
///
/// Warning: This method is lossy for PointGraphs, so calling it many times may
/// cause node positions to lose precision. For example if you set the scale
/// to 0 in one call then all nodes will be scaled/moved to the same point and
/// you will not be able to recover their original positions. The same thing
/// happens for other - less extreme - values as well, but to a lesser degree.
/// </summary>
public virtual void RelocateNodes (Matrix4x4 deltaMatrix) {
GetNodes(node => node.position = ((Int3)deltaMatrix.MultiplyPoint((Vector3)node.position)));
}
/// <summary>
/// Returns the nearest node to a position.
/// See: Pathfinding.NNConstraint.None
/// </summary>
/// <param name="position">The position to try to find a close node to</param>
public NNInfoInternal GetNearest (Vector3 position) {
return GetNearest(position, NNConstraint.None);
}
/// <summary>Returns the nearest node to a position using the specified NNConstraint.</summary>
/// <param name="position">The position to try to find a close node to</param>
/// <param name="constraint">Can for example tell the function to try to return a walkable node. If you do not get a good node back, consider calling GetNearestForce.</param>
public NNInfoInternal GetNearest (Vector3 position, NNConstraint constraint) {
return GetNearest(position, constraint, null);
}
/// <summary>Returns the nearest node to a position using the specified NNConstraint.</summary>
/// <param name="position">The position to try to find a close node to</param>
/// <param name="hint">Can be passed to enable some graph generators to find the nearest node faster.</param>
/// <param name="constraint">Can for example tell the function to try to return a walkable node. If you do not get a good node back, consider calling GetNearestForce.</param>
public virtual NNInfoInternal GetNearest (Vector3 position, NNConstraint constraint, GraphNode hint) {
// This is a default implementation and it is pretty slow
// Graphs usually override this to provide faster and more specialised implementations
float maxDistSqr = constraint == null || constraint.constrainDistance ? AstarPath.active.maxNearestNodeDistanceSqr : float.PositiveInfinity;
float minDist = float.PositiveInfinity;
GraphNode minNode = null;
float minConstDist = float.PositiveInfinity;
GraphNode minConstNode = null;
// Loop through all nodes and find the closest suitable node
GetNodes(node => {
float dist = (position-(Vector3)node.position).sqrMagnitude;
if (dist < minDist) {
minDist = dist;
minNode = node;
}
if (dist < minConstDist && dist < maxDistSqr && (constraint == null || constraint.Suitable(node))) {
minConstDist = dist;
minConstNode = node;
}
});
var nnInfo = new NNInfoInternal(minNode);
nnInfo.constrainedNode = minConstNode;
if (minConstNode != null) {
nnInfo.constClampedPosition = (Vector3)minConstNode.position;
} else if (minNode != null) {
nnInfo.constrainedNode = minNode;
nnInfo.constClampedPosition = (Vector3)minNode.position;
}
return nnInfo;
}
/// <summary>
/// Returns the nearest node to a position using the specified <see cref="Pathfinding.NNConstraint"/>.
/// Returns: an NNInfo. This method will only return an empty NNInfo if there are no nodes which comply with the specified constraint.
/// </summary>
public virtual NNInfoInternal GetNearestForce (Vector3 position, NNConstraint constraint) {
return GetNearest(position, constraint);
}
/// <summary>
/// Function for cleaning up references.
/// This will be called on the same time as OnDisable on the gameObject which the AstarPath script is attached to (remember, not in the editor).
/// Use for any cleanup code such as cleaning up static variables which otherwise might prevent resources from being collected.
/// Use by creating a function overriding this one in a graph class, but always call base.OnDestroy () in that function.
/// All nodes should be destroyed in this function otherwise a memory leak will arise.
/// </summary>
protected virtual void OnDestroy () {
DestroyAllNodes();
}
/// <summary>
/// Destroys all nodes in the graph.
/// Warning: This is an internal method. Unless you have a very good reason, you should probably not call it.
/// </summary>
protected virtual void DestroyAllNodes () {
GetNodes(node => node.Destroy());
}
/// <summary>
/// Scan the graph.
/// Deprecated: Use AstarPath.Scan() instead
/// </summary>
[System.Obsolete("Use AstarPath.Scan instead")]
public void ScanGraph () {
Scan();
}
/// <summary>
/// Scan the graph.
///
/// Consider using AstarPath.Scan() instead since this function only scans this graph and if you are using multiple graphs
/// with connections between them, then it is better to scan all graphs at once.
/// </summary>
public void Scan () {
active.Scan(this);
}
/// <summary>
/// Internal method to scan the graph.
/// Called from AstarPath.ScanAsync.
/// Override this function to implement custom scanning logic.
/// Progress objects can be yielded to show progress info in the editor and to split up processing
/// over several frames when using async scanning.
/// </summary>
protected abstract IEnumerable<Progress> ScanInternal();
/// <summary>
/// Serializes graph type specific node data.
/// This function can be overriden to serialize extra node information (or graph information for that matter)
/// which cannot be serialized using the standard serialization.
/// Serialize the data in any way you want and return a byte array.
/// When loading, the exact same byte array will be passed to the DeserializeExtraInfo function.
/// These functions will only be called if node serialization is enabled.
/// </summary>
protected virtual void SerializeExtraInfo (GraphSerializationContext ctx) {
}
/// <summary>
/// Deserializes graph type specific node data.
/// See: SerializeExtraInfo
/// </summary>
protected virtual void DeserializeExtraInfo (GraphSerializationContext ctx) {
}
/// <summary>
/// Called after all deserialization has been done for all graphs.
/// Can be used to set up more graph data which is not serialized
/// </summary>
protected virtual void PostDeserialization (GraphSerializationContext ctx) {
}
/// <summary>
/// An old format for serializing settings.
/// Deprecated: This is deprecated now, but the deserialization code is kept to
/// avoid loosing data when upgrading from older versions.
/// </summary>
protected virtual void DeserializeSettingsCompatibility (GraphSerializationContext ctx) {
guid = new Guid(ctx.reader.ReadBytes(16));
initialPenalty = ctx.reader.ReadUInt32();
open = ctx.reader.ReadBoolean();
name = ctx.reader.ReadString();
drawGizmos = ctx.reader.ReadBoolean();
infoScreenOpen = ctx.reader.ReadBoolean();
}
/// <summary>Draw gizmos for the graph</summary>
public virtual void OnDrawGizmos (RetainedGizmos gizmos, bool drawNodes) {
if (!drawNodes) {
return;
}
// This is a relatively slow default implementation.
// subclasses of the base graph class may override
// this method to draw gizmos in a more optimized way
var hasher = new RetainedGizmos.Hasher(active);
GetNodes(node => hasher.HashNode(node));
// Update the gizmo mesh if necessary
if (!gizmos.Draw(hasher)) {
using (var helper = gizmos.GetGizmoHelper(active, hasher)) {
GetNodes((System.Action<GraphNode>)helper.DrawConnections);
}
}
if (active.showUnwalkableNodes) DrawUnwalkableNodes(active.unwalkableNodeDebugSize);
}
protected void DrawUnwalkableNodes (float size) {
Gizmos.color = AstarColor.UnwalkableNode;
GetNodes(node => {
if (!node.Walkable) Gizmos.DrawCube((Vector3)node.position, Vector3.one*size);
});
}
#region IGraphInternals implementation
string IGraphInternals.SerializedEditorSettings { get { return serializedEditorSettings; } set { serializedEditorSettings = value; } }
void IGraphInternals.OnDestroy () { OnDestroy(); }
void IGraphInternals.DestroyAllNodes () { DestroyAllNodes(); }
IEnumerable<Progress> IGraphInternals.ScanInternal () { return ScanInternal(); }
void IGraphInternals.SerializeExtraInfo (GraphSerializationContext ctx) { SerializeExtraInfo(ctx); }
void IGraphInternals.DeserializeExtraInfo (GraphSerializationContext ctx) { DeserializeExtraInfo(ctx); }
void IGraphInternals.PostDeserialization (GraphSerializationContext ctx) { PostDeserialization(ctx); }
void IGraphInternals.DeserializeSettingsCompatibility (GraphSerializationContext ctx) { DeserializeSettingsCompatibility(ctx); }
#endregion
}
/// <summary>
/// Handles collision checking for graphs.
/// Mostly used by grid based graphs
/// </summary>
[System.Serializable]
public class GraphCollision {
/// <summary>
/// Collision shape to use.
/// See: <see cref="Pathfinding.ColliderType"/>
/// </summary>
public ColliderType type = ColliderType.Capsule;
/// <summary>
/// Diameter of capsule or sphere when checking for collision.
/// When checking for collisions the system will check if any colliders
/// overlap a specific shape at the node's position. The shape is determined
/// by the <see cref="type"/> field.
///
/// A diameter of 1 means that the shape has a diameter equal to the node's width,
/// or in other words it is equal to <see cref="Pathfinding.GridGraph.nodeSize"/>.
///
/// If <see cref="type"/> is set to Ray, this does not affect anything.
///
/// [Open online documentation to see images]
/// </summary>
public float diameter = 1F;
/// <summary>
/// Height of capsule or length of ray when checking for collision.
/// If <see cref="type"/> is set to Sphere, this does not affect anything.
///
/// [Open online documentation to see images]
///
/// Warning: In contrast to Unity's capsule collider and character controller this height does not include the end spheres of the capsule, but only the cylinder part.
/// This is mostly for historical reasons.
/// </summary>
public float height = 2F;
/// <summary>
/// Height above the ground that collision checks should be done.
/// For example, if the ground was found at y=0, collisionOffset = 2
/// type = Capsule and height = 3 then the physics system
/// will be queried to see if there are any colliders in a capsule
/// for which the bottom sphere that is made up of is centered at y=2
/// and the top sphere has its center at y=2+3=5.
///
/// If type = Sphere then the sphere's center would be at y=2 in this case.
/// </summary>
public float collisionOffset;
/// <summary>
/// Direction of the ray when checking for collision.
/// If <see cref="type"/> is not Ray, this does not affect anything
/// </summary>
public RayDirection rayDirection = RayDirection.Both;
/// <summary>Layers to be treated as obstacles.</summary>
public LayerMask mask;
/// <summary>Layers to be included in the height check.</summary>
public LayerMask heightMask = -1;
/// <summary>
/// The height to check from when checking height ('ray length' in the inspector).
///
/// As the image below visualizes, different ray lengths can make the ray hit different things.
/// The distance is measured up from the graph plane.
///
/// [Open online documentation to see images]
/// </summary>
public float fromHeight = 100;
/// <summary>
/// Toggles thick raycast.
/// See: https://docs.unity3d.com/ScriptReference/Physics.SphereCast.html
/// </summary>
public bool thickRaycast;
/// <summary>
/// Diameter of the thick raycast in nodes.
/// 1 equals <see cref="Pathfinding.GridGraph.nodeSize"/>
/// </summary>
public float thickRaycastDiameter = 1;
/// <summary>Make nodes unwalkable when no ground was found with the height raycast. If height raycast is turned off, this doesn't affect anything.</summary>
public bool unwalkableWhenNoGround = true;
/// <summary>
/// Use Unity 2D Physics API.
/// See: http://docs.unity3d.com/ScriptReference/Physics2D.html
/// </summary>
public bool use2D;
/// <summary>Toggle collision check</summary>
public bool collisionCheck = true;
/// <summary>Toggle height check. If false, the grid will be flat</summary>
public bool heightCheck = true;
/// <summary>
/// Direction to use as UP.
/// See: Initialize
/// </summary>
public Vector3 up;
/// <summary>
/// <see cref="up"/> * <see cref="height"/>.
/// See: Initialize
/// </summary>
private Vector3 upheight;
/// <summary>Used for 2D collision queries</summary>
private ContactFilter2D contactFilter;
/// <summary>
/// Just so that the Physics2D.OverlapPoint method has some buffer to store things in.
/// We never actually read from this array, so we don't even care if this is thread safe.
/// </summary>
private static Collider2D[] dummyArray = new Collider2D[1];
/// <summary>
/// <see cref="diameter"/> * scale * 0.5.
/// Where scale usually is <see cref="Pathfinding.GridGraph.nodeSize"/>
/// See: Initialize
/// </summary>
private float finalRadius;
/// <summary>
/// <see cref="thickRaycastDiameter"/> * scale * 0.5.
/// Where scale usually is <see cref="Pathfinding.GridGraph.nodeSize"/> See: Initialize
/// </summary>
private float finalRaycastRadius;
/// <summary>Offset to apply after each raycast to make sure we don't hit the same point again in CheckHeightAll</summary>
public const float RaycastErrorMargin = 0.005F;
/// <summary>
/// Sets up several variables using the specified matrix and scale.
/// See: GraphCollision.up
/// See: GraphCollision.upheight
/// See: GraphCollision.finalRadius
/// See: GraphCollision.finalRaycastRadius
/// </summary>
public void Initialize (GraphTransform transform, float scale) {
up = (transform.Transform(Vector3.up) - transform.Transform(Vector3.zero)).normalized;
upheight = up*height;
finalRadius = diameter*scale*0.5F;
finalRaycastRadius = thickRaycastDiameter*scale*0.5F;
contactFilter = new ContactFilter2D { layerMask = mask, useDepth = false, useLayerMask = true, useNormalAngle = false, useTriggers = false };
}
/// <summary>
/// Returns true if the position is not obstructed.
/// If <see cref="collisionCheck"/> is false, this will always return true.
/// </summary>
public bool Check (Vector3 position) {
if (!collisionCheck) {
return true;
}
if (use2D) {
switch (type) {
case ColliderType.Capsule:
case ColliderType.Sphere:
return Physics2D.OverlapCircle(position, finalRadius, contactFilter, dummyArray) == 0;
default:
return Physics2D.OverlapPoint(position, contactFilter, dummyArray) == 0;
}
}
position += up*collisionOffset;
switch (type) {
case ColliderType.Capsule:
return !Physics.CheckCapsule(position, position+upheight, finalRadius, mask, QueryTriggerInteraction.Ignore);
case ColliderType.Sphere:
return !Physics.CheckSphere(position, finalRadius, mask, QueryTriggerInteraction.Ignore);
default:
switch (rayDirection) {
case RayDirection.Both:
return !Physics.Raycast(position, up, height, mask, QueryTriggerInteraction.Ignore) && !Physics.Raycast(position+upheight, -up, height, mask, QueryTriggerInteraction.Ignore);
case RayDirection.Up:
return !Physics.Raycast(position, up, height, mask, QueryTriggerInteraction.Ignore);
default:
return !Physics.Raycast(position+upheight, -up, height, mask, QueryTriggerInteraction.Ignore);
}
}
}
/// <summary>
/// Returns the position with the correct height.
/// If <see cref="heightCheck"/> is false, this will return position.
/// </summary>
public Vector3 CheckHeight (Vector3 position) {
RaycastHit hit;
bool walkable;
return CheckHeight(position, out hit, out walkable);
}
/// <summary>
/// Returns the position with the correct height.
/// If <see cref="heightCheck"/> is false, this will return position.
/// walkable will be set to false if nothing was hit.
/// The ray will check a tiny bit further than to the grids base to avoid floating point errors when the ground is exactly at the base of the grid
/// </summary>
public Vector3 CheckHeight (Vector3 position, out RaycastHit hit, out bool walkable) {
walkable = true;
if (!heightCheck || use2D) {
hit = new RaycastHit();
return position;
}
if (thickRaycast) {
var ray = new Ray(position+up*fromHeight, -up);
if (Physics.SphereCast(ray, finalRaycastRadius, out hit, fromHeight+0.005F, heightMask, QueryTriggerInteraction.Ignore)) {
return VectorMath.ClosestPointOnLine(ray.origin, ray.origin+ray.direction, hit.point);
}
walkable &= !unwalkableWhenNoGround;
} else {
// Cast a ray from above downwards to try to find the ground
if (Physics.Raycast(position+up*fromHeight, -up, out hit, fromHeight+0.005F, heightMask, QueryTriggerInteraction.Ignore)) {
return hit.point;
}
walkable &= !unwalkableWhenNoGround;
}
return position;
}
/// <summary>Internal buffer used by <see cref="CheckHeightAll"/></summary>
RaycastHit[] hitBuffer = new RaycastHit[8];
/// <summary>
/// Returns all hits when checking height for position.
/// Warning: Does not work well with thick raycast, will only return an object a single time
///
/// Warning: The returned array is ephermal. It will be invalidated when this method is called again.
/// If you need persistent results you should copy it.
///
/// The returned array may be larger than the actual number of hits, the numHits out parameter indicates how many hits there actually were.
/// </summary>
public RaycastHit[] CheckHeightAll (Vector3 position, out int numHits) {
if (!heightCheck || use2D) {
hitBuffer[0] = new RaycastHit {
point = position,
distance = 0,
};
numHits = 1;
return hitBuffer;
}
// Cast a ray from above downwards to try to find the ground
#if UNITY_2017_1_OR_NEWER
numHits = Physics.RaycastNonAlloc(position+up*fromHeight, -up, hitBuffer, fromHeight+0.005F, heightMask, QueryTriggerInteraction.Ignore);
if (numHits == hitBuffer.Length) {
// Try again with a larger buffer
hitBuffer = new RaycastHit[hitBuffer.Length*2];
return CheckHeightAll(position, out numHits);
}
return hitBuffer;
#else
var result = Physics.RaycastAll(position+up*fromHeight, -up, fromHeight+0.005F, heightMask, QueryTriggerInteraction.Ignore);
numHits = result.Length;
return result;
#endif
}
public void DeserializeSettingsCompatibility (GraphSerializationContext ctx) {
type = (ColliderType)ctx.reader.ReadInt32();
diameter = ctx.reader.ReadSingle();
height = ctx.reader.ReadSingle();
collisionOffset = ctx.reader.ReadSingle();
rayDirection = (RayDirection)ctx.reader.ReadInt32();
mask = (LayerMask)ctx.reader.ReadInt32();
heightMask = (LayerMask)ctx.reader.ReadInt32();
fromHeight = ctx.reader.ReadSingle();
thickRaycast = ctx.reader.ReadBoolean();
thickRaycastDiameter = ctx.reader.ReadSingle();
unwalkableWhenNoGround = ctx.reader.ReadBoolean();
use2D = ctx.reader.ReadBoolean();
collisionCheck = ctx.reader.ReadBoolean();
heightCheck = ctx.reader.ReadBoolean();
}
}
/// <summary>
/// Determines collision check shape.
/// See: <see cref="Pathfinding.GraphCollision"/>
/// </summary>
public enum ColliderType {
/// <summary>Uses a Sphere, Physics.CheckSphere. In 2D this is a circle instead.</summary>
Sphere,
/// <summary>Uses a Capsule, Physics.CheckCapsule. This will behave identically to the Sphere mode in 2D.</summary>
Capsule,
/// <summary>Uses a Ray, Physics.Linecast. In 2D this is a single point instead.</summary>
Ray
}
/// <summary>Determines collision check ray direction</summary>
public enum RayDirection {
Up, /// <summary>< Casts the ray from the bottom upwards</summary>
Down, /// <summary>< Casts the ray from the top downwards</summary>
Both /// <summary>< Casts two rays in both directions</summary>
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c7636164485c04efe8fad73ab1ee985f
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d5d28978e568e40429b2981fab3e380e
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d6ea17a678e4042de89cdfa01860ad8a
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,280 @@
using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding {
using Pathfinding.Util;
using Pathfinding.Serialization;
public interface INavmesh {
void GetNodes(System.Action<GraphNode> del);
}
/// <summary>
/// Generates graphs based on navmeshes.
/// [Open online documentation to see images]
///
/// Navmeshes are meshes in which each triangle defines a walkable area.
/// These are great because the AI can get so much more information on how it can walk.
/// Polygons instead of points mean that the <see cref="FunnelModifier"/> can produce really nice looking paths, and the graphs are also really fast to search
/// and have a low memory footprint because fewer nodes are usually needed to describe the same area compared to grid graphs.
///
/// The navmesh graph requires that you create a navmesh manually. The package also has support for generating navmeshes automatically using the <see cref="RecastGraph"/>.
///
/// For a tutorial on how to configure a navmesh graph, take a look at getstarted2 (view in online documentation for working links).
///
/// [Open online documentation to see images]
/// [Open online documentation to see images]
///
/// See: Pathfinding.RecastGraph
/// </summary>
[JsonOptIn]
[Pathfinding.Util.Preserve]
public class NavMeshGraph : NavmeshBase, IUpdatableGraph {
/// <summary>Mesh to construct navmesh from</summary>
[JsonMember]
public Mesh sourceMesh;
/// <summary>Offset in world space</summary>
[JsonMember]
public Vector3 offset;
/// <summary>Rotation in degrees</summary>
[JsonMember]
public Vector3 rotation;
/// <summary>Scale of the graph</summary>
[JsonMember]
public float scale = 1;
/// <summary>
/// Determines how normals are calculated.
/// Disable for spherical graphs or other complicated surfaces that allow the agents to e.g walk on walls or ceilings.
///
/// By default the normals of the mesh will be flipped so that they point as much as possible in the upwards direction.
/// The normals are important when connecting adjacent nodes. Two adjacent nodes will only be connected if they are oriented the same way.
/// This is particularly important if you have a navmesh on the walls or even on the ceiling of a room. Or if you are trying to make a spherical navmesh.
/// If you do one of those things then you should set disable this setting and make sure the normals in your source mesh are properly set.
///
/// If you for example take a look at the image below. In the upper case then the nodes on the bottom half of the
/// mesh haven't been connected with the nodes on the upper half because the normals on the lower half will have been
/// modified to point inwards (as that is the direction that makes them face upwards the most) while the normals on
/// the upper half point outwards. This causes the nodes to not connect properly along the seam. When this option
/// is set to false instead the nodes are connected properly as in the original mesh all normals point outwards.
/// [Open online documentation to see images]
///
/// The default value of this field is true to reduce the risk for errors in the common case. If a mesh is supplied that
/// has all normals pointing downwards and this option is false, then some methods like <see cref="PointOnNavmesh"/> will not work correctly
/// as they assume that the normals point upwards. For a more complicated surface like a spherical graph those methods make no sense anyway
/// as there is no clear definition of what it means to be "inside" a triangle when there is no clear up direction.
/// </summary>
[JsonMember]
public bool recalculateNormals = true;
/// <summary>
/// Cached bounding box minimum of <see cref="sourceMesh"/>.
/// This is important when the graph has been saved to a file and is later loaded again, but the original mesh does not exist anymore (or has been moved).
/// In that case we still need to be able to find the bounding box since the <see cref="CalculateTransform"/> method uses it.
/// </summary>
[JsonMember]
Vector3 cachedSourceMeshBoundsMin;
public override bool RecalculateNormals { get { return recalculateNormals; } }
public override float TileWorldSizeX {
get {
return forcedBoundsSize.x;
}
}
public override float TileWorldSizeZ {
get {
return forcedBoundsSize.z;
}
}
protected override float MaxTileConnectionEdgeDistance {
get {
// Tiles are not supported, so this is irrelevant
return 0f;
}
}
/// <summary>
/// True if the point is inside the bounding box of this graph.
///
/// Warning: If your input mesh is entirely flat, the bounding box will also end up entirely flat (with a height of zero), this will make this function always return false for almost all points, unless they are at exactly the right y-coordinate.
///
/// Note: For an unscanned graph, this will always return false.
/// </summary>
public override bool IsInsideBounds (Vector3 point) {
if (this.tiles == null || this.tiles.Length == 0 || sourceMesh == null) return false;
var local = transform.InverseTransform(point);
var size = sourceMesh.bounds.size*scale;
// Allow a small margin
const float EPS = 0.0001f;
return local.x >= -EPS && local.y >= -EPS && local.z >= -EPS && local.x <= size.x + EPS && local.y <= size.y + EPS && local.z <= size.z + EPS;
}
public override GraphTransform CalculateTransform () {
return new GraphTransform(Matrix4x4.TRS(offset, Quaternion.Euler(rotation), Vector3.one) * Matrix4x4.TRS(sourceMesh != null ? sourceMesh.bounds.min * scale : cachedSourceMeshBoundsMin * scale, Quaternion.identity, Vector3.one));
}
GraphUpdateThreading IUpdatableGraph.CanUpdateAsync (GraphUpdateObject o) {
return GraphUpdateThreading.UnityThread;
}
void IUpdatableGraph.UpdateAreaInit (GraphUpdateObject o) {}
void IUpdatableGraph.UpdateAreaPost (GraphUpdateObject o) {}
void IUpdatableGraph.UpdateArea (GraphUpdateObject o) {
UpdateArea(o, this);
}
public static void UpdateArea (GraphUpdateObject o, INavmeshHolder graph) {
Bounds bounds = graph.transform.InverseTransform(o.bounds);
// Bounding rectangle with integer coordinates
var irect = new IntRect(
Mathf.FloorToInt(bounds.min.x*Int3.Precision),
Mathf.FloorToInt(bounds.min.z*Int3.Precision),
Mathf.CeilToInt(bounds.max.x*Int3.Precision),
Mathf.CeilToInt(bounds.max.z*Int3.Precision)
);
// Corners of the bounding rectangle
var a = new Int3(irect.xmin, 0, irect.ymin);
var b = new Int3(irect.xmin, 0, irect.ymax);
var c = new Int3(irect.xmax, 0, irect.ymin);
var d = new Int3(irect.xmax, 0, irect.ymax);
var ymin = ((Int3)bounds.min).y;
var ymax = ((Int3)bounds.max).y;
// Loop through all nodes and check if they intersect the bounding box
graph.GetNodes(_node => {
var node = _node as TriangleMeshNode;
bool inside = false;
int allLeft = 0;
int allRight = 0;
int allTop = 0;
int allBottom = 0;
// Check bounding box rect in XZ plane
for (int v = 0; v < 3; v++) {
Int3 p = node.GetVertexInGraphSpace(v);
if (irect.Contains(p.x, p.z)) {
inside = true;
break;
}
if (p.x < irect.xmin) allLeft++;
if (p.x > irect.xmax) allRight++;
if (p.z < irect.ymin) allTop++;
if (p.z > irect.ymax) allBottom++;
}
if (!inside && (allLeft == 3 || allRight == 3 || allTop == 3 || allBottom == 3)) {
return;
}
// Check if the polygon edges intersect the bounding rect
for (int v = 0; v < 3; v++) {
int v2 = v > 1 ? 0 : v+1;
Int3 vert1 = node.GetVertexInGraphSpace(v);
Int3 vert2 = node.GetVertexInGraphSpace(v2);
if (VectorMath.SegmentsIntersectXZ(a, b, vert1, vert2)) { inside = true; break; }
if (VectorMath.SegmentsIntersectXZ(a, c, vert1, vert2)) { inside = true; break; }
if (VectorMath.SegmentsIntersectXZ(c, d, vert1, vert2)) { inside = true; break; }
if (VectorMath.SegmentsIntersectXZ(d, b, vert1, vert2)) { inside = true; break; }
}
// Check if the node contains any corner of the bounding rect
if (inside || node.ContainsPointInGraphSpace(a) || node.ContainsPointInGraphSpace(b) || node.ContainsPointInGraphSpace(c) || node.ContainsPointInGraphSpace(d)) {
inside = true;
}
if (!inside) {
return;
}
int allAbove = 0;
int allBelow = 0;
// Check y coordinate
for (int v = 0; v < 3; v++) {
Int3 p = node.GetVertexInGraphSpace(v);
if (p.y < ymin) allBelow++;
if (p.y > ymax) allAbove++;
}
// Polygon is either completely above the bounding box or completely below it
if (allBelow == 3 || allAbove == 3) return;
// Triangle is inside the bounding box!
// Update it!
o.WillUpdateNode(node);
o.Apply(node);
});
}
protected override IEnumerable<Progress> ScanInternal () {
cachedSourceMeshBoundsMin = sourceMesh != null ? sourceMesh.bounds.min : Vector3.zero;
transform = CalculateTransform();
tileZCount = tileXCount = 1;
tiles = new NavmeshTile[tileZCount*tileXCount];
TriangleMeshNode.SetNavmeshHolder(AstarPath.active.data.GetGraphIndex(this), this);
if (sourceMesh == null) {
FillWithEmptyTiles();
yield break;
}
yield return new Progress(0.0f, "Transforming Vertices");
forcedBoundsSize = sourceMesh.bounds.size * scale;
Vector3[] vectorVertices = sourceMesh.vertices;
var intVertices = ListPool<Int3>.Claim(vectorVertices.Length);
var matrix = Matrix4x4.TRS(-sourceMesh.bounds.min * scale, Quaternion.identity, Vector3.one * scale);
// Convert the vertices to integer coordinates and also position them in graph space
// so that the minimum of the bounding box of the mesh is at the origin
// (the vertices will later be transformed to world space)
for (int i = 0; i < vectorVertices.Length; i++) {
intVertices.Add((Int3)matrix.MultiplyPoint3x4(vectorVertices[i]));
}
yield return new Progress(0.1f, "Compressing Vertices");
// Remove duplicate vertices
Int3[] compressedVertices = null;
int[] compressedTriangles = null;
Polygon.CompressMesh(intVertices, new List<int>(sourceMesh.triangles), out compressedVertices, out compressedTriangles);
ListPool<Int3>.Release(ref intVertices);
yield return new Progress(0.2f, "Building Nodes");
ReplaceTile(0, 0, compressedVertices, compressedTriangles);
// Signal that tiles have been recalculated to the navmesh cutting system.
navmeshUpdateData.OnRecalculatedTiles(tiles);
if (OnRecalculatedTiles != null) OnRecalculatedTiles(tiles.Clone() as NavmeshTile[]);
}
protected override void DeserializeSettingsCompatibility (GraphSerializationContext ctx) {
base.DeserializeSettingsCompatibility(ctx);
sourceMesh = ctx.DeserializeUnityObject() as Mesh;
offset = ctx.DeserializeVector3();
rotation = ctx.DeserializeVector3();
scale = ctx.reader.ReadSingle();
nearestSearchOnlyXZ = !ctx.reader.ReadBoolean();
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: ffae751ca240c466185a168f4a9836cb
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f8b76f19632144117a3ac7f28faf2c15
timeCreated: 1474405146
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e8a4fbed4121f4ae5a44468fdf1bce0e

View File

@@ -0,0 +1,927 @@
using System.Collections.Generic;
using Pathfinding.Serialization;
using UnityEngine;
namespace Pathfinding {
/// <summary>Node used for the GridGraph</summary>
public class GridNode : GridNodeBase {
public GridNode (AstarPath astar) : base(astar) {
}
#if !ASTAR_NO_GRID_GRAPH
private static GridGraph[] _gridGraphs = new GridGraph[0];
public static GridGraph GetGridGraph (uint graphIndex) { return _gridGraphs[(int)graphIndex]; }
public static void SetGridGraph (int graphIndex, GridGraph graph) {
if (_gridGraphs.Length <= graphIndex) {
var gg = new GridGraph[graphIndex+1];
for (int i = 0; i < _gridGraphs.Length; i++) gg[i] = _gridGraphs[i];
_gridGraphs = gg;
}
_gridGraphs[graphIndex] = graph;
}
public static void ClearGridGraph (int graphIndex, GridGraph graph) {
if (graphIndex < _gridGraphs.Length && _gridGraphs[graphIndex] == graph) {
_gridGraphs[graphIndex] = null;
}
}
/// <summary>Internal use only</summary>
internal ushort InternalGridFlags {
get { return gridFlags; }
set { gridFlags = value; }
}
const int GridFlagsConnectionOffset = 0;
const int GridFlagsConnectionBit0 = 1 << GridFlagsConnectionOffset;
const int GridFlagsConnectionMask = 0xFF << GridFlagsConnectionOffset;
const int GridFlagsAxisAlignedConnectionMask = 0xF << GridFlagsConnectionOffset;
const int GridFlagsEdgeNodeOffset = 10;
const int GridFlagsEdgeNodeMask = 1 << GridFlagsEdgeNodeOffset;
public override bool HasConnectionsToAllEightNeighbours {
get {
return (InternalGridFlags & GridFlagsConnectionMask) == GridFlagsConnectionMask;
}
}
public override bool HasConnectionsToAllAxisAlignedNeighbours {
get {
return (InternalGridFlags & GridFlagsAxisAlignedConnectionMask) == GridFlagsAxisAlignedConnectionMask;
}
}
/// <summary>
/// True if the node has a connection in the specified direction.
/// The dir parameter corresponds to directions in the grid as:
/// <code>
/// Z
/// |
/// |
///
/// 6 2 5
/// \ | /
/// -- 3 - X - 1 ----- X
/// / | \
/// 7 0 4
///
/// |
/// |
/// </code>
///
/// See: SetConnectionInternal
/// </summary>
public override bool HasConnectionInDirection (int dir) {
return (gridFlags >> dir & GridFlagsConnectionBit0) != 0;
}
/// <summary>
/// True if the node has a connection in the specified direction.
/// Deprecated: Use HasConnectionInDirection
/// </summary>
[System.Obsolete("Use HasConnectionInDirection")]
public bool GetConnectionInternal (int dir) {
return HasConnectionInDirection(dir);
}
/// <summary>
/// Enables or disables a connection in a specified direction on the graph.
/// See: HasConnectionInDirection
/// </summary>
public void SetConnectionInternal (int dir, bool value) {
// Set bit number #dir to 1 or 0 depending on #value
unchecked { gridFlags = (ushort)(gridFlags & ~((ushort)1 << GridFlagsConnectionOffset << dir) | (value ? (ushort)1 : (ushort)0) << GridFlagsConnectionOffset << dir); }
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
}
/// <summary>
/// Sets the state of all grid connections.
///
/// See: SetConnectionInternal
/// </summary>
/// <param name="connections">a bitmask of the connections (bit 0 is the first connection, bit 1 the second connection, etc.).</param>
public void SetAllConnectionInternal (int connections) {
unchecked { gridFlags = (ushort)((gridFlags & ~GridFlagsConnectionMask) | (connections << GridFlagsConnectionOffset)); }
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
}
/// <summary>
/// Disables all grid connections from this node.
/// Note: Other nodes might still be able to get to this node.
/// Therefore it is recommended to also disable the relevant connections on adjacent nodes.
/// </summary>
public void ResetConnectionsInternal () {
unchecked {
gridFlags = (ushort)(gridFlags & ~GridFlagsConnectionMask);
}
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
}
/// <summary>
/// Work in progress for a feature that required info about which nodes were at the border of the graph.
/// Note: This property is not functional at the moment.
/// </summary>
public bool EdgeNode {
get {
return (gridFlags & GridFlagsEdgeNodeMask) != 0;
}
set {
unchecked { gridFlags = (ushort)(gridFlags & ~GridFlagsEdgeNodeMask | (value ? GridFlagsEdgeNodeMask : 0)); }
}
}
public override GridNodeBase GetNeighbourAlongDirection (int direction) {
if (HasConnectionInDirection(direction)) {
GridGraph gg = GetGridGraph(GraphIndex);
return gg.nodes[NodeInGridIndex+gg.neighbourOffsets[direction]];
}
return null;
}
public override void ClearConnections (bool alsoReverse) {
if (alsoReverse) {
// Note: This assumes that all connections are bidirectional
// which should hold for all grid graphs unless some custom code has been added
for (int i = 0; i < 8; i++) {
var other = GetNeighbourAlongDirection(i) as GridNode;
if (other != null) {
// Remove reverse connection. See doc for GridGraph.neighbourOffsets to see which indices are used for what.
other.SetConnectionInternal(OppositeConnectionDirection(i), false);
}
}
}
ResetConnectionsInternal();
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
base.ClearConnections(alsoReverse);
#endif
}
public override void GetConnections (System.Action<GraphNode> action) {
GridGraph gg = GetGridGraph(GraphIndex);
int[] neighbourOffsets = gg.neighbourOffsets;
var nodes = gg.nodes;
for (int i = 0; i < 8; i++) {
if (HasConnectionInDirection(i)) {
var other = nodes[NodeInGridIndex + neighbourOffsets[i]];
if (other != null) action(other);
}
}
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
base.GetConnections(action);
#endif
}
public override Vector3 ClosestPointOnNode (Vector3 p) {
var gg = GetGridGraph(GraphIndex);
// Convert to graph space
p = gg.transform.InverseTransform(p);
// Calculate graph position of this node
int x = NodeInGridIndex % gg.width;
int z = NodeInGridIndex / gg.width;
// Handle the y coordinate separately
float y = gg.transform.InverseTransform((Vector3)position).y;
var closestInGraphSpace = new Vector3(Mathf.Clamp(p.x, x, x+1f), y, Mathf.Clamp(p.z, z, z+1f));
// Convert to world space
return gg.transform.Transform(closestInGraphSpace);
}
public override bool GetPortal (GraphNode other, List<Vector3> left, List<Vector3> right, bool backwards) {
if (backwards) return true;
GridGraph gg = GetGridGraph(GraphIndex);
int[] neighbourOffsets = gg.neighbourOffsets;
var nodes = gg.nodes;
for (int i = 0; i < 4; i++) {
if (HasConnectionInDirection(i) && other == nodes[NodeInGridIndex + neighbourOffsets[i]]) {
Vector3 middle = ((Vector3)(position + other.position))*0.5f;
Vector3 cross = Vector3.Cross(gg.collision.up, (Vector3)(other.position-position));
cross.Normalize();
cross *= gg.nodeSize*0.5f;
left.Add(middle - cross);
right.Add(middle + cross);
return true;
}
}
for (int i = 4; i < 8; i++) {
if (HasConnectionInDirection(i) && other == nodes[NodeInGridIndex + neighbourOffsets[i]]) {
bool rClear = false;
bool lClear = false;
if (HasConnectionInDirection(i-4)) {
var n2 = nodes[NodeInGridIndex + neighbourOffsets[i-4]];
if (n2.Walkable && n2.HasConnectionInDirection((i-4+1)%4)) {
rClear = true;
}
}
if (HasConnectionInDirection((i-4+1)%4)) {
var n2 = nodes[NodeInGridIndex + neighbourOffsets[(i-4+1)%4]];
if (n2.Walkable && n2.HasConnectionInDirection(i-4)) {
lClear = true;
}
}
Vector3 middle = ((Vector3)(position + other.position))*0.5f;
Vector3 cross = Vector3.Cross(gg.collision.up, (Vector3)(other.position-position));
cross.Normalize();
cross *= gg.nodeSize*1.4142f;
left.Add(middle - (lClear ? cross : Vector3.zero));
right.Add(middle + (rClear ? cross : Vector3.zero));
return true;
}
}
return false;
}
public override void UpdateRecursiveG (Path path, PathNode pathNode, PathHandler handler) {
GridGraph gg = GetGridGraph(GraphIndex);
int[] neighbourOffsets = gg.neighbourOffsets;
var nodes = gg.nodes;
pathNode.UpdateG(path);
handler.heap.Add(pathNode);
ushort pid = handler.PathID;
var index = NodeInGridIndex;
for (int i = 0; i < 8; i++) {
if (HasConnectionInDirection(i)) {
var other = nodes[index + neighbourOffsets[i]];
PathNode otherPN = handler.GetPathNode(other);
if (otherPN.parent == pathNode && otherPN.pathID == pid) other.UpdateRecursiveG(path, otherPN, handler);
}
}
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
base.UpdateRecursiveG(path, pathNode, handler);
#endif
}
#if ASTAR_JPS
/*
* Non Cyclic
* [0] = -Y
* [1] = +X
* [2] = +Y
* [3] = -X
* [4] = -Y+X
* [5] = +Y+X
* [6] = +Y-X
* [7] = -Y-X
*
* Cyclic
* [0] = -X
* [1] = -X+Y
* [2] = +Y
* [3] = +X+Y
* [4] = +X
* [5] = +X-Y
* [6] = -Y
* [7] = -Y-X
*/
static byte[] JPSForced = {
0xFF, // Shouldn't really happen
0,
(1<<3),
0,
0,
0,
(1<<5),
0
};
static byte[] JPSForcedDiagonal = {
0xFF, // Shouldn't really happen
(1<<2),
0,
0,
0,
0,
0,
(1<<6)
};
/// <summary>
/// Permutation of the standard grid node neighbour order to put them on a clockwise cycle around the node.
/// Enables easier math in some cases
/// </summary>
static int[] JPSCyclic = {
6,
4,
2,
0,
5,
3,
1,
7
};
/// <summary>Inverse permutation of <see cref="JPSCyclic"/></summary>
static int[] JPSInverseCyclic = {
3, // 0
6, // 1
2, // 2
5, // 3
1, // 4
4, // 5
0, // 6
7 // 7
};
const int JPSNaturalStraightNeighbours = 1 << 4;
const int JPSNaturalDiagonalNeighbours = (1 << 5) | (1 << 4) | (1 << 3);
/// <summary>Memoization of what results to return from the Jump method.</summary>
GridNodeBase[] JPSCache;
/// <summary>
/// Each byte is a bitfield where each bit indicates if direction number i should return null from the Jump method.
/// Used as a cache.
/// I would like to make this a ulong instead, but that could run into data races.
/// </summary>
byte[] JPSDead;
ushort[] JPSLastCacheID;
/// <summary>
/// Executes a straight jump search.
/// See: http://en.wikipedia.org/wiki/Jump_point_search
/// </summary>
static GridNodeBase JPSJumpStraight (GridNode node, Path path, PathHandler handler, int parentDir, int depth = 0) {
GridGraph gg = GetGridGraph(node.GraphIndex);
int[] neighbourOffsets = gg.neighbourOffsets;
var nodes = gg.nodes;
var origin = node;
// Indexing into the cache arrays from multiple threads like this should cause
// a lot of false sharing and cache trashing, but after profiling it seems
// that this is not a major concern
int threadID = handler.threadID;
int threadOffset = 8*handler.threadID;
int cyclicParentDir = JPSCyclic[parentDir];
GridNodeBase result = null;
// Rotate 180 degrees
const int forwardDir = 4;
int forwardOffset = neighbourOffsets[JPSInverseCyclic[(forwardDir + cyclicParentDir) % 8]];
// Move forwards in the same direction
// until a node is encountered which we either
// * know the result for (memoization)
// * is a special node (flag2 set)
// * has custom connections
// * the node has a forced neighbour
// Then break out of the loop
// and start another loop which goes through the same nodes and sets the
// memoization caches to avoid expensive calls in the future
while (true) {
// This is needed to make sure different threads don't overwrite each others results
// It doesn't matter if we throw away some caching done by other threads as this will only
// happen during the first few path requests
if (node.JPSLastCacheID == null || node.JPSLastCacheID.Length < handler.totalThreadCount) {
lock (node) {
// Check again in case another thread has already created the array
if (node.JPSLastCacheID == null || node.JPSLastCacheID.Length < handler.totalThreadCount) {
node.JPSCache = new GridNode[8*handler.totalThreadCount];
node.JPSDead = new byte[handler.totalThreadCount];
node.JPSLastCacheID = new ushort[handler.totalThreadCount];
}
}
}
if (node.JPSLastCacheID[threadID] != path.pathID) {
for (int i = 0; i < 8; i++) node.JPSCache[i + threadOffset] = null;
node.JPSLastCacheID[threadID] = path.pathID;
node.JPSDead[threadID] = 0;
}
// Cache earlier results, major optimization
// It is important to read from it once and then return the same result,
// if we read from it twice, we might get different results due to other threads clearing the array sometimes
var cachedResult = node.JPSCache[parentDir + threadOffset];
if (cachedResult != null) {
result = cachedResult;
break;
}
if (((node.JPSDead[threadID] >> parentDir)&1) != 0) return null;
// Special node (e.g end node), take care of
if (handler.GetPathNode(node).flag2) {
//Debug.Log ("Found end Node!");
//Debug.DrawRay ((Vector3)position, Vector3.up*2, Color.green);
result = node;
break;
}
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
// Special node which has custom connections, take care of
if (node.connections != null && node.connections.Length > 0) {
result = node;
break;
}
#endif
// These are the nodes this node is connected to, one bit for each of the 8 directions
int noncyclic = node.gridFlags;//We don't actually need to & with this because we don't use the other bits. & 0xFF;
int cyclic = 0;
for (int i = 0; i < 8; i++) cyclic |= ((noncyclic >> i)&0x1) << JPSCyclic[i];
int forced = 0;
// Loop around to be able to assume -X is where we came from
cyclic = ((cyclic >> cyclicParentDir) | ((cyclic << 8) >> cyclicParentDir)) & 0xFF;
//for ( int i = 0; i < 8; i++ ) if ( ((cyclic >> i)&1) == 0 ) forced |= JPSForced[i];
if ((cyclic & (1 << 2)) == 0) forced |= (1<<3);
if ((cyclic & (1 << 6)) == 0) forced |= (1<<5);
int natural = JPSNaturalStraightNeighbours;
// Check if there are any forced neighbours which we can reach that are not natural neighbours
//if ( ((forced & cyclic) & (~(natural & cyclic))) != 0 ) {
if ((forced & (~natural) & cyclic) != 0) {
// Some of the neighbour nodes are forced
result = node;
break;
}
// Make sure we can reach the next node
if ((cyclic & (1 << forwardDir)) != 0) {
node = nodes[node.nodeInGridIndex + forwardOffset] as GridNode;
//Debug.DrawLine ( (Vector3)position + Vector3.up*0.2f*(depth), (Vector3)other.position + Vector3.up*0.2f*(depth+1), Color.magenta);
} else {
result = null;
break;
}
}
if (result == null) {
while (origin != node) {
origin.JPSDead[threadID] |= (byte)(1 << parentDir);
origin = nodes[origin.nodeInGridIndex + forwardOffset] as GridNode;
}
} else {
while (origin != node) {
origin.JPSCache[parentDir + threadOffset] = result;
origin = nodes[origin.nodeInGridIndex + forwardOffset] as GridNode;
}
}
return result;
}
/// <summary>
/// Executes a diagonal jump search.
/// See: http://en.wikipedia.org/wiki/Jump_point_search
/// </summary>
GridNodeBase JPSJumpDiagonal (Path path, PathHandler handler, int parentDir, int depth = 0) {
// Indexing into the cache arrays from multiple threads like this should cause
// a lot of false sharing and cache trashing, but after profiling it seems
// that this is not a major concern
int threadID = handler.threadID;
int threadOffset = 8*handler.threadID;
// This is needed to make sure different threads don't overwrite each others results
// It doesn't matter if we throw away some caching done by other threads as this will only
// happen during the first few path requests
if (JPSLastCacheID == null || JPSLastCacheID.Length < handler.totalThreadCount) {
lock (this) {
// Check again in case another thread has already created the array
if (JPSLastCacheID == null || JPSLastCacheID.Length < handler.totalThreadCount) {
JPSCache = new GridNode[8*handler.totalThreadCount];
JPSDead = new byte[handler.totalThreadCount];
JPSLastCacheID = new ushort[handler.totalThreadCount];
}
}
}
if (JPSLastCacheID[threadID] != path.pathID) {
for (int i = 0; i < 8; i++) JPSCache[i + threadOffset] = null;
JPSLastCacheID[threadID] = path.pathID;
JPSDead[threadID] = 0;
}
// Cache earlier results, major optimization
// It is important to read from it once and then return the same result,
// if we read from it twice, we might get different results due to other threads clearing the array sometimes
var cachedResult = JPSCache[parentDir + threadOffset];
if (cachedResult != null) {
//return cachedResult;
}
//if ( ((JPSDead[threadID] >> parentDir)&1) != 0 ) return null;
// Special node (e.g end node), take care of
if (handler.GetPathNode(this).flag2) {
//Debug.Log ("Found end Node!");
//Debug.DrawRay ((Vector3)position, Vector3.up*2, Color.green);
JPSCache[parentDir + threadOffset] = this;
return this;
}
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
// Special node which has custom connections, take care of
if (connections != null && connections.Length > 0) {
JPSCache[parentDir] = this;
return this;
}
#endif
int noncyclic = gridFlags;//We don't actually need to & with this because we don't use the other bits. & 0xFF;
int cyclic = 0;
for (int i = 0; i < 8; i++) cyclic |= ((noncyclic >> i)&0x1) << JPSCyclic[i];
int forced = 0;
int cyclicParentDir = JPSCyclic[parentDir];
// Loop around to be able to assume -X is where we came from
cyclic = ((cyclic >> cyclicParentDir) | ((cyclic << 8) >> cyclicParentDir)) & 0xFF;
int natural;
for (int i = 0; i < 8; i++) if (((cyclic >> i)&1) == 0) forced |= JPSForcedDiagonal[i];
natural = JPSNaturalDiagonalNeighbours;
/*
* if ( ((Vector3)position - new Vector3(1.5f,0,-1.5f)).magnitude < 0.5f ) {
* Debug.Log (noncyclic + " " + parentDir + " " + cyclicParentDir);
* Debug.Log (System.Convert.ToString (cyclic, 2)+"\n"+System.Convert.ToString (noncyclic, 2)+"\n"+System.Convert.ToString (natural, 2)+"\n"+System.Convert.ToString (forced, 2));
* }*/
// Don't force nodes we cannot reach anyway
forced &= cyclic;
natural &= cyclic;
if ((forced & (~natural)) != 0) {
// Some of the neighbour nodes are forced
JPSCache[parentDir+threadOffset] = this;
return this;
}
int forwardDir;
GridGraph gg = GetGridGraph(GraphIndex);
int[] neighbourOffsets = gg.neighbourOffsets;
var nodes = gg.nodes;
{
// Rotate 180 degrees - 1 node
forwardDir = 3;
if (((cyclic >> forwardDir)&1) != 0) {
int oi = JPSInverseCyclic[(forwardDir + cyclicParentDir) % 8];
var other = nodes[nodeInGridIndex + neighbourOffsets[oi]];
//Debug.DrawLine ( (Vector3)position + Vector3.up*0.2f*(depth), (Vector3)other.position + Vector3.up*0.2f*(depth+1), Color.black);
GridNodeBase v;
if (oi < 4) {
v = JPSJumpStraight(other as GridNode, path, handler, JPSInverseCyclic[(cyclicParentDir-1+8)%8], depth+1);
} else {
v = (other as GridNode).JPSJumpDiagonal(path, handler, JPSInverseCyclic[(cyclicParentDir-1+8)%8], depth+1);
}
if (v != null) {
JPSCache[parentDir+threadOffset] = this;
return this;
}
}
// Rotate 180 degrees + 1 node
forwardDir = 5;
if (((cyclic >> forwardDir)&1) != 0) {
int oi = JPSInverseCyclic[(forwardDir + cyclicParentDir) % 8];
var other = nodes[nodeInGridIndex + neighbourOffsets[oi]];
//Debug.DrawLine ( (Vector3)position + Vector3.up*0.2f*(depth), (Vector3)other.position + Vector3.up*0.2f*(depth+1), Color.grey);
GridNodeBase v;
if (oi < 4) {
v = JPSJumpStraight(other as GridNode, path, handler, JPSInverseCyclic[(cyclicParentDir+1+8)%8], depth+1);
} else {
v = (other as GridNode).JPSJumpDiagonal(path, handler, JPSInverseCyclic[(cyclicParentDir+1+8)%8], depth+1);
}
if (v != null) {
JPSCache[parentDir+threadOffset] = this;
return this;
}
}
}
// Rotate 180 degrees
forwardDir = 4;
if (((cyclic >> forwardDir)&1) != 0) {
int oi = JPSInverseCyclic[(forwardDir + cyclicParentDir) % 8];
var other = nodes[nodeInGridIndex + neighbourOffsets[oi]] as GridNode;
//Debug.DrawLine ( (Vector3)position + Vector3.up*0.2f*(depth), (Vector3)other.position + Vector3.up*0.2f*(depth+1), Color.magenta);
var v = other.JPSJumpDiagonal(path, handler, parentDir, depth+1);
if (v != null) {
JPSCache[parentDir+threadOffset] = v;
return v;
}
}
JPSDead[threadID] |= (byte)(1 << parentDir);
return null;
}
/// <summary>
/// Opens a node using Jump Point Search.
/// See: http://en.wikipedia.org/wiki/Jump_point_search
/// </summary>
public void JPSOpen (Path path, PathNode pathNode, PathHandler handler) {
GridGraph gg = GetGridGraph(GraphIndex);
int[] neighbourOffsets = gg.neighbourOffsets;
var nodes = gg.nodes;
ushort pid = handler.PathID;
int noncyclic = gridFlags & 0xFF;
int cyclic = 0;
for (int i = 0; i < 8; i++) cyclic |= ((noncyclic >> i)&0x1) << JPSCyclic[i];
var parent = pathNode.parent != null ? pathNode.parent.node as GridNode : null;
int parentDir = -1;
if (parent != null) {
int diff = parent != null ? parent.nodeInGridIndex - nodeInGridIndex : 0;
int x2 = nodeInGridIndex % gg.width;
int x1 = parent.nodeInGridIndex % gg.width;
if (diff < 0) {
if (x1 == x2) {
parentDir = 0;
} else if (x1 < x2) {
parentDir = 7;
} else {
parentDir = 4;
}
} else {
if (x1 == x2) {
parentDir = 1;
} else if (x1 < x2) {
parentDir = 6;
} else {
parentDir = 5;
}
}
}
int cyclicParentDir = 0;
// Check for -1
int forced = 0;
if (parentDir != -1) {
cyclicParentDir = JPSCyclic[parentDir];
// Loop around to be able to assume -X is where we came from
cyclic = ((cyclic >> cyclicParentDir) | ((cyclic << 8) >> cyclicParentDir)) & 0xFF;
} else {
forced = 0xFF;
//parentDir = 0;
}
bool diagonal = parentDir >= 4;
int natural;
if (diagonal) {
for (int i = 0; i < 8; i++) if (((cyclic >> i)&1) == 0) forced |= JPSForcedDiagonal[i];
natural = JPSNaturalDiagonalNeighbours;
} else {
for (int i = 0; i < 8; i++) if (((cyclic >> i)&1) == 0) forced |= JPSForced[i];
natural = JPSNaturalStraightNeighbours;
}
// Don't force nodes we cannot reach anyway
forced &= cyclic;
natural &= cyclic;
int nb = forced | natural;
/*if ( ((Vector3)position - new Vector3(0.5f,0,3.5f)).magnitude < 0.5f ) {
* Debug.Log (noncyclic + " " + parentDir + " " + cyclicParentDir);
* Debug.Log (System.Convert.ToString (cyclic, 2)+"\n"+System.Convert.ToString (noncyclic, 2)+"\n"+System.Convert.ToString (natural, 2)+"\n"+System.Convert.ToString (forced, 2));
* }*/
for (int i = 0; i < 8; i++) {
if (((nb >> i)&1) != 0) {
int oi = JPSInverseCyclic[(i + cyclicParentDir) % 8];
var other = nodes[nodeInGridIndex + neighbourOffsets[oi]];
#if ASTARDEBUG
if (((forced >> i)&1) != 0) {
Debug.DrawLine((Vector3)position, Vector3.Lerp((Vector3)other.position, (Vector3)position, 0.6f), Color.red);
}
if (((natural >> i)&1) != 0) {
Debug.DrawLine((Vector3)position + Vector3.up*0.2f, Vector3.Lerp((Vector3)other.position, (Vector3)position, 0.6f) + Vector3.up*0.2f, Color.green);
}
#endif
if (oi < 4) {
other = JPSJumpStraight(other as GridNode, path, handler, JPSInverseCyclic[(i + 4 + cyclicParentDir) % 8]);
} else {
other = (other as GridNode).JPSJumpDiagonal(path, handler, JPSInverseCyclic[(i + 4 + cyclicParentDir) % 8]);
}
if (other != null) {
//Debug.DrawLine ( (Vector3)position + Vector3.up*0.0f, (Vector3)other.position + Vector3.up*0.3f, Color.cyan);
//Debug.DrawRay ( (Vector3)other.position, Vector3.up, Color.cyan);
//GridNode other = nodes[nodeInGridIndex + neighbourOffsets[i]];
//if (!path.CanTraverse (other)) continue;
PathNode otherPN = handler.GetPathNode(other);
if (otherPN.pathID != pid) {
otherPN.parent = pathNode;
otherPN.pathID = pid;
otherPN.cost = (uint)(other.position - position).costMagnitude;//neighbourCosts[i];
otherPN.H = path.CalculateHScore(other);
otherPN.UpdateG(path);
//Debug.Log ("G " + otherPN.G + " F " + otherPN.F);
handler.heap.Add(otherPN);
//Debug.DrawRay ((Vector3)otherPN.node.Position, Vector3.up,Color.blue);
} else {
//If not we can test if the path from the current node to this one is a better one then the one already used
uint tmpCost = (uint)(other.position - position).costMagnitude;//neighbourCosts[i];
if (pathNode.G+tmpCost+path.GetTraversalCost(other) < otherPN.G) {
//Debug.Log ("Path better from " + NodeIndex + " to " + otherPN.node.NodeIndex + " " + (pathNode.G+tmpCost+path.GetTraversalCost(other)) + " < " + otherPN.G);
otherPN.cost = tmpCost;
otherPN.parent = pathNode;
other.UpdateRecursiveG(path, otherPN, handler);
}
}
}
}
#if ASTARDEBUG
if (i == 0 && parentDir != -1 && this.nodeInGridIndex > 10) {
int oi = JPSInverseCyclic[(i + cyclicParentDir) % 8];
if (nodeInGridIndex + neighbourOffsets[oi] < 0 || nodeInGridIndex + neighbourOffsets[oi] >= nodes.Length) {
//Debug.LogError ("ERR: " + (nodeInGridIndex + neighbourOffsets[oi]) + " " + cyclicParentDir + " " + parentDir + " Reverted " + oi);
//Debug.DrawRay ((Vector3)position, Vector3.up, Color.red);
} else {
var other = nodes[nodeInGridIndex + neighbourOffsets[oi]];
Debug.DrawLine((Vector3)position - Vector3.up*0.2f, Vector3.Lerp((Vector3)other.position, (Vector3)position, 0.6f) - Vector3.up*0.2f, Color.blue);
}
}
#endif
}
}
#endif
public override void Open (Path path, PathNode pathNode, PathHandler handler) {
GridGraph gg = GetGridGraph(GraphIndex);
ushort pid = handler.PathID;
#if ASTAR_JPS
if (gg.useJumpPointSearch && !path.FloodingPath) {
JPSOpen(path, pathNode, handler);
} else
#endif
{
int[] neighbourOffsets = gg.neighbourOffsets;
uint[] neighbourCosts = gg.neighbourCosts;
GridNodeBase[] nodes = gg.nodes;
var index = NodeInGridIndex;
for (int i = 0; i < 8; i++) {
if (HasConnectionInDirection(i)) {
GridNodeBase other = nodes[index + neighbourOffsets[i]];
if (!path.CanTraverse(other)) continue;
PathNode otherPN = handler.GetPathNode(other);
uint tmpCost = neighbourCosts[i];
// Check if the other node has not yet been visited by this path
if (otherPN.pathID != pid) {
otherPN.parent = pathNode;
otherPN.pathID = pid;
otherPN.cost = tmpCost;
otherPN.H = path.CalculateHScore(other);
otherPN.UpdateG(path);
handler.heap.Add(otherPN);
} else {
// Sorry for the huge number of #ifs
//If not we can test if the path from the current node to this one is a better one then the one already used
#if ASTAR_NO_TRAVERSAL_COST
if (pathNode.G+tmpCost < otherPN.G)
#else
if (pathNode.G+tmpCost+path.GetTraversalCost(other) < otherPN.G)
#endif
{
//Debug.Log ("Path better from " + NodeIndex + " to " + otherPN.node.NodeIndex + " " + (pathNode.G+tmpCost+path.GetTraversalCost(other)) + " < " + otherPN.G);
otherPN.cost = tmpCost;
otherPN.parent = pathNode;
other.UpdateRecursiveG(path, otherPN, handler);
}
}
}
}
}
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
base.Open(path, pathNode, handler);
#endif
}
public override void SerializeNode (GraphSerializationContext ctx) {
base.SerializeNode(ctx);
ctx.SerializeInt3(position);
ctx.writer.Write(gridFlags);
}
public override void DeserializeNode (GraphSerializationContext ctx) {
base.DeserializeNode(ctx);
position = ctx.DeserializeInt3();
gridFlags = ctx.reader.ReadUInt16();
}
public override void AddConnection (GraphNode node, uint cost) {
// In case the node was already added as an internal grid connection,
// we need to remove that connection before we insert it as a custom connection.
// Using a custom connection is necessary because it has a custom cost.
if (node is GridNode gn && gn.GraphIndex == GraphIndex) {
RemoveGridConnection(gn);
}
base.AddConnection(node, cost);
}
public override void RemoveConnection (GraphNode node) {
base.RemoveConnection(node);
// If the node is a grid node on the same graph, it might be added as an internal connection and not a custom one.
if (node is GridNode gn && gn.GraphIndex == GraphIndex) {
RemoveGridConnection(gn);
}
}
/// <summary>
/// Removes a connection from the internal grid connections.
/// See: SetConnectionInternal
/// </summary>
protected void RemoveGridConnection (GridNode node) {
var nodeIndex = NodeInGridIndex;
var gg = GetGridGraph(GraphIndex);
for (int i = 0; i < 8; i++) {
if (nodeIndex + gg.neighbourOffsets[i] == node.NodeInGridIndex && GetNeighbourAlongDirection(i) == node) {
SetConnectionInternal(i, false);
break;
}
}
}
#else
public override void AddConnection (GraphNode node, uint cost) {
throw new System.NotImplementedException();
}
public override void ClearConnections (bool alsoReverse) {
throw new System.NotImplementedException();
}
public override void GetConnections (GraphNodeDelegate del) {
throw new System.NotImplementedException();
}
public override void Open (Path path, PathNode pathNode, PathHandler handler) {
throw new System.NotImplementedException();
}
public override void RemoveConnection (GraphNode node) {
throw new System.NotImplementedException();
}
#endif
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5291f1ec332d746138ac025aecb1e12d
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,498 @@
using UnityEngine;
using Pathfinding.Serialization;
namespace Pathfinding {
/// <summary>Base class for GridNode and LevelGridNode</summary>
public abstract class GridNodeBase : GraphNode {
protected GridNodeBase (AstarPath astar) : base(astar) {
}
const int GridFlagsWalkableErosionOffset = 8;
const int GridFlagsWalkableErosionMask = 1 << GridFlagsWalkableErosionOffset;
const int GridFlagsWalkableTmpOffset = 9;
const int GridFlagsWalkableTmpMask = 1 << GridFlagsWalkableTmpOffset;
protected const int NodeInGridIndexLayerOffset = 24;
protected const int NodeInGridIndexMask = 0xFFFFFF;
/// <summary>
/// Bitfield containing the x and z coordinates of the node as well as the layer (for layered grid graphs).
/// See: NodeInGridIndex
/// </summary>
protected int nodeInGridIndex;
protected ushort gridFlags;
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
/// <summary>
/// Custon non-grid connections from this node.
/// See: <see cref="AddConnection"/>
/// See: <see cref="RemoveConnection"/>
///
/// This field is removed if the ASTAR_GRID_NO_CUSTOM_CONNECTIONS compiler directive is used.
/// Removing it can save a tiny bit of memory. You can enable the define in the Optimizations tab in the A* inspector.
/// See: compiler-directives (view in online documentation for working links)
///
/// Note: If you modify this array or the contents of it you must call <see cref="SetConnectivityDirty"/>.
/// </summary>
public Connection[] connections;
#endif
/// <summary>
/// The index of the node in the grid.
/// This is x + z*graph.width
/// So you can get the X and Z indices using
/// <code>
/// int index = node.NodeInGridIndex;
/// int x = index % graph.width;
/// int z = index / graph.width;
/// // where graph is GridNode.GetGridGraph (node.graphIndex), i.e the graph the nodes are contained in.
/// </code>
/// </summary>
public int NodeInGridIndex { get { return nodeInGridIndex & NodeInGridIndexMask; } set { nodeInGridIndex = (nodeInGridIndex & ~NodeInGridIndexMask) | value; } }
/// <summary>
/// X coordinate of the node in the grid.
/// The node in the bottom left corner has (x,z) = (0,0) and the one in the opposite
/// corner has (x,z) = (width-1, depth-1)
/// See: ZCoordInGrid
/// See: NodeInGridIndex
/// </summary>
public int XCoordinateInGrid {
get {
return NodeInGridIndex % GridNode.GetGridGraph(GraphIndex).width;
}
}
/// <summary>
/// Z coordinate of the node in the grid.
/// The node in the bottom left corner has (x,z) = (0,0) and the one in the opposite
/// corner has (x,z) = (width-1, depth-1)
/// See: XCoordInGrid
/// See: NodeInGridIndex
/// </summary>
public int ZCoordinateInGrid {
get {
return NodeInGridIndex / GridNode.GetGridGraph(GraphIndex).width;
}
}
/// <summary>
/// Stores walkability before erosion is applied.
/// Used internally when updating the graph.
/// </summary>
public bool WalkableErosion {
get {
return (gridFlags & GridFlagsWalkableErosionMask) != 0;
}
set {
unchecked { gridFlags = (ushort)(gridFlags & ~GridFlagsWalkableErosionMask | (value ? (ushort)GridFlagsWalkableErosionMask : (ushort)0)); }
}
}
/// <summary>Temporary variable used internally when updating the graph.</summary>
public bool TmpWalkable {
get {
return (gridFlags & GridFlagsWalkableTmpMask) != 0;
}
set {
unchecked { gridFlags = (ushort)(gridFlags & ~GridFlagsWalkableTmpMask | (value ? (ushort)GridFlagsWalkableTmpMask : (ushort)0)); }
}
}
/// <summary>
/// True if the node has grid connections to all its 8 neighbours.
/// Note: This will always return false if GridGraph.neighbours is set to anything other than Eight.
/// See: GetNeighbourAlongDirection
/// See: <see cref="HasConnectionsToAllAxisAlignedNeighbours"/>
/// </summary>
public abstract bool HasConnectionsToAllEightNeighbours { get; }
/// <summary>
/// True if the node has grid connections to all its 4 axis-aligned neighbours.
/// See: GetNeighbourAlongDirection
/// See: <see cref="HasConnectionsToAllEightNeighbours"/>
/// </summary>
public abstract bool HasConnectionsToAllAxisAlignedNeighbours { get; }
/// <summary>
/// The connection oppositie the given one.
///
/// <code>
/// Z
/// |
/// |
///
/// 6 2 5
/// \ | /
/// -- 3 - X - 1 ----- X
/// / | \
/// 7 0 4
///
/// |
/// |
/// </code>
///
/// For example, dir=1 outputs 3, dir=6 outputs 4 and so on.
///
/// See: <see cref="HasConnectionInDirection"/>
/// </summary>
public static int OppositeConnectionDirection (int dir) {
return dir < 4 ? ((dir + 2) % 4) : (((dir-2) % 4) + 4);
}
/// <summary>
/// Checks if point is inside the node when seen from above.
///
/// The borders of a node are considered to be inside the node.
///
/// Note that <see cref="ContainsPointInGraphSpace"/> is faster than this method as it avoids
/// some coordinate transformations. If you are repeatedly calling this method
/// on many different nodes but with the same point then you should consider
/// transforming the point first and then calling ContainsPointInGraphSpace.
/// <code>
/// Int3 p = (Int3)graph.transform.InverseTransform(point);
///
/// node.ContainsPointInGraphSpace(p);
/// </code>
/// </summary>
public override bool ContainsPoint (Vector3 point) {
var gg = Graph as GridGraph;
// Convert to graph space
return ContainsPointInGraphSpace((Int3)gg.transform.InverseTransform(point));
}
/// <summary>
/// Checks if point is inside the node in graph space.
///
/// The borders of a node are considered to be inside the node.
///
/// The y coordinate of the point is ignored.
/// </summary>
public override bool ContainsPointInGraphSpace (Int3 point) {
// Calculate graph position of this node
var x = XCoordinateInGrid*Int3.Precision;
var z = ZCoordinateInGrid*Int3.Precision;
return point.x >= x && point.x <= x + Int3.Precision && point.z >= z && point.z <= z + Int3.Precision;
}
public override float SurfaceArea () {
GridGraph gg = GridNode.GetGridGraph(GraphIndex);
return gg.nodeSize*gg.nodeSize;
}
public override Vector3 RandomPointOnSurface () {
GridGraph gg = GridNode.GetGridGraph(GraphIndex);
var graphSpacePosition = gg.transform.InverseTransform((Vector3)position);
return gg.transform.Transform(graphSpacePosition + new Vector3(Random.value - 0.5f, 0, Random.value - 0.5f));
}
/// <summary>
/// Transforms a world space point to a normalized point on this node's surface.
/// (0.5,0.5) represents the node's center. (0,0), (1,0), (1,1) and (0,1) each represent the corners of the node.
///
/// See: <see cref="UnNormalizePoint"/>
/// </summary>
public Vector2 NormalizePoint (Vector3 worldPoint) {
GridGraph gg = GridNode.GetGridGraph(GraphIndex);
var graphSpacePosition = gg.transform.InverseTransform(worldPoint);
return new Vector2(graphSpacePosition.x - this.XCoordinateInGrid, graphSpacePosition.z - this.ZCoordinateInGrid);
}
/// <summary>
/// Transforms a normalized point on this node's surface to a world space point.
/// (0.5,0.5) represents the node's center. (0,0), (1,0), (1,1) and (0,1) each represent the corners of the node.
///
/// See: <see cref="NormalizePoint"/>
/// </summary>
public Vector3 UnNormalizePoint (Vector2 normalizedPointOnSurface) {
GridGraph gg = GridNode.GetGridGraph(GraphIndex);
return (Vector3)this.position + gg.transform.TransformVector(new Vector3(normalizedPointOnSurface.x - 0.5f, 0, normalizedPointOnSurface.y - 0.5f));
}
public override int GetGizmoHashCode () {
var hash = base.GetGizmoHashCode();
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
if (connections != null) {
for (int i = 0; i < connections.Length; i++) {
hash ^= 17 * connections[i].GetHashCode();
}
}
#endif
hash ^= 109 * gridFlags;
return hash;
}
/// <summary>
/// Adjacent grid node in the specified direction.
/// This will return null if the node does not have a connection to a node
/// in that direction.
///
/// The dir parameter corresponds to directions in the grid as:
/// <code>
/// Z
/// |
/// |
///
/// 6 2 5
/// \ | /
/// -- 3 - X - 1 ----- X
/// / | \
/// 7 0 4
///
/// |
/// |
/// </code>
///
/// See: GetConnections
///
/// Note: This method only takes grid connections into account, not custom connections (i.e. those added using <see cref="AddConnection"/> or using node links).
/// </summary>
public abstract GridNodeBase GetNeighbourAlongDirection(int direction);
/// <summary>
/// True if the node has a connection to an adjecent node in the specified direction.
///
/// The dir parameter corresponds to directions in the grid as:
/// <code>
/// Z
/// |
/// |
///
/// 6 2 5
/// \ | /
/// -- 3 - X - 1 ----- X
/// / | \
/// 7 0 4
///
/// |
/// |
/// </code>
///
/// See: <see cref="GetConnections"/>
/// See: <see cref="GetNeighbourAlongDirection"/>
///
/// Note: This method only takes grid connections into account, not custom connections (i.e. those added using <see cref="AddConnection"/> or using node links).
/// </summary>
public virtual bool HasConnectionInDirection (int direction) {
// TODO: Can be optimized if overriden in each subclass
return GetNeighbourAlongDirection(direction) != null;
}
public override bool ContainsConnection (GraphNode node) {
#if !ASTAR_GRID_NO_CUSTOM_CONNECTIONS
if (connections != null) {
for (int i = 0; i < connections.Length; i++) {
if (connections[i].node == node) {
return true;
}
}
}
#endif
for (int i = 0; i < 8; i++) {
if (node == GetNeighbourAlongDirection(i)) {
return true;
}
}
return false;
}
#if ASTAR_GRID_NO_CUSTOM_CONNECTIONS
public override void AddConnection (GraphNode node, uint cost) {
throw new System.NotImplementedException("GridNodes do not have support for adding manual connections with your current settings."+
"\nPlease disable ASTAR_GRID_NO_CUSTOM_CONNECTIONS in the Optimizations tab in the A* Inspector");
}
public override void RemoveConnection (GraphNode node) {
throw new System.NotImplementedException("GridNodes do not have support for adding manual connections with your current settings."+
"\nPlease disable ASTAR_GRID_NO_CUSTOM_CONNECTIONS in the Optimizations tab in the A* Inspector");
}
public void ClearCustomConnections (bool alsoReverse) {
}
#else
/// <summary>Same as <see cref="ClearConnections"/>, but does not clear grid connections, only custom ones (e.g added by <see cref="AddConnection"/> or a NodeLink component)</summary>
public void ClearCustomConnections (bool alsoReverse) {
if (connections != null) for (int i = 0; i < connections.Length; i++) connections[i].node.RemoveConnection(this);
connections = null;
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
}
public override void ClearConnections (bool alsoReverse) {
ClearCustomConnections(alsoReverse);
}
public override void GetConnections (System.Action<GraphNode> action) {
if (connections != null) for (int i = 0; i < connections.Length; i++) action(connections[i].node);
}
public override void UpdateRecursiveG (Path path, PathNode pathNode, PathHandler handler) {
ushort pid = handler.PathID;
if (connections != null) for (int i = 0; i < connections.Length; i++) {
GraphNode other = connections[i].node;
PathNode otherPN = handler.GetPathNode(other);
if (otherPN.parent == pathNode && otherPN.pathID == pid) other.UpdateRecursiveG(path, otherPN, handler);
}
}
public override void Open (Path path, PathNode pathNode, PathHandler handler) {
ushort pid = handler.PathID;
if (connections != null) for (int i = 0; i < connections.Length; i++) {
GraphNode other = connections[i].node;
if (!path.CanTraverse(other)) continue;
PathNode otherPN = handler.GetPathNode(other);
uint tmpCost = connections[i].cost;
if (otherPN.pathID != pid) {
otherPN.parent = pathNode;
otherPN.pathID = pid;
otherPN.cost = tmpCost;
otherPN.H = path.CalculateHScore(other);
otherPN.UpdateG(path);
//Debug.Log ("G " + otherPN.G + " F " + otherPN.F);
handler.heap.Add(otherPN);
//Debug.DrawRay ((Vector3)otherPN.node.Position, Vector3.up,Color.blue);
} else {
// Sorry for the huge number of #ifs
//If not we can test if the path from the current node to this one is a better one then the one already used
#if ASTAR_NO_TRAVERSAL_COST
if (pathNode.G+tmpCost < otherPN.G)
#else
if (pathNode.G+tmpCost+path.GetTraversalCost(other) < otherPN.G)
#endif
{
//Debug.Log ("Path better from " + NodeIndex + " to " + otherPN.node.NodeIndex + " " + (pathNode.G+tmpCost+path.GetTraversalCost(other)) + " < " + otherPN.G);
otherPN.cost = tmpCost;
otherPN.parent = pathNode;
other.UpdateRecursiveG(path, otherPN, handler);
}
}
}
}
/// <summary>
/// Add a connection from this node to the specified node.
/// If the connection already exists, the cost will simply be updated and
/// no extra connection added.
///
/// Note: Only adds a one-way connection. Consider calling the same function on the other node
/// to get a two-way connection.
///
/// Note that this will always add a custom connection which is a bit slower than the internal connection
/// list to adjacent grid nodes. If you only want to modify connections between grid nodes that are
/// adjacent to each other, and don't want custom costs, then using <see cref="SetConnectionInternal"/> might be
/// a better option.
/// </summary>
public override void AddConnection (GraphNode node, uint cost) {
if (node == null) throw new System.ArgumentNullException();
if (connections != null) {
for (int i = 0; i < connections.Length; i++) {
if (connections[i].node == node) {
connections[i].cost = cost;
return;
}
}
}
int connLength = connections != null ? connections.Length : 0;
var newconns = new Connection[connLength+1];
for (int i = 0; i < connLength; i++) {
newconns[i] = connections[i];
}
newconns[connLength] = new Connection(node, cost);
connections = newconns;
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
}
/// <summary>
/// Removes any connection from this node to the specified node.
/// If no such connection exists, nothing will be done.
///
/// Note: This only removes the connection from this node to the other node.
/// You may want to call the same function on the other node to remove its eventual connection
/// to this node.
///
/// Version: Before 4.3.48 This method only handled custom connections (those added using link components or the AddConnection method).
/// Regular grid connections had to be added or removed using <see cref="Pathfinding.GridNode.SetConnectionInternal"/>. Starting with 4.3.48 this method
/// can remove all types of connections.
/// </summary>
public override void RemoveConnection (GraphNode node) {
if (connections == null) return;
for (int i = 0; i < connections.Length; i++) {
if (connections[i].node == node) {
int connLength = connections.Length;
var newconns = new Connection[connLength-1];
for (int j = 0; j < i; j++) {
newconns[j] = connections[j];
}
for (int j = i+1; j < connLength; j++) {
newconns[j-1] = connections[j];
}
connections = newconns;
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
return;
}
}
}
public override void SerializeReferences (GraphSerializationContext ctx) {
// TODO: Deduplicate code
if (connections == null) {
ctx.writer.Write(-1);
} else {
ctx.writer.Write(connections.Length);
for (int i = 0; i < connections.Length; i++) {
ctx.SerializeNodeReference(connections[i].node);
ctx.writer.Write(connections[i].cost);
}
}
}
public override void DeserializeReferences (GraphSerializationContext ctx) {
// Grid nodes didn't serialize references before 3.8.3
if (ctx.meta.version < AstarSerializer.V3_8_3)
return;
int count = ctx.reader.ReadInt32();
if (count == -1) {
connections = null;
} else {
connections = new Connection[count];
for (int i = 0; i < count; i++) {
connections[i] = new Connection(ctx.DeserializeNodeReference(), ctx.reader.ReadUInt32());
}
}
}
#endif
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 1b723ddfaa37b46eca23cd8d042ad3e9
timeCreated: 1459629300
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,290 @@
using UnityEngine;
using Pathfinding.Serialization;
namespace Pathfinding {
/// <summary>
/// Node used for the PointGraph.
/// This is just a simple point with a list of connections (and associated costs) to other nodes.
/// It does not have any concept of a surface like many other node types.
///
/// See: PointGraph
/// </summary>
public class PointNode : GraphNode {
/// <summary>
/// All connections from this node.
/// See: <see cref="AddConnection"/>
/// See: <see cref="RemoveConnection"/>
///
/// Note: If you modify this array or the contents of it you must call <see cref="SetConnectivityDirty"/>.
///
/// Note: If you modify this array or the contents of it you must call <see cref="PointGraph.RegisterConnectionLength"/> with the length of the new connections.
/// </summary>
public Connection[] connections;
/// <summary>
/// GameObject this node was created from (if any).
/// Warning: When loading a graph from a saved file or from cache, this field will be null.
///
/// <code>
/// var node = AstarPath.active.GetNearest(transform.position).node;
/// var pointNode = node as PointNode;
///
/// if (pointNode != null) {
/// Debug.Log("That node was created from the GameObject named " + pointNode.gameObject.name);
/// } else {
/// Debug.Log("That node is not a PointNode");
/// }
/// </code>
/// </summary>
public GameObject gameObject;
public void SetPosition (Int3 value) {
position = value;
}
public PointNode (AstarPath astar) : base(astar) {
}
/// <summary>
/// Closest point on the surface of this node to the point p.
///
/// For a point node this is always the node's <see cref="position"/> sicne it has no surface.
/// </summary>
public override Vector3 ClosestPointOnNode (Vector3 p) {
return (Vector3)this.position;
}
/// <summary>
/// Checks if point is inside the node when seen from above.
///
/// Since point nodes have no surface area, this method always returns false.
/// </summary>
public override bool ContainsPoint (Vector3 point) {
return false;
}
/// <summary>
/// Checks if point is inside the node in graph space.
///
/// Since point nodes have no surface area, this method always returns false.
/// </summary>
public override bool ContainsPointInGraphSpace (Int3 point) {
return false;
}
public override void GetConnections (System.Action<GraphNode> action) {
if (connections == null) return;
for (int i = 0; i < connections.Length; i++) action(connections[i].node);
}
public override void ClearConnections (bool alsoReverse) {
if (alsoReverse && connections != null) {
for (int i = 0; i < connections.Length; i++) {
connections[i].node.RemoveConnection(this);
}
}
connections = null;
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
}
public override void UpdateRecursiveG (Path path, PathNode pathNode, PathHandler handler) {
pathNode.UpdateG(path);
handler.heap.Add(pathNode);
for (int i = 0; i < connections.Length; i++) {
GraphNode other = connections[i].node;
PathNode otherPN = handler.GetPathNode(other);
if (otherPN.parent == pathNode && otherPN.pathID == handler.PathID) {
other.UpdateRecursiveG(path, otherPN, handler);
}
}
}
public override bool ContainsConnection (GraphNode node) {
if (connections == null) return false;
for (int i = 0; i < connections.Length; i++) if (connections[i].node == node) return true;
return false;
}
/// <summary>
/// Add a connection from this node to the specified node.
/// If the connection already exists, the cost will simply be updated and
/// no extra connection added.
///
/// Note: Only adds a one-way connection. Consider calling the same function on the other node
/// to get a two-way connection.
///
/// <code>
/// AstarPath.active.AddWorkItem(new AstarWorkItem(ctx => {
/// // Connect two nodes
/// var node1 = AstarPath.active.GetNearest(transform.position, NNConstraint.None).node;
/// var node2 = AstarPath.active.GetNearest(transform.position + Vector3.right, NNConstraint.None).node;
/// var cost = (uint)(node2.position - node1.position).costMagnitude;
/// node1.AddConnection(node2, cost);
/// node2.AddConnection(node1, cost);
///
/// node1.ContainsConnection(node2); // True
///
/// node1.RemoveConnection(node2);
/// node2.RemoveConnection(node1);
/// }));
/// </code>
/// </summary>
public override void AddConnection (GraphNode node, uint cost) {
if (node == null) throw new System.ArgumentNullException();
if (connections != null) {
for (int i = 0; i < connections.Length; i++) {
if (connections[i].node == node) {
connections[i].cost = cost;
return;
}
}
}
int connLength = connections != null ? connections.Length : 0;
var newconns = new Connection[connLength+1];
for (int i = 0; i < connLength; i++) {
newconns[i] = connections[i];
}
newconns[connLength] = new Connection(node, cost);
connections = newconns;
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
// Make sure the graph knows that there exists a connection with this length
(this.Graph as PointGraph).RegisterConnectionLength((node.position - position).sqrMagnitudeLong);
}
/// <summary>
/// Removes any connection from this node to the specified node.
/// If no such connection exists, nothing will be done.
///
/// Note: This only removes the connection from this node to the other node.
/// You may want to call the same function on the other node to remove its possible connection
/// to this node.
///
/// <code>
/// AstarPath.active.AddWorkItem(new AstarWorkItem(ctx => {
/// // Connect two nodes
/// var node1 = AstarPath.active.GetNearest(transform.position, NNConstraint.None).node;
/// var node2 = AstarPath.active.GetNearest(transform.position + Vector3.right, NNConstraint.None).node;
/// var cost = (uint)(node2.position - node1.position).costMagnitude;
/// node1.AddConnection(node2, cost);
/// node2.AddConnection(node1, cost);
///
/// node1.ContainsConnection(node2); // True
///
/// node1.RemoveConnection(node2);
/// node2.RemoveConnection(node1);
/// }));
/// </code>
/// </summary>
public override void RemoveConnection (GraphNode node) {
if (connections == null) return;
for (int i = 0; i < connections.Length; i++) {
if (connections[i].node == node) {
int connLength = connections.Length;
var newconns = new Connection[connLength-1];
for (int j = 0; j < i; j++) {
newconns[j] = connections[j];
}
for (int j = i+1; j < connLength; j++) {
newconns[j-1] = connections[j];
}
connections = newconns;
AstarPath.active.hierarchicalGraph.AddDirtyNode(this);
return;
}
}
}
public override void Open (Path path, PathNode pathNode, PathHandler handler) {
if (connections == null) return;
for (int i = 0; i < connections.Length; i++) {
GraphNode other = connections[i].node;
if (path.CanTraverse(other)) {
PathNode pathOther = handler.GetPathNode(other);
if (pathOther.pathID != handler.PathID) {
pathOther.parent = pathNode;
pathOther.pathID = handler.PathID;
pathOther.cost = connections[i].cost;
pathOther.H = path.CalculateHScore(other);
pathOther.UpdateG(path);
handler.heap.Add(pathOther);
} else {
//If not we can test if the path from this node to the other one is a better one then the one already used
uint tmpCost = connections[i].cost;
if (pathNode.G + tmpCost + path.GetTraversalCost(other) < pathOther.G) {
pathOther.cost = tmpCost;
pathOther.parent = pathNode;
other.UpdateRecursiveG(path, pathOther, handler);
}
}
}
}
}
public override int GetGizmoHashCode () {
var hash = base.GetGizmoHashCode();
if (connections != null) {
for (int i = 0; i < connections.Length; i++) {
hash ^= 17 * connections[i].GetHashCode();
}
}
return hash;
}
public override void SerializeNode (GraphSerializationContext ctx) {
base.SerializeNode(ctx);
ctx.SerializeInt3(position);
}
public override void DeserializeNode (GraphSerializationContext ctx) {
base.DeserializeNode(ctx);
position = ctx.DeserializeInt3();
}
public override void SerializeReferences (GraphSerializationContext ctx) {
if (connections == null) {
ctx.writer.Write(-1);
} else {
ctx.writer.Write(connections.Length);
for (int i = 0; i < connections.Length; i++) {
ctx.SerializeNodeReference(connections[i].node);
ctx.writer.Write(connections[i].cost);
}
}
}
public override void DeserializeReferences (GraphSerializationContext ctx) {
int count = ctx.reader.ReadInt32();
if (count == -1) {
connections = null;
} else {
connections = new Connection[count];
for (int i = 0; i < count; i++) {
connections[i] = new Connection(ctx.DeserializeNodeReference(), ctx.reader.ReadUInt32());
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 986ad6174b59e40068c715a916740ce9
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:

View File

@@ -0,0 +1,477 @@
using UnityEngine;
using Pathfinding.Serialization;
namespace Pathfinding {
/// <summary>Interface for something that holds a triangle based navmesh</summary>
public interface INavmeshHolder : ITransformedGraph, INavmesh {
/// <summary>Position of vertex number i in the world</summary>
Int3 GetVertex(int i);
/// <summary>
/// Position of vertex number i in coordinates local to the graph.
/// The up direction is always the +Y axis for these coordinates.
/// </summary>
Int3 GetVertexInGraphSpace(int i);
int GetVertexArrayIndex(int index);
/// <summary>Transforms coordinates from graph space to world space</summary>
void GetTileCoordinates(int tileIndex, out int x, out int z);
}
/// <summary>Node represented by a triangle</summary>
public class TriangleMeshNode : MeshNode {
public TriangleMeshNode (AstarPath astar) : base(astar) {}
/// <summary>Internal vertex index for the first vertex</summary>
public int v0;
/// <summary>Internal vertex index for the second vertex</summary>
public int v1;
/// <summary>Internal vertex index for the third vertex</summary>
public int v2;
/// <summary>Holds INavmeshHolder references for all graph indices to be able to access them in a performant manner</summary>
protected static INavmeshHolder[] _navmeshHolders = new INavmeshHolder[0];
/// <summary>Used for synchronised access to the <see cref="_navmeshHolders"/> array</summary>
protected static readonly System.Object lockObject = new System.Object();
public static INavmeshHolder GetNavmeshHolder (uint graphIndex) {
return _navmeshHolders[(int)graphIndex];
}
/// <summary>
/// Sets the internal navmesh holder for a given graph index.
/// Warning: Internal method
/// </summary>
public static void SetNavmeshHolder (int graphIndex, INavmeshHolder graph) {
// We need to lock to make sure that
// the resize operation is thread safe
lock (lockObject) {
if (graphIndex >= _navmeshHolders.Length) {
var gg = new INavmeshHolder[graphIndex+1];
_navmeshHolders.CopyTo(gg, 0);
_navmeshHolders = gg;
}
_navmeshHolders[graphIndex] = graph;
}
}
/// <summary>Set the position of this node to the average of its 3 vertices</summary>
public void UpdatePositionFromVertices () {
Int3 a, b, c;
GetVertices(out a, out b, out c);
position = (a + b + c) * 0.333333f;
}
/// <summary>
/// Return a number identifying a vertex.
/// This number does not necessarily need to be a index in an array but two different vertices (in the same graph) should
/// not have the same vertex numbers.
/// </summary>
public int GetVertexIndex (int i) {
return i == 0 ? v0 : (i == 1 ? v1 : v2);
}
/// <summary>
/// Return a number specifying an index in the source vertex array.
/// The vertex array can for example be contained in a recast tile, or be a navmesh graph, that is graph dependant.
/// This is slower than GetVertexIndex, if you only need to compare vertices, use GetVertexIndex.
/// </summary>
public int GetVertexArrayIndex (int i) {
return GetNavmeshHolder(GraphIndex).GetVertexArrayIndex(i == 0 ? v0 : (i == 1 ? v1 : v2));
}
/// <summary>Returns all 3 vertices of this node in world space</summary>
public void GetVertices (out Int3 v0, out Int3 v1, out Int3 v2) {
// Get the object holding the vertex data for this node
// This is usually a graph or a recast graph tile
var holder = GetNavmeshHolder(GraphIndex);
v0 = holder.GetVertex(this.v0);
v1 = holder.GetVertex(this.v1);
v2 = holder.GetVertex(this.v2);
}
/// <summary>Returns all 3 vertices of this node in graph space</summary>
public void GetVerticesInGraphSpace (out Int3 v0, out Int3 v1, out Int3 v2) {
// Get the object holding the vertex data for this node
// This is usually a graph or a recast graph tile
var holder = GetNavmeshHolder(GraphIndex);
v0 = holder.GetVertexInGraphSpace(this.v0);
v1 = holder.GetVertexInGraphSpace(this.v1);
v2 = holder.GetVertexInGraphSpace(this.v2);
}
public override Int3 GetVertex (int i) {
return GetNavmeshHolder(GraphIndex).GetVertex(GetVertexIndex(i));
}
public Int3 GetVertexInGraphSpace (int i) {
return GetNavmeshHolder(GraphIndex).GetVertexInGraphSpace(GetVertexIndex(i));
}
public override int GetVertexCount () {
// A triangle has 3 vertices
return 3;
}
public override Vector3 ClosestPointOnNode (Vector3 p) {
Int3 a, b, c;
GetVertices(out a, out b, out c);
return Pathfinding.Polygon.ClosestPointOnTriangle((Vector3)a, (Vector3)b, (Vector3)c, p);
}
/// <summary>
/// Closest point on the node when seen from above.
/// This method is mostly for internal use as the <see cref="Pathfinding.NavmeshBase.Linecast"/> methods use it.
///
/// - The returned point is the closest one on the node to p when seen from above (relative to the graph).
/// This is important mostly for sloped surfaces.
/// - The returned point is an Int3 point in graph space.
/// - It is guaranteed to be inside the node, so if you call <see cref="ContainsPointInGraphSpace"/> with the return value from this method the result is guaranteed to be true.
///
/// This method is slower than e.g <see cref="ClosestPointOnNode"/> or <see cref="ClosestPointOnNodeXZ"/>.
/// However they do not have the same guarantees as this method has.
/// </summary>
internal Int3 ClosestPointOnNodeXZInGraphSpace (Vector3 p) {
// Get the vertices that make up the triangle
Int3 a, b, c;
GetVerticesInGraphSpace(out a, out b, out c);
// Convert p to graph space
p = GetNavmeshHolder(GraphIndex).transform.InverseTransform(p);
// Find the closest point on the triangle to p when looking at the triangle from above (relative to the graph)
var closest = Pathfinding.Polygon.ClosestPointOnTriangleXZ((Vector3)a, (Vector3)b, (Vector3)c, p);
// Make sure the point is actually inside the node
var i3closest = (Int3)closest;
if (ContainsPointInGraphSpace(i3closest)) {
// Common case
return i3closest;
} else {
// Annoying...
// The closest point when converted from floating point coordinates to integer coordinates
// is not actually inside the node. It needs to be inside the node for some methods
// (like for example Linecast) to work properly.
// Try the 8 integer coordinates around the closest point
// and check if any one of them are completely inside the node.
// This will most likely succeed as it should be very close.
for (int dx = -1; dx <= 1; dx++) {
for (int dz = -1; dz <= 1; dz++) {
if ((dx != 0 || dz != 0)) {
var candidate = new Int3(i3closest.x + dx, i3closest.y, i3closest.z + dz);
if (ContainsPointInGraphSpace(candidate)) return candidate;
}
}
}
// Happens veery rarely.
// Pick the closest vertex of the triangle.
// The vertex is guaranteed to be inside the triangle.
var da = (a - i3closest).sqrMagnitudeLong;
var db = (b - i3closest).sqrMagnitudeLong;
var dc = (c - i3closest).sqrMagnitudeLong;
return da < db ? (da < dc ? a : c) : (db < dc ? b : c);
}
}
public override Vector3 ClosestPointOnNodeXZ (Vector3 p) {
// Get all 3 vertices for this node
GetVertices(out Int3 tp1, out Int3 tp2, out Int3 tp3);
return Polygon.ClosestPointOnTriangleXZ((Vector3)tp1, (Vector3)tp2, (Vector3)tp3, p);
}
/// <summary>
/// Checks if point is inside the node when seen from above.
///
/// Note that <see cref="ContainsPointInGraphSpace"/> is faster than this method as it avoids
/// some coordinate transformations. If you are repeatedly calling this method
/// on many different nodes but with the same point then you should consider
/// transforming the point first and then calling ContainsPointInGraphSpace.
/// <code>
/// Int3 p = (Int3)graph.transform.InverseTransform(point);
///
/// node.ContainsPointInGraphSpace(p);
/// </code>
/// </summary>
public override bool ContainsPoint (Vector3 p) {
return ContainsPointInGraphSpace((Int3)GetNavmeshHolder(GraphIndex).transform.InverseTransform(p));
}
/// <summary>
/// Checks if point is inside the node in graph space.
///
/// In graph space the up direction is always the Y axis so in principle
/// we project the triangle down on the XZ plane and check if the point is inside the 2D triangle there.
/// </summary>
public override bool ContainsPointInGraphSpace (Int3 p) {
// Get all 3 vertices for this node
Int3 a, b, c;
GetVerticesInGraphSpace(out a, out b, out c);
if ((long)(b.x - a.x) * (long)(p.z - a.z) - (long)(p.x - a.x) * (long)(b.z - a.z) > 0) return false;
if ((long)(c.x - b.x) * (long)(p.z - b.z) - (long)(p.x - b.x) * (long)(c.z - b.z) > 0) return false;
if ((long)(a.x - c.x) * (long)(p.z - c.z) - (long)(p.x - c.x) * (long)(a.z - c.z) > 0) return false;
return true;
// Equivalent code, but the above code is faster
//return Polygon.IsClockwiseMargin (a,b, p) && Polygon.IsClockwiseMargin (b,c, p) && Polygon.IsClockwiseMargin (c,a, p);
//return Polygon.ContainsPoint(g.GetVertex(v0),g.GetVertex(v1),g.GetVertex(v2),p);
}
public override void UpdateRecursiveG (Path path, PathNode pathNode, PathHandler handler) {
pathNode.UpdateG(path);
handler.heap.Add(pathNode);
if (connections == null) return;
for (int i = 0; i < connections.Length; i++) {
GraphNode other = connections[i].node;
PathNode otherPN = handler.GetPathNode(other);
if (otherPN.parent == pathNode && otherPN.pathID == handler.PathID) other.UpdateRecursiveG(path, otherPN, handler);
}
}
public override void Open (Path path, PathNode pathNode, PathHandler handler) {
if (connections == null) return;
// Flag2 indicates if this node needs special treatment
// with regard to connection costs
bool flag2 = pathNode.flag2;
// Loop through all connections
for (int i = connections.Length-1; i >= 0; i--) {
var conn = connections[i];
var other = conn.node;
// Make sure we can traverse the neighbour
if (path.CanTraverse(conn.node)) {
PathNode pathOther = handler.GetPathNode(conn.node);
// Fast path out, worth it for triangle mesh nodes since they usually have degree 2 or 3
if (pathOther == pathNode.parent) {
continue;
}
uint cost = conn.cost;
if (flag2 || pathOther.flag2) {
// Get special connection cost from the path
// This is used by the start and end nodes
cost = path.GetConnectionSpecialCost(this, conn.node, cost);
}
// Test if we have seen the other node before
if (pathOther.pathID != handler.PathID) {
// We have not seen the other node before
// So the path from the start through this node to the other node
// must be the shortest one so far
// Might not be assigned
pathOther.node = conn.node;
pathOther.parent = pathNode;
pathOther.pathID = handler.PathID;
pathOther.cost = cost;
pathOther.H = path.CalculateHScore(other);
pathOther.UpdateG(path);
handler.heap.Add(pathOther);
} else {
// If not we can test if the path from this node to the other one is a better one than the one already used
if (pathNode.G + cost + path.GetTraversalCost(other) < pathOther.G) {
pathOther.cost = cost;
pathOther.parent = pathNode;
other.UpdateRecursiveG(path, pathOther, handler);
}
}
}
}
}
/// <summary>
/// Returns the edge which is shared with other.
/// If no edge is shared, -1 is returned.
/// If there is a connection with the other node, but the connection is not marked as using a particular edge of the shape of the node
/// then Connection.NoSharedEdge will be returned.
///
/// The vertices in the edge can be retrieved using
/// <code>
/// var edge = node.SharedEdge(other);
/// var a = node.GetVertex(edge);
/// var b = node.GetVertex((edge+1) % node.GetVertexCount());
/// </code>
///
/// See: <see cref="GetPortal"/> which also handles edges that are shared over tile borders and some types of node links
/// </summary>
public int SharedEdge (GraphNode other) {
var edge = -1;
if (connections != null) {
for (int i = 0; i < connections.Length; i++) {
if (connections[i].node == other) edge = connections[i].shapeEdge;
}
}
return edge;
}
public override bool GetPortal (GraphNode toNode, System.Collections.Generic.List<Vector3> left, System.Collections.Generic.List<Vector3> right, bool backwards) {
int aIndex, bIndex;
return GetPortal(toNode, left, right, backwards, out aIndex, out bIndex);
}
public bool GetPortal (GraphNode toNode, System.Collections.Generic.List<Vector3> left, System.Collections.Generic.List<Vector3> right, bool backwards, out int aIndex, out int bIndex) {
aIndex = -1;
bIndex = -1;
//If the nodes are in different graphs, this function has no idea on how to find a shared edge.
if (backwards || toNode.GraphIndex != GraphIndex) return false;
// Since the nodes are in the same graph, they are both TriangleMeshNodes
// So we don't need to care about other types of nodes
var toTriNode = toNode as TriangleMeshNode;
var edge = SharedEdge(toTriNode);
// A connection was found, but it specifically didn't use an edge
if (edge == Connection.NoSharedEdge) return false;
// No connection was found between the nodes
// Check if there is a node link that connects them
if (edge == -1) {
#if !ASTAR_NO_POINT_GRAPH
if (connections != null) {
for (int i = 0; i < connections.Length; i++) {
if (connections[i].node.GraphIndex != GraphIndex) {
var mid = connections[i].node as NodeLink3Node;
if (mid != null && mid.GetOther(this) == toTriNode) {
// We have found a node which is connected through a NodeLink3Node
mid.GetPortal(toTriNode, left, right, false);
return true;
}
}
}
}
#endif
return false;
}
aIndex = edge;
bIndex = (edge + 1) % GetVertexCount();
// Get the vertices of the shared edge for the first node
Int3 v1a = GetVertex(edge);
Int3 v1b = GetVertex((edge+1) % GetVertexCount());
// Get tile indices
int tileIndex1 = (GetVertexIndex(0) >> NavmeshBase.TileIndexOffset) & NavmeshBase.TileIndexMask;
int tileIndex2 = (toTriNode.GetVertexIndex(0) >> NavmeshBase.TileIndexOffset) & NavmeshBase.TileIndexMask;
if (tileIndex1 != tileIndex2) {
// When the nodes are in different tiles, the edges might not be completely identical
// so another technique is needed.
// Get the tile coordinates, from them we can figure out which edge is going to be shared
int x1, x2, z1, z2, coord;
INavmeshHolder nm = GetNavmeshHolder(GraphIndex);
nm.GetTileCoordinates(tileIndex1, out x1, out z1);
nm.GetTileCoordinates(tileIndex2, out x2, out z2);
if (System.Math.Abs(x1-x2) == 1) coord = 2;
else if (System.Math.Abs(z1-z2) == 1) coord = 0;
else return false; // Tiles are not adjacent. This is likely a custom connection between two nodes.
var otherEdge = toTriNode.SharedEdge(this);
// A connection was found, but it specifically didn't use an edge. This is odd since the connection in the other direction did use an edge
if (otherEdge == Connection.NoSharedEdge) throw new System.Exception("Connection used edge in one direction, but not in the other direction. Has the wrong overload of AddConnection been used?");
// If it is -1 then it must be a one-way connection. Fall back to using the whole edge
if (otherEdge != -1) {
// When the nodes are in different tiles, they might not share exactly the same edge
// so we clamp the portal to the segment of the edges which they both have.
int mincoord = System.Math.Min(v1a[coord], v1b[coord]);
int maxcoord = System.Math.Max(v1a[coord], v1b[coord]);
// Get the vertices of the shared edge for the second node
Int3 v2a = toTriNode.GetVertex(otherEdge);
Int3 v2b = toTriNode.GetVertex((otherEdge+1) % toTriNode.GetVertexCount());
mincoord = System.Math.Max(mincoord, System.Math.Min(v2a[coord], v2b[coord]));
maxcoord = System.Math.Min(maxcoord, System.Math.Max(v2a[coord], v2b[coord]));
if (v1a[coord] < v1b[coord]) {
v1a[coord] = mincoord;
v1b[coord] = maxcoord;
} else {
v1a[coord] = maxcoord;
v1b[coord] = mincoord;
}
}
}
if (left != null) {
// All triangles should be laid out in clockwise order so v1b is the rightmost vertex (seen from this node)
left.Add((Vector3)v1a);
right.Add((Vector3)v1b);
}
return true;
}
/// <summary>TODO: This is the area in XZ space, use full 3D space for higher correctness maybe?</summary>
public override float SurfaceArea () {
var holder = GetNavmeshHolder(GraphIndex);
return System.Math.Abs(VectorMath.SignedTriangleAreaTimes2XZ(holder.GetVertex(v0), holder.GetVertex(v1), holder.GetVertex(v2))) * 0.5f;
}
public override Vector3 RandomPointOnSurface () {
// Find a random point inside the triangle
// This generates uniformly distributed trilinear coordinates
// See http://mathworld.wolfram.com/TrianglePointPicking.html
float r1;
float r2;
do {
r1 = Random.value;
r2 = Random.value;
} while (r1+r2 > 1);
var holder = GetNavmeshHolder(GraphIndex);
// Pick the point corresponding to the trilinear coordinate
return ((Vector3)(holder.GetVertex(v1)-holder.GetVertex(v0)))*r1 + ((Vector3)(holder.GetVertex(v2)-holder.GetVertex(v0)))*r2 + (Vector3)holder.GetVertex(v0);
}
public override void SerializeNode (GraphSerializationContext ctx) {
base.SerializeNode(ctx);
ctx.writer.Write(v0);
ctx.writer.Write(v1);
ctx.writer.Write(v2);
}
public override void DeserializeNode (GraphSerializationContext ctx) {
base.DeserializeNode(ctx);
v0 = ctx.reader.ReadInt32();
v1 = ctx.reader.ReadInt32();
v2 = ctx.reader.ReadInt32();
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 54908f58720324c048a5b475a27077fa
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:

View File

@@ -0,0 +1,777 @@
using UnityEngine;
using System.Collections.Generic;
using Pathfinding.Serialization;
namespace Pathfinding {
/// <summary>
/// Basic point graph.
///
/// The point graph is the most basic graph structure, it consists of a number of interconnected points in space called nodes or waypoints.
/// The point graph takes a Transform object as "root", this Transform will be searched for child objects, every child object will be treated as a node.
/// If <see cref="recursive"/> is enabled, it will also search the child objects of the children recursively.
/// It will then check if any connections between the nodes can be made, first it will check if the distance between the nodes isn't too large (<see cref="maxDistance)"/>
/// and then it will check if the axis aligned distance isn't too high. The axis aligned distance, named <see cref="limits"/>,
/// is useful because usually an AI cannot climb very high, but linking nodes far away from each other,
/// but on the same Y level should still be possible. <see cref="limits"/> and <see cref="maxDistance"/> are treated as being set to infinity if they are set to 0 (zero).
/// Lastly it will check if there are any obstructions between the nodes using
/// <a href="http://unity3d.com/support/documentation/ScriptReference/Physics.Raycast.html">raycasting</a> which can optionally be thick.
/// One thing to think about when using raycasting is to either place the nodes a small
/// distance above the ground in your scene or to make sure that the ground is not in the raycast mask to avoid the raycast from hitting the ground.
///
/// Alternatively, a tag can be used to search for nodes.
/// See: http://docs.unity3d.com/Manual/Tags.html
///
/// For larger graphs, it can take quite some time to scan the graph with the default settings.
/// If you have the pro version you can enable <see cref="optimizeForSparseGraph"/> which will in most cases reduce the calculation times
/// drastically.
///
/// Note: Does not support linecast because of obvious reasons.
///
/// [Open online documentation to see images]
/// [Open online documentation to see images]
/// </summary>
[JsonOptIn]
[Pathfinding.Util.Preserve]
public class PointGraph : NavGraph
, IUpdatableGraph {
/// <summary>Childs of this transform are treated as nodes</summary>
[JsonMember]
public Transform root;
/// <summary>If no <see cref="root"/> is set, all nodes with the tag is used as nodes</summary>
[JsonMember]
public string searchTag;
/// <summary>
/// Max distance for a connection to be valid.
/// The value 0 (zero) will be read as infinity and thus all nodes not restricted by
/// other constraints will be added as connections.
///
/// A negative value will disable any neighbours to be added.
/// It will completely stop the connection processing to be done, so it can save you processing
/// power if you don't these connections.
/// </summary>
[JsonMember]
public float maxDistance;
/// <summary>Max distance along the axis for a connection to be valid. 0 = infinity</summary>
[JsonMember]
public Vector3 limits;
/// <summary>Use raycasts to check connections</summary>
[JsonMember]
public bool raycast = true;
/// <summary>Use the 2D Physics API</summary>
[JsonMember]
public bool use2DPhysics;
/// <summary>Use thick raycast</summary>
[JsonMember]
public bool thickRaycast;
/// <summary>Thick raycast radius</summary>
[JsonMember]
public float thickRaycastRadius = 1;
/// <summary>Recursively search for child nodes to the <see cref="root"/></summary>
[JsonMember]
public bool recursive = true;
/// <summary>Layer mask to use for raycast</summary>
[JsonMember]
public LayerMask mask;
/// <summary>
/// Optimizes the graph for sparse graphs.
///
/// This can reduce calculation times for both scanning and for normal path requests by huge amounts.
/// It reduces the number of node-node checks that need to be done during scan, and can also optimize getting the nearest node from the graph (such as when querying for a path).
///
/// Try enabling and disabling this option, check the scan times logged when you scan the graph to see if your graph is suited for this optimization
/// or if it makes it slower.
///
/// The gain of using this optimization increases with larger graphs, the default scan algorithm is brute force and requires O(n^2) checks, this optimization
/// along with a graph suited for it, requires only O(n) checks during scan (assuming the connection distance limits are reasonable).
///
/// Warning:
/// When you have this enabled, you will not be able to move nodes around using scripting unless you recalculate the lookup structure at the same time.
/// See: <see cref="RebuildNodeLookup"/>
///
/// If you enable this during runtime, you will need to call <see cref="RebuildNodeLookup"/> to make sure any existing nodes are added to the lookup structure.
/// If the graph doesn't have any nodes yet or if you are going to scan the graph afterwards then you do not need to do this.
/// </summary>
[JsonMember]
public bool optimizeForSparseGraph;
PointKDTree lookupTree = new PointKDTree();
/// <summary>
/// Longest known connection.
/// In squared Int3 units.
///
/// See: <see cref="RegisterConnectionLength"/>
/// </summary>
long maximumConnectionLength = 0;
/// <summary>
/// All nodes in this graph.
/// Note that only the first <see cref="nodeCount"/> will be non-null.
///
/// You can also use the GetNodes method to get all nodes.
/// </summary>
public PointNode[] nodes;
/// <summary>
/// \copydoc Pathfinding::PointGraph::NodeDistanceMode
///
/// See: <see cref="NodeDistanceMode"/>
///
/// If you enable this during runtime, you will need to call <see cref="RebuildConnectionDistanceLookup"/> to make sure some cache data is properly recalculated.
/// If the graph doesn't have any nodes yet or if you are going to scan the graph afterwards then you do not need to do this.
/// </summary>
[JsonMember]
public NodeDistanceMode nearestNodeDistanceMode;
/// <summary>Number of nodes in this graph</summary>
public int nodeCount { get; protected set; }
public override bool isScanned => nodes != null;
/// <summary>
/// Distance query mode.
/// [Open online documentation to see images]
///
/// In the image above there are a few red nodes. Assume the agent is the orange circle. Using the Node mode the closest point on the graph that would be found would be the node at the bottom center which
/// may not be what you want. Using the Connection mode it will find the closest point on the connection between the two nodes in the top half of the image.
///
/// When using the Connection option you may also want to use the Connection option for the Seeker's Start End Modifier snapping options.
/// This is not strictly necessary, but it most cases it is what you want.
///
/// See: <see cref="Pathfinding.StartEndModifier.exactEndPoint"/>
/// </summary>
public enum NodeDistanceMode {
/// <summary>
/// All nearest node queries find the closest node center.
/// This is the fastest option but it may not be what you want if you have long connections.
/// </summary>
Node,
/// <summary>
/// All nearest node queries find the closest point on edges between nodes.
/// This is useful if you have long connections where the agent might be closer to some unrelated node if it is standing on a long connection between two nodes.
/// This mode is however slower than the Node mode.
/// </summary>
Connection,
}
public override int CountNodes () {
return nodeCount;
}
public override void GetNodes (System.Action<GraphNode> action) {
if (nodes == null) return;
var count = nodeCount;
for (int i = 0; i < count; i++) action(nodes[i]);
}
/// <summary>
/// True if the point is inside the bounding box of this graph.
///
/// Warning: A point graph has no limits to its bounding box, so this method always returns true for point graphs.
/// </summary>
public override bool IsInsideBounds (Vector3 point) {
return true;
}
public override NNInfoInternal GetNearest (Vector3 position, NNConstraint constraint, GraphNode hint) {
return GetNearestInternal(position, constraint, true);
}
public override NNInfoInternal GetNearestForce (Vector3 position, NNConstraint constraint) {
return GetNearestInternal(position, constraint, false);
}
NNInfoInternal GetNearestInternal (Vector3 position, NNConstraint constraint, bool fastCheck) {
if (nodes == null) return new NNInfoInternal();
var iposition = (Int3)position;
if (optimizeForSparseGraph) {
if (nearestNodeDistanceMode == NodeDistanceMode.Node) {
return new NNInfoInternal(lookupTree.GetNearest(iposition, fastCheck ? null : constraint));
} else {
var closestNode = lookupTree.GetNearestConnection(iposition, fastCheck ? null : constraint, maximumConnectionLength);
if (closestNode == null) return new NNInfoInternal();
return FindClosestConnectionPoint(closestNode as PointNode, position);
}
}
float maxDistSqr = constraint == null || constraint.constrainDistance ? AstarPath.active.maxNearestNodeDistanceSqr : float.PositiveInfinity;
maxDistSqr *= Int3.FloatPrecision * Int3.FloatPrecision;
var nnInfo = new NNInfoInternal(null);
long minDist = long.MaxValue;
long minConstDist = long.MaxValue;
for (int i = 0; i < nodeCount; i++) {
PointNode node = nodes[i];
long dist = (iposition - node.position).sqrMagnitudeLong;
if (dist < minDist) {
minDist = dist;
nnInfo.node = node;
}
if (dist < minConstDist && (float)dist < maxDistSqr && (constraint == null || constraint.Suitable(node))) {
minConstDist = dist;
nnInfo.constrainedNode = node;
}
}
if (!fastCheck) nnInfo.node = nnInfo.constrainedNode;
nnInfo.UpdateInfo();
return nnInfo;
}
NNInfoInternal FindClosestConnectionPoint (PointNode node, Vector3 position) {
var closestConnectionPoint = (Vector3)node.position;
var conns = node.connections;
var nodePos = (Vector3)node.position;
var bestDist = float.PositiveInfinity;
if (conns != null) {
for (int i = 0; i < conns.Length; i++) {
var connectionMidpoint = ((UnityEngine.Vector3)conns[i].node.position + nodePos) * 0.5f;
var closestPoint = VectorMath.ClosestPointOnSegment(nodePos, connectionMidpoint, position);
var dist = (closestPoint - position).sqrMagnitude;
if (dist < bestDist) {
bestDist = dist;
closestConnectionPoint = closestPoint;
}
}
}
var result = new NNInfoInternal();
result.node = node;
result.clampedPosition = closestConnectionPoint;
return result;
}
/// <summary>
/// Add a node to the graph at the specified position.
/// Note: Vector3 can be casted to Int3 using (Int3)myVector.
///
/// Note: This needs to be called when it is safe to update nodes, which is
/// - when scanning
/// - during a graph update
/// - inside a callback registered using AstarPath.AddWorkItem
///
/// <code>
/// AstarPath.active.AddWorkItem(new AstarWorkItem(ctx => {
/// var graph = AstarPath.active.data.pointGraph;
/// // Add 2 nodes and connect them
/// var node1 = graph.AddNode((Int3)transform.position);
/// var node2 = graph.AddNode((Int3)(transform.position + Vector3.right));
/// var cost = (uint)(node2.position - node1.position).costMagnitude;
/// node1.AddConnection(node2, cost);
/// node2.AddConnection(node1, cost);
/// }));
/// </code>
///
/// See: runtime-graphs (view in online documentation for working links)
/// </summary>
public PointNode AddNode (Int3 position) {
return AddNode(new PointNode(active), position);
}
/// <summary>
/// Add a node with the specified type to the graph at the specified position.
///
/// Note: Vector3 can be casted to Int3 using (Int3)myVector.
///
/// Note: This needs to be called when it is safe to update nodes, which is
/// - when scanning
/// - during a graph update
/// - inside a callback registered using AstarPath.AddWorkItem
///
/// See: <see cref="AstarPath.AddWorkItem"/>
/// See: runtime-graphs (view in online documentation for working links)
/// </summary>
/// <param name="node">This must be a node created using T(AstarPath.active) right before the call to this method.
/// The node parameter is only there because there is no new(AstarPath) constraint on
/// generic type parameters.</param>
/// <param name="position">The node will be set to this position.</param>
public T AddNode<T>(T node, Int3 position) where T : PointNode {
if (nodes == null || nodeCount == nodes.Length) {
var newNodes = new PointNode[nodes != null ? System.Math.Max(nodes.Length+4, nodes.Length*2) : 4];
if (nodes != null) nodes.CopyTo(newNodes, 0);
nodes = newNodes;
}
node.SetPosition(position);
node.GraphIndex = graphIndex;
node.Walkable = true;
nodes[nodeCount] = node;
nodeCount++;
if (optimizeForSparseGraph) AddToLookup(node);
return node;
}
/// <summary>Recursively counds children of a transform</summary>
protected static int CountChildren (Transform tr) {
int c = 0;
foreach (Transform child in tr) {
c++;
c += CountChildren(child);
}
return c;
}
/// <summary>Recursively adds childrens of a transform as nodes</summary>
protected void AddChildren (ref int c, Transform tr) {
foreach (Transform child in tr) {
nodes[c].position = (Int3)child.position;
nodes[c].Walkable = true;
nodes[c].gameObject = child.gameObject;
c++;
AddChildren(ref c, child);
}
}
/// <summary>
/// Rebuilds the lookup structure for nodes.
///
/// This is used when <see cref="optimizeForSparseGraph"/> is enabled.
///
/// You should call this method every time you move a node in the graph manually and
/// you are using <see cref="optimizeForSparseGraph"/>, otherwise pathfinding might not work correctly.
///
/// You may also call this after you have added many nodes using the
/// <see cref="AddNode"/> method. When adding nodes using the <see cref="AddNode"/> method they
/// will be added to the lookup structure. The lookup structure will
/// rebalance itself when it gets too unbalanced however if you are
/// sure you won't be adding any more nodes in the short term, you can
/// make sure it is perfectly balanced and thus squeeze out the last
/// bit of performance by calling this method. This can improve the
/// performance of the <see cref="GetNearest"/> method slightly. The improvements
/// are on the order of 10-20%.
/// </summary>
public void RebuildNodeLookup () {
if (!optimizeForSparseGraph || nodes == null) {
lookupTree = new PointKDTree();
} else {
lookupTree.Rebuild(nodes, 0, nodeCount);
}
RebuildConnectionDistanceLookup();
}
/// <summary>Rebuilds a cache used when <see cref="nearestNodeDistanceMode"/> = <see cref="NodeDistanceMode.ToConnection"/></summary>
public void RebuildConnectionDistanceLookup () {
maximumConnectionLength = 0;
if (nearestNodeDistanceMode == NodeDistanceMode.Connection) {
for (int j = 0; j < nodeCount; j++) {
var node = nodes[j];
var conns = node.connections;
if (conns != null) {
for (int i = 0; i < conns.Length; i++) {
var dist = (node.position - conns[i].node.position).sqrMagnitudeLong;
RegisterConnectionLength(dist);
}
}
}
}
}
void AddToLookup (PointNode node) {
lookupTree.Add(node);
}
/// <summary>
/// Ensures the graph knows that there is a connection with this length.
/// This is used when the nearest node distance mode is set to ToConnection.
/// If you are modifying node connections yourself (i.e. manipulating the PointNode.connections array) then you must call this function
/// when you add any connections.
///
/// When using PointNode.AddConnection this is done automatically.
/// It is also done for all nodes when <see cref="RebuildNodeLookup"/> is called.
/// </summary>
/// <param name="sqrLength">The length of the connection in squared Int3 units. This can be calculated using (node1.position - node2.position).sqrMagnitudeLong.</param>
public void RegisterConnectionLength (long sqrLength) {
maximumConnectionLength = System.Math.Max(maximumConnectionLength, sqrLength);
}
protected virtual PointNode[] CreateNodes (int count) {
var nodes = new PointNode[count];
for (int i = 0; i < nodeCount; i++) nodes[i] = new PointNode(active);
return nodes;
}
protected override IEnumerable<Progress> ScanInternal () {
yield return new Progress(0, "Searching for GameObjects");
if (root == null) {
// If there is no root object, try to find nodes with the specified tag instead
GameObject[] gos = searchTag != null? GameObject.FindGameObjectsWithTag(searchTag) : null;
if (gos == null) {
nodes = new PointNode[0];
nodeCount = 0;
} else {
yield return new Progress(0.1f, "Creating nodes");
// Create all the nodes
nodeCount = gos.Length;
nodes = CreateNodes(nodeCount);
for (int i = 0; i < gos.Length; i++) {
nodes[i].position = (Int3)gos[i].transform.position;
nodes[i].Walkable = true;
nodes[i].gameObject = gos[i].gameObject;
}
}
} else {
// Search the root for children and create nodes for them
if (!recursive) {
nodeCount = root.childCount;
nodes = CreateNodes(nodeCount);
int c = 0;
foreach (Transform child in root) {
nodes[c].position = (Int3)child.position;
nodes[c].Walkable = true;
nodes[c].gameObject = child.gameObject;
c++;
}
} else {
nodeCount = CountChildren(root);
nodes = CreateNodes(nodeCount);
int startID = 0;
AddChildren(ref startID, root);
}
}
yield return new Progress(0.15f, "Building node lookup");
// Note that this *must* run every scan
RebuildNodeLookup();
foreach (var progress in ConnectNodesAsync()) yield return progress.MapTo(0.15f, 0.95f);
yield return new Progress(0.95f, "Building connection distances");
// Note that this *must* run every scan
RebuildConnectionDistanceLookup();
}
/// <summary>
/// Recalculates connections for all nodes in the graph.
/// This is useful if you have created nodes manually using <see cref="AddNode"/> and then want to connect them in the same way as the point graph normally connects nodes.
/// </summary>
public void ConnectNodes () {
var ie = ConnectNodesAsync().GetEnumerator();
while (ie.MoveNext()) {}
RebuildConnectionDistanceLookup();
}
/// <summary>
/// Calculates connections for all nodes in the graph.
/// This is an IEnumerable, you can iterate through it using e.g foreach to get progress information.
/// </summary>
IEnumerable<Progress> ConnectNodesAsync () {
if (maxDistance >= 0) {
// To avoid too many allocations, these lists are reused for each node
var connections = new List<Connection>();
var candidateConnections = new List<GraphNode>();
long maxSquaredRange;
// Max possible squared length of a connection between two nodes
// This is used to speed up the calculations by skipping a lot of nodes that do not need to be checked
if (maxDistance == 0 && (limits.x == 0 || limits.y == 0 || limits.z == 0)) {
maxSquaredRange = long.MaxValue;
} else {
maxSquaredRange = (long)(Mathf.Max(limits.x, Mathf.Max(limits.y, Mathf.Max(limits.z, maxDistance))) * Int3.Precision) + 1;
maxSquaredRange *= maxSquaredRange;
}
// Report progress every N nodes
const int YieldEveryNNodes = 512;
// Loop through all nodes and add connections to other nodes
for (int i = 0; i < nodeCount; i++) {
if (i % YieldEveryNNodes == 0) {
yield return new Progress(i/(float)nodeCount, "Connecting nodes");
}
connections.Clear();
var node = nodes[i];
if (optimizeForSparseGraph) {
candidateConnections.Clear();
lookupTree.GetInRange(node.position, maxSquaredRange, candidateConnections);
for (int j = 0; j < candidateConnections.Count; j++) {
var other = candidateConnections[j] as PointNode;
float dist;
if (other != node && IsValidConnection(node, other, out dist)) {
connections.Add(new Connection(
other,
/// <summary>TODO: Is this equal to .costMagnitude</summary>
(uint)Mathf.RoundToInt(dist*Int3.FloatPrecision)
));
}
}
} else {
// Only brute force is available in the free version
for (int j = 0; j < nodeCount; j++) {
if (i == j) continue;
PointNode other = nodes[j];
float dist;
if (IsValidConnection(node, other, out dist)) {
connections.Add(new Connection(
other,
/// <summary>TODO: Is this equal to .costMagnitude</summary>
(uint)Mathf.RoundToInt(dist*Int3.FloatPrecision)
));
}
}
}
node.connections = connections.ToArray();
node.SetConnectivityDirty();
}
}
}
/// <summary>
/// Returns if the connection between a and b is valid.
/// Checks for obstructions using raycasts (if enabled) and checks for height differences.
/// As a bonus, it outputs the distance between the nodes too if the connection is valid.
///
/// Note: This is not the same as checking if node a is connected to node b.
/// That should be done using a.ContainsConnection(b)
/// </summary>
public virtual bool IsValidConnection (GraphNode a, GraphNode b, out float dist) {
dist = 0;
if (!a.Walkable || !b.Walkable) return false;
var dir = (Vector3)(b.position-a.position);
if (
(!Mathf.Approximately(limits.x, 0) && Mathf.Abs(dir.x) > limits.x) ||
(!Mathf.Approximately(limits.y, 0) && Mathf.Abs(dir.y) > limits.y) ||
(!Mathf.Approximately(limits.z, 0) && Mathf.Abs(dir.z) > limits.z)) {
return false;
}
dist = dir.magnitude;
if (maxDistance == 0 || dist < maxDistance) {
if (raycast) {
var ray = new Ray((Vector3)a.position, dir);
var invertRay = new Ray((Vector3)b.position, -dir);
if (use2DPhysics) {
if (thickRaycast) {
return !Physics2D.CircleCast(ray.origin, thickRaycastRadius, ray.direction, dist, mask) && !Physics2D.CircleCast(invertRay.origin, thickRaycastRadius, invertRay.direction, dist, mask);
} else {
return !Physics2D.Linecast((Vector2)(Vector3)a.position, (Vector2)(Vector3)b.position, mask) && !Physics2D.Linecast((Vector2)(Vector3)b.position, (Vector2)(Vector3)a.position, mask);
}
} else {
if (thickRaycast) {
return !Physics.SphereCast(ray, thickRaycastRadius, dist, mask) && !Physics.SphereCast(invertRay, thickRaycastRadius, dist, mask);
} else {
return !Physics.Linecast((Vector3)a.position, (Vector3)b.position, mask) && !Physics.Linecast((Vector3)b.position, (Vector3)a.position, mask);
}
}
} else {
return true;
}
}
return false;
}
GraphUpdateThreading IUpdatableGraph.CanUpdateAsync (GraphUpdateObject o) {
return GraphUpdateThreading.UnityThread;
}
void IUpdatableGraph.UpdateAreaInit (GraphUpdateObject o) {}
void IUpdatableGraph.UpdateAreaPost (GraphUpdateObject o) {}
/// <summary>
/// Updates an area in the list graph.
/// Recalculates possibly affected connections, i.e all connectionlines passing trough the bounds of the guo will be recalculated
/// </summary>
void IUpdatableGraph.UpdateArea (GraphUpdateObject guo) {
if (nodes == null) return;
for (int i = 0; i < nodeCount; i++) {
var node = nodes[i];
if (guo.bounds.Contains((Vector3)node.position)) {
guo.WillUpdateNode(node);
guo.Apply(node);
}
}
if (guo.updatePhysics) {
// Use a copy of the bounding box, we should not change the GUO's bounding box since it might be used for other graph updates
Bounds bounds = guo.bounds;
if (thickRaycast) {
// Expand the bounding box to account for the thick raycast
bounds.Expand(thickRaycastRadius*2);
}
// Create a temporary list used for holding connection data
List<Connection> tmpList = Pathfinding.Util.ListPool<Connection>.Claim();
for (int i = 0; i < nodeCount; i++) {
PointNode node = nodes[i];
var nodePos = (Vector3)node.position;
List<Connection> conn = null;
for (int j = 0; j < nodeCount; j++) {
if (j == i) continue;
var otherNodePos = (Vector3)nodes[j].position;
// Check if this connection intersects the bounding box.
// If it does we need to recalculate that connection.
if (VectorMath.SegmentIntersectsBounds(bounds, nodePos, otherNodePos)) {
float dist;
PointNode other = nodes[j];
bool contains = node.ContainsConnection(other);
bool validConnection = IsValidConnection(node, other, out dist);
// Fill the 'conn' list when we need to change a connection
if (conn == null && (contains != validConnection)) {
tmpList.Clear();
conn = tmpList;
conn.AddRange(node.connections);
}
if (!contains && validConnection) {
// A new connection should be added
uint cost = (uint)Mathf.RoundToInt(dist*Int3.FloatPrecision);
conn.Add(new Connection(other, cost));
RegisterConnectionLength((other.position - node.position).sqrMagnitudeLong);
} else if (contains && !validConnection) {
// A connection should be removed
for (int q = 0; q < conn.Count; q++) {
if (conn[q].node == other) {
conn.RemoveAt(q);
break;
}
}
}
}
}
// Save the new connections if any were changed
if (conn != null) {
node.connections = conn.ToArray();
node.SetConnectivityDirty();
}
}
// Release buffers back to the pool
Pathfinding.Util.ListPool<Connection>.Release(ref tmpList);
}
}
#if UNITY_EDITOR
public override void OnDrawGizmos (Pathfinding.Util.RetainedGizmos gizmos, bool drawNodes) {
base.OnDrawGizmos(gizmos, drawNodes);
if (!drawNodes) return;
Gizmos.color = new Color(0.161f, 0.341f, 1f, 0.5f);
if (root != null) {
DrawChildren(this, root);
} else if (!string.IsNullOrEmpty(searchTag)) {
GameObject[] gos = GameObject.FindGameObjectsWithTag(searchTag);
for (int i = 0; i < gos.Length; i++) {
Gizmos.DrawCube(gos[i].transform.position, Vector3.one*UnityEditor.HandleUtility.GetHandleSize(gos[i].transform.position)*0.1F);
}
}
}
static void DrawChildren (PointGraph graph, Transform tr) {
foreach (Transform child in tr) {
Gizmos.DrawCube(child.position, Vector3.one*UnityEditor.HandleUtility.GetHandleSize(child.position)*0.1F);
if (graph.recursive) DrawChildren(graph, child);
}
}
#endif
protected override void PostDeserialization (GraphSerializationContext ctx) {
RebuildNodeLookup();
}
public override void RelocateNodes (Matrix4x4 deltaMatrix) {
base.RelocateNodes(deltaMatrix);
RebuildNodeLookup();
}
protected override void DeserializeSettingsCompatibility (GraphSerializationContext ctx) {
base.DeserializeSettingsCompatibility(ctx);
root = ctx.DeserializeUnityObject() as Transform;
searchTag = ctx.reader.ReadString();
maxDistance = ctx.reader.ReadSingle();
limits = ctx.DeserializeVector3();
raycast = ctx.reader.ReadBoolean();
use2DPhysics = ctx.reader.ReadBoolean();
thickRaycast = ctx.reader.ReadBoolean();
thickRaycastRadius = ctx.reader.ReadSingle();
recursive = ctx.reader.ReadBoolean();
ctx.reader.ReadBoolean(); // Deprecated field
mask = (LayerMask)ctx.reader.ReadInt32();
optimizeForSparseGraph = ctx.reader.ReadBoolean();
ctx.reader.ReadBoolean(); // Deprecated field
}
protected override void SerializeExtraInfo (GraphSerializationContext ctx) {
// Serialize node data
if (nodes == null) ctx.writer.Write(-1);
// Length prefixed array of nodes
ctx.writer.Write(nodeCount);
for (int i = 0; i < nodeCount; i++) {
// -1 indicates a null field
if (nodes[i] == null) ctx.writer.Write(-1);
else {
ctx.writer.Write(0);
nodes[i].SerializeNode(ctx);
}
}
}
protected override void DeserializeExtraInfo (GraphSerializationContext ctx) {
int count = ctx.reader.ReadInt32();
if (count == -1) {
nodes = null;
return;
}
nodes = new PointNode[count];
nodeCount = count;
for (int i = 0; i < nodes.Length; i++) {
if (ctx.reader.ReadInt32() == -1) continue;
nodes[i] = new PointNode(active);
nodes[i].DeserializeNode(ctx);
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f7458b5480c614cebb219a8f7f5df111
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,4 @@
// This file has been removed from the project. Since UnityPackages cannot
// delete files, only replace them, this message is left here to prevent old
// files from causing compiler errors

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4931e76e5e5784200a904f4b59596556
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,996 @@
using Math = System.Math;
using UnityEngine;
using System.Collections.Generic;
using UnityEngine.Profiling;
namespace Pathfinding {
using Pathfinding.Voxels;
using Pathfinding.Serialization;
using Pathfinding.Recast;
using Pathfinding.Util;
using System.Threading;
/// <summary>
/// Automatically generates navmesh graphs based on world geometry.
///
/// [Open online documentation to see images]
///
/// The recast graph is based on Recast (http://code.google.com/p/recastnavigation/).
/// I have translated a good portion of it to C# to run it natively in Unity.
///
/// [Open online documentation to see images]
///
/// \section howitworks How a recast graph works
/// When generating a recast graph what happens is that the world is voxelized.
/// You can think of this as constructing an approximation of the world out of lots of boxes.
/// If you have played Minecraft it looks very similar (but with smaller boxes).
/// [Open online documentation to see images]
///
/// The Recast process is described as follows:
/// - The voxel mold is build from the input triangle mesh by rasterizing the triangles into a multi-layer heightfield.
/// Some simple filters are then applied to the mold to prune out locations where the character would not be able to move.
/// - The walkable areas described by the mold are divided into simple overlayed 2D regions.
/// The resulting regions have only one non-overlapping contour, which simplifies the final step of the process tremendously.
/// - The navigation polygons are peeled off from the regions by first tracing the boundaries and then simplifying them.
/// The resulting polygons are finally converted to triangles which makes them perfect for pathfinding and spatial reasoning about the level.
///
/// The recast generation process usually works directly on the visiable geometry in the world. This is usually a good thing, because world geometry is usually more detailed than the colliders.
/// You can, however, specify that colliders should be rasterized instead. If you have very detailed world geometry, this can speed up scanning and updating the graph.
///
/// \section export Exporting for manual editing
/// In the editor there is a button for exporting the generated graph to a .obj file.
/// Usually the generation process is good enough for the game directly, but in some cases you might want to edit some minor details.
/// So you can export the graph to a .obj file, open it in your favourite 3D application, edit it, and export it to a mesh which Unity can import.
/// You can then use that mesh in a navmesh graph.
///
/// Since many 3D modelling programs use different axis systems (unity uses X=right, Y=up, Z=forward), it can be a bit tricky to get the rotation and scaling right.
/// For blender for example, what you have to do is to first import the mesh using the .obj importer. Don't change anything related to axes in the settings.
/// Then select the mesh, open the transform tab (usually the thin toolbar to the right of the 3D view) and set Scale -> Z to -1.
/// If you transform it using the S (scale) hotkey, it seems to set both Z and Y to -1 for some reason.
/// Then make the edits you need and export it as an .obj file to somewhere in the Unity project.
/// But this time, edit the setting named "Forward" to "Z forward" (not -Z as it is per default).
/// </summary>
[JsonOptIn]
[Pathfinding.Util.Preserve]
public class RecastGraph : NavmeshBase, IUpdatableGraph {
[JsonMember]
/// <summary>
/// Radius of the agent which will traverse the navmesh.
/// The navmesh will be eroded with this radius.
/// [Open online documentation to see images]
/// </summary>
public float characterRadius = 1.5F;
/// <summary>
/// Max distance from simplified edge to real edge.
/// This value is measured in voxels. So with the default value of 2 it means that the final navmesh contour may be at most
/// 2 voxels (i.e 2 times <see cref="cellSize)"/> away from the border that was calculated when voxelizing the world.
/// A higher value will yield a more simplified and cleaner navmesh while a lower value may capture more details.
/// However a too low value will cause the individual voxels to be visible (see image below).
///
/// [Open online documentation to see images]
///
/// See: <see cref="cellSize"/>
/// </summary>
[JsonMember]
public float contourMaxError = 2F;
/// <summary>
/// Voxel sample size (x,z).
/// When generating a recast graph what happens is that the world is voxelized.
/// You can think of this as constructing an approximation of the world out of lots of boxes.
/// If you have played Minecraft it looks very similar (but with smaller boxes).
/// [Open online documentation to see images]
/// The cell size is the width and depth of those boxes. The height of the boxes is usually much smaller
/// and automatically calculated however. See <see cref="CellHeight"/>.
///
/// Lower values will yield higher quality navmeshes, however the graph will be slower to scan.
///
/// [Open online documentation to see images]
/// </summary>
[JsonMember]
public float cellSize = 0.5F;
/// <summary>
/// Character height.
/// [Open online documentation to see images]
/// </summary>
[JsonMember]
public float walkableHeight = 2F;
/// <summary>
/// Height the character can climb.
/// [Open online documentation to see images]
/// </summary>
[JsonMember]
public float walkableClimb = 0.5F;
/// <summary>
/// Max slope in degrees the character can traverse.
/// [Open online documentation to see images]
/// </summary>
[JsonMember]
public float maxSlope = 30;
/// <summary>
/// Longer edges will be subdivided.
/// Reducing this value can sometimes improve path quality since similarly sized triangles
/// yield better paths than really large and really triangles small next to each other.
/// However it will also add a lot more nodes which will make pathfinding slower.
/// For more information about this take a look at navmeshnotes (view in online documentation for working links).
///
/// [Open online documentation to see images]
/// </summary>
[JsonMember]
public float maxEdgeLength = 20;
/// <summary>
/// Minumum region size.
/// Small regions will be removed from the navmesh.
/// Measured in square world units (square meters in most games).
///
/// [Open online documentation to see images]
///
/// If a region is adjacent to a tile border, it will not be removed
/// even though it is small since the adjacent tile might join it
/// to form a larger region.
///
/// [Open online documentation to see images]
/// [Open online documentation to see images]
/// </summary>
[JsonMember]
public float minRegionSize = 3;
/// <summary>
/// Size in voxels of a single tile.
/// This is the width of the tile.
///
/// [Open online documentation to see images]
///
/// A large tile size can be faster to initially scan (but beware of out of memory issues if you try with a too large tile size in a large world)
/// smaller tile sizes are (much) faster to update.
///
/// Different tile sizes can affect the quality of paths. It is often good to split up huge open areas into several tiles for
/// better quality paths, but too small tiles can also lead to effects looking like invisible obstacles.
/// For more information about this take a look at navmeshnotes (view in online documentation for working links).
/// Usually it is best to experiment and see what works best for your game.
///
/// When scanning a recast graphs individual tiles can be calculated in parallel which can make it much faster to scan large worlds.
/// When you want to recalculate a part of a recast graph, this can only be done on a tile-by-tile basis which means that if you often try to update a region
/// of the recast graph much smaller than the tile size, then you will be doing a lot of unnecessary calculations. However if you on the other hand
/// update regions of the recast graph that are much larger than the tile size then it may be slower than necessary as there is some overhead in having lots of tiles
/// instead of a few larger ones (not that much though).
///
/// Recommended values are between 64 and 256, but these are very soft limits. It is possible to use both larger and smaller values.
/// </summary>
[JsonMember]
public int editorTileSize = 128;
/// <summary>
/// Size of a tile along the X axis in voxels.
/// \copydetails editorTileSize
///
/// Warning: Do not modify, it is set from <see cref="editorTileSize"/> at Scan
///
/// See: <see cref="tileSizeZ"/>
/// </summary>
[JsonMember]
public int tileSizeX = 128;
/// <summary>
/// Size of a tile along the Z axis in voxels.
/// \copydetails editorTileSize
///
/// Warning: Do not modify, it is set from <see cref="editorTileSize"/> at Scan
///
/// See: <see cref="tileSizeX"/>
/// </summary>
[JsonMember]
public int tileSizeZ = 128;
/// <summary>
/// If true, divide the graph into tiles, otherwise use a single tile covering the whole graph.
///
/// Using tiles is useful for a number of things. But it also has some drawbacks.
/// - Using tiles allows you to update only a part of the graph at a time. When doing graph updates on a recast graph, it will always recalculate whole tiles (or the whole graph if there are no tiles).
/// <see cref="NavmeshCut"/> components also work on a tile-by-tile basis.
/// - Using tiles allows you to use <see cref="NavmeshPrefab"/>s.
/// - Using tiles can break up very large triangles, which can improve path quality in some cases, and make the navmesh more closely follow the y-coordinates of the ground.
/// - Using tiles can make it much faster to generate the navmesh, because each tile can be calculated in parallel.
/// But if the tiles are made too small, then the overhead of having many tiles can make it slower than having fewer tiles.
/// - Using small tiles can make the path quality worse in some cases, but setting the <see cref="FunnelModifier"/>s quality setting to high (or using <see cref="RichAI.funnelSimplification"/>) will mostly mitigate this.
///
/// See: <see cref="editorTileSize"/>
///
/// Since: Since 4.1 the default value is true.
/// </summary>
[JsonMember]
public bool useTiles = true;
/// <summary>
/// If true, scanning the graph will yield a completely empty graph.
/// Useful if you want to replace the graph with a custom navmesh for example
/// </summary>
public bool scanEmptyGraph;
public enum RelevantGraphSurfaceMode {
/// <summary>No RelevantGraphSurface components are required anywhere</summary>
DoNotRequire,
/// <summary>
/// Any surfaces that are completely inside tiles need to have a <see cref="Pathfinding.RelevantGraphSurface"/> component
/// positioned on that surface, otherwise it will be stripped away.
/// </summary>
OnlyForCompletelyInsideTile,
/// <summary>
/// All surfaces need to have one <see cref="Pathfinding.RelevantGraphSurface"/> component
/// positioned somewhere on the surface and in each tile that it touches, otherwise it will be stripped away.
/// Only tiles that have a RelevantGraphSurface component for that surface will keep it.
/// </summary>
RequireForAll
}
/// <summary>
/// Require every region to have a RelevantGraphSurface component inside it.
/// A RelevantGraphSurface component placed in the scene specifies that
/// the navmesh region it is inside should be included in the navmesh.
///
/// If this is set to OnlyForCompletelyInsideTile
/// a navmesh region is included in the navmesh if it
/// has a RelevantGraphSurface inside it, or if it
/// is adjacent to a tile border. This can leave some small regions
/// which you didn't want to have included because they are adjacent
/// to tile borders, but it removes the need to place a component
/// in every single tile, which can be tedious (see below).
///
/// If this is set to RequireForAll
/// a navmesh region is included only if it has a RelevantGraphSurface
/// inside it. Note that even though the navmesh
/// looks continous between tiles, the tiles are computed individually
/// and therefore you need a RelevantGraphSurface component for each
/// region and for each tile.
///
/// [Open online documentation to see images]
/// In the above image, the mode OnlyForCompletelyInsideTile was used. Tile borders
/// are highlighted in black. Note that since all regions are adjacent to a tile border,
/// this mode didn't remove anything in this case and would give the same result as DoNotRequire.
/// The RelevantGraphSurface component is shown using the green gizmo in the top-right of the blue plane.
///
/// [Open online documentation to see images]
/// In the above image, the mode RequireForAll was used. No tiles were used.
/// Note that the small region at the top of the orange cube is now gone, since it was not the in the same
/// region as the relevant graph surface component.
/// The result would have been identical with OnlyForCompletelyInsideTile since there are no tiles (or a single tile, depending on how you look at it).
///
/// [Open online documentation to see images]
/// The mode RequireForAll was used here. Since there is only a single RelevantGraphSurface component, only the region
/// it was in, in the tile it is placed in, will be enabled. If there would have been several RelevantGraphSurface in other tiles,
/// those regions could have been enabled as well.
///
/// [Open online documentation to see images]
/// Here another tile size was used along with the OnlyForCompletelyInsideTile.
/// Note that the region on top of the orange cube is gone now since the region borders do not intersect that region (and there is no
/// RelevantGraphSurface component inside it).
///
/// Note: When not using tiles. OnlyForCompletelyInsideTile is equivalent to RequireForAll.
/// </summary>
[JsonMember]
public RelevantGraphSurfaceMode relevantGraphSurfaceMode = RelevantGraphSurfaceMode.DoNotRequire;
[JsonMember]
/// <summary>Use colliders to calculate the navmesh</summary>
public bool rasterizeColliders;
[JsonMember]
/// <summary>Use scene meshes to calculate the navmesh</summary>
public bool rasterizeMeshes = true;
/// <summary>Include the Terrain in the scene.</summary>
[JsonMember]
public bool rasterizeTerrain = true;
/// <summary>
/// Rasterize tree colliders on terrains.
///
/// If the tree prefab has a collider, that collider will be rasterized.
/// Otherwise a simple box collider will be used and the script will
/// try to adjust it to the tree's scale, it might not do a very good job though so
/// an attached collider is preferable.
///
/// Note: It seems that Unity will only generate tree colliders at runtime when the game is started.
/// For this reason, this graph will not pick up tree colliders when scanned outside of play mode
/// but it will pick them up if the graph is scanned when the game has started. If it still does not pick them up
/// make sure that the trees actually have colliders attached to them and that the tree prefabs are
/// in the correct layer (the layer should be included in the layer mask).
///
/// See: rasterizeTerrain
/// See: colliderRasterizeDetail
/// </summary>
[JsonMember]
public bool rasterizeTrees = true;
/// <summary>
/// Controls detail on rasterization of sphere and capsule colliders.
/// This controls the number of rows and columns on the generated meshes.
/// A higher value does not necessarily increase quality of the mesh, but a lower
/// value will often speed it up.
///
/// You should try to keep this value as low as possible without affecting the mesh quality since
/// that will yield the fastest scan times.
///
/// See: rasterizeColliders
/// </summary>
[JsonMember]
public float colliderRasterizeDetail = 10;
/// <summary>
/// Layer mask which filters which objects to include.
/// See: tagMask
/// </summary>
[JsonMember]
public LayerMask mask = -1;
/// <summary>
/// Objects tagged with any of these tags will be rasterized.
/// Note that this extends the layer mask, so if you only want to use tags, set <see cref="mask"/> to 'Nothing'.
///
/// See: mask
/// </summary>
[JsonMember]
public List<string> tagMask = new List<string>();
/// <summary>
/// Controls how large the sample size for the terrain is.
/// A higher value is faster to scan but less accurate
/// </summary>
[JsonMember]
public int terrainSampleSize = 3;
/// <summary>Rotation of the graph in degrees</summary>
[JsonMember]
public Vector3 rotation;
/// <summary>
/// Center of the bounding box.
/// Scanning will only be done inside the bounding box
/// </summary>
[JsonMember]
public Vector3 forcedBoundsCenter;
private Voxelize globalVox;
public const int BorderVertexMask = 1;
public const int BorderVertexOffset = 31;
/// <summary>
/// List of tiles that have been calculated in a graph update, but have not yet been added to the graph.
/// When updating the graph in a separate thread, large changes cannot be made directly to the graph
/// as other scripts might use the graph data structures at the same time in another thread.
/// So the tiles are calculated, but they are not yet connected to the existing tiles
/// that will be done in UpdateAreaPost which runs in the Unity thread.
/// </summary>
List<NavmeshTile> stagingTiles = new List<NavmeshTile>();
public override bool RecalculateNormals { get { return true; } }
public override float TileWorldSizeX {
get {
return tileSizeX*cellSize;
}
}
public override float TileWorldSizeZ {
get {
return tileSizeZ*cellSize;
}
}
protected override float MaxTileConnectionEdgeDistance {
get {
return walkableClimb;
}
}
/// <summary>
/// World bounds for the graph.
/// Defined as a bounds object with size <see cref="forcedBoundsSize"/> and centered at <see cref="forcedBoundsCenter"/>
/// Deprecated: Obsolete since this is not accurate when the graph is rotated (rotation was not supported when this property was created)
/// </summary>
[System.Obsolete("Obsolete since this is not accurate when the graph is rotated (rotation was not supported when this property was created)")]
public Bounds forcedBounds {
get {
return new Bounds(forcedBoundsCenter, forcedBoundsSize);
}
}
/// <summary>
/// True if the point is inside the bounding box of this graph.
///
/// Note: This method uses a tighter non-axis-aligned bounding box than you can get from the <see cref="bounds"/> property.
///
/// Note: What is considered inside the bounds is only updated when the graph is scanned. For an unscanned graph, this will always return false.
/// </summary>
public override bool IsInsideBounds (Vector3 point) {
if (this.tiles == null || this.tiles.Length == 0) return false;
var local = transform.InverseTransform(point);
return local.x >= 0 && local.y >= 0 && local.z >= 0 && local.x <= forcedBoundsSize.x && local.y <= forcedBoundsSize.y && local.z <= forcedBoundsSize.z;
}
/// <summary>
/// Returns the closest point of the node.
/// Deprecated: Use <see cref="Pathfinding.TriangleMeshNode.ClosestPointOnNode"/> instead
/// </summary>
[System.Obsolete("Use node.ClosestPointOnNode instead")]
public Vector3 ClosestPointOnNode (TriangleMeshNode node, Vector3 pos) {
return node.ClosestPointOnNode(pos);
}
/// <summary>
/// Returns if the point is inside the node in XZ space.
/// Deprecated: Use <see cref="Pathfinding.TriangleMeshNode.ContainsPoint"/> instead
/// </summary>
[System.Obsolete("Use node.ContainsPoint instead")]
public bool ContainsPoint (TriangleMeshNode node, Vector3 pos) {
return node.ContainsPoint((Int3)pos);
}
/// <summary>
/// Changes the bounds of the graph to precisely encapsulate all objects in the scene that can be included in the scanning process based on the settings.
/// Which objects are used depends on the settings. If an object would have affected the graph with the current settings if it would have
/// been inside the bounds of the graph, it will be detected and the bounds will be expanded to contain that object.
///
/// This method corresponds to the 'Snap bounds to scene' button in the inspector.
///
/// See: rasterizeMeshes
/// See: rasterizeTerrain
/// See: rasterizeColliders
/// See: mask
/// See: tagMask
///
/// See: forcedBoundsCenter
/// See: forcedBoundsSize
/// </summary>
public void SnapForceBoundsToScene () {
var meshes = CollectMeshes(new Bounds(Vector3.zero, new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity)));
if (meshes.Count == 0) {
return;
}
var bounds = meshes[0].bounds;
for (int i = 1; i < meshes.Count; i++) {
bounds.Encapsulate(meshes[i].bounds);
meshes[i].Pool();
}
forcedBoundsCenter = bounds.center;
forcedBoundsSize = bounds.size;
}
GraphUpdateThreading IUpdatableGraph.CanUpdateAsync (GraphUpdateObject o) {
return o.updatePhysics ? GraphUpdateThreading.UnityInit | GraphUpdateThreading.SeparateThread | GraphUpdateThreading.UnityPost : GraphUpdateThreading.SeparateThread;
}
void IUpdatableGraph.UpdateAreaInit (GraphUpdateObject o) {
if (!o.updatePhysics) {
return;
}
RelevantGraphSurface.UpdateAllPositions();
// Calculate world bounds of all affected tiles
// Expand TileBorderSizeInWorldUnits voxels in all directions to make sure
// all tiles that could be affected by the update are recalculated.
IntRect touchingTiles = GetTouchingTiles(o.bounds, TileBorderSizeInWorldUnits);
Bounds tileBounds = GetTileBounds(touchingTiles);
// Expand TileBorderSizeInWorldUnits voxels in all directions to make sure we grab all meshes that could affect the tiles.
tileBounds.Expand(new Vector3(1, 0, 1)*TileBorderSizeInWorldUnits*2);
var meshes = CollectMeshes(tileBounds);
if (globalVox == null) {
// Create the voxelizer and set all settings
globalVox = new Voxelize(CellHeight, cellSize, walkableClimb, walkableHeight, maxSlope, maxEdgeLength);
}
globalVox.inputMeshes = meshes;
}
void IUpdatableGraph.UpdateArea (GraphUpdateObject guo) {
// Figure out which tiles are affected
// Expand TileBorderSizeInWorldUnits voxels in all directions to make sure
// all tiles that could be affected by the update are recalculated.
var affectedTiles = GetTouchingTiles(guo.bounds, TileBorderSizeInWorldUnits);
if (!guo.updatePhysics) {
for (int z = affectedTiles.ymin; z <= affectedTiles.ymax; z++) {
for (int x = affectedTiles.xmin; x <= affectedTiles.xmax; x++) {
NavmeshTile tile = tiles[z*tileXCount + x];
NavMeshGraph.UpdateArea(guo, tile);
}
}
return;
}
Voxelize vox = globalVox;
if (vox == null) {
throw new System.InvalidOperationException("No Voxelizer object. UpdateAreaInit should have been called before this function.");
}
// Build the new tiles
for (int x = affectedTiles.xmin; x <= affectedTiles.xmax; x++) {
for (int z = affectedTiles.ymin; z <= affectedTiles.ymax; z++) {
stagingTiles.Add(BuildTileMesh(vox, x, z));
}
}
uint graphIndex = (uint)AstarPath.active.data.GetGraphIndex(this);
// Set the correct graph index
for (int i = 0; i < stagingTiles.Count; i++) {
NavmeshTile tile = stagingTiles[i];
GraphNode[] nodes = tile.nodes;
for (int j = 0; j < nodes.Length; j++) nodes[j].GraphIndex = graphIndex;
}
for (int i = 0; i < vox.inputMeshes.Count; i++) vox.inputMeshes[i].Pool();
ListPool<RasterizationMesh>.Release(ref vox.inputMeshes);
}
/// <summary>Called on the Unity thread to complete a graph update</summary>
void IUpdatableGraph.UpdateAreaPost (GraphUpdateObject guo) {
Profiler.BeginSample("RemoveConnections");
// Remove connections from existing tiles destroy the nodes
// Replace the old tile by the new tile
for (int i = 0; i < stagingTiles.Count; i++) {
var tile = stagingTiles[i];
int index = tile.x + tile.z * tileXCount;
var oldTile = tiles[index];
// Destroy the previous nodes
for (int j = 0; j < oldTile.nodes.Length; j++) {
oldTile.nodes[j].Destroy();
}
tiles[index] = tile;
}
Profiler.EndSample();
Profiler.BeginSample("Connect With Neighbours");
// Connect the new tiles with their neighbours
for (int i = 0; i < stagingTiles.Count; i++) {
var tile = stagingTiles[i];
ConnectTileWithNeighbours(tile, false);
}
// This may be used to update the tile again to take into
// account NavmeshCut components.
// It is not the super efficient, but it works.
// Usually you only use either normal graph updates OR navmesh
// cutting, not both.
var updatedTiles = stagingTiles.ToArray();
navmeshUpdateData.OnRecalculatedTiles(updatedTiles);
if (OnRecalculatedTiles != null) OnRecalculatedTiles(updatedTiles);
stagingTiles.Clear();
Profiler.EndSample();
}
protected override IEnumerable<Progress> ScanInternal () {
TriangleMeshNode.SetNavmeshHolder(AstarPath.active.data.GetGraphIndex(this), this);
if (!Application.isPlaying) {
RelevantGraphSurface.FindAllGraphSurfaces();
}
RelevantGraphSurface.UpdateAllPositions();
foreach (var progress in ScanAllTiles()) {
yield return progress;
}
}
public override GraphTransform CalculateTransform () {
return new GraphTransform(Matrix4x4.TRS(forcedBoundsCenter, Quaternion.Euler(rotation), Vector3.one) * Matrix4x4.TRS(-forcedBoundsSize*0.5f, Quaternion.identity, Vector3.one));
}
void InitializeTileInfo () {
// Voxel grid size
int totalVoxelWidth = (int)(forcedBoundsSize.x/cellSize + 0.5f);
int totalVoxelDepth = (int)(forcedBoundsSize.z/cellSize + 0.5f);
if (!useTiles) {
tileSizeX = totalVoxelWidth;
tileSizeZ = totalVoxelDepth;
} else {
tileSizeX = editorTileSize;
tileSizeZ = editorTileSize;
}
// Number of tiles
tileXCount = (totalVoxelWidth + tileSizeX-1) / tileSizeX;
tileZCount = (totalVoxelDepth + tileSizeZ-1) / tileSizeZ;
if (tileXCount * tileZCount > TileIndexMask+1) {
throw new System.Exception("Too many tiles ("+(tileXCount * tileZCount)+") maximum is "+(TileIndexMask+1)+
"\nTry disabling ASTAR_RECAST_LARGER_TILES under the 'Optimizations' tab in the A* inspector.");
}
tiles = new NavmeshTile[tileXCount*tileZCount];
}
/// <summary>Creates a list for every tile and adds every mesh that touches a tile to the corresponding list</summary>
List<RasterizationMesh>[] PutMeshesIntoTileBuckets (List<RasterizationMesh> meshes) {
var result = new List<RasterizationMesh>[tiles.Length];
var borderExpansion = new Vector3(1, 0, 1)*TileBorderSizeInWorldUnits*2;
for (int i = 0; i < result.Length; i++) {
result[i] = ListPool<RasterizationMesh>.Claim();
}
for (int i = 0; i < meshes.Count; i++) {
var mesh = meshes[i];
var bounds = mesh.bounds;
// Expand borderSize voxels on each side
bounds.Expand(borderExpansion);
var rect = GetTouchingTiles(bounds);
for (int z = rect.ymin; z <= rect.ymax; z++) {
for (int x = rect.xmin; x <= rect.xmax; x++) {
result[x + z*tileXCount].Add(mesh);
}
}
}
return result;
}
protected IEnumerable<Progress> ScanAllTiles () {
transform = CalculateTransform();
InitializeTileInfo();
// If this is true, just fill the graph with empty tiles
if (scanEmptyGraph) {
FillWithEmptyTiles();
yield break;
}
// A walkableClimb higher than walkableHeight can cause issues when generating the navmesh since then it can in some cases
// Both be valid for a character to walk under an obstacle and climb up on top of it (and that cannot be handled with navmesh without links)
// The editor scripts also enforce this but we enforce it here too just to be sure
walkableClimb = Mathf.Min(walkableClimb, walkableHeight);
yield return new Progress(0, "Finding Meshes");
var bounds = transform.Transform(new Bounds(forcedBoundsSize*0.5f, forcedBoundsSize));
var meshes = CollectMeshes(bounds);
var buckets = PutMeshesIntoTileBuckets(meshes);
Queue<Int2> tileQueue = new Queue<Int2>();
// Put all tiles in the queue
for (int z = 0; z < tileZCount; z++) {
for (int x = 0; x < tileXCount; x++) {
tileQueue.Enqueue(new Int2(x, z));
}
}
var workQueue = new ParallelWorkQueue<Int2>(tileQueue);
// Create the voxelizers and set all settings (one for each thread)
var voxelizers = new Voxelize[workQueue.threadCount];
for (int i = 0; i < voxelizers.Length; i++) voxelizers[i] = new Voxelize(CellHeight, cellSize, walkableClimb, walkableHeight, maxSlope, maxEdgeLength);
workQueue.action = (tile, threadIndex) => {
voxelizers[threadIndex].inputMeshes = buckets[tile.x + tile.y*tileXCount];
tiles[tile.x + tile.y*tileXCount] = BuildTileMesh(voxelizers[threadIndex], tile.x, tile.y, threadIndex);
};
// Prioritize responsiveness while playing
// but when not playing prioritize throughput
// (the Unity progress bar is also pretty slow to update)
int timeoutMillis = Application.isPlaying ? 1 : 200;
// Scan all tiles in parallel
foreach (var done in workQueue.Run(timeoutMillis)) {
yield return new Progress(Mathf.Lerp(0.1f, 0.9f, done / (float)tiles.Length), "Calculated Tiles: " + done + "/" + tiles.Length);
}
yield return new Progress(0.9f, "Assigning Graph Indices");
// Assign graph index to nodes
uint graphIndex = (uint)AstarPath.active.data.GetGraphIndex(this);
GetNodes(node => node.GraphIndex = graphIndex);
// First connect all tiles with an EVEN coordinate sum
// This would be the white squares on a chess board.
// Then connect all tiles with an ODD coordinate sum (which would be all black squares on a chess board).
// This will prevent the different threads that do all
// this in parallel from conflicting with each other.
// The directions are also done separately
// first they are connected along the X direction and then along the Z direction.
// Looping over 0 and then 1
for (int coordinateSum = 0; coordinateSum <= 1; coordinateSum++) {
for (int direction = 0; direction <= 1; direction++) {
for (int i = 0; i < tiles.Length; i++) {
if ((tiles[i].x + tiles[i].z) % 2 == coordinateSum) {
tileQueue.Enqueue(new Int2(tiles[i].x, tiles[i].z));
}
}
workQueue = new ParallelWorkQueue<Int2>(tileQueue);
workQueue.action = (tile, threadIndex) => {
// Connect with tile at (x+1,z) and (x,z+1)
if (direction == 0 && tile.x < tileXCount - 1)
ConnectTiles(tiles[tile.x + tile.y * tileXCount], tiles[tile.x + 1 + tile.y * tileXCount]);
if (direction == 1 && tile.y < tileZCount - 1)
ConnectTiles(tiles[tile.x + tile.y * tileXCount], tiles[tile.x + (tile.y + 1) * tileXCount]);
};
var numTilesInQueue = tileQueue.Count;
// Connect all tiles in parallel
foreach (var done in workQueue.Run(timeoutMillis)) {
yield return new Progress(0.95f, "Connected Tiles " + (numTilesInQueue - done) + "/" + numTilesInQueue + " (Phase " + (direction + 1 + 2*coordinateSum) + " of 4)");
}
}
}
for (int i = 0; i < meshes.Count; i++) meshes[i].Pool();
ListPool<RasterizationMesh>.Release(ref meshes);
// Signal that tiles have been recalculated to the navmesh cutting system.
navmeshUpdateData.OnRecalculatedTiles(tiles);
if (OnRecalculatedTiles != null) OnRecalculatedTiles(tiles.Clone() as NavmeshTile[]);
}
List<RasterizationMesh> CollectMeshes (Bounds bounds) {
Profiler.BeginSample("Find Meshes for rasterization");
var result = ListPool<RasterizationMesh>.Claim();
var meshGatherer = new RecastMeshGatherer(bounds, terrainSampleSize, mask, tagMask, colliderRasterizeDetail);
if (rasterizeMeshes) {
Profiler.BeginSample("Find meshes");
meshGatherer.CollectSceneMeshes(result);
Profiler.EndSample();
}
Profiler.BeginSample("Find RecastMeshObj components");
meshGatherer.CollectRecastMeshObjs(result);
Profiler.EndSample();
if (rasterizeTerrain) {
Profiler.BeginSample("Find terrains");
// Split terrains up into meshes approximately the size of a single chunk
var desiredTerrainChunkSize = cellSize*Math.Max(tileSizeX, tileSizeZ);
meshGatherer.CollectTerrainMeshes(rasterizeTrees, desiredTerrainChunkSize, result);
Profiler.EndSample();
}
if (rasterizeColliders) {
Profiler.BeginSample("Find colliders");
meshGatherer.CollectColliderMeshes(result);
Profiler.EndSample();
}
if (result.Count == 0) {
Debug.LogWarning("No MeshFilters were found contained in the layers specified by the 'mask' variables");
}
Profiler.EndSample();
return result;
}
float CellHeight {
get {
// Voxel y coordinates will be stored as ushorts which have 65536 values
// Leave a margin to make sure things do not overflow
return Mathf.Max(forcedBoundsSize.y / 64000, 0.001f);
}
}
/// <summary>Convert character radius to a number of voxels</summary>
int CharacterRadiusInVoxels {
get {
// Round it up most of the time, but round it down
// if it is very close to the result when rounded down
return Mathf.CeilToInt((characterRadius / cellSize) - 0.1f);
}
}
/// <summary>
/// Number of extra voxels on each side of a tile to ensure accurate navmeshes near the tile border.
/// The width of a tile is expanded by 2 times this value (1x to the left and 1x to the right)
/// </summary>
int TileBorderSizeInVoxels {
get {
return CharacterRadiusInVoxels + 3;
}
}
float TileBorderSizeInWorldUnits {
get {
return TileBorderSizeInVoxels*cellSize;
}
}
Bounds CalculateTileBoundsWithBorder (int x, int z) {
var bounds = new Bounds();
bounds.SetMinMax(new Vector3(x*TileWorldSizeX, 0, z*TileWorldSizeZ),
new Vector3((x+1)*TileWorldSizeX, forcedBoundsSize.y, (z+1)*TileWorldSizeZ)
);
// Expand borderSize voxels on each side
bounds.Expand(new Vector3(1, 0, 1)*TileBorderSizeInWorldUnits*2);
return bounds;
}
protected NavmeshTile BuildTileMesh (Voxelize vox, int x, int z, int threadIndex = 0) {
vox.borderSize = TileBorderSizeInVoxels;
vox.forcedBounds = CalculateTileBoundsWithBorder(x, z);
vox.width = tileSizeX + vox.borderSize*2;
vox.depth = tileSizeZ + vox.borderSize*2;
if (!useTiles && relevantGraphSurfaceMode == RelevantGraphSurfaceMode.OnlyForCompletelyInsideTile) {
// This best reflects what the user would actually want
vox.relevantGraphSurfaceMode = RelevantGraphSurfaceMode.RequireForAll;
} else {
vox.relevantGraphSurfaceMode = relevantGraphSurfaceMode;
}
vox.minRegionSize = Mathf.RoundToInt(minRegionSize / (cellSize*cellSize));
// Init voxelizer
vox.Init();
vox.VoxelizeInput(transform, CalculateTileBoundsWithBorder(x, z));
vox.FilterLedges(vox.voxelWalkableHeight, vox.voxelWalkableClimb, vox.cellSize, vox.cellHeight);
vox.FilterLowHeightSpans(vox.voxelWalkableHeight, vox.cellSize, vox.cellHeight);
vox.BuildCompactField();
vox.BuildVoxelConnections();
vox.ErodeWalkableArea(CharacterRadiusInVoxels);
vox.BuildDistanceField();
vox.BuildRegions();
var cset = new VoxelContourSet();
vox.BuildContours(contourMaxError, 1, cset, Voxelize.RC_CONTOUR_TESS_WALL_EDGES | Voxelize.RC_CONTOUR_TESS_TILE_EDGES);
VoxelMesh mesh;
vox.BuildPolyMesh(cset, 3, out mesh);
// Position the vertices correctly in graph space (all tiles are laid out on the xz plane with the (0,0) tile at the origin)
for (int i = 0; i < mesh.verts.Length; i++) {
mesh.verts[i] *= Int3.Precision;
}
vox.transformVoxel2Graph.Transform(mesh.verts);
NavmeshTile tile = CreateTile(vox, mesh, x, z, threadIndex);
return tile;
}
/// <summary>
/// Create a tile at tile index x, z from the mesh.
/// Version: Since version 3.7.6 the implementation is thread safe
/// </summary>
NavmeshTile CreateTile (Voxelize vox, VoxelMesh mesh, int x, int z, int threadIndex) {
if (mesh.tris == null) throw new System.ArgumentNullException("mesh.tris");
if (mesh.verts == null) throw new System.ArgumentNullException("mesh.verts");
if (mesh.tris.Length % 3 != 0) throw new System.ArgumentException("Indices array's length must be a multiple of 3 (mesh.tris)");
if (mesh.verts.Length >= VertexIndexMask) {
if (tileXCount*tileZCount == 1) {
throw new System.ArgumentException("Too many vertices per tile (more than " + VertexIndexMask + ")." +
"\n<b>Try enabling tiling in the recast graph settings.</b>\n");
} else {
throw new System.ArgumentException("Too many vertices per tile (more than " + VertexIndexMask + ")." +
"\n<b>Try reducing tile size or enabling ASTAR_RECAST_LARGER_TILES under the 'Optimizations' tab in the A* Inspector</b>");
}
}
// Create a new navmesh tile and assign its settings
var tile = new NavmeshTile {
x = x,
z = z,
w = 1,
d = 1,
tris = mesh.tris,
bbTree = new BBTree(),
graph = this,
};
tile.vertsInGraphSpace = Utility.RemoveDuplicateVertices(mesh.verts, tile.tris);
tile.verts = (Int3[])tile.vertsInGraphSpace.Clone();
transform.Transform(tile.verts);
// Here we are faking a new graph
// The tile is not added to any graphs yet, but to get the position queries from the nodes
// to work correctly (not throw exceptions because the tile is not calculated) we fake a new graph
// and direct the position queries directly to the tile
// The thread index is added to make sure that if multiple threads are calculating tiles at the same time
// they will not use the same temporary graph index
uint temporaryGraphIndex = (uint)(active.data.graphs.Length + threadIndex);
if (temporaryGraphIndex > GraphNode.MaxGraphIndex) {
// Multithreaded tile calculations use fake graph indices, see above.
throw new System.Exception("Graph limit reached. Multithreaded recast calculations cannot be done because a few scratch graph indices are required.");
}
TriangleMeshNode.SetNavmeshHolder((int)temporaryGraphIndex, tile);
// We need to lock here because creating nodes is not thread safe
// and we may be doing this from multiple threads at the same time
tile.nodes = new TriangleMeshNode[tile.tris.Length/3];
lock (active) {
CreateNodes(tile.nodes, tile.tris, x + z*tileXCount, temporaryGraphIndex);
}
tile.bbTree.RebuildFrom(tile.nodes);
CreateNodeConnections(tile.nodes);
// Remove the fake graph
TriangleMeshNode.SetNavmeshHolder((int)temporaryGraphIndex, null);
return tile;
}
protected override void DeserializeSettingsCompatibility (GraphSerializationContext ctx) {
base.DeserializeSettingsCompatibility(ctx);
characterRadius = ctx.reader.ReadSingle();
contourMaxError = ctx.reader.ReadSingle();
cellSize = ctx.reader.ReadSingle();
ctx.reader.ReadSingle(); // Backwards compatibility, cellHeight was previously read here
walkableHeight = ctx.reader.ReadSingle();
maxSlope = ctx.reader.ReadSingle();
maxEdgeLength = ctx.reader.ReadSingle();
editorTileSize = ctx.reader.ReadInt32();
tileSizeX = ctx.reader.ReadInt32();
nearestSearchOnlyXZ = ctx.reader.ReadBoolean();
useTiles = ctx.reader.ReadBoolean();
relevantGraphSurfaceMode = (RelevantGraphSurfaceMode)ctx.reader.ReadInt32();
rasterizeColliders = ctx.reader.ReadBoolean();
rasterizeMeshes = ctx.reader.ReadBoolean();
rasterizeTerrain = ctx.reader.ReadBoolean();
rasterizeTrees = ctx.reader.ReadBoolean();
colliderRasterizeDetail = ctx.reader.ReadSingle();
forcedBoundsCenter = ctx.DeserializeVector3();
forcedBoundsSize = ctx.DeserializeVector3();
mask = ctx.reader.ReadInt32();
int count = ctx.reader.ReadInt32();
tagMask = new List<string>(count);
for (int i = 0; i < count; i++) {
tagMask.Add(ctx.reader.ReadString());
}
showMeshOutline = ctx.reader.ReadBoolean();
showNodeConnections = ctx.reader.ReadBoolean();
terrainSampleSize = ctx.reader.ReadInt32();
// These were originally forgotten but added in an upgrade
// To keep backwards compatibility, they are only deserialized
// If they exist in the streamed data
walkableClimb = ctx.DeserializeFloat(walkableClimb);
minRegionSize = ctx.DeserializeFloat(minRegionSize);
// Make the world square if this value is not in the stream
tileSizeZ = ctx.DeserializeInt(tileSizeX);
showMeshSurface = ctx.reader.ReadBoolean();
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f840a2329e5964f00be5080879bae016
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e9fd53d051f6d4130872d5c2244b2fc6

View File

@@ -0,0 +1,547 @@
//#define ASTARDEBUG //"BBTree Debug" If enables, some queries to the tree will show debug lines. Turn off multithreading when using this since DrawLine calls cannot be called from a different thread
using System;
using UnityEngine;
namespace Pathfinding {
using Pathfinding.Util;
/// <summary>
/// Axis Aligned Bounding Box Tree.
/// Holds a bounding box tree of triangles.
/// </summary>
public class BBTree : IAstarPooledObject {
/// <summary>Holds all tree nodes</summary>
BBTreeBox[] tree = null;
TriangleMeshNode[] nodeLookup = null;
int count;
int leafNodes;
const int MaximumLeafSize = 4;
public Rect Size {
get {
if (count == 0) {
return new Rect(0, 0, 0, 0);
} else {
var rect = tree[0].rect;
return Rect.MinMaxRect(rect.xmin*Int3.PrecisionFactor, rect.ymin*Int3.PrecisionFactor, rect.xmax*Int3.PrecisionFactor, rect.ymax*Int3.PrecisionFactor);
}
}
}
/// <summary>
/// Clear the tree.
/// Note that references to old nodes will still be intact so the GC cannot immediately collect them.
/// </summary>
public void Clear () {
count = 0;
leafNodes = 0;
if (tree != null) ArrayPool<BBTreeBox>.Release(ref tree);
if (nodeLookup != null) {
// Prevent memory leaks as the pool does not clear the array
for (int i = 0; i < nodeLookup.Length; i++) nodeLookup[i] = null;
ArrayPool<TriangleMeshNode>.Release(ref nodeLookup);
}
tree = ArrayPool<BBTreeBox>.Claim(0);
nodeLookup = ArrayPool<TriangleMeshNode>.Claim(0);
}
void IAstarPooledObject.OnEnterPool () {
Clear();
}
void EnsureCapacity (int c) {
if (c > tree.Length) {
var newArr = ArrayPool<BBTreeBox>.Claim(c);
tree.CopyTo(newArr, 0);
ArrayPool<BBTreeBox>.Release(ref tree);
tree = newArr;
}
}
void EnsureNodeCapacity (int c) {
if (c > nodeLookup.Length) {
var newArr = ArrayPool<TriangleMeshNode>.Claim(c);
nodeLookup.CopyTo(newArr, 0);
ArrayPool<TriangleMeshNode>.Release(ref nodeLookup);
nodeLookup = newArr;
}
}
int GetBox (IntRect rect) {
if (count >= tree.Length) EnsureCapacity(count+1);
tree[count] = new BBTreeBox(rect);
count++;
return count-1;
}
/// <summary>Rebuilds the tree using the specified nodes</summary>
public void RebuildFrom (TriangleMeshNode[] nodes) {
Clear();
if (nodes.Length == 0) return;
// We will use approximately 2N tree nodes
EnsureCapacity(Mathf.CeilToInt(nodes.Length * 2.1f));
// We will use approximately N node references
EnsureNodeCapacity(Mathf.CeilToInt(nodes.Length * 1.1f));
// This will store the order of the nodes while the tree is being built
// It turns out that it is a lot faster to do this than to actually modify
// the nodes and nodeBounds arrays (presumably since that involves shuffling
// around 20 bytes of memory (sizeof(pointer) + sizeof(IntRect)) per node
// instead of 4 bytes (sizeof(int)).
// It also means we don't have to make a copy of the nodes array since
// we do not modify it
var permutation = ArrayPool<int>.Claim(nodes.Length);
for (int i = 0; i < nodes.Length; i++) {
permutation[i] = i;
}
// Precalculate the bounds of the nodes in XZ space.
// It turns out that calculating the bounds is a bottleneck and precalculating
// the bounds makes it around 3 times faster to build a tree
var nodeBounds = ArrayPool<IntRect>.Claim(nodes.Length);
for (int i = 0; i < nodes.Length; i++) {
Int3 v0, v1, v2;
nodes[i].GetVertices(out v0, out v1, out v2);
var rect = new IntRect(v0.x, v0.z, v0.x, v0.z);
rect = rect.ExpandToContain(v1.x, v1.z);
rect = rect.ExpandToContain(v2.x, v2.z);
nodeBounds[i] = rect;
}
RebuildFromInternal(nodes, permutation, nodeBounds, 0, nodes.Length, false);
ArrayPool<int>.Release(ref permutation);
ArrayPool<IntRect>.Release(ref nodeBounds);
}
static int SplitByX (TriangleMeshNode[] nodes, int[] permutation, int from, int to, int divider) {
int mx = to;
for (int i = from; i < mx; i++) {
if (nodes[permutation[i]].position.x > divider) {
mx--;
// Swap items i and mx
var tmp = permutation[mx];
permutation[mx] = permutation[i];
permutation[i] = tmp;
i--;
}
}
return mx;
}
static int SplitByZ (TriangleMeshNode[] nodes, int[] permutation, int from, int to, int divider) {
int mx = to;
for (int i = from; i < mx; i++) {
if (nodes[permutation[i]].position.z > divider) {
mx--;
// Swap items i and mx
var tmp = permutation[mx];
permutation[mx] = permutation[i];
permutation[i] = tmp;
i--;
}
}
return mx;
}
int RebuildFromInternal (TriangleMeshNode[] nodes, int[] permutation, IntRect[] nodeBounds, int from, int to, bool odd) {
var rect = NodeBounds(permutation, nodeBounds, from, to);
int box = GetBox(rect);
if (to - from <= MaximumLeafSize) {
var nodeOffset = tree[box].nodeOffset = leafNodes*MaximumLeafSize;
EnsureNodeCapacity(nodeOffset + MaximumLeafSize);
leafNodes++;
// Assign all nodes to the array. Note that we also need clear unused slots as the array from the pool may contain any information
for (int i = 0; i < MaximumLeafSize; i++) {
nodeLookup[nodeOffset + i] = i < to - from ? nodes[permutation[from + i]] : null;
}
return box;
}
int splitIndex;
if (odd) {
// X
int divider = (rect.xmin + rect.xmax)/2;
splitIndex = SplitByX(nodes, permutation, from, to, divider);
} else {
// Y/Z
int divider = (rect.ymin + rect.ymax)/2;
splitIndex = SplitByZ(nodes, permutation, from, to, divider);
}
if (splitIndex == from || splitIndex == to) {
// All nodes were on one side of the divider
// Try to split along the other axis
if (!odd) {
// X
int divider = (rect.xmin + rect.xmax)/2;
splitIndex = SplitByX(nodes, permutation, from, to, divider);
} else {
// Y/Z
int divider = (rect.ymin + rect.ymax)/2;
splitIndex = SplitByZ(nodes, permutation, from, to, divider);
}
if (splitIndex == from || splitIndex == to) {
// All nodes were on one side of the divider
// Just pick one half
splitIndex = (from+to)/2;
}
}
tree[box].left = RebuildFromInternal(nodes, permutation, nodeBounds, from, splitIndex, !odd);
tree[box].right = RebuildFromInternal(nodes, permutation, nodeBounds, splitIndex, to, !odd);
return box;
}
/// <summary>Calculates the bounding box in XZ space of all nodes between from (inclusive) and to (exclusive)</summary>
static IntRect NodeBounds (int[] permutation, IntRect[] nodeBounds, int from, int to) {
var rect = nodeBounds[permutation[from]];
for (int j = from + 1; j < to; j++) {
var otherRect = nodeBounds[permutation[j]];
// Equivalent to rect = IntRect.Union(rect, otherRect)
// but manually inlining is approximately
// 25% faster when building an entire tree.
// This code is hot when using navmesh cutting.
rect.xmin = Math.Min(rect.xmin, otherRect.xmin);
rect.ymin = Math.Min(rect.ymin, otherRect.ymin);
rect.xmax = Math.Max(rect.xmax, otherRect.xmax);
rect.ymax = Math.Max(rect.ymax, otherRect.ymax);
}
return rect;
}
[System.Diagnostics.Conditional("ASTARDEBUG")]
static void DrawDebugRect (IntRect rect) {
Debug.DrawLine(new Vector3(rect.xmin, 0, rect.ymin), new Vector3(rect.xmax, 0, rect.ymin), Color.white);
Debug.DrawLine(new Vector3(rect.xmin, 0, rect.ymax), new Vector3(rect.xmax, 0, rect.ymax), Color.white);
Debug.DrawLine(new Vector3(rect.xmin, 0, rect.ymin), new Vector3(rect.xmin, 0, rect.ymax), Color.white);
Debug.DrawLine(new Vector3(rect.xmax, 0, rect.ymin), new Vector3(rect.xmax, 0, rect.ymax), Color.white);
}
[System.Diagnostics.Conditional("ASTARDEBUG")]
static void DrawDebugNode (TriangleMeshNode node, float yoffset, Color color) {
Debug.DrawLine((Vector3)node.GetVertex(1) + Vector3.up*yoffset, (Vector3)node.GetVertex(2) + Vector3.up*yoffset, color);
Debug.DrawLine((Vector3)node.GetVertex(0) + Vector3.up*yoffset, (Vector3)node.GetVertex(1) + Vector3.up*yoffset, color);
Debug.DrawLine((Vector3)node.GetVertex(2) + Vector3.up*yoffset, (Vector3)node.GetVertex(0) + Vector3.up*yoffset, color);
}
/// <summary>
/// Queries the tree for the closest node to p constrained by the NNConstraint.
/// Note that this function will only fill in the constrained node.
/// If you want a node not constrained by any NNConstraint, do an additional search with constraint = NNConstraint.None
/// </summary>
public NNInfoInternal QueryClosest (Vector3 p, NNConstraint constraint, out float distance) {
distance = float.PositiveInfinity;
return QueryClosest(p, constraint, ref distance, new NNInfoInternal(null));
}
/// <summary>
/// Queries the tree for the closest node to p constrained by the NNConstraint trying to improve an existing solution.
/// Note that this function will only fill in the constrained node.
/// If you want a node not constrained by any NNConstraint, do an additional search with constraint = NNConstraint.None
///
/// This method will completely ignore any Y-axis differences in positions.
/// </summary>
/// <param name="p">Point to search around</param>
/// <param name="constraint">Optionally set to constrain which nodes to return</param>
/// <param name="distance">The best distance for the previous solution. Will be updated with the best distance
/// after this search. Will be positive infinity if no node could be found.
/// Set to positive infinity if there was no previous solution.</param>
/// <param name="previous">This search will start from the previous NNInfo and improve it if possible.
/// Even if the search fails on this call, the solution will never be worse than previous.
/// Note that the distance parameter need to be configured with the distance for the previous result
/// otherwise it may get overwritten even though it was actually closer.</param>
public NNInfoInternal QueryClosestXZ (Vector3 p, NNConstraint constraint, ref float distance, NNInfoInternal previous) {
var sqrDistance = distance*distance;
var origSqrDistance = sqrDistance;
if (count > 0 && SquaredRectPointDistance(tree[0].rect, p) < sqrDistance) {
SearchBoxClosestXZ(0, p, ref sqrDistance, constraint, ref previous);
// Only update the distance if the squared distance changed as otherwise #distance
// might change due to rounding errors even if no better solution was found
if (sqrDistance < origSqrDistance) distance = Mathf.Sqrt(sqrDistance);
}
return previous;
}
void SearchBoxClosestXZ (int boxi, Vector3 p, ref float closestSqrDist, NNConstraint constraint, ref NNInfoInternal nnInfo) {
BBTreeBox box = tree[boxi];
if (box.IsLeaf) {
var nodes = nodeLookup;
for (int i = 0; i < MaximumLeafSize && nodes[box.nodeOffset+i] != null; i++) {
var node = nodes[box.nodeOffset+i];
// Update the NNInfo
DrawDebugNode(node, 0.2f, Color.red);
if (constraint == null || constraint.Suitable(node)) {
Vector3 closest = node.ClosestPointOnNodeXZ(p);
// XZ squared distance
float dist = (closest.x-p.x)*(closest.x-p.x)+(closest.z-p.z)*(closest.z-p.z);
// There's a theoretical case when the closest point is on the edge of a node which may cause the
// closest point's xz coordinates to not line up perfectly with p's xz coordinates even though they should
// (because floating point errors are annoying). So use a tiny margin to cover most of those cases.
const float fuzziness = 0.000001f;
if (nnInfo.constrainedNode == null || dist < closestSqrDist - fuzziness || (dist <= closestSqrDist + fuzziness && Mathf.Abs(closest.y - p.y) < Mathf.Abs(nnInfo.constClampedPosition.y - p.y))) {
nnInfo.constrainedNode = node;
nnInfo.constClampedPosition = closest;
closestSqrDist = dist;
}
}
}
} else {
DrawDebugRect(box.rect);
int first = box.left, second = box.right;
float firstDist, secondDist;
GetOrderedChildren(ref first, ref second, out firstDist, out secondDist, p);
// Search children (closest box first to improve performance)
if (firstDist <= closestSqrDist) {
SearchBoxClosestXZ(first, p, ref closestSqrDist, constraint, ref nnInfo);
}
if (secondDist <= closestSqrDist) {
SearchBoxClosestXZ(second, p, ref closestSqrDist, constraint, ref nnInfo);
}
}
}
/// <summary>
/// Queries the tree for the closest node to p constrained by the NNConstraint trying to improve an existing solution.
/// Note that this function will only fill in the constrained node.
/// If you want a node not constrained by any NNConstraint, do an additional search with constraint = NNConstraint.None
/// </summary>
/// <param name="p">Point to search around</param>
/// <param name="constraint">Optionally set to constrain which nodes to return</param>
/// <param name="distance">The best distance for the previous solution. Will be updated with the best distance
/// after this search. Will be positive infinity if no node could be found.
/// Set to positive infinity if there was no previous solution.</param>
/// <param name="previous">This search will start from the previous NNInfo and improve it if possible.
/// Even if the search fails on this call, the solution will never be worse than previous.</param>
public NNInfoInternal QueryClosest (Vector3 p, NNConstraint constraint, ref float distance, NNInfoInternal previous) {
var sqrDistance = distance*distance;
var origSqrDistance = sqrDistance;
if (count > 0 && SquaredRectPointDistance(tree[0].rect, p) < sqrDistance) {
SearchBoxClosest(0, p, ref sqrDistance, constraint, ref previous);
// Only update the distance if the squared distance changed as otherwise #distance
// might change due to rounding errors even if no better solution was found
if (sqrDistance < origSqrDistance) distance = Mathf.Sqrt(sqrDistance);
}
return previous;
}
void SearchBoxClosest (int boxi, Vector3 p, ref float closestSqrDist, NNConstraint constraint, ref NNInfoInternal nnInfo) {
BBTreeBox box = tree[boxi];
if (box.IsLeaf) {
var nodes = nodeLookup;
for (int i = 0; i < MaximumLeafSize && nodes[box.nodeOffset+i] != null; i++) {
var node = nodes[box.nodeOffset+i];
Vector3 closest = node.ClosestPointOnNode(p);
float dist = (closest-p).sqrMagnitude;
if (dist < closestSqrDist) {
DrawDebugNode(node, 0.2f, Color.red);
if (constraint == null || constraint.Suitable(node)) {
// Update the NNInfo
nnInfo.constrainedNode = node;
nnInfo.constClampedPosition = closest;
closestSqrDist = dist;
}
} else {
DrawDebugNode(node, 0.0f, Color.blue);
}
}
} else {
DrawDebugRect(box.rect);
int first = box.left, second = box.right;
float firstDist, secondDist;
GetOrderedChildren(ref first, ref second, out firstDist, out secondDist, p);
// Search children (closest box first to improve performance)
if (firstDist < closestSqrDist) {
SearchBoxClosest(first, p, ref closestSqrDist, constraint, ref nnInfo);
}
if (secondDist < closestSqrDist) {
SearchBoxClosest(second, p, ref closestSqrDist, constraint, ref nnInfo);
}
}
}
/// <summary>Orders the box indices first and second by the approximate distance to the point p</summary>
void GetOrderedChildren (ref int first, ref int second, out float firstDist, out float secondDist, Vector3 p) {
firstDist = SquaredRectPointDistance(tree[first].rect, p);
secondDist = SquaredRectPointDistance(tree[second].rect, p);
if (secondDist < firstDist) {
// Swap
var tmp = first;
first = second;
second = tmp;
var tmp2 = firstDist;
firstDist = secondDist;
secondDist = tmp2;
}
}
/// <summary>
/// Searches for a node which contains the specified point.
/// If there are multiple nodes that contain the point any one of them
/// may be returned.
///
/// See: TriangleMeshNode.ContainsPoint
/// </summary>
public TriangleMeshNode QueryInside (Vector3 p, NNConstraint constraint) {
return count != 0 && tree[0].Contains(p) ? SearchBoxInside(0, p, constraint) : null;
}
TriangleMeshNode SearchBoxInside (int boxi, Vector3 p, NNConstraint constraint) {
BBTreeBox box = tree[boxi];
if (box.IsLeaf) {
var nodes = nodeLookup;
for (int i = 0; i < MaximumLeafSize && nodes[box.nodeOffset+i] != null; i++) {
var node = nodes[box.nodeOffset+i];
if (node.ContainsPoint((Int3)p)) {
DrawDebugNode(node, 0.2f, Color.red);
if (constraint == null || constraint.Suitable(node)) {
return node;
}
} else {
DrawDebugNode(node, 0.0f, Color.blue);
}
}
} else {
DrawDebugRect(box.rect);
//Search children
if (tree[box.left].Contains(p)) {
var result = SearchBoxInside(box.left, p, constraint);
if (result != null) return result;
}
if (tree[box.right].Contains(p)) {
var result = SearchBoxInside(box.right, p, constraint);
if (result != null) return result;
}
}
return null;
}
struct BBTreeBox {
public IntRect rect;
public int nodeOffset;
public int left, right;
public bool IsLeaf {
get {
return nodeOffset >= 0;
}
}
public BBTreeBox (IntRect rect) {
nodeOffset = -1;
this.rect = rect;
left = right = -1;
}
public BBTreeBox (int nodeOffset, IntRect rect) {
this.nodeOffset = nodeOffset;
this.rect = rect;
left = right = -1;
}
public bool Contains (Vector3 point) {
var pi = (Int3)point;
return rect.Contains(pi.x, pi.z);
}
}
public void OnDrawGizmos () {
Gizmos.color = new Color(1, 1, 1, 0.5F);
if (count == 0) return;
OnDrawGizmos(0, 0);
}
void OnDrawGizmos (int boxi, int depth) {
BBTreeBox box = tree[boxi];
var min = (Vector3) new Int3(box.rect.xmin, 0, box.rect.ymin);
var max = (Vector3) new Int3(box.rect.xmax, 0, box.rect.ymax);
Vector3 center = (min+max)*0.5F;
Vector3 size = (max-center)*2;
size = new Vector3(size.x, 1, size.z);
center.y += depth * 2;
Gizmos.color = AstarMath.IntToColor(depth, 1f);
Gizmos.DrawCube(center, size);
if (!box.IsLeaf) {
OnDrawGizmos(box.left, depth + 1);
OnDrawGizmos(box.right, depth + 1);
}
}
static bool NodeIntersectsCircle (TriangleMeshNode node, Vector3 p, float radius) {
if (float.IsPositiveInfinity(radius)) return true;
/// <summary>Bug: Is not correct on the Y axis</summary>
return (p - node.ClosestPointOnNode(p)).sqrMagnitude < radius*radius;
}
/// <summary>
/// Returns true if p is within radius from r.
/// Correctly handles cases where radius is positive infinity.
/// </summary>
static bool RectIntersectsCircle (IntRect r, Vector3 p, float radius) {
if (float.IsPositiveInfinity(radius)) return true;
Vector3 po = p;
p.x = Math.Max(p.x, r.xmin*Int3.PrecisionFactor);
p.x = Math.Min(p.x, r.xmax*Int3.PrecisionFactor);
p.z = Math.Max(p.z, r.ymin*Int3.PrecisionFactor);
p.z = Math.Min(p.z, r.ymax*Int3.PrecisionFactor);
// XZ squared magnitude comparison
return (p.x-po.x)*(p.x-po.x) + (p.z-po.z)*(p.z-po.z) < radius*radius;
}
/// <summary>Returns distance from p to the rectangle r</summary>
static float SquaredRectPointDistance (IntRect r, Vector3 p) {
Vector3 po = p;
p.x = Math.Max(p.x, r.xmin*Int3.PrecisionFactor);
p.x = Math.Min(p.x, r.xmax*Int3.PrecisionFactor);
p.z = Math.Max(p.z, r.ymin*Int3.PrecisionFactor);
p.z = Math.Min(p.z, r.ymax*Int3.PrecisionFactor);
// XZ squared magnitude comparison
return (p.x-po.x)*(p.x-po.x) + (p.z-po.z)*(p.z-po.z);
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 3a20480c673fd40a5bd2a4cc2206dbc4
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,440 @@
#pragma warning disable 414
using System.Collections.Generic;
using UnityEngine;
namespace Pathfinding {
public enum HeuristicOptimizationMode {
None,
Random,
RandomSpreadOut,
Custom
}
/// <summary>
/// Implements heuristic optimizations.
///
/// See: heuristic-opt
/// See: Game AI Pro - Pathfinding Architecture Optimizations by Steve Rabin and Nathan R. Sturtevant
/// </summary>
[System.Serializable]
public class EuclideanEmbedding {
/// <summary>
/// If heuristic optimization should be used and how to place the pivot points.
/// See: heuristic-opt
/// See: Game AI Pro - Pathfinding Architecture Optimizations by Steve Rabin and Nathan R. Sturtevant
/// </summary>
public HeuristicOptimizationMode mode;
public int seed;
/// <summary>All children of this transform will be used as pivot points</summary>
public Transform pivotPointRoot;
public int spreadOutCount = 1;
[System.NonSerialized]
public bool dirty;
/// <summary>
/// Costs laid out as n*[int],n*[int],n*[int] where n is the number of pivot points.
/// Each node has n integers which is the cost from that node to the pivot node.
/// They are at around the same place in the array for simplicity and for cache locality.
///
/// cost(nodeIndex, pivotIndex) = costs[nodeIndex*pivotCount+pivotIndex]
/// </summary>
uint[] costs = new uint[8];
int maxNodeIndex;
int pivotCount;
GraphNode[] pivots;
/*
* Seed for random number generator.
* Must not be zero
*/
const uint ra = 12820163;
/*
* Seed for random number generator.
* Must not be zero
*/
const uint rc = 1140671485;
/*
* Parameter for random number generator.
*/
uint rval;
System.Object lockObj = new object();
/// <summary>
/// Simple linear congruential generator.
/// See: http://en.wikipedia.org/wiki/Linear_congruential_generator
/// </summary>
uint GetRandom () {
rval = (ra*rval + rc);
return rval;
}
void EnsureCapacity (int index) {
if (index > maxNodeIndex) {
lock (lockObj) {
if (index > maxNodeIndex) {
if (index >= costs.Length) {
var newCosts = new uint[System.Math.Max(index*2, pivots.Length*2)];
for (int i = 0; i < costs.Length; i++) newCosts[i] = costs[i];
costs = newCosts;
}
maxNodeIndex = index;
}
}
}
}
public uint GetHeuristic (int nodeIndex1, int nodeIndex2) {
nodeIndex1 *= pivotCount;
nodeIndex2 *= pivotCount;
if (nodeIndex1 >= costs.Length || nodeIndex2 >= costs.Length) {
EnsureCapacity(nodeIndex1 > nodeIndex2 ? nodeIndex1 : nodeIndex2);
}
uint mx = 0;
for (int i = 0; i < pivotCount; i++) {
uint d = (uint)System.Math.Abs((int)costs[nodeIndex1+i] - (int)costs[nodeIndex2+i]);
if (d > mx) mx = d;
}
return mx;
}
void GetClosestWalkableNodesToChildrenRecursively (Transform tr, List<GraphNode> nodes) {
foreach (Transform ch in tr) {
var info = AstarPath.active.GetNearest(ch.position, NNConstraint.Walkable);
if (info.node != null && info.node.Walkable) {
nodes.Add(info.node);
}
GetClosestWalkableNodesToChildrenRecursively(ch, nodes);
}
}
/// <summary>
/// Pick N random walkable nodes from all nodes in all graphs and add them to the buffer.
///
/// Here we select N random nodes from a stream of nodes.
/// Probability of choosing the first N nodes is 1
/// Probability of choosing node i is min(N/i,1)
/// A selected node will replace a random node of the previously
/// selected ones.
///
/// See: https://en.wikipedia.org/wiki/Reservoir_sampling
/// </summary>
void PickNRandomNodes (int count, List<GraphNode> buffer) {
int n = 0;
var graphs = AstarPath.active.graphs;
// Loop through all graphs
for (int j = 0; j < graphs.Length; j++) {
// Loop through all nodes in the graph
graphs[j].GetNodes(node => {
if (!node.Destroyed && node.Walkable) {
n++;
if ((GetRandom() % n) < count) {
if (buffer.Count < count) {
buffer.Add(node);
} else {
buffer[(int)(GetRandom()%buffer.Count)] = node;
}
}
}
});
}
}
GraphNode PickAnyWalkableNode () {
var graphs = AstarPath.active.graphs;
GraphNode first = null;
// Find any node in the graphs
for (int j = 0; j < graphs.Length; j++) {
graphs[j].GetNodes(node => {
if (node != null && node.Walkable && first == null) {
first = node;
}
});
}
return first;
}
public void RecalculatePivots () {
if (mode == HeuristicOptimizationMode.None) {
pivotCount = 0;
pivots = null;
return;
}
// Reset the random number generator
rval = (uint)seed;
// Get a List<GraphNode> from a pool
var pivotList = Pathfinding.Util.ListPool<GraphNode>.Claim();
switch (mode) {
case HeuristicOptimizationMode.Custom:
if (pivotPointRoot == null) throw new System.Exception("heuristicOptimizationMode is HeuristicOptimizationMode.Custom, " +
"but no 'customHeuristicOptimizationPivotsRoot' is set");
GetClosestWalkableNodesToChildrenRecursively(pivotPointRoot, pivotList);
break;
case HeuristicOptimizationMode.Random:
PickNRandomNodes(spreadOutCount, pivotList);
break;
case HeuristicOptimizationMode.RandomSpreadOut:
if (pivotPointRoot != null) {
GetClosestWalkableNodesToChildrenRecursively(pivotPointRoot, pivotList);
}
// If no pivot points were found, fall back to picking arbitrary nodes
if (pivotList.Count == 0) {
GraphNode first = PickAnyWalkableNode();
if (first != null) {
pivotList.Add(first);
} else {
Debug.LogError("Could not find any walkable node in any of the graphs.");
Pathfinding.Util.ListPool<GraphNode>.Release(ref pivotList);
return;
}
}
// Fill remaining slots with null
int toFill = spreadOutCount - pivotList.Count;
for (int i = 0; i < toFill; i++) pivotList.Add(null);
break;
default:
throw new System.Exception("Invalid HeuristicOptimizationMode: " + mode);
}
pivots = pivotList.ToArray();
Pathfinding.Util.ListPool<GraphNode>.Release(ref pivotList);
}
public void RecalculateCosts () {
if (pivots == null) RecalculatePivots();
if (mode == HeuristicOptimizationMode.None) return;
pivotCount = 0;
for (int i = 0; i < pivots.Length; i++) {
if (pivots[i] != null && (pivots[i].Destroyed || !pivots[i].Walkable)) {
throw new System.Exception("Invalid pivot nodes (destroyed or unwalkable)");
}
}
if (mode != HeuristicOptimizationMode.RandomSpreadOut)
for (int i = 0; i < pivots.Length; i++)
if (pivots[i] == null)
throw new System.Exception("Invalid pivot nodes (null)");
Debug.Log("Recalculating costs...");
pivotCount = pivots.Length;
System.Action<int> startCostCalculation = null;
int numComplete = 0;
OnPathDelegate onComplete = (Path path) => {
numComplete++;
if (numComplete == pivotCount) {
// Last completed path
ApplyGridGraphEndpointSpecialCase();
}
};
startCostCalculation = (int pivotIndex) => {
GraphNode pivot = pivots[pivotIndex];
FloodPath floodPath = null;
floodPath = FloodPath.Construct(pivot, onComplete);
floodPath.immediateCallback = (Path _p) => {
// Handle path pooling
_p.Claim(this);
// When paths are calculated on navmesh based graphs
// the costs are slightly modified to match the actual target and start points
// instead of the node centers
// so we have to remove the cost for the first and last connection
// in each path
var meshNode = pivot as MeshNode;
uint costOffset = 0;
if (meshNode != null && meshNode.connections != null) {
for (int i = 0; i < meshNode.connections.Length; i++) {
costOffset = System.Math.Max(costOffset, meshNode.connections[i].cost);
}
}
var graphs = AstarPath.active.graphs;
// Process graphs in reverse order to raise probability that we encounter large NodeIndex values quicker
// to avoid resizing the internal array too often
for (int j = graphs.Length-1; j >= 0; j--) {
graphs[j].GetNodes(node => {
int idx = node.NodeIndex*pivotCount + pivotIndex;
EnsureCapacity(idx);
PathNode pn = ((IPathInternals)floodPath).PathHandler.GetPathNode(node);
if (costOffset > 0) {
costs[idx] = pn.pathID == floodPath.pathID && pn.parent != null ? System.Math.Max(pn.parent.G-costOffset, 0) : 0;
} else {
costs[idx] = pn.pathID == floodPath.pathID ? pn.G : 0;
}
});
}
if (mode == HeuristicOptimizationMode.RandomSpreadOut && pivotIndex < pivots.Length-1) {
// If the next pivot is null
// then find the node which is furthest away from the earlier
// pivot points
if (pivots[pivotIndex+1] == null) {
int best = -1;
uint bestScore = 0;
// Actual number of nodes
int totCount = maxNodeIndex/pivotCount;
// Loop through all nodes
for (int j = 1; j < totCount; j++) {
// Find the minimum distance from the node to all existing pivot points
uint mx = 1 << 30;
for (int p = 0; p <= pivotIndex; p++) mx = System.Math.Min(mx, costs[j*pivotCount + p]);
// Pick the node which has the largest minimum distance to the existing pivot points
// (i.e pick the one furthest away from the existing ones)
GraphNode node = ((IPathInternals)floodPath).PathHandler.GetPathNode(j).node;
if ((mx > bestScore || best == -1) && node != null && !node.Destroyed && node.Walkable) {
best = j;
bestScore = mx;
}
}
if (best == -1) {
Debug.LogError("Failed generating random pivot points for heuristic optimizations");
return;
}
pivots[pivotIndex+1] = ((IPathInternals)floodPath).PathHandler.GetPathNode(best).node;
}
// Start next path
startCostCalculation(pivotIndex+1);
}
// Handle path pooling
_p.Release(this);
};
AstarPath.StartPath(floodPath, true);
};
if (mode != HeuristicOptimizationMode.RandomSpreadOut) {
// All calculated in parallel
for (int i = 0; i < pivots.Length; i++) {
startCostCalculation(i);
}
} else {
// Recursive and serial
startCostCalculation(0);
}
dirty = false;
}
/// <summary>
/// Special case necessary for paths to unwalkable nodes right next to walkable nodes to be able to use good heuristics.
///
/// This will find all unwalkable nodes in all grid graphs with walkable nodes as neighbours
/// and set the cost to reach them from each of the pivots as the minimum of the cost to
/// reach the neighbours of each node.
///
/// See: ABPath.EndPointGridGraphSpecialCase
/// </summary>
void ApplyGridGraphEndpointSpecialCase () {
#if !ASTAR_NO_GRID_GRAPH
var graphs = AstarPath.active.graphs;
for (int i = 0; i < graphs.Length; i++) {
var gg = graphs[i] as GridGraph;
if (gg != null) {
// Found a grid graph
var nodes = gg.nodes;
// Number of neighbours as an int
int mxnum = gg.neighbours == NumNeighbours.Four ? 4 : (gg.neighbours == NumNeighbours.Eight ? 8 : 6);
for (int z = 0; z < gg.depth; z++) {
for (int x = 0; x < gg.width; x++) {
var node = nodes[z*gg.width + x];
if (!node.Walkable) {
var pivotIndex = node.NodeIndex*pivotCount;
// Set all costs to reach this node to maximum
for (int piv = 0; piv < pivotCount; piv++) {
costs[pivotIndex + piv] = uint.MaxValue;
}
// Loop through all potential neighbours of the node
// and set the cost to reach it as the minimum
// of the costs to reach any of the adjacent nodes
for (int d = 0; d < mxnum; d++) {
int nx, nz;
if (gg.neighbours == NumNeighbours.Six) {
// Hexagon graph
nx = x + gg.neighbourXOffsets[GridGraph.hexagonNeighbourIndices[d]];
nz = z + gg.neighbourZOffsets[GridGraph.hexagonNeighbourIndices[d]];
} else {
nx = x + gg.neighbourXOffsets[d];
nz = z + gg.neighbourZOffsets[d];
}
// Check if the position is still inside the grid
if (nx >= 0 && nz >= 0 && nx < gg.width && nz < gg.depth) {
var adjacentNode = gg.nodes[nz*gg.width + nx];
if (adjacentNode.Walkable) {
for (int piv = 0; piv < pivotCount; piv++) {
uint cost = costs[adjacentNode.NodeIndex*pivotCount + piv] + gg.neighbourCosts[d];
costs[pivotIndex + piv] = System.Math.Min(costs[pivotIndex + piv], cost);
//Debug.DrawLine((Vector3)node.position, (Vector3)adjacentNode.position, Color.blue, 1);
}
}
}
}
// If no adjacent nodes were found
// set the cost to reach it back to zero
for (int piv = 0; piv < pivotCount; piv++) {
if (costs[pivotIndex + piv] == uint.MaxValue) {
costs[pivotIndex + piv] = 0;
}
}
}
}
}
}
}
#endif
}
public void OnDrawGizmos () {
if (pivots != null) {
for (int i = 0; i < pivots.Length; i++) {
Gizmos.color = new Color(159/255.0f, 94/255.0f, 194/255.0f, 0.8f);
if (pivots[i] != null && !pivots[i].Destroyed) {
Gizmos.DrawCube((Vector3)pivots[i].position, Vector3.one);
}
}
}
}
}
}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3ac3213e3eeb14eef91939f5281682e6
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:

View File

@@ -0,0 +1,220 @@
using UnityEngine;
namespace Pathfinding.Util {
/// <summary>
/// Transforms to and from world space to a 2D movement plane.
/// The transformation is guaranteed to be purely a rotation
/// so no scale or offset is used. This interface is primarily
/// used to make it easier to write movement scripts which can
/// handle movement both in the XZ plane and in the XY plane.
///
/// See: <see cref="Pathfinding.Util.GraphTransform"/>
/// </summary>
public interface IMovementPlane {
Vector2 ToPlane(Vector3 p);
Vector2 ToPlane(Vector3 p, out float elevation);
Vector3 ToWorld(Vector2 p, float elevation = 0);
}
/// <summary>Generic 3D coordinate transformation</summary>
public interface ITransform {
Vector3 Transform(Vector3 position);
Vector3 InverseTransform(Vector3 position);
}
/// <summary>
/// Defines a transformation from graph space to world space.
/// This is essentially just a simple wrapper around a matrix, but it has several utilities that are useful.
/// </summary>
public class GraphTransform : IMovementPlane, ITransform {
/// <summary>True if this transform is the identity transform (i.e it does not do anything)</summary>
public readonly bool identity;
/// <summary>True if this transform is a pure translation without any scaling or rotation</summary>
public readonly bool onlyTranslational;
readonly bool isXY;
readonly bool isXZ;
readonly Matrix4x4 matrix;
readonly Matrix4x4 inverseMatrix;
readonly Vector3 up;
readonly Vector3 translation;
readonly Int3 i3translation;
readonly Quaternion rotation;
readonly Quaternion inverseRotation;
public static readonly GraphTransform identityTransform = new GraphTransform(Matrix4x4.identity);
public GraphTransform (Matrix4x4 matrix) {
this.matrix = matrix;
inverseMatrix = matrix.inverse;
identity = matrix.isIdentity;
onlyTranslational = MatrixIsTranslational(matrix);
up = matrix.MultiplyVector(Vector3.up).normalized;
translation = matrix.MultiplyPoint3x4(Vector3.zero);
i3translation = (Int3)translation;
// Extract the rotation from the matrix. This is only correct if the matrix has no skew, but we only
// want to use it for the movement plane so as long as the Up axis is parpendicular to the Forward
// axis everything should be ok. In fact the only case in the project when all three axes are not
// perpendicular is when hexagon or isometric grid graphs are used, but in those cases only the
// X and Z axes are not perpendicular.
rotation = Quaternion.LookRotation(TransformVector(Vector3.forward), TransformVector(Vector3.up));
inverseRotation = Quaternion.Inverse(rotation);
// Some short circuiting code for the movement plane calculations
isXY = rotation == Quaternion.Euler(-90, 0, 0);
isXZ = rotation == Quaternion.Euler(0, 0, 0);
}
public Vector3 WorldUpAtGraphPosition (Vector3 point) {
return up;
}
static bool MatrixIsTranslational (Matrix4x4 matrix) {
return matrix.GetColumn(0) == new Vector4(1, 0, 0, 0) && matrix.GetColumn(1) == new Vector4(0, 1, 0, 0) && matrix.GetColumn(2) == new Vector4(0, 0, 1, 0) && matrix.m33 == 1;
}
public Vector3 Transform (Vector3 point) {
if (onlyTranslational) return point + translation;
return matrix.MultiplyPoint3x4(point);
}
public Vector3 TransformVector (Vector3 point) {
if (onlyTranslational) return point;
return matrix.MultiplyVector(point);
}
public void Transform (Int3[] arr) {
if (onlyTranslational) {
for (int i = arr.Length - 1; i >= 0; i--) arr[i] += i3translation;
} else {
for (int i = arr.Length - 1; i >= 0; i--) arr[i] = (Int3)matrix.MultiplyPoint3x4((Vector3)arr[i]);
}
}
public void Transform (Vector3[] arr) {
if (onlyTranslational) {
for (int i = arr.Length - 1; i >= 0; i--) arr[i] += translation;
} else {
for (int i = arr.Length - 1; i >= 0; i--) arr[i] = matrix.MultiplyPoint3x4(arr[i]);
}
}
public Vector3 InverseTransform (Vector3 point) {
if (onlyTranslational) return point - translation;
return inverseMatrix.MultiplyPoint3x4(point);
}
public Vector3 InverseTransformVector (Vector3 dir) {
if (onlyTranslational) return dir;
return inverseMatrix.MultiplyVector(dir);
}
public Int3 InverseTransform (Int3 point) {
if (onlyTranslational) return point - i3translation;
return (Int3)inverseMatrix.MultiplyPoint3x4((Vector3)point);
}
public void InverseTransform (Int3[] arr) {
for (int i = arr.Length - 1; i >= 0; i--) arr[i] = (Int3)inverseMatrix.MultiplyPoint3x4((Vector3)arr[i]);
}
public static GraphTransform operator * (GraphTransform lhs, Matrix4x4 rhs) {
return new GraphTransform(lhs.matrix * rhs);
}
public static GraphTransform operator * (Matrix4x4 lhs, GraphTransform rhs) {
return new GraphTransform(lhs * rhs.matrix);
}
public Bounds Transform (Bounds bounds) {
if (onlyTranslational) return new Bounds(bounds.center + translation, bounds.size);
var corners = ArrayPool<Vector3>.Claim(8);
var extents = bounds.extents;
corners[0] = Transform(bounds.center + new Vector3(extents.x, extents.y, extents.z));
corners[1] = Transform(bounds.center + new Vector3(extents.x, extents.y, -extents.z));
corners[2] = Transform(bounds.center + new Vector3(extents.x, -extents.y, extents.z));
corners[3] = Transform(bounds.center + new Vector3(extents.x, -extents.y, -extents.z));
corners[4] = Transform(bounds.center + new Vector3(-extents.x, extents.y, extents.z));
corners[5] = Transform(bounds.center + new Vector3(-extents.x, extents.y, -extents.z));
corners[6] = Transform(bounds.center + new Vector3(-extents.x, -extents.y, extents.z));
corners[7] = Transform(bounds.center + new Vector3(-extents.x, -extents.y, -extents.z));
var min = corners[0];
var max = corners[0];
for (int i = 1; i < 8; i++) {
min = Vector3.Min(min, corners[i]);
max = Vector3.Max(max, corners[i]);
}
ArrayPool<Vector3>.Release(ref corners);
return new Bounds((min+max)*0.5f, max - min);
}
public Bounds InverseTransform (Bounds bounds) {
if (onlyTranslational) return new Bounds(bounds.center - translation, bounds.size);
var corners = ArrayPool<Vector3>.Claim(8);
var extents = bounds.extents;
corners[0] = InverseTransform(bounds.center + new Vector3(extents.x, extents.y, extents.z));
corners[1] = InverseTransform(bounds.center + new Vector3(extents.x, extents.y, -extents.z));
corners[2] = InverseTransform(bounds.center + new Vector3(extents.x, -extents.y, extents.z));
corners[3] = InverseTransform(bounds.center + new Vector3(extents.x, -extents.y, -extents.z));
corners[4] = InverseTransform(bounds.center + new Vector3(-extents.x, extents.y, extents.z));
corners[5] = InverseTransform(bounds.center + new Vector3(-extents.x, extents.y, -extents.z));
corners[6] = InverseTransform(bounds.center + new Vector3(-extents.x, -extents.y, extents.z));
corners[7] = InverseTransform(bounds.center + new Vector3(-extents.x, -extents.y, -extents.z));
var min = corners[0];
var max = corners[0];
for (int i = 1; i < 8; i++) {
min = Vector3.Min(min, corners[i]);
max = Vector3.Max(max, corners[i]);
}
ArrayPool<Vector3>.Release(ref corners);
return new Bounds((min+max)*0.5f, max - min);
}
#region IMovementPlane implementation
/// <summary>
/// Transforms from world space to the 'ground' plane of the graph.
/// The transformation is purely a rotation so no scale or offset is used.
///
/// For a graph rotated with the rotation (-90, 0, 0) this will transform
/// a coordinate (x,y,z) to (x,y). For a graph with the rotation (0,0,0)
/// this will tranform a coordinate (x,y,z) to (x,z). More generally for
/// a graph with a quaternion rotation R this will transform a vector V
/// to R * V (i.e rotate the vector V using the rotation R).
/// </summary>
Vector2 IMovementPlane.ToPlane (Vector3 point) {
// These special cases cover most graph orientations used in practice.
// Having them here improves performance in those cases by a factor of
// 2.5 without impacting the generic case in any significant way.
if (isXY) return new Vector2(point.x, point.y);
if (!isXZ) point = inverseRotation * point;
return new Vector2(point.x, point.z);
}
/// <summary>
/// Transforms from world space to the 'ground' plane of the graph.
/// The transformation is purely a rotation so no scale or offset is used.
/// </summary>
Vector2 IMovementPlane.ToPlane (Vector3 point, out float elevation) {
if (!isXZ) point = inverseRotation * point;
elevation = point.y;
return new Vector2(point.x, point.z);
}
/// <summary>
/// Transforms from the 'ground' plane of the graph to world space.
/// The transformation is purely a rotation so no scale or offset is used.
/// </summary>
Vector3 IMovementPlane.ToWorld (Vector2 point, float elevation) {
return rotation * new Vector3(point.x, elevation, point.y);
}
#endregion
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: f9d3961465175430a84fd52d1bd31b05
timeCreated: 1474479722
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,192 @@
using System.Collections.Generic;
namespace Pathfinding.Util {
/// <summary>
/// Holds a lookup datastructure to quickly find objects inside rectangles.
/// Objects of type T occupy an integer rectangle in the grid and they can be
/// moved efficiently. You can query for all objects that touch a specified
/// rectangle that runs in O(m*k+r) time where m is the number of objects that
/// the query returns, k is the average number of cells that an object
/// occupies and r is the area of the rectangle query.
///
/// All objects must be contained within a rectangle with one point at the origin
/// (inclusive) and one at <see cref="size"/> (exclusive) that is specified in the constructor.
/// </summary>
public class GridLookup<T> where T : class {
Int2 size;
Item[] cells;
/// <summary>
/// Linked list of all items.
/// Note that the first item in the list is a dummy item and does not contain any data.
/// </summary>
Root all = new Root();
Dictionary<T, Root> rootLookup = new Dictionary<T, Root>();
Stack<Item> itemPool = new Stack<Item>();
public GridLookup (Int2 size) {
this.size = size;
cells = new Item[size.x*size.y];
for (int i = 0; i < cells.Length; i++) cells[i] = new Item();
}
internal class Item {
public Root root;
public Item prev, next;
}
public class Root {
/// <summary>Underlying object</summary>
public T obj;
/// <summary>Next item in the linked list of all roots</summary>
public Root next;
/// <summary>Previous item in the linked list of all roots</summary>
internal Root prev;
internal IntRect previousBounds = new IntRect(0, 0, -1, -1);
/// <summary>References to an item in each grid cell that this object is contained inside</summary>
internal List<Item> items = new List<Item>();
internal bool flag;
}
/// <summary>Linked list of all items</summary>
public Root AllItems {
get {
return all.next;
}
}
public void Clear () {
rootLookup.Clear();
all.next = null;
foreach (var item in cells) item.next = null;
}
public Root GetRoot (T item) {
Root root;
rootLookup.TryGetValue(item, out root);
return root;
}
/// <summary>
/// Add an object to the lookup data structure.
/// Returns: A handle which can be used for Move operations
/// </summary>
public Root Add (T item, IntRect bounds) {
var root = new Root {
obj = item,
prev = all,
next = all.next
};
all.next = root;
if (root.next != null) root.next.prev = root;
rootLookup.Add(item, root);
Move(item, bounds);
return root;
}
/// <summary>Removes an item from the lookup data structure</summary>
public void Remove (T item) {
Root root;
if (!rootLookup.TryGetValue(item, out root)) {
return;
}
// Make the item occupy no cells at all
Move(item, new IntRect(0, 0, -1, -1));
rootLookup.Remove(item);
root.prev.next = root.next;
if (root.next != null) root.next.prev = root.prev;
}
/// <summary>Move an object to occupy a new set of cells</summary>
public void Move (T item, IntRect bounds) {
Root root;
if (!rootLookup.TryGetValue(item, out root)) {
throw new System.ArgumentException("The item has not been added to this object");
}
var prev = root.previousBounds;
if (prev == bounds) return;
// Remove all
for (int i = 0; i < root.items.Count; i++) {
Item ob = root.items[i];
ob.prev.next = ob.next;
if (ob.next != null) ob.next.prev = ob.prev;
}
root.previousBounds = bounds;
int reusedItems = 0;
for (int z = bounds.ymin; z <= bounds.ymax; z++) {
for (int x = bounds.xmin; x <= bounds.xmax; x++) {
Item ob;
if (reusedItems < root.items.Count) {
ob = root.items[reusedItems];
} else {
ob = itemPool.Count > 0 ? itemPool.Pop() : new Item();
ob.root = root;
root.items.Add(ob);
}
reusedItems++;
ob.prev = cells[x + z*size.x];
ob.next = ob.prev.next;
ob.prev.next = ob;
if (ob.next != null) ob.next.prev = ob;
}
}
for (int i = root.items.Count-1; i >= reusedItems; i--) {
Item ob = root.items[i];
ob.root = null;
ob.next = null;
ob.prev = null;
root.items.RemoveAt(i);
itemPool.Push(ob);
}
}
/// <summary>
/// Returns all objects of a specific type inside the cells marked by the rectangle.
/// Note: For better memory usage, consider pooling the list using Pathfinding.Util.ListPool after you are done with it
/// </summary>
public List<U> QueryRect<U>(IntRect r) where U : class, T {
List<U> result = Pathfinding.Util.ListPool<U>.Claim();
// Loop through tiles and check which objects are inside them
for (int z = r.ymin; z <= r.ymax; z++) {
var zs = z*size.x;
for (int x = r.xmin; x <= r.xmax; x++) {
Item c = cells[x + zs];
// Note, first item is a dummy, so it is ignored
while (c.next != null) {
c = c.next;
var obj = c.root.obj as U;
if (!c.root.flag && obj != null) {
c.root.flag = true;
result.Add(obj);
}
}
}
}
// Reset flags
for (int z = r.ymin; z <= r.ymax; z++) {
var zs = z*size.x;
for (int x = r.xmin; x <= r.xmax; x++) {
Item c = cells[x + zs];
while (c.next != null) {
c = c.next;
c.root.flag = false;
}
}
}
return result;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7b09e5cbe5d4644c2b4ed9eed14cc13a
timeCreated: 1475417043
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,4 @@
// This file has been removed from the project. Since UnityPackages cannot
// delete files, only replace them, this message is left here to prevent old
// files from causing compiler errors

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b56789f958bf1496ba91f7e2b4147166
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,76 @@
namespace Pathfinding {
using Pathfinding.Util;
using UnityEngine;
public class NavmeshTile : INavmeshHolder {
/// <summary>Tile triangles</summary>
public int[] tris;
/// <summary>Tile vertices</summary>
public Int3[] verts;
/// <summary>Tile vertices in graph space</summary>
public Int3[] vertsInGraphSpace;
/// <summary>Tile X Coordinate</summary>
public int x;
/// <summary>Tile Z Coordinate</summary>
public int z;
/// <summary>
/// Width, in tile coordinates.
/// Warning: Widths other than 1 are not supported. This is mainly here for possible future features.
/// </summary>
public int w;
/// <summary>
/// Depth, in tile coordinates.
/// Warning: Depths other than 1 are not supported. This is mainly here for possible future features.
/// </summary>
public int d;
/// <summary>All nodes in the tile</summary>
public TriangleMeshNode[] nodes;
/// <summary>Bounding Box Tree for node lookups</summary>
public BBTree bbTree;
/// <summary>Temporary flag used for batching</summary>
public bool flag;
public NavmeshBase graph;
#region INavmeshHolder implementation
public void GetTileCoordinates (int tileIndex, out int x, out int z) {
x = this.x;
z = this.z;
}
public int GetVertexArrayIndex (int index) {
return index & NavmeshBase.VertexIndexMask;
}
/// <summary>Get a specific vertex in the tile</summary>
public Int3 GetVertex (int index) {
int idx = index & NavmeshBase.VertexIndexMask;
return verts[idx];
}
public Int3 GetVertexInGraphSpace (int index) {
return vertsInGraphSpace[index & NavmeshBase.VertexIndexMask];
}
/// <summary>Transforms coordinates from graph space to world space</summary>
public GraphTransform transform { get { return graph.transform; } }
#endregion
public void GetNodes (System.Action<GraphNode> action) {
if (nodes == null) return;
for (int i = 0; i < nodes.Length; i++) action(nodes[i]);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7408cbadf2e744d22853a92b15abede1
timeCreated: 1474405146
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,309 @@
using System.Collections.Generic;
namespace Pathfinding {
using Pathfinding.Util;
/// <summary>
/// Represents a collection of GraphNodes.
/// It allows for fast lookups of the closest node to a point.
///
/// See: https://en.wikipedia.org/wiki/K-d_tree
/// </summary>
public class PointKDTree {
// TODO: Make constant
public const int LeafSize = 10;
public const int LeafArraySize = LeafSize*2 + 1;
Node[] tree = new Node[16];
int numNodes = 0;
readonly List<GraphNode> largeList = new List<GraphNode>();
readonly Stack<GraphNode[]> arrayCache = new Stack<GraphNode[]>();
static readonly IComparer<GraphNode>[] comparers = new IComparer<GraphNode>[] { new CompareX(), new CompareY(), new CompareZ() };
struct Node {
/// <summary>Nodes in this leaf node (null if not a leaf node)</summary>
public GraphNode[] data;
/// <summary>Split point along the <see cref="splitAxis"/> if not a leaf node</summary>
public int split;
/// <summary>Number of non-null entries in <see cref="data"/></summary>
public ushort count;
/// <summary>Axis to split along if not a leaf node (x=0, y=1, z=2)</summary>
public byte splitAxis;
}
// Pretty ugly with one class for each axis, but it has been verified to make the tree around 5% faster
class CompareX : IComparer<GraphNode> {
public int Compare (GraphNode lhs, GraphNode rhs) { return lhs.position.x.CompareTo(rhs.position.x); }
}
class CompareY : IComparer<GraphNode> {
public int Compare (GraphNode lhs, GraphNode rhs) { return lhs.position.y.CompareTo(rhs.position.y); }
}
class CompareZ : IComparer<GraphNode> {
public int Compare (GraphNode lhs, GraphNode rhs) { return lhs.position.z.CompareTo(rhs.position.z); }
}
public PointKDTree() {
tree[1] = new Node { data = GetOrCreateList() };
}
/// <summary>Add the node to the tree</summary>
public void Add (GraphNode node) {
numNodes++;
Add(node, 1);
}
/// <summary>Rebuild the tree starting with all nodes in the array between index start (inclusive) and end (exclusive)</summary>
public void Rebuild (GraphNode[] nodes, int start, int end) {
if (start < 0 || end < start || end > nodes.Length)
throw new System.ArgumentException();
for (int i = 0; i < tree.Length; i++) {
var data = tree[i].data;
if (data != null) {
for (int j = 0; j < LeafArraySize; j++) data[j] = null;
arrayCache.Push(data);
tree[i].data = null;
}
}
numNodes = end - start;
Build(1, new List<GraphNode>(nodes), start, end);
}
GraphNode[] GetOrCreateList () {
// Note, the lists will never become larger than this initial capacity, so possibly they should be replaced by arrays
return arrayCache.Count > 0 ? arrayCache.Pop() : new GraphNode[LeafArraySize];
}
int Size (int index) {
return tree[index].data != null ? tree[index].count : Size(2 * index) + Size(2 * index + 1);
}
void CollectAndClear (int index, List<GraphNode> buffer) {
var nodes = tree[index].data;
var count = tree[index].count;
if (nodes != null) {
tree[index] = new Node();
for (int i = 0; i < count; i++) {
buffer.Add(nodes[i]);
nodes[i] = null;
}
arrayCache.Push(nodes);
} else {
CollectAndClear(index*2, buffer);
CollectAndClear(index*2 + 1, buffer);
}
}
static int MaxAllowedSize (int numNodes, int depth) {
// Allow a node to be 2.5 times as full as it should ideally be
// but do not allow it to contain more than 3/4ths of the total number of nodes
// (important to make sure nodes near the top of the tree also get rebalanced).
// A node should ideally contain numNodes/(2^depth) nodes below it (^ is exponentiation, not xor)
return System.Math.Min(((5 * numNodes) / 2) >> depth, (3 * numNodes) / 4);
}
void Rebalance (int index) {
CollectAndClear(index, largeList);
Build(index, largeList, 0, largeList.Count);
largeList.ClearFast();
}
void EnsureSize (int index) {
if (index >= tree.Length) {
var newLeaves = new Node[System.Math.Max(index + 1, tree.Length*2)];
tree.CopyTo(newLeaves, 0);
tree = newLeaves;
}
}
void Build (int index, List<GraphNode> nodes, int start, int end) {
EnsureSize(index);
if (end - start <= LeafSize) {
var leafData = tree[index].data = GetOrCreateList();
tree[index].count = (ushort)(end - start);
for (int i = start; i < end; i++)
leafData[i - start] = nodes[i];
} else {
Int3 mn, mx;
mn = mx = nodes[start].position;
for (int i = start; i < end; i++) {
var p = nodes[i].position;
mn = new Int3(System.Math.Min(mn.x, p.x), System.Math.Min(mn.y, p.y), System.Math.Min(mn.z, p.z));
mx = new Int3(System.Math.Max(mx.x, p.x), System.Math.Max(mx.y, p.y), System.Math.Max(mx.z, p.z));
}
Int3 diff = mx - mn;
var axis = diff.x > diff.y ? (diff.x > diff.z ? 0 : 2) : (diff.y > diff.z ? 1 : 2);
nodes.Sort(start, end - start, comparers[axis]);
int mid = (start+end)/2;
tree[index].split = (nodes[mid-1].position[axis] + nodes[mid].position[axis] + 1)/2;
tree[index].splitAxis = (byte)axis;
Build(index*2 + 0, nodes, start, mid);
Build(index*2 + 1, nodes, mid, end);
}
}
void Add (GraphNode point, int index, int depth = 0) {
// Move down in the tree until the leaf node is found that this point is inside of
while (tree[index].data == null) {
index = 2 * index + (point.position[tree[index].splitAxis] < tree[index].split ? 0 : 1);
depth++;
}
// Add the point to the leaf node
tree[index].data[tree[index].count++] = point;
// Check if the leaf node is large enough that we need to do some rebalancing
if (tree[index].count >= LeafArraySize) {
int levelsUp = 0;
// Search upwards for nodes that are too large and should be rebalanced
// Rebalance the node above the node that had a too large size so that it can
// move children over to the sibling
while (depth - levelsUp > 0 && Size(index >> levelsUp) > MaxAllowedSize(numNodes, depth-levelsUp)) {
levelsUp++;
}
Rebalance(index >> levelsUp);
}
}
/// <summary>Closest node to the point which satisfies the constraint</summary>
public GraphNode GetNearest (Int3 point, NNConstraint constraint) {
GraphNode best = null;
long bestSqrDist = long.MaxValue;
GetNearestInternal(1, point, constraint, ref best, ref bestSqrDist);
return best;
}
void GetNearestInternal (int index, Int3 point, NNConstraint constraint, ref GraphNode best, ref long bestSqrDist) {
var data = tree[index].data;
if (data != null) {
for (int i = tree[index].count - 1; i >= 0; i--) {
var dist = (data[i].position - point).sqrMagnitudeLong;
if (dist < bestSqrDist && (constraint == null || constraint.Suitable(data[i]))) {
bestSqrDist = dist;
best = data[i];
}
}
} else {
var dist = (long)(point[tree[index].splitAxis] - tree[index].split);
var childIndex = 2 * index + (dist < 0 ? 0 : 1);
GetNearestInternal(childIndex, point, constraint, ref best, ref bestSqrDist);
// Try the other one if it is possible to find a valid node on the other side
if (dist*dist < bestSqrDist) {
// childIndex ^ 1 will flip the last bit, so if childIndex is odd, then childIndex ^ 1 will be even
GetNearestInternal(childIndex ^ 0x1, point, constraint, ref best, ref bestSqrDist);
}
}
}
/// <summary>Closest node to the point which satisfies the constraint</summary>
public GraphNode GetNearestConnection (Int3 point, NNConstraint constraint, long maximumSqrConnectionLength) {
GraphNode best = null;
long bestSqrDist = long.MaxValue;
// Given a found point at a distance of r world units
// then any node that has a connection on which a closer point lies must have a squared distance lower than
// d^2 < (maximumConnectionLength/2)^2 + r^2
// Note: (x/2)^2 = (x^2)/4
// Note: (x+3)/4 to round up
long offset = (maximumSqrConnectionLength+3)/4;
GetNearestConnectionInternal(1, point, constraint, ref best, ref bestSqrDist, offset);
return best;
}
void GetNearestConnectionInternal (int index, Int3 point, NNConstraint constraint, ref GraphNode best, ref long bestSqrDist, long distanceThresholdOffset) {
var data = tree[index].data;
if (data != null) {
var pointv3 = (UnityEngine.Vector3)point;
for (int i = tree[index].count - 1; i >= 0; i--) {
var dist = (data[i].position - point).sqrMagnitudeLong;
// Note: the subtraction is important. If we used an addition on the RHS instead the result might overflow as bestSqrDist starts as long.MaxValue
if (dist - distanceThresholdOffset < bestSqrDist && (constraint == null || constraint.Suitable(data[i]))) {
// This node may contains the closest connection
// Check all connections
var conns = (data[i] as PointNode).connections;
if (conns != null) {
var nodePos = (UnityEngine.Vector3)data[i].position;
for (int j = 0; j < conns.Length; j++) {
// Find the closest point on the connection, but only on this node's side of the connection
// This ensures that we will find the closest node with the closest connection.
var connectionMidpoint = ((UnityEngine.Vector3)conns[j].node.position + nodePos) * 0.5f;
float sqrConnectionDistance = VectorMath.SqrDistancePointSegment(nodePos, connectionMidpoint, pointv3);
// Convert to Int3 space
long sqrConnectionDistanceInt = (long)(sqrConnectionDistance*Int3.FloatPrecision*Int3.FloatPrecision);
if (sqrConnectionDistanceInt < bestSqrDist) {
bestSqrDist = sqrConnectionDistanceInt;
best = data[i];
}
}
}
// Also check if the node itself is close enough.
// This is important if the node has no connections at all.
if (dist < bestSqrDist) {
bestSqrDist = dist;
best = data[i];
}
}
}
} else {
var dist = (long)(point[tree[index].splitAxis] - tree[index].split);
var childIndex = 2 * index + (dist < 0 ? 0 : 1);
GetNearestConnectionInternal(childIndex, point, constraint, ref best, ref bestSqrDist, distanceThresholdOffset);
// Try the other one if it is possible to find a valid node on the other side
// Note: the subtraction is important. If we used an addition on the RHS instead the result might overflow as bestSqrDist starts as long.MaxValue
if (dist*dist - distanceThresholdOffset < bestSqrDist) {
// childIndex ^ 1 will flip the last bit, so if childIndex is odd, then childIndex ^ 1 will be even
GetNearestConnectionInternal(childIndex ^ 0x1, point, constraint, ref best, ref bestSqrDist, distanceThresholdOffset);
}
}
}
/// <summary>Add all nodes within a squared distance of the point to the buffer.</summary>
/// <param name="point">Nodes around this point will be added to the buffer.</param>
/// <param name="sqrRadius">squared maximum distance in Int3 space. If you are converting from world space you will need to multiply by Int3.Precision:
/// <code> var sqrRadius = (worldSpaceRadius * Int3.Precision) * (worldSpaceRadius * Int3.Precision); </code></param>
/// <param name="buffer">All nodes will be added to this list.</param>
public void GetInRange (Int3 point, long sqrRadius, List<GraphNode> buffer) {
GetInRangeInternal(1, point, sqrRadius, buffer);
}
void GetInRangeInternal (int index, Int3 point, long sqrRadius, List<GraphNode> buffer) {
var data = tree[index].data;
if (data != null) {
for (int i = tree[index].count - 1; i >= 0; i--) {
var dist = (data[i].position - point).sqrMagnitudeLong;
if (dist < sqrRadius) {
buffer.Add(data[i]);
}
}
} else {
var dist = (long)(point[tree[index].splitAxis] - tree[index].split);
// Pick the first child to enter based on which side of the splitting line the point is
var childIndex = 2 * index + (dist < 0 ? 0 : 1);
GetInRangeInternal(childIndex, point, sqrRadius, buffer);
// Try the other one if it is possible to find a valid node on the other side
if (dist*dist < sqrRadius) {
// childIndex ^ 1 will flip the last bit, so if childIndex is odd, then childIndex ^ 1 will be even
GetInRangeInternal(childIndex ^ 0x1, point, sqrRadius, buffer);
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 4aef007a0dd474c20872caa35fbbc8a7
timeCreated: 1462714767
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,184 @@
using System;
using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding {
/// <summary>
/// Axis Aligned Bounding Box Tree.
/// Holds a bounding box tree of RecastMeshObj components.
/// Note that it assumes that once an object has been added, it stays at the same
/// world position. If it is moved, then it might not be able to be found.
/// </summary>
public class RecastBBTree {
RecastBBTreeBox root;
/// <summary>Queries the tree for all RecastMeshObjs inside the specified bounds.</summary>
/// <param name="bounds">World space bounds to search within</param>
/// <param name="buffer">The results will be added to the buffer</param>
public void QueryInBounds (Rect bounds, List<RecastMeshObj> buffer) {
if (root == null) return;
QueryBoxInBounds(root, bounds, buffer);
}
void QueryBoxInBounds (RecastBBTreeBox box, Rect bounds, List<RecastMeshObj> boxes) {
if (box.mesh != null) {
// Leaf node
if (RectIntersectsRect(box.rect, bounds)) {
// Found a RecastMeshObj, add it to the result
boxes.Add(box.mesh);
}
} else {
// Search children
if (RectIntersectsRect(box.c1.rect, bounds)) {
QueryBoxInBounds(box.c1, bounds, boxes);
}
if (RectIntersectsRect(box.c2.rect, bounds)) {
QueryBoxInBounds(box.c2, bounds, boxes);
}
}
}
/// <summary>
/// Removes the specified mesh from the tree.
/// Assumes that it has the correct bounds information.
///
/// Returns: True if the mesh was removed from the tree, false otherwise.
/// </summary>
public bool Remove (RecastMeshObj mesh) {
if (mesh == null) throw new ArgumentNullException("mesh");
if (root == null) {
return false;
}
bool found = false;
Bounds b = mesh.GetBounds();
//Convert to top down rect
Rect r = Rect.MinMaxRect(b.min.x, b.min.z, b.max.x, b.max.z);
root = RemoveBox(root, mesh, r, ref found);
return found;
}
RecastBBTreeBox RemoveBox (RecastBBTreeBox c, RecastMeshObj mesh, Rect bounds, ref bool found) {
if (!RectIntersectsRect(c.rect, bounds)) {
return c;
}
if (c.mesh == mesh) {
found = true;
return null;
}
if (c.mesh == null && !found) {
c.c1 = RemoveBox(c.c1, mesh, bounds, ref found);
if (c.c1 == null) {
return c.c2;
}
if (!found) {
c.c2 = RemoveBox(c.c2, mesh, bounds, ref found);
if (c.c2 == null) {
return c.c1;
}
}
// TODO: Will this ever be called?
if (found) {
c.rect = ExpandToContain(c.c1.rect, c.c2.rect);
}
}
return c;
}
/// <summary>Inserts a RecastMeshObj in the tree at its current position</summary>
public void Insert (RecastMeshObj mesh) {
var box = new RecastBBTreeBox(mesh);
if (root == null) {
root = box;
return;
}
RecastBBTreeBox c = root;
while (true) {
c.rect = ExpandToContain(c.rect, box.rect);
if (c.mesh != null) {
//Is Leaf
c.c1 = box;
var box2 = new RecastBBTreeBox(c.mesh);
c.c2 = box2;
c.mesh = null;
return;
} else {
float e1 = ExpansionRequired(c.c1.rect, box.rect);
float e2 = ExpansionRequired(c.c2.rect, box.rect);
// Choose the rect requiring the least expansion to contain box.rect
if (e1 < e2) {
c = c.c1;
} else if (e2 < e1) {
c = c.c2;
} else {
// Equal, Choose the one with the smallest area
c = RectArea(c.c1.rect) < RectArea(c.c2.rect) ? c.c1 : c.c2;
}
}
}
}
static bool RectIntersectsRect (Rect r, Rect r2) {
return (r.xMax > r2.xMin && r.yMax > r2.yMin && r2.xMax > r.xMin && r2.yMax > r.yMin);
}
/// <summary>Returns the difference in area between r and r expanded to contain r2</summary>
static float ExpansionRequired (Rect r, Rect r2) {
float xMin = Mathf.Min(r.xMin, r2.xMin);
float xMax = Mathf.Max(r.xMax, r2.xMax);
float yMin = Mathf.Min(r.yMin, r2.yMin);
float yMax = Mathf.Max(r.yMax, r2.yMax);
return (xMax-xMin)*(yMax-yMin)-RectArea(r);
}
/// <summary>Returns a new rect which contains both r and r2</summary>
static Rect ExpandToContain (Rect r, Rect r2) {
float xMin = Mathf.Min(r.xMin, r2.xMin);
float xMax = Mathf.Max(r.xMax, r2.xMax);
float yMin = Mathf.Min(r.yMin, r2.yMin);
float yMax = Mathf.Max(r.yMax, r2.yMax);
return Rect.MinMaxRect(xMin, yMin, xMax, yMax);
}
/// <summary>Returns the area of a rect</summary>
static float RectArea (Rect r) {
return r.width*r.height;
}
}
public class RecastBBTreeBox {
public Rect rect;
public RecastMeshObj mesh;
public RecastBBTreeBox c1;
public RecastBBTreeBox c2;
public RecastBBTreeBox (RecastMeshObj mesh) {
this.mesh = mesh;
Vector3 min = mesh.bounds.min;
Vector3 max = mesh.bounds.max;
rect = Rect.MinMaxRect(min.x, min.z, max.x, max.z);
}
public bool Contains (Vector3 p) {
return rect.Contains(p);
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 183e10f9cadca424792b5f940ce3fe3d
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,570 @@
using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding.Recast {
using System;
using Pathfinding;
using Pathfinding.Voxels;
internal class RecastMeshGatherer {
readonly int terrainSampleSize;
readonly LayerMask mask;
readonly List<string> tagMask;
readonly float colliderRasterizeDetail;
readonly Bounds bounds;
public RecastMeshGatherer (Bounds bounds, int terrainSampleSize, LayerMask mask, List<string> tagMask, float colliderRasterizeDetail) {
// Clamp to at least 1 since that's the resolution of the heightmap
terrainSampleSize = Math.Max(terrainSampleSize, 1);
this.bounds = bounds;
this.terrainSampleSize = terrainSampleSize;
this.mask = mask;
this.tagMask = tagMask ?? new List<string>();
this.colliderRasterizeDetail = colliderRasterizeDetail;
}
static List<MeshFilter> FilterMeshes (MeshFilter[] meshFilters, List<string> tagMask, LayerMask layerMask) {
var filtered = new List<MeshFilter>(meshFilters.Length / 3);
for (int i = 0; i < meshFilters.Length; i++) {
MeshFilter filter = meshFilters[i];
Renderer rend = filter.GetComponent<Renderer>();
if (rend != null && filter.sharedMesh != null && rend.enabled && (((1 << filter.gameObject.layer) & layerMask) != 0 || tagMask.Contains(filter.tag))) {
if (filter.GetComponent<RecastMeshObj>() == null) {
filtered.Add(filter);
}
}
}
return filtered;
}
public void CollectSceneMeshes (List<RasterizationMesh> meshes) {
if (tagMask.Count > 0 || mask != 0) {
// This is unfortunately the fastest way to find all mesh filters.. and it is not particularly fast.
var meshFilters = GameObject.FindObjectsOfType<MeshFilter>();
var filteredMeshes = FilterMeshes(meshFilters, tagMask, mask);
var cachedVertices = new Dictionary<Mesh, Vector3[]>();
var cachedTris = new Dictionary<Mesh, int[]>();
bool containedStatic = false;
for (int i = 0; i < filteredMeshes.Count; i++) {
MeshFilter filter = filteredMeshes[i];
// Note, guaranteed to have a renderer
Renderer rend = filter.GetComponent<Renderer>();
if (rend.isPartOfStaticBatch) {
// Statically batched meshes cannot be used due to Unity limitations
// log a warning about this
containedStatic = true;
} else {
// Only include it if it intersects with the graph
if (rend.bounds.Intersects(bounds)) {
Mesh mesh = filter.sharedMesh;
RasterizationMesh smesh;
// Check the cache to avoid allocating
// a new array unless necessary
if (cachedVertices.ContainsKey(mesh)) {
smesh = new RasterizationMesh(cachedVertices[mesh], cachedTris[mesh], rend.bounds);
} else {
smesh = new RasterizationMesh(mesh.vertices, mesh.triangles, rend.bounds);
cachedVertices[mesh] = smesh.vertices;
cachedTris[mesh] = smesh.triangles;
}
smesh.matrix = rend.localToWorldMatrix;
smesh.original = filter;
meshes.Add(smesh);
}
}
if (containedStatic)
Debug.LogWarning("Some meshes were statically batched. These meshes can not be used for navmesh calculation" +
" due to technical constraints.\nDuring runtime scripts cannot access the data of meshes which have been statically batched.\n" +
"One way to solve this problem is to use cached startup (Save & Load tab in the inspector) to only calculate the graph when the game is not playing.");
}
#if ASTARDEBUG
int y = 0;
foreach (RasterizationMesh smesh in meshes) {
y++;
Vector3[] vecs = smesh.vertices;
int[] tris = smesh.triangles;
for (int i = 0; i < tris.Length; i += 3) {
Vector3 p1 = smesh.matrix.MultiplyPoint3x4(vecs[tris[i+0]]);
Vector3 p2 = smesh.matrix.MultiplyPoint3x4(vecs[tris[i+1]]);
Vector3 p3 = smesh.matrix.MultiplyPoint3x4(vecs[tris[i+2]]);
Debug.DrawLine(p1, p2, Color.red, 1);
Debug.DrawLine(p2, p3, Color.red, 1);
Debug.DrawLine(p3, p1, Color.red, 1);
}
}
#endif
}
}
/// <summary>Find all relevant RecastMeshObj components and create ExtraMeshes for them</summary>
public void CollectRecastMeshObjs (List<RasterizationMesh> buffer) {
var buffer2 = Util.ListPool<RecastMeshObj>.Claim();
// Get all recast mesh objects inside the bounds
RecastMeshObj.GetAllInBounds(buffer2, bounds);
var cachedVertices = new Dictionary<Mesh, Vector3[]>();
var cachedTris = new Dictionary<Mesh, int[]>();
// Create an RasterizationMesh object
// for each RecastMeshObj
for (int i = 0; i < buffer2.Count; i++) {
MeshFilter filter = buffer2[i].GetMeshFilter();
Renderer rend = filter != null? filter.GetComponent<Renderer>() : null;
if (filter != null && rend != null && filter.sharedMesh != null) {
Mesh mesh = filter.sharedMesh;
RasterizationMesh smesh;
// Don't read the vertices and triangles from the
// mesh if we have seen the same mesh previously
if (cachedVertices.ContainsKey(mesh)) {
smesh = new RasterizationMesh(cachedVertices[mesh], cachedTris[mesh], rend.bounds);
} else {
smesh = new RasterizationMesh(mesh.vertices, mesh.triangles, rend.bounds);
cachedVertices[mesh] = smesh.vertices;
cachedTris[mesh] = smesh.triangles;
}
smesh.matrix = rend.localToWorldMatrix;
smesh.original = filter;
smesh.area = buffer2[i].area;
buffer.Add(smesh);
} else {
Collider coll = buffer2[i].GetCollider();
if (coll == null) {
Debug.LogError("RecastMeshObject ("+buffer2[i].gameObject.name +") didn't have a collider or MeshFilter+Renderer attached", buffer2[i].gameObject);
continue;
}
RasterizationMesh smesh = RasterizeCollider(coll);
// Make sure a valid RasterizationMesh was returned
if (smesh != null) {
smesh.area = buffer2[i].area;
buffer.Add(smesh);
}
}
}
// Clear cache to avoid memory leak
capsuleCache.Clear();
Util.ListPool<RecastMeshObj>.Release(ref buffer2);
}
public void CollectTerrainMeshes (bool rasterizeTrees, float desiredChunkSize, List<RasterizationMesh> result) {
// Find all terrains in the scene
var terrains = Terrain.activeTerrains;
if (terrains.Length > 0) {
// Loop through all terrains in the scene
for (int j = 0; j < terrains.Length; j++) {
if (terrains[j].terrainData == null) continue;
GenerateTerrainChunks(terrains[j], bounds, desiredChunkSize, result);
if (rasterizeTrees) {
// Rasterize all tree colliders on this terrain object
CollectTreeMeshes(terrains[j], result);
}
}
}
}
void GenerateTerrainChunks (Terrain terrain, Bounds bounds, float desiredChunkSize, List<RasterizationMesh> result) {
var terrainData = terrain.terrainData;
if (terrainData == null)
throw new System.ArgumentException("Terrain contains no terrain data");
Vector3 offset = terrain.GetPosition();
Vector3 center = offset + terrainData.size * 0.5F;
// Figure out the bounds of the terrain in world space
var terrainBounds = new Bounds(center, terrainData.size);
// Only include terrains which intersects the graph
if (!terrainBounds.Intersects(bounds))
return;
// Original heightmap size
#if UNITY_2019_3_OR_NEWER
int heightmapWidth = terrainData.heightmapResolution;
int heightmapDepth = terrainData.heightmapResolution;
#else
int heightmapWidth = terrainData.heightmapWidth;
int heightmapDepth = terrainData.heightmapHeight;
#endif
// Sample the terrain heightmap
float[, ] heights = terrainData.GetHeights(0, 0, heightmapWidth, heightmapDepth);
Vector3 sampleSize = terrainData.heightmapScale;
sampleSize.y = terrainData.size.y;
// Make chunks at least 12 quads wide
// since too small chunks just decreases performance due
// to the overhead of checking for bounds and similar things
const int MinChunkSize = 12;
// Find the number of samples along each edge that corresponds to a world size of desiredChunkSize
// Then round up to the nearest multiple of terrainSampleSize
var chunkSizeAlongX = Mathf.CeilToInt(Mathf.Max(desiredChunkSize / (sampleSize.x * terrainSampleSize), MinChunkSize)) * terrainSampleSize;
var chunkSizeAlongZ = Mathf.CeilToInt(Mathf.Max(desiredChunkSize / (sampleSize.z * terrainSampleSize), MinChunkSize)) * terrainSampleSize;
for (int z = 0; z < heightmapDepth; z += chunkSizeAlongZ) {
for (int x = 0; x < heightmapWidth; x += chunkSizeAlongX) {
var width = Mathf.Min(chunkSizeAlongX, heightmapWidth - x);
var depth = Mathf.Min(chunkSizeAlongZ, heightmapDepth - z);
var chunkMin = offset + new Vector3(z * sampleSize.x, 0, x * sampleSize.z);
var chunkMax = offset + new Vector3((z + depth) * sampleSize.x, sampleSize.y, (x + width) * sampleSize.z);
var chunkBounds = new Bounds();
chunkBounds.SetMinMax(chunkMin, chunkMax);
// Skip chunks that are not inside the desired bounds
if (chunkBounds.Intersects(bounds)) {
var chunk = GenerateHeightmapChunk(heights, sampleSize, offset, x, z, width, depth, terrainSampleSize);
result.Add(chunk);
}
}
}
}
/// <summary>Returns ceil(lhs/rhs), i.e lhs/rhs rounded up</summary>
static int CeilDivision (int lhs, int rhs) {
return (lhs + rhs - 1)/rhs;
}
/// <summary>Generates a terrain chunk mesh</summary>
RasterizationMesh GenerateHeightmapChunk (float[, ] heights, Vector3 sampleSize, Vector3 offset, int x0, int z0, int width, int depth, int stride) {
// Downsample to a smaller mesh (full resolution will take a long time to rasterize)
// Round up the width to the nearest multiple of terrainSampleSize and then add 1
// (off by one because there are vertices at the edge of the mesh)
int resultWidth = CeilDivision(width, terrainSampleSize) + 1;
int resultDepth = CeilDivision(depth, terrainSampleSize) + 1;
var heightmapWidth = heights.GetLength(0);
var heightmapDepth = heights.GetLength(1);
// Create a mesh from the heightmap
var numVerts = resultWidth * resultDepth;
var terrainVertices = Util.ArrayPool<Vector3>.Claim(numVerts);
// Create lots of vertices
for (int z = 0; z < resultDepth; z++) {
for (int x = 0; x < resultWidth; x++) {
int sampleX = Math.Min(x0 + x*stride, heightmapWidth-1);
int sampleZ = Math.Min(z0 + z*stride, heightmapDepth-1);
terrainVertices[z*resultWidth + x] = new Vector3(sampleZ * sampleSize.x, heights[sampleX, sampleZ]*sampleSize.y, sampleX * sampleSize.z) + offset;
}
}
// Create the mesh by creating triangles in a grid like pattern
int numTris = (resultWidth-1)*(resultDepth-1)*2*3;
var tris = Util.ArrayPool<int>.Claim(numTris);
int triangleIndex = 0;
for (int z = 0; z < resultDepth-1; z++) {
for (int x = 0; x < resultWidth-1; x++) {
tris[triangleIndex] = z*resultWidth + x;
tris[triangleIndex+1] = z*resultWidth + x+1;
tris[triangleIndex+2] = (z+1)*resultWidth + x+1;
triangleIndex += 3;
tris[triangleIndex] = z*resultWidth + x;
tris[triangleIndex+1] = (z+1)*resultWidth + x+1;
tris[triangleIndex+2] = (z+1)*resultWidth + x;
triangleIndex += 3;
}
}
#if ASTARDEBUG
var color = AstarMath.IntToColor(x0 + 7 * z0, 0.7f);
for (int i = 0; i < numTris; i += 3) {
Debug.DrawLine(terrainVertices[tris[i]], terrainVertices[tris[i+1]], color, 40);
Debug.DrawLine(terrainVertices[tris[i+1]], terrainVertices[tris[i+2]], color, 40);
Debug.DrawLine(terrainVertices[tris[i+2]], terrainVertices[tris[i]], color, 40);
}
#endif
var mesh = new RasterizationMesh(terrainVertices, tris, new Bounds());
mesh.numVertices = numVerts;
mesh.numTriangles = numTris;
mesh.pool = true;
// Could probably calculate these bounds in a faster way
mesh.RecalculateBounds();
return mesh;
}
void CollectTreeMeshes (Terrain terrain, List<RasterizationMesh> result) {
TerrainData data = terrain.terrainData;
for (int i = 0; i < data.treeInstances.Length; i++) {
TreeInstance instance = data.treeInstances[i];
TreePrototype prot = data.treePrototypes[instance.prototypeIndex];
// Make sure that the tree prefab exists
if (prot.prefab == null) {
continue;
}
var collider = prot.prefab.GetComponent<Collider>();
var treePosition = terrain.transform.position + Vector3.Scale(instance.position, data.size);
var scale = new Vector3(instance.widthScale, instance.heightScale, instance.widthScale);
scale = Vector3.Scale(scale, prot.prefab.transform.localScale);
if (collider == null) {
var instanceBounds = new Bounds(terrain.transform.position + Vector3.Scale(instance.position, data.size), new Vector3(instance.widthScale, instance.heightScale, instance.widthScale));
Matrix4x4 matrix = Matrix4x4.TRS(treePosition, Quaternion.identity, scale*0.5f);
var mesh = new RasterizationMesh(BoxColliderVerts, BoxColliderTris, instanceBounds, matrix);
result.Add(mesh);
} else {
// The prefab has a collider, use that instead
// Generate a mesh from the collider
RasterizationMesh mesh = RasterizeCollider(collider, Matrix4x4.TRS(treePosition, Quaternion.identity, scale));
// Make sure a valid mesh was generated
if (mesh != null) {
// The bounds are incorrectly based on collider.bounds.
// It is incorrect because the collider is on the prefab, not on the tree instance
// so we need to recalculate the bounds based on the actual vertex positions
mesh.RecalculateBounds();
result.Add(mesh);
}
}
}
}
public void CollectColliderMeshes (List<RasterizationMesh> result) {
/// <summary>TODO: Use Physics.OverlapBox on newer Unity versions</summary>
// Find all colliders that could possibly be inside the bounds
var colls = Physics.OverlapSphere(bounds.center, bounds.size.magnitude, -1, QueryTriggerInteraction.Ignore);
if (tagMask.Count > 0 || mask != 0) {
for (int i = 0; i < colls.Length; i++) {
Collider collider = colls[i];
if ((((mask >> collider.gameObject.layer) & 1) != 0 || tagMask.Contains(collider.tag)) && collider.enabled && !collider.isTrigger && collider.bounds.Intersects(bounds) && collider.GetComponent<RecastMeshObj>() == null) {
RasterizationMesh emesh = RasterizeCollider(collider);
//Make sure a valid RasterizationMesh was returned
if (emesh != null)
result.Add(emesh);
}
}
}
// Clear cache to avoid memory leak
capsuleCache.Clear();
}
/// <summary>
/// Box Collider triangle indices can be reused for multiple instances.
/// Warning: This array should never be changed
/// </summary>
private readonly static int[] BoxColliderTris = {
0, 1, 2,
0, 2, 3,
6, 5, 4,
7, 6, 4,
0, 5, 1,
0, 4, 5,
1, 6, 2,
1, 5, 6,
2, 7, 3,
2, 6, 7,
3, 4, 0,
3, 7, 4
};
/// <summary>
/// Box Collider vertices can be reused for multiple instances.
/// Warning: This array should never be changed
/// </summary>
private readonly static Vector3[] BoxColliderVerts = {
new Vector3(-1, -1, -1),
new Vector3(1, -1, -1),
new Vector3(1, -1, 1),
new Vector3(-1, -1, 1),
new Vector3(-1, 1, -1),
new Vector3(1, 1, -1),
new Vector3(1, 1, 1),
new Vector3(-1, 1, 1),
};
/// <summary>Holds meshes for capsules to avoid generating duplicate capsule meshes for identical capsules</summary>
private List<CapsuleCache> capsuleCache = new List<CapsuleCache>();
class CapsuleCache {
public int rows;
public float height;
public Vector3[] verts;
public int[] tris;
}
/// <summary>
/// Rasterizes a collider to a mesh.
/// This will pass the col.transform.localToWorldMatrix to the other overload of this function.
/// </summary>
RasterizationMesh RasterizeCollider (Collider col) {
return RasterizeCollider(col, col.transform.localToWorldMatrix);
}
/// <summary>
/// Rasterizes a collider to a mesh assuming it's vertices should be multiplied with the matrix.
/// Note that the bounds of the returned RasterizationMesh is based on collider.bounds. So you might want to
/// call myExtraMesh.RecalculateBounds on the returned mesh to recalculate it if the collider.bounds would
/// not give the correct value.
/// </summary>
RasterizationMesh RasterizeCollider (Collider col, Matrix4x4 localToWorldMatrix) {
RasterizationMesh result = null;
if (col is BoxCollider) {
result = RasterizeBoxCollider(col as BoxCollider, localToWorldMatrix);
} else if (col is SphereCollider || col is CapsuleCollider) {
var scollider = col as SphereCollider;
var ccollider = col as CapsuleCollider;
float radius = (scollider != null ? scollider.radius : ccollider.radius);
float height = scollider != null ? 0 : (ccollider.height*0.5f/radius) - 1;
Quaternion rot = Quaternion.identity;
// Capsule colliders can be aligned along the X, Y or Z axis
if (ccollider != null) rot = Quaternion.Euler(ccollider.direction == 2 ? 90 : 0, 0, ccollider.direction == 0 ? 90 : 0);
Matrix4x4 matrix = Matrix4x4.TRS(scollider != null ? scollider.center : ccollider.center, rot, Vector3.one*radius);
matrix = localToWorldMatrix * matrix;
result = RasterizeCapsuleCollider(radius, height, col.bounds, matrix);
} else if (col is MeshCollider) {
var collider = col as MeshCollider;
if (collider.sharedMesh != null) {
result = new RasterizationMesh(collider.sharedMesh.vertices, collider.sharedMesh.triangles, collider.bounds, localToWorldMatrix);
}
}
#if ASTARDEBUG
for (int i = 0; i < result.triangles.Length; i += 3) {
Debug.DrawLine(result.matrix.MultiplyPoint3x4(result.vertices[result.triangles[i]]), result.matrix.MultiplyPoint3x4(result.vertices[result.triangles[i+1]]), Color.yellow);
Debug.DrawLine(result.matrix.MultiplyPoint3x4(result.vertices[result.triangles[i+2]]), result.matrix.MultiplyPoint3x4(result.vertices[result.triangles[i+1]]), Color.yellow);
Debug.DrawLine(result.matrix.MultiplyPoint3x4(result.vertices[result.triangles[i]]), result.matrix.MultiplyPoint3x4(result.vertices[result.triangles[i+2]]), Color.yellow);
}
#endif
return result;
}
RasterizationMesh RasterizeBoxCollider (BoxCollider collider, Matrix4x4 localToWorldMatrix) {
Matrix4x4 matrix = Matrix4x4.TRS(collider.center, Quaternion.identity, collider.size*0.5f);
matrix = localToWorldMatrix * matrix;
return new RasterizationMesh(BoxColliderVerts, BoxColliderTris, collider.bounds, matrix);
}
RasterizationMesh RasterizeCapsuleCollider (float radius, float height, Bounds bounds, Matrix4x4 localToWorldMatrix) {
// Calculate the number of rows to use
// grows as sqrt(x) to the radius of the sphere/capsule which I have found works quite well
int rows = Mathf.Max(4, Mathf.RoundToInt(colliderRasterizeDetail*Mathf.Sqrt(localToWorldMatrix.MultiplyVector(Vector3.one).magnitude)));
if (rows > 100) {
Debug.LogWarning("Very large detail for some collider meshes. Consider decreasing Collider Rasterize Detail (RecastGraph)");
}
int cols = rows;
Vector3[] verts;
int[] trisArr;
// Check if we have already calculated a similar capsule
CapsuleCache cached = null;
for (int i = 0; i < capsuleCache.Count; i++) {
CapsuleCache c = capsuleCache[i];
if (c.rows == rows && Mathf.Approximately(c.height, height)) {
cached = c;
}
}
if (cached == null) {
// Generate a sphere/capsule mesh
verts = new Vector3[(rows)*cols + 2];
var tris = new List<int>();
verts[verts.Length-1] = Vector3.up;
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
verts[c + r*cols] = new Vector3(Mathf.Cos(c*Mathf.PI*2/cols)*Mathf.Sin((r*Mathf.PI/(rows-1))), Mathf.Cos((r*Mathf.PI/(rows-1))) + (r < rows/2 ? height : -height), Mathf.Sin(c*Mathf.PI*2/cols)*Mathf.Sin((r*Mathf.PI/(rows-1))));
}
}
verts[verts.Length-2] = Vector3.down;
for (int i = 0, j = cols-1; i < cols; j = i++) {
tris.Add(verts.Length-1);
tris.Add(0*cols + j);
tris.Add(0*cols + i);
}
for (int r = 1; r < rows; r++) {
for (int i = 0, j = cols-1; i < cols; j = i++) {
tris.Add(r*cols + i);
tris.Add(r*cols + j);
tris.Add((r-1)*cols + i);
tris.Add((r-1)*cols + j);
tris.Add((r-1)*cols + i);
tris.Add(r*cols + j);
}
}
for (int i = 0, j = cols-1; i < cols; j = i++) {
tris.Add(verts.Length-2);
tris.Add((rows-1)*cols + j);
tris.Add((rows-1)*cols + i);
}
// Add calculated mesh to the cache
cached = new CapsuleCache();
cached.rows = rows;
cached.height = height;
cached.verts = verts;
cached.tris = tris.ToArray();
capsuleCache.Add(cached);
}
// Read from cache
verts = cached.verts;
trisArr = cached.tris;
return new RasterizationMesh(verts, trisArr, bounds, localToWorldMatrix);
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 6eafb2bafadcd40cab49fa79908ee5f1
timeCreated: 1454097062
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,177 @@
using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding {
/// <summary>
/// Explicit mesh object for recast graphs.
/// Adding this component to an object will make sure it is included in any recast graphs.
/// It will be included even if the Rasterize Meshes toggle is set to false.
///
/// Using RecastMeshObjs instead of relying on the Rasterize Meshes option is good for several reasons.
/// - Rasterize Meshes is slow. If you are using a tiled graph and you are updating it, every time something is recalculated
/// the graph will have to search all meshes in your scene for ones to rasterize, in contrast, RecastMeshObjs are stored
/// in a tree for extremely fast lookup (O(log n + k) compared to O(n) where n is the number of meshes in your scene and k is the number of meshes
/// which should be rasterized, if you know Big-O notation).
/// - The RecastMeshObj exposes some options which can not be accessed using the Rasterize Meshes toggle. See member documentation for more info.
/// This can for example be used to include meshes in the recast graph rasterization, but make sure that the character cannot walk on them.
///
/// Since the objects are stored in a tree, and trees are slow to update, there is an enforcement that objects are not allowed to move
/// unless the <see cref="dynamic"/> option is enabled. When the dynamic option is enabled, the object will be stored in an array instead of in the tree.
/// This will reduce the performance improvement over 'Rasterize Meshes' but is still faster.
///
/// If a mesh filter and a mesh renderer is attached to this GameObject, those will be used in the rasterization
/// otherwise if a collider is attached, that will be used.
/// </summary>
[AddComponentMenu("Pathfinding/Navmesh/RecastMeshObj")]
[HelpURL("https://arongranberg.com/astar/documentation/stable/class_pathfinding_1_1_recast_mesh_obj.php")]
public class RecastMeshObj : VersionedMonoBehaviour {
/// <summary>Static objects are stored in a tree for fast bounds lookups</summary>
protected static RecastBBTree tree = new RecastBBTree();
/// <summary>Dynamic objects are stored in a list since it is costly to update the tree every time they move</summary>
protected static List<RecastMeshObj> dynamicMeshObjs = new List<RecastMeshObj>();
/// <summary>Fills the buffer with all RecastMeshObjs which intersect the specified bounds</summary>
public static void GetAllInBounds (List<RecastMeshObj> buffer, Bounds bounds) {
if (!Application.isPlaying) {
var objs = FindObjectsOfType(typeof(RecastMeshObj)) as RecastMeshObj[];
for (int i = 0; i < objs.Length; i++) {
objs[i].RecalculateBounds();
if (objs[i].GetBounds().Intersects(bounds)) {
buffer.Add(objs[i]);
}
}
return;
} else if (Time.timeSinceLevelLoad == 0) {
// Is is not guaranteed that all RecastMeshObj OnEnable functions have been called, so if it is the first frame since loading a new level
// try to initialize all RecastMeshObj objects.
var objs = FindObjectsOfType(typeof(RecastMeshObj)) as RecastMeshObj[];
for (int i = 0; i < objs.Length; i++) objs[i].Register();
}
for (int q = 0; q < dynamicMeshObjs.Count; q++) {
if (dynamicMeshObjs[q].GetBounds().Intersects(bounds)) {
buffer.Add(dynamicMeshObjs[q]);
}
}
Rect r = Rect.MinMaxRect(bounds.min.x, bounds.min.z, bounds.max.x, bounds.max.z);
tree.QueryInBounds(r, buffer);
}
[HideInInspector]
public Bounds bounds;
/// <summary>
/// Check if the object will move.
/// Recalculation of bounding box trees is expensive so if this is true, the object
/// will simply be stored in an array. Easier to move, but slower lookup, so use wisely.
/// If you for some reason want to move it, but don't want it dynamic (maybe you will only move it veery seldom and have lots of similar
/// objects so they would add overhead by being dynamic). You can enable and disable the component every time you move it.
/// Disabling it will remove it from the bounding box tree and enabling it will add it to the bounding box tree again.
///
/// The object should never move unless being dynamic or disabling/enabling it as described above.
/// </summary>
public bool dynamic = true;
/// <summary>
/// Voxel area for mesh.
/// This area (not to be confused with pathfinding areas, this is only used when rasterizing meshes for the recast graph) field
/// can be used to explicitly insert edges in the navmesh geometry or to make some parts of the mesh unwalkable.
/// If the area is set to -1, it will be removed from the resulting navmesh. This is useful if you have some object that you want to be included in the rasterization,
/// but you don't want to let the character walk on it.
///
/// When rasterizing the world and two objects with different area values are adjacent to each other, a split in the navmesh geometry
/// will be added between them, characters will still be able to walk between them, but this can be useful when working with navmesh updates.
///
/// Navmesh updates which recalculate a whole tile (updatePhysics=True) are very slow So if there are special places
/// which you know are going to be updated quite often, for example at a door opening (opened/closed door) you
/// can use areas to create splits on the navmesh for easier updating using normal graph updates (updatePhysics=False).
/// See the below video for more information.
///
/// Video: https://www.youtube.com/watch?v=CS6UypuEMwM
/// </summary>
public int area = 0;
bool _dynamic;
bool registered;
void OnEnable () {
Register();
}
void Register () {
if (registered) return;
registered = true;
//Clamp area, upper limit isn't really a hard limit, but if it gets much higher it will start to interfere with other stuff
area = Mathf.Clamp(area, -1, 1 << 25);
Renderer rend = GetComponent<Renderer>();
Collider coll = GetComponent<Collider>();
if (rend == null && coll == null) throw new System.Exception("A renderer or a collider should be attached to the GameObject");
MeshFilter filter = GetComponent<MeshFilter>();
if (rend != null && filter == null) throw new System.Exception("A renderer was attached but no mesh filter");
// Default to renderer
bounds = rend != null ? rend.bounds : coll.bounds;
_dynamic = dynamic;
if (_dynamic) {
dynamicMeshObjs.Add(this);
} else {
tree.Insert(this);
}
}
/// <summary>Recalculates the internally stored bounds of the object</summary>
private void RecalculateBounds () {
Renderer rend = GetComponent<Renderer>();
Collider coll = GetCollider();
if (rend == null && coll == null) throw new System.Exception("A renderer or a collider should be attached to the GameObject");
MeshFilter filter = GetComponent<MeshFilter>();
if (rend != null && filter == null) throw new System.Exception("A renderer was attached but no mesh filter");
// Default to renderer
bounds = rend != null ? rend.bounds : coll.bounds;
}
/// <summary>Bounds completely enclosing the mesh for this object</summary>
public Bounds GetBounds () {
if (_dynamic) {
RecalculateBounds();
}
return bounds;
}
public MeshFilter GetMeshFilter () {
return GetComponent<MeshFilter>();
}
public Collider GetCollider () {
return GetComponent<Collider>();
}
void OnDisable () {
registered = false;
if (_dynamic) {
dynamicMeshObjs.Remove(this);
} else {
if (!tree.Remove(this)) {
throw new System.Exception("Could not remove RecastMeshObj from tree even though it should exist in it. Has the object moved without being marked as dynamic?");
}
}
_dynamic = dynamic;
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 220345cbaa167417bbe806f230f68615
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 664ab28b7671144dfa4515ea79a4c49e
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ce1c1f6432f234a46b5e914d99379d70

View File

@@ -0,0 +1,4 @@
// This file has been removed from the project. Since UnityPackages cannot
// delete files, only replace them, this message is left here to prevent old
// files from causing compiler errors

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fb87f73fe038f4ee584208f5f1cc7380
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,553 @@
using UnityEngine;
using System.Collections.Generic;
#if NETFX_CORE && !UNITY_EDITOR
//using MarkerMetro.Unity.WinLegacy.IO;
#endif
namespace Pathfinding.Voxels {
/// <summary>Stores a voxel field. </summary>
public class VoxelArea {
public const uint MaxHeight = 65536;
public const int MaxHeightInt = 65536;
/// <summary>
/// Constant for default LinkedVoxelSpan top and bottom values.
/// It is important with the U since ~0 != ~0U
/// This can be used to check if a LinkedVoxelSpan is valid and not just the default span
/// </summary>
public const uint InvalidSpanValue = ~0U;
/// <summary>Initial estimate on the average number of spans (layers) in the voxel representation. Should be greater or equal to 1</summary>
public const float AvgSpanLayerCountEstimate = 8;
/// <summary>The width of the field along the x-axis. [Limit: >= 0] [Units: vx]</summary>
public readonly int width = 0;
/// <summary>The depth of the field along the z-axis. [Limit: >= 0] [Units: vx]</summary>
public readonly int depth = 0;
#if ASTAR_RECAST_CLASS_BASED_LINKED_LIST
public VoxelCell[] cells;
#endif
public CompactVoxelSpan[] compactSpans;
public CompactVoxelCell[] compactCells;
public int compactSpanCount;
public ushort[] tmpUShortArr;
public int[] areaTypes;
public ushort[] dist;
public ushort maxDistance;
public int maxRegions = 0;
public int[] DirectionX;
public int[] DirectionZ;
public Vector3[] VectorDirection;
public void Reset () {
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
ResetLinkedVoxelSpans();
#else
for (int i = 0; i < cells.Length; i++) cells[i].firstSpan = null;
#endif
for (int i = 0; i < compactCells.Length; i++) {
compactCells[i].count = 0;
compactCells[i].index = 0;
}
}
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
private void ResetLinkedVoxelSpans () {
int len = linkedSpans.Length;
linkedSpanCount = width*depth;
LinkedVoxelSpan df = new LinkedVoxelSpan(InvalidSpanValue, InvalidSpanValue, -1, -1);
for (int i = 0; i < len;) {
// 16x unrolling, actually improves performance
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
linkedSpans[i] = df; i++;
}
removedStackCount = 0;
}
#endif
public VoxelArea (int width, int depth) {
this.width = width;
this.depth = depth;
int wd = width*depth;
compactCells = new CompactVoxelCell[wd];
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
// & ~0xF ensures it is a multiple of 16. Required for unrolling
linkedSpans = new LinkedVoxelSpan[((int)(wd*AvgSpanLayerCountEstimate) + 15)& ~0xF];
ResetLinkedVoxelSpans();
#else
cells = new VoxelCell[wd];
#endif
DirectionX = new int[4] { -1, 0, 1, 0 };
DirectionZ = new int[4] { 0, width, 0, -width };
VectorDirection = new Vector3[4] { Vector3.left, Vector3.forward, Vector3.right, Vector3.back };
}
public int GetSpanCountAll () {
int count = 0;
int wd = width*depth;
for (int x = 0; x < wd; x++) {
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
for (int s = x; s != -1 && linkedSpans[s].bottom != InvalidSpanValue; s = linkedSpans[s].next) {
count++;
}
#else
for (VoxelSpan s = cells[x].firstSpan; s != null; s = s.next) {
count++;
}
#endif
}
return count;
}
public int GetSpanCount () {
int count = 0;
int wd = width*depth;
for (int x = 0; x < wd; x++) {
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
for (int s = x; s != -1 && linkedSpans[s].bottom != InvalidSpanValue; s = linkedSpans[s].next) {
if (linkedSpans[s].area != 0) {
count++;
}
}
#else
for (VoxelSpan s = cells[x].firstSpan; s != null; s = s.next) {
if (s.area != 0) {
count++;
}
}
#endif
}
return count;
}
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
private int linkedSpanCount;
public LinkedVoxelSpan[] linkedSpans;
private int[] removedStack = new int[128];
private int removedStackCount = 0;
void PushToSpanRemovedStack (int index) {
// Make sure we don't overflow the list
if (removedStackCount == removedStack.Length) {
// Create a new list to hold recycled values
int[] st2 = new int[removedStackCount*4];
System.Buffer.BlockCopy(removedStack, 0, st2, 0, removedStackCount*sizeof(int));
removedStack = st2;
}
removedStack[removedStackCount] = index;
removedStackCount++;
}
#endif
public void AddLinkedSpan (int index, uint bottom, uint top, int area, int voxelWalkableClimb) {
#if ASTAR_RECAST_CLASS_BASED_LINKED_LIST
cells[index].AddSpan(bottom, top, area, voxelWalkableClimb);
#else
var linkedSpans = this.linkedSpans;
// linkedSpans[index] is the span with the lowest y-coordinate at the position x,z such that index=x+z*width
// i.e linkedSpans is a 2D array laid out in a 1D array (for performance and simplicity)
// Check if there is a root span, otherwise we can just add a new (valid) span and exit
if (linkedSpans[index].bottom == InvalidSpanValue) {
linkedSpans[index] = new LinkedVoxelSpan(bottom, top, area);
return;
}
int prev = -1;
// Original index, the first span we visited
int oindex = index;
while (index != -1) {
var current = linkedSpans[index];
if (current.bottom > top) {
// If the current span's bottom higher up than the span we want to insert's top, then they do not intersect
// and we should just insert a new span here
break;
} else if (current.top < bottom) {
// The current span and the span we want to insert do not intersect
// so just skip to the next span (it might intersect)
prev = index;
index = current.next;
} else {
// Intersection! Merge the spans
// Find the new bottom and top for the merged span
bottom = System.Math.Min(current.bottom, bottom);
top = System.Math.Max(current.top, top);
// voxelWalkableClimb is flagMergeDistance, when a walkable flag is favored before an unwalkable one
// So if a walkable span intersects an unwalkable span, the walkable span can be up to voxelWalkableClimb
// below the unwalkable span and the merged span will still be walkable
if (System.Math.Abs((int)top - (int)current.top) <= voxelWalkableClimb) {
// linkedSpans[index] is the lowest span, but we might use that span's area anyway if it is walkable
area = System.Math.Max(area, current.area);
}
// Find the next span in the linked list
int next = current.next;
if (prev != -1) {
// There is a previous span
// Remove this span from the linked list
linkedSpans[prev].next = next;
// Add this span index to a list for recycling
PushToSpanRemovedStack(index);
// Move to the next span in the list
index = next;
} else if (next != -1) {
// This was the root span and there is a span left in the linked list
// Remove this span from the linked list by assigning the next span as the root span
linkedSpans[oindex] = linkedSpans[next];
// Recycle the old span index
PushToSpanRemovedStack(next);
// Move to the next span in the list
// NOP since we just removed the current span, the next span
// we want to visit will have the same index as we are on now (i.e oindex)
} else {
// This was the root span and there are no other spans in the linked list
// Just replace the root span with the merged span and exit
linkedSpans[oindex] = new LinkedVoxelSpan(bottom, top, area);
return;
}
}
}
// We now have a merged span that needs to be inserted
// and connected with the existing spans
// The new merged span will be inserted right after 'prev' (if it exists, otherwise before index)
// Make sure that we have enough room in our array
if (linkedSpanCount >= linkedSpans.Length) {
LinkedVoxelSpan[] tmp = linkedSpans;
int count = linkedSpanCount;
int popped = removedStackCount;
this.linkedSpans = linkedSpans = new LinkedVoxelSpan[linkedSpans.Length*2];
ResetLinkedVoxelSpans();
linkedSpanCount = count;
removedStackCount = popped;
for (int i = 0; i < linkedSpanCount; i++) {
linkedSpans[i] = tmp[i];
}
}
// Take a node from the recycling stack if possible
// Otherwise create a new node (well, just a new index really)
int nextIndex;
if (removedStackCount > 0) {
removedStackCount--;
nextIndex = removedStack[removedStackCount];
} else {
nextIndex = linkedSpanCount;
linkedSpanCount++;
}
if (prev != -1) {
//span.next = prev.next;
//prev.next = span;
linkedSpans[nextIndex] = new LinkedVoxelSpan(bottom, top, area, linkedSpans[prev].next);
linkedSpans[prev].next = nextIndex;
} else {
//span.next = firstSpan;
//firstSpan = span;
linkedSpans[nextIndex] = linkedSpans[oindex];
linkedSpans[oindex] = new LinkedVoxelSpan(bottom, top, area, nextIndex);
}
#endif
}
}
public struct LinkedVoxelSpan {
public uint bottom;
public uint top;
public int next;
/*Area
* 0 is an unwalkable span (triangle face down)
* 1 is a walkable span (triangle face up)
*/
public int area;
public LinkedVoxelSpan (uint bottom, uint top, int area) {
this.bottom = bottom; this.top = top; this.area = area; this.next = -1;
}
public LinkedVoxelSpan (uint bottom, uint top, int area, int next) {
this.bottom = bottom; this.top = top; this.area = area; this.next = next;
}
}
/// <summary>
/// Represents a mesh which will be rasterized.
/// The vertices will be multiplied with the matrix when rasterizing it to voxels.
/// The vertices and triangles array may be used in multiple instances, it is not changed when voxelizing.
///
/// See: SceneMesh
/// </summary>
public class RasterizationMesh {
/// <summary>
/// Source of the mesh.
/// May be null if the source was not a mesh filter
/// </summary>
public MeshFilter original;
public int area;
public Vector3[] vertices;
public int[] triangles;
/// <summary>
/// Number of vertices in the <see cref="vertices"/> array.
/// The vertices array is often pooled and then it sometimes makes sense to use a larger array than is actually necessary.
/// </summary>
public int numVertices;
/// <summary>
/// Number of triangles in the <see cref="triangles"/> array.
/// The triangles array is often pooled and then it sometimes makes sense to use a larger array than is actually necessary.
/// </summary>
public int numTriangles;
/// <summary>World bounds of the mesh. Assumed to already be multiplied with the matrix</summary>
public Bounds bounds;
public Matrix4x4 matrix;
/// <summary>
/// If true, the vertex and triangle arrays will be pooled after they have been used.
/// Should be used only if the vertex and triangle arrays were originally taken from a pool.
/// </summary>
public bool pool;
public RasterizationMesh () {
}
public RasterizationMesh (Vector3[] vertices, int[] triangles, Bounds bounds) {
matrix = Matrix4x4.identity;
this.vertices = vertices;
this.numVertices = vertices.Length;
this.triangles = triangles;
this.numTriangles = triangles.Length;
this.bounds = bounds;
original = null;
area = 0;
}
public RasterizationMesh (Vector3[] vertices, int[] triangles, Bounds bounds, Matrix4x4 matrix) {
this.matrix = matrix;
this.vertices = vertices;
this.numVertices = vertices.Length;
this.triangles = triangles;
this.numTriangles = triangles.Length;
this.bounds = bounds;
original = null;
area = 0;
}
/// <summary>Recalculate the bounds based on <see cref="vertices"/> and <see cref="matrix"/></summary>
public void RecalculateBounds () {
Bounds b = new Bounds(matrix.MultiplyPoint3x4(vertices[0]), Vector3.zero);
for (int i = 1; i < numVertices; i++) {
b.Encapsulate(matrix.MultiplyPoint3x4(vertices[i]));
}
// Assigned here to avoid changing bounds if vertices would happen to be null
bounds = b;
}
/// <summary>Pool the <see cref="vertices"/> and <see cref="triangles"/> arrays if the <see cref="pool"/> field is true</summary>
public void Pool () {
if (pool) {
Util.ArrayPool<int>.Release(ref triangles);
Util.ArrayPool<Vector3>.Release(ref vertices);
}
}
}
/// <summary>VoxelContourSet used for recast graphs.</summary>
public class VoxelContourSet {
public List<VoxelContour> conts; // Pointer to all contours.
public Bounds bounds; // Bounding box of the heightfield.
}
/// <summary>VoxelContour used for recast graphs.</summary>
public struct VoxelContour {
public int nverts;
public int[] verts; // Vertex coordinates, each vertex contains 4 components.
public int[] rverts; // Raw vertex coordinates, each vertex contains 4 components.
public int reg; // Region ID of the contour.
public int area; // Area ID of the contour.
}
/// <summary>VoxelMesh used for recast graphs.</summary>
public struct VoxelMesh {
/// <summary>Vertices of the mesh</summary>
public Int3[] verts;
/// <summary>
/// Triangles of the mesh.
/// Each element points to a vertex in the <see cref="verts"/> array
/// </summary>
public int[] tris;
/// <summary>Area index for each triangle</summary>
public int[] areas;
}
/// <summary>VoxelCell used for recast graphs.</summary>
public struct VoxelCell {
public VoxelSpan firstSpan;
//public System.Object lockObject;
public void AddSpan (uint bottom, uint top, int area, int voxelWalkableClimb) {
VoxelSpan span = new VoxelSpan(bottom, top, area);
if (firstSpan == null) {
firstSpan = span;
return;
}
VoxelSpan prev = null;
VoxelSpan cSpan = firstSpan;
while (cSpan != null) {
if (cSpan.bottom > span.top) {
break;
} else if (cSpan.top < span.bottom) {
prev = cSpan;
cSpan = cSpan.next;
} else {
if (cSpan.bottom < bottom) {
span.bottom = cSpan.bottom;
}
if (cSpan.top > top) {
span.top = cSpan.top;
}
//1 is flagMergeDistance, when a walkable flag is favored before an unwalkable one
if (System.Math.Abs((int)span.top - (int)cSpan.top) <= voxelWalkableClimb) {
span.area = System.Math.Max(span.area, cSpan.area);
}
VoxelSpan next = cSpan.next;
if (prev != null) {
prev.next = next;
} else {
firstSpan = next;
}
cSpan = next;
}
}
if (prev != null) {
span.next = prev.next;
prev.next = span;
} else {
span.next = firstSpan;
firstSpan = span;
}
}
}
/// <summary>CompactVoxelCell used for recast graphs.</summary>
public struct CompactVoxelCell {
public uint index;
public uint count;
public CompactVoxelCell (uint i, uint c) {
index = i;
count = c;
}
}
/// <summary>CompactVoxelSpan used for recast graphs.</summary>
public struct CompactVoxelSpan {
public ushort y;
public uint con;
public uint h;
public int reg;
public CompactVoxelSpan (ushort bottom, uint height) {
con = 24;
y = bottom;
h = height;
reg = 0;
}
public void SetConnection (int dir, uint value) {
int shift = dir*6;
con = (uint)((con & ~(0x3f << shift)) | ((value & 0x3f) << shift));
}
public int GetConnection (int dir) {
return ((int)con >> dir*6) & 0x3f;
}
}
/// <summary>VoxelSpan used for recast graphs.</summary>
public class VoxelSpan {
public uint bottom;
public uint top;
public VoxelSpan next;
/*Area
* 0 is an unwalkable span (triangle face down)
* 1 is a walkable span (triangle face up)
*/
public int area;
public VoxelSpan (uint b, uint t, int area) {
bottom = b;
top = t;
this.area = area;
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: f5dd44f89a71b4173850829be7460bfa
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,859 @@
using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding.Voxels {
public partial class Voxelize {
public void BuildContours (float maxError, int maxEdgeLength, VoxelContourSet cset, int buildFlags) {
int w = voxelArea.width;
int d = voxelArea.depth;
int wd = w*d;
//cset.bounds = voxelArea.bounds;
int maxContours = Mathf.Max(8 /*Max Regions*/, 8);
//cset.conts = new VoxelContour[maxContours];
List<VoxelContour> contours = new List<VoxelContour>(maxContours);
//cset.nconts = 0;
//NOTE: This array may contain any data, but since we explicitly set all data in it before we use it, it's OK.
ushort[] flags = voxelArea.tmpUShortArr;
if (flags.Length < voxelArea.compactSpanCount) {
flags = voxelArea.tmpUShortArr = new ushort[voxelArea.compactSpanCount];
}
// Mark boundaries. (@?)
for (int z = 0; z < wd; z += voxelArea.width) {
for (int x = 0; x < voxelArea.width; x++) {
CompactVoxelCell c = voxelArea.compactCells[x+z];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
ushort res = 0;
CompactVoxelSpan s = voxelArea.compactSpans[i];
if (s.reg == 0 || (s.reg & BorderReg) == BorderReg) {
flags[i] = 0;
continue;
}
for (int dir = 0; dir < 4; dir++) {
int r = 0;
if (s.GetConnection(dir) != NotConnected) {
int nx = x + voxelArea.DirectionX[dir];
int nz = z + voxelArea.DirectionZ[dir];
int ni = (int)voxelArea.compactCells[nx+nz].index + s.GetConnection(dir);
r = voxelArea.compactSpans[ni].reg;
}
//@TODO - Why isn't this inside the previous IF
if (r == s.reg) {
res |= (ushort)(1 << dir);
}
}
//Inverse, mark non connected edges.
flags[i] = (ushort)(res ^ 0xf);
}
}
}
List<int> verts = Pathfinding.Util.ListPool<int>.Claim(256);//new List<int> (256);
List<int> simplified = Pathfinding.Util.ListPool<int>.Claim(64);//new List<int> (64);
for (int z = 0; z < wd; z += voxelArea.width) {
for (int x = 0; x < voxelArea.width; x++) {
CompactVoxelCell c = voxelArea.compactCells[x+z];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
//CompactVoxelSpan s = voxelArea.compactSpans[i];
if (flags[i] == 0 || flags[i] == 0xf) {
flags[i] = 0;
continue;
}
int reg = voxelArea.compactSpans[i].reg;
if (reg == 0 || (reg & BorderReg) == BorderReg) {
continue;
}
int area = voxelArea.areaTypes[i];
verts.Clear();
simplified.Clear();
WalkContour(x, z, i, flags, verts);
SimplifyContour(verts, simplified, maxError, maxEdgeLength, buildFlags);
RemoveDegenerateSegments(simplified);
VoxelContour contour = new VoxelContour();
contour.verts = Pathfinding.Util.ArrayPool<int>.Claim(simplified.Count);//simplified.ToArray ();
for (int j = 0; j < simplified.Count; j++) contour.verts[j] = simplified[j];
#if ASTAR_RECAST_INCLUDE_RAW_VERTEX_CONTOUR
//Not used at the moment, just debug stuff
contour.rverts = ClaimIntArr(verts.Count);
for (int j = 0; j < verts.Count; j++) contour.rverts[j] = verts[j];
#endif
contour.nverts = simplified.Count/4;
contour.reg = reg;
contour.area = area;
contours.Add(contour);
#if ASTARDEBUG
for (int q = 0, j = (simplified.Count/4)-1; q < (simplified.Count/4); j = q, q++) {
int i4 = q*4;
int j4 = j*4;
Vector3 p1 = Vector3.Scale(
new Vector3(
simplified[i4+0],
simplified[i4+1],
(simplified[i4+2]/(float)voxelArea.width)
),
cellScale)
+voxelOffset;
Vector3 p2 = Vector3.Scale(
new Vector3(
simplified[j4+0],
simplified[j4+1],
(simplified[j4+2]/(float)voxelArea.width)
)
, cellScale)
+voxelOffset;
if (CalcAreaOfPolygon2D(contour.verts, contour.nverts) > 0) {
Debug.DrawLine(p1, p2, AstarMath.IntToColor(reg, 0.5F));
} else {
Debug.DrawLine(p1, p2, Color.red);
}
}
#endif
}
}
}
Pathfinding.Util.ListPool<int>.Release(ref verts);
Pathfinding.Util.ListPool<int>.Release(ref simplified);
// Check and merge droppings.
// Sometimes the previous algorithms can fail and create several contours
// per area. This pass will try to merge the holes into the main region.
for (int i = 0; i < contours.Count; i++) {
VoxelContour cont = contours[i];
// Check if the contour is would backwards.
if (CalcAreaOfPolygon2D(cont.verts, cont.nverts) < 0) {
// Find another contour which has the same region ID.
int mergeIdx = -1;
for (int j = 0; j < contours.Count; j++) {
if (i == j) continue;
if (contours[j].nverts > 0 && contours[j].reg == cont.reg) {
// Make sure the polygon is correctly oriented.
if (CalcAreaOfPolygon2D(contours[j].verts, contours[j].nverts) > 0) {
mergeIdx = j;
break;
}
}
}
if (mergeIdx == -1) {
Debug.LogError("rcBuildContours: Could not find merge target for bad contour "+i+".");
} else {
// Debugging
// Debug.LogWarning ("Fixing contour");
VoxelContour mcont = contours[mergeIdx];
// Merge by closest points.
int ia = 0, ib = 0;
GetClosestIndices(mcont.verts, mcont.nverts, cont.verts, cont.nverts, ref ia, ref ib);
if (ia == -1 || ib == -1) {
Debug.LogWarning("rcBuildContours: Failed to find merge points for "+i+" and "+mergeIdx+".");
continue;
}
#if ASTARDEBUG
int p4 = ia*4;
int p42 = ib*4;
Vector3 p12 = Vector3.Scale(
new Vector3(
mcont.verts[p4+0],
mcont.verts[p4+1],
(mcont.verts[p4+2]/(float)voxelArea.width)
),
cellScale)
+voxelOffset;
Vector3 p22 = Vector3.Scale(
new Vector3(
cont.verts[p42+0],
cont.verts[p42+1],
(cont.verts[p42+2]/(float)voxelArea.width)
)
, cellScale)
+voxelOffset;
Debug.DrawLine(p12, p22, Color.green);
#endif
if (!MergeContours(ref mcont, ref cont, ia, ib)) {
Debug.LogWarning("rcBuildContours: Failed to merge contours "+i+" and "+mergeIdx+".");
continue;
}
contours[mergeIdx] = mcont;
contours[i] = cont;
#if ASTARDEBUG
Debug.Log(mcont.nverts);
for (int q = 0, j = (mcont.nverts)-1; q < (mcont.nverts); j = q, q++) {
int i4 = q*4;
int j4 = j*4;
Vector3 p1 = Vector3.Scale(
new Vector3(
mcont.verts[i4+0],
mcont.verts[i4+1],
(mcont.verts[i4+2]/(float)voxelArea.width)
),
cellScale)
+voxelOffset;
Vector3 p2 = Vector3.Scale(
new Vector3(
mcont.verts[j4+0],
mcont.verts[j4+1],
(mcont.verts[j4+2]/(float)voxelArea.width)
)
, cellScale)
+voxelOffset;
Debug.DrawLine(p1, p2, Color.red);
//}
}
#endif
}
}
}
cset.conts = contours;
}
void GetClosestIndices (int[] vertsa, int nvertsa,
int[] vertsb, int nvertsb,
ref int ia, ref int ib) {
int closestDist = 0xfffffff;
ia = -1;
ib = -1;
for (int i = 0; i < nvertsa; i++) {
//in is a keyword in C#, so I can't use that as a variable name
int in2 = (i+1) % nvertsa;
int ip = (i+nvertsa-1) % nvertsa;
int va = i*4;
int van = in2*4;
int vap = ip*4;
for (int j = 0; j < nvertsb; ++j) {
int vb = j*4;
// vb must be "infront" of va.
if (Ileft(vap, va, vb, vertsa, vertsa, vertsb) && Ileft(va, van, vb, vertsa, vertsa, vertsb)) {
int dx = vertsb[vb+0] - vertsa[va+0];
int dz = (vertsb[vb+2]/voxelArea.width) - (vertsa[va+2]/voxelArea.width);
int d = dx*dx + dz*dz;
if (d < closestDist) {
ia = i;
ib = j;
closestDist = d;
}
}
}
}
}
/// <summary>Releases contents of a contour set to caches</summary>
static void ReleaseContours (VoxelContourSet cset) {
for (int i = 0; i < cset.conts.Count; i++) {
VoxelContour cont = cset.conts[i];
Pathfinding.Util.ArrayPool<int>.Release(ref cont.verts);
Pathfinding.Util.ArrayPool<int>.Release(ref cont.rverts);
}
cset.conts = null;
}
public static bool MergeContours (ref VoxelContour ca, ref VoxelContour cb, int ia, int ib) {
int maxVerts = ca.nverts + cb.nverts + 2;
int[] verts = Pathfinding.Util.ArrayPool<int>.Claim(maxVerts*4);
//if (!verts)
// return false;
int nv = 0;
// Copy contour A.
for (int i = 0; i <= ca.nverts; i++) {
int dst = nv*4;
int src = ((ia+i) % ca.nverts)*4;
verts[dst+0] = ca.verts[src+0];
verts[dst+1] = ca.verts[src+1];
verts[dst+2] = ca.verts[src+2];
verts[dst+3] = ca.verts[src+3];
nv++;
}
// Copy contour B
for (int i = 0; i <= cb.nverts; i++) {
int dst = nv*4;
int src = ((ib+i) % cb.nverts)*4;
verts[dst+0] = cb.verts[src+0];
verts[dst+1] = cb.verts[src+1];
verts[dst+2] = cb.verts[src+2];
verts[dst+3] = cb.verts[src+3];
nv++;
}
Pathfinding.Util.ArrayPool<int>.Release(ref ca.verts);
Pathfinding.Util.ArrayPool<int>.Release(ref cb.verts);
ca.verts = verts;
ca.nverts = nv;
cb.verts = Pathfinding.Util.ArrayPool<int>.Claim(0);
cb.nverts = 0;
return true;
}
public void SimplifyContour (List<int> verts, List<int> simplified, float maxError, int maxEdgeLenght, int buildFlags) {
// Add initial points.
bool hasConnections = false;
for (int i = 0; i < verts.Count; i += 4) {
if ((verts[i+3] & ContourRegMask) != 0) {
hasConnections = true;
break;
}
}
if (hasConnections) {
// The contour has some portals to other regions.
// Add a new point to every location where the region changes.
for (int i = 0, ni = verts.Count/4; i < ni; i++) {
int ii = (i+1) % ni;
bool differentRegs = (verts[i*4+3] & ContourRegMask) != (verts[ii*4+3] & ContourRegMask);
bool areaBorders = (verts[i*4+3] & RC_AREA_BORDER) != (verts[ii*4+3] & RC_AREA_BORDER);
if (differentRegs || areaBorders) {
simplified.Add(verts[i*4+0]);
simplified.Add(verts[i*4+1]);
simplified.Add(verts[i*4+2]);
simplified.Add(i);
}
}
}
if (simplified.Count == 0) {
// If there is no connections at all,
// create some initial points for the simplification process.
// Find lower-left and upper-right vertices of the contour.
int llx = verts[0];
int lly = verts[1];
int llz = verts[2];
int lli = 0;
int urx = verts[0];
int ury = verts[1];
int urz = verts[2];
int uri = 0;
for (int i = 0; i < verts.Count; i += 4) {
int x = verts[i+0];
int y = verts[i+1];
int z = verts[i+2];
if (x < llx || (x == llx && z < llz)) {
llx = x;
lly = y;
llz = z;
lli = i/4;
}
if (x > urx || (x == urx && z > urz)) {
urx = x;
ury = y;
urz = z;
uri = i/4;
}
}
simplified.Add(llx);
simplified.Add(lly);
simplified.Add(llz);
simplified.Add(lli);
simplified.Add(urx);
simplified.Add(ury);
simplified.Add(urz);
simplified.Add(uri);
}
// Add points until all raw points are within
// error tolerance to the simplified shape.
// This uses the Douglas-Peucker algorithm.
int pn = verts.Count/4;
//Use the max squared error instead
maxError *= maxError;
for (int i = 0; i < simplified.Count/4;) {
int ii = (i+1) % (simplified.Count/4);
int ax = simplified[i*4+0];
int az = simplified[i*4+2];
int ai = simplified[i*4+3];
int bx = simplified[ii*4+0];
int bz = simplified[ii*4+2];
int bi = simplified[ii*4+3];
// Find maximum deviation from the segment.
float maxd = 0;
int maxi = -1;
int ci, cinc, endi;
// Traverse the segment in lexilogical order so that the
// max deviation is calculated similarly when traversing
// opposite segments.
if (bx > ax || (bx == ax && bz > az)) {
cinc = 1;
ci = (ai+cinc) % pn;
endi = bi;
} else {
cinc = pn-1;
ci = (bi+cinc) % pn;
endi = ai;
Util.Memory.Swap(ref ax, ref bx);
Util.Memory.Swap(ref az, ref bz);
}
// Tessellate only outer edges or edges between areas.
if ((verts[ci*4+3] & ContourRegMask) == 0 ||
(verts[ci*4+3] & RC_AREA_BORDER) == RC_AREA_BORDER) {
while (ci != endi) {
float d2 = VectorMath.SqrDistancePointSegmentApproximate(verts[ci*4+0], verts[ci*4+2]/voxelArea.width, ax, az/voxelArea.width, bx, bz/voxelArea.width);
if (d2 > maxd) {
maxd = d2;
maxi = ci;
}
ci = (ci+cinc) % pn;
}
}
// If the max deviation is larger than accepted error,
// add new point, else continue to next segment.
if (maxi != -1 && maxd > maxError) {
// Add space for the new point.
//simplified.resize(simplified.size()+4);
simplified.Add(0);
simplified.Add(0);
simplified.Add(0);
simplified.Add(0);
int n = simplified.Count/4;
for (int j = n-1; j > i; --j) {
simplified[j*4+0] = simplified[(j-1)*4+0];
simplified[j*4+1] = simplified[(j-1)*4+1];
simplified[j*4+2] = simplified[(j-1)*4+2];
simplified[j*4+3] = simplified[(j-1)*4+3];
}
// Add the point.
simplified[(i+1)*4+0] = verts[maxi*4+0];
simplified[(i+1)*4+1] = verts[maxi*4+1];
simplified[(i+1)*4+2] = verts[maxi*4+2];
simplified[(i+1)*4+3] = maxi;
} else {
i++;
}
}
//Split too long edges
float maxEdgeLen = maxEdgeLength / cellSize;
if (maxEdgeLen > 0 && (buildFlags & (RC_CONTOUR_TESS_WALL_EDGES|RC_CONTOUR_TESS_AREA_EDGES|RC_CONTOUR_TESS_TILE_EDGES)) != 0) {
for (int i = 0; i < simplified.Count/4;) {
if (simplified.Count/4 > 200) {
break;
}
int ii = (i+1) % (simplified.Count/4);
int ax = simplified[i*4+0];
int az = simplified[i*4+2];
int ai = simplified[i*4+3];
int bx = simplified[ii*4+0];
int bz = simplified[ii*4+2];
int bi = simplified[ii*4+3];
// Find maximum deviation from the segment.
int maxi = -1;
int ci = (ai+1) % pn;
// Tessellate only outer edges or edges between areas.
bool tess = false;
// Wall edges.
if ((buildFlags & RC_CONTOUR_TESS_WALL_EDGES) != 0 && (verts[ci*4+3] & ContourRegMask) == 0)
tess = true;
// Edges between areas.
if ((buildFlags & RC_CONTOUR_TESS_AREA_EDGES) != 0 && (verts[ci*4+3] & RC_AREA_BORDER) == RC_AREA_BORDER)
tess = true;
// Border of tile
if ((buildFlags & RC_CONTOUR_TESS_TILE_EDGES) != 0 && (verts[ci*4+3] & BorderReg) == BorderReg)
tess = true;
if (tess) {
int dx = bx - ax;
int dz = (bz/voxelArea.width) - (az/voxelArea.width);
if (dx*dx + dz*dz > maxEdgeLen*maxEdgeLen) {
// Round based on the segments in lexilogical order so that the
// max tesselation is consistent regardles in which direction
// segments are traversed.
int n = bi < ai ? (bi+pn - ai) : (bi - ai);
if (n > 1) {
if (bx > ax || (bx == ax && bz > az)) {
maxi = (ai + n/2) % pn;
} else {
maxi = (ai + (n+1)/2) % pn;
}
}
}
}
// If the max deviation is larger than accepted error,
// add new point, else continue to next segment.
if (maxi != -1) {
// Add space for the new point.
//simplified.resize(simplified.size()+4);
simplified.AddRange(new int[4]);
int n = simplified.Count/4;
for (int j = n-1; j > i; --j) {
simplified[j*4+0] = simplified[(j-1)*4+0];
simplified[j*4+1] = simplified[(j-1)*4+1];
simplified[j*4+2] = simplified[(j-1)*4+2];
simplified[j*4+3] = simplified[(j-1)*4+3];
}
// Add the point.
simplified[(i+1)*4+0] = verts[maxi*4+0];
simplified[(i+1)*4+1] = verts[maxi*4+1];
simplified[(i+1)*4+2] = verts[maxi*4+2];
simplified[(i+1)*4+3] = maxi;
} else {
++i;
}
}
}
for (int i = 0; i < simplified.Count/4; i++) {
// The edge vertex flag is take from the current raw point,
// and the neighbour region is take from the next raw point.
int ai = (simplified[i*4+3]+1) % pn;
int bi = simplified[i*4+3];
simplified[i*4+3] = (verts[ai*4+3] & ContourRegMask) | (verts[bi*4+3] & RC_BORDER_VERTEX);
}
}
public void WalkContour (int x, int z, int i, ushort[] flags, List<int> verts) {
// Choose the first non-connected edge
int dir = 0;
while ((flags[i] & (ushort)(1 << dir)) == 0) {
dir++;
}
int startDir = dir;
int startI = i;
int area = voxelArea.areaTypes[i];
int iter = 0;
#if ASTARDEBUG
Vector3 previousPos;
Vector3 currentPos;
previousPos = ConvertPos(
x,
0,
z
);
Vector3 previousPos2 = ConvertPos(
x,
0,
z
);
#endif
while (iter++ < 40000) {
//Are we facing a region edge
if ((flags[i] & (ushort)(1 << dir)) != 0) {
#if ASTARDEBUG
Vector3 pos = ConvertPos(x, 0, z)+new Vector3((voxelArea.DirectionX[dir] != 0) ? Mathf.Sign(voxelArea.DirectionX[dir]) : 0, 0, (voxelArea.DirectionZ[dir]) != 0 ? Mathf.Sign(voxelArea.DirectionZ[dir]) : 0)*0.6F;
//int dir2 = (dir+1) & 0x3;
//pos += new Vector3 ((voxelArea.DirectionX[dir2] != 0) ? Mathf.Sign(voxelArea.DirectionX[dir2]) : 0,0,(voxelArea.DirectionZ[dir2]) != 0 ? Mathf.Sign(voxelArea.DirectionZ[dir2]) : 0)*1.2F;
//Debug.DrawLine (ConvertPos (x,0,z),pos,Color.cyan);
Debug.DrawLine(previousPos2, pos, Color.blue);
previousPos2 = pos;
#endif
//Choose the edge corner
bool isBorderVertex = false;
bool isAreaBorder = false;
int px = x;
int py = GetCornerHeight(x, z, i, dir, ref isBorderVertex);
int pz = z;
switch (dir) {
case 0: pz += voxelArea.width;; break;
case 1: px++; pz += voxelArea.width; break;
case 2: px++; break;
}
/*case 1: px++; break;
* case 2: px++; pz += voxelArea.width; break;
* case 3: pz += voxelArea.width; break;
*/
int r = 0;
CompactVoxelSpan s = voxelArea.compactSpans[i];
if (s.GetConnection(dir) != NotConnected) {
int nx = x + voxelArea.DirectionX[dir];
int nz = z + voxelArea.DirectionZ[dir];
int ni = (int)voxelArea.compactCells[nx+nz].index + s.GetConnection(dir);
r = (int)voxelArea.compactSpans[ni].reg;
if (area != voxelArea.areaTypes[ni]) {
isAreaBorder = true;
}
}
if (isBorderVertex) {
r |= RC_BORDER_VERTEX;
}
if (isAreaBorder) {
r |= RC_AREA_BORDER;
}
verts.Add(px);
verts.Add(py);
verts.Add(pz);
verts.Add(r);
//Debug.DrawRay (previousPos,new Vector3 ((dir == 1 || dir == 2) ? 1 : 0, 0, (dir == 0 || dir == 1) ? 1 : 0),Color.cyan);
flags[i] = (ushort)(flags[i] & ~(1 << dir)); // Remove visited edges
dir = (dir+1) & 0x3; // Rotate CW
} else {
int ni = -1;
int nx = x + voxelArea.DirectionX[dir];
int nz = z + voxelArea.DirectionZ[dir];
CompactVoxelSpan s = voxelArea.compactSpans[i];
if (s.GetConnection(dir) != NotConnected) {
CompactVoxelCell nc = voxelArea.compactCells[nx+nz];
ni = (int)nc.index + s.GetConnection(dir);
}
if (ni == -1) {
Debug.LogWarning("Degenerate triangles might have been generated.\n" +
"Usually this is not a problem, but if you have a static level, try to modify the graph settings slightly to avoid this edge case.");
return;
}
x = nx;
z = nz;
i = ni;
// & 0x3 is the same as % 4 (modulo 4)
dir = (dir+3) & 0x3; // Rotate CCW
#if ASTARDEBUG
currentPos = ConvertPos(
x,
0,
z
);
Debug.DrawLine(previousPos+Vector3.up*0, currentPos, Color.blue);
previousPos = currentPos;
#endif
}
if (startI == i && startDir == dir) {
break;
}
}
#if ASTARDEBUG
Color col = new Color(Random.value, Random.value, Random.value);
for (int q = 0, j = (verts.Count/4)-1; q < (verts.Count/4); j = q, q++) {
int i4 = q*4;
int j4 = j*4;
Vector3 p1 = ConvertPosWithoutOffset(
verts[i4+0],
verts[i4+1],
verts[i4+2]
);
Vector3 p2 = ConvertPosWithoutOffset(
verts[j4+0],
verts[j4+1],
verts[j4+2]
);
Debug.DrawLine(p1, p2, col);
}
#endif
}
public int GetCornerHeight (int x, int z, int i, int dir, ref bool isBorderVertex) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
int ch = (int)s.y;
//dir + clockwise direction
int dirp = (dir+1) & 0x3;
//int dirp = (dir+3) & 0x3;
uint[] regs = new uint[4];
regs[0] = (uint)voxelArea.compactSpans[i].reg | ((uint)voxelArea.areaTypes[i] << 16);
if (s.GetConnection(dir) != NotConnected) {
int nx = x + voxelArea.DirectionX[dir];
int nz = z + voxelArea.DirectionZ[dir];
int ni = (int)voxelArea.compactCells[nx+nz].index + s.GetConnection(dir);
CompactVoxelSpan ns = voxelArea.compactSpans[ni];
ch = System.Math.Max(ch, (int)ns.y);
regs[1] = (uint)ns.reg | ((uint)voxelArea.areaTypes[ni] << 16);
if (ns.GetConnection(dirp) != NotConnected) {
int nx2 = nx + voxelArea.DirectionX[dirp];
int nz2 = nz + voxelArea.DirectionZ[dirp];
int ni2 = (int)voxelArea.compactCells[nx2+nz2].index + ns.GetConnection(dirp);
CompactVoxelSpan ns2 = voxelArea.compactSpans[ni2];
ch = System.Math.Max(ch, (int)ns2.y);
regs[2] = (uint)ns2.reg | ((uint)voxelArea.areaTypes[ni2] << 16);
}
}
if (s.GetConnection(dirp) != NotConnected) {
int nx = x + voxelArea.DirectionX[dirp];
int nz = z + voxelArea.DirectionZ[dirp];
int ni = (int)voxelArea.compactCells[nx+nz].index + s.GetConnection(dirp);
CompactVoxelSpan ns = voxelArea.compactSpans[ni];
ch = System.Math.Max(ch, (int)ns.y);
regs[3] = (uint)ns.reg | ((uint)voxelArea.areaTypes[ni] << 16);
if (ns.GetConnection(dir) != NotConnected) {
int nx2 = nx + voxelArea.DirectionX[dir];
int nz2 = nz + voxelArea.DirectionZ[dir];
int ni2 = (int)voxelArea.compactCells[nx2+nz2].index + ns.GetConnection(dir);
CompactVoxelSpan ns2 = voxelArea.compactSpans[ni2];
ch = System.Math.Max(ch, (int)ns2.y);
regs[2] = (uint)ns2.reg | ((uint)voxelArea.areaTypes[ni2] << 16);
}
}
// Check if the vertex is special edge vertex, these vertices will be removed later.
for (int j = 0; j < 4; ++j) {
int a = j;
int b = (j+1) & 0x3;
int c = (j+2) & 0x3;
int d = (j+3) & 0x3;
// The vertex is a border vertex there are two same exterior cells in a row,
// followed by two interior cells and none of the regions are out of bounds.
bool twoSameExts = (regs[a] & regs[b] & BorderReg) != 0 && regs[a] == regs[b];
bool twoInts = ((regs[c] | regs[d]) & BorderReg) == 0;
bool intsSameArea = (regs[c]>>16) == (regs[d]>>16);
bool noZeros = regs[a] != 0 && regs[b] != 0 && regs[c] != 0 && regs[d] != 0;
if (twoSameExts && twoInts && intsSameArea && noZeros) {
isBorderVertex = true;
break;
}
}
return ch;
}
public void RemoveDegenerateSegments (List<int> simplified) {
// Remove adjacent vertices which are equal on xz-plane,
// or else the triangulator will get confused
for (int i = 0; i < simplified.Count/4; i++) {
int ni = i+1;
if (ni >= (simplified.Count/4))
ni = 0;
if (simplified[i*4+0] == simplified[ni*4+0] &&
simplified[i*4+2] == simplified[ni*4+2]) {
// Degenerate segment, remove.
simplified.RemoveRange(i, 4);
}
}
}
public int CalcAreaOfPolygon2D (int[] verts, int nverts) {
int area = 0;
for (int i = 0, j = nverts-1; i < nverts; j = i++) {
int vi = i*4;
int vj = j*4;
area += verts[vi+0] * (verts[vj+2]/voxelArea.width) - verts[vj+0] * (verts[vi+2]/voxelArea.width);
}
return (area+1) / 2;
}
public static bool Ileft (int a, int b, int c, int[] va, int[] vb, int[] vc) {
return (vb[b+0] - va[a+0]) * (vc[c+2] - va[a+2]) - (vc[c+0] - va[a+0]) * (vb[b+2] - va[a+2]) <= 0;
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e3bbeea6925814e4d864e0ba4e4ee932
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,350 @@
using UnityEngine;
namespace Pathfinding.Voxels {
using Pathfinding.Util;
public partial class Voxelize {
/// <summary>
/// Returns T iff (v_i, v_j) is a proper internal
/// diagonal of P.
/// </summary>
public static bool Diagonal (int i, int j, int n, int[] verts, int[] indices) {
return InCone(i, j, n, verts, indices) && Diagonalie(i, j, n, verts, indices);
}
public static bool InCone (int i, int j, int n, int[] verts, int[] indices) {
int pi = (indices[i] & 0x0fffffff) * 4;
int pj = (indices[j] & 0x0fffffff) * 4;
int pi1 = (indices[Next(i, n)] & 0x0fffffff) * 4;
int pin1 = (indices[Prev(i, n)] & 0x0fffffff) * 4;
// If P[i] is a convex vertex [ i+1 left or on (i-1,i) ].
if (LeftOn(pin1, pi, pi1, verts))
return Left(pi, pj, pin1, verts) && Left(pj, pi, pi1, verts);
// Assume (i-1,i,i+1) not collinear.
// else P[i] is reflex.
return !(LeftOn(pi, pj, pi1, verts) && LeftOn(pj, pi, pin1, verts));
}
/// <summary>
/// Returns true iff c is strictly to the left of the directed
/// line through a to b.
/// </summary>
public static bool Left (int a, int b, int c, int[] verts) {
return Area2(a, b, c, verts) < 0;
}
public static bool LeftOn (int a, int b, int c, int[] verts) {
return Area2(a, b, c, verts) <= 0;
}
public static bool Collinear (int a, int b, int c, int[] verts) {
return Area2(a, b, c, verts) == 0;
}
public static int Area2 (int a, int b, int c, int[] verts) {
return (verts[b] - verts[a]) * (verts[c+2] - verts[a+2]) - (verts[c+0] - verts[a+0]) * (verts[b+2] - verts[a+2]);
}
/// <summary>
/// Returns T iff (v_i, v_j) is a proper internal *or* external
/// diagonal of P, *ignoring edges incident to v_i and v_j*.
/// </summary>
static bool Diagonalie (int i, int j, int n, int[] verts, int[] indices) {
int d0 = (indices[i] & 0x0fffffff) * 4;
int d1 = (indices[j] & 0x0fffffff) * 4;
/*int a = (i+1) % indices.Length;
* if (a == j) a = (i-1 + indices.Length) % indices.Length;
* int a_v = (indices[a] & 0x0fffffff) * 4;
*
* if (a != j && Collinear (d0,a_v,d1,verts)) {
* return false;
* }*/
// For each edge (k,k+1) of P
for (int k = 0; k < n; k++) {
int k1 = Next(k, n);
// Skip edges incident to i or j
if (!((k == i) || (k1 == i) || (k == j) || (k1 == j))) {
int p0 = (indices[k] & 0x0fffffff) * 4;
int p1 = (indices[k1] & 0x0fffffff) * 4;
if (Vequal(d0, p0, verts) || Vequal(d1, p0, verts) || Vequal(d0, p1, verts) || Vequal(d1, p1, verts))
continue;
if (Intersect(d0, d1, p0, p1, verts))
return false;
}
}
return true;
}
// Exclusive or: true iff exactly one argument is true.
// The arguments are negated to ensure that they are 0/1
// values. Then the bitwise Xor operator may apply.
// (This idea is due to Michael Baldwin.)
public static bool Xorb (bool x, bool y) {
return !x ^ !y;
}
// Returns true iff ab properly intersects cd: they share
// a point interior to both segments. The properness of the
// intersection is ensured by using strict leftness.
public static bool IntersectProp (int a, int b, int c, int d, int[] verts) {
// Eliminate improper cases.
if (Collinear(a, b, c, verts) || Collinear(a, b, d, verts) ||
Collinear(c, d, a, verts) || Collinear(c, d, b, verts))
return false;
return Xorb(Left(a, b, c, verts), Left(a, b, d, verts)) && Xorb(Left(c, d, a, verts), Left(c, d, b, verts));
}
// Returns T iff (a,b,c) are collinear and point c lies
// on the closed segement ab.
static bool Between (int a, int b, int c, int[] verts) {
if (!Collinear(a, b, c, verts))
return false;
// If ab not vertical, check betweenness on x; else on y.
if (verts[a+0] != verts[b+0])
return ((verts[a+0] <= verts[c+0]) && (verts[c+0] <= verts[b+0])) || ((verts[a+0] >= verts[c+0]) && (verts[c+0] >= verts[b+0]));
else
return ((verts[a+2] <= verts[c+2]) && (verts[c+2] <= verts[b+2])) || ((verts[a+2] >= verts[c+2]) && (verts[c+2] >= verts[b+2]));
}
// Returns true iff segments ab and cd intersect, properly or improperly.
static bool Intersect (int a, int b, int c, int d, int[] verts) {
if (IntersectProp(a, b, c, d, verts))
return true;
else if (Between(a, b, c, verts) || Between(a, b, d, verts) ||
Between(c, d, a, verts) || Between(c, d, b, verts))
return true;
else
return false;
}
static bool Vequal (int a, int b, int[] verts) {
return verts[a+0] == verts[b+0] && verts[a+2] == verts[b+2];
}
/// <summary>(i-1+n) % n assuming 0 <= i < n</summary>
public static int Prev (int i, int n) { return i-1 >= 0 ? i-1 : n-1; }
/// <summary>(i+1) % n assuming 0 <= i < n</summary>
public static int Next (int i, int n) { return i+1 < n ? i+1 : 0; }
/// <summary>
/// Builds a polygon mesh from a contour set.
///
/// Warning: Currently locked to 3.
/// </summary>
/// <param name="cset">contour set to build a mesh from.</param>
/// <param name="nvp">Maximum allowed vertices per polygon.</param>
/// <param name="mesh">Results will be written to this mesh.</param>
public void BuildPolyMesh (VoxelContourSet cset, int nvp, out VoxelMesh mesh) {
nvp = 3;
int maxVertices = 0;
int maxTris = 0;
int maxVertsPerCont = 0;
for (int i = 0; i < cset.conts.Count; i++) {
// Skip null contours.
if (cset.conts[i].nverts < 3) continue;
maxVertices += cset.conts[i].nverts;
maxTris += cset.conts[i].nverts - 2;
maxVertsPerCont = System.Math.Max(maxVertsPerCont, cset.conts[i].nverts);
}
Int3[] verts = ArrayPool<Int3>.Claim(maxVertices);
int[] polys = ArrayPool<int>.Claim(maxTris*nvp);
int[] areas = ArrayPool<int>.Claim(maxTris);
Pathfinding.Util.Memory.MemSet<int>(polys, 0xff, sizeof(int));
int[] indices = ArrayPool<int>.Claim(maxVertsPerCont);
int[] tris = ArrayPool<int>.Claim(maxVertsPerCont*3);
int vertexIndex = 0;
int polyIndex = 0;
int areaIndex = 0;
for (int i = 0; i < cset.conts.Count; i++) {
VoxelContour cont = cset.conts[i];
// Skip degenerate contours
if (cont.nverts < 3) {
continue;
}
for (int j = 0; j < cont.nverts; j++) {
indices[j] = j;
// Convert the z coordinate from the form z*voxelArea.width which is used in other places for performance
cont.verts[j*4+2] /= voxelArea.width;
}
// Triangulate the contour
int ntris = Triangulate(cont.nverts, cont.verts, ref indices, ref tris);
// Assign the correct vertex indices
int startIndex = vertexIndex;
for (int j = 0; j < ntris*3; polyIndex++, j++) {
//@Error sometimes
polys[polyIndex] = tris[j]+startIndex;
}
// Mark all triangles generated by this contour
// as having the area cont.area
for (int j = 0; j < ntris; areaIndex++, j++) {
areas[areaIndex] = cont.area;
}
// Copy the vertex positions
for (int j = 0; j < cont.nverts; vertexIndex++, j++) {
verts[vertexIndex] = new Int3(cont.verts[j*4], cont.verts[j*4+1], cont.verts[j*4+2]);
}
}
mesh = new VoxelMesh {
verts = Memory.ShrinkArray(verts, vertexIndex),
tris = Memory.ShrinkArray(polys, polyIndex),
areas = Memory.ShrinkArray(areas, areaIndex)
};
ArrayPool<Int3>.Release(ref verts);
ArrayPool<int>.Release(ref polys);
ArrayPool<int>.Release(ref areas);
ArrayPool<int>.Release(ref indices);
ArrayPool<int>.Release(ref tris);
}
int Triangulate (int n, int[] verts, ref int[] indices, ref int[] tris) {
int ntris = 0;
int[] dst = tris;
int dstIndex = 0;
// Debug code
//int on = n;
// The last bit of the index is used to indicate if the vertex can be removed.
for (int i = 0; i < n; i++) {
int i1 = Next(i, n);
int i2 = Next(i1, n);
if (Diagonal(i, i2, n, verts, indices)) {
indices[i1] |= 0x40000000;
}
}
while (n > 3) {
#if ASTARDEBUG
for (int j = 0; j < n; j++) {
DrawLine(Prev(j, n), j, indices, verts, Color.red);
}
#endif
int minLen = -1;
int mini = -1;
for (int q = 0; q < n; q++) {
int q1 = Next(q, n);
if ((indices[q1] & 0x40000000) != 0) {
int p0 = (indices[q] & 0x0fffffff) * 4;
int p2 = (indices[Next(q1, n)] & 0x0fffffff) * 4;
int dx = verts[p2+0] - verts[p0+0];
int dz = verts[p2+2] - verts[p0+2];
#if ASTARDEBUG
DrawLine(q, Next(q1, n), indices, verts, Color.blue);
#endif
//Squared distance
int len = dx*dx + dz*dz;
if (minLen < 0 || len < minLen) {
minLen = len;
mini = q;
}
}
}
if (mini == -1) {
Debug.LogWarning("Degenerate triangles might have been generated.\n" +
"Usually this is not a problem, but if you have a static level, try to modify the graph settings slightly to avoid this edge case.");
// Can't run the debug stuff because we are likely running from a separate thread
//for (int j=0;j<on;j++) {
// DrawLine (Prev(j,on),j,indices,verts,Color.red);
//}
// Should not happen.
/* printf("mini == -1 ntris=%d n=%d\n", ntris, n);
* for (int i = 0; i < n; i++)
* {
* printf("%d ", indices[i] & 0x0fffffff);
* }
* printf("\n");*/
//yield break;
return -ntris;
}
int i = mini;
int i1 = Next(i, n);
int i2 = Next(i1, n);
#if ASTARDEBUG
for (int j = 0; j < n; j++) {
DrawLine(Prev(j, n), j, indices, verts, Color.red);
}
DrawLine(i, i2, indices, verts, Color.magenta);
for (int j = 0; j < n; j++) {
DrawLine(Prev(j, n), j, indices, verts, Color.red);
}
#endif
dst[dstIndex] = indices[i] & 0x0fffffff;
dstIndex++;
dst[dstIndex] = indices[i1] & 0x0fffffff;
dstIndex++;
dst[dstIndex] = indices[i2] & 0x0fffffff;
dstIndex++;
ntris++;
// Removes P[i1] by copying P[i+1]...P[n-1] left one index.
n--;
for (int k = i1; k < n; k++) {
indices[k] = indices[k+1];
}
if (i1 >= n) i1 = 0;
i = Prev(i1, n);
// Update diagonal flags.
if (Diagonal(Prev(i, n), i1, n, verts, indices)) {
indices[i] |= 0x40000000;
} else {
indices[i] &= 0x0fffffff;
}
if (Diagonal(i, Next(i1, n), n, verts, indices)) {
indices[i1] |= 0x40000000;
} else {
indices[i1] &= 0x0fffffff;
}
}
dst[dstIndex] = indices[0] & 0x0fffffff;
dstIndex++;
dst[dstIndex] = indices[1] & 0x0fffffff;
dstIndex++;
dst[dstIndex] = indices[2] & 0x0fffffff;
dstIndex++;
ntris++;
return ntris;
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 66ea80679290d45778de562ccdf7d3cd
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,201 @@
namespace Pathfinding.Voxels {
/// <summary>Utility for clipping polygons</summary>
internal struct VoxelPolygonClipper {
public float[] x;
public float[] y;
public float[] z;
public int n;
public VoxelPolygonClipper (int capacity) {
x = new float[capacity];
y = new float[capacity];
z = new float[capacity];
n = 0;
}
public UnityEngine.Vector3 this[int i] {
set {
x[i] = value.x;
y[i] = value.y;
z[i] = value.z;
}
}
/// <summary>
/// Clips a polygon against an axis aligned half plane.
/// The polygons stored in this object are clipped against the half plane at x = -offset.
/// </summary>
/// <param name="result">Ouput vertices</param>
/// <param name="multi">Scale factor for the input vertices. Should be +1 or -1. If -1 the negative half plane is kept.</param>
/// <param name="offset">Offset to move the input vertices with before cutting</param>
public void ClipPolygonAlongX (ref VoxelPolygonClipper result, float multi, float offset) {
// Number of resulting vertices
int m = 0;
bool prev, curr;
float dj = multi*x[(n-1)]+offset;
for (int i = 0, j = n-1; i < n; j = i, i++) {
float di = multi*x[i]+offset;
prev = dj >= 0;
curr = di >= 0;
if (prev != curr) {
float s = dj / (dj - di);
result.x[m] = x[j] + (x[i]-x[j])*s;
result.y[m] = y[j] + (y[i]-y[j])*s;
result.z[m] = z[j] + (z[i]-z[j])*s;
m++;
}
if (curr) {
result.x[m] = x[i];
result.y[m] = y[i];
result.z[m] = z[i];
m++;
}
dj = di;
}
result.n = m;
}
/// <summary>
/// Clips a polygon against an axis aligned half plane.
/// The polygons stored in this object are clipped against the half plane at z = -offset.
/// </summary>
/// <param name="result">Ouput vertices. Only the Y and Z coordinates are calculated. The X coordinates are undefined.</param>
/// <param name="multi">Scale factor for the input vertices. Should be +1 or -1. If -1 the negative half plane is kept.</param>
/// <param name="offset">Offset to move the input vertices with before cutting</param>
public void ClipPolygonAlongZWithYZ (ref VoxelPolygonClipper result, float multi, float offset) {
// Number of resulting vertices
int m = 0;
bool prev, curr;
float dj = multi*z[(n-1)]+offset;
for (int i = 0, j = n-1; i < n; j = i, i++) {
float di = multi*z[i]+offset;
prev = dj >= 0;
curr = di >= 0;
if (prev != curr) {
float s = dj / (dj - di);
result.y[m] = y[j] + (y[i]-y[j])*s;
result.z[m] = z[j] + (z[i]-z[j])*s;
m++;
}
if (curr) {
result.y[m] = y[i];
result.z[m] = z[i];
m++;
}
dj = di;
}
result.n = m;
}
/// <summary>
/// Clips a polygon against an axis aligned half plane.
/// The polygons stored in this object are clipped against the half plane at z = -offset.
/// </summary>
/// <param name="result">Ouput vertices. Only the Y coordinates are calculated. The X and Z coordinates are undefined.</param>
/// <param name="multi">Scale factor for the input vertices. Should be +1 or -1. If -1 the negative half plane is kept.</param>
/// <param name="offset">Offset to move the input vertices with before cutting</param>
public void ClipPolygonAlongZWithY (ref VoxelPolygonClipper result, float multi, float offset) {
// Number of resulting vertices
int m = 0;
bool prev, curr;
float dj = multi*z[(n-1)]+offset;
for (int i = 0, j = n-1; i < n; j = i, i++) {
float di = multi*z[i]+offset;
prev = dj >= 0;
curr = di >= 0;
if (prev != curr) {
float s = dj / (dj - di);
result.y[m] = y[j] + (y[i]-y[j])*s;
m++;
}
if (curr) {
result.y[m] = y[i];
m++;
}
dj = di;
}
result.n = m;
}
}
/// <summary>Utility for clipping polygons</summary>
internal struct Int3PolygonClipper {
/// <summary>Cache this buffer to avoid unnecessary allocations</summary>
float[] clipPolygonCache;
/// <summary>Cache this buffer to avoid unnecessary allocations</summary>
int[] clipPolygonIntCache;
/// <summary>Initialize buffers if they are null</summary>
public void Init () {
if (clipPolygonCache == null) {
clipPolygonCache = new float[7*3];
clipPolygonIntCache = new int[7*3];
}
}
/// <summary>
/// Clips a polygon against an axis aligned half plane.
///
/// Returns: Number of output vertices
///
/// The vertices will be scaled and then offset, after that they will be cut using either the
/// x axis, y axis or the z axis as the cutting line. The resulting vertices will be added to the
/// vOut array in their original space (i.e before scaling and offsetting).
/// </summary>
/// <param name="vIn">Input vertices</param>
/// <param name="n">Number of input vertices (may be less than the length of the vIn array)</param>
/// <param name="vOut">Output vertices, needs to be large enough</param>
/// <param name="multi">Scale factor for the input vertices</param>
/// <param name="offset">Offset to move the input vertices with before cutting</param>
/// <param name="axis">Axis to cut along, either x=0, y=1, z=2</param>
public int ClipPolygon (Int3[] vIn, int n, Int3[] vOut, int multi, int offset, int axis) {
Init();
int[] d = clipPolygonIntCache;
for (int i = 0; i < n; i++) {
d[i] = multi*vIn[i][axis]+offset;
}
// Number of resulting vertices
int m = 0;
for (int i = 0, j = n-1; i < n; j = i, i++) {
bool prev = d[j] >= 0;
bool curr = d[i] >= 0;
if (prev != curr) {
double s = (double)d[j] / (d[j] - d[i]);
vOut[m] = vIn[j] + (vIn[i]-vIn[j])*s;
m++;
}
if (curr) {
vOut[m] = vIn[i];
m++;
}
}
return m;
}
}
}

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 10347e1eaceee428fa14386ccbaffde5
timeCreated: 1454161567
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,857 @@
using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding.Voxels {
/// <summary>
/// Voxelizer for recast graphs.
///
/// In comments: units wu are World Units, vx are Voxels
/// </summary>
public partial class Voxelize {
public List<RasterizationMesh> inputMeshes;
/// <summary>Maximum ledge height that is considered to still be traversable. [Limit: >=0] [Units: vx]</summary>
public readonly int voxelWalkableClimb;
/// <summary>
/// Minimum floor to 'ceiling' height that will still allow the floor area to
/// be considered walkable. [Limit: >= 3] [Units: vx]
/// </summary>
public readonly uint voxelWalkableHeight;
/// <summary>The xz-plane cell size to use for fields. [Limit: > 0] [Units: wu]</summary>
public readonly float cellSize = 0.2F;
/// <summary>The y-axis cell size to use for fields. [Limit: > 0] [Units: wu]</summary>
public readonly float cellHeight = 0.1F;
public int minRegionSize = 100;
/// <summary>The size of the non-navigable border around the heightfield. [Limit: >=0] [Units: vx]</summary>
public int borderSize = 0;
/// <summary>The maximum allowed length for contour edges along the border of the mesh. [Limit: >= 0] [Units: vx]</summary>
public float maxEdgeLength = 20;
/// <summary>The maximum slope that is considered walkable. [Limits: 0 <= value < 90] [Units: Degrees]</summary>
public float maxSlope = 30;
public RecastGraph.RelevantGraphSurfaceMode relevantGraphSurfaceMode;
/// <summary>The world AABB to rasterize</summary>
public Bounds forcedBounds;
public VoxelArea voxelArea;
public VoxelContourSet countourSet;
/// <summary>Transform from voxel space to world space</summary>
Pathfinding.Util.GraphTransform transform;
/// <summary>Transform from voxel space to graph space</summary>
public Pathfinding.Util.GraphTransform transformVoxel2Graph { get; private set; }
/// <summary>
/// Width in voxels.
/// Must match the <see cref="forcedBounds"/>
/// </summary>
public int width;
/// <summary>
/// Depth in voxels.
/// Must match the <see cref="forcedBounds"/>
/// </summary>
public int depth;
#region Debug
Vector3 voxelOffset = Vector3.zero;
public Vector3 CompactSpanToVector (int x, int z, int i) {
return voxelOffset+new Vector3((x+0.5f)*cellSize, voxelArea.compactSpans[i].y*cellHeight, (z+0.5f)*cellSize);
}
public void VectorToIndex (Vector3 p, out int x, out int z) {
p -= voxelOffset;
x = Mathf.RoundToInt((p.x / cellSize) - 0.5f);
z = Mathf.RoundToInt((p.z / cellSize) - 0.5f);
}
#endregion
#region Constants /// <summary>@name Constants @{</summary>
public const uint NotConnected = 0x3f;
/// <summary>Unmotivated variable, but let's clamp the layers at 65535</summary>
const int MaxLayers = 65535;
/// <summary>TODO: : Check up on this variable</summary>
const int MaxRegions = 500;
const int UnwalkableArea = 0;
/// <summary>
/// If heightfield region ID has the following bit set, the region is on border area
/// and excluded from many calculations.
/// </summary>
const ushort BorderReg = 0x8000;
/// <summary>
/// If contour region ID has the following bit set, the vertex will be later
/// removed in order to match the segments and vertices at tile boundaries.
/// </summary>
const int RC_BORDER_VERTEX = 0x10000;
const int RC_AREA_BORDER = 0x20000;
const int VERTEX_BUCKET_COUNT = 1<<12;
/// <summary>Tessellate wall edges</summary>
public const int RC_CONTOUR_TESS_WALL_EDGES = 1 << 0;
/// <summary>Tessellate edges between areas</summary>
public const int RC_CONTOUR_TESS_AREA_EDGES = 1 << 1;
/// <summary>Tessellate edges at the border of the tile</summary>
public const int RC_CONTOUR_TESS_TILE_EDGES = 1 << 2;
/// <summary>Mask used with contours to extract region id.</summary>
const int ContourRegMask = 0xffff;
#endregion /// <summary>@}</summary>
readonly Vector3 cellScale;
public Voxelize (float ch, float cs, float walkableClimb, float walkableHeight, float maxSlope, float maxEdgeLength) {
cellSize = cs;
cellHeight = ch;
this.maxSlope = maxSlope;
cellScale = new Vector3(cellSize, cellHeight, cellSize);
voxelWalkableHeight = (uint)(walkableHeight/cellHeight);
voxelWalkableClimb = Mathf.RoundToInt(walkableClimb/cellHeight);
this.maxEdgeLength = maxEdgeLength;
}
public void Init () {
// Initialize the voxel area
if (voxelArea == null || voxelArea.width != width || voxelArea.depth != depth)
voxelArea = new VoxelArea(width, depth);
else voxelArea.Reset();
}
public void VoxelizeInput (Pathfinding.Util.GraphTransform graphTransform, Bounds graphSpaceBounds) {
// Transform from voxel space to graph space.
// then scale from voxel space (one unit equals one voxel)
// Finally add min
Matrix4x4 voxelMatrix = Matrix4x4.TRS(graphSpaceBounds.min, Quaternion.identity, Vector3.one) * Matrix4x4.Scale(new Vector3(cellSize, cellHeight, cellSize));
transformVoxel2Graph = new Pathfinding.Util.GraphTransform(voxelMatrix);
// Transform from voxel space to world space
// add half a voxel to fix rounding
transform = graphTransform * voxelMatrix * Matrix4x4.TRS(new Vector3(0.5f, 0, 0.5f), Quaternion.identity, Vector3.one);
int maximumVoxelYCoord = (int)(graphSpaceBounds.size.y / cellHeight);
// Cosine of the slope limit in voxel space (some tweaks are needed because the voxel space might be stretched out along the y axis)
float slopeLimit = Mathf.Cos(Mathf.Atan(Mathf.Tan(maxSlope*Mathf.Deg2Rad)*(cellSize/cellHeight)));
// Temporary arrays used for rasterization
var clipperOrig = new VoxelPolygonClipper(3);
var clipperX1 = new VoxelPolygonClipper(7);
var clipperX2 = new VoxelPolygonClipper(7);
var clipperZ1 = new VoxelPolygonClipper(7);
var clipperZ2 = new VoxelPolygonClipper(7);
if (inputMeshes == null) throw new System.NullReferenceException("inputMeshes not set");
// Find the largest lengths of vertex arrays and check for meshes which can be skipped
int maxVerts = 0;
for (int m = 0; m < inputMeshes.Count; m++) {
maxVerts = System.Math.Max(inputMeshes[m].vertices.Length, maxVerts);
}
// Create buffer, here vertices will be stored multiplied with the local-to-voxel-space matrix
var verts = new Vector3[maxVerts];
// This loop is the hottest place in the whole rasterization process
// it usually accounts for around 50% of the time
for (int m = 0; m < inputMeshes.Count; m++) {
RasterizationMesh mesh = inputMeshes[m];
var meshMatrix = mesh.matrix;
// Flip the orientation of all faces if the mesh is scaled in such a way
// that the face orientations would change
// This happens for example if a mesh has a negative scale along an odd number of axes
// e.g it happens for the scale (-1, 1, 1) but not for (-1, -1, 1) or (1,1,1)
var flipOrientation = VectorMath.ReversesFaceOrientations(meshMatrix);
Vector3[] vs = mesh.vertices;
int[] tris = mesh.triangles;
int trisLength = mesh.numTriangles;
// Transform vertices first to world space and then to voxel space
for (int i = 0; i < vs.Length; i++) verts[i] = transform.InverseTransform(meshMatrix.MultiplyPoint3x4(vs[i]));
int mesharea = mesh.area;
for (int i = 0; i < trisLength; i += 3) {
Vector3 p1 = verts[tris[i]];
Vector3 p2 = verts[tris[i+1]];
Vector3 p3 = verts[tris[i+2]];
if (flipOrientation) {
var tmp = p1;
p1 = p3;
p3 = tmp;
}
int minX = (int)(Utility.Min(p1.x, p2.x, p3.x));
int minZ = (int)(Utility.Min(p1.z, p2.z, p3.z));
int maxX = (int)System.Math.Ceiling(Utility.Max(p1.x, p2.x, p3.x));
int maxZ = (int)System.Math.Ceiling(Utility.Max(p1.z, p2.z, p3.z));
minX = Mathf.Clamp(minX, 0, voxelArea.width-1);
maxX = Mathf.Clamp(maxX, 0, voxelArea.width-1);
minZ = Mathf.Clamp(minZ, 0, voxelArea.depth-1);
maxZ = Mathf.Clamp(maxZ, 0, voxelArea.depth-1);
// Check if the mesh is completely out of bounds
if (minX >= voxelArea.width || minZ >= voxelArea.depth || maxX <= 0 || maxZ <= 0) continue;
Vector3 normal;
int area;
normal = Vector3.Cross(p2-p1, p3-p1);
float cosSlopeAngle = Vector3.Dot(normal.normalized, Vector3.up);
if (cosSlopeAngle < slopeLimit) {
area = UnwalkableArea;
} else {
area = 1 + mesharea;
}
clipperOrig[0] = p1;
clipperOrig[1] = p2;
clipperOrig[2] = p3;
clipperOrig.n = 3;
for (int x = minX; x <= maxX; x++) {
clipperOrig.ClipPolygonAlongX(ref clipperX1, 1f, -x+0.5f);
if (clipperX1.n < 3) {
continue;
}
clipperX1.ClipPolygonAlongX(ref clipperX2, -1F, x+0.5F);
if (clipperX2.n < 3) {
continue;
}
float clampZ1 = clipperX2.z[0];
float clampZ2 = clipperX2.z[0];
for (int q = 1; q < clipperX2.n; q++) {
float val = clipperX2.z[q];
clampZ1 = System.Math.Min(clampZ1, val);
clampZ2 = System.Math.Max(clampZ2, val);
}
int clampZ1I = Mathf.Clamp((int)System.Math.Round(clampZ1), 0, voxelArea.depth-1);
int clampZ2I = Mathf.Clamp((int)System.Math.Round(clampZ2), 0, voxelArea.depth-1);
for (int z = clampZ1I; z <= clampZ2I; z++) {
clipperX2.ClipPolygonAlongZWithYZ(ref clipperZ1, 1F, -z+0.5F);
if (clipperZ1.n < 3) {
continue;
}
clipperZ1.ClipPolygonAlongZWithY(ref clipperZ2, -1F, z+0.5F);
if (clipperZ2.n < 3) {
continue;
}
float sMin = clipperZ2.y[0];
float sMax = clipperZ2.y[0];
for (int q = 1; q < clipperZ2.n; q++) {
float val = clipperZ2.y[q];
sMin = System.Math.Min(sMin, val);
sMax = System.Math.Max(sMax, val);
}
int maxi = (int)System.Math.Ceiling(sMax);
// Skip span if below or above the bounding box
if (maxi >= 0 && sMin <= maximumVoxelYCoord) {
// Make sure mini >= 0
int mini = System.Math.Max(0, (int)sMin);
// Make sure the span is at least 1 voxel high
maxi = System.Math.Max(mini+1, maxi);
voxelArea.AddLinkedSpan(z*voxelArea.width+x, (uint)mini, (uint)maxi, area, voxelWalkableClimb);
}
}
}
}
}
}
public void DebugDrawSpans () {
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
int wd = voxelArea.width*voxelArea.depth;
var min = forcedBounds.min;
LinkedVoxelSpan[] spans = voxelArea.linkedSpans;
for (int z = 0, pz = 0; z < wd; z += voxelArea.width, pz++) {
for (int x = 0; x < voxelArea.width; x++) {
for (int s = z+x; s != -1 && spans[s].bottom != VoxelArea.InvalidSpanValue; s = spans[s].next) {
uint bottom = spans[s].top;
uint top = spans[s].next != -1 ? spans[spans[s].next].bottom : VoxelArea.MaxHeight;
if (bottom > top) {
Debug.Log(bottom + " " + top);
Debug.DrawLine(new Vector3(x*cellSize, bottom*cellHeight, pz*cellSize)+min, new Vector3(x*cellSize, top*cellHeight, pz*cellSize)+min, Color.yellow, 1);
}
//Debug.DrawRay (p,voxelArea.VectorDirection[d]*cellSize*0.5F,Color.red);
if (top - bottom < voxelWalkableHeight) {
//spans[s].area = UnwalkableArea;
}
}
}
}
#else
Debug.LogError("This debug method only works with !ASTAR_RECAST_CLASS_BASED_LINKED_LIST");
#endif
}
public void BuildCompactField () {
//Build compact representation
int spanCount = voxelArea.GetSpanCount();
voxelArea.compactSpanCount = spanCount;
if (voxelArea.compactSpans == null || voxelArea.compactSpans.Length < spanCount) {
voxelArea.compactSpans = new CompactVoxelSpan[spanCount];
voxelArea.areaTypes = new int[spanCount];
}
uint idx = 0;
int w = voxelArea.width;
int d = voxelArea.depth;
int wd = w*d;
if (this.voxelWalkableHeight >= 0xFFFF) {
Debug.LogWarning("Too high walkable height to guarantee correctness. Increase voxel height or lower walkable height.");
}
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
LinkedVoxelSpan[] spans = voxelArea.linkedSpans;
#endif
//Parallel.For (0, voxelArea.depth, delegate (int pz) {
for (int z = 0, pz = 0; z < wd; z += w, pz++) {
for (int x = 0; x < w; x++) {
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
int spanIndex = x+z;
if (spans[spanIndex].bottom == VoxelArea.InvalidSpanValue) {
voxelArea.compactCells[x+z] = new CompactVoxelCell(0, 0);
continue;
}
uint index = idx;
uint count = 0;
//Vector3 p = new Vector3(x,0,pz)*cellSize+voxelOffset;
while (spanIndex != -1) {
if (spans[spanIndex].area != UnwalkableArea) {
int bottom = (int)spans[spanIndex].top;
int next = spans[spanIndex].next;
int top = next != -1 ? (int)spans[next].bottom : VoxelArea.MaxHeightInt;
voxelArea.compactSpans[idx] = new CompactVoxelSpan((ushort)(bottom > 0xFFFF ? 0xFFFF : bottom), (uint)(top-bottom > 0xFFFF ? 0xFFFF : top-bottom));
voxelArea.areaTypes[idx] = spans[spanIndex].area;
idx++;
count++;
}
spanIndex = spans[spanIndex].next;
}
voxelArea.compactCells[x+z] = new CompactVoxelCell(index, count);
#else
VoxelSpan s = voxelArea.cells[x+z].firstSpan;
if (s == null) {
voxelArea.compactCells[x+z] = new CompactVoxelCell(0, 0);
continue;
}
uint index = idx;
uint count = 0;
//Vector3 p = new Vector3(x,0,pz)*cellSize+voxelOffset;
while (s != null) {
if (s.area != UnwalkableArea) {
int bottom = (int)s.top;
int top = s.next != null ? (int)s.next.bottom : VoxelArea.MaxHeightInt;
voxelArea.compactSpans[idx] = new CompactVoxelSpan((ushort)Mathf.Clamp(bottom, 0, 0xffff), (uint)Mathf.Clamp(top-bottom, 0, 0xffff));
voxelArea.areaTypes[idx] = s.area;
idx++;
count++;
}
s = s.next;
}
voxelArea.compactCells[x+z] = new CompactVoxelCell(index, count);
#endif
}
}
}
public void BuildVoxelConnections () {
int wd = voxelArea.width*voxelArea.depth;
CompactVoxelSpan[] spans = voxelArea.compactSpans;
CompactVoxelCell[] cells = voxelArea.compactCells;
// Build voxel connections
for (int z = 0, pz = 0; z < wd; z += voxelArea.width, pz++) {
for (int x = 0; x < voxelArea.width; x++) {
CompactVoxelCell c = cells[x+z];
for (int i = (int)c.index, ni = (int)(c.index+c.count); i < ni; i++) {
CompactVoxelSpan s = spans[i];
spans[i].con = 0xFFFFFFFF;
for (int d = 0; d < 4; d++) {
int nx = x+voxelArea.DirectionX[d];
int nz = z+voxelArea.DirectionZ[d];
if (nx < 0 || nz < 0 || nz >= wd || nx >= voxelArea.width) {
continue;
}
CompactVoxelCell nc = cells[nx+nz];
for (int k = (int)nc.index, nk = (int)(nc.index+nc.count); k < nk; k++) {
CompactVoxelSpan ns = spans[k];
int bottom = System.Math.Max(s.y, ns.y);
int top = System.Math.Min((int)s.y+(int)s.h, (int)ns.y+(int)ns.h);
if ((top-bottom) >= voxelWalkableHeight && System.Math.Abs((int)ns.y - (int)s.y) <= voxelWalkableClimb) {
uint connIdx = (uint)k - nc.index;
if (connIdx > MaxLayers) {
Debug.LogError("Too many layers");
continue;
}
spans[i].SetConnection(d, connIdx);
break;
}
}
}
}
}
}
}
void DrawLine (int a, int b, int[] indices, int[] verts, Color color) {
int p1 = (indices[a] & 0x0fffffff) * 4;
int p2 = (indices[b] & 0x0fffffff) * 4;
Debug.DrawLine(VoxelToWorld(verts[p1+0], verts[p1+1], verts[p1+2]), VoxelToWorld(verts[p2+0], verts[p2+1], verts[p2+2]), color);
}
#if ASTARDEBUG
Vector3 ConvertPos (int x, int y, int z) {
Vector3 p = Vector3.Scale(
new Vector3(
x+0.5F,
y,
(z/(float)voxelArea.width)+0.5F
)
, cellScale)
+voxelOffset;
return p;
}
#endif
/// <summary>
/// Convert from voxel coordinates to world coordinates.
/// (0,0,0) in voxel coordinates is a bottom corner of the bounding box.
/// (1,0,0) is one voxel in the +X direction of that.
/// </summary>
public Vector3 VoxelToWorld (int x, int y, int z) {
Vector3 p = Vector3.Scale(
new Vector3(
x,
y,
z
)
, cellScale)
+voxelOffset;
return p;
}
/// <summary>
/// Convert from voxel coordinates to world coordinates.
/// (0,0,0) in voxel coordinates is a bottom corner of the bounding box.
/// (1,0,0) is one voxel in the +X direction of that.
/// </summary>
public Int3 VoxelToWorldInt3 (Int3 voxelPosition) {
var pos = voxelPosition * Int3.Precision;
pos = new Int3(Mathf.RoundToInt(pos.x * cellScale.x), Mathf.RoundToInt(pos.y * cellScale.y), Mathf.RoundToInt(pos.z * cellScale.z));
return pos +(Int3)voxelOffset;
}
Vector3 ConvertPosWithoutOffset (int x, int y, int z) {
Vector3 p = Vector3.Scale(
new Vector3(
x,
y,
(z/(float)voxelArea.width)
)
, cellScale)
+voxelOffset;
return p;
}
Vector3 ConvertPosition (int x, int z, int i) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
return new Vector3(x*cellSize, s.y*cellHeight, (z/(float)voxelArea.width)*cellSize)+voxelOffset;
}
public void ErodeWalkableArea (int radius) {
ushort[] src = voxelArea.tmpUShortArr;
if (src == null || src.Length < voxelArea.compactSpanCount) {
src = voxelArea.tmpUShortArr = new ushort[voxelArea.compactSpanCount];
}
// Set all elements in src to 0xffff
Pathfinding.Util.Memory.MemSet<ushort>(src, 0xffff, sizeof(ushort));
CalculateDistanceField(src);
for (int i = 0; i < src.Length; i++) {
//Note multiplied with 2 because the distance field increments distance by 2 for each voxel (and 3 for diagonal)
if (src[i] < radius*2) {
voxelArea.areaTypes[i] = UnwalkableArea;
}
}
}
public void BuildDistanceField () {
ushort[] src = voxelArea.tmpUShortArr;
if (src == null || src.Length < voxelArea.compactSpanCount) {
src = voxelArea.tmpUShortArr = new ushort[voxelArea.compactSpanCount];
}
// Set all elements in src to 0xffff
Pathfinding.Util.Memory.MemSet<ushort>(src, 0xffff, sizeof(ushort));
voxelArea.maxDistance = CalculateDistanceField(src);
ushort[] dst = voxelArea.dist;
if (dst == null || dst.Length < voxelArea.compactSpanCount) {
dst = new ushort[voxelArea.compactSpanCount];
}
dst = BoxBlur(src, dst);
voxelArea.dist = dst;
}
/// <summary>TODO: Complete the ErodeVoxels function translation</summary>
[System.Obsolete("This function is not complete and should not be used")]
public void ErodeVoxels (int radius) {
if (radius > 255) {
Debug.LogError("Max Erode Radius is 255");
radius = 255;
}
int wd = voxelArea.width*voxelArea.depth;
int[] dist = new int[voxelArea.compactSpanCount];
for (int i = 0; i < dist.Length; i++) {
dist[i] = 0xFF;
}
for (int z = 0; z < wd; z += voxelArea.width) {
for (int x = 0; x < voxelArea.width; x++) {
CompactVoxelCell c = voxelArea.compactCells[x+z];
for (int i = (int)c.index, ni = (int)(c.index+c.count); i < ni; i++) {
if (voxelArea.areaTypes[i] != UnwalkableArea) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
int nc = 0;
for (int dir = 0; dir < 4; dir++) {
if (s.GetConnection(dir) != NotConnected)
nc++;
}
//At least one missing neighbour
if (nc != 4) {
dist[i] = 0;
}
}
}
}
}
//int nd = 0;
//Pass 1
/*for (int z=0;z < wd;z += voxelArea.width) {
* for (int x=0;x < voxelArea.width;x++) {
*
* CompactVoxelCell c = voxelArea.compactCells[x+z];
*
* for (int i= (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
* CompactVoxelSpan s = voxelArea.compactSpans[i];
*
* if (s.GetConnection (0) != NotConnected) {
* // (-1,0)
* int nx = x+voxelArea.DirectionX[0];
* int nz = z+voxelArea.DirectionZ[0];
*
* int ni = (int)(voxelArea.compactCells[nx+nz].index+s.GetConnection (0));
* CompactVoxelSpan ns = voxelArea.compactSpans[ni];
*
* if (dist[ni]+2 < dist[i]) {
* dist[i] = (ushort)(dist[ni]+2);
* }
*
* if (ns.GetConnection (3) != NotConnected) {
* // (-1,0) + (0,-1) = (-1,-1)
* int nnx = nx+voxelArea.DirectionX[3];
* int nnz = nz+voxelArea.DirectionZ[3];
*
* int nni = (int)(voxelArea.compactCells[nnx+nnz].index+ns.GetConnection (3));
*
* if (src[nni]+3 < src[i]) {
* src[i] = (ushort)(src[nni]+3);
* }
* }
* }
*
* if (s.GetConnection (3) != NotConnected) {
* // (0,-1)
* int nx = x+voxelArea.DirectionX[3];
* int nz = z+voxelArea.DirectionZ[3];
*
* int ni = (int)(voxelArea.compactCells[nx+nz].index+s.GetConnection (3));
*
* if (src[ni]+2 < src[i]) {
* src[i] = (ushort)(src[ni]+2);
* }
*
* CompactVoxelSpan ns = voxelArea.compactSpans[ni];
*
* if (ns.GetConnection (2) != NotConnected) {
*
* // (0,-1) + (1,0) = (1,-1)
* int nnx = nx+voxelArea.DirectionX[2];
* int nnz = nz+voxelArea.DirectionZ[2];
*
* voxelOffset nni = (int)(voxelArea.compactCells[nnx+nnz].index+ns.GetConnection (2));
*
* if (src[nni]+3 < src[i]) {
* src[i] = (ushort)(src[nni]+3);
* }
* }
* }
* }
* }
* }*/
}
public void FilterLowHeightSpans (uint voxelWalkableHeight, float cs, float ch) {
int wd = voxelArea.width*voxelArea.depth;
//Filter all ledges
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
LinkedVoxelSpan[] spans = voxelArea.linkedSpans;
for (int z = 0, pz = 0; z < wd; z += voxelArea.width, pz++) {
for (int x = 0; x < voxelArea.width; x++) {
for (int s = z+x; s != -1 && spans[s].bottom != VoxelArea.InvalidSpanValue; s = spans[s].next) {
uint bottom = spans[s].top;
uint top = spans[s].next != -1 ? spans[spans[s].next].bottom : VoxelArea.MaxHeight;
if (top - bottom < voxelWalkableHeight) {
spans[s].area = UnwalkableArea;
}
}
}
}
#else
for (int z = 0, pz = 0; z < wd; z += voxelArea.width, pz++) {
for (int x = 0; x < voxelArea.width; x++) {
for (VoxelSpan s = voxelArea.cells[z+x].firstSpan; s != null; s = s.next) {
uint bottom = s.top;
uint top = s.next != null ? s.next.bottom : VoxelArea.MaxHeight;
if (top - bottom < voxelWalkableHeight) {
s.area = UnwalkableArea;
}
}
}
}
#endif
}
//Code almost completely ripped from Recast
public void FilterLedges (uint voxelWalkableHeight, int voxelWalkableClimb, float cs, float ch) {
int wd = voxelArea.width*voxelArea.depth;
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
LinkedVoxelSpan[] spans = voxelArea.linkedSpans;
int[] DirectionX = voxelArea.DirectionX;
int[] DirectionZ = voxelArea.DirectionZ;
#endif
int width = voxelArea.width;
//Filter all ledges
for (int z = 0, pz = 0; z < wd; z += width, pz++) {
for (int x = 0; x < width; x++) {
#if !ASTAR_RECAST_CLASS_BASED_LINKED_LIST
if (spans[x+z].bottom == VoxelArea.InvalidSpanValue) continue;
for (int s = x+z; s != -1; s = spans[s].next) {
//Skip non-walkable spans
if (spans[s].area == UnwalkableArea) {
continue;
}
int bottom = (int)spans[s].top;
int top = spans[s].next != -1 ? (int)spans[spans[s].next].bottom : VoxelArea.MaxHeightInt;
int minHeight = VoxelArea.MaxHeightInt;
int aMinHeight = (int)spans[s].top;
int aMaxHeight = aMinHeight;
for (int d = 0; d < 4; d++) {
int nx = x+DirectionX[d];
int nz = z+DirectionZ[d];
//Skip out-of-bounds points
if (nx < 0 || nz < 0 || nz >= wd || nx >= width) {
spans[s].area = UnwalkableArea;
break;
}
int nsx = nx+nz;
int nbottom = -voxelWalkableClimb;
int ntop = spans[nsx].bottom != VoxelArea.InvalidSpanValue ? (int)spans[nsx].bottom : VoxelArea.MaxHeightInt;
if (System.Math.Min(top, ntop) - System.Math.Max(bottom, nbottom) > voxelWalkableHeight) {
minHeight = System.Math.Min(minHeight, nbottom - bottom);
}
//Loop through spans
if (spans[nsx].bottom != VoxelArea.InvalidSpanValue) {
for (int ns = nsx; ns != -1; ns = spans[ns].next) {
nbottom = (int)spans[ns].top;
ntop = spans[ns].next != -1 ? (int)spans[spans[ns].next].bottom : VoxelArea.MaxHeightInt;
if (System.Math.Min(top, ntop) - System.Math.Max(bottom, nbottom) > voxelWalkableHeight) {
minHeight = System.Math.Min(minHeight, nbottom - bottom);
if (System.Math.Abs(nbottom - bottom) <= voxelWalkableClimb) {
if (nbottom < aMinHeight) { aMinHeight = nbottom; }
if (nbottom > aMaxHeight) { aMaxHeight = nbottom; }
}
}
}
}
}
if (minHeight < -voxelWalkableClimb || (aMaxHeight - aMinHeight) > voxelWalkableClimb) {
spans[s].area = UnwalkableArea;
}
}
#else
for (VoxelSpan s = voxelArea.cells[z+x].firstSpan; s != null; s = s.next) {
//Skip non-walkable spans
if (s.area == UnwalkableArea) {
continue;
}
int bottom = (int)s.top;
int top = s.next != null ? (int)s.next.bottom : VoxelArea.MaxHeightInt;
int minHeight = VoxelArea.MaxHeightInt;
int aMinHeight = (int)s.top;
int aMaxHeight = (int)s.top;
for (int d = 0; d < 4; d++) {
int nx = x+voxelArea.DirectionX[d];
int nz = z+voxelArea.DirectionZ[d];
//Skip out-of-bounds points
if (nx < 0 || nz < 0 || nz >= wd || nx >= voxelArea.width) {
s.area = UnwalkableArea;
break;
}
VoxelSpan nsx = voxelArea.cells[nx+nz].firstSpan;
int nbottom = -voxelWalkableClimb;
int ntop = nsx != null ? (int)nsx.bottom : VoxelArea.MaxHeightInt;
if (System.Math.Min(top, ntop) - System.Math.Max(bottom, nbottom) > voxelWalkableHeight) {
minHeight = System.Math.Min(minHeight, nbottom - bottom);
}
//Loop through spans
for (VoxelSpan ns = nsx; ns != null; ns = ns.next) {
nbottom = (int)ns.top;
ntop = ns.next != null ? (int)ns.next.bottom : VoxelArea.MaxHeightInt;
if (System.Math.Min(top, ntop) - System.Math.Max(bottom, nbottom) > voxelWalkableHeight) {
minHeight = System.Math.Min(minHeight, nbottom - bottom);
if (System.Math.Abs(nbottom - bottom) <= voxelWalkableClimb) {
if (nbottom < aMinHeight) { aMinHeight = nbottom; }
if (nbottom > aMaxHeight) { aMaxHeight = nbottom; }
}
}
}
}
if (minHeight < -voxelWalkableClimb || (aMaxHeight - aMinHeight) > voxelWalkableClimb) {
s.area = UnwalkableArea;
}
}
#endif
}
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d0d1f28f02bb44332a1705b450727571
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,710 @@
//#define ASTAR_DEBUGREPLAY
using System.Collections.Generic;
using UnityEngine;
namespace Pathfinding.Voxels {
public partial class Voxelize {
public bool FloodRegion (int x, int z, int i, uint level, ushort r, ushort[] srcReg, ushort[] srcDist, Int3[] stack, int[] flags = null, bool[] closed = null) {
int area = voxelArea.areaTypes[i];
// Flood f mark region.
int stackSize = 1;
stack[0] = new Int3 {
x = x,
y = i,
z = z,
};
srcReg[i] = r;
srcDist[i] = 0;
int lev = (int)(level >= 2 ? level-2 : 0);
int count = 0;
// Store these in local variables (for performance, avoids an extra indirection)
var DirectionX = voxelArea.DirectionX;
var DirectionZ = voxelArea.DirectionZ;
var compactCells = voxelArea.compactCells;
var compactSpans = voxelArea.compactSpans;
var areaTypes = voxelArea.areaTypes;
var dist = voxelArea.dist;
while (stackSize > 0) {
stackSize--;
var c = stack[stackSize];
//Similar to the Pop operation of an array, but Pop is not implemented in List<>
int ci = c.y;
int cx = c.x;
int cz = c.z;
CompactVoxelSpan cs = compactSpans[ci];
//Debug.DrawRay (ConvertPosition(cx,cz,ci),Vector3.up, Color.cyan);
// Check if any of the neighbours already have a valid region set.
ushort ar = 0;
// Loop through four neighbours
// then check one neighbour of the neighbour
// to get the diagonal neighbour
for (int dir = 0; dir < 4; dir++) {
// 8 connected
if (cs.GetConnection(dir) != NotConnected) {
int ax = cx + DirectionX[dir];
int az = cz + DirectionZ[dir];
int ai = (int)compactCells[ax+az].index + cs.GetConnection(dir);
if (areaTypes[ai] != area)
continue;
ushort nr = srcReg[ai];
if ((nr & BorderReg) == BorderReg) // Do not take borders into account.
continue;
if (nr != 0 && nr != r) {
ar = nr;
// Found a valid region, skip checking the rest
break;
}
// Rotate dir 90 degrees
int dir2 = (dir+1) & 0x3;
var neighbour2 = compactSpans[ai].GetConnection(dir2);
// Check the diagonal connection
if (neighbour2 != NotConnected) {
int ax2 = ax + DirectionX[dir2];
int az2 = az + DirectionZ[dir2];
int ai2 = (int)compactCells[ax2+az2].index + neighbour2;
if (areaTypes[ai2] != area)
continue;
ushort nr2 = srcReg[ai2];
if ((nr2 & BorderReg) == BorderReg) // Do not take borders into account.
continue;
if (nr2 != 0 && nr2 != r) {
ar = nr2;
// Found a valid region, skip checking the rest
break;
}
}
}
}
if (ar != 0) {
srcReg[ci] = 0;
srcDist[ci] = (ushort)0xFFFF;
continue;
}
count++;
if (closed != null) {
closed[ci] = true;
}
// Expand neighbours.
for (int dir = 0; dir < 4; ++dir) {
if (cs.GetConnection(dir) != NotConnected) {
int ax = cx + DirectionX[dir];
int az = cz + DirectionZ[dir];
int ai = (int)compactCells[ax+az].index + cs.GetConnection(dir);
if (areaTypes[ai] != area)
continue;
if (srcReg[ai] == 0) {
if (dist[ai] >= lev && flags[ai] == 0) {
srcReg[ai] = r;
srcDist[ai] = 0;
stack[stackSize] = new Int3 {
x = ax,
y = ai,
z = az,
};
stackSize++;
} else if (flags != null) {
flags[ai] = r;
srcDist[ai] = 2;
}
}
}
}
}
return count > 0;
}
public void MarkRectWithRegion (int minx, int maxx, int minz, int maxz, ushort region, ushort[] srcReg) {
int md = maxz * voxelArea.width;
for (int z = minz*voxelArea.width; z < md; z += voxelArea.width) {
for (int x = minx; x < maxx; x++) {
CompactVoxelCell c = voxelArea.compactCells[z+x];
for (int i = (int)c.index, ni = (int)(c.index+c.count); i < ni; i++) {
if (voxelArea.areaTypes[i] != UnwalkableArea) {
srcReg[i] = region;
}
}
}
}
}
public ushort CalculateDistanceField (ushort[] src) {
int wd = voxelArea.width*voxelArea.depth;
//Mark boundary cells
for (int z = 0; z < wd; z += voxelArea.width) {
for (int x = 0; x < voxelArea.width; x++) {
CompactVoxelCell c = voxelArea.compactCells[x+z];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
int nc = 0;
for (int d = 0; d < 4; d++) {
if (s.GetConnection(d) != NotConnected) {
//This function (CalculateDistanceField) is used for both ErodeWalkableArea and by itself.
//The C++ recast source uses different code for those two cases, but I have found it works with one function
//the voxelArea.areaTypes[ni] will actually only be one of two cases when used from ErodeWalkableArea
//so it will have the same effect as
// if (area != UnwalkableArea) {
//This line is the one where the differ most
nc++;
} else {
break;
}
}
if (nc != 4) {
src[i] = 0;
}
}
}
}
//Pass 1
for (int z = 0; z < wd; z += voxelArea.width) {
for (int x = 0; x < voxelArea.width; x++) {
CompactVoxelCell c = voxelArea.compactCells[x+z];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
if (s.GetConnection(0) != NotConnected) {
// (-1,0)
int nx = x+voxelArea.DirectionX[0];
int nz = z+voxelArea.DirectionZ[0];
int ni = (int)(voxelArea.compactCells[nx+nz].index+s.GetConnection(0));
if (src[ni]+2 < src[i]) {
src[i] = (ushort)(src[ni]+2);
}
CompactVoxelSpan ns = voxelArea.compactSpans[ni];
if (ns.GetConnection(3) != NotConnected) {
// (-1,0) + (0,-1) = (-1,-1)
int nnx = nx+voxelArea.DirectionX[3];
int nnz = nz+voxelArea.DirectionZ[3];
int nni = (int)(voxelArea.compactCells[nnx+nnz].index+ns.GetConnection(3));
if (src[nni]+3 < src[i]) {
src[i] = (ushort)(src[nni]+3);
}
}
}
if (s.GetConnection(3) != NotConnected) {
// (0,-1)
int nx = x+voxelArea.DirectionX[3];
int nz = z+voxelArea.DirectionZ[3];
int ni = (int)(voxelArea.compactCells[nx+nz].index+s.GetConnection(3));
if (src[ni]+2 < src[i]) {
src[i] = (ushort)(src[ni]+2);
}
CompactVoxelSpan ns = voxelArea.compactSpans[ni];
if (ns.GetConnection(2) != NotConnected) {
// (0,-1) + (1,0) = (1,-1)
int nnx = nx+voxelArea.DirectionX[2];
int nnz = nz+voxelArea.DirectionZ[2];
int nni = (int)(voxelArea.compactCells[nnx+nnz].index+ns.GetConnection(2));
if (src[nni]+3 < src[i]) {
src[i] = (ushort)(src[nni]+3);
}
}
}
}
}
}
//Pass 2
for (int z = wd-voxelArea.width; z >= 0; z -= voxelArea.width) {
for (int x = voxelArea.width-1; x >= 0; x--) {
CompactVoxelCell c = voxelArea.compactCells[x+z];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
if (s.GetConnection(2) != NotConnected) {
// (-1,0)
int nx = x+voxelArea.DirectionX[2];
int nz = z+voxelArea.DirectionZ[2];
int ni = (int)(voxelArea.compactCells[nx+nz].index+s.GetConnection(2));
if (src[ni]+2 < src[i]) {
src[i] = (ushort)(src[ni]+2);
}
CompactVoxelSpan ns = voxelArea.compactSpans[ni];
if (ns.GetConnection(1) != NotConnected) {
// (-1,0) + (0,-1) = (-1,-1)
int nnx = nx+voxelArea.DirectionX[1];
int nnz = nz+voxelArea.DirectionZ[1];
int nni = (int)(voxelArea.compactCells[nnx+nnz].index+ns.GetConnection(1));
if (src[nni]+3 < src[i]) {
src[i] = (ushort)(src[nni]+3);
}
}
}
if (s.GetConnection(1) != NotConnected) {
// (0,-1)
int nx = x+voxelArea.DirectionX[1];
int nz = z+voxelArea.DirectionZ[1];
int ni = (int)(voxelArea.compactCells[nx+nz].index+s.GetConnection(1));
if (src[ni]+2 < src[i]) {
src[i] = (ushort)(src[ni]+2);
}
CompactVoxelSpan ns = voxelArea.compactSpans[ni];
if (ns.GetConnection(0) != NotConnected) {
// (0,-1) + (1,0) = (1,-1)
int nnx = nx+voxelArea.DirectionX[0];
int nnz = nz+voxelArea.DirectionZ[0];
int nni = (int)(voxelArea.compactCells[nnx+nnz].index+ns.GetConnection(0));
if (src[nni]+3 < src[i]) {
src[i] = (ushort)(src[nni]+3);
}
}
}
}
}
}
ushort maxDist = 0;
for (int i = 0; i < voxelArea.compactSpanCount; i++) {
maxDist = System.Math.Max(src[i], maxDist);
}
return maxDist;
}
public ushort[] BoxBlur (ushort[] src, ushort[] dst) {
ushort thr = 20;
int wd = voxelArea.width*voxelArea.depth;
for (int z = wd-voxelArea.width; z >= 0; z -= voxelArea.width) {
for (int x = voxelArea.width-1; x >= 0; x--) {
CompactVoxelCell c = voxelArea.compactCells[x+z];
for (int i = (int)c.index, ci = (int)(c.index+c.count); i < ci; i++) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
ushort cd = src[i];
if (cd < thr) {
dst[i] = cd;
continue;
}
int total = (int)cd;
for (int d = 0; d < 4; d++) {
if (s.GetConnection(d) != NotConnected) {
int nx = x+voxelArea.DirectionX[d];
int nz = z+voxelArea.DirectionZ[d];
int ni = (int)(voxelArea.compactCells[nx+nz].index+s.GetConnection(d));
total += (int)src[ni];
CompactVoxelSpan ns = voxelArea.compactSpans[ni];
int d2 = (d+1) & 0x3;
if (ns.GetConnection(d2) != NotConnected) {
int nnx = nx+voxelArea.DirectionX[d2];
int nnz = nz+voxelArea.DirectionZ[d2];
int nni = (int)(voxelArea.compactCells[nnx+nnz].index+ns.GetConnection(d2));
total += (int)src[nni];
} else {
total += cd;
}
} else {
total += cd*2;
}
}
dst[i] = (ushort)((total+5)/9F);
}
}
}
return dst;
}
public void BuildRegions () {
/*System.Diagnostics.Stopwatch w0 = new System.Diagnostics.Stopwatch();
System.Diagnostics.Stopwatch w1 = new System.Diagnostics.Stopwatch();
System.Diagnostics.Stopwatch w2 = new System.Diagnostics.Stopwatch();
System.Diagnostics.Stopwatch w3 = new System.Diagnostics.Stopwatch();
System.Diagnostics.Stopwatch w4 = new System.Diagnostics.Stopwatch();
System.Diagnostics.Stopwatch w5 = new System.Diagnostics.Stopwatch();
w3.Start();*/
int w = voxelArea.width;
int d = voxelArea.depth;
int wd = w*d;
int spanCount = voxelArea.compactSpanCount;
int expandIterations = 8;
ushort[] srcReg = Util.ArrayPool<ushort>.Claim(spanCount);
ushort[] srcDist = Util.ArrayPool<ushort>.Claim(spanCount);
bool[] closed = Util.ArrayPool<bool>.Claim(spanCount);
int[] spanFlags = Util.ArrayPool<int>.Claim(spanCount);
Int3[] stack = Util.ArrayPool<Int3>.Claim(spanCount);
// The array pool arrays may contain arbitrary data. We need to zero it out.
Util.Memory.MemSet(srcReg, (ushort)0, sizeof(ushort));
Util.Memory.MemSet(srcDist, (ushort)0xFFFF, sizeof(ushort));
Util.Memory.MemSet(closed, false, sizeof(bool));
Util.Memory.MemSet(spanFlags, 0, sizeof(int));
var DirectionX = voxelArea.DirectionX;
var DirectionZ = voxelArea.DirectionZ;
var spanDistances = voxelArea.dist;
var areaTypes = voxelArea.areaTypes;
var compactCells = voxelArea.compactCells;
ushort regionId = 2;
MarkRectWithRegion(0, borderSize, 0, d, (ushort)(regionId | BorderReg), srcReg); regionId++;
MarkRectWithRegion(w-borderSize, w, 0, d, (ushort)(regionId | BorderReg), srcReg); regionId++;
MarkRectWithRegion(0, w, 0, borderSize, (ushort)(regionId | BorderReg), srcReg); regionId++;
MarkRectWithRegion(0, w, d-borderSize, d, (ushort)(regionId | BorderReg), srcReg); regionId++;
Int3[][] buckedSortedSpans = new Int3[(voxelArea.maxDistance)/2 + 1][];
int[] sortedSpanCounts = new int[buckedSortedSpans.Length];
for (int i = 0; i < buckedSortedSpans.Length; i++) buckedSortedSpans[i] = new Int3[16];
//w3.Stop();
//w5.Start();
// Bucket sort the spans based on distance
for (int z = 0, pz = 0; z < wd; z += w, pz++) {
for (int x = 0; x < voxelArea.width; x++) {
CompactVoxelCell c = compactCells[z+x];
for (int i = (int)c.index, ni = (int)(c.index+c.count); i < ni; i++) {
if ((srcReg[i] & BorderReg) == BorderReg) // Do not take borders into account.
continue;
if (areaTypes[i] == UnwalkableArea)
continue;
int distIndex = voxelArea.dist[i] / 2;
if (sortedSpanCounts[distIndex] >= buckedSortedSpans[distIndex].Length) {
var newBuffer = new Int3[sortedSpanCounts[distIndex]*2];
buckedSortedSpans[distIndex].CopyTo(newBuffer, 0);
buckedSortedSpans[distIndex] = newBuffer;
}
buckedSortedSpans[distIndex][sortedSpanCounts[distIndex]++] = new Int3(x, i, z);
}
}
}
//w5.Stop();
Queue<Int3> srcQue = new Queue<Int3>();
Queue<Int3> dstQue = new Queue<Int3>();
// Go through spans in reverse order (i.e largest distances first)
for (int distIndex = buckedSortedSpans.Length - 1; distIndex >= 0; distIndex--) {
var level = (uint)distIndex * 2;
var spans = buckedSortedSpans[distIndex];
var spansAtLevel = sortedSpanCounts[distIndex];
for (int i = 0; i < spansAtLevel; i++) {
int spanIndex = spans[i].y;
// This span is adjacent to a region, so we should start the BFS search from it
if (spanFlags[spanIndex] != 0 && srcReg[spanIndex] == 0) {
srcReg[spanIndex] = (ushort)spanFlags[spanIndex];
srcQue.Enqueue(spans[i]);
closed[spanIndex] = true;
}
}
// Expand a few iterations out from every known node
for (int expansionIteration = 0; expansionIteration < expandIterations && srcQue.Count > 0; expansionIteration++) {
while (srcQue.Count > 0) {
Int3 spanInfo = srcQue.Dequeue();
var area = areaTypes[spanInfo.y];
var span = voxelArea.compactSpans[spanInfo.y];
var region = srcReg[spanInfo.y];
closed[spanInfo.y] = true;
ushort nextDist = (ushort)(srcDist[spanInfo.y] + 2);
// Go through the neighbours of the span
for (int dir = 0; dir < 4; dir++) {
var neighbour = span.GetConnection(dir);
if (neighbour == NotConnected) continue;
int nx = spanInfo.x + DirectionX[dir];
int nz = spanInfo.z + DirectionZ[dir];
int ni = (int)compactCells[nx+nz].index + neighbour;
if ((srcReg[ni] & BorderReg) == BorderReg) // Do not take borders into account.
continue;
// Do not combine different area types
if (area == areaTypes[ni]) {
if (nextDist < srcDist[ni]) {
if (spanDistances[ni] < level) {
srcDist[ni] = nextDist;
spanFlags[ni] = region;
} else if (!closed[ni]) {
srcDist[ni] = nextDist;
if (srcReg[ni] == 0) dstQue.Enqueue(new Int3(nx, ni, nz));
srcReg[ni] = region;
}
}
}
}
}
Util.Memory.Swap(ref srcQue, ref dstQue);
}
// Find the first span that has not been seen yet and start a new region that expands from there
for (int i = 0; i < spansAtLevel; i++) {
var info = spans[i];
if (srcReg[info.y] == 0) {
if (!FloodRegion(info.x, info.z, info.y, level, regionId, srcReg, srcDist, stack, spanFlags, closed)) {
// The starting voxel was already adjacent to an existing region so we skip flooding it.
// It will be visited in the next area expansion.
} else {
regionId++;
}
}
}
}
voxelArea.maxRegions = regionId;
// Filter out small regions.
FilterSmallRegions(srcReg, minRegionSize, voxelArea.maxRegions);
// Write the result out.
var compactSpans = voxelArea.compactSpans;
for (int i = 0; i < spanCount; i++) {
compactSpans[i].reg = srcReg[i];
}
// Pool arrays
Util.ArrayPool<ushort>.Release(ref srcReg);
Util.ArrayPool<ushort>.Release(ref srcDist);
Util.ArrayPool<bool>.Release(ref closed);
Util.ArrayPool<int>.Release(ref spanFlags);
Util.ArrayPool<Int3>.Release(ref stack);
//Debug.Log(w0.Elapsed.TotalMilliseconds.ToString("0.0") + " " + w1.Elapsed.TotalMilliseconds.ToString("0.0") + " " + w2.Elapsed.TotalMilliseconds.ToString("0.0") + " " + w3.Elapsed.TotalMilliseconds.ToString("0.0") + " " + w4.Elapsed.TotalMilliseconds.ToString("0.0") + " " + w5.Elapsed.TotalMilliseconds.ToString("0.0"));
}
/// <summary>
/// Find method in the UnionFind data structure.
/// See: https://en.wikipedia.org/wiki/Disjoint-set_data_structure
/// </summary>
static int union_find_find (int[] arr, int x) {
if (arr[x] < 0) return x;
return arr[x] = union_find_find(arr, arr[x]);
}
/// <summary>
/// Join method in the UnionFind data structure.
/// See: https://en.wikipedia.org/wiki/Disjoint-set_data_structure
/// </summary>
static void union_find_union (int[] arr, int a, int b) {
a = union_find_find(arr, a);
b = union_find_find(arr, b);
if (a == b) return;
if (arr[a] > arr[b]) {
int tmp = a;
a = b;
b = tmp;
}
arr[a] += arr[b];
arr[b] = a;
}
/// <summary>Filters out or merges small regions.</summary>
public void FilterSmallRegions (ushort[] reg, int minRegionSize, int maxRegions) {
RelevantGraphSurface c = RelevantGraphSurface.Root;
// Need to use ReferenceEquals because it might be called from another thread
bool anySurfaces = !RelevantGraphSurface.ReferenceEquals(c, null) && (relevantGraphSurfaceMode != RecastGraph.RelevantGraphSurfaceMode.DoNotRequire);
// Nothing to do here
if (!anySurfaces && minRegionSize <= 0) {
return;
}
int[] counter = new int[maxRegions];
ushort[] bits = voxelArea.tmpUShortArr;
if (bits == null || bits.Length < maxRegions) {
bits = voxelArea.tmpUShortArr = new ushort[maxRegions];
}
Util.Memory.MemSet(counter, -1, sizeof(int));
Util.Memory.MemSet(bits, (ushort)0, maxRegions, sizeof(ushort));
int nReg = counter.Length;
int wd = voxelArea.width*voxelArea.depth;
const int RelevantSurfaceSet = 1 << 1;
const int BorderBit = 1 << 0;
// Mark RelevantGraphSurfaces
// If they can also be adjacent to tile borders, this will also include the BorderBit
int RelevantSurfaceCheck = RelevantSurfaceSet | ((relevantGraphSurfaceMode == RecastGraph.RelevantGraphSurfaceMode.OnlyForCompletelyInsideTile) ? BorderBit : 0x0);
if (anySurfaces) {
// Need to use ReferenceEquals because it might be called from another thread
while (!RelevantGraphSurface.ReferenceEquals(c, null)) {
int x, z;
this.VectorToIndex(c.Position, out x, out z);
// Check for out of bounds
if (x >= 0 && z >= 0 && x < voxelArea.width && z < voxelArea.depth) {
int y = (int)((c.Position.y - voxelOffset.y)/cellHeight);
int rad = (int)(c.maxRange / cellHeight);
CompactVoxelCell cell = voxelArea.compactCells[x+z*voxelArea.width];
for (int i = (int)cell.index; i < cell.index+cell.count; i++) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
if (System.Math.Abs(s.y - y) <= rad && reg[i] != 0) {
bits[union_find_find(counter, (int)reg[i] & ~BorderReg)] |= RelevantSurfaceSet;
}
}
}
c = c.Next;
}
}
for (int z = 0, pz = 0; z < wd; z += voxelArea.width, pz++) {
for (int x = 0; x < voxelArea.width; x++) {
CompactVoxelCell cell = voxelArea.compactCells[x+z];
for (int i = (int)cell.index; i < cell.index+cell.count; i++) {
CompactVoxelSpan s = voxelArea.compactSpans[i];
int r = (int)reg[i];
if ((r & ~BorderReg) == 0) continue;
if (r >= nReg) { //Probably border
bits[union_find_find(counter, r & ~BorderReg)] |= BorderBit;
continue;
}
int k = union_find_find(counter, r);
// Count this span
counter[k]--;
for (int dir = 0; dir < 4; dir++) {
if (s.GetConnection(dir) == NotConnected) { continue; }
int nx = x + voxelArea.DirectionX[dir];
int nz = z + voxelArea.DirectionZ[dir];
int ni = (int)voxelArea.compactCells[nx+nz].index + s.GetConnection(dir);
int r2 = (int)reg[ni];
if (r != r2 && (r2 & ~BorderReg) != 0) {
if ((r2 & BorderReg) != 0) {
bits[k] |= BorderBit;
} else {
union_find_union(counter, k, r2);
}
//counter[r] = minRegionSize;
}
}
//counter[r]++;
}
}
}
// Propagate bits
for (int i = 0; i < counter.Length; i++) bits[union_find_find(counter, i)] |= bits[i];
for (int i = 0; i < counter.Length; i++) {
int ctr = union_find_find(counter, i);
// Adjacent to border
if ((bits[ctr] & BorderBit) != 0) counter[ctr] = -minRegionSize-2;
// Not in any relevant surface
// or it is adjacent to a border (see RelevantSurfaceCheck)
if (anySurfaces && (bits[ctr] & RelevantSurfaceCheck) == 0) counter[ctr] = -1;
}
for (int i = 0; i < voxelArea.compactSpanCount; i++) {
int r = (int)reg[i];
if (r >= nReg) {
continue;
}
if (counter[union_find_find(counter, r)] >= -minRegionSize-1) {
reg[i] = 0;
}
}
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4111a824dba404c17b6d020ffd41c487
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}

View File

@@ -0,0 +1,57 @@
using UnityEngine;
using System.Collections.Generic;
namespace Pathfinding.Voxels {
using Pathfinding.Util;
/// <summary>Various utilities for voxel rasterization.</summary>
public class Utility {
public static float Min (float a, float b, float c) {
a = a < b ? a : b;
return a < c ? a : c;
}
public static float Max (float a, float b, float c) {
a = a > b ? a : b;
return a > c ? a : c;
}
/// <summary>
/// Removes duplicate vertices from the array and updates the triangle array.
/// Returns: The new array of vertices
/// </summary>
public static Int3[] RemoveDuplicateVertices (Int3[] vertices, int[] triangles) {
// Get a dictionary from an object pool to avoid allocating a new one
var firstVerts = ObjectPoolSimple<Dictionary<Int3, int> >.Claim();
firstVerts.Clear();
// Remove duplicate vertices
var compressedPointers = new int[vertices.Length];
int count = 0;
for (int i = 0; i < vertices.Length; i++) {
if (!firstVerts.ContainsKey(vertices[i])) {
firstVerts.Add(vertices[i], count);
compressedPointers[i] = count;
vertices[count] = vertices[i];
count++;
} else {
// There are some cases, rare but still there, that vertices are identical
compressedPointers[i] = firstVerts[vertices[i]];
}
}
firstVerts.Clear();
ObjectPoolSimple<Dictionary<Int3, int> >.Release(ref firstVerts);
for (int i = 0; i < triangles.Length; i++) {
triangles[i] = compressedPointers[triangles[i]];
}
var compressed = new Int3[count];
for (int i = 0; i < count; i++) compressed[i] = vertices[i];
return compressed;
}
}
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 323b25b9574384857a659b80fb8c1678
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}