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
|
#pragma warning disable 649
using UnityEngine;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using Pathfinding.Util;
namespace Pathfinding {
/// <summary>
/// Moves the target in example scenes.
/// This is a simple script which has the sole purpose
/// of moving the target point of agents in the example
/// scenes for the A* Pathfinding Project.
///
/// It is not meant to be pretty, but it does the job.
/// </summary>
[HelpURL("https://arongranberg.com/astar/documentation/stable/targetmover.html")]
public class TargetMover : VersionedMonoBehaviour {
/// <summary>Mask for the raycast placement</summary>
public LayerMask mask;
public Transform target;
/// <summary>Determines if the target position should be updated every frame or only on double-click</summary>
bool onlyOnDoubleClick;
public Trigger trigger;
public GameObject clickEffect;
public bool use2D;
public PathUtilities.FormationMode formationMode = PathUtilities.FormationMode.SinglePoint;
Camera cam;
public enum Trigger {
Continuously,
SingleClick,
DoubleClick
}
public void Start () {
// Cache the Main Camera
cam = Camera.main;
}
public void OnGUI () {
if (trigger != Trigger.Continuously && cam != null && Event.current.type == EventType.MouseDown) {
if (Event.current.clickCount == (trigger == Trigger.DoubleClick ? 2 : 1)) {
UpdateTargetPosition();
}
}
}
/// <summary>Update is called once per frame</summary>
void Update () {
if (trigger == Trigger.Continuously && cam != null) {
UpdateTargetPosition();
}
}
public void UpdateTargetPosition () {
Vector3 newPosition = Vector3.zero;
bool positionFound = false;
Transform hitObject = null;
// If the game view has never been rendered, the mouse position can be infinite
if (!float.IsFinite(Input.mousePosition.x)) return;
if (use2D) {
newPosition = cam.ScreenToWorldPoint(Input.mousePosition);
newPosition.z = 0;
positionFound = true;
var collider = Physics2D.OverlapPoint(newPosition, mask);
if (collider != null) hitObject = collider.transform;
} else {
// Fire a ray through the scene at the mouse position and place the target where it hits
if (cam.pixelRect.Contains(Input.mousePosition) && Physics.Raycast(cam.ScreenPointToRay(Input.mousePosition), out var hit, Mathf.Infinity, mask)) {
newPosition = hit.point;
hitObject = hit.transform;
positionFound = true;
}
}
if (positionFound) {
if (target != null) target.position = newPosition;
if (trigger != Trigger.Continuously) {
// Slightly inefficient way of finding all AIs, but this is just an example script, so it doesn't matter much.
// FindObjectsByType does not support interfaces unfortunately.
var ais = UnityCompatibility.FindObjectsByTypeSorted<MonoBehaviour>().OfType<IAstarAI>().ToList();
StopAllCoroutines();
if (hitObject != null && hitObject.TryGetComponent<Pathfinding.Examples.Interactable>(out var interactable)) {
// Pick the first AI to interact with the interactable
if (ais.Count > 0) interactable.Interact(ais[0]);
} else {
if (clickEffect != null) {
GameObject.Instantiate(clickEffect, newPosition, Quaternion.identity);
}
// This will calculate individual destinations for each agent, like in a formation pattern.
// The simplest mode, FormationMode.SinglePoint, just assigns newPosition to all entries of the 'destinations' list.
var destinations = PathUtilities.FormationDestinations(ais, newPosition, formationMode, 0.5f);
for (int i = 0; i < ais.Count; i++) {
#if MODULE_ENTITIES
var isFollowerEntity = ais[i] is FollowerEntity;
#else
var isFollowerEntity = false;
#endif
if (ais[i] != null) {
ais[i].destination = destinations[i];
// Make the agents recalculate their path immediately for slighly increased responsiveness.
// The FollowerEntity is better at doing this automatically.
if (!isFollowerEntity) ais[i].SearchPath();
}
}
StartCoroutine(OptimizeFormationDestinations(ais, destinations));
}
}
}
}
/// <summary>
/// Swap the destinations of pairs of agents if it reduces the total distance they need to travel.
///
/// This is a simple optimization algorithm to make group movement smoother and more efficient.
/// It is not perfect and may not always find the optimal solution, but it is very fast and works well in practice.
/// It will not work great for large groups of agents, as the optimization becomes too hard for this simple algorithm.
///
/// See: https://en.wikipedia.org/wiki/Assignment_problem
/// </summary>
IEnumerator OptimizeFormationDestinations (List<IAstarAI> ais, List<Vector3> destinations) {
// Prevent swapping the same agents multiple times.
// This is because the distance measurement is only an approximation, and agents
// may temporarily have to move away from their destination before they can move towards it.
// Allowing multiple swaps could make the agents move back and forth indefinitely as the targets shift around.
var alreadySwapped = new HashSet<(IAstarAI, IAstarAI)>();
const int IterationsPerFrame = 4;
while (true) {
for (int i = 0; i < IterationsPerFrame; i++) {
var a = Random.Range(0, ais.Count);
var b = Random.Range(0, ais.Count);
if (a == b) continue;
if (b < a) Memory.Swap(ref a, ref b);
var aiA = ais[a];
var aiB = ais[b];
if ((MonoBehaviour)aiA == null) continue;
if ((MonoBehaviour)aiB == null) continue;
if (alreadySwapped.Contains((aiA, aiB))) continue;
var pA = aiA.position;
var pB = aiB.position;
var distA = (pA - destinations[a]).sqrMagnitude;
var distB = (pB - destinations[b]).sqrMagnitude;
var newDistA = (pA - destinations[b]).sqrMagnitude;
var newDistB = (pB - destinations[a]).sqrMagnitude;
var cost1 = distA + distB;
var cost2 = newDistA + newDistB;
if (cost2 < cost1 * 0.98f) {
// Swap the destinations
var tmp = destinations[a];
destinations[a] = destinations[b];
destinations[b] = tmp;
aiA.destination = destinations[a];
aiB.destination = destinations[b];
alreadySwapped.Add((aiA, aiB));
}
}
yield return null;
}
}
protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) {
if (migrations.TryMigrateFromLegacyFormat(out var legacyVersion)) {
if (legacyVersion < 2) {
trigger = onlyOnDoubleClick ? Trigger.DoubleClick : Trigger.Continuously;
}
}
base.OnUpgradeSerializedData(ref migrations, unityThread);
}
}
}
|