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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
|
using UnityEngine;
namespace Pathfinding {
using Pathfinding.Util;
using Pathfinding.Drawing;
/// <summary>
/// Interface for handling off-mesh links.
///
/// If a component implements this interface, and is attached to the same GameObject as a NodeLink2 component,
/// then the OnTraverseOffMeshLink method will be called when an agent traverses the off-mesh link.
///
/// This only works with the <see cref="FollowerEntity"/> component.
///
/// Note: <see cref="FollowerEntity.onTraverseOffMeshLink"/> overrides this callback, if set.
///
/// The <see cref="Interactable"/> component implements this interface, and allows a small state-machine to run when the agent traverses the link.
///
/// See: <see cref="FollowerEntity.onTraverseOffMeshLink"/>
/// See: offmeshlinks (view in online documentation for working links)
/// </summary>
public interface IOffMeshLinkHandler {
/// <summary>
/// Name of the handler.
/// This is used to identify the handler in the inspector.
/// </summary>
public string name => null;
#if MODULE_ENTITIES
/// <summary>
/// Called when an agent starts traversing an off-mesh link.
///
/// This can be used to perform any setup that is necessary for the traversal.
///
/// Returns: An object which will be used to control the agent for the full duration of the link traversal.
///
/// For simple cases, you can often implement <see cref="IOffMeshLinkHandler"/> and <see cref="IOffMeshLinkStateMachine"/> in the same class, and then
/// just make this method return this.
/// For more complex cases, you may want to keep track of the agent's identity, and thus want to return a new object for each agent that traverses the link.
/// </summary>
/// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
/// This context is only valid for this method call. Do not store it and use it later.</param>
public IOffMeshLinkStateMachine GetOffMeshLinkStateMachine(ECS.AgentOffMeshLinkTraversalContext context);
#endif
}
public interface IOffMeshLinkStateMachine {
#if MODULE_ENTITIES
/// <summary>
/// Called when an agent traverses an off-mesh link.
/// This method should be a coroutine (i.e return an IEnumerable) which will be iterated over until it finishes, or the agent is destroyed.
/// The coroutine should yield null every frame until the agent has finished traversing the link.
///
/// When the coroutine completes, the agent will be assumed to have reached the end of the link and control
/// will be returned to the normal movement code.
///
/// The coroutine typically moves the agent to the end of the link over some time, and perform any other actions that are necessary.
/// For example, it could play an animation, or move the agent in a specific way.
/// </summary>
/// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
/// This context is only valid when this coroutine steps forward. Do not store it and use it elsewhere.</param>
System.Collections.IEnumerable OnTraverseOffMeshLink(ECS.AgentOffMeshLinkTraversalContext context) => ECS.JobStartOffMeshLinkTransition.DefaultOnTraverseOffMeshLink(context);
/// <summary>
/// Called when an agent finishes traversing an off-mesh link.
///
/// This can be used to perform any cleanup that is necessary after the traversal.
///
/// Either <see cref="OnFinishTraversingOffMeshLink"/> or <see cref="OnAbortTraversingOffMeshLink"/> will be called, but not both.
/// </summary>
/// <param name="context">Context for the traversal. Provides information about the link and the agent, as well as some helper methods for movement.
/// This context is only valid for this method call. Do not store it and use it later.</param>
void OnFinishTraversingOffMeshLink (ECS.AgentOffMeshLinkTraversalContext context) {}
#endif
/// <summary>
/// Called when an agent fails to finish traversing an off-mesh link.
///
/// This can be used to perform any cleanup that is necessary after the traversal.
///
/// An abort can happen if the agent was destroyed while it was traversing the link. It can also happen if the agent was teleported somewhere else while traversing the link.
///
/// Either <see cref="OnFinishTraversingOffMeshLink"/> or <see cref="OnAbortTraversingOffMeshLink"/> will be called, but not both.
///
/// Warning: When this is called, the agent may already be destroyed. The handler component itself could also be destroyed at this point.
/// </summary>
void OnAbortTraversingOffMeshLink () {}
}
/// <summary>
/// Connects two nodes using an off-mesh link.
/// In contrast to the <see cref="NodeLink"/> component, this link type will not connect the nodes directly
/// instead it will create two link nodes at the start and end position of this link and connect
/// through those nodes.
///
/// If the closest node to this object is called A and the closest node to the end transform is called
/// D, then it will create one link node at this object's position (call it B) and one link node at
/// the position of the end transform (call it C), it will then connect A to B, B to C and C to D.
///
/// This link type is possible to detect while following since it has these special link nodes in the middle.
/// The link corresponding to one of those intermediate nodes can be retrieved using the <see cref="GetNodeLink"/> method
/// which can be of great use if you want to, for example, play a link specific animation when reaching the link.
///
/// \inspectorField{End, NodeLink2.end}
/// \inspectorField{Cost Factor, NodeLink2.costFactor}
/// \inspectorField{One Way, NodeLink2.oneWay}
/// \inspectorField{Pathfinding Tag, NodeLink2.pathfindingTag}
/// \inspectorField{Graph Mask, NodeLink2.graphMask}
///
/// See: offmeshlinks (view in online documentation for working links)
/// See: The example scene RecastExample2 contains a few links which you can take a look at to see how they are used.
///
/// Note: If you make any modifications to the node link's settings after it has been created, you need to call the <see cref="Apply"/> method in order to apply the changes to the graph.
/// </summary>
[AddComponentMenu("Pathfinding/Link2")]
[HelpURL("https://arongranberg.com/astar/documentation/stable/nodelink2.html")]
public class NodeLink2 : GraphModifier {
/// <summary>End position of the link</summary>
public Transform end;
/// <summary>
/// The connection will be this times harder/slower to traverse.
///
/// A cost factor of 1 means that the link is equally expensive as moving the same distance on the normal navmesh. But a cost factor greater than 1 means that it is proportionally more expensive.
///
/// You should not use a cost factor less than 1 unless you also change the <see cref="AstarPath.heuristicScale"/> field (A* Inspector -> Settings -> Pathfinding) to at most the minimum cost factor that you use anywhere in the scene (or disable the heuristic altogether).
/// This is because the pathfinding algorithm assumes that paths are at least as costly as walking just the straight line distance to the target, and if you use a cost factor less than 1, that assumption is no longer true.
/// What then happens is that the pathfinding search may ignore some links because it doesn't even think to search in that direction, even if they would have lead to a lower path cost.
///
/// Warning: Reducing the heuristic scale or disabling the heuristic can significantly increase the cpu cost for pathfinding, especially for large graphs.
///
/// Read more about this at https://en.wikipedia.org/wiki/Admissible_heuristic.
/// </summary>
public float costFactor = 1.0f;
/// <summary>Make a one-way connection</summary>
public bool oneWay = false;
/// <summary>
/// The tag to apply to the link.
///
/// This can be used to exclude certain agents from using the link, or make it more expensive to use.
///
/// See: tags (view in online documentation for working links)
/// </summary>
public PathfindingTag pathfindingTag = 0;
/// <summary>
/// Which graphs this link is allowed to connect.
///
/// The link will always connect the nodes closest to the start and end points on the graphs that it is allowed to connect.
/// </summary>
public GraphMask graphMask = -1;
public Transform StartTransform => transform;
public Transform EndTransform => end;
protected OffMeshLinks.OffMeshLinkSource linkSource;
/// <summary>
/// Returns the link component associated with the specified node.
/// Returns: The link associated with the node or null if the node is not a link node, or it is not associated with a <see cref="NodeLink2"/> component.
/// </summary>
/// <param name="node">The node to get the link for.</param>
public static NodeLink2 GetNodeLink(GraphNode node) => node is LinkNode linkNode ? linkNode.linkSource.component as NodeLink2 : null;
/// <summary>
/// True if the link is connected to the graph.
///
/// This will be true if the link has been successfully connected to the graph, and false if it either has failed, or if the component/gameobject is disabled.
///
/// When the component is enabled, the link will be scheduled to be added to the graph, it will not take effect immediately.
/// This means that this property will return false until the next time graph updates are processed (usually later this frame, or next frame).
/// To ensure the link is refreshed immediately, you can call <see cref="AstarPath.active.FlushWorkItems"/>.
/// </summary>
internal bool isActive => linkSource != null && (linkSource.status & OffMeshLinks.OffMeshLinkStatus.Active) != 0;
IOffMeshLinkHandler onTraverseOffMeshLinkHandler;
/// <summary>
/// Callback to be called when an agent starts traversing an off-mesh link.
///
/// The handler will be called when the agent starts traversing an off-mesh link.
/// It allows you to to control the agent for the full duration of the link traversal.
///
/// Use the passed context struct to get information about the link and to control the agent.
///
/// <code>
/// using UnityEngine;
/// using Pathfinding;
/// using System.Collections;
/// using Pathfinding.ECS;
///
/// namespace Pathfinding.Examples {
/// public class FollowerJumpLink : MonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine {
/// // Register this class as the handler for off-mesh links when the component is enabled
/// void OnEnable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = this;
/// void OnDisable() => GetComponent<NodeLink2>().onTraverseOffMeshLink = null;
///
/// IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context) => this;
///
/// void IOffMeshLinkStateMachine.OnFinishTraversingOffMeshLink (AgentOffMeshLinkTraversalContext context) {
/// Debug.Log("An agent finished traversing an off-mesh link");
/// }
///
/// void IOffMeshLinkStateMachine.OnAbortTraversingOffMeshLink () {
/// Debug.Log("An agent aborted traversing an off-mesh link");
/// }
///
/// IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink (AgentOffMeshLinkTraversalContext ctx) {
/// var start = (Vector3)ctx.link.relativeStart;
/// var end = (Vector3)ctx.link.relativeEnd;
/// var dir = end - start;
///
/// // Disable local avoidance while traversing the off-mesh link.
/// // If it was enabled, it will be automatically re-enabled when the agent finishes traversing the link.
/// ctx.DisableLocalAvoidance();
///
/// // Move and rotate the agent to face the other side of the link.
/// // When reaching the off-mesh link, the agent may be facing the wrong direction.
/// while (!ctx.MoveTowards(
/// position: start,
/// rotation: Quaternion.LookRotation(dir, ctx.movementPlane.up),
/// gravity: true,
/// slowdown: true).reached) {
/// yield return null;
/// }
///
/// var bezierP0 = start;
/// var bezierP1 = start + Vector3.up*5;
/// var bezierP2 = end + Vector3.up*5;
/// var bezierP3 = end;
/// var jumpDuration = 1.0f;
///
/// // Animate the AI to jump from the start to the end of the link
/// for (float t = 0; t < jumpDuration; t += ctx.deltaTime) {
/// ctx.transform.Position = AstarSplines.CubicBezier(bezierP0, bezierP1, bezierP2, bezierP3, Mathf.SmoothStep(0, 1, t / jumpDuration));
/// yield return null;
/// }
/// }
/// }
/// }
/// </code>
///
/// Warning: Off-mesh links can be destroyed or disabled at any moment. The built-in code will attempt to make the agent continue following the link even if it is destroyed,
/// but if you write your own traversal code, you should be aware of this.
///
/// You can alternatively set the corresponding property property on the agent (<see cref="FollowerEntity.onTraverseOffMeshLink"/>) to specify a callback for a all off-mesh links.
///
/// Note: The agent's off-mesh link handler takes precedence over the link's off-mesh link handler, if both are set.
///
/// Warning: This property only works with the <see cref="FollowerEntity"/> component. Use <see cref="RichAI.onTraverseOffMeshLink"/> if you are using the <see cref="RichAI"/> movement script.
///
/// See: offmeshlinks (view in online documentation for working links) for more details and example code
/// </summary>
public IOffMeshLinkHandler onTraverseOffMeshLink {
get => onTraverseOffMeshLinkHandler;
set {
onTraverseOffMeshLinkHandler = value;
if (linkSource != null) linkSource.handler = value;
}
}
public override void OnPostScan () {
TryAddLink();
}
protected override void OnEnable () {
base.OnEnable();
if (Application.isPlaying && !BatchedEvents.Has(this)) BatchedEvents.Add(this, BatchedEvents.Event.Update, OnUpdate);
TryAddLink();
}
static void OnUpdate (NodeLink2[] components, int count) {
// Only check for moved links every N frames, for performance
if ((Time.frameCount % 16) != 0) return;
for (int i = 0; i < count; i++) {
var comp = components[i];
var start = comp.StartTransform;
var end = comp.EndTransform;
var added = comp.linkSource != null;
if ((start != null && end != null) != added || (added && (start.hasChanged || end.hasChanged))) {
if (start != null) start.hasChanged = false;
if (end != null) end.hasChanged = false;
comp.RemoveLink();
comp.TryAddLink();
}
}
}
void TryAddLink () {
// In case the AstarPath component has been destroyed (destroying the link).
// But do not clear it if the link is inactive because it failed to become enabled
if (linkSource != null && (linkSource.status == OffMeshLinks.OffMeshLinkStatus.Inactive || (linkSource.status & OffMeshLinks.OffMeshLinkStatus.PendingRemoval) != 0)) linkSource = null;
if (linkSource == null && AstarPath.active != null && EndTransform != null) {
StartTransform.hasChanged = false;
EndTransform.hasChanged = false;
linkSource = new OffMeshLinks.OffMeshLinkSource {
start = new OffMeshLinks.Anchor {
center = StartTransform.position,
rotation = StartTransform.rotation,
width = 0f,
},
end = new OffMeshLinks.Anchor {
center = EndTransform.position,
rotation = EndTransform.rotation,
width = 0f,
},
directionality = oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay,
tag = pathfindingTag,
costFactor = costFactor,
graphMask = graphMask,
maxSnappingDistance = 1, // TODO
component = this,
handler = onTraverseOffMeshLink,
};
AstarPath.active.offMeshLinks.Add(linkSource);
}
}
void RemoveLink () {
if (AstarPath.active != null && linkSource != null) AstarPath.active.offMeshLinks.Remove(linkSource);
linkSource = null;
}
protected override void OnDisable () {
base.OnDisable();
BatchedEvents.Remove(this);
RemoveLink();
}
[ContextMenu("Recalculate neighbours")]
void ContextApplyForce () {
Apply();
}
/// <summary>
/// Disconnects and then reconnects the link to the graph.
///
/// If you have moved the link or otherwise modified it you need to call this method to apply those changes.
/// </summary>
public virtual void Apply () {
RemoveLink();
TryAddLink();
}
private readonly static Color GizmosColor = new Color(206.0f/255.0f, 136.0f/255.0f, 48.0f/255.0f, 0.5f);
private readonly static Color GizmosColorSelected = new Color(235.0f/255.0f, 123.0f/255.0f, 32.0f/255.0f, 1.0f);
public override void DrawGizmos () {
if (StartTransform == null || EndTransform == null) return;
var startPos = StartTransform.position;
var endPos = EndTransform.position;
if (linkSource != null && (Time.renderedFrameCount % 16) == 0 && Application.isEditor) {
// Check if the link has moved
// During runtime, this will be done by the OnUpdate method instead
if (linkSource.start.center != startPos || linkSource.end.center != endPos || linkSource.directionality != (oneWay ? OffMeshLinks.Directionality.OneWay : OffMeshLinks.Directionality.TwoWay) || linkSource.costFactor != costFactor || linkSource.graphMask != graphMask || linkSource.tag != pathfindingTag) {
Apply();
}
}
bool selected = GizmoContext.InActiveSelection(this);
var graphs = linkSource != null && AstarPath.active != null? AstarPath.active.offMeshLinks.ConnectedGraphs(linkSource) : null;
var up = Vector3.up;
// Find the natural up direction of the connected graphs, so that we can orient the gizmos appropriately
if (graphs != null) {
for (int i = 0; i < graphs.Count; i++) {
var graph = graphs[i];
if (graph != null) {
if (graph is NavmeshBase navmesh) {
up = navmesh.transform.WorldUpAtGraphPosition(Vector3.zero);
break;
} else if (graph is GridGraph grid) {
up = grid.transform.WorldUpAtGraphPosition(Vector3.zero);
break;
}
}
}
ListPool<NavGraph>.Release(ref graphs);
}
var active = linkSource != null && linkSource.status == OffMeshLinks.OffMeshLinkStatus.Active;
Color color = selected ? GizmosColorSelected : GizmosColor;
if (active) color = Color.green;
Draw.Circle(startPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectStart) ? Color.red : color);
Draw.Circle(endPos, up, 0.4f, linkSource != null && linkSource.status.HasFlag(OffMeshLinks.OffMeshLinkStatus.FailedToConnectEnd) ? Color.red : color);
NodeLink.DrawArch(startPos, endPos, up, color);
if (selected) {
Vector3 cross = Vector3.Cross(up, endPos-startPos).normalized;
using (Draw.WithLineWidth(2)) {
NodeLink.DrawArch(startPos+cross*0.0f, endPos+cross*0.0f, up, color);
}
// NodeLink.DrawArch(startPos-cross*0.1f, endPos-cross*0.1f, color);
}
}
}
}
|