1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
|
#if MODULE_ENTITIES
using Unity.Entities;
using Unity.Mathematics;
using Unity.Profiling;
using Unity.Transforms;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using GCHandle = System.Runtime.InteropServices.GCHandle;
namespace Pathfinding.ECS {
using Pathfinding;
using Pathfinding.Util;
using Unity.Burst;
using Unity.Jobs;
/// <summary>
/// Repairs the path of agents.
///
/// This job will repair the agent's path based on the agent's current position and its destination.
/// It will also recalculate various statistics like how far the agent is from the destination,
/// and if it has reached the destination or not.
/// </summary>
public struct JobRepairPath : IJobChunk {
public struct Scheduler {
[ReadOnly]
public ComponentTypeHandle<LocalTransform> LocalTransformTypeHandleRO;
public ComponentTypeHandle<MovementState> MovementStateTypeHandleRW;
[ReadOnly]
public ComponentTypeHandle<AgentCylinderShape> AgentCylinderShapeTypeHandleRO;
// NativeDisableContainerSafetyRestriction seems to be necessary because otherwise we will get an error:
// "The ComponentTypeHandle<Pathfinding.ECS.ManagedState> ... can not be accessed. Nested native containers are illegal in jobs."
// However, Unity doesn't seem to check for this at all times. Currently, I can only replicate the error if DoTween Pro is also installed.
// I have no idea how this unrelated package influences unity to actually do the check.
// We know it is safe to access the managed state because we make sure to never access an entity from multiple threads at the same time.
[NativeDisableContainerSafetyRestriction]
public ComponentTypeHandle<ManagedState> ManagedStateTypeHandleRW;
[ReadOnly]
public ComponentTypeHandle<MovementSettings> MovementSettingsTypeHandleRO;
[ReadOnly]
public ComponentTypeHandle<DestinationPoint> DestinationPointTypeHandleRO;
[ReadOnly]
public ComponentTypeHandle<AgentMovementPlane> AgentMovementPlaneTypeHandleRO;
public ComponentTypeHandle<ReadyToTraverseOffMeshLink> ReadyToTraverseOffMeshLinkTypeHandleRW;
public GCHandle entityManagerHandle;
public bool onlyApplyPendingPaths;
public EntityQueryBuilder GetEntityQuery (Allocator allocator) {
return new EntityQueryBuilder(Allocator.Temp)
.WithAllRW<MovementState>()
.WithAllRW<ManagedState>()
.WithAllRW<LocalTransform>()
.WithAll<MovementSettings, DestinationPoint, AgentMovementPlane, AgentCylinderShape>()
// .WithAny<ReadyToTraverseOffMeshLink>() // TODO: Use WithPresent in newer versions
.WithAbsent<AgentOffMeshLinkTraversal>();
}
public Scheduler(ref SystemState systemState) {
entityManagerHandle = GCHandle.Alloc(systemState.EntityManager);
LocalTransformTypeHandleRO = systemState.GetComponentTypeHandle<LocalTransform>(true);
MovementStateTypeHandleRW = systemState.GetComponentTypeHandle<MovementState>(false);
AgentCylinderShapeTypeHandleRO = systemState.GetComponentTypeHandle<AgentCylinderShape>(true);
DestinationPointTypeHandleRO = systemState.GetComponentTypeHandle<DestinationPoint>(true);
AgentMovementPlaneTypeHandleRO = systemState.GetComponentTypeHandle<AgentMovementPlane>(true);
MovementSettingsTypeHandleRO = systemState.GetComponentTypeHandle<MovementSettings>(true);
ReadyToTraverseOffMeshLinkTypeHandleRW = systemState.GetComponentTypeHandle<ReadyToTraverseOffMeshLink>(false);
// Need to bypass the T : unmanaged check in systemState.GetComponentTypeHandle
ManagedStateTypeHandleRW = systemState.EntityManager.GetComponentTypeHandle<ManagedState>(false);
onlyApplyPendingPaths = false;
}
public void Dispose () {
entityManagerHandle.Free();
}
void Update (ref SystemState systemState) {
LocalTransformTypeHandleRO.Update(ref systemState);
MovementStateTypeHandleRW.Update(ref systemState);
AgentCylinderShapeTypeHandleRO.Update(ref systemState);
DestinationPointTypeHandleRO.Update(ref systemState);
ManagedStateTypeHandleRW.Update(ref systemState);
MovementSettingsTypeHandleRO.Update(ref systemState);
AgentMovementPlaneTypeHandleRO.Update(ref systemState);
ReadyToTraverseOffMeshLinkTypeHandleRW.Update(ref systemState);
}
public JobHandle ScheduleParallel (ref SystemState systemState, EntityQuery query, JobHandle dependency) {
Update(ref systemState);
return new JobRepairPath {
scheduler = this,
onlyApplyPendingPaths = onlyApplyPendingPaths
}.ScheduleParallel(query, dependency);
}
}
public Scheduler scheduler;
[NativeDisableContainerSafetyRestriction]
public NativeArray<int> indicesScratch;
[NativeDisableContainerSafetyRestriction]
public NativeList<float3> nextCornersScratch;
public bool onlyApplyPendingPaths;
public void Execute (in ArchetypeChunk chunk, int unfilteredChunkIndex, bool useEnabledMask, in Unity.Burst.Intrinsics.v128 chunkEnabledMask) {
if (!indicesScratch.IsCreated) {
nextCornersScratch = new NativeList<float3>(Allocator.Temp);
indicesScratch = new NativeArray<int>(8, Allocator.Temp);
}
unsafe {
var localTransforms = (LocalTransform*)chunk.GetNativeArray(ref scheduler.LocalTransformTypeHandleRO).GetUnsafeReadOnlyPtr();
var movementStates = (MovementState*)chunk.GetNativeArray(ref scheduler.MovementStateTypeHandleRW).GetUnsafePtr();
var agentCylinderShapes = (AgentCylinderShape*)chunk.GetNativeArray(ref scheduler.AgentCylinderShapeTypeHandleRO).GetUnsafeReadOnlyPtr();
var destinationPoints = (DestinationPoint*)chunk.GetNativeArray(ref scheduler.DestinationPointTypeHandleRO).GetUnsafeReadOnlyPtr();
var movementSettings = (MovementSettings*)chunk.GetNativeArray(ref scheduler.MovementSettingsTypeHandleRO).GetUnsafeReadOnlyPtr();
var agentMovementPlanes = (AgentMovementPlane*)chunk.GetNativeArray(ref scheduler.AgentMovementPlaneTypeHandleRO).GetUnsafeReadOnlyPtr();
var mask = chunk.GetEnabledMask(ref scheduler.ReadyToTraverseOffMeshLinkTypeHandleRW);
var managedStates = chunk.GetManagedComponentAccessor(ref scheduler.ManagedStateTypeHandleRW, (EntityManager)scheduler.entityManagerHandle.Target);
for (int i = 0; i < chunk.Count; i++) {
Execute(
ref localTransforms[i],
ref movementStates[i],
ref agentCylinderShapes[i],
ref agentMovementPlanes[i],
ref destinationPoints[i],
mask.GetEnabledRefRW<ReadyToTraverseOffMeshLink>(i),
managedStates[i],
in movementSettings[i],
nextCornersScratch,
ref indicesScratch,
Allocator.Temp,
onlyApplyPendingPaths
);
}
}
}
private static readonly ProfilerMarker MarkerRepair = new ProfilerMarker("Repair");
private static readonly ProfilerMarker MarkerGetNextCorners = new ProfilerMarker("GetNextCorners");
private static readonly ProfilerMarker MarkerUpdateReachedEndInfo = new ProfilerMarker("UpdateReachedEndInfo");
public static void Execute (ref LocalTransform transform, ref MovementState state, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, EnabledRefRW<ReadyToTraverseOffMeshLink> readyToTraverseOffMeshLink, ManagedState managedState, in MovementSettings settings, NativeList<float3> nextCornersScratch, ref NativeArray<int> indicesScratch, Allocator allocator, bool onlyApplyPendingPaths) {
// Only enabled by the PollPendingPathsSystem
if (onlyApplyPendingPaths) {
if (managedState.pendingPath != null && managedState.pendingPath.IsDone()) {
// The path has been calculated, apply it to the agent
// Immediately after this we must also repair the path to ensure that it is valid and that
// all properties like #MovementState.reachedEndOfPath are correct.
ManagedState.SetPath(managedState.pendingPath, managedState, in movementPlane, ref destination);
} else {
// The agent has no path that has just been calculated, skip it
return;
}
}
ref var pathTracer = ref managedState.pathTracer;
if (pathTracer.hasPath) {
MarkerRepair.Begin();
// Update the start and end points of the path based on the current position and destination.
// This will repair the path if necessary, ensuring that the agent has a valid, if not necessarily optimal, path.
// If it cannot be repaired well, the path will be marked as stale.
state.closestOnNavmesh = pathTracer.UpdateStart(transform.Position, PathTracer.RepairQuality.High, movementPlane.value, managedState.pathfindingSettings.traversalProvider, managedState.activePath);
state.endOfPath = pathTracer.UpdateEnd(destination.destination, PathTracer.RepairQuality.High, movementPlane.value, managedState.pathfindingSettings.traversalProvider, managedState.activePath);
MarkerRepair.End();
if (state.pathTracerVersion != pathTracer.version) {
nextCornersScratch.Clear();
MarkerGetNextCorners.Begin();
// Find the next corners of the path. The first corner is our current position,
// the second corner is the one we are moving towards and the third corner is the one after that.
//
// Using GetNextCorners with the default transformation instead of ConvertCornerIndicesToPathProjected
// is about 20% faster, but it does not work well at all on spherical worlds.
// In the future we might want to switch dynamically between these modes,
// but on the other hand, it is very nice to be able to use the exact same code path for everything.
// pathTracer.GetNextCorners(nextCornersScratch, 3, ref indicesScratch, allocator);
var numCorners = pathTracer.GetNextCornerIndices(ref indicesScratch, pathTracer.desiredCornersForGoodSimplification, allocator, out bool lastCorner, managedState.pathfindingSettings.traversalProvider, managedState.activePath);
pathTracer.ConvertCornerIndicesToPathProjected(indicesScratch, numCorners, lastCorner, nextCornersScratch, movementPlane.value.up);
MarkerGetNextCorners.End();
// We need to copy a few fields to a new struct, in order to be able to pass it to a burstified function
var pathTracerInfo = new JobRepairPathHelpers.PathTracerInfo {
endPointOfFirstPart = pathTracer.endPointOfFirstPart,
partCount = pathTracer.partCount,
isStale = pathTracer.isStale
};
var nextCorners = nextCornersScratch.AsUnsafeSpan();
JobRepairPathHelpers.UpdateReachedEndInfo(ref nextCorners, ref state, ref movementPlane, ref transform, ref shape, ref destination, settings.stopDistance, ref pathTracerInfo);
state.pathTracerVersion = pathTracer.version;
} else {
JobRepairPathHelpers.UpdateReachedOrientation(ref state, ref transform, ref movementPlane, ref destination);
}
if (pathTracer.startNode != null && !pathTracer.startNode.Destroyed && pathTracer.startNode.Walkable) {
state.graphIndex = pathTracer.startNode.GraphIndex;
state.hierarchicalNodeIndex = pathTracer.startNode.HierarchicalNodeIndex;
} else {
state.graphIndex = GraphNode.InvalidGraphIndex;
state.hierarchicalNodeIndex = -1;
}
} else {
state.SetPathIsEmpty(transform.Position);
}
if (readyToTraverseOffMeshLink.IsValid) readyToTraverseOffMeshLink.ValueRW = state.reachedEndOfPart && managedState.pathTracer.isNextPartValidLink;
}
}
[BurstCompile]
static class JobRepairPathHelpers {
public struct PathTracerInfo {
public float3 endPointOfFirstPart;
public int partCount;
// Bools are not blittable by burst so we must use a byte instead. Very ugly, but it is what it is.
byte isStaleBacking;
public bool isStale {
get => isStaleBacking != 0;
set => isStaleBacking = value ? (byte)1 : (byte)0;
}
}
/// <summary>Checks if the agent has reached its destination, or the end of the path</summary>
[BurstCompile]
public static void UpdateReachedEndInfo (ref UnsafeSpan<float3> nextCorners, ref MovementState state, ref AgentMovementPlane movementPlane, ref LocalTransform transform, ref AgentCylinderShape shape, ref DestinationPoint destination, float stopDistance, ref PathTracerInfo pathTracer) {
// TODO: Edit GetNextCorners so that it gets corners until at least stopDistance units from the agent
state.nextCorner = nextCorners.length > 1 ? nextCorners[1] : transform.Position;
state.remainingDistanceToEndOfPart = PathTracer.RemainingDistanceLowerBound(in nextCorners, in pathTracer.endPointOfFirstPart, in movementPlane.value);
// TODO: Check if end node is the globally closest node
movementPlane.value.ToPlane(pathTracer.endPointOfFirstPart - transform.Position, out var elevationDiffEndOfPart);
var validHeightRangeEndOfPart = elevationDiffEndOfPart< shape.height && elevationDiffEndOfPart > -0.5f*shape.height;
movementPlane.value.ToPlane(destination.destination - transform.Position, out var elevationDiffDestination);
var validHeightRangeDestination = elevationDiffDestination< shape.height && elevationDiffDestination > -0.5f*shape.height;
var endOfPathToDestination = math.length(movementPlane.value.ToPlane(destination.destination - state.endOfPath));
// If reachedEndOfPath is true we allow a slightly larger margin of error for reachedDestination.
// This is to ensure that if reachedEndOfPath becomes true, it is very likely that reachedDestination becomes
// true during the same frame.
const float FUZZ = 0.01f;
// When checking if the agent has reached the end of the current part (mostly used for off-mesh-links), we check against
// the agent's radius. This is because when there are many agents trying to reach the same off-mesh-link, the agents will
// crowd up and it may become hard to get to a point closer than the agent's radius.
state.reachedEndOfPart = !pathTracer.isStale && validHeightRangeEndOfPart && state.remainingDistanceToEndOfPart <= shape.radius*1.1f;
state.reachedEndOfPath = !pathTracer.isStale && validHeightRangeEndOfPart && pathTracer.partCount == 1 && state.remainingDistanceToEndOfPart <= stopDistance;
state.reachedDestination = !pathTracer.isStale && validHeightRangeDestination && pathTracer.partCount == 1 && state.remainingDistanceToEndOfPart + endOfPathToDestination <= stopDistance + (state.reachedEndOfPath ? FUZZ : 0);
state.traversingLastPart = pathTracer.partCount == 1;
UpdateReachedOrientation(ref state, ref transform, ref movementPlane, ref destination);
}
/// <summary>Checks if the agent is oriented towards the desired facing direction</summary>
public static void UpdateReachedOrientation (ref MovementState state, ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref DestinationPoint destination) {
state.reachedEndOfPathAndOrientation = state.reachedEndOfPath;
state.reachedDestinationAndOrientation = state.reachedDestination;
if (state.reachedEndOfPathAndOrientation || state.reachedDestinationAndOrientation) {
var reachedOrientation = ReachedDesiredOrientation(ref transform, ref movementPlane, ref destination);
state.reachedEndOfPathAndOrientation &= reachedOrientation;
state.reachedDestinationAndOrientation &= reachedOrientation;
}
}
static bool ReachedDesiredOrientation (ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref DestinationPoint destination) {
var facingDirection2D = math.normalizesafe(movementPlane.value.ToPlane(destination.facingDirection));
// If no desired facing direction is set, then we always treat the orientation as correct
if (math.all(facingDirection2D == 0)) return true;
var forward2D = math.normalizesafe(movementPlane.value.ToPlane(transform.Forward()));
const float ANGLE_THRESHOLD_COS = 0.9999f;
return math.dot(forward2D, facingDirection2D) >= ANGLE_THRESHOLD_COS;
}
}
}
#endif
|