update
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Pathfinding.Examples {
|
||||
using Pathfinding;
|
||||
|
||||
/// <summary>
|
||||
/// Example of how to handle off-mesh link traversal.
|
||||
/// This is used in the "Example4_Recast_Navmesh2" example scene.
|
||||
///
|
||||
/// See: <see cref="Pathfinding.RichAI"/>
|
||||
/// See: <see cref="Pathfinding.RichAI.onTraverseOffMeshLink"/>
|
||||
/// See: <see cref="Pathfinding.AnimationLink"/>
|
||||
/// </summary>
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/animationlinktraverser.html")]
|
||||
public class AnimationLinkTraverser : VersionedMonoBehaviour {
|
||||
public Animation anim;
|
||||
|
||||
RichAI ai;
|
||||
|
||||
void OnEnable () {
|
||||
ai = GetComponent<RichAI>();
|
||||
if (ai != null) ai.onTraverseOffMeshLink += TraverseOffMeshLink;
|
||||
}
|
||||
|
||||
void OnDisable () {
|
||||
if (ai != null) ai.onTraverseOffMeshLink -= TraverseOffMeshLink;
|
||||
}
|
||||
|
||||
protected virtual IEnumerator TraverseOffMeshLink (RichSpecial rs) {
|
||||
var link = rs.nodeLink.gameObject.GetComponent<AnimationLink>();
|
||||
|
||||
if (link == null) {
|
||||
Debug.LogError("Unhandled RichSpecial");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Rotate character to face the correct direction
|
||||
while (true) {
|
||||
var origRotation = ai.rotation;
|
||||
var finalRotation = ai.SimulateRotationTowards(rs.first.rotation * Vector3.forward, ai.rotationSpeed * Time.deltaTime);
|
||||
// Rotate until the rotation does not change anymore
|
||||
if (origRotation == finalRotation) break;
|
||||
ai.FinalizeMovement(ai.position, finalRotation);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
// Reposition
|
||||
transform.parent.position = transform.position;
|
||||
|
||||
transform.parent.rotation = transform.rotation;
|
||||
transform.localPosition = Vector3.zero;
|
||||
transform.localRotation = Quaternion.identity;
|
||||
|
||||
// Set up animation speeds
|
||||
if (rs.reverse && link.reverseAnim) {
|
||||
anim[link.clip].speed = -link.animSpeed;
|
||||
anim[link.clip].normalizedTime = 1;
|
||||
anim.Play(link.clip);
|
||||
anim.Sample();
|
||||
} else {
|
||||
anim[link.clip].speed = link.animSpeed;
|
||||
anim.Rewind(link.clip);
|
||||
anim.Play(link.clip);
|
||||
}
|
||||
|
||||
// Fix required for animations in reverse direction
|
||||
transform.parent.position -= transform.position-transform.parent.position;
|
||||
|
||||
// Wait for the animation to finish
|
||||
yield return new WaitForSeconds(Mathf.Abs(anim[link.clip].length/link.animSpeed));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81825518f1bed49bda1223459d4bdf99
|
||||
timeCreated: 1498659013
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
Packages/com.arongranberg.astar/Utilities/AstarChecksum.cs
Normal file
20
Packages/com.arongranberg.astar/Utilities/AstarChecksum.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Pathfinding.Util {
|
||||
/// <summary>Calculates checksums of byte arrays</summary>
|
||||
internal class Checksum {
|
||||
/// <summary>Calculate checksum for the byte array.</summary>
|
||||
/// <param name="arr">Byte array to calculate checksum for. May be null.</param>
|
||||
/// <param name="hash">Initial hash value. Default is 0. Can be used to chain checksums together.</param>
|
||||
public static uint GetChecksum (byte[] arr, uint hash = 0) {
|
||||
// Sort of implements the Fowler–Noll–Vo hash function
|
||||
const int prime = 16777619;
|
||||
|
||||
hash ^= 2166136261U;
|
||||
if (arr == null) return hash - 1;
|
||||
|
||||
for (int i = 0; i < arr.Length; i++)
|
||||
hash = (hash ^ arr[i]) * prime;
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8959628b7b47f4ccca91bcfa87d9f77e
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
243
Packages/com.arongranberg.astar/Utilities/BatchedEvents.cs
Normal file
243
Packages/com.arongranberg.astar/Utilities/BatchedEvents.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Jobs;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Pathfinding.Util {
|
||||
/// <summary>Helper for batching updates to many objects efficiently</summary>
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/batchedevents.html")]
|
||||
public class BatchedEvents : VersionedMonoBehaviour {
|
||||
const int ArchetypeOffset = 22;
|
||||
const int ArchetypeMask = 0xFF << ArchetypeOffset;
|
||||
|
||||
static Archetype[] data = new Archetype[0];
|
||||
static BatchedEvents instance;
|
||||
static int isIteratingOverTypeIndex = -1;
|
||||
static bool isIterating = false;
|
||||
|
||||
[System.Flags]
|
||||
public enum Event {
|
||||
Update = 1 << 0,
|
||||
LateUpdate = 1 << 1,
|
||||
FixedUpdate = 1 << 2,
|
||||
Custom = 1 << 3,
|
||||
None = 0,
|
||||
};
|
||||
|
||||
|
||||
struct Archetype {
|
||||
public object[] objects;
|
||||
public int objectCount;
|
||||
public System.Type type;
|
||||
public TransformAccessArray transforms;
|
||||
public int variant;
|
||||
public int archetypeIndex;
|
||||
public Event events;
|
||||
public System.Action<object[], int, TransformAccessArray, Event> action;
|
||||
public CustomSampler sampler;
|
||||
|
||||
public void Add (Component obj) {
|
||||
objectCount++;
|
||||
UnityEngine.Assertions.Assert.IsTrue(objectCount < (1 << ArchetypeOffset));
|
||||
if (objects == null) objects = (object[])System.Array.CreateInstance(type, math.ceilpow2(objectCount));
|
||||
if (objectCount > objects.Length) {
|
||||
var newObjects = System.Array.CreateInstance(type, math.ceilpow2(objectCount));
|
||||
objects.CopyTo(newObjects, 0);
|
||||
objects = (object[])newObjects;
|
||||
}
|
||||
objects[objectCount-1] = obj;
|
||||
if (!transforms.isCreated) transforms = new TransformAccessArray(16, -1);
|
||||
transforms.Add(obj.transform);
|
||||
((IEntityIndex)obj).EntityIndex = (archetypeIndex << ArchetypeOffset) | (objectCount-1);
|
||||
}
|
||||
|
||||
public void Remove (int index) {
|
||||
objectCount--;
|
||||
((IEntityIndex)objects[objectCount]).EntityIndex = (archetypeIndex << ArchetypeOffset) | index;
|
||||
((IEntityIndex)objects[index]).EntityIndex = 0;
|
||||
objects[index] = objects[objectCount];
|
||||
objects[objectCount] = null;
|
||||
transforms.RemoveAtSwapBack(index);
|
||||
|
||||
if (objectCount == 0) transforms.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
void DelayedDestroy () {
|
||||
UnityEditor.EditorApplication.update -= DelayedDestroy;
|
||||
GameObject.DestroyImmediate(gameObject);
|
||||
}
|
||||
#endif
|
||||
|
||||
void OnEnable () {
|
||||
if (instance == null) instance = this;
|
||||
if (instance != this) {
|
||||
// We cannot destroy the object while it is being enabled, so we need to delay it a bit
|
||||
#if UNITY_EDITOR
|
||||
// This is only important in the editor to avoid a build-up of old managers.
|
||||
// In an actual game at most 1 (though in practice zero) old managers will be laying around.
|
||||
// It would be nice to use a coroutine for this instead, but unfortunately they do not work for objects marked with HideAndDontSave.
|
||||
UnityEditor.EditorApplication.update += DelayedDestroy;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
static void CreateInstance () {
|
||||
// If scripts are recompiled the the static variable will be lost.
|
||||
// Some users recompile scripts in play mode and then reload the scene (https://forum.arongranberg.com/t/rts-game-pathfinding/6623/48?u=aron_granberg)
|
||||
// which makes handling this a requirement.
|
||||
|
||||
// Here one might try to look for existing instances of the class that haven't yet been enabled.
|
||||
// However, this turns out to be tricky.
|
||||
// Resources.FindObjectsOfTypeAll<T>() is the only call that includes HideInInspector GameObjects.
|
||||
// But it is hard to distinguish between objects that are internal ones which will never be enabled and objects that will be enabled.
|
||||
// Checking .gameObject.scene.isLoaded doesn't work reliably (object may be enabled and working even if isLoaded is false)
|
||||
// Checking .gameObject.scene.isValid doesn't work reliably (object may be enabled and working even if isValid is false)
|
||||
|
||||
// So instead we just always create a new instance. This is not a particularly heavy operation and it only happens once per game, so why not.
|
||||
// The OnEnable call will clean up duplicate managers if there are any.
|
||||
|
||||
var go = new GameObject("Batch Helper") {
|
||||
hideFlags = HideFlags.DontSave | HideFlags.NotEditable | HideFlags.HideInInspector | HideFlags.HideInHierarchy
|
||||
};
|
||||
|
||||
instance = go.AddComponent<BatchedEvents>();
|
||||
DontDestroyOnLoad(go);
|
||||
}
|
||||
|
||||
public static T Find<T, K>(K key, System.Func<T, K, bool> predicate) where T : class, IEntityIndex {
|
||||
var t = typeof(T);
|
||||
for (int i = 0; i < data.Length; i++) {
|
||||
if (data[i].type == t) {
|
||||
var objs = data[i].objects as T[];
|
||||
for (int j = 0; j < data[i].objectCount; j++) {
|
||||
if (predicate(objs[j], key)) return objs[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void Remove<T>(T obj) where T : IEntityIndex {
|
||||
int index = obj.EntityIndex;
|
||||
|
||||
if (index == 0) return;
|
||||
|
||||
var archetypeIndex = ((index & ArchetypeMask) >> ArchetypeOffset) - 1;
|
||||
index &= ~ArchetypeMask;
|
||||
UnityEngine.Assertions.Assert.IsTrue(data[archetypeIndex].type == obj.GetType());
|
||||
|
||||
if (isIterating && isIteratingOverTypeIndex == archetypeIndex) throw new System.Exception("Cannot add or remove entities during an event (Update/LateUpdate/...) that this helper initiated");
|
||||
data[archetypeIndex].Remove(index);
|
||||
}
|
||||
|
||||
public static int GetComponents<T>(Event eventTypes, out TransformAccessArray transforms, out T[] components) where T : Component, IEntityIndex {
|
||||
if (instance == null) CreateInstance();
|
||||
|
||||
// Add in a hash of the event types
|
||||
var archetypeVariant = (int)eventTypes * 12582917;
|
||||
if (isIterating && isIteratingOverTypeIndex == archetypeVariant) throw new System.Exception("Cannot add or remove entities during an event (Update/LateUpdate/...) that this helper initiated");
|
||||
|
||||
var type = typeof(T);
|
||||
for (int i = 0; i < data.Length; i++) {
|
||||
if (data[i].type == type && data[i].variant == archetypeVariant) {
|
||||
transforms = data[i].transforms;
|
||||
components = data[i].objects as T[];
|
||||
return data[i].objectCount;
|
||||
}
|
||||
}
|
||||
|
||||
transforms = default;
|
||||
components = null;
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static bool Has<T>(T obj) where T : IEntityIndex => obj.EntityIndex != 0;
|
||||
|
||||
public static void Add<T>(T obj, Event eventTypes, System.Action<T[], int> action, int archetypeVariant = 0) where T : Component, IEntityIndex {
|
||||
Add(obj, eventTypes, null, action, archetypeVariant);
|
||||
}
|
||||
|
||||
public static void Add<T>(T obj, Event eventTypes, System.Action<T[], int, TransformAccessArray, Event> action, int archetypeVariant = 0) where T : Component, IEntityIndex {
|
||||
Add(obj, eventTypes, action, null, archetypeVariant);
|
||||
}
|
||||
|
||||
static void Add<T>(T obj, Event eventTypes, System.Action<T[], int, TransformAccessArray, Event> action1, System.Action<T[], int> action2, int archetypeVariant = 0) where T : Component, IEntityIndex {
|
||||
if (obj.EntityIndex != 0) {
|
||||
throw new System.ArgumentException("This object is already registered. Call Remove before adding the object again.");
|
||||
}
|
||||
|
||||
if (instance == null) CreateInstance();
|
||||
|
||||
// Add in a hash of the event types
|
||||
archetypeVariant = (int)eventTypes * 12582917;
|
||||
if (isIterating && isIteratingOverTypeIndex == archetypeVariant) throw new System.Exception("Cannot add or remove entities during an event (Update/LateUpdate/...) that this helper initiated");
|
||||
|
||||
|
||||
var type = obj.GetType();
|
||||
for (int i = 0; i < data.Length; i++) {
|
||||
if (data[i].type == type && data[i].variant == archetypeVariant) {
|
||||
data[i].Add(obj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
Memory.Realloc(ref data, data.Length + 1);
|
||||
// A copy is made here so that these variables are captured by the lambdas below instead of the original action1/action2 parameters.
|
||||
// If this is not done then the C# JIT will allocate a lambda capture object every time this function is executed
|
||||
// instead of only when we need to create a new archetype. Doing that would create a lot more unnecessary garbage.
|
||||
var ac1 = action1;
|
||||
var ac2 = action2;
|
||||
System.Action<object[], int, TransformAccessArray, Event> a1 = (objs, count, tr, ev) => ac1((T[])objs, count, tr, ev);
|
||||
System.Action<object[], int, TransformAccessArray, Event> a2 = (objs, count, tr, ev) => ac2((T[])objs, count);
|
||||
data[data.Length - 1] = new Archetype {
|
||||
type = type,
|
||||
events = eventTypes,
|
||||
variant = archetypeVariant,
|
||||
archetypeIndex = (data.Length - 1) + 1, // Note: offset by +1 to ensure that entity index = 0 is an invalid index
|
||||
action = ac1 != null ? a1 : a2,
|
||||
sampler = CustomSampler.Create(type.Name),
|
||||
};
|
||||
data[data.Length - 1].Add(obj);
|
||||
}
|
||||
}
|
||||
|
||||
void Process (Event eventType, System.Type typeFilter) {
|
||||
try {
|
||||
isIterating = true;
|
||||
for (int i = 0; i < data.Length; i++) {
|
||||
ref var archetype = ref data[i];
|
||||
if (archetype.objectCount > 0 && (archetype.events & eventType) != 0 && (typeFilter == null || typeFilter == archetype.type)) {
|
||||
isIteratingOverTypeIndex = archetype.variant;
|
||||
try {
|
||||
archetype.sampler.Begin();
|
||||
archetype.action(archetype.objects, archetype.objectCount, archetype.transforms, eventType);
|
||||
} finally {
|
||||
archetype.sampler.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isIterating = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ProcessEvent<T>(Event eventType) {
|
||||
instance?.Process(eventType, typeof(T));
|
||||
}
|
||||
|
||||
void Update () {
|
||||
Process(Event.Update, null);
|
||||
}
|
||||
|
||||
void LateUpdate () {
|
||||
Process(Event.LateUpdate, null);
|
||||
}
|
||||
|
||||
void FixedUpdate () {
|
||||
Process(Event.FixedUpdate, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74164bd7b3a34430db5f19fbdf70c375
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
150
Packages/com.arongranberg.astar/Utilities/DotNetReplacements.cs
Normal file
150
Packages/com.arongranberg.astar/Utilities/DotNetReplacements.cs
Normal file
@@ -0,0 +1,150 @@
|
||||
namespace Pathfinding.Util {
|
||||
/// <summary>
|
||||
/// Simple implementation of a GUID.
|
||||
/// Version: Since 3.6.4 this struct works properly on platforms with different endianness such as Wii U.
|
||||
/// </summary>
|
||||
public struct Guid {
|
||||
const string hex = "0123456789ABCDEF";
|
||||
|
||||
public static readonly Guid zero = new Guid(new byte[16]);
|
||||
public static readonly string zeroString = new Guid(new byte[16]).ToString();
|
||||
|
||||
readonly ulong _a, _b;
|
||||
|
||||
public Guid (byte[] bytes) {
|
||||
// Pack 128 bits into 2 longs
|
||||
ulong a = ((ulong)bytes[0] << 8*0) |
|
||||
((ulong)bytes[1] << 8*1) |
|
||||
((ulong)bytes[2] << 8*2) |
|
||||
((ulong)bytes[3] << 8*3) |
|
||||
((ulong)bytes[4] << 8*4) |
|
||||
((ulong)bytes[5] << 8*5) |
|
||||
((ulong)bytes[6] << 8*6) |
|
||||
((ulong)bytes[7] << 8*7);
|
||||
|
||||
ulong b = ((ulong)bytes[8] << 8*0) |
|
||||
((ulong)bytes[9] << 8*1) |
|
||||
((ulong)bytes[10] << 8*2) |
|
||||
((ulong)bytes[11] << 8*3) |
|
||||
((ulong)bytes[12] << 8*4) |
|
||||
((ulong)bytes[13] << 8*5) |
|
||||
((ulong)bytes[14] << 8*6) |
|
||||
((ulong)bytes[15] << 8*7);
|
||||
|
||||
// Need to swap endianness on e.g Wii U
|
||||
_a = System.BitConverter.IsLittleEndian ? a : SwapEndianness(a);
|
||||
_b = System.BitConverter.IsLittleEndian ? b : SwapEndianness(b);
|
||||
}
|
||||
|
||||
public Guid (string str) {
|
||||
_a = 0;
|
||||
_b = 0;
|
||||
|
||||
if (str.Length < 32)
|
||||
throw new System.FormatException("Invalid Guid format");
|
||||
|
||||
int counter = 0;
|
||||
int i = 0;
|
||||
int offset = 15*4;
|
||||
|
||||
for (; counter < 16; i++) {
|
||||
if (i >= str.Length)
|
||||
throw new System.FormatException("Invalid Guid format. String too short");
|
||||
|
||||
char c = str[i];
|
||||
if (c == '-') continue;
|
||||
|
||||
//Neat trick, perhaps a bit slow, but one will probably not use Guid parsing that much
|
||||
int value = hex.IndexOf(char.ToUpperInvariant(c));
|
||||
if (value == -1)
|
||||
throw new System.FormatException("Invalid Guid format : "+c+" is not a hexadecimal character");
|
||||
|
||||
_a |= (ulong)value << offset;
|
||||
//SetByte (counter,(byte)value);
|
||||
offset -= 4;
|
||||
counter++;
|
||||
}
|
||||
|
||||
offset = 15*4;
|
||||
for (; counter < 32; i++) {
|
||||
if (i >= str.Length)
|
||||
throw new System.FormatException("Invalid Guid format. String too short");
|
||||
|
||||
char c = str[i];
|
||||
if (c == '-') continue;
|
||||
|
||||
//Neat trick, perhaps a bit slow, but one will probably not use Guid parsing that much
|
||||
int value = hex.IndexOf(char.ToUpperInvariant(c));
|
||||
if (value == -1)
|
||||
throw new System.FormatException("Invalid Guid format : "+c+" is not a hexadecimal character");
|
||||
|
||||
_b |= (ulong)value << offset;
|
||||
//SetByte (counter,(byte)value);
|
||||
offset -= 4;
|
||||
counter++;
|
||||
}
|
||||
}
|
||||
|
||||
public static Guid Parse (string input) {
|
||||
return new Guid(input);
|
||||
}
|
||||
|
||||
/// <summary>Swaps between little and big endian</summary>
|
||||
static ulong SwapEndianness (ulong value) {
|
||||
var b1 = (value >> 0) & 0xff;
|
||||
var b2 = (value >> 8) & 0xff;
|
||||
var b3 = (value >> 16) & 0xff;
|
||||
var b4 = (value >> 24) & 0xff;
|
||||
var b5 = (value >> 32) & 0xff;
|
||||
var b6 = (value >> 40) & 0xff;
|
||||
var b7 = (value >> 48) & 0xff;
|
||||
var b8 = (value >> 56) & 0xff;
|
||||
|
||||
return b1 << 56 | b2 << 48 | b3 << 40 | b4 << 32 | b5 << 24 | b6 << 16 | b7 << 8 | b8 << 0;
|
||||
}
|
||||
|
||||
private static System.Random random = new System.Random();
|
||||
|
||||
public static Guid NewGuid () {
|
||||
var bytes = new byte[16];
|
||||
|
||||
random.NextBytes(bytes);
|
||||
return new Guid(bytes);
|
||||
}
|
||||
|
||||
public static bool operator == (Guid lhs, Guid rhs) {
|
||||
return lhs._a == rhs._a && lhs._b == rhs._b;
|
||||
}
|
||||
|
||||
public static bool operator != (Guid lhs, Guid rhs) {
|
||||
return lhs._a != rhs._a || lhs._b != rhs._b;
|
||||
}
|
||||
|
||||
public override bool Equals (System.Object _rhs) {
|
||||
if (!(_rhs is Guid)) return false;
|
||||
|
||||
var rhs = (Guid)_rhs;
|
||||
|
||||
return _a == rhs._a && _b == rhs._b;
|
||||
}
|
||||
|
||||
public override int GetHashCode () {
|
||||
ulong ab = _a ^ _b;
|
||||
|
||||
return (int)(ab >> 32) ^ (int)ab;
|
||||
}
|
||||
|
||||
private static System.Text.StringBuilder text;
|
||||
|
||||
public override string ToString () {
|
||||
if (text == null) {
|
||||
text = new System.Text.StringBuilder();
|
||||
}
|
||||
lock (text) {
|
||||
text.Length = 0;
|
||||
text.Append(_a.ToString("x16")).Append('-').Append(_b.ToString("x16"));
|
||||
return text.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c306b1bfc824f40909c5a91e06e4090f
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
@@ -0,0 +1,3 @@
|
||||
// This file has been removed from the package. Since UnityPackages cannot
|
||||
// delete files, only replace them, this message is left here to prevent old
|
||||
// files from causing compiler errors.
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4798c30909122bed75d4aeaf2d5584c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
258
Packages/com.arongranberg.astar/Utilities/DynamicObstacle.cs
Normal file
258
Packages/com.arongranberg.astar/Utilities/DynamicObstacle.cs
Normal file
@@ -0,0 +1,258 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pathfinding {
|
||||
/// <summary>
|
||||
/// Updates graphs around the object as it moves.
|
||||
///
|
||||
/// Attach this script to any obstacle with a collider to enable dynamic updates of the graphs around it.
|
||||
/// When the object has moved or rotated at least <see cref="updateError"/> world units
|
||||
/// then it will call <see cref="AstarPath.UpdateGraphs"/> to update the graph around it.
|
||||
///
|
||||
/// Make sure that any children colliders do not extend beyond the bounds of the collider attached to the
|
||||
/// GameObject that the DynamicObstacle component is attached to since this script only updates the graph
|
||||
/// around the bounds of the collider on the same GameObject.
|
||||
///
|
||||
/// An update will be triggered whenever the bounding box of the attached collider has changed (moved/expanded/etc.) by at least <see cref="updateError"/> world units or if
|
||||
/// the GameObject has rotated enough so that the outmost point of the object has moved at least <see cref="updateError"/> world units.
|
||||
///
|
||||
/// This script works with both 2D colliders and normal 3D colliders.
|
||||
///
|
||||
/// Note: This script works with a GridGraph, PointGraph, LayerGridGraph or RecastGraph.
|
||||
/// However, for recast graphs, you can often use the <see cref="NavmeshCut"/> instead, for simple obstacles. The <see cref="NavmeshCut"/> can be faster, but it's not quite as flexible.
|
||||
///
|
||||
/// See: AstarPath.UpdateGraphs
|
||||
/// See: graph-updates (view in online documentation for working links)
|
||||
/// See: navmeshcutting (view in online documentation for working links)
|
||||
/// </summary>
|
||||
[AddComponentMenu("Pathfinding/Dynamic Obstacle")]
|
||||
#pragma warning disable 618 // Ignore obsolete warning
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/dynamicobstacle.html")]
|
||||
public class DynamicObstacle : GraphModifier, DynamicGridObstacle {
|
||||
#pragma warning restore 618
|
||||
/// <summary>Collider to get bounds information from</summary>
|
||||
Collider coll;
|
||||
|
||||
/// <summary>2D Collider to get bounds information from</summary>
|
||||
Collider2D coll2D;
|
||||
|
||||
/// <summary>Cached transform component</summary>
|
||||
Transform tr;
|
||||
|
||||
/// <summary>The minimum change in world units along one of the axis of the bounding box of the collider to trigger a graph update</summary>
|
||||
public float updateError = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Time in seconds between bounding box checks.
|
||||
/// If AstarPath.batchGraphUpdates is enabled, it is not beneficial to have a checkTime much lower
|
||||
/// than AstarPath.graphUpdateBatchingInterval because that will just add extra unnecessary graph updates.
|
||||
///
|
||||
/// In real time seconds (based on Time.realtimeSinceStartup).
|
||||
/// </summary>
|
||||
public float checkTime = 0.2F;
|
||||
|
||||
/// <summary>Bounds of the collider the last time the graphs were updated</summary>
|
||||
Bounds prevBounds;
|
||||
|
||||
/// <summary>Rotation of the collider the last time the graphs were updated</summary>
|
||||
Quaternion prevRotation;
|
||||
|
||||
/// <summary>True if the collider was enabled last time the graphs were updated</summary>
|
||||
bool prevEnabled;
|
||||
|
||||
float lastCheckTime = -9999;
|
||||
Queue<GraphUpdateObject> pendingGraphUpdates = new Queue<GraphUpdateObject>();
|
||||
|
||||
Bounds bounds {
|
||||
get {
|
||||
if (coll != null) {
|
||||
return coll.bounds;
|
||||
} else if (coll2D != null) {
|
||||
var b = coll2D.bounds;
|
||||
// Make sure the bounding box stretches close to infinitely along the Z axis (which is the axis perpendicular to the 2D plane).
|
||||
// We don't want any change along the Z axis to make a difference.
|
||||
b.extents += new Vector3(0, 0, 10000);
|
||||
return b;
|
||||
} else {
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool colliderEnabled {
|
||||
get {
|
||||
return coll != null ? coll.enabled : coll2D.enabled;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Awake () {
|
||||
base.Awake();
|
||||
|
||||
coll = GetComponent<Collider>();
|
||||
coll2D = GetComponent<Collider2D>();
|
||||
tr = transform;
|
||||
if (coll == null && coll2D == null && Application.isPlaying) {
|
||||
throw new System.Exception("A collider or 2D collider must be attached to the GameObject(" + gameObject.name + ") for the DynamicObstacle to work");
|
||||
}
|
||||
|
||||
prevBounds = bounds;
|
||||
prevRotation = tr.rotation;
|
||||
// Make sure we update the graph as soon as we find that the collider is enabled
|
||||
prevEnabled = false;
|
||||
}
|
||||
|
||||
public override void OnPostScan () {
|
||||
// Make sure we find the collider
|
||||
// AstarPath.Awake may run before Awake on this component
|
||||
if (coll == null) Awake();
|
||||
|
||||
// In case the object was in the scene from the start and the graphs
|
||||
// were scanned then we ignore the first update since it is unnecessary.
|
||||
if (coll != null) prevEnabled = colliderEnabled;
|
||||
}
|
||||
|
||||
void Update () {
|
||||
if (!Application.isPlaying) return;
|
||||
|
||||
if (coll == null && coll2D == null) {
|
||||
Debug.LogError("No collider attached to this GameObject. The DynamicObstacle component requires a collider.", this);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the previous graph updates have been completed yet.
|
||||
// We don't want to update the graph again until the last graph updates are done.
|
||||
// This is particularly important for recast graphs for which graph updates can take a long time.
|
||||
while (pendingGraphUpdates.Count > 0 && pendingGraphUpdates.Peek().stage != GraphUpdateStage.Pending) {
|
||||
pendingGraphUpdates.Dequeue();
|
||||
}
|
||||
|
||||
if (AstarPath.active == null || AstarPath.active.isScanning || Time.realtimeSinceStartup - lastCheckTime < checkTime || !Application.isPlaying || pendingGraphUpdates.Count > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastCheckTime = Time.realtimeSinceStartup;
|
||||
if (colliderEnabled) {
|
||||
// The current bounds of the collider
|
||||
Bounds newBounds = bounds;
|
||||
var newRotation = tr.rotation;
|
||||
|
||||
Vector3 minDiff = prevBounds.min - newBounds.min;
|
||||
Vector3 maxDiff = prevBounds.max - newBounds.max;
|
||||
|
||||
var extents = newBounds.extents.magnitude;
|
||||
// This is the distance that a point furthest out on the bounding box
|
||||
// would have moved due to the changed rotation of the object
|
||||
var errorFromRotation = extents*Quaternion.Angle(prevRotation, newRotation)*Mathf.Deg2Rad;
|
||||
|
||||
// If the difference between the previous bounds and the new bounds is greater than some value, update the graphs
|
||||
if (minDiff.sqrMagnitude > updateError*updateError || maxDiff.sqrMagnitude > updateError*updateError ||
|
||||
errorFromRotation > updateError || !prevEnabled) {
|
||||
// Update the graphs as soon as possible
|
||||
DoUpdateGraphs();
|
||||
}
|
||||
} else {
|
||||
// Collider has just been disabled
|
||||
if (prevEnabled) {
|
||||
DoUpdateGraphs();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Revert graphs when disabled.
|
||||
/// When the DynamicObstacle is disabled or destroyed, a last graph update should be done to revert nodes to their original state
|
||||
/// </summary>
|
||||
protected override void OnDisable () {
|
||||
base.OnDisable();
|
||||
if (AstarPath.active != null && Application.isPlaying) {
|
||||
var guo = new GraphUpdateObject(prevBounds);
|
||||
pendingGraphUpdates.Enqueue(guo);
|
||||
AstarPath.active.UpdateGraphs(guo);
|
||||
prevEnabled = false;
|
||||
}
|
||||
|
||||
// Stop caring about pending graph updates if this object is disabled.
|
||||
// This avoids a memory leak since `Update` will never be called again to remove pending updates
|
||||
// that have been completed.
|
||||
pendingGraphUpdates.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the graphs around this object.
|
||||
/// Note: The graphs will not be updated immediately since the pathfinding threads need to be paused first.
|
||||
/// If you want to guarantee that the graphs have been updated then call <see cref="AstarPath.FlushGraphUpdates"/>
|
||||
/// after the call to this method.
|
||||
/// </summary>
|
||||
public void DoUpdateGraphs () {
|
||||
if (coll == null && coll2D == null) return;
|
||||
|
||||
// Required to ensure we get the most up to date bounding box from the physics engine
|
||||
UnityEngine.Physics.SyncTransforms();
|
||||
UnityEngine.Physics2D.SyncTransforms();
|
||||
|
||||
if (!colliderEnabled) {
|
||||
// If the collider is not enabled, then col.bounds will empty
|
||||
// so just update prevBounds
|
||||
var guo = new GraphUpdateObject(prevBounds);
|
||||
pendingGraphUpdates.Enqueue(guo);
|
||||
AstarPath.active.UpdateGraphs(guo);
|
||||
} else {
|
||||
Bounds newBounds = bounds;
|
||||
|
||||
Bounds merged = newBounds;
|
||||
merged.Encapsulate(prevBounds);
|
||||
|
||||
// Check what seems to be fastest, to update the union of prevBounds and newBounds in a single request
|
||||
// or to update them separately, the smallest volume is usually the fastest
|
||||
if (BoundsVolume(merged) < BoundsVolume(newBounds) + BoundsVolume(prevBounds)) {
|
||||
// Send an update request to update the nodes inside the 'merged' volume
|
||||
var guo = new GraphUpdateObject(merged);
|
||||
pendingGraphUpdates.Enqueue(guo);
|
||||
AstarPath.active.UpdateGraphs(guo);
|
||||
} else {
|
||||
// Send two update request to update the nodes inside the 'prevBounds' and 'newBounds' volumes
|
||||
var guo1 = new GraphUpdateObject(prevBounds);
|
||||
var guo2 = new GraphUpdateObject(newBounds);
|
||||
pendingGraphUpdates.Enqueue(guo1);
|
||||
pendingGraphUpdates.Enqueue(guo2);
|
||||
AstarPath.active.UpdateGraphs(guo1);
|
||||
AstarPath.active.UpdateGraphs(guo2);
|
||||
}
|
||||
|
||||
#if ASTARDEBUG
|
||||
Debug.DrawLine(prevBounds.min, prevBounds.max, Color.yellow);
|
||||
Debug.DrawLine(newBounds.min, newBounds.max, Color.red);
|
||||
#endif
|
||||
prevBounds = newBounds;
|
||||
}
|
||||
|
||||
prevEnabled = colliderEnabled;
|
||||
prevRotation = tr.rotation;
|
||||
|
||||
// Set this here as well since the DoUpdateGraphs method can be called from other scripts
|
||||
lastCheckTime = Time.realtimeSinceStartup;
|
||||
}
|
||||
|
||||
/// <summary>Volume of a Bounds object. X*Y*Z</summary>
|
||||
static float BoundsVolume (Bounds b) {
|
||||
return System.Math.Abs(b.size.x * b.size.y * b.size.z);
|
||||
}
|
||||
|
||||
// bool RecastMeshObj.enabled { get => enabled; set => enabled = value; }
|
||||
float DynamicGridObstacle.updateError { get => updateError; set => updateError = value; }
|
||||
float DynamicGridObstacle.checkTime { get => checkTime; set => checkTime = value; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates graphs around the object as it moves.
|
||||
/// Deprecated: Has been renamed to <see cref="DynamicObstacle"/>.
|
||||
/// </summary>
|
||||
[System.Obsolete("Has been renamed to DynamicObstacle")]
|
||||
public interface DynamicGridObstacle {
|
||||
bool enabled { get; set; }
|
||||
float updateError { get; set; }
|
||||
float checkTime { get; set; }
|
||||
void DoUpdateGraphs();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 183748dd497aa473a98ff0cd8cb67fc5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: -220
|
||||
icon: {fileID: 2800000, guid: edb61d815bfcfa548b5e947d759e9fb6, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
884
Packages/com.arongranberg.astar/Utilities/Funnel.cs
Normal file
884
Packages/com.arongranberg.astar/Utilities/Funnel.cs
Normal file
@@ -0,0 +1,884 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
using UnityEngine.Assertions;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.Pooling;
|
||||
using Pathfinding.Collections;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
/// <summary>
|
||||
/// Implements the funnel algorithm as well as various related methods.
|
||||
/// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html
|
||||
/// See: Usually you do not use this class directly. Instead use the <see cref="FunnelModifier"/> component.
|
||||
///
|
||||
/// <code>
|
||||
/// using UnityEngine;
|
||||
/// using Pathfinding;
|
||||
/// using Pathfinding.Drawing;
|
||||
///
|
||||
/// public class FunnelExample : MonoBehaviour {
|
||||
/// public Transform target = null;
|
||||
///
|
||||
/// void Update () {
|
||||
/// var path = ABPath.Construct(transform.position, target.position);
|
||||
///
|
||||
/// AstarPath.StartPath(path);
|
||||
/// path.BlockUntilCalculated();
|
||||
///
|
||||
/// // Apply some default adjustments to the path
|
||||
/// // not necessary if you are using the Seeker component
|
||||
/// new StartEndModifier().Apply(path);
|
||||
///
|
||||
/// // Split the path into segments and links
|
||||
/// var parts = Funnel.SplitIntoParts(path);
|
||||
/// // Optionally simplify the path to make it straighter
|
||||
/// var nodes = path.path;
|
||||
/// Funnel.Simplify(parts, ref nodes);
|
||||
///
|
||||
/// using (Draw.WithLineWidth(2)) {
|
||||
/// // Go through all the parts and draw them in the scene view
|
||||
/// for (int i = 0; i < parts.Count; i++) {
|
||||
/// var part = parts[i];
|
||||
/// if (part.type == Funnel.PartType.OffMeshLink) {
|
||||
/// // Draw off-mesh links as a single line
|
||||
/// Draw.Line(part.startPoint, part.endPoint, Color.cyan);
|
||||
/// } else {
|
||||
/// // Calculate the shortest path through the funnel
|
||||
/// var portals = Funnel.ConstructFunnelPortals(nodes, part);
|
||||
/// var pathThroghPortals = Funnel.Calculate(portals, splitAtEveryPortal: false);
|
||||
/// Draw.Polyline(pathThroghPortals, Color.black);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// In the image you can see the output from the code example above. The cyan lines represent off-mesh links.
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
public static class Funnel {
|
||||
/// <summary>Funnel in which the path to the target will be</summary>
|
||||
public struct FunnelPortals {
|
||||
public List<Vector3> left;
|
||||
public List<Vector3> right;
|
||||
}
|
||||
|
||||
/// <summary>The type of a <see cref="PathPart"/></summary>
|
||||
public enum PartType {
|
||||
/// <summary>An off-mesh link between two nodes in the same or different graphs</summary>
|
||||
OffMeshLink,
|
||||
/// <summary>A sequence of adjacent nodes in the same graph</summary>
|
||||
NodeSequence,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Part of a path.
|
||||
/// This is either a sequence of adjacent triangles
|
||||
/// or a link.
|
||||
/// See: NodeLink2
|
||||
/// </summary>
|
||||
public struct PathPart {
|
||||
/// <summary>Index of the first node in this part</summary>
|
||||
public int startIndex;
|
||||
/// <summary>Index of the last node in this part</summary>
|
||||
public int endIndex;
|
||||
/// <summary>Exact start-point of this part or off-mesh link</summary>
|
||||
public Vector3 startPoint;
|
||||
/// <summary>Exact end-point of this part or off-mesh link</summary>
|
||||
public Vector3 endPoint;
|
||||
/// <summary>If this is an off-mesh link or a sequence of nodes in a single graph</summary>
|
||||
public PartType type;
|
||||
}
|
||||
|
||||
/// <summary>Splits the path into a sequence of parts which are either off-mesh links or sequences of adjacent triangles</summary>
|
||||
|
||||
public static List<PathPart> SplitIntoParts (Path path) {
|
||||
var nodes = path.path;
|
||||
|
||||
var result = ListPool<PathPart>.Claim();
|
||||
|
||||
if (nodes == null || nodes.Count == 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Loop through the path and split it into
|
||||
// parts joined by links
|
||||
for (int i = 0; i < nodes.Count; i++) {
|
||||
var node = nodes[i];
|
||||
if (node is TriangleMeshNode || node is GridNodeBase) {
|
||||
var startIndex = i;
|
||||
uint currentGraphIndex = node.GraphIndex;
|
||||
|
||||
// Loop up until we find a node in another graph
|
||||
// Ignore NodeLink3 nodes
|
||||
while (i < nodes.Count && (nodes[i].GraphIndex == currentGraphIndex || nodes[i] is NodeLink3Node)) i++;
|
||||
|
||||
i--;
|
||||
var endIndex = i;
|
||||
result.Add(new PathPart {
|
||||
type = PartType.NodeSequence,
|
||||
startIndex = startIndex,
|
||||
endIndex = endIndex,
|
||||
// If this is the first part in the path, use the exact start point
|
||||
// otherwise use the position of the node right before the start of this
|
||||
// part which is likely the end of the link to this part
|
||||
startPoint = startIndex == 0 ? path.vectorPath[0] : (Vector3)nodes[startIndex-1].position,
|
||||
endPoint = endIndex == nodes.Count-1 ? path.vectorPath[path.vectorPath.Count-1] : (Vector3)nodes[endIndex+1].position,
|
||||
});
|
||||
} else if (node is LinkNode) {
|
||||
var startIndex = i;
|
||||
var currentGraphIndex = node.GraphIndex;
|
||||
|
||||
while (i < nodes.Count && nodes[i].GraphIndex == currentGraphIndex) i++;
|
||||
i--;
|
||||
|
||||
if (i - startIndex == 0) {
|
||||
// The link is a single node.
|
||||
// Just ignore it. It can happen in very rare circumstances with some path types.
|
||||
// For example, a RandomPath can stop at the first node of a node link, without including the other end of the link.
|
||||
|
||||
if (startIndex > 0 && startIndex + 1 < nodes.Count && nodes[startIndex - 1] == nodes[startIndex + 1]) {
|
||||
// We can also move to a node link node and then immediately move back to the previous node in rare circumstances.
|
||||
// Since triangle nodes are represented as 3 nodes during pathfinding, this is a possibility.
|
||||
// (TODO: How can this happen in practice? It has been empirically observed on a standard graph, but the edge costs must be kinda weird for it to happen?)
|
||||
|
||||
// [A, LinkNode, A] => [A]
|
||||
nodes.RemoveRange(startIndex, 2);
|
||||
i--;
|
||||
throw new System.Exception("Link node connected back to the previous node in the path. This should not happen.");
|
||||
} else {
|
||||
// [A, LinkNode] => [A]
|
||||
// [LinkNode, A] => [A]
|
||||
Assert.IsTrue(startIndex == 0 || startIndex == nodes.Count - 1);
|
||||
nodes.RemoveAt(startIndex);
|
||||
i--;
|
||||
}
|
||||
|
||||
continue;
|
||||
} else if (i - startIndex != 1) {
|
||||
throw new System.Exception("Off mesh link included more than two nodes: " + (i - startIndex + 1));
|
||||
}
|
||||
|
||||
result.Add(new PathPart {
|
||||
type = PartType.OffMeshLink,
|
||||
startIndex = startIndex,
|
||||
endIndex = i,
|
||||
startPoint = (Vector3)nodes[startIndex].position,
|
||||
endPoint = (Vector3)nodes[i].position,
|
||||
});
|
||||
} else {
|
||||
throw new System.Exception("Unsupported node type or null node");
|
||||
}
|
||||
}
|
||||
|
||||
// The path should always start and stop on regular nodes
|
||||
if (result[0].type == PartType.OffMeshLink) {
|
||||
result.RemoveAt(0);
|
||||
}
|
||||
if (result[result.Count - 1].type == PartType.OffMeshLink) {
|
||||
result.RemoveAt(result.Count - 1);
|
||||
}
|
||||
|
||||
Assert.IsTrue(result.Count > 0);
|
||||
Assert.AreEqual(result[0].startIndex, 0);
|
||||
Assert.AreEqual(result[0].type, PartType.NodeSequence);
|
||||
Assert.AreEqual(result[result.Count-1].type, PartType.NodeSequence);
|
||||
Assert.AreEqual(result[result.Count-1].endIndex, nodes.Count - 1);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public static void Simplify (List<PathPart> parts, ref List<GraphNode> nodes) {
|
||||
List<GraphNode> resultNodes = ListPool<GraphNode>.Claim();
|
||||
|
||||
for (int i = 0; i < parts.Count; i++) {
|
||||
var part = parts[i];
|
||||
|
||||
// We are changing the nodes list, so indices may change
|
||||
var newPart = part;
|
||||
newPart.startIndex = resultNodes.Count;
|
||||
|
||||
if (part.type == PartType.NodeSequence) {
|
||||
if (nodes[part.startIndex].Graph is IRaycastableGraph graph) {
|
||||
Simplify(part, graph, nodes, resultNodes, Path.ZeroTagPenalties, -1);
|
||||
newPart.endIndex = resultNodes.Count - 1;
|
||||
parts[i] = newPart;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = part.startIndex; j <= part.endIndex; j++) {
|
||||
resultNodes.Add(nodes[j]);
|
||||
}
|
||||
newPart.endIndex = resultNodes.Count - 1;
|
||||
parts[i] = newPart;
|
||||
}
|
||||
|
||||
ListPool<GraphNode>.Release(ref nodes);
|
||||
nodes = resultNodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simplifies a funnel path using linecasting.
|
||||
/// Running time is roughly O(n^2 log n) in the worst case (where n = end-start)
|
||||
/// Actually it depends on how the graph looks, so in theory the actual upper limit on the worst case running time is O(n*m log n) (where n = end-start and m = nodes in the graph)
|
||||
/// but O(n^2 log n) is a much more realistic worst case limit.
|
||||
///
|
||||
/// Requires graph to implement IRaycastableGraph
|
||||
/// </summary>
|
||||
public static void Simplify (PathPart part, IRaycastableGraph graph, List<GraphNode> nodes, List<GraphNode> result, int[] tagPenalties, int traversableTags) {
|
||||
var start = part.startIndex;
|
||||
var end = part.endIndex;
|
||||
var startPoint = part.startPoint;
|
||||
var endPoint = part.endPoint;
|
||||
|
||||
if (graph == null) throw new System.ArgumentNullException(nameof(graph));
|
||||
|
||||
if (start > end) {
|
||||
throw new System.ArgumentException("start > end");
|
||||
}
|
||||
|
||||
// Do a straight line of sight check to see if the path can be simplified to a single line
|
||||
{
|
||||
if (!graph.Linecast(startPoint, endPoint, out GraphHitInfo hit) && hit.node == nodes[end]) {
|
||||
graph.Linecast(startPoint, endPoint, out hit, result);
|
||||
|
||||
long penaltySum = 0;
|
||||
long penaltySum2 = 0;
|
||||
for (int i = start; i <= end; i++) {
|
||||
penaltySum += nodes[i].Penalty + tagPenalties[nodes[i].Tag];
|
||||
}
|
||||
|
||||
bool walkable = true;
|
||||
for (int i = 0; i < result.Count; i++) {
|
||||
penaltySum2 += result[i].Penalty + tagPenalties[result[i].Tag];
|
||||
walkable &= ((traversableTags >> (int)result[i].Tag) & 1) == 1;
|
||||
}
|
||||
|
||||
// Allow 40% more penalty on average per node
|
||||
if (!walkable || (penaltySum*1.4*result.Count) < (penaltySum2*(end-start+1))) {
|
||||
// The straight line penalties are much higher than the original path.
|
||||
// Revert the simplification
|
||||
result.Clear();
|
||||
} else {
|
||||
// The straight line simplification looks good.
|
||||
// We are done here.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int ostart = start;
|
||||
|
||||
int count = 0;
|
||||
while (true) {
|
||||
if (count++ > 1000) {
|
||||
Debug.LogError("Was the path really long or have we got cought in an infinite loop?");
|
||||
break;
|
||||
}
|
||||
|
||||
if (start == end) {
|
||||
result.Add(nodes[end]);
|
||||
return;
|
||||
}
|
||||
|
||||
int resCount = result.Count;
|
||||
|
||||
// Run a binary search to find the furthest node that we have a clear line of sight to
|
||||
int mx = end+1;
|
||||
int mn = start+1;
|
||||
bool anySucceded = false;
|
||||
while (mx > mn+1) {
|
||||
int mid = (mx+mn)/2;
|
||||
|
||||
Vector3 sp = start == ostart ? startPoint : (Vector3)nodes[start].position;
|
||||
Vector3 ep = mid == end ? endPoint : (Vector3)nodes[mid].position;
|
||||
|
||||
// Check if there is an obstacle between these points, or if there is no obstacle, but we didn't end up at the right node.
|
||||
// The second case can happen for example in buildings with multiple floors.
|
||||
if (graph.Linecast(sp, ep, out GraphHitInfo hit) || hit.node != nodes[mid]) {
|
||||
mx = mid;
|
||||
} else {
|
||||
anySucceded = true;
|
||||
mn = mid;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anySucceded) {
|
||||
result.Add(nodes[start]);
|
||||
|
||||
// It is guaranteed that mn = start+1
|
||||
start = mn;
|
||||
} else {
|
||||
// Replace a part of the path with the straight path to the furthest node we had line of sight to.
|
||||
// Need to redo the linecast to get the trace (i.e. list of nodes along the line of sight).
|
||||
Vector3 sp = start == ostart ? startPoint : (Vector3)nodes[start].position;
|
||||
Vector3 ep = mn == end ? endPoint : (Vector3)nodes[mn].position;
|
||||
graph.Linecast(sp, ep, out _, result);
|
||||
|
||||
long penaltySum = 0;
|
||||
long penaltySum2 = 0;
|
||||
for (int i = start; i <= mn; i++) {
|
||||
penaltySum += nodes[i].Penalty + tagPenalties[nodes[i].Tag];
|
||||
}
|
||||
|
||||
bool walkable = true;
|
||||
for (int i = resCount; i < result.Count; i++) {
|
||||
penaltySum2 += result[i].Penalty + tagPenalties[result[i].Tag];
|
||||
walkable &= ((traversableTags >> (int)result[i].Tag) & 1) == 1;
|
||||
}
|
||||
|
||||
// Allow 40% more penalty on average per node
|
||||
if (!walkable || (penaltySum*1.4*(result.Count-resCount)) < (penaltySum2*(mn-start+1)) || result[result.Count-1] != nodes[mn]) {
|
||||
// Linecast hit the wrong node or it is a lot more expensive than the original path
|
||||
result.RemoveRange(resCount, result.Count-resCount);
|
||||
|
||||
result.Add(nodes[start]);
|
||||
start++;
|
||||
} else {
|
||||
//Remove nodes[end]
|
||||
result.RemoveAt(result.Count-1);
|
||||
start = mn;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static FunnelPortals ConstructFunnelPortals (List<GraphNode> nodes, PathPart part) {
|
||||
if (nodes == null || nodes.Count == 0) {
|
||||
return new FunnelPortals { left = ListPool<Vector3>.Claim(0), right = ListPool<Vector3>.Claim(0) };
|
||||
}
|
||||
|
||||
if (part.endIndex < part.startIndex || part.startIndex < 0 || part.endIndex > nodes.Count) throw new System.ArgumentOutOfRangeException();
|
||||
|
||||
// Claim temporary lists and try to find lists with a high capacity
|
||||
var left = ListPool<Vector3>.Claim(nodes.Count+1);
|
||||
var right = ListPool<Vector3>.Claim(nodes.Count+1);
|
||||
|
||||
// Add start point
|
||||
left.Add(part.startPoint);
|
||||
right.Add(part.startPoint);
|
||||
|
||||
// Loop through all nodes in the path (except the last one)
|
||||
for (int i = part.startIndex; i < part.endIndex; i++) {
|
||||
// Get the portal between path[i] and path[i+1] and add it to the left and right lists
|
||||
if (nodes[i].GetPortal(nodes[i+1], out var lp, out var rp)) {
|
||||
left.Add(lp);
|
||||
right.Add(rp);
|
||||
} else {
|
||||
// Fallback, just use the positions of the nodes
|
||||
left.Add((Vector3)nodes[i].position);
|
||||
right.Add((Vector3)nodes[i].position);
|
||||
|
||||
left.Add((Vector3)nodes[i+1].position);
|
||||
right.Add((Vector3)nodes[i+1].position);
|
||||
}
|
||||
}
|
||||
|
||||
// Add end point
|
||||
left.Add(part.endPoint);
|
||||
right.Add(part.endPoint);
|
||||
|
||||
return new FunnelPortals { left = left, right = right };
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public struct FunnelState {
|
||||
/// <summary>Left side of the funnel</summary>
|
||||
public NativeCircularBuffer<float3> leftFunnel;
|
||||
/// <summary>Right side of the funnel</summary>
|
||||
public NativeCircularBuffer<float3> rightFunnel;
|
||||
/// <summary>
|
||||
/// Unwrapped version of the funnel portals in 2D space.
|
||||
///
|
||||
/// The input is a funnel like in the image below. It may be rotated and twisted.
|
||||
/// [Open online documentation to see images]
|
||||
/// The output will be a funnel in 2D space like in the image below. All twists and bends will have been straightened out.
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// This array is used as a cache and the unwrapped portals are calculated on demand. Thus it may not contain all portals.
|
||||
/// </summary>
|
||||
public NativeCircularBuffer<float4> unwrappedPortals;
|
||||
|
||||
/// <summary>
|
||||
/// If set to anything other than (0,0,0), then all portals will be projected on a plane with this normal.
|
||||
///
|
||||
/// This is used to make the funnel fit a rotated graph better.
|
||||
/// It is ideally used for grid graphs, but navmesh/recast graphs are probably better off with it set to zero.
|
||||
///
|
||||
/// The vector should be normalized (unless zero), in world space, and should never be changed after the first portal has been added (unless the funnel is cleared first).
|
||||
/// </summary>
|
||||
public float3 projectionAxis;
|
||||
|
||||
|
||||
public FunnelState(int initialCapacity, Allocator allocator) {
|
||||
leftFunnel = new NativeCircularBuffer<float3>(initialCapacity, allocator);
|
||||
rightFunnel = new NativeCircularBuffer<float3>(initialCapacity, allocator);
|
||||
unwrappedPortals = new NativeCircularBuffer<float4>(initialCapacity, allocator);
|
||||
projectionAxis = float3.zero;
|
||||
}
|
||||
|
||||
public FunnelState(FunnelPortals portals, Allocator allocator) : this(portals.left.Count, allocator) {
|
||||
if (portals.left.Count != portals.right.Count) throw new System.ArgumentException("portals.left.Count != portals.right.Count");
|
||||
for (int i = 0; i < portals.left.Count; i++) {
|
||||
PushEnd(portals.left[i], portals.right[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public FunnelState Clone () {
|
||||
return new FunnelState {
|
||||
leftFunnel = leftFunnel.Clone(),
|
||||
rightFunnel = rightFunnel.Clone(),
|
||||
unwrappedPortals = unwrappedPortals.Clone(),
|
||||
projectionAxis = projectionAxis,
|
||||
};
|
||||
}
|
||||
|
||||
public void Clear () {
|
||||
leftFunnel.Clear();
|
||||
rightFunnel.Clear();
|
||||
unwrappedPortals.Clear();
|
||||
projectionAxis = float3.zero;
|
||||
}
|
||||
|
||||
public void PopStart () {
|
||||
leftFunnel.PopStart();
|
||||
rightFunnel.PopStart();
|
||||
if (unwrappedPortals.Length > 0) unwrappedPortals.PopStart();
|
||||
}
|
||||
|
||||
public void PopEnd () {
|
||||
leftFunnel.PopEnd();
|
||||
rightFunnel.PopEnd();
|
||||
unwrappedPortals.TrimTo(leftFunnel.Length);
|
||||
}
|
||||
|
||||
public void Pop (bool fromStart) {
|
||||
if (fromStart) PopStart();
|
||||
else PopEnd();
|
||||
}
|
||||
|
||||
public void PushStart (float3 newLeftPortal, float3 newRightPortal) {
|
||||
PushStart(ref leftFunnel, ref rightFunnel, ref unwrappedPortals, ref newLeftPortal, ref newRightPortal, ref projectionAxis);
|
||||
}
|
||||
|
||||
/// <summary>True if a and b lie on different sides of the infinite line that passes through start and end</summary>
|
||||
static bool DifferentSidesOfLine (float3 start, float3 end, float3 a, float3 b) {
|
||||
var portal = math.normalizesafe(end - start);
|
||||
var d1 = a - start;
|
||||
var d2 = b - start;
|
||||
d1 -= portal * math.dot(d1, portal);
|
||||
d2 -= portal * math.dot(d2, portal);
|
||||
return math.dot(d1, d2) < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if it is reasonable that the given start point has passed the first portal in the funnel.
|
||||
///
|
||||
/// If this is true, it is most likely better to pop the start/end portal of the funnel first.
|
||||
///
|
||||
/// This can be used as a heuristic to determine if the agent has passed a portal and we should pop it,
|
||||
/// in those cases when node information is not available (e.g. because the path has been invalidated).
|
||||
/// </summary>
|
||||
public bool IsReasonableToPopStart (float3 startPoint, float3 endPoint) {
|
||||
if (leftFunnel.Length == 0) return false;
|
||||
|
||||
var reference = 1;
|
||||
while (reference < leftFunnel.Length && VectorMath.IsColinear(leftFunnel.First, rightFunnel.First, leftFunnel[reference])) {
|
||||
reference++;
|
||||
}
|
||||
return !DifferentSidesOfLine(leftFunnel.First, rightFunnel.First, startPoint, reference < leftFunnel.Length ? leftFunnel[reference] : endPoint);
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="IsReasonableToPopStart"/> but for the end of the funnel</summary>
|
||||
public bool IsReasonableToPopEnd (float3 startPoint, float3 endPoint) {
|
||||
if (leftFunnel.Length == 0) return false;
|
||||
|
||||
var reference = leftFunnel.Length - 1;
|
||||
while (reference >= 0 && VectorMath.IsColinear(leftFunnel.Last, rightFunnel.Last, leftFunnel[reference])) {
|
||||
reference--;
|
||||
}
|
||||
return !DifferentSidesOfLine(leftFunnel.Last, rightFunnel.Last, endPoint, reference >= 0 ? leftFunnel[reference] : startPoint);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
static void PushStart (ref NativeCircularBuffer<float3> leftPortals, ref NativeCircularBuffer<float3> rightPortals, ref NativeCircularBuffer<float4> unwrappedPortals, ref float3 newLeftPortal, ref float3 newRightPortal, ref float3 projectionAxis) {
|
||||
if (unwrappedPortals.Length == 0) {
|
||||
leftPortals.PushStart(newLeftPortal);
|
||||
rightPortals.PushStart(newRightPortal);
|
||||
return;
|
||||
}
|
||||
|
||||
var firstUnwrapped = unwrappedPortals.First;
|
||||
var unwrappedRight = Unwrap(leftPortals.First, rightPortals.First, firstUnwrapped.xy, firstUnwrapped.zw, newRightPortal, -1, projectionAxis);
|
||||
var unwrappedLeft = Unwrap(leftPortals.First, newRightPortal, firstUnwrapped.xy, unwrappedRight, newLeftPortal, -1, projectionAxis);
|
||||
leftPortals.PushStart(newLeftPortal);
|
||||
rightPortals.PushStart(newRightPortal);
|
||||
unwrappedPortals.PushStart(new float4(unwrappedLeft, unwrappedRight));
|
||||
}
|
||||
|
||||
public void Splice (int startIndex, int toRemove, List<float3> newLeftPortal, List<float3> newRightPortal) {
|
||||
this.leftFunnel.Splice(startIndex, toRemove, newLeftPortal);
|
||||
this.rightFunnel.Splice(startIndex, toRemove, newRightPortal);
|
||||
unwrappedPortals.TrimTo(startIndex);
|
||||
}
|
||||
|
||||
public void PushEnd (Vector3 newLeftPortal, Vector3 newRightPortal) {
|
||||
leftFunnel.PushEnd(newLeftPortal);
|
||||
rightFunnel.PushEnd(newRightPortal);
|
||||
}
|
||||
|
||||
public void Push (bool toStart, Vector3 newLeftPortal, Vector3 newRightPortal) {
|
||||
if (toStart) PushStart(newLeftPortal, newRightPortal);
|
||||
else PushEnd(newLeftPortal, newRightPortal);
|
||||
}
|
||||
|
||||
public void Dispose () {
|
||||
leftFunnel.Dispose();
|
||||
rightFunnel.Dispose();
|
||||
unwrappedPortals.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the shortest path through the funnel.
|
||||
///
|
||||
/// Returns: The number of corners added to the result array.
|
||||
///
|
||||
/// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html
|
||||
/// </summary>
|
||||
/// <param name="maxCorners">The maximum number of corners to add to the result array. Should be positive.</param>
|
||||
/// <param name="result">Output indices. Contains an index as well as possibly the \reflink{RightSideBit} set. Corresponds to an index of the left or right portals, depending on if \reflink{RightSideBit} is set. This must point to an array which is at least maxCorners long.</param>
|
||||
/// <param name="startPoint">Start point of the funnel. The agent will move from here to the best point along the first portal.</param>
|
||||
/// <param name="endPoint">End point of the funnel.</param>
|
||||
/// <param name="lastCorner">True if the final corner of the path was reached. If true, then the return value is guaranteed to be at most maxCorners - 1 (unless maxCorners = 0).</param>
|
||||
public int CalculateNextCornerIndices (int maxCorners, NativeArray<int> result, float3 startPoint, float3 endPoint, out bool lastCorner) {
|
||||
Assert.AreEqual(leftFunnel.Length, rightFunnel.Length);
|
||||
Assert.IsTrue(unwrappedPortals.Length <= leftFunnel.Length);
|
||||
if (result.Length < math.min(maxCorners, leftFunnel.Length)) throw new System.ArgumentException("result array may not be large enough to hold all corners");
|
||||
|
||||
unsafe {
|
||||
// TODO: Pass this as ref instead?
|
||||
var resultsSpan = result.AsUnsafeSpan();
|
||||
return Calculate(ref unwrappedPortals, ref leftFunnel, ref rightFunnel, ref startPoint, ref endPoint, ref resultsSpan, maxCorners, ref projectionAxis, out lastCorner);
|
||||
}
|
||||
}
|
||||
|
||||
public void CalculateNextCorners (int maxCorners, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, NativeList<float3> result) {
|
||||
var indices = new NativeArray<int>(math.min(maxCorners, leftFunnel.Length), Allocator.Temp);
|
||||
var numCorners = CalculateNextCornerIndices(maxCorners, indices, startPoint, endPoint, out bool lastCorner);
|
||||
ConvertCornerIndicesToPath(indices, numCorners, splitAtEveryPortal, startPoint, endPoint, lastCorner, result);
|
||||
indices.Dispose();
|
||||
}
|
||||
|
||||
public void ConvertCornerIndicesToPath (NativeArray<int> indices, int numCorners, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, bool lastCorner, NativeList<float3> result) {
|
||||
if (result.Capacity < numCorners) result.Capacity = numCorners;
|
||||
|
||||
Assert.IsTrue(numCorners == 0 || (indices[numCorners-1] & FunnelPortalIndexMask) < unwrappedPortals.Length);
|
||||
result.Add(startPoint);
|
||||
if (leftFunnel.Length == 0) {
|
||||
if (lastCorner) result.Add(endPoint);
|
||||
return;
|
||||
}
|
||||
|
||||
if (splitAtEveryPortal) {
|
||||
float2 prev2D = Unwrap(leftFunnel[0], rightFunnel[0], unwrappedPortals[0].xy, unwrappedPortals[0].zw, startPoint, -1, projectionAxis);
|
||||
var prevIdx = 0;
|
||||
for (int i = 0; i < numCorners; i++) {
|
||||
var idx = indices[i] & FunnelPortalIndexMask;
|
||||
var rightSide = (indices[i] & RightSideBit) != 0;
|
||||
// Check intersections with every portal segment
|
||||
float2 next2D = rightSide ? unwrappedPortals[idx].zw : unwrappedPortals[idx].xy;
|
||||
CalculatePortalIntersections(prevIdx + 1, idx - 1, leftFunnel, rightFunnel, unwrappedPortals, prev2D, next2D, result);
|
||||
prevIdx = math.abs(idx);
|
||||
prev2D = next2D;
|
||||
|
||||
result.Add(rightSide ? rightFunnel[idx] : leftFunnel[idx]);
|
||||
}
|
||||
if (lastCorner) {
|
||||
var next2D = Unwrap(leftFunnel.Last, rightFunnel.Last, unwrappedPortals.Last.xy, unwrappedPortals.Last.zw, endPoint, 1, projectionAxis);
|
||||
CalculatePortalIntersections(prevIdx + 1, unwrappedPortals.Length - 1, leftFunnel, rightFunnel, unwrappedPortals, prev2D, next2D, result);
|
||||
result.Add(endPoint);
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < numCorners; i++) {
|
||||
var idx = indices[i];
|
||||
result.Add((idx & RightSideBit) != 0 ? rightFunnel[idx & FunnelPortalIndexMask] : leftFunnel[idx & FunnelPortalIndexMask]);
|
||||
}
|
||||
if (lastCorner) result.Add(endPoint);
|
||||
}
|
||||
}
|
||||
|
||||
public void ConvertCornerIndicesToPathProjected (UnsafeSpan<int> indices, bool splitAtEveryPortal, float3 startPoint, float3 endPoint, bool lastCorner, NativeList<float3> result, float3 up) {
|
||||
var resultCount = indices.Length + 1 + (lastCorner ? 1 : 0);
|
||||
if (result.Capacity < resultCount) result.Capacity = resultCount;
|
||||
result.ResizeUninitialized(resultCount);
|
||||
var resultSpan = result.AsUnsafeSpan();
|
||||
ConvertCornerIndicesToPathProjected(ref this, ref indices, splitAtEveryPortal, in startPoint, in endPoint, lastCorner, in projectionAxis, ref resultSpan, in up);
|
||||
}
|
||||
|
||||
public float4x3 UnwrappedPortalsToWorldMatrix (float3 up) {
|
||||
int startIndex = 0;
|
||||
while (startIndex < unwrappedPortals.Length && math.lengthsq(unwrappedPortals[startIndex].xy - unwrappedPortals[startIndex].zw) <= 0.00001f) startIndex++;
|
||||
if (startIndex >= unwrappedPortals.Length) return new float4x3(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1);
|
||||
var left2D = unwrappedPortals[startIndex].xy;
|
||||
var right2D = unwrappedPortals[startIndex].zw;
|
||||
var left3D = leftFunnel[startIndex];
|
||||
var right3D = rightFunnel[startIndex];
|
||||
var portal2D = right2D - left2D;
|
||||
var portal3D = right3D - left3D;
|
||||
var portal2DInv = portal2D * math.rcp(math.lengthsq(portal2D));
|
||||
// Matrix to rotate unwrapped portals so that portal2D maps to the x-axis (1,0)
|
||||
var mr = new float2x2(
|
||||
new float2(portal2DInv.x, -portal2DInv.y),
|
||||
new float2(portal2DInv.y, portal2DInv.x)
|
||||
);
|
||||
|
||||
// Matrix to transform points in unwrapped-portal-space so left2D maps to (0,0) and right2D maps to (1,0)
|
||||
var offset = math.mul(mr, -left2D);
|
||||
var m1 = new float4x3(
|
||||
new float4(mr.c0.x, 0, mr.c0.y, 0),
|
||||
new float4(mr.c1.x, 0, mr.c1.y, 0),
|
||||
new float4(offset.x, 0, offset.y, 1)
|
||||
);
|
||||
|
||||
// Matrix that maps (0,0,0) to left3D and (1,0,0) to right3D, as well as (0,1,0) to up.
|
||||
var m2 = new float4x4(
|
||||
new float4(portal3D, 0),
|
||||
new float4(up, 0),
|
||||
new float4(math.cross(portal3D, up), 0),
|
||||
new float4(left3D, 1)
|
||||
);
|
||||
|
||||
// Matrix to transform points in unwrapped-portal-space to 3D space. Such that left2D maps to left3D.
|
||||
return math.mul(m2, m1);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public static void ConvertCornerIndicesToPathProjected (ref FunnelState funnelState, ref UnsafeSpan<int> indices, bool splitAtEveryPortal, in float3 startPoint, in float3 endPoint, bool lastCorner, in float3 projectionAxis, ref UnsafeSpan<float3> result, in float3 up) {
|
||||
Assert.IsTrue(indices.Length == 0 || (indices[indices.Length-1] & FunnelPortalIndexMask) < funnelState.unwrappedPortals.Length);
|
||||
int resultIndex = 0;
|
||||
result[resultIndex++] = startPoint;
|
||||
if (funnelState.leftFunnel.Length == 0) {
|
||||
if (lastCorner) result[resultIndex++] = endPoint;
|
||||
Assert.AreEqual(resultIndex, result.Length);
|
||||
return;
|
||||
}
|
||||
|
||||
var unwrappedToWorld = funnelState.UnwrappedPortalsToWorldMatrix(up);
|
||||
|
||||
if (splitAtEveryPortal) {
|
||||
throw new System.NotImplementedException();
|
||||
} else {
|
||||
for (int i = 0; i < indices.Length; i++) {
|
||||
var idx = indices[i];
|
||||
var corner = (idx & RightSideBit) != 0 ? funnelState.unwrappedPortals[idx & FunnelPortalIndexMask].zw : funnelState.unwrappedPortals[idx & FunnelPortalIndexMask].xy;
|
||||
result[resultIndex++] = math.mul(unwrappedToWorld, new float3(corner, 1)).xyz;
|
||||
}
|
||||
if (lastCorner) {
|
||||
float2 endPoint2D = Unwrap(funnelState.leftFunnel.Last, funnelState.rightFunnel.Last, funnelState.unwrappedPortals.Last.xy, funnelState.unwrappedPortals.Last.zw, endPoint, 1, projectionAxis);
|
||||
result[resultIndex++] = math.mul(unwrappedToWorld, new float3(endPoint2D, 1)).xyz;
|
||||
}
|
||||
}
|
||||
Assert.AreEqual(resultIndex, result.Length);
|
||||
}
|
||||
|
||||
static void CalculatePortalIntersections (int startIndex, int endIndex, NativeCircularBuffer<float3> leftPortals, NativeCircularBuffer<float3> rightPortals, NativeCircularBuffer<float4> unwrappedPortals, float2 from, float2 to, NativeList<float3> result) {
|
||||
for (int j = startIndex; j < endIndex; j++) {
|
||||
var portal = unwrappedPortals[j];
|
||||
var left = portal.xy;
|
||||
var right = portal.zw;
|
||||
if (!VectorMath.LineLineIntersectionFactor(left, right - left, from, to - from, out float factor)) {
|
||||
// This really shouldn't happen
|
||||
factor = 0.5f;
|
||||
}
|
||||
result.Add(math.lerp(leftPortals[j], rightPortals[j], factor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float2 Unwrap (float3 leftPortal, float3 rightPortal, float2 leftUnwrappedPortal, float2 rightUnwrappedPortal, float3 point, float sideMultiplier, float3 projectionAxis) {
|
||||
// TODO: On grid graphs this is kind of a weird way to do it.
|
||||
// We project all points onto a plane and then unwrap them.
|
||||
// It would be faster (and possibly more numerically accurate) to transform the points to graph space and then just use the xz coordinates.
|
||||
// This branch is extremely well predicted, since it will always be true for grid graphs, and always false for other graphs.
|
||||
if (!math.all(projectionAxis == 0)) {
|
||||
leftPortal -= projectionAxis * math.dot(leftPortal, projectionAxis);
|
||||
rightPortal -= projectionAxis * math.dot(rightPortal, projectionAxis);
|
||||
point -= projectionAxis * math.dot(point, projectionAxis);
|
||||
}
|
||||
|
||||
var portal = rightPortal - leftPortal;
|
||||
var portalLengthInvSq = 1.0f / math.lengthsq(portal);
|
||||
if (float.IsPositiveInfinity(portalLengthInvSq)) {
|
||||
return leftUnwrappedPortal + new float2(-math.length(point - leftPortal), 0);
|
||||
}
|
||||
var distance = math.length(math.cross(point - leftPortal, portal)) * portalLengthInvSq;
|
||||
var projection = math.dot(point - leftPortal, portal) * portalLengthInvSq;
|
||||
|
||||
// Weld corner vertices if they are close enough.
|
||||
// This is important for grid graphs, as if the unwrapped portals are not quite identical in the corners,
|
||||
// the grid simplification may fail to remove inner corners. This is because it will detect 2+ almost identical corners in each turn, instead of 1.
|
||||
// TODO: Unwrap grid portals in a different way. It really can be done much faster and more numerically robustly.
|
||||
// We should not use graph space directly, though, as grid graphs can move around (ProceduralGraphMover).
|
||||
if (distance < 0.002f) {
|
||||
if (math.abs(projection) < 0.002f) {
|
||||
return leftUnwrappedPortal;
|
||||
} else if (math.abs(projection - 1) < 0.002f) {
|
||||
return rightUnwrappedPortal;
|
||||
}
|
||||
}
|
||||
|
||||
var unwrappedPortal = rightUnwrappedPortal - leftUnwrappedPortal;
|
||||
var unwrappedNormal = new float2(-unwrappedPortal.y, unwrappedPortal.x);
|
||||
return leftUnwrappedPortal + math.mad(unwrappedPortal, projection, unwrappedNormal * (distance * sideMultiplier));
|
||||
}
|
||||
|
||||
/// <summary>True if b is to the right of or on the line from (0,0) to a</summary>
|
||||
private static bool RightOrColinear (Vector2 a, Vector2 b) {
|
||||
return (a.x*b.y - b.x*a.y) <= 0;
|
||||
}
|
||||
|
||||
/// <summary>True if b is to the left of or on the line from (0,0) to a</summary>
|
||||
private static bool LeftOrColinear (Vector2 a, Vector2 b) {
|
||||
return (a.x*b.y - b.x*a.y) >= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the shortest path through the funnel.
|
||||
///
|
||||
/// The path will be unwrapped into 2D space before the funnel algorithm runs.
|
||||
/// This makes it possible to support the funnel algorithm in XY space as well as in more complicated cases, such as on curved worlds.
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// See: Unwrap
|
||||
/// </summary>
|
||||
/// <param name="funnel">The portals of the funnel. The first and last vertices portals must be single points (so for example left[0] == right[0]).</param>
|
||||
/// <param name="splitAtEveryPortal">If true, then a vertex will be inserted every time the path crosses a portal
|
||||
/// instead of only at the corners of the path. The result will have exactly one vertex per portal if this is enabled.
|
||||
/// This may introduce vertices with the same position in the output (esp. in corners where many portals meet).</param>
|
||||
public static List<Vector3> Calculate (FunnelPortals funnel, bool splitAtEveryPortal) {
|
||||
var state = new FunnelState(funnel, Allocator.Temp);
|
||||
var startPoint = state.leftFunnel.First;
|
||||
var endPoint = state.leftFunnel.Last;
|
||||
state.PopStart();
|
||||
state.PopEnd();
|
||||
var nativeResult = new NativeList<float3>(Allocator.Temp);
|
||||
state.CalculateNextCorners(int.MaxValue, splitAtEveryPortal, startPoint, endPoint, nativeResult);
|
||||
state.Dispose();
|
||||
var result = ListPool<Vector3>.Claim(nativeResult.Length);
|
||||
for (int i = 0; i < nativeResult.Length; i++) result.Add((Vector3)nativeResult[i]);
|
||||
nativeResult.Dispose();
|
||||
return result;
|
||||
}
|
||||
|
||||
public const int RightSideBit = 1 << 30;
|
||||
public const int FunnelPortalIndexMask = RightSideBit - 1;
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the shortest path through the funnel.
|
||||
///
|
||||
/// Returns: The number of corners added to the funnelPath array.
|
||||
///
|
||||
/// See: http://digestingduck.blogspot.se/2010/03/simple-stupid-funnel-algorithm.html
|
||||
/// </summary>
|
||||
/// <param name="leftPortals">Left side of the funnel. Should not contain the start point.</param>
|
||||
/// <param name="rightPortals">Right side of the funnel. Should not contain the end point.</param>
|
||||
/// <param name="unwrappedPortals">Cache of unwrapped portal segments. This may be empty, but it will be filled with unwrapped portals and next time you run the algorithm it will be faster.</param>
|
||||
/// <param name="startPoint">Start point of the funnel. The agent will move from here to the best point between leftPortals[0] and rightPortals[0].</param>
|
||||
/// <param name="endPoint">End point of the funnel.</param>
|
||||
/// <param name="funnelPath">Output indices. Contains an index as well as possibly the \reflink{RightSideBit} set. Corresponds to an index into leftPortals or rightPortals depending on if \reflink{RightSideBit} is set. This must point to an array which is at least maxCorners long.</param>
|
||||
/// <param name="lastCorner">True if the final corner of the path was reached. If true, then the return value is guaranteed to be at most maxCorners - 1 (unless maxCorners = 0).</param>
|
||||
/// <param name="maxCorners">The first N corners of the optimized path will be calculated. Calculating fewer corners is faster. Pass int.MaxValue if you want to calculate all corners.</param>
|
||||
/// <param name="projectionAxis">If set to anything other than (0,0,0), then all portals will be projected on a plane with this normal.</param>
|
||||
[BurstCompile]
|
||||
static unsafe int Calculate (ref NativeCircularBuffer<float4> unwrappedPortals, ref NativeCircularBuffer<float3> leftPortals, ref NativeCircularBuffer<float3> rightPortals, ref float3 startPoint, ref float3 endPoint, ref UnsafeSpan<int> funnelPath, int maxCorners, ref float3 projectionAxis, out bool lastCorner) {
|
||||
lastCorner = false;
|
||||
if (leftPortals.Length <= 0) {
|
||||
lastCorner = true;
|
||||
return 0;
|
||||
}
|
||||
if (maxCorners <= 0) return 0;
|
||||
|
||||
int apexIndex = 0;
|
||||
int rightIndex = 0;
|
||||
int leftIndex = 0;
|
||||
|
||||
int outputCount = 0;
|
||||
|
||||
if (unwrappedPortals.Length == 0) {
|
||||
unwrappedPortals.PushEnd(new float4(new float2(0, 0), new float2(math.length(rightPortals[0] - leftPortals[0]))));
|
||||
}
|
||||
|
||||
float2 portalApex = Unwrap(leftPortals[0], rightPortals[0], unwrappedPortals[0].xy, unwrappedPortals[0].zw, startPoint, -1, projectionAxis);
|
||||
float2 portalLeft = float2.zero;
|
||||
float2 portalRight = float2.zero;
|
||||
|
||||
for (int i = 0; i <= leftPortals.Length; i++) {
|
||||
// Unwrap the funnel on the fly as needed
|
||||
float2 rLeft, rRight;
|
||||
if (i == unwrappedPortals.Length) {
|
||||
if (i == leftPortals.Length) {
|
||||
// The end point of the path
|
||||
rLeft = rRight = Unwrap(leftPortals[i-1], rightPortals[i-1], unwrappedPortals[i-1].xy, unwrappedPortals[i-1].zw, endPoint, 1, projectionAxis) - portalApex;
|
||||
} else {
|
||||
// The funnel needs unwrapping
|
||||
var unwrappedLeft = Unwrap(leftPortals[i-1], rightPortals[i-1], unwrappedPortals[i-1].xy, unwrappedPortals[i-1].zw, leftPortals[i], 1, projectionAxis);
|
||||
var unwrappedRight = Unwrap(leftPortals[i], rightPortals[i-1], unwrappedLeft, unwrappedPortals[i-1].zw, rightPortals[i], 1, projectionAxis);
|
||||
unwrappedPortals.PushEnd(new float4(unwrappedLeft, unwrappedRight));
|
||||
rLeft = unwrappedLeft - portalApex;
|
||||
rRight = unwrappedRight - portalApex;
|
||||
}
|
||||
} else {
|
||||
// Common case
|
||||
rLeft = unwrappedPortals[i].xy - portalApex;
|
||||
rRight = unwrappedPortals[i].zw - portalApex;
|
||||
}
|
||||
|
||||
if (LeftOrColinear(portalRight, rRight)) {
|
||||
if (RightOrColinear(portalLeft, rRight)) {
|
||||
portalRight = rRight;
|
||||
rightIndex = i;
|
||||
} else {
|
||||
portalRight = portalLeft = float2.zero;
|
||||
i = apexIndex = rightIndex = leftIndex;
|
||||
portalApex = unwrappedPortals[i].xy;
|
||||
|
||||
funnelPath[outputCount++] = apexIndex;
|
||||
if (outputCount >= maxCorners) return outputCount;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (RightOrColinear(portalLeft, rLeft)) {
|
||||
if (LeftOrColinear(portalRight, rLeft)) {
|
||||
portalLeft = rLeft;
|
||||
leftIndex = i;
|
||||
} else {
|
||||
portalRight = portalLeft = float2.zero;
|
||||
i = apexIndex = leftIndex = rightIndex;
|
||||
portalApex = unwrappedPortals[i].zw;
|
||||
|
||||
funnelPath[outputCount++] = apexIndex | RightSideBit;
|
||||
if (outputCount >= maxCorners) return outputCount;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastCorner = true;
|
||||
return outputCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Packages/com.arongranberg.astar/Utilities/Funnel.cs.meta
Normal file
12
Packages/com.arongranberg.astar/Utilities/Funnel.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 332338376eb42424caa0e3d6bf984a8b
|
||||
timeCreated: 1488237337
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
265
Packages/com.arongranberg.astar/Utilities/GraphGizmoHelper.cs
Normal file
265
Packages/com.arongranberg.astar/Utilities/GraphGizmoHelper.cs
Normal file
@@ -0,0 +1,265 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Pathfinding.Util {
|
||||
using Pathfinding.Drawing;
|
||||
using Pathfinding.Collections;
|
||||
using Pathfinding.Pooling;
|
||||
|
||||
/// <summary>Combines hashes into a single hash value</summary>
|
||||
public struct NodeHasher {
|
||||
readonly bool includePathSearchInfo;
|
||||
readonly bool includeAreaInfo;
|
||||
readonly bool includeHierarchicalNodeInfo;
|
||||
readonly PathHandler debugData;
|
||||
public DrawingData.Hasher hasher;
|
||||
|
||||
public NodeHasher(AstarPath active) {
|
||||
hasher = default;
|
||||
this.debugData = active.debugPathData;
|
||||
includePathSearchInfo = debugData != null && (active.debugMode == GraphDebugMode.F || active.debugMode == GraphDebugMode.G || active.debugMode == GraphDebugMode.H || active.showSearchTree);
|
||||
includeAreaInfo = active.debugMode == GraphDebugMode.Areas;
|
||||
includeHierarchicalNodeInfo = active.debugMode == GraphDebugMode.HierarchicalNode;
|
||||
hasher.Add(active.debugMode);
|
||||
hasher.Add(active.debugFloor);
|
||||
hasher.Add(active.debugRoof);
|
||||
hasher.Add(active.showSearchTree);
|
||||
hasher.Add(AstarColor.ColorHash());
|
||||
}
|
||||
|
||||
public void HashNode (GraphNode node) {
|
||||
hasher.Add(node.GetGizmoHashCode());
|
||||
if (includeAreaInfo) hasher.Add((int)node.Area);
|
||||
if (includeHierarchicalNodeInfo) hasher.Add(node.HierarchicalNodeIndex);
|
||||
|
||||
if (includePathSearchInfo) {
|
||||
var pathNode = debugData.pathNodes[node.NodeIndex];
|
||||
hasher.Add(pathNode.pathID);
|
||||
hasher.Add(pathNode.pathID == debugData.PathID);
|
||||
// hasher.Add(pathNode.F);
|
||||
}
|
||||
}
|
||||
|
||||
public void Add<T>(T v) {
|
||||
hasher.Add(v);
|
||||
}
|
||||
|
||||
public static implicit operator DrawingData.Hasher(NodeHasher hasher) {
|
||||
return hasher.hasher;
|
||||
}
|
||||
}
|
||||
|
||||
public class GraphGizmoHelper : IAstarPooledObject, System.IDisposable {
|
||||
public DrawingData.Hasher hasher { get; private set; }
|
||||
PathHandler debugData;
|
||||
ushort debugPathID;
|
||||
GraphDebugMode debugMode;
|
||||
public bool showSearchTree;
|
||||
float debugFloor;
|
||||
float debugRoof;
|
||||
public CommandBuilder builder;
|
||||
Vector3 drawConnectionStart;
|
||||
Color drawConnectionColor;
|
||||
readonly System.Action<GraphNode> drawConnection;
|
||||
#if UNITY_EDITOR
|
||||
UnsafeSpan<GlobalNodeStorage.DebugPathNode> debugPathNodes;
|
||||
#endif
|
||||
GlobalNodeStorage nodeStorage;
|
||||
|
||||
public GraphGizmoHelper () {
|
||||
// Cache a delegate to avoid allocating memory for it every time
|
||||
drawConnection = DrawConnection;
|
||||
}
|
||||
|
||||
public static GraphGizmoHelper GetSingleFrameGizmoHelper (DrawingData gizmos, AstarPath active, RedrawScope redrawScope) {
|
||||
return GetGizmoHelper(gizmos, active, DrawingData.Hasher.NotSupplied, redrawScope);
|
||||
}
|
||||
|
||||
public static GraphGizmoHelper GetGizmoHelper (DrawingData gizmos, AstarPath active, DrawingData.Hasher hasher, RedrawScope redrawScope) {
|
||||
var helper = ObjectPool<GraphGizmoHelper>.Claim();
|
||||
|
||||
helper.Init(active, hasher, gizmos, redrawScope);
|
||||
return helper;
|
||||
}
|
||||
|
||||
public void Init (AstarPath active, DrawingData.Hasher hasher, DrawingData gizmos, RedrawScope redrawScope) {
|
||||
if (active != null) {
|
||||
debugData = active.debugPathData;
|
||||
debugPathID = active.debugPathID;
|
||||
debugMode = active.debugMode;
|
||||
debugFloor = active.debugFloor;
|
||||
debugRoof = active.debugRoof;
|
||||
nodeStorage = active.nodeStorage;
|
||||
#if UNITY_EDITOR
|
||||
if (debugData != null && debugData.threadID < active.nodeStorage.pathfindingThreadData.Length) debugPathNodes = active.nodeStorage.pathfindingThreadData[debugData.threadID].debugPathNodes;
|
||||
else debugPathNodes = default;
|
||||
showSearchTree = active.showSearchTree && debugPathNodes.Length > 0;
|
||||
#else
|
||||
showSearchTree = false;
|
||||
#endif
|
||||
}
|
||||
this.hasher = hasher;
|
||||
builder = gizmos.GetBuilder(hasher, redrawScope);
|
||||
}
|
||||
|
||||
public void OnEnterPool () {
|
||||
builder.Dispose();
|
||||
debugData = null;
|
||||
}
|
||||
|
||||
public void DrawConnections (GraphNode node) {
|
||||
if (showSearchTree) {
|
||||
#if UNITY_EDITOR
|
||||
if (debugPathNodes.Length > 0) {
|
||||
var nodeIndex = node.NodeIndex;
|
||||
var variants = (uint)node.PathNodeVariants;
|
||||
for (uint i = 0; i < variants; i++) {
|
||||
var pnode = debugPathNodes[nodeIndex + i];
|
||||
if (pnode.pathID == debugPathID) {
|
||||
if (pnode.parentIndex != 0 && debugPathNodes[pnode.parentIndex].pathID == debugPathID) {
|
||||
var parent = nodeStorage.GetNode(pnode.parentIndex);
|
||||
if (parent != null) {
|
||||
var nodePos = node.DecodeVariantPosition(nodeIndex + i, pnode.fractionAlongEdge);
|
||||
var parentPos = parent.DecodeVariantPosition(pnode.parentIndex, debugPathNodes[pnode.parentIndex].fractionAlongEdge);
|
||||
builder.Line((Vector3)parentPos, (Vector3)nodePos, NodeColor(node));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// Calculate which color to use for drawing the node
|
||||
// based on the settings specified in the editor
|
||||
drawConnectionColor = NodeColor(node);
|
||||
// Get the node position
|
||||
// Cast it here to avoid doing it for every neighbour
|
||||
drawConnectionStart = (Vector3)node.position;
|
||||
node.GetConnections(drawConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void DrawConnection (GraphNode other) {
|
||||
builder.Line(drawConnectionStart, ((Vector3)other.position + drawConnectionStart)*0.5f, drawConnectionColor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color to use for gizmos.
|
||||
/// Returns a color to be used for the specified node with the current debug settings (editor only).
|
||||
///
|
||||
/// Version: Since 3.6.1 this method will not handle null nodes
|
||||
/// </summary>
|
||||
public Color NodeColor (GraphNode node) {
|
||||
#if UNITY_EDITOR
|
||||
if (showSearchTree && !InSearchTree(node, debugPathNodes, debugPathID)) return Color.clear;
|
||||
#endif
|
||||
|
||||
Color color;
|
||||
|
||||
if (node.Walkable) {
|
||||
switch (debugMode) {
|
||||
case GraphDebugMode.Areas:
|
||||
color = AstarColor.GetAreaColor(node.Area);
|
||||
break;
|
||||
case GraphDebugMode.HierarchicalNode:
|
||||
case GraphDebugMode.NavmeshBorderObstacles:
|
||||
color = AstarColor.GetTagColor((uint)node.HierarchicalNodeIndex);
|
||||
break;
|
||||
case GraphDebugMode.Penalty:
|
||||
color = Color.Lerp(AstarColor.ConnectionLowLerp, AstarColor.ConnectionHighLerp, ((float)node.Penalty-debugFloor) / (debugRoof-debugFloor));
|
||||
break;
|
||||
case GraphDebugMode.Tags:
|
||||
color = AstarColor.GetTagColor(node.Tag);
|
||||
break;
|
||||
case GraphDebugMode.SolidColor:
|
||||
color = AstarColor.SolidColor;
|
||||
break;
|
||||
default:
|
||||
#if UNITY_EDITOR
|
||||
if (debugPathNodes.Length == 0) {
|
||||
color = AstarColor.SolidColor;
|
||||
break;
|
||||
}
|
||||
|
||||
var variants = (uint)node.PathNodeVariants;
|
||||
float value1 = float.PositiveInfinity;
|
||||
float value2 = float.PositiveInfinity;
|
||||
for (uint i = 0; i < variants; i++) {
|
||||
var pathNode = debugPathNodes[node.NodeIndex + i];
|
||||
float v;
|
||||
if (debugMode == GraphDebugMode.G) {
|
||||
v = pathNode.g;
|
||||
} else if (debugMode == GraphDebugMode.H) {
|
||||
v = pathNode.h;
|
||||
} else {
|
||||
// mode == F
|
||||
v = pathNode.g + pathNode.h;
|
||||
}
|
||||
if (pathNode.pathID == debugPathID) {
|
||||
value1 = System.Math.Min(value1, v);
|
||||
} else {
|
||||
value2 = System.Math.Min(value2, v);
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the minimum of only the variants searched by the current path if any, otherwise take minimum of all variants.
|
||||
// For graphs without multiple variants per node (all graphs except recast graphs), this will always just pick the value for the single node variant.
|
||||
float value = float.IsPositiveInfinity(value1) ? value2 : value1;
|
||||
|
||||
color = Color.Lerp(AstarColor.ConnectionLowLerp, AstarColor.ConnectionHighLerp, (value-debugFloor) / (debugRoof-debugFloor));
|
||||
#else
|
||||
color = AstarColor.SolidColor;
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
color = AstarColor.UnwalkableNode;
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// Returns if the node is in the search tree of the path.
|
||||
/// Only guaranteed to be correct if path is the latest path calculated.
|
||||
/// Use for gizmo drawing only.
|
||||
/// </summary>
|
||||
internal static bool InSearchTree (GraphNode node, UnsafeSpan<GlobalNodeStorage.DebugPathNode> debugPathNodes, ushort pathID) {
|
||||
if (debugPathNodes.Length > 0) {
|
||||
for (uint i = 0; i < node.PathNodeVariants; i++) {
|
||||
if (debugPathNodes[node.NodeIndex + i].pathID == pathID) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
public void DrawWireTriangle (Vector3 a, Vector3 b, Vector3 c, Color color) {
|
||||
builder.Line(a, b, color);
|
||||
builder.Line(b, c, color);
|
||||
builder.Line(c, a, color);
|
||||
}
|
||||
|
||||
public void DrawTriangles (Vector3[] vertices, Color[] colors, int numTriangles) {
|
||||
var triangles = ArrayPool<int>.Claim(numTriangles*3);
|
||||
|
||||
for (int i = 0; i < numTriangles*3; i++) triangles[i] = i;
|
||||
builder.SolidMesh(vertices, triangles, colors, numTriangles*3, numTriangles*3);
|
||||
ArrayPool<int>.Release(ref triangles);
|
||||
}
|
||||
|
||||
public void DrawWireTriangles (Vector3[] vertices, Color[] colors, int numTriangles) {
|
||||
for (int i = 0; i < numTriangles; i++) {
|
||||
DrawWireTriangle(vertices[i*3+0], vertices[i*3+1], vertices[i*3+2], colors[i*3+0]);
|
||||
}
|
||||
}
|
||||
|
||||
void System.IDisposable.Dispose () {
|
||||
var tmp = this;
|
||||
|
||||
ObjectPool<GraphGizmoHelper>.Release(ref tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20ed0bf48b2a14d78b8aa2ebe33f0069
|
||||
timeCreated: 1473875958
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,104 @@
|
||||
using System.Collections.Generic;
|
||||
using Pathfinding.Pooling;
|
||||
|
||||
namespace Pathfinding {
|
||||
/// <summary>
|
||||
/// Contains useful functions for updating graphs.
|
||||
/// This class works a lot with the GraphNode class, a useful function to get nodes is <see cref="AstarPath.GetNearest"/>.
|
||||
///
|
||||
/// See: <see cref="AstarPath.GetNearest"/>
|
||||
/// See: <see cref="Pathfinding.PathUtilities"/>
|
||||
/// </summary>
|
||||
public static class GraphUpdateUtilities {
|
||||
/// <summary>
|
||||
/// Updates graphs and checks if all nodes are still reachable from each other.
|
||||
/// Graphs are updated, then a check is made to see if the nodes are still reachable from each other.
|
||||
/// If they are not, the graphs are reverted to before the update and false is returned.
|
||||
/// This is slower than a normal graph update.
|
||||
/// All queued graph updates and thread safe callbacks will be flushed during this function.
|
||||
///
|
||||
/// Returns: True if the given nodes are still reachable from each other after the guo has been applied. False otherwise.
|
||||
///
|
||||
/// <code>
|
||||
/// var graphUpdate = new GraphUpdateObject(tower.GetComponent<Collider>().bounds);
|
||||
/// var spawnPointNode = AstarPath.active.GetNearest(spawnPoint.position).node;
|
||||
/// var goalNode = AstarPath.active.GetNearest(goalPoint.position).node;
|
||||
///
|
||||
/// if (GraphUpdateUtilities.UpdateGraphsNoBlock(graphUpdate, spawnPointNode, goalNode, false)) {
|
||||
/// // Valid tower position
|
||||
/// // Since the last parameter (which is called "alwaysRevert") in the method call was false
|
||||
/// // The graph is now updated and the game can just continue
|
||||
/// } else {
|
||||
/// // Invalid tower position. It blocks the path between the spawn point and the goal
|
||||
/// // The effect on the graph has been reverted
|
||||
/// Destroy(tower);
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// Warning: This will not work for recast graphs if <see cref="GraphUpdateObject.updatePhysics"/> is enabled (the default).
|
||||
/// </summary>
|
||||
/// <param name="guo">The GraphUpdateObject to update the graphs with</param>
|
||||
/// <param name="node1">Node which should have a valid path to node2. All nodes should be walkable or false will be returned.</param>
|
||||
/// <param name="node2">Node which should have a valid path to node1. All nodes should be walkable or false will be returned.</param>
|
||||
/// <param name="alwaysRevert">If true, reverts the graphs to the old state even if no blocking occurred</param>
|
||||
public static bool UpdateGraphsNoBlock (GraphUpdateObject guo, GraphNode node1, GraphNode node2, bool alwaysRevert = false) {
|
||||
List<GraphNode> buffer = ListPool<GraphNode>.Claim();
|
||||
|
||||
buffer.Add(node1);
|
||||
buffer.Add(node2);
|
||||
|
||||
bool worked = UpdateGraphsNoBlock(guo, buffer, alwaysRevert);
|
||||
ListPool<GraphNode>.Release(ref buffer);
|
||||
return worked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates graphs and checks if all nodes are still reachable from each other.
|
||||
/// Graphs are updated, then a check is made to see if the nodes are still reachable from each other.
|
||||
/// If they are not, the graphs are reverted to before the update and false is returned.
|
||||
/// This is slower than a normal graph update.
|
||||
/// All queued graph updates will be flushed during this function.
|
||||
///
|
||||
/// Returns: True if the given nodes are still reachable from each other after the guo has been applied. False otherwise.
|
||||
/// </summary>
|
||||
/// <param name="guo">The GraphUpdateObject to update the graphs with</param>
|
||||
/// <param name="nodes">Nodes which should have valid paths between them. All nodes should be walkable or false will be returned.</param>
|
||||
/// <param name="alwaysRevert">If true, reverts the graphs to the old state even if no blocking occurred</param>
|
||||
public static bool UpdateGraphsNoBlock (GraphUpdateObject guo, List<GraphNode> nodes, bool alwaysRevert = false) {
|
||||
bool worked;
|
||||
|
||||
// Pause pathfinding while modifying the graphs
|
||||
var graphLock = AstarPath.active.PausePathfinding();
|
||||
|
||||
try {
|
||||
// Make sure any pending graph updates have been done before we start
|
||||
AstarPath.active.FlushGraphUpdates();
|
||||
|
||||
// Make sure all nodes are walkable
|
||||
for (int i = 0; i < nodes.Count; i++) if (!nodes[i].Walkable) return false;
|
||||
|
||||
// Create a snapshot to allow us to revert the graphs to their original state
|
||||
var snapshot = AstarPath.active.Snapshot(guo.bounds, guo.nnConstraint.graphMask);
|
||||
|
||||
AstarPath.active.UpdateGraphs(guo);
|
||||
|
||||
// Update the graphs immediately
|
||||
AstarPath.active.FlushGraphUpdates();
|
||||
|
||||
// Check if all nodes are in the same area and that they are walkable, i.e that there are paths between all of them
|
||||
worked = PathUtilities.IsPathPossible(nodes);
|
||||
|
||||
// If it did not work, revert the GUO
|
||||
if (!worked || alwaysRevert) {
|
||||
AstarPath.active.AddWorkItem(snapshot.Restore);
|
||||
AstarPath.active.FlushWorkItems();
|
||||
}
|
||||
snapshot.Dispose();
|
||||
} finally {
|
||||
graphLock.Release();
|
||||
}
|
||||
|
||||
return worked;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5818f65b5e1449c1ae6f20de9f75ff5
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
576
Packages/com.arongranberg.astar/Utilities/GridStringPulling.cs
Normal file
576
Packages/com.arongranberg.astar/Utilities/GridStringPulling.cs
Normal file
@@ -0,0 +1,576 @@
|
||||
using System.Collections.Generic;
|
||||
using Pathfinding.Pooling;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Pathfinding {
|
||||
/// <summary>
|
||||
/// Simplifies a path on a grid graph using a string pulling algorithm.
|
||||
/// This is based on a paper called "Toward a String-Pulling Approach to Path Smoothing on Grid Graphs",
|
||||
/// with some optimizations as well as fixes for some edge cases that the paper didn't handle.
|
||||
///
|
||||
/// The result is conceptually similar to the well known funnel string pulling algorithm for navmesh graphs
|
||||
/// but it uses a different algorithm.
|
||||
///
|
||||
/// This class is used by the <see cref="FunnelModifier"/> on grid graphs.
|
||||
///
|
||||
/// See: <see cref="Funnel"/>
|
||||
/// See: <see cref="FunnelModifier"/>
|
||||
/// See: article: https://ojs.aaai.org/index.php/SOCS/article/view/18541
|
||||
/// </summary>
|
||||
public static class GridStringPulling {
|
||||
/// <summary>
|
||||
/// Z
|
||||
/// |
|
||||
/// |
|
||||
///
|
||||
/// 3 2
|
||||
/// \ | /
|
||||
/// -- - X - ----- X
|
||||
/// / | \
|
||||
/// 0 1
|
||||
///
|
||||
/// |
|
||||
/// |
|
||||
/// </summary>
|
||||
static int2[] directionToCorners = new int2[] {
|
||||
new int2(0, 0),
|
||||
new int2(FixedPrecisionScale, 0),
|
||||
new int2(FixedPrecisionScale, FixedPrecisionScale),
|
||||
new int2(0, FixedPrecisionScale),
|
||||
};
|
||||
|
||||
static long Cross (int2 lhs, int2 rhs) {
|
||||
return (long)lhs.x*(long)rhs.y - (long)lhs.y*(long)rhs.x;
|
||||
}
|
||||
|
||||
static long Dot (int2 a, int2 b) {
|
||||
return (long)a.x*(long)b.x + (long)a.y*(long)b.y;
|
||||
}
|
||||
|
||||
static bool RightOrColinear (int2 a, int2 b, int2 p) {
|
||||
return (long)(b.x - a.x) * (long)(p.y - a.y) - (long)(p.x - a.x) * (long)(b.y - a.y) <= 0;
|
||||
}
|
||||
|
||||
static int2 Perpendicular (int2 v) {
|
||||
return new int2(-v.y, v.x);
|
||||
}
|
||||
|
||||
struct TriangleBounds {
|
||||
int2 d1, d2, d3;
|
||||
long t1, t2, t3;
|
||||
|
||||
public TriangleBounds(int2 p1, int2 p2, int2 p3) {
|
||||
if (RightOrColinear(p1, p2, p3)) {
|
||||
var tmp = p3;
|
||||
p3 = p1;
|
||||
p1 = tmp;
|
||||
}
|
||||
d1 = Perpendicular(p2 - p1);
|
||||
d2 = Perpendicular(p3 - p2);
|
||||
d3 = Perpendicular(p1 - p3);
|
||||
t1 = Dot(d1, p1);
|
||||
t2 = Dot(d2, p2);
|
||||
t3 = Dot(d3, p3);
|
||||
}
|
||||
|
||||
public bool Contains (int2 p) {
|
||||
return Dot(d1, p) >= t1 && Dot(d2, p) >= t2 && Dot(d3, p) >= t3;
|
||||
}
|
||||
}
|
||||
|
||||
const int FixedPrecisionScale = 1024;
|
||||
|
||||
static int2 ToFixedPrecision (Vector2 p) {
|
||||
return new int2(math.round(new float2(p)*FixedPrecisionScale));
|
||||
}
|
||||
|
||||
static Vector2 FromFixedPrecision (int2 p) {
|
||||
return (Vector2)(((float2)p) * (1.0f/FixedPrecisionScale));
|
||||
}
|
||||
|
||||
/// <summary>Returns which side of the line a - b that p lies on</summary>
|
||||
static Side Side2D (int2 a, int2 b, int2 p) {
|
||||
var s = Cross(b-a, p-a);
|
||||
|
||||
return s > 0 ? Side.Left : (s < 0 ? Side.Right : Side.Colinear);
|
||||
}
|
||||
|
||||
static Unity.Profiling.ProfilerMarker marker1 = new Unity.Profiling.ProfilerMarker("Linecast hit");
|
||||
static Unity.Profiling.ProfilerMarker marker2 = new Unity.Profiling.ProfilerMarker("Linecast success");
|
||||
static Unity.Profiling.ProfilerMarker marker3 = new Unity.Profiling.ProfilerMarker("Trace");
|
||||
static Unity.Profiling.ProfilerMarker marker4 = new Unity.Profiling.ProfilerMarker("Neighbours");
|
||||
static Unity.Profiling.ProfilerMarker marker5 = new Unity.Profiling.ProfilerMarker("Re-evaluate linecast");
|
||||
static Unity.Profiling.ProfilerMarker marker6 = new Unity.Profiling.ProfilerMarker("Init");
|
||||
static Unity.Profiling.ProfilerMarker marker7 = new Unity.Profiling.ProfilerMarker("Initloop");
|
||||
|
||||
/// <summary>
|
||||
/// Intersection length of the given segment with a square of size Int3.Precision centered at nodeCenter.
|
||||
/// The return value is between 0 and sqrt(2).
|
||||
/// </summary>
|
||||
public static float IntersectionLength (int2 nodeCenter, int2 segmentStart, int2 segmentEnd) {
|
||||
// TODO: Calculations can be hoisted
|
||||
var invNormal = math.rcp((float2)(segmentEnd - segmentStart));
|
||||
var normalMagn = math.length((float2)(segmentEnd - segmentStart));
|
||||
|
||||
float tmin = float.NegativeInfinity, tmax = float.PositiveInfinity;
|
||||
|
||||
var normal = segmentEnd - segmentStart;
|
||||
var bmin = nodeCenter; // - new int2(Int3.Precision/2, Int3.Precision/2);
|
||||
var bmax = nodeCenter + new int2(FixedPrecisionScale, FixedPrecisionScale);
|
||||
|
||||
if (normal.x != 0.0) {
|
||||
float tx1 = (bmin.x - segmentStart.x)*invNormal.x;
|
||||
float tx2 = (bmax.x - segmentStart.x)*invNormal.x;
|
||||
|
||||
tmin = math.max(tmin, math.min(tx1, tx2));
|
||||
tmax = math.min(tmax, math.max(tx1, tx2));
|
||||
} else if (segmentStart.x < bmin.x || segmentStart.x > bmax.x) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
if (normal.y != 0.0) {
|
||||
float ty1 = (bmin.y - segmentStart.y)*invNormal.y;
|
||||
float ty2 = (bmax.y - segmentStart.y)*invNormal.y;
|
||||
|
||||
tmin = math.max(tmin, math.min(ty1, ty2));
|
||||
tmax = math.min(tmax, math.max(ty1, ty2));
|
||||
} else if (segmentStart.y < bmin.y || segmentStart.y > bmax.y) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
tmin = math.max(0, tmin);
|
||||
tmax = math.min(1, tmax);
|
||||
return math.max(tmax - tmin, 0)*normalMagn*(1.0f/FixedPrecisionScale);
|
||||
}
|
||||
|
||||
internal static void TestIntersectionLength () {
|
||||
var s = FixedPrecisionScale;
|
||||
|
||||
UnityEngine.Assertions.Assert.AreApproximatelyEqual(math.sqrt(2), IntersectionLength(new int2(1*s, 1*s), new int2(0, 0), new int2(2*s, 2*s)));
|
||||
UnityEngine.Assertions.Assert.AreApproximatelyEqual(0.0f, IntersectionLength(new int2(1*s, 1*s), new int2(0, 0), new int2(0, 0)));
|
||||
UnityEngine.Assertions.Assert.AreApproximatelyEqual(1.0f, IntersectionLength(new int2(1*s, 1*s), new int2(-1*s, s+1), new int2(2*s, s+1)));
|
||||
UnityEngine.Assertions.Assert.AreApproximatelyEqual(1.0f, IntersectionLength(new int2(1*s, 1*s), new int2(2*s, s), new int2(-1*s, s)));
|
||||
|
||||
// All sides of the square should be included
|
||||
UnityEngine.Assertions.Assert.AreApproximatelyEqual(1.0f, IntersectionLength(new int2(1*s, 1*s), new int2(s, s), new int2(s+s, s)));
|
||||
UnityEngine.Assertions.Assert.AreApproximatelyEqual(1.0f, IntersectionLength(new int2(1*s, 1*s), new int2(s+s, s), new int2(s+s, s+s)));
|
||||
UnityEngine.Assertions.Assert.AreApproximatelyEqual(1.0f, IntersectionLength(new int2(1*s, 1*s), new int2(s+s, s+s), new int2(s, s+s)));
|
||||
UnityEngine.Assertions.Assert.AreApproximatelyEqual(1.0f, IntersectionLength(new int2(1*s, 1*s), new int2(s, s+s), new int2(s, s)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cost of moving across all the nodes in the list, along the given segment.
|
||||
/// It is assumed that the segment intersects the nodes. Any potentially intersecting nodes that are not part of the list will be ignored.
|
||||
/// </summary>
|
||||
static uint LinecastCost (List<GraphNode> trace, int2 segmentStart, int2 segmentEnd, GridGraph gg, System.Func<GraphNode, uint> traversalCost) {
|
||||
// Check the cost of the segment compared to not taking this "shortcut"
|
||||
uint cost = 0;
|
||||
|
||||
for (int k = 0; k < trace.Count; k++) {
|
||||
var node = trace[k] as GridNodeBase;
|
||||
// Note: this assumes the default grid connection costs are used. Which is relatively reasonable
|
||||
// since they require changing the code to modify.
|
||||
cost += (uint)(((float)traversalCost(node) + gg.nodeSize*Int3.Precision) * IntersectionLength(new int2(node.XCoordinateInGrid, node.ZCoordinateInGrid)*FixedPrecisionScale, segmentStart, segmentEnd));
|
||||
}
|
||||
return cost;
|
||||
}
|
||||
|
||||
enum PredicateFailMode {
|
||||
Undefined,
|
||||
Turn,
|
||||
LinecastObstacle,
|
||||
LinecastCost,
|
||||
ReachedEnd,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simplifies a path on a grid graph using a string pulling algorithm.
|
||||
/// See the class documentation for more details.
|
||||
/// </summary>
|
||||
/// <param name="pathNodes">A list of input nodes. Only the slice of nodes from nodeStartIndex to nodeEndIndex (inclusive) will be used. These must all be of type GridNodeBase and must form a path (i.e. each node must be a neighbor to the next one in the list).</param>
|
||||
/// <param name="nodeStartIndex">The index in pathNodes to start from.</param>
|
||||
/// <param name="nodeEndIndex">The last index in pathNodes that is used.</param>
|
||||
/// <param name="startPoint">A more exact start point for the path. This should be a point inside the first node (if not, it will be clamped to the node's surface).</param>
|
||||
/// <param name="endPoint">A more exact end point for the path. This should be a point inside the first node (if not, it will be clamped to the node's surface).</param>
|
||||
/// <param name="traversalCost">Can be used to specify how much it costs to traverse each node. If this is null, node penalties and tag penalties will be completely ignored.</param>
|
||||
/// <param name="filter">Can be used to filter out additional nodes that should be treated as unwalkable. It is assumed that all nodes in pathNodes pass this filter.</param>
|
||||
/// <param name="maxCorners">If you only need the first N points of the result, you can specify that here, to avoid unnecessary work.</param>
|
||||
public static List<Vector3> Calculate (List<GraphNode> pathNodes, int nodeStartIndex, int nodeEndIndex, Vector3 startPoint, Vector3 endPoint, System.Func<GraphNode, uint> traversalCost = null, System.Func<GraphNode, bool> filter = null, int maxCorners = int.MaxValue) {
|
||||
Profiler.BeginSample("Funnel");
|
||||
marker6.Begin();
|
||||
// A list of indices into the arrays defined below.
|
||||
// Each index represents a point. But it's more convenient to use indices here and keep all the data separately (also probably faster).
|
||||
var outputPath = ListPool<int>.Claim();
|
||||
outputPath.Add(0);
|
||||
|
||||
var numInputNodes = nodeEndIndex - nodeStartIndex + 1;
|
||||
var gg = pathNodes[nodeStartIndex].Graph as GridGraph;
|
||||
var trace = ListPool<GraphNode>.Claim();
|
||||
var turn = Side.Colinear;
|
||||
int counter = 0;
|
||||
|
||||
// We will add two points, see comments inside the loop.
|
||||
// We may end up adding even more points later, therefore we get arrays that are a bit larger than we need for the initial path.
|
||||
numInputNodes += 2;
|
||||
int numPoints = numInputNodes;
|
||||
var nodes = ArrayPool<GridNodeBase>.Claim(numPoints*2);
|
||||
var points = ArrayPool<int2>.Claim(numPoints*2);
|
||||
var normalizedPoints = ArrayPool<int2>.Claim(numPoints*2);
|
||||
var costs = ArrayPool<uint>.Claim(numPoints*2);
|
||||
|
||||
marker7.Begin();
|
||||
uint costSoFar = 0;
|
||||
// Go through all points and store all relevant data we need about them
|
||||
for (int j = 0; j < numInputNodes; j++) {
|
||||
// After the start-end modifier has adjusted the endpoints of the path, the line from the start point to the center of the second node in the path
|
||||
// might not actually have line of sight.
|
||||
// Assume the path starts at N1 with a diagonal move to node N2.
|
||||
// The start-end modifier adjusts the start point of the path to point S.
|
||||
// This causes the path to cut the corner to the unwalkable node in the bottom right.
|
||||
// ┌─────────┬────────┐
|
||||
// │ │ │
|
||||
// │ N2 │ │
|
||||
// │ \ │ │
|
||||
// │ \ │ │
|
||||
// ├───────\─┼────────┤
|
||||
// │########\│ │
|
||||
// │#########│S N1 │
|
||||
// │#########│ │
|
||||
// │#########│ │
|
||||
// └─────────┴────────┘
|
||||
// We can solve this case by making the path go from S to N1 and then to N2 instead of directly from S to N2.
|
||||
// We also do the same thing for the end of the path.
|
||||
// The clamping and indexing here is essentially equivalent to one insert at the beginning of the arrays and one at the end.
|
||||
var node = nodes[j] = pathNodes[math.clamp(nodeStartIndex + j-1, nodeStartIndex, nodeEndIndex)] as GridNodeBase;
|
||||
var gridCoordinates = new int2(node.XCoordinateInGrid, node.ZCoordinateInGrid);
|
||||
var point = gridCoordinates * FixedPrecisionScale;
|
||||
int2 normalized;
|
||||
if (j == 0) {
|
||||
normalized = ToFixedPrecision(node.NormalizePoint(startPoint));
|
||||
normalized = math.clamp(normalized, int2.zero, new int2(FixedPrecisionScale, FixedPrecisionScale));
|
||||
} else if (j == numInputNodes - 1) {
|
||||
normalized = ToFixedPrecision(node.NormalizePoint(endPoint));
|
||||
normalized = math.clamp(normalized, int2.zero, new int2(FixedPrecisionScale, FixedPrecisionScale));
|
||||
} else {
|
||||
normalized = new int2(FixedPrecisionScale/2, FixedPrecisionScale/2);
|
||||
}
|
||||
points[j] = point + normalized;
|
||||
normalizedPoints[j] = normalized;
|
||||
if (j > 0 && traversalCost != null) {
|
||||
// Calculate the cost of moving along the original path
|
||||
costSoFar += (uint)(((float)traversalCost(nodes[j-1]) + gg.nodeSize*Int3.Precision) * IntersectionLength(new int2(nodes[j-1].XCoordinateInGrid, nodes[j-1].ZCoordinateInGrid)*FixedPrecisionScale, points[j-1], points[j]));
|
||||
costSoFar += (uint)(((float)traversalCost(nodes[j]) + gg.nodeSize*Int3.Precision) * IntersectionLength(gridCoordinates*FixedPrecisionScale, points[j-1], points[j]));
|
||||
}
|
||||
costs[j] = costSoFar;
|
||||
}
|
||||
marker7.End();
|
||||
|
||||
// We know that there is line of sight from the first point to the second point in the path.
|
||||
var lastSuccessfulStart = 0;
|
||||
var lastSuccessfulEnd = 1;
|
||||
marker6.End();
|
||||
|
||||
int i = 1;
|
||||
while (true) {
|
||||
if (i >= numInputNodes) {
|
||||
// We are done, add the last point
|
||||
outputPath.Add(numInputNodes-1);
|
||||
break;
|
||||
}
|
||||
if (outputPath.Count >= maxCorners) {
|
||||
// We are done with the partial result
|
||||
break;
|
||||
}
|
||||
|
||||
counter++;
|
||||
if (counter > 10000) {
|
||||
Debug.LogError("Inf loop");
|
||||
break;
|
||||
}
|
||||
|
||||
// In the paper, they just use a straight forward loop over the input path.
|
||||
// However, it is better for performance to use a binary search to figure out the next time we need to do something.
|
||||
// We only need an 'i' which succeeds and 'i+1' which fails.
|
||||
// The success in this case is defined by the predicate below. We only need to do stuff if that returns true.
|
||||
var last = outputPath[outputPath.Count-1];
|
||||
var normalizedLast = normalizedPoints[last];
|
||||
var prev = outputPath.Count > 1 ? outputPath[outputPath.Count-2] : -1;
|
||||
var nodeLast = nodes[last];
|
||||
var upperBound = numInputNodes - i - 1;
|
||||
|
||||
// Lower and upper bounds for the binary search
|
||||
int mn = 0;
|
||||
// It is reasonable that most paths can be simplified at least a bit. Assume that seeing 4 or more nodes ahead is common.
|
||||
int mx = math.min(4, upperBound);
|
||||
var mxFailMode = PredicateFailMode.Undefined;
|
||||
uint mxLinecastCost = 0;
|
||||
|
||||
// The calculations are not perfectly accurate. Allow the shortcut's cost to be a tiny bit higher.
|
||||
const uint COST_FUDGE = 5;
|
||||
|
||||
GridHitInfo hit;
|
||||
// First fire off linecasts to nodes exponentially further away until the predicate returns true.
|
||||
while (true) {
|
||||
var idx = i + mx;
|
||||
|
||||
var turnPredicate = outputPath.Count > 1 && Side2D(points[prev], points[last], points[idx]) != turn;
|
||||
if (turnPredicate) {
|
||||
mxFailMode = PredicateFailMode.Turn;
|
||||
break;
|
||||
} else {
|
||||
trace.Clear();
|
||||
if (gg.Linecast(nodeLast, normalizedLast, nodes[idx], normalizedPoints[idx], out hit, trace, filter)) {
|
||||
mxFailMode = PredicateFailMode.LinecastObstacle;
|
||||
break;
|
||||
} else if (traversalCost != null) {
|
||||
var cost = LinecastCost(trace, points[last], points[idx], gg, traversalCost);
|
||||
if (cost > costs[idx] - costs[last] + COST_FUDGE) {
|
||||
// The "shortcut" had such a high penalty that it's not worth taking it
|
||||
mxFailMode = PredicateFailMode.LinecastCost;
|
||||
mxLinecastCost = cost;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mx < upperBound) {
|
||||
mn = mx;
|
||||
mx = math.min(mx*2, upperBound);
|
||||
} else {
|
||||
mxFailMode = PredicateFailMode.ReachedEnd;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mxFailMode == PredicateFailMode.ReachedEnd) {
|
||||
// Reached final node without any hits, we can stop here
|
||||
outputPath.Add(numInputNodes-1);
|
||||
break;
|
||||
}
|
||||
|
||||
// Run a standard binary search
|
||||
while (mx > mn + 1) {
|
||||
int mid = (mn+mx)/2;
|
||||
int idx = i + mid;
|
||||
|
||||
var turnPredicate = outputPath.Count > 1 && Side2D(points[prev], points[last], points[idx]) != turn;
|
||||
bool pred = turnPredicate;
|
||||
if (turnPredicate) {
|
||||
mxFailMode = PredicateFailMode.Turn;
|
||||
} else {
|
||||
trace.Clear();
|
||||
if (gg.Linecast(nodeLast, normalizedLast, nodes[idx], normalizedPoints[idx], out hit, trace, filter)) {
|
||||
mxFailMode = PredicateFailMode.LinecastObstacle;
|
||||
pred = true;
|
||||
} else if (traversalCost != null) {
|
||||
var cost = LinecastCost(trace, points[last], points[idx], gg, traversalCost);
|
||||
if (cost > costs[idx] - costs[last] + COST_FUDGE) {
|
||||
// The "shortcut" had such a high penalty that it's not worth taking it
|
||||
mxFailMode = PredicateFailMode.LinecastCost;
|
||||
mxLinecastCost = cost;
|
||||
pred = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pred) {
|
||||
mx = mid;
|
||||
} else {
|
||||
mn = mid;
|
||||
}
|
||||
}
|
||||
|
||||
// i+mn is now a succeeding index, and i+mn+1 (or i+mx) is a failing index
|
||||
if (mn > 0) {
|
||||
lastSuccessfulStart = last;
|
||||
lastSuccessfulEnd = i+mn;
|
||||
} else {
|
||||
// We are not actually completely sure that i+mn is a succeeding index if mn=0
|
||||
// So double check it.
|
||||
// TODO: This is a lot of code duplication. Tidy this up.
|
||||
var turnPredicate = outputPath.Count > 1 && Side2D(points[prev], points[last], points[i+mn]) != turn;
|
||||
bool pred = turnPredicate;
|
||||
if (turnPredicate) {
|
||||
} else {
|
||||
trace.Clear();
|
||||
if (gg.Linecast(nodeLast, normalizedLast, nodes[i+mn], normalizedPoints[i+mn], out hit, trace, filter)) {
|
||||
pred = true;
|
||||
} else if (traversalCost != null) {
|
||||
var cost = LinecastCost(trace, points[last], points[i+mn], gg, traversalCost);
|
||||
if (cost > costs[i+mn] - costs[last] + COST_FUDGE) {
|
||||
// The "shortcut" had such a high penalty that it's not worth taking it
|
||||
mxLinecastCost = cost;
|
||||
pred = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!pred) {
|
||||
// Success!
|
||||
lastSuccessfulStart = last;
|
||||
lastSuccessfulEnd = i+mn;
|
||||
}
|
||||
}
|
||||
|
||||
// Move to the failing index
|
||||
i += mx;
|
||||
UnityEngine.Assertions.Assert.AreNotEqual(mxFailMode, PredicateFailMode.Undefined);
|
||||
|
||||
marker5.Begin();
|
||||
trace.Clear();
|
||||
trace.Clear();
|
||||
if (mxFailMode == PredicateFailMode.LinecastCost) {
|
||||
outputPath.Add(lastSuccessfulEnd);
|
||||
turn = Side2D(points[last], points[lastSuccessfulEnd], points[i]);
|
||||
// It is guaranteed that there is line of sight from lastSuccessfulStart to lastSuccessfulEnd
|
||||
lastSuccessfulStart = lastSuccessfulEnd;
|
||||
i--;
|
||||
marker5.End();
|
||||
continue;
|
||||
} else if (mxFailMode == PredicateFailMode.LinecastObstacle) {
|
||||
marker5.End();
|
||||
// Draw.Line(nodes[last].UnNormalizePoint(FromFixedPrecision(normalizedPoints[last])), toNode.UnNormalizePoint(FromFixedPrecision(normalizedTo)), Color.red);
|
||||
marker1.Begin();
|
||||
marker3.Begin();
|
||||
// Re-run a previously successfully linecast to get all nodes it traversed.
|
||||
trace.Clear();
|
||||
int chosenCorner;
|
||||
if (gg.Linecast(nodes[lastSuccessfulStart], normalizedPoints[lastSuccessfulStart], nodes[lastSuccessfulEnd], normalizedPoints[lastSuccessfulEnd], out hit, trace, filter)) {
|
||||
// Weird! This linecast should have succeeded.
|
||||
// Maybe the path crosses some unwalkable nodes it shouldn't cross (the graph could have changed).
|
||||
// Or possibly the linecast implementation doesn't handle some edge case (there are so many!)
|
||||
// In any case, we fall back to just assuming there is a valid line of sight.
|
||||
chosenCorner = lastSuccessfulEnd;
|
||||
Debug.LogError("Inconsistent linecasts");
|
||||
} else {
|
||||
trace.Add(nodes[i]);
|
||||
marker3.End();
|
||||
marker4.Begin();
|
||||
|
||||
GridNodeBase candidateNode = null;
|
||||
var candidateNormalizedPoint = new int2();
|
||||
uint candidateCost = 0;
|
||||
var dirToCandidateCorner = new int2();
|
||||
var lastSuccessfulStartPoint = points[lastSuccessfulStart];
|
||||
var lastSuccessfulEndPoint = points[lastSuccessfulEnd];
|
||||
var dir = lastSuccessfulEndPoint - lastSuccessfulStartPoint;
|
||||
var bounds = new TriangleBounds(
|
||||
lastSuccessfulStartPoint,
|
||||
lastSuccessfulEndPoint,
|
||||
points[i]
|
||||
);
|
||||
|
||||
var desiredSide = System.Math.Sign(Cross(dir, points[i] - lastSuccessfulStartPoint));
|
||||
var candidateCostSoFar = costs[lastSuccessfulStart];
|
||||
for (int j = 0; j < trace.Count; j++) {
|
||||
var node = trace[j] as GridNodeBase;
|
||||
var nodeGridPos = new int2(node.XCoordinateInGrid, node.ZCoordinateInGrid);
|
||||
var nodeCenter = nodeGridPos * FixedPrecisionScale;
|
||||
if (traversalCost != null) {
|
||||
// Not perfectly accurate as it doesn't measure the cost to the exact corner
|
||||
candidateCostSoFar += (uint)(((float)traversalCost(node) + gg.nodeSize*Int3.Precision) * IntersectionLength(nodeCenter, lastSuccessfulStartPoint, lastSuccessfulEndPoint));
|
||||
}
|
||||
for (int d = 0; d < 4; d++) {
|
||||
if (!node.HasConnectionInDirection(d) || (filter != null && !filter(node.GetNeighbourAlongDirection(d)))) {
|
||||
for (int q = 0; q < 2; q++) {
|
||||
var ncorner = directionToCorners[(d+q)&0x3];
|
||||
var corner = nodeCenter + ncorner;
|
||||
|
||||
if (!bounds.Contains(corner)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var dirToCorner = corner - lastSuccessfulStartPoint;
|
||||
// We shouldn't pick corners at our current position
|
||||
if (math.all(dirToCorner == 0)) continue;
|
||||
if (math.all(corner == lastSuccessfulEndPoint)) continue;
|
||||
|
||||
var side = Cross(dirToCorner, dirToCandidateCorner);
|
||||
if (candidateNode == null || System.Math.Sign(side) == desiredSide || (side == 0 && math.lengthsq(dirToCorner) > math.lengthsq(dirToCandidateCorner))) {
|
||||
dirToCandidateCorner = dirToCorner;
|
||||
candidateNode = node;
|
||||
candidateNormalizedPoint = ncorner;
|
||||
candidateCost = candidateCostSoFar;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
marker4.End();
|
||||
|
||||
if (candidateNode == null) {
|
||||
// Fall back to adding the lastSuccessfulEnd node. We know there's line of sight to that one.
|
||||
chosenCorner = lastSuccessfulEnd;
|
||||
} else {
|
||||
chosenCorner = numPoints;
|
||||
// TODO: Reallocate
|
||||
nodes[numPoints] = candidateNode;
|
||||
normalizedPoints[numPoints] = candidateNormalizedPoint;
|
||||
var gridCoordinates = new int2(candidateNode.XCoordinateInGrid, candidateNode.ZCoordinateInGrid);
|
||||
points[numPoints] = gridCoordinates * FixedPrecisionScale + candidateNormalizedPoint;
|
||||
costs[numPoints] = candidateCost;
|
||||
numPoints++;
|
||||
}
|
||||
}
|
||||
|
||||
outputPath.Add(chosenCorner);
|
||||
turn = Side2D(points[last], points[chosenCorner], points[i]);
|
||||
// It is guaranteed that there is line of sight from lastSuccessfulStart to chosenCorner because of how we choose the corner.
|
||||
lastSuccessfulStart = chosenCorner;
|
||||
i--;
|
||||
marker1.End();
|
||||
continue;
|
||||
} else {
|
||||
marker5.End();
|
||||
marker2.Begin();
|
||||
lastSuccessfulStart = last;
|
||||
lastSuccessfulEnd = i;
|
||||
// Draw.Line(nodes[last].UnNormalizePoint(FromFixedPrecision(normalizedPoints[last])), toNode.UnNormalizePoint(FromFixedPrecision(normalizedTo)), Color.green);
|
||||
if (outputPath.Count > 1) {
|
||||
var spPrev = outputPath[outputPath.Count-2];
|
||||
var nextTurn = Side2D(points[spPrev], points[last], points[i]);
|
||||
// Check if the string is no longer taut. If it is not we can remove a previous point.
|
||||
if (turn != nextTurn) {
|
||||
// Draw.SphereOutline(nodes[pts[pts.Count-1]].UnNormalizePoint(FromFixedPrecision(normalizedPoints[pts[pts.Count-1]])), 0.05f, Color.black);
|
||||
|
||||
lastSuccessfulStart = outputPath[outputPath.Count-2];
|
||||
lastSuccessfulEnd = outputPath[outputPath.Count-1];
|
||||
|
||||
outputPath.RemoveAt(outputPath.Count-1);
|
||||
if (outputPath.Count > 1) {
|
||||
last = spPrev;
|
||||
spPrev = outputPath[outputPath.Count-2];
|
||||
turn = Side2D(points[spPrev], points[last], points[i]);
|
||||
} else {
|
||||
// TODO: Should be separate value
|
||||
turn = Side.Colinear;
|
||||
}
|
||||
i--;
|
||||
marker2.End();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
marker2.End();
|
||||
}
|
||||
}
|
||||
|
||||
Profiler.EndSample();
|
||||
|
||||
var result = ListPool<Vector3>.Claim(outputPath.Count);
|
||||
for (int j = 0; j < outputPath.Count; j++) {
|
||||
var idx = outputPath[j];
|
||||
result.Add(nodes[idx].UnNormalizePoint(FromFixedPrecision(normalizedPoints[idx])));
|
||||
}
|
||||
|
||||
ArrayPool<GridNodeBase>.Release(ref nodes);
|
||||
ArrayPool<int2>.Release(ref points);
|
||||
ArrayPool<int2>.Release(ref normalizedPoints);
|
||||
ArrayPool<uint>.Release(ref costs);
|
||||
ListPool<int>.Release(ref outputPath);
|
||||
ListPool<GraphNode>.Release(ref trace);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 138b968807644d3ac8c22ad9d087f012
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
// This file is only included because the Unity.Jobs package is currently experimental and it seems bad to rely on it.
|
||||
// The Unity.Jobs version of this interface will be used when it is stable.
|
||||
using System;
|
||||
using Unity.Jobs.LowLevel.Unsafe;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using Unity.Jobs;
|
||||
|
||||
namespace Pathfinding.Jobs {
|
||||
[JobProducerType(typeof(JobParallelForBatchedExtensions.ParallelForBatchJobStruct<>))]
|
||||
public interface IJobParallelForBatched {
|
||||
bool allowBoundsChecks { get; }
|
||||
void Execute(int startIndex, int count);
|
||||
}
|
||||
|
||||
static class JobParallelForBatchedExtensions {
|
||||
internal struct ParallelForBatchJobStruct<T> where T : struct, IJobParallelForBatched {
|
||||
static public IntPtr jobReflectionData;
|
||||
|
||||
public static IntPtr Initialize () {
|
||||
if (jobReflectionData == IntPtr.Zero) {
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
jobReflectionData = JobsUtility.CreateJobReflectionData(typeof(T), (ExecuteJobFunction)Execute, null, null);
|
||||
#else
|
||||
jobReflectionData = JobsUtility.CreateJobReflectionData(typeof(T), JobType.ParallelFor, (ExecuteJobFunction)Execute);
|
||||
#endif
|
||||
}
|
||||
return jobReflectionData;
|
||||
}
|
||||
|
||||
public delegate void ExecuteJobFunction(ref T data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref JobRanges ranges, int jobIndex);
|
||||
public unsafe static void Execute (ref T jobData, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, ref JobRanges ranges, int jobIndex) {
|
||||
while (true) {
|
||||
int begin;
|
||||
int end;
|
||||
if (!JobsUtility.GetWorkStealingRange(ref ranges, jobIndex, out begin, out end))
|
||||
return;
|
||||
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (jobData.allowBoundsChecks) JobsUtility.PatchBufferMinMaxRanges(bufferRangePatchData, UnsafeUtility.AddressOf(ref jobData), begin, end - begin);
|
||||
#endif
|
||||
|
||||
jobData.Execute(begin, end - begin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe static public JobHandle ScheduleBatch<T>(this T jobData, int arrayLength, int minIndicesPerJobCount, JobHandle dependsOn = new JobHandle()) where T : struct, IJobParallelForBatched {
|
||||
#if UNITY_2020_2_OR_NEWER
|
||||
// This was renamed in Unity 2020.2
|
||||
var scheduleMode = ScheduleMode.Parallel;
|
||||
#else
|
||||
var scheduleMode = ScheduleMode.Batched;
|
||||
#endif
|
||||
var scheduleParams = new JobsUtility.JobScheduleParameters(UnsafeUtility.AddressOf(ref jobData), ParallelForBatchJobStruct<T>.Initialize(), dependsOn, scheduleMode);
|
||||
|
||||
return JobsUtility.ScheduleParallelFor(ref scheduleParams, arrayLength, minIndicesPerJobCount);
|
||||
}
|
||||
|
||||
unsafe static public void RunBatch<T>(this T jobData, int arrayLength) where T : struct, IJobParallelForBatched {
|
||||
var scheduleParams = new JobsUtility.JobScheduleParameters(UnsafeUtility.AddressOf(ref jobData), ParallelForBatchJobStruct<T>.Initialize(), new JobHandle(), ScheduleMode.Run);
|
||||
|
||||
JobsUtility.ScheduleParallelFor(ref scheduleParams, arrayLength, arrayLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a27178adf123341a3b302184fe478f8b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,790 @@
|
||||
// #define DEBUG_JOBS
|
||||
namespace Pathfinding.Jobs {
|
||||
using System.Reflection;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using Pathfinding.Pooling;
|
||||
using Pathfinding.Collections;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// Disable the check that prevents jobs from including uninitialized native arrays open for reading.
|
||||
///
|
||||
/// Sometimes jobs have to include a readable native array that starts out uninitialized.
|
||||
/// The job might for example write to it and later read from it in the same job.
|
||||
///
|
||||
/// See: <see cref="JobDependencyTracker.NewNativeArray"/>
|
||||
/// </summary>
|
||||
class DisableUninitializedReadCheckAttribute : System.Attribute {
|
||||
}
|
||||
|
||||
public interface IArenaDisposable {
|
||||
void DisposeWith(DisposeArena arena);
|
||||
}
|
||||
|
||||
/// <summary>Convenient collection of items that can be disposed together</summary>
|
||||
public class DisposeArena {
|
||||
List<NativeArray<byte> > buffer;
|
||||
List<NativeList<byte> > buffer2;
|
||||
List<NativeQueue<byte> > buffer3;
|
||||
List<GCHandle> gcHandles;
|
||||
|
||||
public void Add<T>(NativeArray<T> data) where T : unmanaged {
|
||||
if (buffer == null) buffer = ListPool<NativeArray<byte> >.Claim();
|
||||
buffer.Add(data.Reinterpret<byte>(UnsafeUtility.SizeOf<T>()));
|
||||
}
|
||||
|
||||
public void Add<T>(NativeList<T> data) where T : unmanaged {
|
||||
// SAFETY: This is safe because NativeList<byte> and NativeList<T> have the same memory layout.
|
||||
var byteList = Unity.Collections.LowLevel.Unsafe.UnsafeUtility.As<NativeList<T>, NativeList<byte> >(ref data);
|
||||
if (buffer2 == null) buffer2 = ListPool<NativeList<byte> >.Claim();
|
||||
buffer2.Add(byteList);
|
||||
}
|
||||
|
||||
public void Add<T>(NativeQueue<T> data) where T : unmanaged {
|
||||
// SAFETY: This is safe because NativeQueue<byte> and NativeQueue<T> have the same memory layout.
|
||||
var byteList = Unity.Collections.LowLevel.Unsafe.UnsafeUtility.As<NativeQueue<T>, NativeQueue<byte> >(ref data);
|
||||
if (buffer3 == null) buffer3 = ListPool<NativeQueue<byte> >.Claim();
|
||||
buffer3.Add(byteList);
|
||||
}
|
||||
|
||||
public void Remove<T>(NativeArray<T> data) where T : unmanaged {
|
||||
if (buffer == null) return;
|
||||
unsafe {
|
||||
var ptr = NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(data);
|
||||
for (int i = 0; i < buffer.Count; i++) {
|
||||
if (NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(buffer[i]) == ptr) {
|
||||
buffer.RemoveAtSwapBack(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Add<T>(T data) where T : IArenaDisposable {
|
||||
data.DisposeWith(this);
|
||||
}
|
||||
|
||||
public void Add (GCHandle handle) {
|
||||
if (gcHandles == null) gcHandles = ListPool<GCHandle>.Claim();
|
||||
gcHandles.Add(handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispose all items in the arena.
|
||||
/// This also clears the arena and makes it available for reuse.
|
||||
/// </summary>
|
||||
public void DisposeAll () {
|
||||
UnityEngine.Profiling.Profiler.BeginSample("Disposing");
|
||||
if (buffer != null) {
|
||||
for (int i = 0; i < buffer.Count; i++) buffer[i].Dispose();
|
||||
ListPool<NativeArray<byte> >.Release(ref buffer);
|
||||
}
|
||||
if (buffer2 != null) {
|
||||
for (int i = 0; i < buffer2.Count; i++) buffer2[i].Dispose();
|
||||
ListPool<NativeList<byte> >.Release(ref buffer2);
|
||||
}
|
||||
if (buffer3 != null) {
|
||||
for (int i = 0; i < buffer3.Count; i++) buffer3[i].Dispose();
|
||||
ListPool<NativeQueue<byte> >.Release(ref buffer3);
|
||||
}
|
||||
if (gcHandles != null) {
|
||||
for (int i = 0; i < gcHandles.Count; i++) gcHandles[i].Free();
|
||||
ListPool<GCHandle>.Release(ref gcHandles);
|
||||
}
|
||||
UnityEngine.Profiling.Profiler.EndSample();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Remove or use?
|
||||
public struct JobHandleWithMainThreadWork<T> where T : struct {
|
||||
JobDependencyTracker tracker;
|
||||
IEnumerator<(JobHandle, T)> coroutine;
|
||||
|
||||
public JobHandleWithMainThreadWork (IEnumerator<(JobHandle, T)> handles, JobDependencyTracker tracker) {
|
||||
this.coroutine = handles;
|
||||
this.tracker = tracker;
|
||||
}
|
||||
|
||||
public void Complete () {
|
||||
tracker.timeSlice = TimeSlice.Infinite;
|
||||
while (coroutine.MoveNext()) {
|
||||
coroutine.Current.Item1.Complete();
|
||||
}
|
||||
}
|
||||
|
||||
public System.Collections.Generic.IEnumerable<T?> CompleteTimeSliced (float maxMillisPerStep) {
|
||||
tracker.timeSlice = TimeSlice.MillisFromNow(maxMillisPerStep);
|
||||
while (true) {
|
||||
if (!coroutine.MoveNext()) yield break;
|
||||
if (maxMillisPerStep < float.PositiveInfinity) {
|
||||
while (!coroutine.Current.Item1.IsCompleted) {
|
||||
yield return null;
|
||||
tracker.timeSlice = TimeSlice.MillisFromNow(maxMillisPerStep);
|
||||
}
|
||||
}
|
||||
coroutine.Current.Item1.Complete();
|
||||
yield return coroutine.Current.Item2;
|
||||
tracker.timeSlice = TimeSlice.MillisFromNow(maxMillisPerStep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum LinearDependencies : byte {
|
||||
Check,
|
||||
Enabled,
|
||||
Disabled,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Automatic dependency tracking for the Unity Job System.
|
||||
///
|
||||
/// Uses reflection to find the [ReadOnly] and [WriteOnly] attributes on job data struct fields.
|
||||
/// These are used to automatically figure out dependencies between jobs.
|
||||
///
|
||||
/// A job that reads from an array depends on the last job that wrote to that array.
|
||||
/// A job that writes to an array depends on the last job that wrote to the array as well as all jobs that read from the array.
|
||||
///
|
||||
/// <code>
|
||||
/// struct ExampleJob : IJob {
|
||||
/// public NativeArray<int> someData;
|
||||
///
|
||||
/// public void Execute () {
|
||||
/// // Do something
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// void Start () {
|
||||
/// var tracker = new JobDependencyTracker();
|
||||
/// var data = new NativeArray<int>(100, Allocator.TempJob);
|
||||
/// var job1 = new ExampleJob {
|
||||
/// someData = data
|
||||
/// }.Schedule(tracker);
|
||||
///
|
||||
/// var job2 = new ExampleJob {
|
||||
/// someData = data
|
||||
/// }.Schedule(tracker);
|
||||
///
|
||||
/// // job2 automatically depends on job1 because they both require read/write access to the data array
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// See: <see cref="IJobExtensions"/>
|
||||
/// </summary>
|
||||
public class JobDependencyTracker : IAstarPooledObject {
|
||||
internal List<NativeArraySlot> slots = ListPool<NativeArraySlot>.Claim();
|
||||
DisposeArena arena;
|
||||
internal NativeArray<JobHandle> dependenciesScratchBuffer;
|
||||
LinearDependencies linearDependencies;
|
||||
internal TimeSlice timeSlice = TimeSlice.Infinite;
|
||||
|
||||
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
~JobDependencyTracker() {
|
||||
if (dependenciesScratchBuffer.IsCreated) {
|
||||
UnityEngine.Debug.LogError("JobDependencyTracker was not disposed. This will cause a memory leak. Please call Dispose on the JobDependencyTracker when you are done with it.");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
public bool forceLinearDependencies {
|
||||
get {
|
||||
if (linearDependencies == LinearDependencies.Check) SetLinearDependencies(false);
|
||||
return linearDependencies == LinearDependencies.Enabled;
|
||||
}
|
||||
}
|
||||
|
||||
internal struct JobInstance {
|
||||
public JobHandle handle;
|
||||
public int hash;
|
||||
#if DEBUG_JOBS
|
||||
public string name;
|
||||
#endif
|
||||
}
|
||||
|
||||
internal struct NativeArraySlot {
|
||||
public long hash;
|
||||
public JobInstance lastWrite;
|
||||
public List<JobInstance> lastReads;
|
||||
public bool initialized;
|
||||
public bool hasWrite;
|
||||
}
|
||||
|
||||
// Note: burst compiling even an empty job can avoid the overhead of going from unmanaged to managed code.
|
||||
/* [BurstCompile]
|
||||
struct JobDispose<T> : IJob where T : struct {
|
||||
[DeallocateOnJobCompletion]
|
||||
[DisableUninitializedReadCheck]
|
||||
public NativeArray<T> data;
|
||||
|
||||
public void Execute () {
|
||||
}
|
||||
}*/
|
||||
|
||||
struct JobRaycastCommandDummy : IJob {
|
||||
[ReadOnly]
|
||||
public NativeArray<UnityEngine.RaycastCommand> commands;
|
||||
[WriteOnly]
|
||||
public NativeArray<UnityEngine.RaycastHit> results;
|
||||
|
||||
public void Execute () {}
|
||||
}
|
||||
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
struct JobOverlapCapsuleCommandDummy : IJob {
|
||||
[ReadOnly]
|
||||
public NativeArray<UnityEngine.OverlapCapsuleCommand> commands;
|
||||
[WriteOnly]
|
||||
public NativeArray<UnityEngine.ColliderHit> results;
|
||||
|
||||
public void Execute () {}
|
||||
}
|
||||
|
||||
struct JobOverlapSphereCommandDummy : IJob {
|
||||
[ReadOnly]
|
||||
public NativeArray<UnityEngine.OverlapSphereCommand> commands;
|
||||
[WriteOnly]
|
||||
public NativeArray<UnityEngine.ColliderHit> results;
|
||||
|
||||
public void Execute () {}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// JobHandle that represents a dependency for all jobs.
|
||||
/// All native arrays that are written (and have been tracked by this tracker) to will have their final results in them
|
||||
/// when the returned job handle is complete.
|
||||
/// </summary>
|
||||
public JobHandle AllWritesDependency {
|
||||
get {
|
||||
var handles = new NativeArray<JobHandle>(slots.Count, Allocator.Temp);
|
||||
for (int i = 0; i < slots.Count; i++) handles[i] = slots[i].lastWrite.handle;
|
||||
var dependencies = JobHandle.CombineDependencies(handles);
|
||||
handles.Dispose();
|
||||
return dependencies;
|
||||
}
|
||||
}
|
||||
|
||||
bool supportsMultithreading {
|
||||
get {
|
||||
#if UNITY_WEBGL
|
||||
return false;
|
||||
#else
|
||||
return Unity.Jobs.LowLevel.Unsafe.JobsUtility.JobWorkerCount > 0;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disable dependency tracking and just run jobs one after the other.
|
||||
/// This may be faster in some cases since dependency tracking has some overhead.
|
||||
/// </summary>
|
||||
public void SetLinearDependencies (bool linearDependencies) {
|
||||
if (!supportsMultithreading) linearDependencies = true;
|
||||
|
||||
if (linearDependencies) {
|
||||
AllWritesDependency.Complete();
|
||||
}
|
||||
this.linearDependencies = linearDependencies ? LinearDependencies.Enabled : LinearDependencies.Disabled;
|
||||
}
|
||||
|
||||
public NativeArray<T> NewNativeArray<T>(int length, Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory) where T : unmanaged {
|
||||
var res = new NativeArray<T>(length, allocator, options);
|
||||
Track(res, options == NativeArrayOptions.ClearMemory);
|
||||
return res;
|
||||
}
|
||||
|
||||
public void Track<T>(NativeArray<T> array, bool initialized = true) where T : unmanaged {
|
||||
unsafe {
|
||||
slots.Add(new NativeArraySlot {
|
||||
hash = (long)NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(array),
|
||||
lastWrite = default,
|
||||
lastReads = ListPool<JobInstance>.Claim(),
|
||||
initialized = initialized,
|
||||
});
|
||||
}
|
||||
if (this.arena == null) this.arena = new DisposeArena();
|
||||
arena.Add(array);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the given array not be disposed when this tracker is disposed.
|
||||
/// This is useful if you want to keep the array around after the tracker has been disposed.
|
||||
/// The array will still be tracked for the purposes of automatic dependency management.
|
||||
/// </summary>
|
||||
public void Persist<T>(NativeArray<T> array) where T : unmanaged {
|
||||
if (this.arena == null) return;
|
||||
arena.Remove(array);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules a raycast batch command.
|
||||
/// Like RaycastCommand.ScheduleBatch, but dependencies are tracked automatically.
|
||||
/// </summary>
|
||||
public JobHandle ScheduleBatch (NativeArray<UnityEngine.RaycastCommand> commands, NativeArray<UnityEngine.RaycastHit> results, int minCommandsPerJob) {
|
||||
if (forceLinearDependencies) {
|
||||
UnityEngine.RaycastCommand.ScheduleBatch(commands, results, minCommandsPerJob).Complete();
|
||||
return default;
|
||||
}
|
||||
|
||||
// Create a dummy structure to allow the analyzer to determine how the job reads/writes data
|
||||
var dummy = new JobRaycastCommandDummy { commands = commands, results = results };
|
||||
var dependencies = JobDependencyAnalyzer<JobRaycastCommandDummy>.GetDependencies(ref dummy, this);
|
||||
var job = UnityEngine.RaycastCommand.ScheduleBatch(commands, results, minCommandsPerJob, dependencies);
|
||||
|
||||
JobDependencyAnalyzer<JobRaycastCommandDummy>.Scheduled(ref dummy, this, job);
|
||||
return job;
|
||||
}
|
||||
|
||||
#if UNITY_2022_2_OR_NEWER
|
||||
/// <summary>
|
||||
/// Schedules an overlap capsule batch command.
|
||||
/// Like OverlapCapsuleCommand.ScheduleBatch, but dependencies are tracked automatically.
|
||||
/// </summary>
|
||||
public JobHandle ScheduleBatch (NativeArray<UnityEngine.OverlapCapsuleCommand> commands, NativeArray<UnityEngine.ColliderHit> results, int minCommandsPerJob) {
|
||||
if (forceLinearDependencies) {
|
||||
UnityEngine.OverlapCapsuleCommand.ScheduleBatch(commands, results, minCommandsPerJob, 1).Complete();
|
||||
return default;
|
||||
}
|
||||
|
||||
// Create a dummy structure to allow the analyzer to determine how the job reads/writes data
|
||||
var dummy = new JobOverlapCapsuleCommandDummy { commands = commands, results = results };
|
||||
var dependencies = JobDependencyAnalyzer<JobOverlapCapsuleCommandDummy>.GetDependencies(ref dummy, this);
|
||||
var job = UnityEngine.OverlapCapsuleCommand.ScheduleBatch(commands, results, minCommandsPerJob, 1, dependencies);
|
||||
|
||||
JobDependencyAnalyzer<JobOverlapCapsuleCommandDummy>.Scheduled(ref dummy, this, job);
|
||||
return job;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedules an overlap sphere batch command.
|
||||
/// Like OverlapSphereCommand.ScheduleBatch, but dependencies are tracked automatically.
|
||||
/// </summary>
|
||||
public JobHandle ScheduleBatch (NativeArray<UnityEngine.OverlapSphereCommand> commands, NativeArray<UnityEngine.ColliderHit> results, int minCommandsPerJob) {
|
||||
if (forceLinearDependencies) {
|
||||
UnityEngine.OverlapSphereCommand.ScheduleBatch(commands, results, minCommandsPerJob, 1).Complete();
|
||||
return default;
|
||||
}
|
||||
|
||||
// Create a dummy structure to allow the analyzer to determine how the job reads/writes data
|
||||
var dummy = new JobOverlapSphereCommandDummy { commands = commands, results = results };
|
||||
var dependencies = JobDependencyAnalyzer<JobOverlapSphereCommandDummy>.GetDependencies(ref dummy, this);
|
||||
var job = UnityEngine.OverlapSphereCommand.ScheduleBatch(commands, results, minCommandsPerJob, 1, dependencies);
|
||||
|
||||
JobDependencyAnalyzer<JobOverlapSphereCommandDummy>.Scheduled(ref dummy, this, job);
|
||||
return job;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>Frees the GCHandle when the JobDependencyTracker is disposed</summary>
|
||||
public void DeferFree (GCHandle handle, JobHandle dependsOn) {
|
||||
if (this.arena == null) this.arena = new DisposeArena();
|
||||
this.arena.Add(handle);
|
||||
}
|
||||
|
||||
#if DEBUG_JOBS
|
||||
internal void JobReadsFrom (JobHandle job, long nativeArrayHash, int jobHash, string jobName)
|
||||
#else
|
||||
internal void JobReadsFrom (JobHandle job, long nativeArrayHash, int jobHash)
|
||||
#endif
|
||||
{
|
||||
for (int j = 0; j < slots.Count; j++) {
|
||||
var slot = slots[j];
|
||||
if (slot.hash == nativeArrayHash) {
|
||||
// If the job only reads from the array then we just add this job to the list of readers
|
||||
slot.lastReads.Add(new JobInstance {
|
||||
handle = job,
|
||||
hash = jobHash,
|
||||
#if DEBUG_JOBS
|
||||
name = jobName,
|
||||
#endif
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG_JOBS
|
||||
internal void JobWritesTo (JobHandle job, long nativeArrayHash, int jobHash, string jobName)
|
||||
#else
|
||||
internal void JobWritesTo (JobHandle job, long nativeArrayHash, int jobHash)
|
||||
#endif
|
||||
{
|
||||
for (int j = 0; j < slots.Count; j++) {
|
||||
var slot = slots[j];
|
||||
if (slot.hash == nativeArrayHash) {
|
||||
// If the job writes to the array then this job is now the last writer
|
||||
slot.lastWrite = new JobInstance {
|
||||
handle = job,
|
||||
hash = jobHash,
|
||||
#if DEBUG_JOBS
|
||||
name = jobName,
|
||||
#endif
|
||||
};
|
||||
slot.lastReads.Clear();
|
||||
// The array no longer contains uninitialized data.
|
||||
// Parts of it may still be uninitialized if the job doesn't write to everything, but that's something that this class cannot track.
|
||||
slot.initialized = true;
|
||||
slot.hasWrite = true;
|
||||
slots[j] = slot;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes this tracker.
|
||||
/// This will pool all used lists which makes the GC happy.
|
||||
///
|
||||
/// Note: It is necessary to call this method to avoid memory leaks if you are using the DeferDispose method. But it's a good thing to do otherwise as well.
|
||||
/// It is automatically called if you are using the ObjectPool<T>.Release method.
|
||||
/// </summary>
|
||||
void Dispose () {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS && UNITY_2022_2_OR_NEWER
|
||||
// Note: This can somehow fail in Unity 2021 and 2022.1, even when calling Complete on all jobs
|
||||
UnityEngine.Assertions.Assert.IsTrue(AllWritesDependency.IsCompleted);
|
||||
#endif
|
||||
for (int i = 0; i < slots.Count; i++) ListPool<JobInstance>.Release(slots[i].lastReads);
|
||||
|
||||
slots.Clear();
|
||||
if (arena != null) arena.DisposeAll();
|
||||
linearDependencies = LinearDependencies.Check;
|
||||
if (dependenciesScratchBuffer.IsCreated) dependenciesScratchBuffer.Dispose();
|
||||
}
|
||||
|
||||
public void ClearMemory () {
|
||||
AllWritesDependency.Complete();
|
||||
Dispose();
|
||||
}
|
||||
|
||||
void IAstarPooledObject.OnEnterPool () {
|
||||
Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public struct TimeSlice {
|
||||
public long endTick;
|
||||
public static readonly TimeSlice Infinite = new TimeSlice { endTick = long.MaxValue };
|
||||
public bool isInfinite => endTick == long.MaxValue;
|
||||
public bool expired => Stopwatch.GetTimestamp() > endTick;
|
||||
|
||||
public static TimeSlice MillisFromNow (float millis) => new TimeSlice { endTick = Stopwatch.GetTimestamp() + (long)(millis * 10000) };
|
||||
}
|
||||
|
||||
public interface IJobTimeSliced : IJob {
|
||||
/// <summary>
|
||||
/// Returns true if the job completed.
|
||||
/// If false is returned this job may be called again until the job completes.
|
||||
/// </summary>
|
||||
bool Execute(TimeSlice timeSlice);
|
||||
}
|
||||
|
||||
/// <summary>Extension methods for IJob and related interfaces</summary>
|
||||
public static class IJobExtensions {
|
||||
struct ManagedJob : IJob {
|
||||
public GCHandle handle;
|
||||
|
||||
public void Execute () {
|
||||
((IJob)handle.Target).Execute();
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
struct ManagedActionJob : IJob {
|
||||
public GCHandle handle;
|
||||
|
||||
public void Execute () {
|
||||
((System.Action)handle.Target)();
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Schedule a job with automatic dependency tracking.
|
||||
/// You need to have "using Pathfinding.Util" in your script to be able to use this extension method.
|
||||
///
|
||||
/// See: <see cref="JobDependencyTracker"/>
|
||||
/// </summary>
|
||||
// TODO: Compare performance impact by using ref this, and ScheduleByRef
|
||||
public static JobHandle Schedule<T>(this T data, JobDependencyTracker tracker) where T : struct, IJob {
|
||||
if (tracker.forceLinearDependencies) {
|
||||
data.Run();
|
||||
return default;
|
||||
} else {
|
||||
var job = data.Schedule(JobDependencyAnalyzer<T>.GetDependencies(ref data, tracker));
|
||||
JobDependencyAnalyzer<T>.Scheduled(ref data, tracker, job);
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Schedules an <see cref="IJobParallelForBatched"/> job with automatic dependency tracking</summary>
|
||||
public static JobHandle ScheduleBatch<T>(this T data, int arrayLength, int minIndicesPerJobCount, JobDependencyTracker tracker, JobHandle additionalDependency = default) where T : struct, IJobParallelForBatched {
|
||||
if (tracker.forceLinearDependencies) {
|
||||
additionalDependency.Complete();
|
||||
//data.ScheduleBatch(arrayLength, minIndicesPerJobCount, additionalDependency).Complete();
|
||||
data.RunBatch(arrayLength);
|
||||
return default;
|
||||
} else {
|
||||
var job = data.ScheduleBatch(arrayLength, minIndicesPerJobCount, JobDependencyAnalyzer<T>.GetDependencies(ref data, tracker, additionalDependency));
|
||||
|
||||
JobDependencyAnalyzer<T>.Scheduled(ref data, tracker, job);
|
||||
return job;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Schedules a managed job to run in the job system</summary>
|
||||
public static JobHandle ScheduleManaged<T>(this T data, JobHandle dependsOn) where T : struct, IJob {
|
||||
return new ManagedJob { handle = GCHandle.Alloc(data) }.Schedule(dependsOn);
|
||||
}
|
||||
|
||||
/// <summary>Schedules a managed job to run in the job system</summary>
|
||||
public static JobHandle ScheduleManaged (this System.Action data, JobHandle dependsOn) {
|
||||
return new ManagedActionJob {
|
||||
handle = GCHandle.Alloc(data)
|
||||
}.Schedule(dependsOn);
|
||||
}
|
||||
|
||||
public static JobHandle GetDependencies<T>(this T data, JobDependencyTracker tracker) where T : struct, IJob {
|
||||
if (tracker.forceLinearDependencies) return default;
|
||||
else return JobDependencyAnalyzer<T>.GetDependencies(ref data, tracker);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes this job in the main thread using a coroutine.
|
||||
/// Usage:
|
||||
/// - 1. Optionally schedule some other jobs before this one (using the dependency tracker)
|
||||
/// - 2. Call job.ExecuteMainThreadJob(tracker)
|
||||
/// - 3. Iterate over the enumerator until it is finished. Call handle.Complete on all yielded job handles. Usually this only yields once, but if you use the <see cref="JobHandleWithMainThreadWork"/> wrapper it will
|
||||
/// yield once for every time slice.
|
||||
/// - 4. Continue scheduling other jobs.
|
||||
///
|
||||
/// You must not schedule other jobs (that may touch the same data) while executing this job.
|
||||
///
|
||||
/// See: <see cref="JobHandleWithMainThreadWork"/>
|
||||
/// </summary>
|
||||
public static IEnumerator<JobHandle> ExecuteMainThreadJob<T>(this T data, JobDependencyTracker tracker) where T : struct, IJobTimeSliced {
|
||||
if (tracker.forceLinearDependencies) {
|
||||
UnityEngine.Profiling.Profiler.BeginSample("Main Thread Work");
|
||||
data.Execute();
|
||||
UnityEngine.Profiling.Profiler.EndSample();
|
||||
yield break;
|
||||
}
|
||||
|
||||
var dependsOn = JobDependencyAnalyzer<T>.GetDependencies(ref data, tracker);
|
||||
yield return dependsOn;
|
||||
|
||||
while (true) {
|
||||
UnityEngine.Profiling.Profiler.BeginSample("Main Thread Work");
|
||||
var didComplete = data.Execute(tracker.timeSlice);
|
||||
UnityEngine.Profiling.Profiler.EndSample();
|
||||
if (didComplete) yield break;
|
||||
else yield return new JobHandle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class JobDependencyAnalyzerAssociated {
|
||||
internal static int[] tempJobDependencyHashes = new int[16];
|
||||
internal static int jobCounter = 1;
|
||||
}
|
||||
|
||||
struct JobDependencyAnalyzer<T> where T : struct {
|
||||
static ReflectionData reflectionData;
|
||||
|
||||
/// <summary>Offset to the m_Buffer field inside each NativeArray<T></summary>
|
||||
// Note: Due to a Unity bug we have to calculate this for NativeArray<int> instead of NativeArray<>. NativeArray<> will return an incorrect value (-16) when using IL2CPP.
|
||||
static readonly int BufferOffset = UnsafeUtility.GetFieldOffset(typeof(NativeArray<int>).GetField("m_Buffer", BindingFlags.Instance | BindingFlags.NonPublic));
|
||||
static readonly int SpanPtrOffset = UnsafeUtility.GetFieldOffset(typeof(UnsafeSpan<int>).GetField("ptr", BindingFlags.Instance | BindingFlags.NonPublic));
|
||||
struct ReflectionData {
|
||||
public int[] fieldOffsets;
|
||||
public bool[] writes;
|
||||
public bool[] checkUninitializedRead;
|
||||
public string[] fieldNames;
|
||||
|
||||
public void Build () {
|
||||
// Find the byte offsets within the struct to all m_Buffer fields in all the native arrays in the struct
|
||||
var fields = new List<int>();
|
||||
var writes = new List<bool>();
|
||||
var reads = new List<bool>();
|
||||
var names = new List<string>();
|
||||
|
||||
Build(typeof(T), fields, writes, reads, names, 0, false, false, false);
|
||||
this.fieldOffsets = fields.ToArray();
|
||||
this.writes = writes.ToArray();
|
||||
this.fieldNames = names.ToArray();
|
||||
this.checkUninitializedRead = reads.ToArray();
|
||||
}
|
||||
|
||||
void Build (System.Type type, List<int> fields, List<bool> writes, List<bool> reads, List<string> names, int offset, bool forceReadOnly, bool forceWriteOnly, bool forceDisableUninitializedCheck) {
|
||||
foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) {
|
||||
if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(NativeArray<>)) {
|
||||
// Handle NativeArrays
|
||||
fields.Add(offset + UnsafeUtility.GetFieldOffset(field) + BufferOffset);
|
||||
writes.Add(!forceReadOnly && field.GetCustomAttribute(typeof(ReadOnlyAttribute)) == null);
|
||||
reads.Add(!forceWriteOnly && !forceDisableUninitializedCheck && field.GetCustomAttribute(typeof(WriteOnlyAttribute)) == null && field.GetCustomAttribute(typeof(DisableUninitializedReadCheckAttribute)) == null);
|
||||
names.Add(field.Name);
|
||||
} else if (field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(UnsafeSpan<>)) {
|
||||
// Handle UnsafeSpans
|
||||
fields.Add(offset + UnsafeUtility.GetFieldOffset(field) + SpanPtrOffset);
|
||||
writes.Add(!forceReadOnly && field.GetCustomAttribute(typeof(ReadOnlyAttribute)) == null);
|
||||
reads.Add(!forceWriteOnly && !forceDisableUninitializedCheck && field.GetCustomAttribute(typeof(WriteOnlyAttribute)) == null && field.GetCustomAttribute(typeof(DisableUninitializedReadCheckAttribute)) == null);
|
||||
names.Add(field.Name);
|
||||
} else if (!field.FieldType.IsPrimitive && field.FieldType.IsValueType && !field.FieldType.IsEnum) {
|
||||
// Recurse to handle nested types
|
||||
bool readOnly = field.GetCustomAttribute(typeof(ReadOnlyAttribute)) != null;
|
||||
bool writeOnly = field.GetCustomAttribute(typeof(WriteOnlyAttribute)) != null;
|
||||
bool disableUninitializedCheck = field.GetCustomAttribute(typeof(DisableUninitializedReadCheckAttribute)) != null;
|
||||
Build(field.FieldType, fields, writes, reads, names, offset + UnsafeUtility.GetFieldOffset(field), readOnly, writeOnly, disableUninitializedCheck);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void initReflectionData () {
|
||||
if (reflectionData.fieldOffsets == null) {
|
||||
reflectionData.Build();
|
||||
}
|
||||
}
|
||||
|
||||
static bool HasHash (int[] hashes, int hash, int count) {
|
||||
for (int i = 0; i < count; i++) if (hashes[i] == hash) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Returns the dependencies for the given job.</summary>
|
||||
/// <param name="data">Job data. Must be allocated on the stack.</param>
|
||||
/// <param name="tracker">The tracker to use for dependency tracking.</param>
|
||||
public static JobHandle GetDependencies (ref T data, JobDependencyTracker tracker) {
|
||||
return GetDependencies(ref data, tracker, default, false);
|
||||
}
|
||||
|
||||
public static JobHandle GetDependencies (ref T data, JobDependencyTracker tracker, JobHandle additionalDependency) {
|
||||
return GetDependencies(ref data, tracker, additionalDependency, true);
|
||||
}
|
||||
|
||||
static JobHandle GetDependencies (ref T data, JobDependencyTracker tracker, JobHandle additionalDependency, bool useAdditionalDependency) {
|
||||
if (!tracker.dependenciesScratchBuffer.IsCreated) tracker.dependenciesScratchBuffer = new NativeArray<JobHandle>(16, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
|
||||
var dependencies = tracker.dependenciesScratchBuffer;
|
||||
var slots = tracker.slots;
|
||||
var dependencyHashes = JobDependencyAnalyzerAssociated.tempJobDependencyHashes;
|
||||
|
||||
int numDependencies = 0;
|
||||
|
||||
initReflectionData();
|
||||
#if DEBUG_JOBS
|
||||
string dependenciesDebug = "";
|
||||
#endif
|
||||
unsafe {
|
||||
// Note: data is a struct. It is stored on the stack and can thus not be moved by the GC.
|
||||
// Therefore we do not need to pin it first.
|
||||
// It is guaranteed to be stored on the stack since the Schedule method takes the data parameter by value and not by reference.
|
||||
byte* dataPtr = (byte*)UnsafeUtility.AddressOf(ref data);
|
||||
|
||||
var offsets = reflectionData.fieldOffsets;
|
||||
for (int i = 0; i < offsets.Length; i++) {
|
||||
// This is the internal value of the m_Buffer field of the NativeArray
|
||||
void* nativeArrayBufferPtr = *(void**)(dataPtr + offsets[i]);
|
||||
|
||||
// Use the pointer as a hash to uniquely identify a NativeArray
|
||||
var hash = (long)nativeArrayBufferPtr;
|
||||
|
||||
//JobDependencyAnalyzerAssociated.iteratingSlotsSampler.Begin();
|
||||
for (int j = 0; j <= slots.Count; j++) {
|
||||
// No slot found. Add a new one
|
||||
if (j == slots.Count) {
|
||||
slots.Add(new JobDependencyTracker.NativeArraySlot {
|
||||
hash = hash,
|
||||
lastWrite = default,
|
||||
lastReads = ListPool<JobDependencyTracker.JobInstance>.Claim(),
|
||||
initialized = true, // We don't know anything about the array, so assume it contains initialized data. JobDependencyTracker.NewNativeArray should be used otherwise.
|
||||
hasWrite = false,
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we know about this NativeArray yet
|
||||
var slot = slots[j];
|
||||
if (slot.hash == hash) {
|
||||
if (reflectionData.checkUninitializedRead[i] && !slot.initialized) {
|
||||
throw new System.InvalidOperationException("A job tries to read from the native array " + typeof(T).Name + "." + reflectionData.fieldNames[i] + " which contains uninitialized data");
|
||||
}
|
||||
|
||||
if (slot.hasWrite && !HasHash(dependencyHashes, slot.lastWrite.hash, numDependencies)) {
|
||||
// Reads/writes always depend on the last write to the native array
|
||||
dependencies[numDependencies] = slot.lastWrite.handle;
|
||||
dependencyHashes[numDependencies] = slot.lastWrite.hash;
|
||||
numDependencies++;
|
||||
if (numDependencies >= dependencies.Length) throw new System.Exception("Too many dependencies for job");
|
||||
#if DEBUG_JOBS
|
||||
dependenciesDebug += slot.lastWrite.name + " ";
|
||||
#endif
|
||||
}
|
||||
|
||||
// If we want to write to the array we additionally depend on all previous reads of the array
|
||||
if (reflectionData.writes[i]) {
|
||||
for (int q = 0; q < slot.lastReads.Count; q++) {
|
||||
if (!HasHash(dependencyHashes, slot.lastReads[q].hash, numDependencies)) {
|
||||
dependencies[numDependencies] = slot.lastReads[q].handle;
|
||||
dependencyHashes[numDependencies] = slot.lastReads[q].hash;
|
||||
numDependencies++;
|
||||
if (numDependencies >= dependencies.Length) throw new System.Exception("Too many dependencies for job");
|
||||
#if DEBUG_JOBS
|
||||
dependenciesDebug += slot.lastReads[q].name + " ";
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useAdditionalDependency) {
|
||||
dependencies[numDependencies] = additionalDependency;
|
||||
numDependencies++;
|
||||
#if DEBUG_JOBS
|
||||
dependenciesDebug += "[additional dependency]";
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DEBUG_JOBS
|
||||
UnityEngine.Debug.Log(typeof(T) + " depends on " + dependenciesDebug);
|
||||
#endif
|
||||
|
||||
if (numDependencies == 0) {
|
||||
return default;
|
||||
} else if (numDependencies == 1) {
|
||||
return dependencies[0];
|
||||
} else {
|
||||
return JobHandle.CombineDependencies(dependencies.Slice(0, numDependencies));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static void Scheduled (ref T data, JobDependencyTracker tracker, JobHandle job) {
|
||||
unsafe {
|
||||
int jobHash = JobDependencyAnalyzerAssociated.jobCounter++;
|
||||
// Note: data is a struct. It is stored on the stack and can thus not be moved by the GC.
|
||||
// Therefore we do not need to pin it first.
|
||||
// It is guaranteed to be stored on the stack since the Schedule method takes the data parameter by value and not by reference.
|
||||
byte* dataPtr = (byte*)UnsafeUtility.AddressOf(ref data);
|
||||
for (int i = 0; i < reflectionData.fieldOffsets.Length; i++) {
|
||||
// This is the internal value of the m_Buffer field of the NativeArray
|
||||
void* nativeArrayBufferPtr = *(void**)(dataPtr + reflectionData.fieldOffsets[i]);
|
||||
|
||||
// Use the pointer as a hash to uniquely identify a NativeArray
|
||||
var hash = (long)nativeArrayBufferPtr;
|
||||
#if DEBUG_JOBS
|
||||
if (reflectionData.writes[i]) tracker.JobWritesTo(job, hash, jobHash, typeof(T).Name);
|
||||
else tracker.JobReadsFrom(job, hash, jobHash, typeof(T).Name);
|
||||
#else
|
||||
if (reflectionData.writes[i]) tracker.JobWritesTo(job, hash, jobHash);
|
||||
else tracker.JobReadsFrom(job, hash, jobHash);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1d49fc8ef50f4358b3813b82043ee07
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
118
Packages/com.arongranberg.astar/Utilities/MeshUtility.cs
Normal file
118
Packages/com.arongranberg.astar/Utilities/MeshUtility.cs
Normal file
@@ -0,0 +1,118 @@
|
||||
using UnityEngine;
|
||||
using Unity.Collections;
|
||||
using Unity.Burst;
|
||||
using Unity.Jobs;
|
||||
using Pathfinding.Collections;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.Util {
|
||||
#if MODULE_COLLECTIONS_2_1_0_OR_NEWER
|
||||
using NativeHashMapInt3Int = Unity.Collections.NativeHashMap<Int3, int>;
|
||||
#else
|
||||
using NativeHashMapInt3Int = Unity.Collections.NativeParallelHashMap<Int3, int>;
|
||||
#endif
|
||||
|
||||
/// <summary>Helper class for working with meshes efficiently</summary>
|
||||
[BurstCompile]
|
||||
static class MeshUtility {
|
||||
public static void GetMeshData (Mesh.MeshDataArray meshData, int meshIndex, out NativeArray<Vector3> vertices, out NativeArray<int> indices) {
|
||||
var rawMeshData = meshData[meshIndex];
|
||||
vertices = new NativeArray<Vector3>(rawMeshData.vertexCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
|
||||
rawMeshData.GetVertices(vertices);
|
||||
int totalIndices = 0;
|
||||
for (int subMeshIndex = 0; subMeshIndex < rawMeshData.subMeshCount; subMeshIndex++) {
|
||||
totalIndices += rawMeshData.GetSubMesh(subMeshIndex).indexCount;
|
||||
}
|
||||
indices = new NativeArray<int>(totalIndices, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
|
||||
int offset = 0;
|
||||
for (int subMeshIndex = 0; subMeshIndex < rawMeshData.subMeshCount; subMeshIndex++) {
|
||||
var submesh = rawMeshData.GetSubMesh(subMeshIndex);
|
||||
rawMeshData.GetIndices(indices.GetSubArray(offset, submesh.indexCount), subMeshIndex);
|
||||
offset += submesh.indexCount;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips triangles such that they are all clockwise in graph space.
|
||||
///
|
||||
/// The triangles may not be clockwise in world space since the graphs can be rotated.
|
||||
///
|
||||
/// The triangles array will be modified in-place.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
public static void MakeTrianglesClockwise (ref UnsafeSpan<Int3> vertices, ref UnsafeSpan<int> triangles) {
|
||||
for (int i = 0; i < triangles.Length; i += 3) {
|
||||
// Make sure the triangle is clockwise in graph space (it may not be in world space since the graphs can be rotated)
|
||||
// Note that we also modify the original triangle array because if the graph is cached then we will re-initialize the nodes from that array and assume all triangles are clockwise.
|
||||
if (!VectorMath.IsClockwiseXZ(vertices[triangles[i+0]], vertices[triangles[i+1]], vertices[triangles[i+2]])) {
|
||||
var tmp = triangles[i+0];
|
||||
triangles[i+0] = triangles[i+2];
|
||||
triangles[i+2] = tmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes duplicate vertices from the array and updates the triangle array.</summary>
|
||||
[BurstCompile]
|
||||
public struct JobRemoveDuplicateVertices : IJob {
|
||||
public NativeList<Int3> vertices;
|
||||
public NativeList<int> triangles;
|
||||
public NativeList<int> tags;
|
||||
|
||||
public static int3 cross(int3 x, int3 y) => (x * y.yzx - x.yzx * y).yzx;
|
||||
|
||||
public void Execute () {
|
||||
int numDegenerate = 0;
|
||||
unsafe {
|
||||
var firstVerts = new NativeHashMapInt3Int(vertices.Length, Allocator.Temp);
|
||||
|
||||
// Remove duplicate vertices
|
||||
var compressedPointers = new NativeArray<int>(vertices.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
|
||||
|
||||
var trianglesSpan = triangles.AsUnsafeSpan().Reinterpret<int3>(4);
|
||||
var verticesSpan = vertices.AsUnsafeSpan();
|
||||
var tagsSpan = tags.AsUnsafeSpan();
|
||||
|
||||
int vertexCount = 0;
|
||||
for (int i = 0; i < verticesSpan.length; i++) {
|
||||
if (firstVerts.TryAdd(verticesSpan[i], vertexCount)) {
|
||||
compressedPointers[i] = vertexCount;
|
||||
verticesSpan[vertexCount++] = verticesSpan[i];
|
||||
} else {
|
||||
// There are some cases, rare but still there, that vertices are identical
|
||||
compressedPointers[i] = firstVerts[vertices[i]];
|
||||
}
|
||||
}
|
||||
vertices.Length = vertexCount;
|
||||
|
||||
var verticesSpanI3 = vertices.AsUnsafeSpan().Reinterpret<int3>();
|
||||
|
||||
uint triCount = 0;
|
||||
for (uint ti = 0; ti < trianglesSpan.length; ti++) {
|
||||
var tri = trianglesSpan[ti];
|
||||
tri = new int3(compressedPointers[tri.x], compressedPointers[tri.y], compressedPointers[tri.z]);
|
||||
|
||||
// In some cases, users feed a navmesh graph a mesh with degenerate triangles.
|
||||
// These are triangles with a zero area.
|
||||
// We must remove these as they can otherwise cause issues for the JobCalculateTriangleConnections job, and they are generally just bad to include a navmesh.
|
||||
// Note: This cross product calculation can result in overflows if the triangle is large, but since we check for equality with zero it should not be a problem in practice.
|
||||
if (math.all(cross(verticesSpanI3[tri.y] - verticesSpanI3[tri.x], verticesSpanI3[tri.z] - verticesSpanI3[tri.x]) == 0)) {
|
||||
// Degenerate triangle
|
||||
numDegenerate++;
|
||||
continue;
|
||||
}
|
||||
trianglesSpan[triCount] = tri;
|
||||
tagsSpan[triCount] = tagsSpan[ti];
|
||||
triCount++;
|
||||
}
|
||||
|
||||
triangles.Length = (int)triCount * 3;
|
||||
tags.Length = (int)triCount;
|
||||
}
|
||||
if (numDegenerate > 0) {
|
||||
Debug.LogWarning($"Input mesh contained {numDegenerate} degenerate triangles. These have been removed.\nA degenerate triangle is a triangle with zero area. It resembles a line or a point.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8c56d04719caa93e8baf69ed1ce8fb0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1986
Packages/com.arongranberg.astar/Utilities/PathTracer.cs
Normal file
1986
Packages/com.arongranberg.astar/Utilities/PathTracer.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
Packages/com.arongranberg.astar/Utilities/PathTracer.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Utilities/PathTracer.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ecd3295b97b21b4491934afc7c8a60b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
748
Packages/com.arongranberg.astar/Utilities/PathUtilities.cs
Normal file
748
Packages/com.arongranberg.astar/Utilities/PathUtilities.cs
Normal file
@@ -0,0 +1,748 @@
|
||||
using Pathfinding.Util;
|
||||
using Pathfinding.Pooling;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Jobs;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
|
||||
namespace Pathfinding {
|
||||
/// <summary>
|
||||
/// Contains useful functions for working with paths and nodes.
|
||||
/// This class works a lot with the <see cref="Pathfinding.GraphNode"/> class, a useful function to get nodes is AstarPath.GetNearest.
|
||||
/// See: <see cref="AstarPath.GetNearest"/>
|
||||
/// See: <see cref="Pathfinding.GraphUpdateUtilities"/>
|
||||
/// See: <see cref="Pathfinding.GraphUtilities"/>
|
||||
/// </summary>
|
||||
public static class PathUtilities {
|
||||
/// <summary>
|
||||
/// Returns if there is a walkable path from node1 to node2.
|
||||
/// This method is extremely fast because it only uses precalculated information.
|
||||
///
|
||||
/// <code>
|
||||
/// GraphNode node1 = AstarPath.active.GetNearest(point1, NNConstraint.Walkable).node;
|
||||
/// GraphNode node2 = AstarPath.active.GetNearest(point2, NNConstraint.Walkable).node;
|
||||
///
|
||||
/// if (PathUtilities.IsPathPossible(node1, node2)) {
|
||||
/// // Yay, there is a path between those two nodes
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// Equivalent to calling <see cref="IsPathPossible(List<GraphNode>)"/> with a list containing node1 and node2.
|
||||
///
|
||||
/// See: graph-updates (view in online documentation for working links)
|
||||
/// See: <see cref="AstarPath.GetNearest"/>
|
||||
/// See: <see cref="Pathfinding.HierarchicalGraph"/>
|
||||
/// </summary>
|
||||
public static bool IsPathPossible (GraphNode node1, GraphNode node2) {
|
||||
return node1.Walkable && node2.Walkable && node1.Area == node2.Area;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if there are walkable paths between all nodes in the list.
|
||||
///
|
||||
/// Returns true for empty lists.
|
||||
///
|
||||
/// See: graph-updates (view in online documentation for working links)
|
||||
/// See: <see cref="AstarPath.GetNearest"/>
|
||||
/// </summary>
|
||||
public static bool IsPathPossible (List<GraphNode> nodes) {
|
||||
if (nodes.Count == 0) return true;
|
||||
|
||||
uint area = nodes[0].Area;
|
||||
for (int i = 0; i < nodes.Count; i++) if (!nodes[i].Walkable || nodes[i].Area != area) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns if there are walkable paths between all nodes in the list.
|
||||
///
|
||||
/// This method will actually only check if the first node can reach all other nodes. However this is
|
||||
/// equivalent in 99% of the cases since almost always the graph connections are bidirectional.
|
||||
/// If you are not aware of any cases where you explicitly create unidirectional connections
|
||||
/// this method can be used without worries.
|
||||
///
|
||||
/// Returns true for empty lists
|
||||
///
|
||||
/// Warning: This method is significantly slower than the IsPathPossible method which does not take a tagMask
|
||||
///
|
||||
/// See: graph-updates (view in online documentation for working links)
|
||||
/// See: <see cref="AstarPath.GetNearest"/>
|
||||
/// </summary>
|
||||
public static bool IsPathPossible (List<GraphNode> nodes, int tagMask) {
|
||||
if (nodes.Count == 0) return true;
|
||||
|
||||
// Make sure that the first node has a valid tag
|
||||
if (((tagMask >> (int)nodes[0].Tag) & 1) == 0) return false;
|
||||
|
||||
// Fast check first
|
||||
if (!IsPathPossible(nodes)) return false;
|
||||
|
||||
// Make sure that the first node can reach all other nodes
|
||||
var reachable = GetReachableNodes(nodes[0], tagMask);
|
||||
bool result = true;
|
||||
|
||||
// Make sure that the first node can reach all other nodes
|
||||
for (int i = 1; i < nodes.Count; i++) {
|
||||
if (!reachable.Contains(nodes[i])) {
|
||||
result = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pool the temporary list
|
||||
ListPool<GraphNode>.Release(ref reachable);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all nodes reachable from the seed node.
|
||||
/// This function performs a DFS (depth-first-search) or flood fill of the graph and returns all nodes which can be reached from
|
||||
/// the seed node. In almost all cases this will be identical to returning all nodes which have the same area as the seed node.
|
||||
/// In the editor areas are displayed as different colors of the nodes.
|
||||
/// The only case where it will not be so is when there is a one way path from some part of the area to the seed node
|
||||
/// but no path from the seed node to that part of the graph.
|
||||
///
|
||||
/// The returned list is not sorted in any particular way.
|
||||
///
|
||||
/// Depending on the number of reachable nodes, this function can take quite some time to calculate
|
||||
/// so don't use it too often or it might affect the framerate of your game.
|
||||
///
|
||||
/// See: bitmasks (view in online documentation for working links).
|
||||
///
|
||||
/// Returns: A List<Node> containing all nodes reachable from the seed node.
|
||||
/// For better memory management the returned list should be pooled, see Pathfinding.Pooling.ListPool.
|
||||
/// </summary>
|
||||
/// <param name="seed">The node to start the search from.</param>
|
||||
/// <param name="tagMask">Optional mask for tags. This is a bitmask.</param>
|
||||
/// <param name="filter">Optional filter for which nodes to search. You can combine this with tagMask = -1 to make the filter determine everything.
|
||||
/// Only walkable nodes are searched regardless of the filter. If the filter function returns false the node will be treated as unwalkable.</param>
|
||||
public static List<GraphNode> GetReachableNodes (GraphNode seed, int tagMask = -1, System.Func<GraphNode, bool> filter = null) {
|
||||
Stack<GraphNode> dfsStack = StackPool<GraphNode>.Claim();
|
||||
List<GraphNode> reachable = ListPool<GraphNode>.Claim();
|
||||
|
||||
/// <summary>TODO: Pool</summary>
|
||||
var map = new HashSet<GraphNode>();
|
||||
|
||||
System.Action<GraphNode> callback;
|
||||
// Check if we can use the fast path
|
||||
if (tagMask == -1 && filter == null) {
|
||||
callback = (GraphNode node) => {
|
||||
if (node.Walkable && map.Add(node)) {
|
||||
reachable.Add(node);
|
||||
dfsStack.Push(node);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
callback = (GraphNode node) => {
|
||||
if (node.Walkable && ((tagMask >> (int)node.Tag) & 0x1) != 0 && map.Add(node)) {
|
||||
if (filter != null && !filter(node)) return;
|
||||
|
||||
reachable.Add(node);
|
||||
dfsStack.Push(node);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
callback(seed);
|
||||
|
||||
while (dfsStack.Count > 0) {
|
||||
dfsStack.Pop().GetConnections(callback);
|
||||
}
|
||||
|
||||
StackPool<GraphNode>.Release(dfsStack);
|
||||
return reachable;
|
||||
}
|
||||
|
||||
static Queue<GraphNode> BFSQueue;
|
||||
static Dictionary<GraphNode, int> BFSMap;
|
||||
|
||||
/// <summary>
|
||||
/// Returns all nodes up to a given node-distance from the seed node.
|
||||
/// This function performs a BFS (<a href="https://en.wikipedia.org/wiki/Breadth-first_search">breadth-first search</a>) or flood fill of the graph and returns all nodes within a specified node distance which can be reached from
|
||||
/// the seed node. In almost all cases when depth is large enough this will be identical to returning all nodes which have the same area as the seed node.
|
||||
/// In the editor areas are displayed as different colors of the nodes.
|
||||
/// The only case where it will not be so is when there is a one way path from some part of the area to the seed node
|
||||
/// but no path from the seed node to that part of the graph.
|
||||
///
|
||||
/// The returned list is sorted by node distance from the seed node
|
||||
/// i.e distance is measured in the number of nodes the shortest path from seed to that node would pass through.
|
||||
/// Note that the distance measurement does not take heuristics, penalties or tag penalties.
|
||||
///
|
||||
/// Depending on the number of nodes, this function can take quite some time to calculate
|
||||
/// so don't use it too often or it might affect the framerate of your game.
|
||||
///
|
||||
/// Returns: A List<GraphNode> containing all nodes reachable up to a specified node distance from the seed node.
|
||||
/// For better memory management the returned list should be pooled, see Pathfinding.Pooling.ListPool
|
||||
///
|
||||
/// Warning: This method is not thread safe. Only use it from the Unity thread (i.e normal game code).
|
||||
///
|
||||
/// The video below shows the BFS result with varying values of depth. Points are sampled on the nodes using <see cref="GetPointsOnNodes"/>.
|
||||
/// [Open online documentation to see videos]
|
||||
///
|
||||
/// <code>
|
||||
/// var seed = AstarPath.active.GetNearest(transform.position, NNConstraint.Walkable).node;
|
||||
/// var nodes = PathUtilities.BFS(seed, 10);
|
||||
/// foreach (var node in nodes) {
|
||||
/// Debug.DrawRay((Vector3)node.position, Vector3.up, Color.red, 10);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="seed">The node to start the search from.</param>
|
||||
/// <param name="depth">The maximum node-distance from the seed node.</param>
|
||||
/// <param name="tagMask">Optional mask for tags. This is a bitmask.</param>
|
||||
/// <param name="filter">Optional filter for which nodes to search. You can combine this with depth = int.MaxValue and tagMask = -1 to make the filter determine everything.
|
||||
/// Only walkable nodes are searched regardless of the filter. If the filter function returns false the node will be treated as unwalkable.</param>
|
||||
public static List<GraphNode> BFS (GraphNode seed, int depth, int tagMask = -1, System.Func<GraphNode, bool> filter = null) {
|
||||
BFSQueue = BFSQueue ?? new Queue<GraphNode>();
|
||||
var que = BFSQueue;
|
||||
|
||||
BFSMap = BFSMap ?? new Dictionary<GraphNode, int>();
|
||||
var map = BFSMap;
|
||||
|
||||
// Even though we clear at the end of this function, it is good to
|
||||
// do it here as well in case the previous invocation of the method
|
||||
// threw an exception for some reason
|
||||
// and didn't clear the que and map
|
||||
que.Clear();
|
||||
map.Clear();
|
||||
|
||||
List<GraphNode> result = ListPool<GraphNode>.Claim();
|
||||
|
||||
int currentDist = -1;
|
||||
System.Action<GraphNode> callback;
|
||||
if (tagMask == -1) {
|
||||
callback = node => {
|
||||
if (node.Walkable && !map.ContainsKey(node)) {
|
||||
if (filter != null && !filter(node)) return;
|
||||
|
||||
map.Add(node, currentDist+1);
|
||||
result.Add(node);
|
||||
que.Enqueue(node);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
callback = node => {
|
||||
if (node.Walkable && ((tagMask >> (int)node.Tag) & 0x1) != 0 && !map.ContainsKey(node)) {
|
||||
if (filter != null && !filter(node)) return;
|
||||
|
||||
map.Add(node, currentDist+1);
|
||||
result.Add(node);
|
||||
que.Enqueue(node);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
callback(seed);
|
||||
|
||||
while (que.Count > 0) {
|
||||
GraphNode n = que.Dequeue();
|
||||
currentDist = map[n];
|
||||
|
||||
if (currentDist >= depth) break;
|
||||
|
||||
n.GetConnections(callback);
|
||||
}
|
||||
|
||||
que.Clear();
|
||||
map.Clear();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns points in a spiral centered around the origin with a minimum clearance from other points.
|
||||
/// The points are laid out on the involute of a circle
|
||||
/// See: http://en.wikipedia.org/wiki/Involute
|
||||
/// Which has some nice properties.
|
||||
/// All points are separated by clearance world units.
|
||||
/// This method is O(n), yes if you read the code you will see a binary search, but that binary search
|
||||
/// has an upper bound on the number of steps, so it does not yield a log factor.
|
||||
///
|
||||
/// Note: Consider recycling the list after usage to reduce allocations.
|
||||
/// See: Pathfinding.Pooling.ListPool
|
||||
/// </summary>
|
||||
public static List<Vector3> GetSpiralPoints (int count, float clearance) {
|
||||
List<Vector3> pts = ListPool<Vector3>.Claim(count);
|
||||
|
||||
// The radius of the smaller circle used for generating the involute of a circle
|
||||
// Calculated from the separation distance between the turns
|
||||
float a = clearance/(2*Mathf.PI);
|
||||
float t = 0;
|
||||
|
||||
|
||||
pts.Add(InvoluteOfCircle(a, t));
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
Vector3 prev = pts[pts.Count-1];
|
||||
|
||||
// d = -t0/2 + sqrt( t0^2/4 + 2d/a )
|
||||
// Minimum angle (radians) which would create an arc distance greater than clearance
|
||||
float d = -t/2 + Mathf.Sqrt(t*t/4 + 2*clearance/a);
|
||||
|
||||
// Binary search for separating this point and the previous one
|
||||
float mn = t + d;
|
||||
float mx = t + 2*d;
|
||||
while (mx - mn > 0.01f) {
|
||||
float mid = (mn + mx)/2;
|
||||
Vector3 p = InvoluteOfCircle(a, mid);
|
||||
if ((p - prev).sqrMagnitude < clearance*clearance) {
|
||||
mn = mid;
|
||||
} else {
|
||||
mx = mid;
|
||||
}
|
||||
}
|
||||
|
||||
pts.Add(InvoluteOfCircle(a, mx));
|
||||
t = mx;
|
||||
}
|
||||
|
||||
return pts;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the XZ coordinate of the involute of circle.
|
||||
/// See: http://en.wikipedia.org/wiki/Involute
|
||||
/// </summary>
|
||||
private static Vector3 InvoluteOfCircle (float a, float t) {
|
||||
return new Vector3(a*(Mathf.Cos(t) + t*Mathf.Sin(t)), 0, a*(Mathf.Sin(t) - t*Mathf.Cos(t)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will calculate a number of points around p which are on the graph and are separated by clearance from each other.
|
||||
/// This is like GetPointsAroundPoint except that previousPoints are treated as being in world space.
|
||||
/// The average of the points will be found and then that will be treated as the group center.
|
||||
/// </summary>
|
||||
/// <param name="p">The point to generate points around</param>
|
||||
/// <param name="g">The graph to use for linecasting. If you are only using one graph, you can get this by AstarPath.active.graphs[0] as IRaycastableGraph.
|
||||
/// Note that not all graphs are raycastable, recast, navmesh and grid graphs are raycastable. On recast and navmesh it works the best.</param>
|
||||
/// <param name="previousPoints">The points to use for reference. Note that these are in world space.
|
||||
/// The new points will overwrite the existing points in the list. The result will be in world space.</param>
|
||||
/// <param name="radius">The final points will be at most this distance from p.</param>
|
||||
/// <param name="clearanceRadius">The points will if possible be at least this distance from each other.</param>
|
||||
public static void GetPointsAroundPointWorld (Vector3 p, IRaycastableGraph g, List<Vector3> previousPoints, float radius, float clearanceRadius) {
|
||||
if (previousPoints.Count == 0) return;
|
||||
|
||||
Vector3 avg = Vector3.zero;
|
||||
for (int i = 0; i < previousPoints.Count; i++) avg += previousPoints[i];
|
||||
avg /= previousPoints.Count;
|
||||
|
||||
for (int i = 0; i < previousPoints.Count; i++) previousPoints[i] -= avg;
|
||||
|
||||
GetPointsAroundPoint(p, g, previousPoints, radius, clearanceRadius);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Will calculate a number of points around center which are on the graph and are separated by clearance from each other.
|
||||
/// The maximum distance from center to any point will be radius.
|
||||
/// Points will first be tried to be laid out as previousPoints and if that fails, random points will be selected.
|
||||
/// This is great if you want to pick a number of target points for group movement. If you pass all current agent points from e.g the group's average position
|
||||
/// this method will return target points so that the units move very little within the group, this is often aesthetically pleasing and reduces jitter if using
|
||||
/// some kind of local avoidance.
|
||||
///
|
||||
/// TODO: Write unit tests
|
||||
/// </summary>
|
||||
/// <param name="center">The point to generate points around</param>
|
||||
/// <param name="g">The graph to use for linecasting. If you are only using one graph, you can get this by AstarPath.active.graphs[0] as IRaycastableGraph.
|
||||
/// Note that not all graphs are raycastable, recast, navmesh and grid graphs are raycastable. On recast and navmesh it works the best.</param>
|
||||
/// <param name="previousPoints">The points to use for reference. Note that these should not be in world space. They are treated as relative to center.
|
||||
/// The new points will overwrite the existing points in the list. The result will be in world space, not relative to center.</param>
|
||||
/// <param name="radius">The final points will be at most this distance from center.</param>
|
||||
/// <param name="clearanceRadius">The points will if possible be at least this distance from each other.</param>
|
||||
public static void GetPointsAroundPoint (Vector3 center, IRaycastableGraph g, List<Vector3> previousPoints, float radius, float clearanceRadius) {
|
||||
if (g == null) throw new System.ArgumentNullException("g");
|
||||
|
||||
var graph = g as NavGraph;
|
||||
|
||||
if (graph == null) throw new System.ArgumentException("g is not a NavGraph");
|
||||
|
||||
var nn = graph.GetNearest(center, NNConstraint.Walkable);
|
||||
center = nn.position;
|
||||
|
||||
if (nn.node == null) {
|
||||
// No valid point to start from
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Make sure the enclosing circle has a radius which can pack circles with packing density 0.5
|
||||
radius = Mathf.Max(radius, 1.4142f*clearanceRadius*Mathf.Sqrt(previousPoints.Count)); //Mathf.Sqrt(previousPoints.Count*clearanceRadius*2));
|
||||
clearanceRadius *= clearanceRadius;
|
||||
|
||||
for (int i = 0; i < previousPoints.Count; i++) {
|
||||
Vector3 dir = previousPoints[i];
|
||||
float magn = dir.magnitude;
|
||||
|
||||
if (magn > 0) dir /= magn;
|
||||
|
||||
float newMagn = radius;//magn > radius ? radius : magn;
|
||||
dir *= newMagn;
|
||||
|
||||
GraphHitInfo hit;
|
||||
|
||||
int tests = 0;
|
||||
while (true) {
|
||||
Vector3 pt = center + dir;
|
||||
|
||||
if (g.Linecast(center, pt, out hit)) {
|
||||
if (hit.point == Vector3.zero) {
|
||||
// Oops, linecast actually failed completely
|
||||
// try again unless we have tried lots of times
|
||||
// then we just continue anyway
|
||||
tests++;
|
||||
if (tests > 8) {
|
||||
previousPoints[i] = pt;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
pt = hit.point;
|
||||
}
|
||||
}
|
||||
|
||||
bool worked = false;
|
||||
|
||||
for (float q = 0.1f; q <= 1.0f; q += 0.05f) {
|
||||
Vector3 qt = Vector3.Lerp(center, pt, q);
|
||||
worked = true;
|
||||
for (int j = 0; j < i; j++) {
|
||||
if ((previousPoints[j] - qt).sqrMagnitude < clearanceRadius) {
|
||||
worked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Abort after 8 tests or when we have found a valid point
|
||||
if (worked || tests > 8) {
|
||||
worked = true;
|
||||
previousPoints[i] = qt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Break out of nested loop
|
||||
if (worked) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If we could not find a valid point, reduce the clearance radius slightly to improve
|
||||
// the chances next time
|
||||
clearanceRadius *= 0.9f;
|
||||
// This will pick points in 2D closer to the edge of the circle with a higher probability
|
||||
dir = UnityEngine.Random.onUnitSphere * Mathf.Lerp(newMagn, radius, tests / 5);
|
||||
dir.y = 0;
|
||||
tests++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[BurstCompile(FloatMode = FloatMode.Fast)]
|
||||
struct JobFormationPacked : IJob {
|
||||
public NativeArray<float3> positions;
|
||||
public float3 destination;
|
||||
public float agentRadius;
|
||||
public NativeMovementPlane movementPlane;
|
||||
|
||||
public float CollisionTime (float2 pos1, float2 pos2, float2 v1, float2 v2, float r1, float r2) {
|
||||
var relativeVelocity = v1 - v2;
|
||||
|
||||
if (math.all(relativeVelocity == float2.zero)) {
|
||||
// No collision
|
||||
return float.MaxValue;
|
||||
}
|
||||
var radius = r1 + r2;
|
||||
var relativePos = pos2 - pos1;
|
||||
var relativeDir = math.normalize(relativeVelocity);
|
||||
var d1 = math.dot(relativePos, relativeDir);
|
||||
var d2sq = math.lengthsq(relativePos - relativeDir * d1);
|
||||
var offsetSq = radius*radius - d2sq;
|
||||
if (offsetSq <= 0) {
|
||||
// No collision
|
||||
return float.MaxValue;
|
||||
}
|
||||
var offset = math.sqrt(offsetSq);
|
||||
var collisionDistance = d1 - offset;
|
||||
if (collisionDistance < -radius) {
|
||||
// No collision (collision is in the imagined past)
|
||||
return float.MaxValue;
|
||||
}
|
||||
return collisionDistance * math.rsqrt(math.lengthsq(relativeVelocity));
|
||||
//return collisionDistance / math.length(relativeVelocity);
|
||||
}
|
||||
|
||||
struct DistanceComparer : IComparer<int> {
|
||||
public NativeArray<float2> positions;
|
||||
|
||||
public int Compare (int x, int y) {
|
||||
return (int)math.sign(math.lengthsq(positions[x]) - math.lengthsq(positions[y]));
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute () {
|
||||
if (positions.Length == 0) return;
|
||||
|
||||
NativeArray<float2> positions2D = new NativeArray<float2>(positions.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
|
||||
NativeArray<int> indices = new NativeArray<int>(positions.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
|
||||
for (int i = 0; i < positions.Length; i++) {
|
||||
positions2D[i] = movementPlane.ToPlane(positions[i]);
|
||||
indices[i] = i;
|
||||
}
|
||||
|
||||
float2 mean = float2.zero;
|
||||
for (int i = 0; i < positions2D.Length; i++) {
|
||||
mean += positions2D[i];
|
||||
}
|
||||
mean /= positions2D.Length;
|
||||
|
||||
for (int i = 0; i < positions2D.Length; i++) {
|
||||
positions2D[i] -= mean;
|
||||
}
|
||||
|
||||
// Sort agents by their distance to the center
|
||||
indices.Sort(new DistanceComparer { positions = positions2D });
|
||||
|
||||
NativeArray<float> minTimes = new NativeArray<float>(positions.Length, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
|
||||
for (int a = 0; a < positions.Length; a++) {
|
||||
var ta = float.MaxValue;
|
||||
var ia = indices[a];
|
||||
for (int b = 0; b < a; b++) {
|
||||
var ib = indices[b];
|
||||
//float tb = CollisionTime(positions2D[ia], positions2D[ib], -positions2D[ia], -positions2D[ib], agentRadius, agentRadius);
|
||||
float tb = CollisionTime(positions2D[ia], positions2D[ib], -positions2D[ia], float2.zero, agentRadius, agentRadius);
|
||||
ta = math.min(ta, tb);
|
||||
}
|
||||
minTimes[ia] = ta;
|
||||
positions2D[ia] -= positions2D[ia] * math.min(1.0f, minTimes[indices[a]]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < positions.Length; i++) {
|
||||
positions[i] = movementPlane.ToWorld(positions2D[i]) + destination;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void FormationPacked (List<Vector3> currentPositions, Vector3 destination, float clearanceRadius, NativeMovementPlane movementPlane) {
|
||||
var positions = new NativeArray<float3>(currentPositions.Count, Allocator.TempJob, NativeArrayOptions.UninitializedMemory);
|
||||
|
||||
for (int i = 0; i < positions.Length; i++) positions[i] = currentPositions[i];
|
||||
new JobFormationPacked {
|
||||
positions = positions,
|
||||
destination = destination,
|
||||
agentRadius = clearanceRadius,
|
||||
movementPlane = movementPlane,
|
||||
}.Schedule().Complete();
|
||||
for (int i = 0; i < positions.Length; i++) currentPositions[i] = positions[i];
|
||||
positions.Dispose();
|
||||
}
|
||||
|
||||
public enum FormationMode {
|
||||
SinglePoint,
|
||||
Packed,
|
||||
}
|
||||
|
||||
public static List<Vector3> FormationDestinations (List<IAstarAI> group, Vector3 destination, FormationMode formationMode, float marginFactor = 0.1f) {
|
||||
if (group.Count == 0) return new List<Vector3>();
|
||||
|
||||
var positions = group.Select(u => u.position).ToList();
|
||||
|
||||
if (formationMode == FormationMode.SinglePoint) {
|
||||
for (int i = 0; i < positions.Count; i++) positions[i] = destination;
|
||||
} else {
|
||||
var previousMean = Vector3.zero;
|
||||
for (int i = 0; i < positions.Count; i++) previousMean += positions[i];
|
||||
previousMean /= positions.Count;
|
||||
|
||||
// Assume the whole group uses the same movement plane, or at least a similar one
|
||||
var movementPlane = group[0].movementPlane;
|
||||
Debug.Log(((Quaternion)movementPlane.rotation).eulerAngles);
|
||||
|
||||
// Figure out if the group if the destination point is in the middle of the group,
|
||||
// or if it is outside the group
|
||||
var standardDeviation = Mathf.Sqrt(positions.Average(p => Vector3.SqrMagnitude(p - previousMean)));
|
||||
var thresholdDistance = standardDeviation*1.0f;
|
||||
|
||||
if (Vector3.Distance(destination, previousMean) > thresholdDistance) {
|
||||
// If the destination is outside of the group, use a packed formation
|
||||
Pathfinding.PathUtilities.FormationPacked(positions, destination, group[0].radius * (1 + marginFactor), movementPlane);
|
||||
} else {
|
||||
// If the destination is inside the group, move all agents to the same point
|
||||
for (int i = 0; i < positions.Count; i++) positions[i] = destination;
|
||||
}
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
class ConstrainToSet : NNConstraint {
|
||||
public HashSet<GraphNode> nodes;
|
||||
|
||||
public override bool Suitable (GraphNode node) {
|
||||
return nodes.Contains(node);
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetPointsAroundPointWorldFlexible (Vector3 center, Quaternion rotation, List<Vector3> positions) {
|
||||
if (positions.Count == 0) return;
|
||||
|
||||
var snapped = AstarPath.active.GetNearest(center, NNConstraint.Walkable);
|
||||
|
||||
// Move slightly toward the node center just to avoid the group center being on a node edge
|
||||
var groupPos = Vector3.Lerp(snapped.position, (Vector3)snapped.node.position, 0.001f);
|
||||
|
||||
var previousMean = Vector3.zero;
|
||||
for (int i = 0; i < positions.Count; i++) previousMean += positions[i];
|
||||
previousMean /= positions.Count;
|
||||
|
||||
var maxSqrDistance = 0f;
|
||||
for (int i = 0; i < positions.Count; i++) {
|
||||
positions[i] -= previousMean;
|
||||
maxSqrDistance = Mathf.Max(maxSqrDistance, positions[i].sqrMagnitude);
|
||||
}
|
||||
|
||||
// Multiplying by 4 doubles the normal distance
|
||||
maxSqrDistance *= 2*2;
|
||||
|
||||
// Search at least this number of nodes regardless of the distance to the nodes
|
||||
int minNodes = 10;
|
||||
|
||||
var nodes = PathUtilities.BFS(snapped.node, int.MaxValue, -1, node => {
|
||||
minNodes--;
|
||||
return minNodes > 0 || ((Vector3)node.position - groupPos).sqrMagnitude < maxSqrDistance;
|
||||
});
|
||||
|
||||
NNConstraint nn = new ConstrainToSet() {
|
||||
nodes = new HashSet<GraphNode>(nodes)
|
||||
};
|
||||
|
||||
int iterations = 3;
|
||||
for (int k = 0; k < iterations; k++) {
|
||||
float totalWeight = 0f;
|
||||
Vector3 totalSum = Vector3.zero;
|
||||
|
||||
for (int i = 0; i < positions.Count; i++) {
|
||||
var rel = rotation * positions[i];
|
||||
var p = groupPos + rel;
|
||||
|
||||
var near = AstarPath.active.GetNearest(p, nn).position;
|
||||
// TODO: Handle case when no close node was found
|
||||
|
||||
var weight = Vector3.Distance(p, near);
|
||||
|
||||
totalSum += (near - rel) * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
|
||||
// If no changes were required, then break early
|
||||
if (totalWeight <= 0.0000001f) break;
|
||||
|
||||
var newCenter = totalSum / totalWeight;
|
||||
|
||||
groupPos = AstarPath.active.GetNearest(newCenter, nn).position;
|
||||
}
|
||||
|
||||
for (int i = 0; i < positions.Count; i++) {
|
||||
positions[i] = groupPos + rotation * positions[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns randomly selected points on the specified nodes with each point being separated by clearanceRadius from each other.
|
||||
/// Selecting points ON the nodes only works for TriangleMeshNode (used by Recast Graph and Navmesh Graph) and GridNode (used by GridGraph).
|
||||
/// For other node types, only the positions of the nodes will be used.
|
||||
///
|
||||
/// clearanceRadius will be reduced if no valid points can be found.
|
||||
///
|
||||
/// Note: This method assumes that the nodes in the list have the same type for some special cases.
|
||||
/// More specifically if the first node is not a TriangleMeshNode or a GridNode, it will use a fast path
|
||||
/// which assumes that all nodes in the list have the same surface area (which usually is a surface area of zero and the
|
||||
/// nodes are all PointNodes).
|
||||
/// </summary>
|
||||
public static List<Vector3> GetPointsOnNodes (List<GraphNode> nodes, int count, float clearanceRadius = 0) {
|
||||
if (nodes == null) throw new System.ArgumentNullException("nodes");
|
||||
if (nodes.Count == 0) throw new System.ArgumentException("no nodes passed");
|
||||
|
||||
List<Vector3> pts = ListPool<Vector3>.Claim(count);
|
||||
|
||||
// Square
|
||||
clearanceRadius *= clearanceRadius;
|
||||
|
||||
if (clearanceRadius > 0 || nodes[0] is TriangleMeshNode
|
||||
#if !ASTAR_NO_GRID_GRAPH
|
||||
|| nodes[0] is GridNode
|
||||
#endif
|
||||
) {
|
||||
// Accumulated area of all nodes
|
||||
List<float> accs = ListPool<float>.Claim(nodes.Count);
|
||||
|
||||
// Total area of all nodes so far
|
||||
float tot = 0;
|
||||
|
||||
for (int i = 0; i < nodes.Count; i++) {
|
||||
var surfaceArea = nodes[i].SurfaceArea();
|
||||
// Ensures that even if the nodes have a surface area of 0, a random one will still be picked
|
||||
// instead of e.g always picking the first or the last one.
|
||||
surfaceArea += 0.001f;
|
||||
tot += surfaceArea;
|
||||
accs.Add(tot);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
// Pick point
|
||||
int testCount = 0;
|
||||
int testLimit = 10;
|
||||
|
||||
while (true) {
|
||||
bool worked = true;
|
||||
|
||||
// If no valid points could be found, progressively lower the clearance radius until such a point is found
|
||||
if (testCount >= testLimit) {
|
||||
// Note that clearanceRadius is a squared radius
|
||||
clearanceRadius *= 0.9f*0.9f;
|
||||
testLimit += 10;
|
||||
if (testLimit > 100) clearanceRadius = 0;
|
||||
}
|
||||
|
||||
// Pick a random node among the ones in the list weighted by their area
|
||||
float tg = UnityEngine.Random.value*tot;
|
||||
int v = accs.BinarySearch(tg);
|
||||
if (v < 0) v = ~v;
|
||||
|
||||
if (v >= nodes.Count) {
|
||||
// Cover edge cases
|
||||
continue;
|
||||
}
|
||||
|
||||
var node = nodes[v];
|
||||
var p = node.RandomPointOnSurface();
|
||||
|
||||
// Test if it is some distance away from the other points
|
||||
if (clearanceRadius > 0) {
|
||||
for (int j = 0; j < pts.Count; j++) {
|
||||
if ((pts[j]-p).sqrMagnitude < clearanceRadius) {
|
||||
worked = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (worked) {
|
||||
pts.Add(p);
|
||||
break;
|
||||
}
|
||||
testCount++;
|
||||
}
|
||||
}
|
||||
|
||||
ListPool<float>.Release(ref accs);
|
||||
} else {
|
||||
// Fast path, assumes all nodes have the same area (usually zero)
|
||||
for (int i = 0; i < count; i++) {
|
||||
pts.Add((Vector3)nodes[UnityEngine.Random.Range(0, nodes.Count)].RandomPointOnSurface());
|
||||
}
|
||||
}
|
||||
|
||||
return pts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d93ae7d64ab84e23819ac5754065f34
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
@@ -0,0 +1,257 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.Util;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine.Profiling;
|
||||
using Pathfinding.Graphs.Navmesh;
|
||||
using Pathfinding.Jobs;
|
||||
using Pathfinding.Drawing;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Moves a grid or recast graph to follow a target.
|
||||
///
|
||||
/// This is useful if you have a very large, or even infinite, world, but pathfinding is only necessary in a small region around an object (for example the player).
|
||||
/// This component will move a graph around so that its center stays close to the <see cref="target"/> object.
|
||||
///
|
||||
/// Note: This component can only be used with grid graphs, layered grid graphs and (tiled) recast graphs.
|
||||
///
|
||||
/// <b>Usage</b>
|
||||
/// Take a look at the example scene called "Procedural" for an example of how to use this script
|
||||
///
|
||||
/// Attach this to some object in the scene and assign the target to e.g the player.
|
||||
/// Then the graph will follow that object around as it moves.
|
||||
///
|
||||
/// [Open online documentation to see videos]
|
||||
///
|
||||
/// [Open online documentation to see videos]
|
||||
///
|
||||
/// <b>Performance</b>
|
||||
/// When the graph is moved you may notice an fps drop.
|
||||
/// If this grows too large you can try a few things:
|
||||
///
|
||||
/// General advice:
|
||||
/// - Turn on multithreading (A* Inspector -> Settings)
|
||||
/// - Make sure you have 'Show Graphs' disabled in the A* inspector, since gizmos in the scene view can take some
|
||||
/// time to update when the graph moves, and thus make it seem like this script is slower than it actually is.
|
||||
///
|
||||
/// For grid graphs:
|
||||
/// - Avoid using any erosion in the grid graph settings. This is relatively slow. Each erosion iteration requires expanding the region that is updated by 1 node.
|
||||
/// - Reduce the grid size or resolution.
|
||||
/// - Reduce the <see cref="updateDistance"/>. This will make the updates smaller but more frequent.
|
||||
/// This only works to some degree however since an update has an inherent overhead.
|
||||
/// - Disable Height Testing or Collision Testing in the grid graph if you can. This can give a performance boost
|
||||
/// since fewer calls to the physics engine need to be done.
|
||||
///
|
||||
/// For recast graphs:
|
||||
/// - Rasterize colliders instead of meshes. This is typically faster.
|
||||
/// - Use a reasonable tile size. Very small tiles can cause more overhead, and too large tiles might mean that you are updating too much in one go.
|
||||
/// Typical values are around 64 to 256 voxels.
|
||||
/// - Use a larger cell size. A lower cell size will give better quality graphs, but it will also be slower to scan.
|
||||
///
|
||||
/// The graph updates will be offloaded to worker threads as much as possible.
|
||||
///
|
||||
/// See: large-worlds (view in online documentation for working links)
|
||||
/// </summary>
|
||||
[AddComponentMenu("Pathfinding/Procedural Graph Mover")]
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/proceduralgraphmover.html")]
|
||||
public class ProceduralGraphMover : VersionedMonoBehaviour {
|
||||
/// <summary>
|
||||
/// Grid graphs will be updated if the target is more than this number of nodes from the graph center.
|
||||
/// Note that this is in nodes, not world units.
|
||||
///
|
||||
/// Note: For recast graphs, this setting has no effect.
|
||||
/// </summary>
|
||||
public float updateDistance = 10;
|
||||
|
||||
/// <summary>Graph will be moved to follow this target</summary>
|
||||
public Transform target;
|
||||
|
||||
/// <summary>True while the graph is being updated by this script</summary>
|
||||
public bool updatingGraph { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Graph to update.
|
||||
/// This will be set at Start based on <see cref="graphIndex"/>.
|
||||
/// During runtime you may set this to any graph or to null to disable updates.
|
||||
/// </summary>
|
||||
public NavGraph graph;
|
||||
|
||||
/// <summary>
|
||||
/// Index for the graph to update.
|
||||
/// This will be used at Start to set <see cref="graph"/>.
|
||||
///
|
||||
/// This is an index into the AstarPath.active.data.graphs array.
|
||||
/// </summary>
|
||||
[HideInInspector]
|
||||
public int graphIndex;
|
||||
|
||||
void Start () {
|
||||
if (AstarPath.active == null) throw new System.Exception("There is no AstarPath object in the scene");
|
||||
|
||||
// If one creates this component via a script then they may have already set the graph field.
|
||||
// In that case don't replace it.
|
||||
if (graph == null) {
|
||||
if (graphIndex < 0) throw new System.Exception("Graph index should not be negative");
|
||||
if (graphIndex >= AstarPath.active.data.graphs.Length) throw new System.Exception("The ProceduralGraphMover was configured to use graph index " + graphIndex + ", but only " + AstarPath.active.data.graphs.Length + " graphs exist");
|
||||
|
||||
graph = AstarPath.active.data.graphs[graphIndex];
|
||||
if (!(graph is GridGraph || graph is RecastGraph)) throw new System.Exception("The ProceduralGraphMover was configured to use graph index " + graphIndex + " but that graph either does not exist or is not a GridGraph, LayerGridGraph or RecastGraph");
|
||||
|
||||
if (graph is RecastGraph rg && !rg.useTiles) Debug.LogWarning("The ProceduralGraphMover component only works with tiled recast graphs. Enable tiling in the recast graph inspector.", this);
|
||||
}
|
||||
|
||||
UpdateGraph();
|
||||
}
|
||||
|
||||
void OnDisable () {
|
||||
// Just in case this script is performing an update while being disabled
|
||||
if (AstarPath.active != null) AstarPath.active.FlushWorkItems();
|
||||
|
||||
updatingGraph = false;
|
||||
}
|
||||
|
||||
/// <summary>Update is called once per frame</summary>
|
||||
void Update () {
|
||||
if (AstarPath.active == null || graph == null || !graph.isScanned) return;
|
||||
|
||||
if (graph is GridGraph gg) {
|
||||
// Calculate where the graph center and the target position is in graph space
|
||||
// In graph space, (0,0,0) is bottom left corner of the graph
|
||||
// For grid graphs, one unit along the X and Z axes in graph space equals the distance between two nodes.
|
||||
// The Y axis still uses world units
|
||||
var graphCenterInGraphSpace = gg.transform.InverseTransform(gg.center);
|
||||
var targetPositionInGraphSpace = gg.transform.InverseTransform(target.position);
|
||||
|
||||
// Check the distance in graph space
|
||||
// We only care about the X and Z axes since the Y axis is the "height" coordinate of the nodes (in graph space)
|
||||
// We only care about the plane that the nodes are placed in
|
||||
if (VectorMath.SqrDistanceXZ(graphCenterInGraphSpace, targetPositionInGraphSpace) > updateDistance*updateDistance) {
|
||||
UpdateGraph();
|
||||
}
|
||||
} else if (graph is RecastGraph rg) {
|
||||
UpdateGraph();
|
||||
} else {
|
||||
throw new System.Exception("ProceduralGraphMover cannot be used with graphs of type " + graph.GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the graph asynchronously.
|
||||
/// This will move the graph so that the target's position is (roughly) the center of the graph.
|
||||
/// If the graph is already being updated, the call will be ignored.
|
||||
///
|
||||
/// The image below shows which nodes will be updated when the graph moves.
|
||||
/// The whole graph is not recalculated each time it is moved, but only those
|
||||
/// nodes that have to be updated, the rest will keep their old values.
|
||||
/// The image is a bit simplified but it shows the main idea.
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// If you want to move the graph synchronously then pass false to the async parameter.
|
||||
/// </summary>
|
||||
public void UpdateGraph (bool async = true) {
|
||||
if (!enabled) throw new System.InvalidOperationException("This component has been disabled");
|
||||
|
||||
if (updatingGraph) {
|
||||
// We are already updating the graph
|
||||
// so ignore this call
|
||||
return;
|
||||
}
|
||||
|
||||
if (graph is GridGraph gg) {
|
||||
UpdateGridGraph(gg, async);
|
||||
} else if (graph is RecastGraph rg) {
|
||||
var delta = RecastGraphTileShift(rg, target.position);
|
||||
if (delta.x != 0 || delta.y != 0) {
|
||||
updatingGraph = true;
|
||||
UpdateRecastGraph(rg, delta, async);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateGridGraph (GridGraph graph, bool async) {
|
||||
// Start a work item for updating the graph
|
||||
// This will pause the pathfinding threads
|
||||
// so that it is safe to update the graph
|
||||
// and then do it over several frames
|
||||
// to avoid too large FPS drops
|
||||
|
||||
updatingGraph = true;
|
||||
List<(IGraphUpdatePromise, IEnumerator<JobHandle>)> promises = new List<(IGraphUpdatePromise, IEnumerator<JobHandle>)>();
|
||||
AstarPath.active.AddWorkItem(new AstarWorkItem(
|
||||
ctx => {
|
||||
// Find the direction that we want to move the graph in.
|
||||
// Calculate this in graph space (where a distance of one is the size of one node)
|
||||
Vector3 dir = graph.transform.InverseTransformVector(target.position - graph.center);
|
||||
|
||||
// Snap to a whole number of nodes to offset in each direction
|
||||
int dx = Mathf.RoundToInt(dir.x);
|
||||
int dz = Mathf.RoundToInt(dir.z);
|
||||
|
||||
if (dx != 0 || dz != 0) {
|
||||
var promise = graph.TranslateInDirection(dx, dz);
|
||||
promises.Add((promise, promise.Prepare()));
|
||||
}
|
||||
},
|
||||
(ctx, force) => {
|
||||
if (GraphUpdateProcessor.ProcessGraphUpdatePromises(promises, ctx, force ? TimeSlice.Infinite : TimeSlice.MillisFromNow(2)) == -1) {
|
||||
updatingGraph = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
));
|
||||
if (!async) AstarPath.active.FlushWorkItems();
|
||||
}
|
||||
|
||||
static Vector2Int RecastGraphTileShift (RecastGraph graph, Vector3 targetCenter) {
|
||||
// Find the direction that we want to move the graph in.
|
||||
// Calcuculate this in graph space, to take the graph rotation into account
|
||||
Vector3 dir = graph.transform.InverseTransform(targetCenter) - graph.transform.InverseTransform(graph.forcedBoundsCenter);
|
||||
|
||||
// Only move in one direction at a time for simplicity
|
||||
if (Mathf.Abs(dir.x) > Mathf.Abs(dir.z)) dir.z = 0;
|
||||
else dir.x = 0;
|
||||
|
||||
// Calculate how many whole tiles to move.
|
||||
// Avoid moving unless we want to move at least 0.5+#Hysteresis full tiles
|
||||
// Hysteresis must be at least 0.
|
||||
const float Hysteresis = 0.2f;
|
||||
return new Vector2Int(
|
||||
(int)(Mathf.Max(0, Mathf.Abs(dir.x) / graph.TileWorldSizeX + 0.5f - Hysteresis) * Mathf.Sign(dir.x)),
|
||||
(int)(Mathf.Max(0, Mathf.Abs(dir.z) / graph.TileWorldSizeZ + 0.5f - Hysteresis) * Mathf.Sign(dir.z))
|
||||
);
|
||||
}
|
||||
|
||||
void UpdateRecastGraph (RecastGraph graph, Vector2Int delta, bool async) {
|
||||
updatingGraph = true;
|
||||
List<(IGraphUpdatePromise, IEnumerator<JobHandle>)> promises = new List<(IGraphUpdatePromise, IEnumerator<JobHandle>)>();
|
||||
AstarPath.active.AddWorkItem(new AstarWorkItem(
|
||||
ctx => {
|
||||
var promise = graph.TranslateInDirection(delta.x, delta.y);
|
||||
promises.Add((promise, promise.Prepare()));
|
||||
},
|
||||
(ctx, force) => {
|
||||
if (GraphUpdateProcessor.ProcessGraphUpdatePromises(promises, ctx, force ? TimeSlice.Infinite : TimeSlice.MillisFromNow(2)) == -1) {
|
||||
updatingGraph = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
));
|
||||
if (!async) AstarPath.active.FlushWorkItems();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This class has been renamed to <see cref="ProceduralGraphMover"/>.
|
||||
///
|
||||
/// Deprecated: Use <see cref="ProceduralGraphMover"/> instead
|
||||
/// </summary>
|
||||
[System.Obsolete("This class has been renamed to ProceduralGraphMover", true)]
|
||||
public class ProceduralGridMover {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a634230476dc478b88eceac73b1c8a4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: ddf758bc3c379c843a3c9b6030f06959, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Packages/com.arongranberg.astar/Utilities/Promise.cs
Normal file
3
Packages/com.arongranberg.astar/Utilities/Promise.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file has been removed from the package. Since UnityPackages cannot
|
||||
// delete files, only replace them, this message is left here to prevent old
|
||||
// files from causing compiler errors.
|
||||
11
Packages/com.arongranberg.astar/Utilities/Promise.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Utilities/Promise.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ffac5a6557b05460bb1adc051e20f4ca
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Packages/com.arongranberg.astar/Utilities/RWLock.cs
Normal file
3
Packages/com.arongranberg.astar/Utilities/RWLock.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file has been removed from the package. Since UnityPackages cannot
|
||||
// delete files, only replace them, this message is left here to prevent old
|
||||
// files from causing compiler errors.
|
||||
11
Packages/com.arongranberg.astar/Utilities/RWLock.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Utilities/RWLock.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78c62f7a9ebf8614db5f43ee33f118b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
Packages/com.arongranberg.astar/Utilities/SpinLock.cs
Normal file
3
Packages/com.arongranberg.astar/Utilities/SpinLock.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
// This file has been removed from the package. Since UnityPackages cannot
|
||||
// delete files, only replace them, this message is left here to prevent old
|
||||
// files from causing compiler errors.
|
||||
11
Packages/com.arongranberg.astar/Utilities/SpinLock.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Utilities/SpinLock.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a281c042e9d12449b73bda8e3828059
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,39 @@
|
||||
using Pathfinding.Util;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Pathfinding {
|
||||
[ExecuteInEditMode]
|
||||
/// <summary>
|
||||
/// Helper class to keep track of references to GameObjects.
|
||||
/// Does nothing more than to hold a GUID value.
|
||||
/// </summary>
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/unityreferencehelper.html")]
|
||||
public class UnityReferenceHelper : MonoBehaviour {
|
||||
[HideInInspector]
|
||||
[SerializeField]
|
||||
private string guid;
|
||||
|
||||
public string GetGUID() => guid;
|
||||
|
||||
public void Awake () {
|
||||
Reset();
|
||||
}
|
||||
|
||||
public void Reset () {
|
||||
if (string.IsNullOrEmpty(guid)) {
|
||||
guid = Pathfinding.Util.Guid.NewGuid().ToString();
|
||||
Debug.Log("Created new GUID - " + guid, this);
|
||||
} else if (gameObject.scene.name != null) {
|
||||
// Create a new GUID if there are duplicates in the scene.
|
||||
// Don't do this if this is a prefab (scene.name == null)
|
||||
foreach (UnityReferenceHelper urh in UnityCompatibility.FindObjectsByTypeUnsorted<UnityReferenceHelper>()) {
|
||||
if (urh != this && guid == urh.guid) {
|
||||
guid = Pathfinding.Util.Guid.NewGuid().ToString();
|
||||
Debug.Log("Created new GUID - " + guid, this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 632ae7eb1d4ce49d38a1e38ee7efe7c5
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
Reference in New Issue
Block a user