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
|
using System.Collections;
using System.Collections.Generic;
using Pathfinding.ECS;
using UnityEngine;
namespace Pathfinding.Examples {
/// <summary>
/// Example script for handling interactable objects in the example scenes.
///
/// It implements a very simple and lightweight state machine.
///
/// Note: This is an example script intended for the A* Pathfinding Project's example scenes.
/// If you need a proper state machine for your game, you may be better served by other state machine solutions on the Unity Asset Store.
///
/// It works by keeping a linear list of states, each with an associated action.
/// When an agent iteracts with this object, it immediately does the first action in the list.
/// Once that action is done, it will do the next action and so on.
///
/// Some actions may cancel the whole interaction. For example the MoveTo action will cancel the interaction if the agent
/// suddenly had its destination to something else. Presumably because the agent was interrupted by something.
///
/// If this component is added to the same GameObject as a <see cref="NodeLink2"/> component, the interactable will automatically trigger when the agent traverses the link.
/// Some components behave differently when used during an off-mesh link component.
/// For example the <see cref="MoveToAction"/> will move the agent without taking the navmesh into account (becoming a thin wrapper for <see cref="AgentOffMeshLinkTraversalContext.MoveTowards"/>).
/// </summary>
[HelpURL("https://arongranberg.com/astar/documentation/stable/interactable.html")]
public class Interactable : VersionedMonoBehaviour, IOffMeshLinkHandler, IOffMeshLinkStateMachine {
public enum CoroutineAction {
Tick,
Cancel,
}
[System.Serializable]
public abstract class InteractableAction {
public virtual IEnumerator<CoroutineAction> Execute (IAstarAI ai) {
return Execute();
}
#if MODULE_ENTITIES
public virtual IEnumerator<CoroutineAction> Execute (Pathfinding.ECS.AgentOffMeshLinkTraversalContext context) {
return Execute();
}
#endif
public virtual IEnumerator<CoroutineAction> Execute () {
throw new System.NotImplementedException("This action has no implementation");
}
}
[System.Serializable]
public class AnimatorPlay : InteractableAction {
public string stateName;
public float normalizedTime = 0;
public Animator animator;
public override IEnumerator<CoroutineAction> Execute () {
animator.Play(stateName, -1, normalizedTime);
yield break;
}
}
[System.Serializable]
public class AnimatorSetBoolAction : InteractableAction {
public string propertyName;
public bool value;
public Animator animator;
public override IEnumerator<CoroutineAction> Execute () {
animator.SetBool(propertyName, value);
yield break;
}
}
[System.Serializable]
public class ActivateParticleSystem : InteractableAction {
public ParticleSystem particleSystem;
public override IEnumerator<CoroutineAction> Execute () {
particleSystem.Play();
yield break;
}
}
[System.Serializable]
public class DelayAction : InteractableAction {
public float delay;
public override IEnumerator<CoroutineAction> Execute () {
float time = Time.time + delay;
while (Time.time < time) yield return CoroutineAction.Tick;
yield break;
}
}
[System.Serializable]
public class SetObjectActiveAction : InteractableAction {
public GameObject target;
public bool active;
public override IEnumerator<CoroutineAction> Execute () {
target.SetActive(active);
yield break;
}
}
[System.Serializable]
public class InstantiatePrefab : InteractableAction {
public GameObject prefab;
public Transform position;
public override IEnumerator<CoroutineAction> Execute () {
if (prefab != null && position != null) {
GameObject.Instantiate(prefab, position.position, position.rotation);
}
yield break;
}
}
[System.Serializable]
public class CallFunction : InteractableAction {
public UnityEngine.Events.UnityEvent function;
public override IEnumerator<CoroutineAction> Execute () {
function.Invoke();
yield break;
}
}
[System.Serializable]
public class TeleportAgentAction : InteractableAction {
public Transform destination;
public override IEnumerator<CoroutineAction> Execute (IAstarAI ai) {
ai.Teleport(destination.position);
yield break;
}
#if MODULE_ENTITIES
public override IEnumerator<CoroutineAction> Execute (AgentOffMeshLinkTraversalContext context) {
context.Teleport(destination.position);
yield break;
}
#endif
}
[System.Serializable]
public class TeleportAgentOnLinkAction : InteractableAction {
public enum Destination {
/// <summary>The side of the link that the agent starts traversing it from</summary>
RelativeStartOfLink,
/// <summary>The side of the link that is opposite the one the agent starts traversing it from</summary>
RelativeEndOfLink,
}
public Destination destination = Destination.RelativeEndOfLink;
public override IEnumerator<CoroutineAction> Execute() => throw new System.NotImplementedException("This action only works for agents traversing off-mesh links.");
#if MODULE_ENTITIES
public override IEnumerator<CoroutineAction> Execute (AgentOffMeshLinkTraversalContext context) {
context.Teleport(destination == Destination.RelativeStartOfLink ? context.link.relativeStart : context.link.relativeEnd);
yield break;
}
#endif
}
[System.Serializable]
public class SetTransformAction : InteractableAction {
public Transform transform;
public Transform source;
public bool setPosition = true;
public bool setRotation;
public bool setScale;
public override IEnumerator<CoroutineAction> Execute () {
if (setPosition) transform.position = source.position;
if (setRotation) transform.rotation = source.rotation;
if (setScale) transform.localScale = source.localScale;
yield break;
}
}
[System.Serializable]
public class MoveToAction : InteractableAction {
public Transform destination;
public bool useRotation;
public bool waitUntilReached;
public override IEnumerator<CoroutineAction> Execute (IAstarAI ai) {
var dest = destination.position;
#if MODULE_ENTITIES
if (useRotation && ai is FollowerEntity follower) {
follower.SetDestination(dest, destination.rotation * Vector3.forward);
} else
#endif
{
if (useRotation) Debug.LogError("useRotation is only supported for FollowerEntity agents", ai as MonoBehaviour);
ai.destination = dest;
}
if (waitUntilReached) {
if (ai is AIBase || ai is AILerp) {
// Only the FollowerEntity component is good enough to set the reachedDestination property to false immediately.
// The other movement scripts need to wait for the new path to be available, which is somewhat annoying.
ai.SearchPath();
while (ai.pathPending) yield return CoroutineAction.Tick;
}
while (!ai.reachedDestination) {
if (ai.destination != dest) {
// Something else must have changed the destination
yield return CoroutineAction.Cancel;
}
if (ai.reachedEndOfPath) {
// We have reached the end of the path, but not the destination
// This must mean that we cannot get any closer
// TODO: More accurate 'cannot move forwards' check
yield return CoroutineAction.Cancel;
}
yield return CoroutineAction.Tick;
}
}
}
#if MODULE_ENTITIES
public override IEnumerator<CoroutineAction> Execute (AgentOffMeshLinkTraversalContext context) {
while (!context.MoveTowards(destination.position, destination.rotation, true, true).reached) {
yield return CoroutineAction.Tick;
}
yield break;
}
#endif
}
[System.Serializable]
public class InteractAction : InteractableAction {
public Interactable interactable;
public override IEnumerator<CoroutineAction> Execute (IAstarAI ai) {
var it = interactable.InteractCoroutine(ai);
while (it.MoveNext()) {
yield return it.Current;
}
}
}
[SerializeReference]
public List<InteractableAction> actions;
public void Interact (IAstarAI ai) {
StartCoroutine(InteractCoroutine(ai));
}
#if MODULE_ENTITIES
IOffMeshLinkStateMachine IOffMeshLinkHandler.GetOffMeshLinkStateMachine(AgentOffMeshLinkTraversalContext context) => this;
IEnumerable IOffMeshLinkStateMachine.OnTraverseOffMeshLink (AgentOffMeshLinkTraversalContext context) {
var it = InteractCoroutine(context);
while (it.MoveNext()) {
yield return null;
}
}
public IEnumerator<CoroutineAction> InteractCoroutine (Pathfinding.ECS.AgentOffMeshLinkTraversalContext context) {
if (actions.Count == 0) {
Debug.LogWarning("No actions have been set up for this interactable", this);
yield break;
}
var actionIndex = 0;
while (actionIndex < actions.Count) {
var action = actions[actionIndex];
if (action == null) {
actionIndex++;
continue;
}
var enumerator = action.Execute(context);
while (enumerator.MoveNext()) {
yield return enumerator.Current;
if (enumerator.Current == CoroutineAction.Cancel) yield break;
}
actionIndex++;
}
}
#endif
public IEnumerator<CoroutineAction> InteractCoroutine (IAstarAI ai) {
if (actions.Count == 0) {
Debug.LogWarning("No actions have been set up for this interactable", this);
yield break;
}
var actionIndex = 0;
while (actionIndex < actions.Count) {
var action = actions[actionIndex];
if (action == null) {
actionIndex++;
continue;
}
var enumerator = action.Execute(ai);
while (enumerator.MoveNext()) {
yield return enumerator.Current;
if (enumerator.Current == CoroutineAction.Cancel) yield break;
}
actionIndex++;
}
}
void OnEnable () {
// Allow the interactable to be triggered by an agent traversing an off-mesh link
if (TryGetComponent<NodeLink2>(out var link)) link.onTraverseOffMeshLink = this;
}
void OnDisable () {
if (TryGetComponent<NodeLink2>(out var link) && link.onTraverseOffMeshLink == (IOffMeshLinkHandler)this) link.onTraverseOffMeshLink = null;
}
}
}
|