using UnityEngine;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using Pathfinding.Util;
using UnityEngine.Profiling;
using System.Collections.Generic;
using Pathfinding.Jobs;
using Pathfinding.Graphs.Grid.Jobs;
using Pathfinding.Collections;
using Unity.Jobs.LowLevel.Unsafe;
namespace Pathfinding.Graphs.Grid {
public struct GridGraphNodeData {
public Allocator allocationMethod;
public int numNodes;
///
/// Bounds for the part of the graph that this data represents.
/// For example if the first layer of a layered grid graph is being updated between x=10 and x=20, z=5 and z=15
/// then this will be IntBounds(xmin=10, ymin=0, zmin=5, xmax=20, ymax=0, zmax=15)
///
public IntBounds bounds;
///
/// Number of layers that the data contains.
/// For a non-layered grid graph this will always be 1.
///
public int layers => bounds.size.y;
///
/// Positions of all nodes.
///
/// Data is valid in these passes:
/// - BeforeCollision: Valid
/// - BeforeConnections: Valid
/// - AfterConnections: Valid
/// - AfterErosion: Valid
/// - PostProcess: Valid
///
public NativeArray positions;
///
/// Bitpacked connections of all nodes.
///
/// Connections are stored in different formats depending on .
/// You can use and to access connections for the different data layouts.
///
/// Data is valid in these passes:
/// - BeforeCollision: Invalid
/// - BeforeConnections: Invalid
/// - AfterConnections: Valid
/// - AfterErosion: Valid (but will be overwritten)
/// - PostProcess: Valid
///
public NativeArray connections;
///
/// Bitpacked connections of all nodes.
///
/// Data is valid in these passes:
/// - BeforeCollision: Valid
/// - BeforeConnections: Valid
/// - AfterConnections: Valid
/// - AfterErosion: Valid
/// - PostProcess: Valid
///
public NativeArray penalties;
///
/// Tags of all nodes
///
/// Data is valid in these passes:
/// - BeforeCollision: Valid (but if erosion uses tags then it will be overwritten later)
/// - BeforeConnections: Valid (but if erosion uses tags then it will be overwritten later)
/// - AfterConnections: Valid (but if erosion uses tags then it will be overwritten later)
/// - AfterErosion: Valid
/// - PostProcess: Valid
///
public NativeArray tags;
///
/// Normals of all nodes.
/// If height testing is disabled the normal will be (0,1,0) for all nodes.
/// If a node doesn't exist (only happens in layered grid graphs) or if the height raycast didn't hit anything then the normal will be (0,0,0).
///
/// Data is valid in these passes:
/// - BeforeCollision: Valid
/// - BeforeConnections: Valid
/// - AfterConnections: Valid
/// - AfterErosion: Valid
/// - PostProcess: Valid
///
public NativeArray normals;
///
/// Walkability of all nodes before erosion happens.
///
/// Data is valid in these passes:
/// - BeforeCollision: Valid (it will be combined with collision testing later)
/// - BeforeConnections: Valid
/// - AfterConnections: Valid
/// - AfterErosion: Valid
/// - PostProcess: Valid
///
public NativeArray walkable;
///
/// Walkability of all nodes after erosion happens. This is the final walkability of the nodes.
/// If no erosion is used then the data will just be copied from the array.
///
/// Data is valid in these passes:
/// - BeforeCollision: Invalid
/// - BeforeConnections: Invalid
/// - AfterConnections: Invalid
/// - AfterErosion: Valid
/// - PostProcess: Valid
///
public NativeArray walkableWithErosion;
///
/// True if the data may have multiple layers.
/// For layered data the nodes are laid out as `data[y*width*depth + z*width + x]`.
/// For non-layered data the nodes are laid out as `data[z*width + x]` (which is equivalent to the above layout assuming y=0).
///
/// This also affects how node connections are stored. You can use and to access
/// connections for the different data layouts.
///
public bool layeredDataLayout;
public void AllocateBuffers (JobDependencyTracker dependencyTracker) {
Profiler.BeginSample("Allocating buffers");
// Allocate buffers for jobs
// Allocating buffers with uninitialized memory is much faster if no jobs assume anything about their contents
if (dependencyTracker != null) {
positions = dependencyTracker.NewNativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
normals = dependencyTracker.NewNativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
connections = dependencyTracker.NewNativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
penalties = dependencyTracker.NewNativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
walkable = dependencyTracker.NewNativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
walkableWithErosion = dependencyTracker.NewNativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
tags = dependencyTracker.NewNativeArray(numNodes, allocationMethod, NativeArrayOptions.ClearMemory);
} else {
positions = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
normals = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
connections = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
penalties = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
walkable = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
walkableWithErosion = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
tags = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.ClearMemory);
}
Profiler.EndSample();
}
public void TrackBuffers (JobDependencyTracker dependencyTracker) {
if (positions.IsCreated) dependencyTracker.Track(positions);
if (normals.IsCreated) dependencyTracker.Track(normals);
if (connections.IsCreated) dependencyTracker.Track(connections);
if (penalties.IsCreated) dependencyTracker.Track(penalties);
if (walkable.IsCreated) dependencyTracker.Track(walkable);
if (walkableWithErosion.IsCreated) dependencyTracker.Track(walkableWithErosion);
if (tags.IsCreated) dependencyTracker.Track(tags);
}
public void PersistBuffers (JobDependencyTracker dependencyTracker) {
dependencyTracker.Persist(positions);
dependencyTracker.Persist(normals);
dependencyTracker.Persist(connections);
dependencyTracker.Persist(penalties);
dependencyTracker.Persist(walkable);
dependencyTracker.Persist(walkableWithErosion);
dependencyTracker.Persist(tags);
}
public void Dispose () {
bounds = default;
numNodes = 0;
if (positions.IsCreated) positions.Dispose();
if (normals.IsCreated) normals.Dispose();
if (connections.IsCreated) connections.Dispose();
if (penalties.IsCreated) penalties.Dispose();
if (walkable.IsCreated) walkable.Dispose();
if (walkableWithErosion.IsCreated) walkableWithErosion.Dispose();
if (tags.IsCreated) tags.Dispose();
}
public JobHandle Rotate2D (int dx, int dz, JobHandle dependency) {
var size = bounds.size;
unsafe {
var jobs = stackalloc JobHandle[7];
jobs[0] = positions.Rotate3D(size, dx, dz).Schedule(dependency);
jobs[1] = normals.Rotate3D(size, dx, dz).Schedule(dependency);
jobs[2] = connections.Rotate3D(size, dx, dz).Schedule(dependency);
jobs[3] = penalties.Rotate3D(size, dx, dz).Schedule(dependency);
jobs[4] = walkable.Rotate3D(size, dx, dz).Schedule(dependency);
jobs[5] = walkableWithErosion.Rotate3D(size, dx, dz).Schedule(dependency);
jobs[6] = tags.Rotate3D(size, dx, dz).Schedule(dependency);
return JobHandleUnsafeUtility.CombineDependencies(jobs, 7);
}
}
public void ResizeLayerCount (int layerCount, JobDependencyTracker dependencyTracker) {
if (layerCount > layers) {
var oldData = this;
this.bounds.max.y = layerCount;
this.numNodes = bounds.volume;
this.AllocateBuffers(dependencyTracker);
// Ensure the normals for the upper layers are zeroed out.
// All other node data in the upper layers can be left uninitialized.
this.normals.MemSet(float4.zero).Schedule(dependencyTracker);
this.walkable.MemSet(false).Schedule(dependencyTracker);
this.walkableWithErosion.MemSet(false).Schedule(dependencyTracker);
new JobCopyBuffers {
input = oldData,
output = this,
copyPenaltyAndTags = true,
bounds = oldData.bounds,
}.Schedule(dependencyTracker);
}
if (layerCount < layers) {
throw new System.ArgumentException("Cannot reduce the number of layers");
}
}
struct LightReader : GridIterationUtilities.ISliceAction {
public GridNodeBase[] nodes;
public UnsafeSpan nodePositions;
public UnsafeSpan nodeWalkable;
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
public void Execute (uint outerIdx, uint innerIdx) {
// The data bounds may have more layers than the existing nodes if a new layer is being added.
// We can only copy from the nodes that exist.
if (outerIdx < nodes.Length) {
var node = nodes[outerIdx];
if (node != null) {
nodePositions[innerIdx] = (Vector3)node.position;
nodeWalkable[innerIdx] = node.Walkable;
return;
}
}
// Fallback in case the node was null (only happens for layered grid graphs),
// or if we are adding more layers to the graph, in which case we are outside
// the bounds of the nodes array.
nodePositions[innerIdx] = Vector3.zero;
nodeWalkable[innerIdx] = false;
}
}
public void ReadFromNodesForConnectionCalculations (GridNodeBase[] nodes, Slice3D slice, JobHandle nodesDependsOn, NativeArray graphNodeNormals, JobDependencyTracker dependencyTracker) {
bounds = slice.slice;
numNodes = slice.slice.volume;
Profiler.BeginSample("Allocating buffers");
positions = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
normals = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
connections = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
walkableWithErosion = new NativeArray(numNodes, allocationMethod, NativeArrayOptions.UninitializedMemory);
Profiler.EndSample();
Profiler.BeginSample("Reading node data");
var reader = new LightReader {
nodes = nodes,
nodePositions = this.positions.AsUnsafeSpan(),
nodeWalkable = this.walkableWithErosion.AsUnsafeSpan(),
};
GridIterationUtilities.ForEachCellIn3DSlice(slice, ref reader);
Profiler.EndSample();
ReadNodeNormals(slice, graphNodeNormals, dependencyTracker);
}
void ReadNodeNormals (Slice3D slice, NativeArray graphNodeNormals, JobDependencyTracker dependencyTracker) {
UnityEngine.Assertions.Assert.IsTrue(graphNodeNormals.IsCreated);
// Read the normal data from the graphNodeNormals array and copy it to the nodeNormals array.
// The nodeArrayBounds may have fewer layers than the readBounds if layers are being added.
// This means we can copy only a subset of the normals.
// We MemSet the array to zero first to avoid any uninitialized data remaining.
// TODO: Do clamping in caller
//var clampedReadBounds = new IntBounds(readBounds.min, new int3(readBounds.max.x, math.min(nodeArrayBounds.y, readBounds.max.y), readBounds.max.z));
if (dependencyTracker != null) {
normals.MemSet(float4.zero).Schedule(dependencyTracker);
new JobCopyRectangle {
input = graphNodeNormals,
output = normals,
inputSlice = slice,
outputSlice = new Slice3D(bounds, slice.slice),
}.Schedule(dependencyTracker);
} else {
Profiler.BeginSample("ReadNodeNormals");
normals.AsUnsafeSpan().FillZeros();
JobCopyRectangle.Copy(graphNodeNormals, normals, slice, new Slice3D(bounds, slice.slice));
Profiler.EndSample();
}
}
public static GridGraphNodeData ReadFromNodes (GridNodeBase[] nodes, Slice3D slice, JobHandle nodesDependsOn, NativeArray graphNodeNormals, Allocator allocator, bool layeredDataLayout, JobDependencyTracker dependencyTracker) {
var nodeData = new GridGraphNodeData {
allocationMethod = allocator,
numNodes = slice.slice.volume,
bounds = slice.slice,
layeredDataLayout = layeredDataLayout,
};
nodeData.AllocateBuffers(dependencyTracker);
// This is a managed type, we need to trick Unity to allow this inside of a job
var nodesHandle = System.Runtime.InteropServices.GCHandle.Alloc(nodes);
var job = new JobReadNodeData {
nodesHandle = nodesHandle,
nodePositions = nodeData.positions,
nodePenalties = nodeData.penalties,
nodeTags = nodeData.tags,
nodeConnections = nodeData.connections,
nodeWalkableWithErosion = nodeData.walkableWithErosion,
nodeWalkable = nodeData.walkable,
slice = slice,
}.ScheduleBatch(nodeData.numNodes, math.max(2000, nodeData.numNodes/16), dependencyTracker, nodesDependsOn);
dependencyTracker.DeferFree(nodesHandle, job);
if (graphNodeNormals.IsCreated) nodeData.ReadNodeNormals(slice, graphNodeNormals, dependencyTracker);
return nodeData;
}
public GridGraphNodeData ReadFromNodesAndCopy (GridNodeBase[] nodes, Slice3D slice, JobHandle nodesDependsOn, NativeArray graphNodeNormals, bool copyPenaltyAndTags, JobDependencyTracker dependencyTracker) {
var newData = GridGraphNodeData.ReadFromNodes(nodes, slice, nodesDependsOn, graphNodeNormals, allocationMethod, layeredDataLayout, dependencyTracker);
// Overwrite a rectangle in the center with the data from this object.
// In the end we will have newly calculated data in the middle and data read from nodes along the borders
newData.CopyFrom(this, copyPenaltyAndTags, dependencyTracker);
return newData;
}
public void CopyFrom(GridGraphNodeData other, bool copyPenaltyAndTags, JobDependencyTracker dependencyTracker) => CopyFrom(other, IntBounds.Intersection(bounds, other.bounds), copyPenaltyAndTags, dependencyTracker);
public void CopyFrom (GridGraphNodeData other, IntBounds bounds, bool copyPenaltyAndTags, JobDependencyTracker dependencyTracker) {
var job = new JobCopyBuffers {
input = other,
output = this,
copyPenaltyAndTags = copyPenaltyAndTags,
bounds = bounds,
};
if (dependencyTracker != null) {
job.Schedule(dependencyTracker);
} else {
#if UNITY_2022_2_OR_NEWER
job.RunByRef();
#else
job.Run();
#endif
}
}
public JobHandle AssignToNodes (GridNodeBase[] nodes, int3 nodeArrayBounds, IntBounds writeMask, uint graphIndex, JobHandle nodesDependsOn, JobDependencyTracker dependencyTracker) {
// This is a managed type, we need to trick Unity to allow this inside of a job
var nodesHandle = System.Runtime.InteropServices.GCHandle.Alloc(nodes);
// Assign the data to the nodes (in parallel for performance)
// This will also dirty all nodes, but that is a thread-safe operation.
var job2 = new JobWriteNodeData {
nodesHandle = nodesHandle,
graphIndex = graphIndex,
nodePositions = positions,
nodePenalties = penalties,
nodeTags = tags,
nodeConnections = connections,
nodeWalkableWithErosion = walkableWithErosion,
nodeWalkable = walkable,
nodeArrayBounds = nodeArrayBounds,
dataBounds = bounds,
writeMask = writeMask,
}.ScheduleBatch(writeMask.volume, math.max(1000, writeMask.volume/16), dependencyTracker, nodesDependsOn);
dependencyTracker.DeferFree(nodesHandle, job2);
return job2;
}
}
public struct GridGraphScanData {
///
/// Tracks dependencies between jobs to allow parallelism without tediously specifying dependencies manually.
/// Always use when scheduling jobs.
///
public JobDependencyTracker dependencyTracker;
/// The up direction of the graph, in world space
public Vector3 up;
/// Transforms graph-space to world space
public GraphTransform transform;
/// Data for all nodes in the graph update that is being calculated
public GridGraphNodeData nodes;
///
/// Bounds of the data arrays.
/// Deprecated: Use nodes.bounds or heightHitsBounds depending on if you are using the heightHits array or not
///
[System.Obsolete("Use nodes.bounds or heightHitsBounds depending on if you are using the heightHits array or not")]
public IntBounds bounds => nodes.bounds;
///
/// True if the data may have multiple layers.
/// For layered data the nodes are laid out as `data[y*width*depth + z*width + x]`.
/// For non-layered data the nodes are laid out as `data[z*width + x]` (which is equivalent to the above layout assuming y=0).
///
/// Deprecated: Use nodes.layeredDataLayout instead
///
[System.Obsolete("Use nodes.layeredDataLayout instead")]
public bool layeredDataLayout => nodes.layeredDataLayout;
///
/// Raycasts hits used for height testing.
/// This data is only valid if height testing is enabled, otherwise the array is uninitialized (heightHits.IsCreated will be false).
///
/// Data is valid in these passes:
/// - BeforeCollision: Valid (if height testing is enabled)
/// - BeforeConnections: Valid (if height testing is enabled)
/// - AfterConnections: Valid (if height testing is enabled)
/// - AfterErosion: Valid (if height testing is enabled)
/// - PostProcess: Valid (if height testing is enabled)
///
/// Warning: This array does not have the same size as the arrays in . It will usually be slightly smaller. See .
///
public NativeArray heightHits;
///
/// Bounds for the array.
///
/// During an update, the scan data may contain more nodes than we are doing height testing for.
/// For a few nodes around the update, the data will be read from the existing graph, instead. This is done for performance.
/// This means that there may not be any height testing information these nodes.
/// However, all nodes that will be written to will always have height testing information.
///
public IntBounds heightHitsBounds;
///
/// Node positions.
/// Deprecated: Use instead
///
[System.Obsolete("Use nodes.positions instead")]
public NativeArray nodePositions => nodes.positions;
///
/// Node connections.
/// Deprecated: Use instead
///
[System.Obsolete("Use nodes.connections instead")]
public NativeArray nodeConnections => nodes.connections;
///
/// Node penalties.
/// Deprecated: Use instead
///
[System.Obsolete("Use nodes.penalties instead")]
public NativeArray nodePenalties => nodes.penalties;
///
/// Node tags.
/// Deprecated: Use instead
///
[System.Obsolete("Use nodes.tags instead")]
public NativeArray nodeTags => nodes.tags;
///
/// Node normals.
/// Deprecated: Use instead
///
[System.Obsolete("Use nodes.normals instead")]
public NativeArray nodeNormals => nodes.normals;
///
/// Node walkability.
/// Deprecated: Use instead
///
[System.Obsolete("Use nodes.walkable instead")]
public NativeArray nodeWalkable => nodes.walkable;
///
/// Node walkability with erosion.
/// Deprecated: Use instead
///
[System.Obsolete("Use nodes.walkableWithErosion instead")]
public NativeArray nodeWalkableWithErosion => nodes.walkableWithErosion;
public void SetDefaultPenalties (uint initialPenalty) {
nodes.penalties.MemSet(initialPenalty).Schedule(dependencyTracker);
}
public void SetDefaultNodePositions (GraphTransform transform) {
new JobNodeGridLayout {
graphToWorld = transform.matrix,
bounds = nodes.bounds,
nodePositions = nodes.positions,
}.Schedule(dependencyTracker);
}
public JobHandle HeightCheck (GraphCollision collision, int maxHits, IntBounds recalculationBounds, NativeArray outLayerCount, float characterHeight, Allocator allocator) {
// For some reason the physics code crashes when allocating raycastCommands with UninitializedMemory, even though I have verified that every
// element in the array is set to a well defined value before the physics code gets to it... Mysterious.
var cellCount = recalculationBounds.size.x * recalculationBounds.size.z;
var raycastCommands = dependencyTracker.NewNativeArray(cellCount, allocator, NativeArrayOptions.ClearMemory);
heightHits = dependencyTracker.NewNativeArray(cellCount * maxHits, allocator, NativeArrayOptions.ClearMemory);
heightHitsBounds = recalculationBounds;
// Due to floating point inaccuracies we don't want the rays to end *exactly* at the base of the graph
// The rays may or may not hit colliders with the exact same y coordinate.
// We extend the rays a bit to ensure they always hit
const float RayLengthMargin = 0.01f;
var prepareJob = new JobPrepareGridRaycast {
graphToWorld = transform.matrix,
bounds = recalculationBounds,
physicsScene = Physics.defaultPhysicsScene,
raycastOffset = up * collision.fromHeight,
raycastDirection = -up * (collision.fromHeight + RayLengthMargin),
raycastMask = collision.heightMask,
raycastCommands = raycastCommands,
}.Schedule(dependencyTracker);
if (maxHits > 1) {
// Skip this distance between each hit.
// It is pretty arbitrarily chosen, but it must be lower than characterHeight.
// If it would be set too low then many thin colliders stacked on top of each other could lead to a very large number of hits
// that will not lead to any walkable nodes anyway.
float minStep = characterHeight * 0.5f;
var dependency = new JobRaycastAll(raycastCommands, heightHits, Physics.defaultPhysicsScene, maxHits, allocator, dependencyTracker, minStep).Schedule(prepareJob);
dependency = new JobMaxHitCount {
hits = heightHits,
maxHits = maxHits,
layerStride = cellCount,
maxHitCount = outLayerCount,
}.Schedule(dependency);
return dependency;
} else {
dependencyTracker.ScheduleBatch(raycastCommands, heightHits, 2048);
outLayerCount[0] = 1;
return default;
}
}
public void CopyHits (IntBounds recalculationBounds) {
// Copy the hit points and normals to separate arrays
// Ensure the normals for the upper layers are zeroed out.
nodes.normals.MemSet(float4.zero).Schedule(dependencyTracker);
new JobCopyHits {
hits = heightHits,
points = nodes.positions,
normals = nodes.normals,
slice = new Slice3D(nodes.bounds, recalculationBounds),
}.Schedule(dependencyTracker);
}
public void CalculateWalkabilityFromHeightData (bool useRaycastNormal, bool unwalkableWhenNoGround, float maxSlope, float characterHeight) {
new JobNodeWalkability {
useRaycastNormal = useRaycastNormal,
unwalkableWhenNoGround = unwalkableWhenNoGround,
maxSlope = maxSlope,
up = up,
nodeNormals = nodes.normals,
nodeWalkable = nodes.walkable,
nodePositions = nodes.positions.Reinterpret(),
characterHeight = characterHeight,
layerStride = nodes.bounds.size.x*nodes.bounds.size.z,
}.Schedule(dependencyTracker);
}
public IEnumerator CollisionCheck (GraphCollision collision, IntBounds calculationBounds) {
if (collision.type == ColliderType.Ray && !collision.use2D) {
var collisionCheckResult = dependencyTracker.NewNativeArray(nodes.numNodes, nodes.allocationMethod, NativeArrayOptions.UninitializedMemory);
collision.JobCollisionRay(nodes.positions, collisionCheckResult, up, nodes.allocationMethod, dependencyTracker);
nodes.walkable.BitwiseAndWith(collisionCheckResult).WithLength(nodes.numNodes).Schedule(dependencyTracker);
return null;
// Before Unity 2023.3, these features compile, but they will cause memory corruption in some cases, due to a bug in Unity
#if UNITY_2022_2_OR_NEWER && UNITY_2023_3_OR_NEWER && UNITY_HAS_FIXED_MEMORY_CORRUPTION_ISSUE
} else if (collision.type == ColliderType.Capsule && !collision.use2D) {
var collisionCheckResult = dependencyTracker.NewNativeArray(nodes.numNodes, nodes.allocationMethod, NativeArrayOptions.UninitializedMemory);
collision.JobCollisionCapsule(nodes.positions, collisionCheckResult, up, nodes.allocationMethod, dependencyTracker);
nodes.walkable.BitwiseAndWith(collisionCheckResult).WithLength(nodes.numNodes).Schedule(dependencyTracker);
return null;
} else if (collision.type == ColliderType.Sphere && !collision.use2D) {
var collisionCheckResult = dependencyTracker.NewNativeArray(nodes.numNodes, nodes.allocationMethod, NativeArrayOptions.UninitializedMemory);
collision.JobCollisionSphere(nodes.positions, collisionCheckResult, up, nodes.allocationMethod, dependencyTracker);
nodes.walkable.BitwiseAndWith(collisionCheckResult).WithLength(nodes.numNodes).Schedule(dependencyTracker);
return null;
#endif
} else {
// This part can unfortunately not be jobified yet
return new JobCheckCollisions {
nodePositions = nodes.positions,
collisionResult = nodes.walkable,
collision = collision,
}.ExecuteMainThreadJob(dependencyTracker);
}
}
public void Connections (float maxStepHeight, bool maxStepUsesSlope, IntBounds calculationBounds, NumNeighbours neighbours, bool cutCorners, bool use2D, bool useErodedWalkability, float characterHeight) {
var job = new JobCalculateGridConnections {
maxStepHeight = maxStepHeight,
maxStepUsesSlope = maxStepUsesSlope,
graphToWorld = transform.matrix,
bounds = calculationBounds.Offset(-nodes.bounds.min),
arrayBounds = nodes.bounds.size,
neighbours = neighbours,
use2D = use2D,
cutCorners = cutCorners,
nodeWalkable = (useErodedWalkability ? nodes.walkableWithErosion : nodes.walkable).AsUnsafeSpanNoChecks(),
nodePositions = nodes.positions.AsUnsafeSpanNoChecks(),
nodeNormals = nodes.normals.AsUnsafeSpanNoChecks(),
nodeConnections = nodes.connections.AsUnsafeSpanNoChecks(),
characterHeight = characterHeight,
layeredDataLayout = nodes.layeredDataLayout,
};
if (dependencyTracker != null) {
job.ScheduleBatch(calculationBounds.size.z, 20, dependencyTracker);
} else {
job.RunBatch(calculationBounds.size.z);
}
// For single layer graphs this will have already been done in the JobCalculateGridConnections job
// but for layered grid graphs we need to handle things differently because the data layout is different.
// It needs to be done after all axis aligned connections have been calculated.
if (nodes.layeredDataLayout) {
var job2 = new JobFilterDiagonalConnections {
slice = new Slice3D(nodes.bounds, calculationBounds),
neighbours = neighbours,
cutCorners = cutCorners,
nodeConnections = nodes.connections.AsUnsafeSpanNoChecks(),
};
if (dependencyTracker != null) {
job2.ScheduleBatch(calculationBounds.size.z, 20, dependencyTracker);
} else {
job2.RunBatch(calculationBounds.size.z);
}
}
}
public void Erosion (NumNeighbours neighbours, int erodeIterations, IntBounds erosionWriteMask, bool erosionUsesTags, int erosionStartTag, int erosionTagsPrecedenceMask) {
if (!nodes.layeredDataLayout) {
new JobErosion {
bounds = nodes.bounds,
writeMask = erosionWriteMask,
neighbours = neighbours,
nodeConnections = nodes.connections,
erosion = erodeIterations,
nodeWalkable = nodes.walkable,
outNodeWalkable = nodes.walkableWithErosion,
nodeTags = nodes.tags,
erosionUsesTags = erosionUsesTags,
erosionStartTag = erosionStartTag,
erosionTagsPrecedenceMask = erosionTagsPrecedenceMask,
}.Schedule(dependencyTracker);
} else {
new JobErosion {
bounds = nodes.bounds,
writeMask = erosionWriteMask,
neighbours = neighbours,
nodeConnections = nodes.connections,
erosion = erodeIterations,
nodeWalkable = nodes.walkable,
outNodeWalkable = nodes.walkableWithErosion,
nodeTags = nodes.tags,
erosionUsesTags = erosionUsesTags,
erosionStartTag = erosionStartTag,
erosionTagsPrecedenceMask = erosionTagsPrecedenceMask,
}.Schedule(dependencyTracker);
}
}
public void AssignNodeConnections (GridNodeBase[] nodes, int3 nodeArrayBounds, IntBounds writeBounds) {
var bounds = this.nodes.bounds;
var writeDataOffset = writeBounds.min - bounds.min;
var nodeConnections = this.nodes.connections.AsUnsafeReadOnlySpan();
for (int y = 0; y < writeBounds.size.y; y++) {
var yoffset = (y + writeBounds.min.y)*nodeArrayBounds.x*nodeArrayBounds.z;
for (int z = 0; z < writeBounds.size.z; z++) {
var zoffset = yoffset + (z + writeBounds.min.z)*nodeArrayBounds.x + writeBounds.min.x;
var zoffset2 = (y+writeDataOffset.y)*bounds.size.x*bounds.size.z + (z+writeDataOffset.z)*bounds.size.x + writeDataOffset.x;
for (int x = 0; x < writeBounds.size.x; x++) {
var node = nodes[zoffset + x];
var dataIdx = zoffset2 + x;
var conn = nodeConnections[dataIdx];
if (node == null) continue;
if (node is LevelGridNode lgnode) {
lgnode.SetAllConnectionInternal(conn);
} else {
var gnode = node as GridNode;
gnode.SetAllConnectionInternal((int)conn);
}
}
}
}
}
}
}