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