190 lines
6.6 KiB
C#
190 lines
6.6 KiB
C#
#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);
|
|
}
|
|
}
|
|
}
|