diff options
author | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
---|---|---|
committer | chai <215380520@qq.com> | 2024-05-23 10:08:29 +0800 |
commit | 8722a9920c1f6119bf6e769cba270e63097f8e25 (patch) | |
tree | 2eaf9865de7fb1404546de4a4296553d8f68cc3b /Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs | |
parent | 3ba4020b69e5971bb0df7ee08b31d10ea4d01937 (diff) |
+ astar project
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs')
34 files changed, 1536 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobAllocateNodes.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobAllocateNodes.cs new file mode 100644 index 0000000..d301d44 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobAllocateNodes.cs @@ -0,0 +1,57 @@ +using Pathfinding.Util; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Allocates and deallocates nodes in a grid graph. + /// + /// This will inspect every cell in the dataBounds and allocate or deallocate the node depending on if that slot should have a node or not according to the nodeNormals array (pure zeroes means no node). + /// + /// This is only used for incremental updates of grid graphs. + /// The initial layer of the grid graph (which is always filled with nodes) is allocated in the <see cref="GridGraph.AllocateNodesJob"/> method. + /// </summary> + public struct JobAllocateNodes : IJob { + public AstarPath active; + [ReadOnly] + public NativeArray<float4> nodeNormals; + public IntBounds dataBounds; + public int3 nodeArrayBounds; + public GridNodeBase[] nodes; + public System.Func<GridNodeBase> newGridNodeDelegate; + + public void Execute () { + var size = dataBounds.size; + + // Start at y=1 because all nodes at y=0 are guaranteed to already be allocated (they are always allocated in a layered grid graph). + var nodeNormalsSpan = nodeNormals.AsUnsafeReadOnlySpan(); + for (int y = 1; y < size.y; y++) { + for (int z = 0; z < size.z; z++) { + var rowOffset = ((y + dataBounds.min.y) * nodeArrayBounds.z + (z + dataBounds.min.z)) * nodeArrayBounds.x + dataBounds.min.x; + for (int x = 0; x < size.x; x++) { + var nodeIndex = rowOffset + x; + var shouldHaveNode = math.any(nodeNormalsSpan[nodeIndex]); + var node = nodes[nodeIndex]; + var hasNode = node != null; + if (shouldHaveNode != hasNode) { + if (shouldHaveNode) { + node = nodes[nodeIndex] = newGridNodeDelegate(); + active.InitializeNode(node); + } else { + // Clear custom connections first and clear connections from other nodes to this one + node.ClearCustomConnections(true); + // Clear grid connections without clearing the connections from other nodes to this one (a bit slow) + // Since this is inside a graph update we guarantee that the grid connections will be correct at the end + // of the update anyway + node.ResetConnectionsInternal(); + node.Destroy(); + nodes[nodeIndex] = null; + } + } + } + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobAllocateNodes.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobAllocateNodes.cs.meta new file mode 100644 index 0000000..a2391b4 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobAllocateNodes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 773a80d74b04a904faae186478d396c4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCalculateGridConnections.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCalculateGridConnections.cs new file mode 100644 index 0000000..0a14596 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCalculateGridConnections.cs @@ -0,0 +1,219 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using Pathfinding.Jobs; +using Pathfinding.Util; +using System.Data; +using UnityEngine.Assertions; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Calculates the grid connections for all nodes. + /// + /// This is a IJobParallelForBatch job. Calculating the connections in multiple threads is faster, + /// but due to hyperthreading (used on most intel processors) the individual threads will become slower. + /// It is still worth it though. + /// </summary> + [BurstCompile(FloatMode = FloatMode.Fast, CompileSynchronously = true)] + public struct JobCalculateGridConnections : IJobParallelForBatched { + public float maxStepHeight; + /// <summary>Normalized up direction</summary> + public Vector3 up; + public IntBounds bounds; + public int3 arrayBounds; + public NumNeighbours neighbours; + public bool use2D; + public bool cutCorners; + public bool maxStepUsesSlope; + public float characterHeight; + public bool layeredDataLayout; + + [ReadOnly] + public UnsafeSpan<bool> nodeWalkable; + + [ReadOnly] + public UnsafeSpan<float4> nodeNormals; + + [ReadOnly] + public UnsafeSpan<Vector3> nodePositions; + + /// <summary>All bitpacked node connections</summary> + [WriteOnly] + public UnsafeSpan<ulong> nodeConnections; + + public bool allowBoundsChecks => false; + + + /// <summary> + /// Check if a connection to node B is valid. + /// Node A is assumed to be walkable already + /// </summary> + public static bool IsValidConnection (float4 nodePosA, float4 nodeNormalA, bool nodeWalkableB, float4 nodePosB, float4 nodeNormalB, bool maxStepUsesSlope, float maxStepHeight, float4 up) { + if (!nodeWalkableB) return false; + + if (!maxStepUsesSlope) { + // Check their differences along the Y coordinate (well, the up direction really. It is not necessarily the Y axis). + return math.abs(math.dot(up, nodePosB - nodePosA)) <= maxStepHeight; + } else { + float4 v = nodePosB - nodePosA; + float heightDifference = math.dot(v, up); + + // Check if the step is small enough. + // This is a fast path for the common case. + if (math.abs(heightDifference) <= maxStepHeight) return true; + + float4 v_flat = (v - heightDifference * up) * 0.5f; + + // Math! + // Calculates the approximate offset along the up direction + // that the ground will have moved at the midpoint between the + // nodes compared to the nodes' center points. + float NDotU = math.dot(nodeNormalA, up); + float offsetA = -math.dot(nodeNormalA - NDotU * up, v_flat); + + NDotU = math.dot(nodeNormalB, up); + float offsetB = math.dot(nodeNormalB - NDotU * up, v_flat); + + // Check the height difference with slopes taken into account. + // Note that since we also do the heightDifference check above we will ensure slope offsets do not increase the height difference. + // If we allowed this then some connections might not be valid near the start of steep slopes. + return math.abs(heightDifference + offsetB - offsetA) <= maxStepHeight; + } + } + + public void Execute (int start, int count) { + if (layeredDataLayout) ExecuteLayered(start, count); + else ExecuteFlat(start, count); + } + + public void ExecuteFlat (int start, int count) { + if (maxStepHeight <= 0 || use2D) maxStepHeight = float.PositiveInfinity; + + float4 up = new float4(this.up.x, this.up.y, this.up.z, 0); + + NativeArray<int> neighbourOffsets = new NativeArray<int>(8, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + for (int i = 0; i < 8; i++) neighbourOffsets[i] = GridGraph.neighbourZOffsets[i] * arrayBounds.x + GridGraph.neighbourXOffsets[i]; + var nodePositions = this.nodePositions.Reinterpret<float3>(); + + // The loop is parallelized over z coordinates + start += bounds.min.z; + for (int z = start; z < start + count; z++) { + var initialConnections = 0xFF; + + // Disable connections to out-of-bounds nodes + // See GridNode.HasConnectionInDirection + if (z == 0) initialConnections &= ~((1 << 0) | (1 << 7) | (1 << 4)); + if (z == arrayBounds.z - 1) initialConnections &= ~((1 << 2) | (1 << 5) | (1 << 6)); + + for (int x = bounds.min.x; x < bounds.max.x; x++) { + int nodeIndex = z * arrayBounds.x + x; + if (!nodeWalkable[nodeIndex]) { + nodeConnections[nodeIndex] = 0; + continue; + } + + // Bitpacked connections + // bit 0 is set if connection 0 is enabled + // bit 1 is set if connection 1 is enabled etc. + int conns = initialConnections; + + // Disable connections to out-of-bounds nodes + if (x == 0) conns &= ~((1 << 3) | (1 << 6) | (1 << 7)); + if (x == arrayBounds.x - 1) conns &= ~((1 << 1) | (1 << 4) | (1 << 5)); + + float4 pos = new float4(nodePositions[nodeIndex], 0); + float4 normal = nodeNormals[nodeIndex]; + + for (int i = 0; i < 8; i++) { + int neighbourIndex = nodeIndex + neighbourOffsets[i]; + if ((conns & (1 << i)) != 0 && !IsValidConnection(pos, normal, nodeWalkable[neighbourIndex], new float4(nodePositions[neighbourIndex], 0), nodeNormals[neighbourIndex], maxStepUsesSlope, maxStepHeight, up)) { + // Enable connection i + conns &= ~(1 << i); + } + } + + nodeConnections[nodeIndex] = (ulong)GridNode.FilterDiagonalConnections(conns, neighbours, cutCorners); + } + } + } + + public void ExecuteLayered (int start, int count) { + if (maxStepHeight <= 0 || use2D) maxStepHeight = float.PositiveInfinity; + + float4 up = new float4(this.up.x, this.up.y, this.up.z, 0); + + NativeArray<int> neighbourOffsets = new NativeArray<int>(8, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + for (int i = 0; i < 8; i++) neighbourOffsets[i] = GridGraph.neighbourZOffsets[i] * arrayBounds.x + GridGraph.neighbourXOffsets[i]; + + var layerStride = arrayBounds.z*arrayBounds.x; + start += bounds.min.z; + for (int y = bounds.min.y; y < bounds.max.y; y++) { + // The loop is parallelized over z coordinates + for (int z = start; z < start + count; z++) { + for (int x = bounds.min.x; x < bounds.max.x; x++) { + // Bitpacked connections + ulong conns = 0; + int nodeIndexXZ = z * arrayBounds.x + x; + int nodeIndex = nodeIndexXZ + y * layerStride; + float4 pos = new float4(nodePositions[nodeIndex], 0); + float4 normal = nodeNormals[nodeIndex]; + + if (nodeWalkable[nodeIndex]) { + var ourY = math.dot(up, pos); + + float ourHeight; + if (y == arrayBounds.y-1 || !math.any(nodeNormals[nodeIndex + layerStride])) { + ourHeight = float.PositiveInfinity; + } else { + var nodeAboveNeighbourPos = new float4(nodePositions[nodeIndex + layerStride], 0); + ourHeight = math.max(0, math.dot(up, nodeAboveNeighbourPos) - ourY); + } + + for (int i = 0; i < 8; i++) { + int nx = x + GridGraph.neighbourXOffsets[i]; + int nz = z + GridGraph.neighbourZOffsets[i]; + + // Check if the new position is inside the grid + int conn = LevelGridNode.NoConnection; + if (nx >= 0 && nz >= 0 && nx < arrayBounds.x && nz < arrayBounds.z) { + int neighbourStartIndex = nodeIndexXZ + neighbourOffsets[i]; + for (int y2 = 0; y2 < arrayBounds.y; y2++) { + var neighbourIndex = neighbourStartIndex + y2 * layerStride; + float4 nodePosB = new float4(nodePositions[neighbourIndex], 0); + var neighbourY = math.dot(up, nodePosB); + // Is there a node above this one + float neighbourHeight; + if (y2 == arrayBounds.y-1 || !math.any(nodeNormals[neighbourIndex + layerStride])) { + neighbourHeight = float.PositiveInfinity; + } else { + var nodeAboveNeighbourPos = new float4(nodePositions[neighbourIndex + layerStride], 0); + neighbourHeight = math.max(0, math.dot(up, nodeAboveNeighbourPos) - neighbourY); + } + + float bottom = math.max(neighbourY, ourY); + float top = math.min(neighbourY + neighbourHeight, ourY + ourHeight); + + float dist = top-bottom; + + if (dist >= characterHeight && IsValidConnection(pos, normal, nodeWalkable[neighbourIndex], new float4(nodePositions[neighbourIndex], 0), nodeNormals[neighbourIndex], maxStepUsesSlope, maxStepHeight, up)) { + conn = y2; + } + } + } + + conns |= (ulong)conn << LevelGridNode.ConnectionStride*i; + } + } else { + conns = LevelGridNode.AllConnectionsMask; + } + + nodeConnections[nodeIndex] = conns; + } + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCalculateGridConnections.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCalculateGridConnections.cs.meta new file mode 100644 index 0000000..089d203 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCalculateGridConnections.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63dd791a75e95424ea05940b4c155c25 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCheckCollisions.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCheckCollisions.cs new file mode 100644 index 0000000..71a3d2c --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCheckCollisions.cs @@ -0,0 +1,33 @@ +using UnityEngine; +using Unity.Collections; +using Pathfinding.Jobs; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Checks if nodes are obstructed by obstacles or not. + /// + /// See: <see cref="GraphCollision"/> + /// </summary> + struct JobCheckCollisions : IJobTimeSliced { + [ReadOnly] + public NativeArray<Vector3> nodePositions; + public NativeArray<bool> collisionResult; + public GraphCollision collision; + int startIndex; + + public void Execute () { + Execute(TimeSlice.Infinite); + } + + public bool Execute (TimeSlice timeSlice) { + for (int i = startIndex; i < nodePositions.Length; i++) { + collisionResult[i] = collisionResult[i] && collision.Check(nodePositions[i]); + if ((i & 127) == 0 && timeSlice.expired) { + startIndex = i + 1; + return false; + } + } + return true; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCheckCollisions.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCheckCollisions.cs.meta new file mode 100644 index 0000000..9311878 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCheckCollisions.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6977ffeeb891185449eae9f827667558 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobColliderHitsToBooleans.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobColliderHitsToBooleans.cs new file mode 100644 index 0000000..84ae59e --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobColliderHitsToBooleans.cs @@ -0,0 +1,28 @@ +#if UNITY_2022_2_OR_NEWER +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Fills the output with true or false depending on if the collider hit was a hit. + /// + /// result[i] = false if hits[i] is a valid hit, otherwise true. + /// </summary> + [BurstCompile] + public struct JobColliderHitsToBooleans : IJob { + [ReadOnly] + public NativeArray<ColliderHit> hits; + + [WriteOnly] + public NativeArray<bool> result; + + public void Execute () { + for (int i = 0; i < hits.Length; i++) { + result[i] = hits[i].instanceID == 0; + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobColliderHitsToBooleans.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobColliderHitsToBooleans.cs.meta new file mode 100644 index 0000000..39e8bb7 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobColliderHitsToBooleans.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0e741e2fc3248564998e707ba25fe69c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCopyBuffers.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCopyBuffers.cs new file mode 100644 index 0000000..690448f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCopyBuffers.cs @@ -0,0 +1,50 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using Pathfinding.Jobs; +using UnityEngine.Assertions; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Copies 3D arrays with grid data from one shape to another. + /// + /// Only the data for the nodes that exist in both buffers will be copied. + /// + /// This essentially is several <see cref="JobCopyRectangle"/> jobs in one (to avoid scheduling overhead). + /// See that job for more documentation. + /// </summary> + [BurstCompile] + public struct JobCopyBuffers : IJob { + [ReadOnly] + [DisableUninitializedReadCheck] + public GridGraphNodeData input; + + [WriteOnly] + public GridGraphNodeData output; + public IntBounds bounds; + + public bool copyPenaltyAndTags; + + public void Execute () { +#if ENABLE_UNITY_COLLECTIONS_CHECKS + if (!input.bounds.Contains(bounds)) throw new System.ArgumentException("Bounds are outside the source buffer"); + if (!output.bounds.Contains(bounds)) throw new System.ArgumentException("Bounds are outside the destination buffer"); +#endif + var inputSlice = new Slice3D(input.bounds, bounds); + var outputSlice = new Slice3D(output.bounds, bounds); + // Note: Having a single job that copies all of the buffers avoids a lot of scheduling overhead. + // We do miss out on parallelization, however for this job it is not that significant. + JobCopyRectangle<Vector3>.Copy(input.positions, output.positions, inputSlice, outputSlice); + JobCopyRectangle<float4>.Copy(input.normals, output.normals, inputSlice, outputSlice); + JobCopyRectangle<ulong>.Copy(input.connections, output.connections, inputSlice, outputSlice); + if (copyPenaltyAndTags) { + JobCopyRectangle<uint>.Copy(input.penalties, output.penalties, inputSlice, outputSlice); + JobCopyRectangle<int>.Copy(input.tags, output.tags, inputSlice, outputSlice); + } + JobCopyRectangle<bool>.Copy(input.walkable, output.walkable, inputSlice, outputSlice); + JobCopyRectangle<bool>.Copy(input.walkableWithErosion, output.walkableWithErosion, inputSlice, outputSlice); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCopyBuffers.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCopyBuffers.cs.meta new file mode 100644 index 0000000..9c60956 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobCopyBuffers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a3c4dcf30e4497e44a9c7421803ec902 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobErosion.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobErosion.cs new file mode 100644 index 0000000..5b5ba35 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobErosion.cs @@ -0,0 +1,190 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using Pathfinding.Jobs; +using UnityEngine.Assertions; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Calculates erosion. + /// Note that to ensure that connections are completely up to date after updating a node you + /// have to calculate the connections for both the changed node and its neighbours. + /// + /// In a layered grid graph, this will recalculate the connections for all nodes + /// in the (x,z) cell (it may have multiple layers of nodes). + /// + /// See: CalculateConnections(GridNodeBase) + /// </summary> + [BurstCompile] + public struct JobErosion<AdjacencyMapper> : IJob where AdjacencyMapper : GridAdjacencyMapper, new() { + public IntBounds bounds; + public IntBounds writeMask; + public NumNeighbours neighbours; + public int erosion; + public bool erosionUsesTags; + public int erosionStartTag; + + [ReadOnly] + public NativeArray<ulong> nodeConnections; + + [ReadOnly] + public NativeArray<bool> nodeWalkable; + + [WriteOnly] + public NativeArray<bool> outNodeWalkable; + + public NativeArray<int> nodeTags; + public int erosionTagsPrecedenceMask; + + // Note: the first 3 connections are to nodes with a higher x or z coordinate + // The last 3 connections are to nodes with a lower x or z coordinate + // This is required for the grassfire transform to work properly + // This is a permutation of GridGraph.hexagonNeighbourIndices + static readonly int[] hexagonNeighbourIndices = { 1, 2, 5, 0, 3, 7 }; + + public void Execute () { + var slice = new Slice3D(bounds, bounds); + var size = slice.slice.size; + slice.AssertMatchesOuter(nodeConnections); + slice.AssertMatchesOuter(nodeWalkable); + slice.AssertMatchesOuter(outNodeWalkable); + slice.AssertMatchesOuter(nodeTags); + Assert.IsTrue(bounds.Contains(writeMask)); + + var(outerStrideX, outerStrideY, outerStrideZ) = slice.outerStrides; + var(innerStrideX, innerStrideY, innerStrideZ) = slice.innerStrides; + NativeArray<int> neighbourOffsets = new NativeArray<int>(8, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + for (int i = 0; i < 8; i++) neighbourOffsets[i] = GridGraph.neighbourZOffsets[i] * innerStrideZ + GridGraph.neighbourXOffsets[i] * innerStrideX; + + var erosionDistances = new NativeArray<int>(slice.length, Allocator.Temp, NativeArrayOptions.ClearMemory); + var adjacencyMapper = new AdjacencyMapper(); + var layers = adjacencyMapper.LayerCount(slice.slice); + var outerOffset = slice.outerStartIndex; + if (neighbours == NumNeighbours.Six) { + // Use the grassfire transform: https://en.wikipedia.org/wiki/Grassfire_transform extended to hexagonal graphs + for (int z = 1; z < size.z - 1; z++) { + for (int x = 1; x < size.x - 1; x++) { + for (int y = 0; y < layers; y++) { + // Note: This is significantly faster than using csum, because burst can optimize it better + int outerIndex = z * outerStrideZ + x * outerStrideX + y * outerStrideY + outerOffset; + var innerIndexXZ = z * innerStrideZ + x * innerStrideX; + int innerIndex = innerIndexXZ + y * innerStrideY; + int v = int.MaxValue; + for (int i = 3; i < 6; i++) { + int connection = hexagonNeighbourIndices[i]; + if (!adjacencyMapper.HasConnection(outerIndex, connection, nodeConnections)) v = -1; + else v = math.min(v, erosionDistances[adjacencyMapper.GetNeighbourIndex(innerIndexXZ, innerIndex, connection, nodeConnections, neighbourOffsets, innerStrideY)]); + } + + erosionDistances[innerIndex] = v + 1; + } + } + } + + for (int z = size.z - 2; z > 0; z--) { + for (int x = size.x - 2; x > 0; x--) { + for (int y = 0; y < layers; y++) { + int outerIndex = z * outerStrideZ + x * outerStrideX + y * outerStrideY + outerOffset; + var innerIndexXZ = z * innerStrideZ + x * innerStrideX; + int innerIndex = innerIndexXZ + y * innerStrideY; + int v = int.MaxValue; + for (int i = 3; i < 6; i++) { + int connection = hexagonNeighbourIndices[i]; + if (!adjacencyMapper.HasConnection(outerIndex, connection, nodeConnections)) v = -1; + else v = math.min(v, erosionDistances[adjacencyMapper.GetNeighbourIndex(innerIndexXZ, innerIndex, connection, nodeConnections, neighbourOffsets, innerStrideY)]); + } + + erosionDistances[innerIndex] = math.min(erosionDistances[innerIndex], v + 1); + } + } + } + } else { + /* Index offset to get neighbour nodes. Added to a node's index to get a neighbour node index. + * + * \code + * Z + * | + * | + * + * 6 2 5 + * \ | / + * -- 3 - X - 1 ----- X + * / | \ + * 7 0 4 + * + * | + * | + * \endcode + */ + const int DirectionDown = 0; + const int DirectionRight = 1; + const int DirectionUp = 2; + const int DirectionLeft = 3; + + // Use the grassfire transform: https://en.wikipedia.org/wiki/Grassfire_transform + for (int z = 1; z < size.z - 1; z++) { + for (int x = 1; x < size.x - 1; x++) { + for (int y = 0; y < layers; y++) { + int outerIndex = z * outerStrideZ + x * outerStrideX + y * outerStrideY + outerOffset; + var innerIndexXZ = z * innerStrideZ + x * innerStrideX; + int innerIndex = innerIndexXZ + y * innerStrideY; + var v1 = -1; + if (adjacencyMapper.HasConnection(outerIndex, DirectionDown, nodeConnections)) v1 = erosionDistances[adjacencyMapper.GetNeighbourIndex(innerIndexXZ, innerIndex, DirectionDown, nodeConnections, neighbourOffsets, innerStrideY)]; + var v2 = -1; + if (adjacencyMapper.HasConnection(outerIndex, DirectionLeft, nodeConnections)) v2 = erosionDistances[adjacencyMapper.GetNeighbourIndex(innerIndexXZ, innerIndex, DirectionLeft, nodeConnections, neighbourOffsets, innerStrideY)]; + + erosionDistances[innerIndex] = math.min(v1, v2) + 1; + } + } + } + + for (int z = size.z - 2; z > 0; z--) { + for (int x = size.x - 2; x > 0; x--) { + for (int y = 0; y < layers; y++) { + int outerIndex = z * outerStrideZ + x * outerStrideX + y * outerStrideY + outerOffset; + var innerIndexXZ = z * innerStrideZ + x * innerStrideX; + int innerIndex = innerIndexXZ + y * innerStrideY; + var v1 = -1; + if (adjacencyMapper.HasConnection(outerIndex, DirectionUp, nodeConnections)) v1 = erosionDistances[adjacencyMapper.GetNeighbourIndex(innerIndexXZ, innerIndex, DirectionUp, nodeConnections, neighbourOffsets, innerStrideY)]; + var v2 = -1; + if (adjacencyMapper.HasConnection(outerIndex, DirectionRight, nodeConnections)) v2 = erosionDistances[adjacencyMapper.GetNeighbourIndex(innerIndexXZ, innerIndex, DirectionRight, nodeConnections, neighbourOffsets, innerStrideY)]; + + erosionDistances[innerIndex] = math.min(erosionDistances[outerIndex], math.min(v1, v2) + 1); + } + } + } + } + + var relativeWriteMask = writeMask.Offset(-bounds.min); + + // Erosion tags are allowed to overwrite the ones the user specifies, as well as the ones that are already reserved for erosion. + for (int i = erosionStartTag; i < erosionStartTag + erosion; i++) erosionTagsPrecedenceMask |= 1 << i; + + for (int y = relativeWriteMask.min.y; y < relativeWriteMask.max.y; y++) { + for (int z = relativeWriteMask.min.z; z < relativeWriteMask.max.z; z++) { + for (int x = relativeWriteMask.min.x; x < relativeWriteMask.max.x; x++) { + int outerIndex = x * outerStrideX + y * outerStrideY + z * outerStrideZ + outerOffset; + int innerIndex = x * innerStrideX + y * innerStrideY + z * innerStrideZ; + if (erosionUsesTags) { + var prevTag = nodeTags[outerIndex]; + outNodeWalkable[outerIndex] = nodeWalkable[outerIndex]; + + if (erosionDistances[innerIndex] < erosion) { + if (((erosionTagsPrecedenceMask >> prevTag) & 0x1) != 0) { + nodeTags[outerIndex] = nodeWalkable[outerIndex] ? math.min(GraphNode.MaxTagIndex, erosionDistances[innerIndex] + erosionStartTag) : 0; + } + } else if (prevTag >= erosionStartTag && prevTag < erosionStartTag + erosion) { + // If the node already had a tag that was reserved for erosion, but it shouldn't have that tag, then we remove it. + nodeTags[outerIndex] = 0; + } + } else { + outNodeWalkable[outerIndex] = nodeWalkable[outerIndex] & (erosionDistances[innerIndex] >= erosion); + } + } + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobErosion.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobErosion.cs.meta new file mode 100644 index 0000000..09484b2 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobErosion.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 12ba2e44c6c183644a4ad6186f4ab21e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobFilterDiagonalConnections.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobFilterDiagonalConnections.cs new file mode 100644 index 0000000..6b40a5f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobFilterDiagonalConnections.cs @@ -0,0 +1,128 @@ +using Unity.Burst; +using Unity.Collections; +using Unity.Mathematics; +using Pathfinding.Jobs; +using Pathfinding.Util; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Filters out diagonal connections that are not allowed in layered grid graphs. + /// + /// This is a IJobParallelForBatched job which is parallelelized over the z coordinate of the <see cref="slice"/>. + /// + /// The <see cref="JobCalculateGridConnections"/> job will run first, and calculate the connections for all nodes. + /// However, for layered grid graphs, the connections for diagonal nodes may be incorrect, and this + /// post-processing pass is needed to validate the diagonal connections. + /// </summary> + [BurstCompile] + public struct JobFilterDiagonalConnections : IJobParallelForBatched { + public Slice3D slice; + public NumNeighbours neighbours; + public bool cutCorners; + + /// <summary>All bitpacked node connections</summary> + public UnsafeSpan<ulong> nodeConnections; + + public bool allowBoundsChecks => false; + + public void Execute (int start, int count) { + slice.AssertMatchesOuter(nodeConnections); + + // 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 + + int3 size = slice.outerSize; + NativeArray<int> neighbourOffsets = new NativeArray<int>(8, Allocator.Temp, NativeArrayOptions.UninitializedMemory); + + for (int i = 0; i < 8; i++) neighbourOffsets[i] = GridGraph.neighbourZOffsets[i] * size.x + GridGraph.neighbourXOffsets[i]; + + ulong hexagonConnectionMask = 0; + for (int i = 0; i < GridGraph.hexagonNeighbourIndices.Length; i++) hexagonConnectionMask |= (ulong)LevelGridNode.ConnectionMask << (LevelGridNode.ConnectionStride*GridGraph.hexagonNeighbourIndices[i]); + + int adjacencyThreshold = cutCorners ? 1 : 2; + int layerStride = size.x * size.z; + start += slice.slice.min.z; + for (int y = slice.slice.min.y; y < slice.slice.max.y; y++) { + // The loop is parallelized over z coordinates + for (int z = start; z < start + count; z++) { + for (int x = slice.slice.min.x; x < slice.slice.max.x; x++) { + int nodeIndexXZ = z * size.x + x; + int nodeIndex = nodeIndexXZ + y * layerStride; + + switch (neighbours) { + case NumNeighbours.Four: + // Mask out all the diagonal connections + nodeConnections[nodeIndex] = nodeConnections[nodeIndex] | LevelGridNode.DiagonalConnectionsMask; + break; + case NumNeighbours.Eight: + var conns = nodeConnections[nodeIndex]; + + // Skip node if no connections are enabled already + if (conns == LevelGridNode.AllConnectionsMask) continue; + + // When cutCorners is enabled then the diagonal connection is allowed + // if at least one axis aligned connection is adjacent to this diagonal. + // Otherwise both axis aligned connections must be present. + // + // X ----- axis2 + // | \ + // | \ + // | \ + // axis1 diagonal + // + // Z + // | + // | + // + // 6 2 5 + // \ | / + // -- 3 - X - 1 ----- X + // / | \ + // 7 0 4 + // + // | + // | + // + for (int dir = 0; dir < 4; dir++) { + int adjacent = 0; + var axis1 = (conns >> dir*LevelGridNode.ConnectionStride) & LevelGridNode.ConnectionMask; + var axis2 = (conns >> ((dir+1) % 4)*LevelGridNode.ConnectionStride) & LevelGridNode.ConnectionMask; + var diagonal = (conns >> (dir+4)*LevelGridNode.ConnectionStride) & LevelGridNode.ConnectionMask; + + // Check if the diagonal connection is present at all. + // The JobCalculateGridConnections calculated this. + if (diagonal == LevelGridNode.NoConnection) continue; + + if (axis1 != LevelGridNode.NoConnection) { + // We also check that the neighbour node is also connected to the diagonal node + var neighbourDir = (dir + 1) % 4; + var neighbourIndex = nodeIndexXZ + neighbourOffsets[dir] + (int)axis1 * layerStride; + if (((nodeConnections[neighbourIndex] >> neighbourDir*LevelGridNode.ConnectionStride) & LevelGridNode.ConnectionMask) == diagonal) { + adjacent++; + } + } + if (axis2 != LevelGridNode.NoConnection) { + var neighbourDir = dir; + var neighbourIndex = nodeIndexXZ + neighbourOffsets[(dir+1)%4] + (int)axis2 * layerStride; + if (((nodeConnections[neighbourIndex] >> neighbourDir*LevelGridNode.ConnectionStride) & LevelGridNode.ConnectionMask) == diagonal) { + adjacent++; + } + } + + if (adjacent < adjacencyThreshold) conns |= (ulong)LevelGridNode.NoConnection << (dir + 4)*LevelGridNode.ConnectionStride; + } + nodeConnections[nodeIndex] = conns; + break; + case NumNeighbours.Six: + // Hexagon layout + // Note that for layered nodes NoConnection is all bits set (see LevelGridNode.NoConnection) + // So in contrast to the non-layered grid graph we do a bitwise OR here + nodeConnections[nodeIndex] = (nodeConnections[nodeIndex] | ~hexagonConnectionMask) & LevelGridNode.AllConnectionsMask; + break; + } + } + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobFilterDiagonalConnections.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobFilterDiagonalConnections.cs.meta new file mode 100644 index 0000000..a3d3d9b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobFilterDiagonalConnections.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: e737565f0f6416f4ab1470d4f194ed25 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobMergeRaycastCollisionHits.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobMergeRaycastCollisionHits.cs new file mode 100644 index 0000000..61707dd --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobMergeRaycastCollisionHits.cs @@ -0,0 +1,31 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Calculates if either of the two input hits actually hit something. + /// + /// result[i] = true if hit1[i] or hit2[i] is a valid hit. + /// + /// A valid hit will always have a non-zero normal. + /// </summary> + [BurstCompile] + public struct JobMergeRaycastCollisionHits : IJob { + [ReadOnly] + public NativeArray<RaycastHit> hit1; + + [ReadOnly] + public NativeArray<RaycastHit> hit2; + + [WriteOnly] + public NativeArray<bool> result; + + public void Execute () { + for (int i = 0; i < hit1.Length; i++) { + result[i] = hit1[i].normal == Vector3.zero && hit2[i].normal == Vector3.zero; + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobMergeRaycastCollisionHits.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobMergeRaycastCollisionHits.cs.meta new file mode 100644 index 0000000..73957ca --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobMergeRaycastCollisionHits.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: be7b697c92431a74a8db8ae716c3f0b6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeGridLayout.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeGridLayout.cs new file mode 100644 index 0000000..e9bdaf3 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeGridLayout.cs @@ -0,0 +1,36 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using Pathfinding.Jobs; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Calculates the default node positions for a grid graph. + /// + /// The node positions will lie on the base plane of the grid graph. + /// + /// See: <see cref="GridGraph.CalculateTransform"/> + /// </summary> + [BurstCompile(FloatMode = FloatMode.Fast)] + public struct JobNodeGridLayout : IJob, GridIterationUtilities.ICellAction { + public Matrix4x4 graphToWorld; + public IntBounds bounds; + + [WriteOnly] + public NativeArray<Vector3> nodePositions; + + public static Vector3 NodePosition (Matrix4x4 graphToWorld, int x, int z) { + return graphToWorld.MultiplyPoint3x4(new Vector3(x + 0.5f, 0, z + 0.5f)); + } + + public void Execute () { + GridIterationUtilities.ForEachCellIn3DArray(bounds.size, ref this); + } + + public void Execute (uint innerIndex, int x, int y, int z) { + nodePositions[(int)innerIndex] = NodePosition(graphToWorld, x + bounds.min.x, z + bounds.min.z); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeGridLayout.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeGridLayout.cs.meta new file mode 100644 index 0000000..4a82431 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeGridLayout.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7dba760f9d6d81e458c5122c0d28b2df +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeWalkability.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeWalkability.cs new file mode 100644 index 0000000..7629df7 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeWalkability.cs @@ -0,0 +1,73 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary>Calculates for each grid node if it should be walkable or not</summary> + [BurstCompile(FloatMode = FloatMode.Fast)] + public struct JobNodeWalkability : IJob { + /// <summary> + /// If true, use the normal of the raycast hit to check if the ground is flat enough to stand on. + /// + /// Any nodes with a steeper slope than <see cref="maxSlope"/> will be made unwalkable. + /// </summary> + public bool useRaycastNormal; + /// <summary>Max slope in degrees</summary> + public float maxSlope; + /// <summary>Normalized up direction of the graph</summary> + public Vector3 up; + /// <summary>If true, nodes will be made unwalkable if no ground was found under them</summary> + public bool unwalkableWhenNoGround; + /// <summary>For layered grid graphs, if there's a node above another node closer than this distance, the lower node will be made unwalkable</summary> + public float characterHeight; + /// <summary>Number of nodes in each layer</summary> + public int layerStride; + + [ReadOnly] + public NativeArray<float3> nodePositions; + + public NativeArray<float4> nodeNormals; + + [WriteOnly] + public NativeArray<bool> nodeWalkable; + + public void Execute () { + // Cosinus of the max slope + float cosMaxSlopeAngle = math.cos(math.radians(maxSlope)); + float4 upNative = new float4(up.x, up.y, up.z, 0); + float3 upNative3 = upNative.xyz; + + for (int i = 0; i < nodeNormals.Length; i++) { + // walkable will be set to false if no ground was found (unless that setting has been disabled) + // The normal will only be non-zero if something was hit. + bool didHit = math.any(nodeNormals[i]); + var walkable = didHit; + if (!didHit && !unwalkableWhenNoGround && i < layerStride) { + walkable = true; + // If there was no hit, but we still want to make the node walkable, then we set the normal to the up direction + nodeNormals[i] = upNative; + } + + // Check if the node is on a slope steeper than permitted + if (walkable && useRaycastNormal && didHit) { + // Take the dot product to find out the cosine of the angle it has (faster than Vector3.Angle) + float angle = math.dot(nodeNormals[i], upNative); + + // Check if the ground is flat enough to stand on + if (angle < cosMaxSlopeAngle) { + walkable = false; + } + } + + // Check if there is a node above this one (layered grid graph only) + if (walkable && i + layerStride < nodeNormals.Length && math.any(nodeNormals[i + layerStride])) { + walkable = math.dot(upNative3, nodePositions[i + layerStride] - nodePositions[i]) >= characterHeight; + } + + nodeWalkable[i] = walkable; + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeWalkability.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeWalkability.cs.meta new file mode 100644 index 0000000..2a3d418 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobNodeWalkability.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a4ace8275be3d2f4b990df3672acb302 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareCapsuleCommands.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareCapsuleCommands.cs new file mode 100644 index 0000000..4644787 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareCapsuleCommands.cs @@ -0,0 +1,45 @@ +#if UNITY_2022_2_OR_NEWER +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Collections.LowLevel.Unsafe; +using Pathfinding.Util; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Prepares a set of capsule commands for collision checking in a grid graph. + /// + /// See: <see cref="GraphCollision"/> + /// </summary> + [BurstCompile] + public struct JobPrepareCapsuleCommands : IJob { + public Vector3 direction; + public Vector3 originOffset; + public float radius; + public LayerMask mask; + public PhysicsScene physicsScene; + + [ReadOnly] + public NativeArray<Vector3> origins; + + [WriteOnly] + public NativeArray<OverlapCapsuleCommand> commands; + + public void Execute () { + var commandSpan = commands.AsUnsafeSpan(); + // It turns out it is faster to set all commands to the same value using MemCpyReplicate and then patch point0 and point1, + // rather than setting each command individually (about 30% faster even). + // MemCpy is very fast. + var queryParameters = new QueryParameters(mask, false, QueryTriggerInteraction.Ignore, false); + commandSpan.Fill(new OverlapCapsuleCommand(physicsScene, Vector3.zero, Vector3.zero, radius, queryParameters)); + + for (int i = 0; i < commandSpan.Length; i++) { + var origin = origins[i] + originOffset; + commandSpan[i].point0 = origin; + commandSpan[i].point1 = origin + direction; + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareCapsuleCommands.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareCapsuleCommands.cs.meta new file mode 100644 index 0000000..414cc20 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareCapsuleCommands.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af594897c5b7f094585ba27f8be57d45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareGridRaycast.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareGridRaycast.cs new file mode 100644 index 0000000..e05347d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareGridRaycast.cs @@ -0,0 +1,61 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Collections.LowLevel.Unsafe; +using Pathfinding.Util; +using UnityEngine.Assertions; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Prepares a set of raycast commands for a grid graph. + /// + /// Each ray will start at <see cref="raycastOffset"/> from the node's position. The end point of the raycast will be the start point + <see cref="raycastDirection"/>. + /// + /// See: <see cref="GraphCollision"/> + /// </summary> + [BurstCompile] + public struct JobPrepareGridRaycast : IJob { + public Matrix4x4 graphToWorld; + public IntBounds bounds; + public Vector3 raycastOffset; + public Vector3 raycastDirection; + public LayerMask raycastMask; + public PhysicsScene physicsScene; + + [WriteOnly] + public NativeArray<RaycastCommand> raycastCommands; + + public void Execute () { + float raycastLength = raycastDirection.magnitude; + var size = bounds.size; + + // In particular Unity 2022.2 seems to assert that RaycastCommands use normalized directions + var direction = raycastDirection.normalized; + var commands = raycastCommands.AsUnsafeSpan(); + + Assert.AreEqual(commands.Length, size.x * size.z); + +#if UNITY_2022_2_OR_NEWER + var queryParameters = new QueryParameters(raycastMask, false, QueryTriggerInteraction.Ignore, false); + // This is about 30% faster than setting each command individually. MemCpy is fast. + commands.Fill(new RaycastCommand(physicsScene, Vector3.zero, direction, queryParameters, raycastLength)); +#else + const int RaycastMaxHits = 1; +#endif + + for (int z = 0; z < size.z; z++) { + int zw = z * size.x; + for (int x = 0; x < size.x; x++) { + int idx = zw + x; + var pos = JobNodeGridLayout.NodePosition(graphToWorld, x + bounds.min.x, z + bounds.min.z); +#if UNITY_2022_2_OR_NEWER + commands[idx].from = pos + raycastOffset; +#else + commands[idx] = new RaycastCommand(pos + raycastOffset, direction, raycastLength, raycastMask, RaycastMaxHits); +#endif + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareGridRaycast.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareGridRaycast.cs.meta new file mode 100644 index 0000000..3800b46 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareGridRaycast.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f081267224f64d44a99b9962d3d300a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareRaycasts.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareRaycasts.cs new file mode 100644 index 0000000..8a7fc7a --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareRaycasts.cs @@ -0,0 +1,51 @@ +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Collections.LowLevel.Unsafe; +using Pathfinding.Util; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Prepares a set of raycast commands for a grid graph. + /// + /// This is very similar to <see cref="JobPrepareGridRaycast"/> but it uses an array of origin points instead of a grid pattern. + /// + /// See: <see cref="GraphCollision"/> + /// </summary> + [BurstCompile] + public struct JobPrepareRaycasts : IJob { + public Vector3 direction; + public Vector3 originOffset; + public float distance; + public LayerMask mask; + public PhysicsScene physicsScene; + + [ReadOnly] + public NativeArray<Vector3> origins; + + [WriteOnly] + public NativeArray<RaycastCommand> raycastCommands; + + public void Execute () { + // In particular Unity 2022.2 seems to assert that RaycastCommands use normalized directions + var direction = this.direction.normalized; + var commands = raycastCommands.AsUnsafeSpan(); + +#if UNITY_2022_2_OR_NEWER + var queryParameters = new QueryParameters(mask, false, QueryTriggerInteraction.Ignore, false); + var defaultCommand = new RaycastCommand(physicsScene, Vector3.zero, direction, queryParameters, distance); + // This is about 30% faster than setting each command individually. MemCpy is fast. + commands.Fill(defaultCommand); +#endif + + for (int i = 0; i < raycastCommands.Length; i++) { +#if UNITY_2022_2_OR_NEWER + commands[i].from = origins[i] + originOffset; +#else + raycastCommands[i] = new RaycastCommand(origins[i] + originOffset, direction, distance, mask, 1); +#endif + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareRaycasts.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareRaycasts.cs.meta new file mode 100644 index 0000000..c998e6b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareRaycasts.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 41ad6b54690bf4949a2974c3ca5d2503 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareSphereCommands.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareSphereCommands.cs new file mode 100644 index 0000000..50562a2 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareSphereCommands.cs @@ -0,0 +1,42 @@ +#if UNITY_2022_2_OR_NEWER +using UnityEngine; +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using Unity.Collections.LowLevel.Unsafe; +using Pathfinding.Util; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Prepares a set of sphere commands for collision checking in a grid graph. + /// + /// See: <see cref="GraphCollision"/> + /// </summary> + [BurstCompile] + public struct JobPrepareSphereCommands : IJob { + public Vector3 originOffset; + public float radius; + public LayerMask mask; + public PhysicsScene physicsScene; + + [ReadOnly] + public NativeArray<Vector3> origins; + + [WriteOnly] + public NativeArray<OverlapSphereCommand> commands; + + public void Execute () { + var commandSpan = commands.AsUnsafeSpan(); + // It turns out it is faster to set all commands to the same value using MemCpyReplicate and then patch point, + // rather than setting each command individually. + var queryParameters = new QueryParameters(mask, false, QueryTriggerInteraction.Ignore, false); + commandSpan.Fill(new OverlapSphereCommand(physicsScene, Vector3.zero, radius, queryParameters)); + + for (int i = 0; i < commandSpan.Length; i++) { + var origin = origins[i] + originOffset; + commandSpan[i].point = origin; + } + } + } +} +#endif diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareSphereCommands.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareSphereCommands.cs.meta new file mode 100644 index 0000000..f5fbad1 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobPrepareSphereCommands.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: db154518b4b8b044d946bb6a9770bdbf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobRaycastAll.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobRaycastAll.cs new file mode 100644 index 0000000..fd34a1d --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobRaycastAll.cs @@ -0,0 +1,121 @@ +using Unity.Burst; +using Unity.Collections; +using Unity.Jobs; +using UnityEngine; +using Pathfinding.Jobs; +using Unity.Mathematics; + +namespace Pathfinding.Jobs { + public struct JobRaycastAll { + int maxHits; + public readonly float minStep; + + NativeArray<RaycastHit> results; + NativeArray<RaycastHit> semiResults; + NativeArray<RaycastCommand> commands; + public PhysicsScene physicsScene; + + [BurstCompile] + private struct JobCreateCommands : IJobParallelFor { + public NativeArray<RaycastCommand> commands; + [ReadOnly] + public NativeArray<RaycastHit> raycastHits; + + public float minStep; + public PhysicsScene physicsScene; + + public void Execute (int index) { + var rayHit = raycastHits[index]; + + if (rayHit.normal != default(Vector3)) { + var previousCommand = commands[index]; + // Little hack to bypass same collider hit in specific cases + var point = rayHit.point + previousCommand.direction.normalized * minStep; + var distance = previousCommand.distance - (point - previousCommand.from).magnitude; +#if UNITY_2022_2_OR_NEWER + // TODO: In 2022.2 with the 'hit multiple faces' option, this whole class might be redundant. + var queryParameters = new QueryParameters(previousCommand.queryParameters.layerMask, false, QueryTriggerInteraction.Ignore, false); + commands[index] = new RaycastCommand(physicsScene, point, previousCommand.direction, queryParameters, distance); +#else + commands[index] = new RaycastCommand(point, previousCommand.direction, distance, previousCommand.layerMask, 1); +#endif + } else { +#if UNITY_2022_2_OR_NEWER + // Note: Using a default RaycastCommand may cause Unity to crash. + // This seems to be primarily because it assumes a non-zero direction. + commands[index] = new RaycastCommand(physicsScene, Vector3.zero, Vector3.up, new QueryParameters(0, false, QueryTriggerInteraction.Ignore, false), 1); +#else + commands[index] = new RaycastCommand(Vector3.zero, Vector3.up, 1, 0, 1); +#endif + } + } + } + + [BurstCompile] + private struct JobCombineResults : IJob { + public int maxHits; + [ReadOnly] + public NativeArray<RaycastHit> semiResults; + public NativeArray<RaycastHit> results; + + public void Execute () { + int layerStride = semiResults.Length / maxHits; + + for (int i = 0; i < layerStride; i++) { + int layerOffset = 0; + + for (int j = maxHits - 1; j >= 0; j--) { + if (math.any(semiResults[i + j*layerStride].normal)) { + results[i + layerOffset] = semiResults[i + j*layerStride]; + layerOffset += layerStride; + } + } + } + } + } + + /// <summary>Jobified version of Physics.RaycastNonAlloc.</summary> + /// <param name="commands">Array of commands to perform.</param> + /// <param name="results">Array to store results in.</param> + /// <param name="physicsScene">PhysicsScene to use for the raycasts. Only used in Unity 2022.2 or later.</param> + /// <param name="maxHits">Max hits count per command.</param> + /// <param name="allocator">Allocator to use for the results array.</param> + /// <param name="dependencyTracker">Tracker to use for dependencies.</param> + /// <param name="minStep">Minimal distance each Raycast should progress.</param> + public JobRaycastAll(NativeArray<RaycastCommand> commands, NativeArray<RaycastHit> results, PhysicsScene physicsScene, int maxHits, Allocator allocator, JobDependencyTracker dependencyTracker, float minStep = 0.0001f) { + if (maxHits <= 0) throw new System.ArgumentException("maxHits should be greater than zero"); + if (results.Length < commands.Length * maxHits) throw new System.ArgumentException("Results array length does not match maxHits count"); + if (minStep < 0f) throw new System.ArgumentException("minStep should be more or equal to zero"); + + this.results = results; + this.maxHits = maxHits; + this.minStep = minStep; + this.commands = commands; + this.physicsScene = physicsScene; + + semiResults = dependencyTracker.NewNativeArray<RaycastHit>(maxHits * commands.Length, allocator); + } + + public JobHandle Schedule (JobHandle dependency) { + for (int i = 0; i < maxHits; i++) { + var semiResultsPart = semiResults.GetSubArray(i*commands.Length, commands.Length); + dependency = RaycastCommand.ScheduleBatch(commands, semiResultsPart, 128, dependency); + if (i < maxHits - 1) { + var filter = new JobCreateCommands { + commands = commands, + raycastHits = semiResultsPart, + minStep = minStep, + physicsScene = physicsScene, + }; + dependency = filter.Schedule(commands.Length, 256, dependency); + } + } + + return new JobCombineResults { + semiResults = semiResults, + maxHits = maxHits, + results = results + }.Schedule(dependency); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobRaycastAll.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobRaycastAll.cs.meta new file mode 100644 index 0000000..aed1b42 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobRaycastAll.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f8eb8ba655e2d2dc88958d073ff52f3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobReadNodeData.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobReadNodeData.cs new file mode 100644 index 0000000..a94b652 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobReadNodeData.cs @@ -0,0 +1,93 @@ +using UnityEngine; +using Unity.Collections; +using Unity.Mathematics; +using Pathfinding.Jobs; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Reads node data from managed <see cref="GridNodeBase"/> objects into unmanaged arrays. + /// + /// This is done so that burst jobs can later access this data directly. + /// + /// Later, data will be written back to the managed objects using the <see cref="JobWriteNodeData"/> job. + /// </summary> + public struct JobReadNodeData : IJobParallelForBatched { + public System.Runtime.InteropServices.GCHandle nodesHandle; + public uint graphIndex; + + public Slice3D slice; + + [WriteOnly] + public NativeArray<Vector3> nodePositions; + + [WriteOnly] + public NativeArray<uint> nodePenalties; + + [WriteOnly] + public NativeArray<int> nodeTags; + + [WriteOnly] + public NativeArray<ulong> nodeConnections; + + [WriteOnly] + public NativeArray<bool> nodeWalkableWithErosion; + + [WriteOnly] + public NativeArray<bool> nodeWalkable; + + public bool allowBoundsChecks => false; + + struct Reader : GridIterationUtilities.ISliceAction { + public GridNodeBase[] nodes; + public NativeArray<Vector3> nodePositions; + public NativeArray<uint> nodePenalties; + public NativeArray<int> nodeTags; + public NativeArray<ulong> nodeConnections; + public NativeArray<bool> nodeWalkableWithErosion; + public NativeArray<bool> nodeWalkable; + + [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)] + public void Execute (uint outerIdx, uint innerIdx) { + var dataIdx = (int)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[dataIdx] = (Vector3)node.position; + nodePenalties[dataIdx] = node.Penalty; + nodeTags[dataIdx] = (int)node.Tag; + nodeConnections[dataIdx] = node is GridNode gn ? (ulong)gn.GetAllConnectionInternal() : (node as LevelGridNode).gridConnections; + nodeWalkableWithErosion[dataIdx] = node.Walkable; + nodeWalkable[dataIdx] = node.WalkableErosion; + 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[dataIdx] = Vector3.zero; + nodePenalties[dataIdx] = 0; + nodeTags[dataIdx] = 0; + nodeConnections[dataIdx] = 0; + nodeWalkableWithErosion[dataIdx] = false; + nodeWalkable[dataIdx] = false; + } + } + + public void Execute (int startIndex, int count) { + var reader = new Reader { + // This is a managed type, we need to trick Unity to allow this inside of a job + nodes = (GridNodeBase[])nodesHandle.Target, + nodePositions = nodePositions, + nodePenalties = nodePenalties, + nodeTags = nodeTags, + nodeConnections = nodeConnections, + nodeWalkableWithErosion = nodeWalkableWithErosion, + nodeWalkable = nodeWalkable + }; + GridIterationUtilities.ForEachCellIn3DSlice(slice, ref reader); + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobReadNodeData.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobReadNodeData.cs.meta new file mode 100644 index 0000000..2092692 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobReadNodeData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 203c976aeb6f3d84caeab084738a53c8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobWriteNodeData.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobWriteNodeData.cs new file mode 100644 index 0000000..96a58a8 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobWriteNodeData.cs @@ -0,0 +1,91 @@ +using UnityEngine; +using Unity.Collections; +using Unity.Mathematics; +using Pathfinding.Jobs; +using UnityEngine.Assertions; + +namespace Pathfinding.Graphs.Grid.Jobs { + /// <summary> + /// Writes node data from unmanaged arrays into managed <see cref="GridNodeBase"/> objects. + /// + /// This is done after burst jobs have been working on graph data, as they cannot access the managed objects directly. + /// + /// Earlier, data will have been either calculated from scratch, or read from the managed objects using the <see cref="JobReadNodeData"/> job. + /// </summary> + public struct JobWriteNodeData : IJobParallelForBatched { + public System.Runtime.InteropServices.GCHandle nodesHandle; + public uint graphIndex; + + /// <summary>(width, depth) of the array that the <see cref="nodesHandle"/> refers to</summary> + public int3 nodeArrayBounds; + public IntBounds dataBounds; + public IntBounds writeMask; + + [ReadOnly] + public NativeArray<Vector3> nodePositions; + + [ReadOnly] + public NativeArray<uint> nodePenalties; + + [ReadOnly] + public NativeArray<int> nodeTags; + + [ReadOnly] + public NativeArray<ulong> nodeConnections; + + [ReadOnly] + public NativeArray<bool> nodeWalkableWithErosion; + + [ReadOnly] + public NativeArray<bool> nodeWalkable; + + public bool allowBoundsChecks => false; + + public void Execute (int startIndex, int count) { + // This is a managed type, we need to trick Unity to allow this inside of a job + var nodes = (GridNodeBase[])nodesHandle.Target; + + var relativeMask = writeMask.Offset(-dataBounds.min); + + // Determinstically convert the indices to rows. It is much easier to process a number of whole rows. + var writeSize = writeMask.size; + var zstart = startIndex / (writeSize.x*writeSize.y); + var zend = (startIndex+count) / (writeSize.x*writeSize.y); + + Assert.IsTrue(zstart >= 0 && zstart <= writeSize.z); + Assert.IsTrue(zend >= 0 && zend <= writeSize.z); + relativeMask.min.z = writeMask.min.z + zstart - dataBounds.min.z; + relativeMask.max.z = writeMask.min.z + zend - dataBounds.min.z; + + var dataSize = dataBounds.size; + for (int y = relativeMask.min.y; y < relativeMask.max.y; y++) { + for (int z = relativeMask.min.z; z < relativeMask.max.z; z++) { + var rowOffset1 = (y*dataSize.z + z)*dataSize.x; + var rowOffset2 = (z + dataBounds.min.z)*nodeArrayBounds.x + dataBounds.min.x; + var rowOffset3 = (y + dataBounds.min.y)*nodeArrayBounds.z*nodeArrayBounds.x + rowOffset2; + for (int x = relativeMask.min.x; x < relativeMask.max.x; x++) { + int dataIndex = rowOffset1 + x; + int nodeIndex = rowOffset3 + x; + var node = nodes[nodeIndex]; + if (node != null) { + node.GraphIndex = graphIndex; + node.NodeInGridIndex = rowOffset2 + x; + // TODO: Use UnsafeSpan + node.position = (Int3)nodePositions[dataIndex]; + node.Penalty = nodePenalties[dataIndex]; + node.Tag = (uint)nodeTags[dataIndex]; + if (node is GridNode gridNode) { + gridNode.SetAllConnectionInternal((int)nodeConnections[dataIndex]); + } else if (node is LevelGridNode levelGridNode) { + levelGridNode.LayerCoordinateInGrid = y + dataBounds.min.y; + levelGridNode.SetAllConnectionInternal(nodeConnections[dataIndex]); + } + node.Walkable = nodeWalkableWithErosion[dataIndex]; + node.WalkableErosion = nodeWalkable[dataIndex]; + } + } + } + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobWriteNodeData.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobWriteNodeData.cs.meta new file mode 100644 index 0000000..503743b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Graphs/Grid/Jobs/JobWriteNodeData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 86fcb7ce1d754024290197052acf39d5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: |