update
This commit is contained in:
13
.idea/.idea.BABA_YAGA/.idea/workspace.xml
generated
13
.idea/.idea.BABA_YAGA/.idea/workspace.xml
generated
@@ -4,16 +4,7 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="d308d1cb-09fc-4331-ba20-00f7b43d1576" name="Changes" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.BABA_YAGA/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.BABA_YAGA/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/All for one/Basic Locomotion Demo/Invector_BasicLocomotion.unity" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/All for one/Basic Locomotion Demo/Invector_BasicLocomotion.unity" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/Prefabs/Shooter/Player/vShooterMelee_Inventory.prefab" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Prefabs/Shooter/Player/vShooterMelee_Inventory.prefab" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/Prefabs/Shooter/Player/vShooterMelee_NoInventory.prefab" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Prefabs/Shooter/Player/vShooterMelee_NoInventory.prefab" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/Presets/DefaultMazeProfile.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Presets/DefaultMazeProfile.asset" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/Scenes/Main Scene.unity" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scenes/Main Scene.unity" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/Scenes/TEST.unity" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scenes/TEST.unity" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Assets/UI/MainPanelSettings.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/UI/MainPanelSettings.asset" afterDir="false" />
|
||||
</list>
|
||||
<list default="true" id="d308d1cb-09fc-4331-ba20-00f7b43d1576" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
@@ -113,7 +104,7 @@
|
||||
<workItem from="1780929175157" duration="11052000" />
|
||||
<workItem from="1780970213630" duration="5518000" />
|
||||
<workItem from="1781015858277" duration="4448000" />
|
||||
<workItem from="1781181289365" duration="1309000" />
|
||||
<workItem from="1781181289365" duration="1919000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
|
||||
BIN
Packages/app.rive.rive-unity/Runtime/Libraries/Windows/rive.dll
Normal file
BIN
Packages/app.rive.rive-unity/Runtime/Libraries/Windows/rive.dll
Normal file
Binary file not shown.
1
Packages/com.arongranberg.astar/AssemblyInfo.cs
Normal file
1
Packages/com.arongranberg.astar/AssemblyInfo.cs
Normal file
@@ -0,0 +1 @@
|
||||
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("AstarPathfindingProjectEditor")]
|
||||
11
Packages/com.arongranberg.astar/AssemblyInfo.cs.meta
Normal file
11
Packages/com.arongranberg.astar/AssemblyInfo.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: de324f1b7181d202dbbc6b1421023e3f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"name": "AstarPathfindingProject",
|
||||
"rootNamespace": "",
|
||||
"references": [
|
||||
"GUID:2bafac87e7f4b9b418d9448d219b01ab",
|
||||
"GUID:f4059aaf6c60a4a58a177a2609feb769",
|
||||
"GUID:2665a8d13d1b3f18800f46e256720795",
|
||||
"GUID:d8b63aba1907145bea998dd612889d6b",
|
||||
"GUID:e0cd26848372d4e5c891c569017e11f1",
|
||||
"GUID:8a2eafa29b15f444eb6d74f94a930e1d",
|
||||
"GUID:de4e6084e6d474788bb8c799d6b461eb",
|
||||
"GUID:734d92eba21c94caba915361bd5ac177",
|
||||
"GUID:a5baed0c9693541a5bd947d336ec7659",
|
||||
"GUID:db11b4b5d7520bc479416b48c98206cb"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": true,
|
||||
"overrideReferences": true,
|
||||
"precompiledReferences": [
|
||||
"Pathfinding.Ionic.Zip.Reduced.dll",
|
||||
"Clipper2Lib.dll"
|
||||
],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [
|
||||
"MODULE_BURST",
|
||||
"MODULE_MATHEMATICS",
|
||||
"MODULE_COLLECTIONS"
|
||||
],
|
||||
"versionDefines": [
|
||||
{
|
||||
"name": "com.unity.burst",
|
||||
"expression": "1.8.7",
|
||||
"define": "MODULE_BURST"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.mathematics",
|
||||
"expression": "1.2.6",
|
||||
"define": "MODULE_MATHEMATICS"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.collections",
|
||||
"expression": "1.5.1",
|
||||
"define": "MODULE_COLLECTIONS"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.collections",
|
||||
"expression": "0.11-preview",
|
||||
"define": "MODULE_COLLECTIONS_0_11_0_OR_NEWER"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.collections",
|
||||
"expression": "2.0.0",
|
||||
"define": "MODULE_COLLECTIONS_2_0_0_OR_NEWER"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.collections",
|
||||
"expression": "2.1.0",
|
||||
"define": "MODULE_COLLECTIONS_2_1_0_OR_NEWER"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.collections",
|
||||
"expression": "2.2.0",
|
||||
"define": "MODULE_COLLECTIONS_2_2_0_OR_NEWER"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.entities",
|
||||
"expression": "1.0.0-pre.47",
|
||||
"define": "MODULE_ENTITIES"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.entities",
|
||||
"expression": "1.0.8",
|
||||
"define": "MODULE_ENTITIES_1_0_8_OR_NEWER"
|
||||
},
|
||||
{
|
||||
"name": "com.unity.test-framework",
|
||||
"expression": "2.0.0",
|
||||
"define": "MODULE_TEST_FRAMEWORK"
|
||||
}
|
||||
],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: efa45043feb7e4147a305b73b5cea642
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
9
Packages/com.arongranberg.astar/Behaviors.meta
Normal file
9
Packages/com.arongranberg.astar/Behaviors.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5eeb3c5df5ca840d3ae6b93e6b207d74
|
||||
folderAsset: yes
|
||||
timeCreated: 1497206641
|
||||
licenseType: Store
|
||||
DefaultImporter:
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
108
Packages/com.arongranberg.astar/Behaviors/AIDestinationSetter.cs
Normal file
108
Packages/com.arongranberg.astar/Behaviors/AIDestinationSetter.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using UnityEngine;
|
||||
using Pathfinding.Util;
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
#endif
|
||||
|
||||
namespace Pathfinding {
|
||||
/// <summary>
|
||||
/// Sets the destination of an AI to the position of a specified object.
|
||||
/// This component should be attached to a GameObject together with a movement script such as AIPath, RichAI or AILerp.
|
||||
/// This component will then make the AI move towards the <see cref="target"/> set on this component.
|
||||
///
|
||||
/// Essentially the only thing this component does is to set the <see cref="Pathfinding.IAstarAI.destination"/> property to the position of the target every frame.
|
||||
/// But there is some additional complexity to make sure that the destination is updated immediately before the AI searches for a path as well, in case the
|
||||
/// target moved since the last Update. There is also some complexity to reduce the performance impact, by using the <see cref="BatchedEvents"/> system to
|
||||
/// process all AIDestinationSetter components in a single batch.
|
||||
///
|
||||
/// When using ECS, this component is instead added as a managed component to the entity.
|
||||
/// The destination syncing is then handled by the <see cref="SyncDestinationTransformSystem"/> for better performance.
|
||||
///
|
||||
/// See: <see cref="Pathfinding.IAstarAI.destination"/>
|
||||
/// </summary>
|
||||
[UniqueComponent(tag = "ai.destination")]
|
||||
[AddComponentMenu("Pathfinding/AI/Behaviors/AIDestinationSetter")]
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/aidestinationsetter.html")]
|
||||
public class AIDestinationSetter : VersionedMonoBehaviour
|
||||
#if MODULE_ENTITIES
|
||||
, IComponentData, IRuntimeBaker
|
||||
#endif
|
||||
{
|
||||
/// <summary>The object that the AI should move to</summary>
|
||||
public Transform target;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the agent will try to align itself with the rotation of the <see cref="target"/>.
|
||||
///
|
||||
/// This can only be used together with the <see cref="FollowerEntity"/> movement script.
|
||||
/// Other movement scripts will ignore it.
|
||||
///
|
||||
/// [Open online documentation to see videos]
|
||||
///
|
||||
/// See: <see cref="FollowerEntity.SetDestination"/>
|
||||
/// </summary>
|
||||
public bool useRotation;
|
||||
|
||||
IAstarAI ai;
|
||||
#if MODULE_ENTITIES
|
||||
Entity entity;
|
||||
World world;
|
||||
#endif
|
||||
|
||||
void OnEnable () {
|
||||
ai = GetComponent<IAstarAI>();
|
||||
#if MODULE_ENTITIES
|
||||
if (ai is FollowerEntity follower) {
|
||||
// This will call OnCreatedEntity on this component, if the entity has already been created.
|
||||
follower.RegisterRuntimeBaker(this);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
// Update the destination right before searching for a path as well.
|
||||
// This is enough in theory, but this script will also update the destination every
|
||||
// frame as the destination is used for debugging and may be used for other things by other
|
||||
// scripts as well. So it makes sense that it is up to date every frame.
|
||||
if (ai != null) ai.onSearchPath += UpdateDestination;
|
||||
|
||||
// Will make OnUpdate be called once every frame with all components.
|
||||
// This is significantly faster than letting Unity call the Update method
|
||||
// on each component.
|
||||
// See https://blog.unity.com/technology/1k-update-calls
|
||||
BatchedEvents.Add(this, BatchedEvents.Event.Update, OnUpdate, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable () {
|
||||
#if MODULE_ENTITIES
|
||||
if (world != null && world.IsCreated && world.EntityManager.Exists(entity)) {
|
||||
world.EntityManager.RemoveComponent<AIDestinationSetter>(entity);
|
||||
}
|
||||
if (ai != null && !(ai is FollowerEntity)) ai.onSearchPath -= UpdateDestination;
|
||||
#else
|
||||
if (ai != null) ai.onSearchPath -= UpdateDestination;
|
||||
#endif
|
||||
BatchedEvents.Remove(this);
|
||||
}
|
||||
|
||||
#if MODULE_ENTITIES
|
||||
void IRuntimeBaker.OnCreatedEntity (World world, Entity entity) {
|
||||
// Do nothing except add the component. Actual syncing is handled by the SyncDestinationTransformSystem.
|
||||
this.entity = entity;
|
||||
this.world = world;
|
||||
world.EntityManager.AddComponentData<AIDestinationSetter>(entity, this);
|
||||
}
|
||||
#endif
|
||||
|
||||
/// <summary>Updates the AI's destination every frame</summary>
|
||||
static void OnUpdate (AIDestinationSetter[] components, int count) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
components[i].UpdateDestination();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Updates the AI's destination immediately</summary>
|
||||
void UpdateDestination () {
|
||||
if (target != null && ai != null) ai.destination = target.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9679e68a0f1144e79c664d9a11ca121
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 8dca5cfad5de0f444847aaaaa7cf73b8, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,141 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.Pooling;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
|
||||
/// <summary>
|
||||
/// Movement script for curved worlds.
|
||||
/// This script inherits from AIPath, but adjusts its movement plane every frame using the ground normal.
|
||||
/// </summary>
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/aipathalignedtosurface.html")]
|
||||
public class AIPathAlignedToSurface : AIPath {
|
||||
/// <summary>Scratch dictionary used to avoid allocations every frame</summary>
|
||||
static readonly Dictionary<Mesh, int> scratchDictionary = new Dictionary<Mesh, int>();
|
||||
|
||||
protected override void OnEnable () {
|
||||
base.OnEnable();
|
||||
movementPlane = new Util.SimpleMovementPlane(rotation);
|
||||
}
|
||||
|
||||
protected override void ApplyGravity (float deltaTime) {
|
||||
// Apply gravity
|
||||
if (usingGravity) {
|
||||
// Gravity is relative to the current surface.
|
||||
// Only the normal direction is well defined however so x and z are ignored.
|
||||
verticalVelocity += deltaTime * (float.IsNaN(gravity.x) ? Physics.gravity.y : gravity.y);
|
||||
} else {
|
||||
verticalVelocity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates smoothly interpolated normals for all raycast hits and uses that to set the movement planes of the agents.
|
||||
///
|
||||
/// To support meshes that change at any time, we use Mesh.AcquireReadOnlyMeshData to get a read-only view of the mesh data.
|
||||
/// This is only efficient if we batch all updates and make a single call to Mesh.AcquireReadOnlyMeshData.
|
||||
///
|
||||
/// This method is quite convoluted due to having to read the raw vertex data streams from unity meshes to avoid allocations.
|
||||
/// </summary>
|
||||
public static void UpdateMovementPlanes (AIPathAlignedToSurface[] components, int count) {
|
||||
Profiler.BeginSample("UpdateMovementPlanes");
|
||||
var meshes = ListPool<Mesh>.Claim();
|
||||
var componentsByMesh = new List<List<AIPathAlignedToSurface> >();
|
||||
var meshToIndex = scratchDictionary;
|
||||
for (int i = 0; i < count; i++) {
|
||||
var c = components[i].lastRaycastHit.collider;
|
||||
// triangleIndex can be -1 if the mesh collider is convex, and the raycast started inside it.
|
||||
// This is not a documented behavior, but it seems to happen in practice.
|
||||
if (c is MeshCollider mc && components[i].lastRaycastHit.triangleIndex != -1) {
|
||||
var sharedMesh = mc.sharedMesh;
|
||||
if (meshToIndex.TryGetValue(sharedMesh, out var meshIndex)) {
|
||||
componentsByMesh[meshIndex].Add(components[i]);
|
||||
} else if (sharedMesh != null && sharedMesh.isReadable) {
|
||||
meshToIndex[sharedMesh] = meshes.Count;
|
||||
meshes.Add(sharedMesh);
|
||||
componentsByMesh.Add(ListPool<AIPathAlignedToSurface>.Claim());
|
||||
componentsByMesh[meshes.Count-1].Add(components[i]);
|
||||
} else {
|
||||
// Unreadable mesh
|
||||
components[i].SetInterpolatedNormal(components[i].lastRaycastHit.normal);
|
||||
}
|
||||
} else {
|
||||
// Not a mesh collider, or the triangle index was -1
|
||||
components[i].SetInterpolatedNormal(components[i].lastRaycastHit.normal);
|
||||
}
|
||||
}
|
||||
var meshDatas = Mesh.AcquireReadOnlyMeshData(meshes);
|
||||
for (int i = 0; i < meshes.Count; i++) {
|
||||
var m = meshes[i];
|
||||
var meshIndex = meshToIndex[m];
|
||||
var meshData = meshDatas[meshIndex];
|
||||
var componentsForMesh = componentsByMesh[meshIndex];
|
||||
|
||||
var stream = meshData.GetVertexAttributeStream(UnityEngine.Rendering.VertexAttribute.Normal);
|
||||
|
||||
if (stream == -1) {
|
||||
// Mesh does not have normals
|
||||
for (int j = 0; j < componentsForMesh.Count; j++) componentsForMesh[j].SetInterpolatedNormal(componentsForMesh[j].lastRaycastHit.normal);
|
||||
continue;
|
||||
}
|
||||
var vertexData = meshData.GetVertexData<byte>(stream);
|
||||
var stride = meshData.GetVertexBufferStride(stream);
|
||||
var normalOffset = meshData.GetVertexAttributeOffset(UnityEngine.Rendering.VertexAttribute.Normal);
|
||||
unsafe {
|
||||
var normals = (byte*)vertexData.GetUnsafeReadOnlyPtr() + normalOffset;
|
||||
|
||||
for (int j = 0; j < componentsForMesh.Count; j++) {
|
||||
var comp = componentsForMesh[j];
|
||||
var hit = comp.lastRaycastHit;
|
||||
int t0, t1, t2;
|
||||
|
||||
// Get the vertex indices corresponding to the triangle that was hit
|
||||
if (meshData.indexFormat == UnityEngine.Rendering.IndexFormat.UInt16) {
|
||||
var indices = meshData.GetIndexData<ushort>();
|
||||
t0 = indices[hit.triangleIndex * 3 + 0];
|
||||
t1 = indices[hit.triangleIndex * 3 + 1];
|
||||
t2 = indices[hit.triangleIndex * 3 + 2];
|
||||
} else {
|
||||
var indices = meshData.GetIndexData<int>();
|
||||
t0 = indices[hit.triangleIndex * 3 + 0];
|
||||
t1 = indices[hit.triangleIndex * 3 + 1];
|
||||
t2 = indices[hit.triangleIndex * 3 + 2];
|
||||
}
|
||||
|
||||
// Get the normals corresponding to the 3 vertices
|
||||
var n0 = *((Vector3*)(normals + t0 * stride));
|
||||
var n1 = *((Vector3*)(normals + t1 * stride));
|
||||
var n2 = *((Vector3*)(normals + t2 * stride));
|
||||
|
||||
// Interpolate the normal using the barycentric coordinates
|
||||
Vector3 baryCenter = hit.barycentricCoordinate;
|
||||
Vector3 interpolatedNormal = n0 * baryCenter.x + n1 * baryCenter.y + n2 * baryCenter.z;
|
||||
interpolatedNormal = interpolatedNormal.normalized;
|
||||
Transform hitTransform = hit.collider.transform;
|
||||
interpolatedNormal = hitTransform.TransformDirection(interpolatedNormal);
|
||||
comp.SetInterpolatedNormal(interpolatedNormal);
|
||||
}
|
||||
}
|
||||
}
|
||||
meshDatas.Dispose();
|
||||
for (int i = 0; i < componentsByMesh.Count; i++) ListPool<AIPathAlignedToSurface>.Release(componentsByMesh[i]);
|
||||
ListPool<Mesh>.Release(ref meshes);
|
||||
scratchDictionary.Clear();
|
||||
Profiler.EndSample();
|
||||
}
|
||||
|
||||
void SetInterpolatedNormal (Vector3 normal) {
|
||||
if (normal != Vector3.zero) {
|
||||
var fwd = Vector3.Cross(movementPlane.rotation * Vector3.right, normal);
|
||||
movementPlane = new Util.SimpleMovementPlane(Quaternion.LookRotation(fwd, normal));
|
||||
}
|
||||
if (rvoController != null) rvoController.movementPlane = movementPlane;
|
||||
}
|
||||
|
||||
protected override void UpdateMovementPlane () {
|
||||
// The UpdateMovementPlanes method will take care of this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cf55afa704da4bada4b28145f5f4685
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
51
Packages/com.arongranberg.astar/Behaviors/MoveInCircle.cs
Normal file
51
Packages/com.arongranberg.astar/Behaviors/MoveInCircle.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using UnityEngine;
|
||||
using Pathfinding.Drawing;
|
||||
|
||||
namespace Pathfinding {
|
||||
/// <summary>
|
||||
/// Moves an agent in a circle around a point.
|
||||
///
|
||||
/// This script is intended as an example of how you can make an agent move in a circle.
|
||||
/// In a real game, you may want to replace this script with your own custom script that is tailored to your game.
|
||||
/// The code in this script is simple enough to copy and paste wherever you need it.
|
||||
///
|
||||
/// [Open online documentation to see videos]
|
||||
///
|
||||
/// See: move_in_circle (view in online documentation for working links)
|
||||
/// See: <see cref="AIDestinationSetter"/>
|
||||
/// See: <see cref="FollowerEntity"/>
|
||||
/// See: <see cref="AIPath"/>
|
||||
/// See: <see cref="RichAI"/>
|
||||
/// See: <see cref="AILerp"/>
|
||||
/// </summary>
|
||||
[UniqueComponent(tag = "ai.destination")]
|
||||
[AddComponentMenu("Pathfinding/AI/Behaviors/MoveInCircle")]
|
||||
/// <summary>[MoveInCircle]</summary>
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/moveincircle.html")]
|
||||
public class MoveInCircle : VersionedMonoBehaviour {
|
||||
/// <summary>Target point to rotate around</summary>
|
||||
public Transform target;
|
||||
/// <summary>Radius of the circle</summary>
|
||||
public float radius = 5;
|
||||
/// <summary>Distance between the agent's current position, and the destination it will get. Use a negative value to make the agent move in the opposite direction around the circle.</summary>
|
||||
public float offset = 2;
|
||||
|
||||
IAstarAI ai;
|
||||
|
||||
void OnEnable () {
|
||||
ai = GetComponent<IAstarAI>();
|
||||
}
|
||||
|
||||
void Update () {
|
||||
var normal = (ai.position - target.position).normalized;
|
||||
var tangent = Vector3.Cross(normal, target.up);
|
||||
|
||||
ai.destination = target.position + normal * radius + tangent * offset;
|
||||
}
|
||||
|
||||
public override void DrawGizmos () {
|
||||
if (target) Draw.Circle(target.position, target.up, radius, Color.white);
|
||||
}
|
||||
}
|
||||
/// <summary>[MoveInCircle]</summary>
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3124c7cfbe5ac4b1cbe42bd6b1e4279d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 8cf8c12a4f0221b438c0d7ffa0eab6f4, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
68
Packages/com.arongranberg.astar/Behaviors/Patrol.cs
Normal file
68
Packages/com.arongranberg.astar/Behaviors/Patrol.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
|
||||
namespace Pathfinding {
|
||||
/// <summary>
|
||||
/// Simple patrol behavior.
|
||||
/// This will set the destination on the agent so that it moves through the sequence of objects in the <see cref="targets"/> array.
|
||||
/// Upon reaching a target it will wait for <see cref="delay"/> seconds.
|
||||
///
|
||||
/// [Open online documentation to see videos]
|
||||
///
|
||||
/// See: <see cref="Pathfinding.AIDestinationSetter"/>
|
||||
/// See: <see cref="Pathfinding.AIPath"/>
|
||||
/// See: <see cref="Pathfinding.RichAI"/>
|
||||
/// See: <see cref="Pathfinding.AILerp"/>
|
||||
/// </summary>
|
||||
[UniqueComponent(tag = "ai.destination")]
|
||||
[AddComponentMenu("Pathfinding/AI/Behaviors/Patrol")]
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/patrol.html")]
|
||||
public class Patrol : VersionedMonoBehaviour {
|
||||
/// <summary>Target points to move to in order</summary>
|
||||
public Transform[] targets;
|
||||
|
||||
/// <summary>Time in seconds to wait at each target</summary>
|
||||
public float delay = 0;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the agent's destination will be updated every frame instead of only when switching targets.
|
||||
///
|
||||
/// This is good if you have moving targets, but is otherwise unnecessary and slightly slower.
|
||||
/// </summary>
|
||||
public bool updateDestinationEveryFrame = false;
|
||||
|
||||
/// <summary>Current target index</summary>
|
||||
int index = -1;
|
||||
|
||||
IAstarAI agent;
|
||||
float switchTime = float.NegativeInfinity;
|
||||
|
||||
protected override void Awake () {
|
||||
base.Awake();
|
||||
agent = GetComponent<IAstarAI>();
|
||||
}
|
||||
|
||||
/// <summary>Update is called once per frame</summary>
|
||||
void Update () {
|
||||
if (targets.Length == 0) return;
|
||||
|
||||
// Note: using reachedEndOfPath and pathPending instead of reachedDestination here because
|
||||
// if the destination cannot be reached by the agent, we don't want it to get stuck, we just want it to get as close as possible and then move on.
|
||||
if (agent.reachedEndOfPath && !agent.pathPending && float.IsPositiveInfinity(switchTime)) {
|
||||
switchTime = Time.time + delay;
|
||||
}
|
||||
|
||||
if (Time.time >= switchTime) {
|
||||
index++;
|
||||
switchTime = float.PositiveInfinity;
|
||||
|
||||
index = index % targets.Length;
|
||||
agent.destination = targets[index].position;
|
||||
agent.SearchPath();
|
||||
} else if (updateDestinationEveryFrame) {
|
||||
index = index % targets.Length;
|
||||
agent.destination = targets[index].position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/com.arongranberg.astar/Behaviors/Patrol.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Behaviors/Patrol.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22e6c29e32504465faa943c537d8029b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: af22be3b7ab2b3b44afe7297460363ff, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2817
Packages/com.arongranberg.astar/CHANGELOG.md
Normal file
2817
Packages/com.arongranberg.astar/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
7
Packages/com.arongranberg.astar/CHANGELOG.md.meta
Normal file
7
Packages/com.arongranberg.astar/CHANGELOG.md.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e271255bdfe8941f9ab0acccbb14dd82
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
2
Packages/com.arongranberg.astar/Core.meta
Normal file
2
Packages/com.arongranberg.astar/Core.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6b8abb917bca4ce0ad1b26452b3c58d
|
||||
2
Packages/com.arongranberg.astar/Core/AI.meta
Normal file
2
Packages/com.arongranberg.astar/Core/AI.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ab9be352d07b44e68ad7c1a03eef3a5
|
||||
866
Packages/com.arongranberg.astar/Core/AI/AIBase.cs
Normal file
866
Packages/com.arongranberg.astar/Core/AI/AIBase.cs
Normal file
@@ -0,0 +1,866 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using UnityEngine.Serialization;
|
||||
using Unity.Jobs;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.RVO;
|
||||
using Pathfinding.Util;
|
||||
using Pathfinding.Jobs;
|
||||
using Pathfinding.Drawing;
|
||||
using UnityEngine.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for AIPath and RichAI.
|
||||
/// This class holds various methods and fields that are common to both AIPath and RichAI.
|
||||
///
|
||||
/// See: <see cref="Pathfinding.AIPath"/>
|
||||
/// See: <see cref="Pathfinding.RichAI"/>
|
||||
/// See: <see cref="Pathfinding.IAstarAI"/> (all movement scripts implement this interface)
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Seeker))]
|
||||
public abstract class AIBase : VersionedMonoBehaviour {
|
||||
/// <summary>\copydocref{IAstarAI.radius}</summary>
|
||||
public float radius = 0.5f;
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.height}</summary>
|
||||
public float height = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how often the agent will search for new paths (in seconds).
|
||||
/// The agent will plan a new path to the target every N seconds.
|
||||
///
|
||||
/// If you have fast moving targets or AIs, you might want to set it to a lower value.
|
||||
///
|
||||
/// See: <see cref="shouldRecalculatePath"/>
|
||||
/// See: <see cref="SearchPath"/>
|
||||
///
|
||||
/// Deprecated: This has been renamed to <see cref="autoRepath.period"/>.
|
||||
/// See: <see cref="AutoRepathPolicy"/>
|
||||
/// </summary>
|
||||
public float repathRate {
|
||||
get {
|
||||
return this.autoRepath.period;
|
||||
}
|
||||
set {
|
||||
this.autoRepath.period = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// \copydocref{IAstarAI::canSearch}
|
||||
/// Deprecated: This has been superseded by <see cref="autoRepath.mode"/>.
|
||||
/// </summary>
|
||||
public bool canSearch {
|
||||
get {
|
||||
return this.autoRepath.mode != AutoRepathPolicy.Mode.Never;
|
||||
}
|
||||
set {
|
||||
if (value) {
|
||||
if (this.autoRepath.mode == AutoRepathPolicy.Mode.Never) {
|
||||
this.autoRepath.mode = AutoRepathPolicy.Mode.EveryNSeconds;
|
||||
}
|
||||
} else {
|
||||
this.autoRepath.mode = AutoRepathPolicy.Mode.Never;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.canMove}</summary>
|
||||
public bool canMove = true;
|
||||
|
||||
/// <summary>Max speed in world units per second</summary>
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("speed")]
|
||||
public float maxSpeed = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Gravity to use.
|
||||
/// If set to (NaN,NaN,NaN) then Physics.Gravity (configured in the Unity project settings) will be used.
|
||||
/// If set to (0,0,0) then no gravity will be used and no raycast to check for ground penetration will be performed.
|
||||
/// </summary>
|
||||
public Vector3 gravity = new Vector3(float.NaN, float.NaN, float.NaN);
|
||||
|
||||
/// <summary>
|
||||
/// Layer mask to use for ground placement.
|
||||
/// Make sure this does not include the layer of any colliders attached to this gameobject.
|
||||
///
|
||||
/// See: <see cref="gravity"/>
|
||||
/// See: https://docs.unity3d.com/Manual/Layers.html
|
||||
/// </summary>
|
||||
public LayerMask groundMask = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Distance to the end point to consider the end of path to be reached.
|
||||
///
|
||||
/// When the end of the path is within this distance then <see cref="IAstarAI.reachedEndOfPath"/> will return true.
|
||||
/// When the <see cref="destination"/> is within this distance then <see cref="IAstarAI.reachedDestination"/> will return true.
|
||||
///
|
||||
/// Note that the <see cref="destination"/> may not be reached just because the end of the path was reached. The <see cref="destination"/> may not be reachable at all.
|
||||
///
|
||||
/// See: <see cref="IAstarAI.reachedEndOfPath"/>
|
||||
/// See: <see cref="IAstarAI.reachedDestination"/>
|
||||
/// </summary>
|
||||
public float endReachedDistance = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// What to do when within <see cref="endReachedDistance"/> units from the destination.
|
||||
/// The character can either stop immediately when it comes within that distance, which is useful for e.g archers
|
||||
/// or other ranged units that want to fire on a target. Or the character can continue to try to reach the exact
|
||||
/// destination point and come to a full stop there. This is useful if you want the character to reach the exact
|
||||
/// point that you specified.
|
||||
///
|
||||
/// Note: <see cref="IAstarAI.reachedEndOfPath"/> will become true when the character is within <see cref="endReachedDistance"/> units from the destination
|
||||
/// regardless of what this field is set to.
|
||||
/// </summary>
|
||||
public CloseToDestinationMode whenCloseToDestination = CloseToDestinationMode.Stop;
|
||||
|
||||
/// <summary>
|
||||
/// Controls if the agent slows down to a stop if the area around the destination is crowded.
|
||||
///
|
||||
/// Using this module requires that local avoidance is used: i.e. that an RVOController is attached to the GameObject.
|
||||
///
|
||||
/// See: <see cref="Pathfinding.RVO.RVODestinationCrowdedBehavior"/>
|
||||
/// See: local-avoidance (view in online documentation for working links)
|
||||
/// </summary>
|
||||
public RVODestinationCrowdedBehavior rvoDensityBehavior = new RVODestinationCrowdedBehavior(true, 0.5f, false);
|
||||
|
||||
[SerializeField]
|
||||
[HideInInspector]
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("repathRate")]
|
||||
float repathRateCompatibility = float.NaN;
|
||||
|
||||
[SerializeField]
|
||||
[HideInInspector]
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("canSearch")]
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("repeatedlySearchPaths")]
|
||||
bool canSearchCompability = false;
|
||||
|
||||
/// <summary>
|
||||
/// Determines which direction the agent moves in.
|
||||
/// For 3D games you most likely want the ZAxisIsForward option as that is the convention for 3D games.
|
||||
/// For 2D games you most likely want the YAxisIsForward option as that is the convention for 2D games.
|
||||
///
|
||||
/// Using the YAxisForward option will also allow the agent to assume that the movement will happen in the 2D (XY) plane instead of the XZ plane
|
||||
/// if it does not know. This is important only for the point graph which does not have a well defined up direction. The other built-in graphs (e.g the grid graph)
|
||||
/// will all tell the agent which movement plane it is supposed to use.
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
/// </summary>
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("rotationIn2D")]
|
||||
public OrientationMode orientation = OrientationMode.ZAxisForward;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the AI will rotate to face the movement direction.
|
||||
/// See: <see cref="orientation"/>
|
||||
/// </summary>
|
||||
public bool enableRotation = true;
|
||||
|
||||
/// <summary>
|
||||
/// Position of the agent.
|
||||
/// If <see cref="updatePosition"/> is true then this value will be synchronized every frame with Transform.position.
|
||||
/// </summary>
|
||||
protected Vector3 simulatedPosition;
|
||||
|
||||
/// <summary>
|
||||
/// Rotation of the agent.
|
||||
/// If <see cref="updateRotation"/> is true then this value will be synchronized every frame with Transform.rotation.
|
||||
/// </summary>
|
||||
protected Quaternion simulatedRotation;
|
||||
|
||||
/// <summary>
|
||||
/// Position of the agent.
|
||||
/// In world space.
|
||||
/// If <see cref="updatePosition"/> is true then this value is idential to transform.position.
|
||||
/// See: <see cref="Teleport"/>
|
||||
/// See: <see cref="Move"/>
|
||||
/// </summary>
|
||||
public Vector3 position { get { return updatePosition ? tr.position : simulatedPosition; } }
|
||||
|
||||
/// <summary>
|
||||
/// Rotation of the agent.
|
||||
/// If <see cref="updateRotation"/> is true then this value is identical to transform.rotation.
|
||||
/// </summary>
|
||||
public virtual Quaternion rotation {
|
||||
get { return updateRotation ? tr.rotation : simulatedRotation; }
|
||||
set {
|
||||
if (updateRotation) {
|
||||
tr.rotation = value;
|
||||
} else {
|
||||
simulatedRotation = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Accumulated movement deltas from the <see cref="Move"/> method</summary>
|
||||
protected Vector3 accumulatedMovementDelta = Vector3.zero;
|
||||
|
||||
/// <summary>
|
||||
/// Current desired velocity of the agent (does not include local avoidance and physics).
|
||||
/// Lies in the movement plane.
|
||||
/// </summary>
|
||||
protected Vector2 velocity2D;
|
||||
|
||||
/// <summary>
|
||||
/// Velocity due to gravity.
|
||||
/// Perpendicular to the movement plane.
|
||||
///
|
||||
/// When the agent is grounded this may not accurately reflect the velocity of the agent.
|
||||
/// It may be non-zero even though the agent is not moving.
|
||||
/// </summary>
|
||||
protected float verticalVelocity;
|
||||
|
||||
/// <summary>Cached Seeker component</summary>
|
||||
protected Seeker seeker;
|
||||
|
||||
/// <summary>Cached Transform component</summary>
|
||||
protected Transform tr;
|
||||
|
||||
/// <summary>Cached Rigidbody component</summary>
|
||||
protected Rigidbody rigid;
|
||||
|
||||
/// <summary>Cached Rigidbody component</summary>
|
||||
protected Rigidbody2D rigid2D;
|
||||
|
||||
/// <summary>Cached CharacterController component</summary>
|
||||
protected CharacterController controller;
|
||||
|
||||
/// <summary>Cached RVOController component</summary>
|
||||
protected RVOController rvoController;
|
||||
|
||||
/// <summary>
|
||||
/// Plane which this agent is moving in.
|
||||
/// This is used to convert between world space and a movement plane to make it possible to use this script in
|
||||
/// both 2D games and 3D games.
|
||||
/// </summary>
|
||||
public SimpleMovementPlane movementPlane = new SimpleMovementPlane(Quaternion.identity);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the character's position should be coupled to the Transform's position.
|
||||
/// If false then all movement calculations will happen as usual, but the object that this component is attached to will not move
|
||||
/// instead only the <see cref="position"/> property will change.
|
||||
///
|
||||
/// This is useful if you want to control the movement of the character using some other means such
|
||||
/// as for example root motion but still want the AI to move freely.
|
||||
/// See: Combined with calling <see cref="MovementUpdate"/> from a separate script instead of it being called automatically one can take a similar approach to what is documented here: https://docs.unity3d.com/Manual/nav-CouplingAnimationAndNavigation.html
|
||||
///
|
||||
/// See: <see cref="canMove"/> which in contrast to this field will disable all movement calculations.
|
||||
/// See: <see cref="updateRotation"/>
|
||||
/// </summary>
|
||||
public bool updatePosition { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the character's rotation should be coupled to the Transform's rotation.
|
||||
/// If false then all movement calculations will happen as usual, but the object that this component is attached to will not rotate
|
||||
/// instead only the <see cref="rotation"/> property will change.
|
||||
///
|
||||
/// See: <see cref="updatePosition"/>
|
||||
/// </summary>
|
||||
public bool updateRotation { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how the agent recalculates its path automatically.
|
||||
/// This corresponds to the settings under the "Recalculate Paths Automatically" field in the inspector.
|
||||
/// </summary>
|
||||
public AutoRepathPolicy autoRepath = new AutoRepathPolicy();
|
||||
|
||||
/// <summary>Indicates if gravity is used during this frame</summary>
|
||||
protected bool usingGravity { get; set; }
|
||||
|
||||
/// <summary>Delta time used for movement during the last frame</summary>
|
||||
protected float lastDeltaTime;
|
||||
|
||||
/// <summary>Position of the character at the end of the last frame</summary>
|
||||
protected Vector3 prevPosition1;
|
||||
|
||||
/// <summary>Position of the character at the end of the frame before the last frame</summary>
|
||||
protected Vector3 prevPosition2;
|
||||
|
||||
/// <summary>Amount which the character wants or tried to move with during the last frame</summary>
|
||||
protected Vector2 lastDeltaPosition;
|
||||
|
||||
/// <summary>Only when the previous path has been calculated should the script consider searching for a new path</summary>
|
||||
protected bool waitingForPathCalculation = false;
|
||||
|
||||
/// <summary>Time when the last path request was started</summary>
|
||||
protected float lastRepath = float.NegativeInfinity;
|
||||
|
||||
/// <summary>
|
||||
/// True if the Start method has been executed.
|
||||
/// Used to test if coroutines should be started in OnEnable to prevent calculating paths
|
||||
/// in the awake stage (or rather before start on frame 0).
|
||||
/// </summary>
|
||||
protected bool startHasRun = false;
|
||||
|
||||
/// <summary>Backing field for <see cref="destination"/></summary>
|
||||
Vector3 destinationBackingField = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.destination}</summary>
|
||||
public Vector3 destination {
|
||||
get { return destinationBackingField; }
|
||||
set {
|
||||
// Note: vector3 equality operator will return false if both are (inf,inf,inf). So do the extra check to see if both are infinity.
|
||||
if (rvoDensityBehavior.enabled && !(value == destinationBackingField || (float.IsPositiveInfinity(value.x) && float.IsPositiveInfinity(destinationBackingField.x)))) {
|
||||
destinationBackingField = value;
|
||||
rvoDensityBehavior.OnDestinationChanged(value, reachedDestination);
|
||||
} else {
|
||||
destinationBackingField = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.velocity}</summary>
|
||||
public Vector3 velocity {
|
||||
get {
|
||||
return lastDeltaTime > 0.000001f ? (prevPosition1 - prevPosition2) / lastDeltaTime : Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.desiredVelocity}</summary>
|
||||
public Vector3 desiredVelocity {
|
||||
get { return lastDeltaTime > 0.00001f ? movementPlane.ToWorld(lastDeltaPosition / lastDeltaTime, verticalVelocity) : Vector3.zero; }
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.desiredVelocityWithoutLocalAvoidance}</summary>
|
||||
public Vector3 desiredVelocityWithoutLocalAvoidance {
|
||||
get { return movementPlane.ToWorld(velocity2D, verticalVelocity); }
|
||||
set { velocity2D = movementPlane.ToPlane(value, out verticalVelocity); }
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.endOfPath}</summary>
|
||||
public abstract Vector3 endOfPath { get; }
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.reachedDestination}</summary>
|
||||
public abstract bool reachedDestination { get; }
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.isStopped}</summary>
|
||||
public bool isStopped { get; set; }
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.onSearchPath}</summary>
|
||||
public System.Action onSearchPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Cached delegate for the <see cref="OnPathComplete"/> method.
|
||||
///
|
||||
/// Caching this avoids allocating a new one every time a path is calculated, which reduces GC pressure.
|
||||
/// </summary>
|
||||
protected OnPathDelegate onPathComplete;
|
||||
|
||||
/// <summary>True if the path should be automatically recalculated as soon as possible</summary>
|
||||
protected virtual bool shouldRecalculatePath {
|
||||
get {
|
||||
return !waitingForPathCalculation && autoRepath.ShouldRecalculatePath(position, radius, destination, Time.time);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks for any attached components like RVOController and CharacterController etc.
|
||||
///
|
||||
/// This is done during <see cref="OnEnable"/>. If you are adding/removing components during runtime you may want to call this function
|
||||
/// to make sure that this script finds them. It is unfortunately prohibitive from a performance standpoint to look for components every frame.
|
||||
/// </summary>
|
||||
public virtual void FindComponents () {
|
||||
tr = transform;
|
||||
// GetComponent is a bit slow, so only call it if we don't know about the component already.
|
||||
// This is important when selecting a lot of objects in the editor as OnDrawGizmos will call
|
||||
// this method every frame when outside of play mode.
|
||||
if (!seeker) TryGetComponent(out seeker);
|
||||
if (!rvoController) TryGetComponent(out rvoController);
|
||||
// Find attached movement components
|
||||
if (!controller) TryGetComponent(out controller);
|
||||
if (!rigid) TryGetComponent(out rigid);
|
||||
if (!rigid2D) TryGetComponent(out rigid2D);
|
||||
}
|
||||
|
||||
/// <summary>Called when the component is enabled</summary>
|
||||
protected virtual void OnEnable () {
|
||||
FindComponents();
|
||||
onPathComplete = OnPathComplete;
|
||||
Init();
|
||||
|
||||
// When using rigidbodies all movement is done inside FixedUpdate instead of Update
|
||||
bool fixedUpdate = rigid != null || rigid2D != null;
|
||||
BatchedEvents.Add(this, fixedUpdate ? BatchedEvents.Event.FixedUpdate : BatchedEvents.Event.Update, OnUpdate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called every frame.
|
||||
/// This may be called during FixedUpdate or Update depending on if a rigidbody is attached to the GameObject.
|
||||
/// </summary>
|
||||
static void OnUpdate (AIBase[] components, int count, TransformAccessArray transforms, BatchedEvents.Event ev) {
|
||||
// Sync transforms to ensure raycasts will hit the correct colliders
|
||||
Physics.SyncTransforms();
|
||||
Physics2D.SyncTransforms();
|
||||
|
||||
float dt = ev == BatchedEvents.Event.FixedUpdate ? Time.fixedDeltaTime : Time.deltaTime;
|
||||
|
||||
var simulator = RVOSimulator.active?.GetSimulator();
|
||||
if (simulator != null) {
|
||||
int agentsWithRVOControllers = 0;
|
||||
for (int i = 0; i < count; i++) agentsWithRVOControllers += (components[i].rvoController != null && components[i].rvoController.enabled ? 1 : 0);
|
||||
RVODestinationCrowdedBehavior.JobDensityCheck densityJobData = new RVODestinationCrowdedBehavior.JobDensityCheck(agentsWithRVOControllers, dt, simulator);
|
||||
|
||||
for (int i = 0, j = 0; i < count; i++) {
|
||||
var agent = components[i];
|
||||
if (agent.rvoController != null && agent.rvoController.enabled) {
|
||||
densityJobData.Set(j, agent.rvoController.rvoAgent.AgentIndex, agent.endOfPath, agent.rvoDensityBehavior.densityThreshold, agent.rvoDensityBehavior.progressAverage);
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
var readLock = simulator.LockSimulationDataReadOnly();
|
||||
var densityJob = densityJobData.ScheduleBatch(agentsWithRVOControllers, agentsWithRVOControllers / 16, readLock.dependency);
|
||||
readLock.UnlockAfter(densityJob);
|
||||
densityJob.Complete();
|
||||
|
||||
for (int i = 0, j = 0; i < count; i++) {
|
||||
var agent = components[i];
|
||||
if (agent.rvoController != null && agent.rvoController.enabled) {
|
||||
agent.rvoDensityBehavior.ReadJobResult(ref densityJobData, j);
|
||||
j++;
|
||||
}
|
||||
}
|
||||
|
||||
densityJobData.Dispose();
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
var agent = components[i];
|
||||
UnityEngine.Profiling.Profiler.BeginSample("OnUpdate");
|
||||
agent.OnUpdate(dt);
|
||||
UnityEngine.Profiling.Profiler.EndSample();
|
||||
}
|
||||
|
||||
if (count > 0 && components[0] is AIPathAlignedToSurface) {
|
||||
AIPathAlignedToSurface.UpdateMovementPlanes(components as AIPathAlignedToSurface[], count);
|
||||
}
|
||||
|
||||
Physics.SyncTransforms();
|
||||
Physics2D.SyncTransforms();
|
||||
}
|
||||
|
||||
/// <summary>Called every frame</summary>
|
||||
protected virtual void OnUpdate (float dt) {
|
||||
// If gravity is used depends on a lot of things.
|
||||
// For example when a non-kinematic rigidbody is used then the rigidbody will apply the gravity itself
|
||||
// Note that the gravity can contain NaN's, which is why the comparison uses !(a==b) instead of just a!=b.
|
||||
usingGravity = !(gravity == Vector3.zero) && (!updatePosition || ((rigid == null || rigid.isKinematic) && (rigid2D == null || rigid2D.bodyType == RigidbodyType2D.Kinematic)));
|
||||
|
||||
if (shouldRecalculatePath) SearchPath();
|
||||
|
||||
if (canMove) {
|
||||
MovementUpdate(dt, out var nextPosition, out var nextRotation);
|
||||
UnityEngine.Profiling.Profiler.BeginSample("Finalize");
|
||||
FinalizeMovement(nextPosition, nextRotation);
|
||||
UnityEngine.Profiling.Profiler.EndSample();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts searching for paths.
|
||||
/// If you override this method you should in most cases call base.Start () at the start of it.
|
||||
/// See: <see cref="Init"/>
|
||||
/// </summary>
|
||||
protected virtual void Start () {
|
||||
startHasRun = true;
|
||||
Init();
|
||||
}
|
||||
|
||||
void Init () {
|
||||
if (startHasRun) {
|
||||
// Clamp the agent to the navmesh (which is what the Teleport call will do essentially. Though only some movement scripts require this, like RichAI).
|
||||
// The Teleport call will also make sure some variables are properly initialized (like #prevPosition1 and #prevPosition2)
|
||||
if (canMove) Teleport(position, false);
|
||||
autoRepath.Reset();
|
||||
if (shouldRecalculatePath) SearchPath();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.Teleport}</summary>
|
||||
public virtual void Teleport (Vector3 newPosition, bool clearPath = true) {
|
||||
if (clearPath) ClearPath();
|
||||
prevPosition1 = prevPosition2 = simulatedPosition = newPosition;
|
||||
if (updatePosition) tr.position = newPosition;
|
||||
if (rvoController != null) rvoController.Move(Vector3.zero);
|
||||
if (clearPath) SearchPath();
|
||||
}
|
||||
|
||||
protected void CancelCurrentPathRequest () {
|
||||
waitingForPathCalculation = false;
|
||||
// Abort calculation of the current path
|
||||
if (seeker != null) seeker.CancelCurrentPathRequest();
|
||||
}
|
||||
|
||||
protected virtual void OnDisable () {
|
||||
BatchedEvents.Remove(this);
|
||||
ClearPath();
|
||||
|
||||
velocity2D = Vector3.zero;
|
||||
accumulatedMovementDelta = Vector3.zero;
|
||||
verticalVelocity = 0f;
|
||||
lastDeltaTime = 0;
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.MovementUpdate}</summary>
|
||||
public void MovementUpdate (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) {
|
||||
lastDeltaTime = deltaTime;
|
||||
MovementUpdateInternal(deltaTime, out nextPosition, out nextRotation);
|
||||
}
|
||||
|
||||
/// <summary>Called during either Update or FixedUpdate depending on if rigidbodies are used for movement or not</summary>
|
||||
protected abstract void MovementUpdateInternal(float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation);
|
||||
|
||||
/// <summary>
|
||||
/// Outputs the start point and end point of the next automatic path request.
|
||||
/// This is a separate method to make it easy for subclasses to swap out the endpoints
|
||||
/// of path requests. For example the <see cref="LocalSpaceRichAI"/> script which requires the endpoints
|
||||
/// to be transformed to graph space first.
|
||||
/// </summary>
|
||||
protected virtual void CalculatePathRequestEndpoints (out Vector3 start, out Vector3 end) {
|
||||
start = GetFeetPosition();
|
||||
end = destination;
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.SearchPath}</summary>
|
||||
public virtual void SearchPath () {
|
||||
if (float.IsPositiveInfinity(destination.x)) return;
|
||||
if (onSearchPath != null) onSearchPath();
|
||||
|
||||
// Find out where we are and where we are going
|
||||
Vector3 start, end;
|
||||
CalculatePathRequestEndpoints(out start, out end);
|
||||
|
||||
// Request a path to be calculated from our current position to the destination
|
||||
ABPath p = ABPath.Construct(start, end, null);
|
||||
SetPath(p, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Position of the base of the character.
|
||||
/// This is used for pathfinding as the character's pivot point is sometimes placed
|
||||
/// at the center of the character instead of near the feet. In a building with multiple floors
|
||||
/// the center of a character may in some scenarios be closer to the navmesh on the floor above
|
||||
/// than to the floor below which could cause an incorrect path to be calculated.
|
||||
/// To solve this the start point of the requested paths is always at the base of the character.
|
||||
/// </summary>
|
||||
public virtual Vector3 GetFeetPosition () {
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>Called when a requested path has been calculated</summary>
|
||||
protected abstract void OnPathComplete(Path newPath);
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current path of the agent.
|
||||
///
|
||||
/// Usually invoked using SetPath(null).
|
||||
///
|
||||
/// See: <see cref="SetPath"/>
|
||||
/// See: <see cref="isStopped"/>
|
||||
/// </summary>
|
||||
protected abstract void ClearPath();
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.SetPath}</summary>
|
||||
public void SetPath (Path path, bool updateDestinationFromPath = true) {
|
||||
if (updateDestinationFromPath && path is ABPath abPath && abPath.endPointKnownBeforeCalculation) {
|
||||
this.destination = abPath.originalEndPoint;
|
||||
}
|
||||
|
||||
if (path == null) {
|
||||
CancelCurrentPathRequest();
|
||||
ClearPath();
|
||||
} else if (path.PipelineState == PathState.Created) {
|
||||
// Path has not started calculation yet
|
||||
waitingForPathCalculation = true;
|
||||
seeker.CancelCurrentPathRequest();
|
||||
seeker.StartPath(path, onPathComplete);
|
||||
autoRepath.DidRecalculatePath(destination, Time.time);
|
||||
} else if (path.PipelineState >= PathState.Returning) {
|
||||
// Path has already been calculated
|
||||
|
||||
// We might be calculating another path at the same time, and we don't want that path to override this one. So cancel it.
|
||||
if (seeker.GetCurrentPath() != path) seeker.CancelCurrentPathRequest();
|
||||
|
||||
OnPathComplete(path);
|
||||
} else {
|
||||
// Path calculation has been started, but it is not yet complete. Cannot really handle this.
|
||||
throw new System.ArgumentException("You must call the SetPath method with a path that either has been completely calculated or one whose path calculation has not been started at all. It looks like the path calculation for the path you tried to use has been started, but is not yet finished.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accelerates the agent downwards.
|
||||
/// See: <see cref="verticalVelocity"/>
|
||||
/// See: <see cref="gravity"/>
|
||||
/// </summary>
|
||||
protected virtual void ApplyGravity (float deltaTime) {
|
||||
// Apply gravity
|
||||
if (usingGravity) {
|
||||
float verticalGravity;
|
||||
velocity2D += movementPlane.ToPlane(deltaTime * (float.IsNaN(gravity.x) ? Physics.gravity : gravity), out verticalGravity);
|
||||
verticalVelocity += verticalGravity;
|
||||
} else {
|
||||
verticalVelocity = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculates how far to move during a single frame</summary>
|
||||
protected Vector2 CalculateDeltaToMoveThisFrame (Vector3 position, float distanceToEndOfPath, float deltaTime) {
|
||||
if (rvoController != null && rvoController.enabled) {
|
||||
// Use RVOController to get a processed delta position
|
||||
// such that collisions will be avoided if possible
|
||||
return movementPlane.ToPlane(rvoController.CalculateMovementDelta(position, deltaTime));
|
||||
}
|
||||
// Direction and distance to move during this frame
|
||||
return Vector2.ClampMagnitude(velocity2D * deltaTime, distanceToEndOfPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates rotating the agent towards the specified direction and returns the new rotation.
|
||||
///
|
||||
/// Note that this only calculates a new rotation, it does not change the actual rotation of the agent.
|
||||
/// Useful when you are handling movement externally using <see cref="FinalizeMovement"/> but you want to use the built-in rotation code.
|
||||
///
|
||||
/// See: <see cref="orientation"/>
|
||||
/// </summary>
|
||||
/// <param name="direction">Direction in world space to rotate towards.</param>
|
||||
/// <param name="maxDegrees">Maximum number of degrees to rotate this frame.</param>
|
||||
public Quaternion SimulateRotationTowards (Vector3 direction, float maxDegrees) {
|
||||
return SimulateRotationTowards(movementPlane.ToPlane(direction), maxDegrees, maxDegrees);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Simulates rotating the agent towards the specified direction and returns the new rotation.
|
||||
///
|
||||
/// Note that this only calculates a new rotation, it does not change the actual rotation of the agent.
|
||||
///
|
||||
/// See: <see cref="orientation"/>
|
||||
/// See: <see cref="movementPlane"/>
|
||||
/// </summary>
|
||||
/// <param name="direction">Direction in the movement plane to rotate towards.</param>
|
||||
/// <param name="maxDegreesMainAxis">Maximum number of degrees to rotate this frame around the character's main axis. This is rotating left and right as a character normally does.</param>
|
||||
/// <param name="maxDegreesOffAxis">Maximum number of degrees to rotate this frame around other axes. This is used to ensure the character's up direction is correct.
|
||||
/// It is only used for non-planar worlds where the up direction changes depending on the position of the character.
|
||||
/// More precisely a faster code path which ignores this parameter is used whenever the current #movementPlane is exactly the XZ or XY plane.
|
||||
/// This must be at least as large as maxDegreesMainAxis.</param>
|
||||
protected Quaternion SimulateRotationTowards (Vector2 direction, float maxDegreesMainAxis, float maxDegreesOffAxis = float.PositiveInfinity) {
|
||||
Quaternion targetRotation;
|
||||
|
||||
if (movementPlane.isXY || movementPlane.isXZ) {
|
||||
if (direction == Vector2.zero) return simulatedRotation;
|
||||
|
||||
// Common fast path.
|
||||
// A standard XY or XZ movement plane indicates that the character is moving in a normal planar world.
|
||||
// We will use a much faster code path for this case since we don't have to deal with changing the 'up' direction of the character all the time.
|
||||
// This code path mostly works for non-planar worlds too, but it will fail in some cases.
|
||||
// In particular it will not be able to adjust the up direction of the character while it is standing still (because then a zero maxDegreesMainAxis is usually passed).
|
||||
// That case may be important, especially when the character has just been spawned and does not have a destination yet.
|
||||
targetRotation = Quaternion.LookRotation(movementPlane.ToWorld(direction, 0), movementPlane.ToWorld(Vector2.zero, 1));
|
||||
maxDegreesOffAxis = maxDegreesMainAxis;
|
||||
} else {
|
||||
// Decompose the rotation into two parts: a rotation around the main axis of the character, and a rotation around the other axes.
|
||||
// Then limit the rotation speed along those two components separately.
|
||||
var forwardInPlane = movementPlane.ToPlane(rotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward));
|
||||
|
||||
// Can happen if the character is perpendicular to the plane
|
||||
if (forwardInPlane == Vector2.zero) forwardInPlane = Vector2.right;
|
||||
|
||||
var rotationVectorAroundMainAxis = VectorMath.ComplexMultiplyConjugate(direction, forwardInPlane);
|
||||
|
||||
// Note: If the direction is zero, then angle will also be zero since atan2(0,0) = 0
|
||||
var angle = Mathf.Atan2(rotationVectorAroundMainAxis.y, rotationVectorAroundMainAxis.x) * Mathf.Rad2Deg;
|
||||
var rotationAroundMainAxis = Quaternion.AngleAxis(-Mathf.Min(Mathf.Abs(angle), maxDegreesMainAxis) * Mathf.Sign(angle), Vector3.up);
|
||||
|
||||
targetRotation = Quaternion.LookRotation(movementPlane.ToWorld(forwardInPlane, 0), movementPlane.ToWorld(Vector2.zero, 1));
|
||||
targetRotation = targetRotation * rotationAroundMainAxis;
|
||||
}
|
||||
|
||||
// This causes the character to only rotate around the Z axis
|
||||
if (orientation == OrientationMode.YAxisForward) targetRotation *= Quaternion.Euler(90, 0, 0);
|
||||
return Quaternion.RotateTowards(simulatedRotation, targetRotation, maxDegreesOffAxis);
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.Move}</summary>
|
||||
public virtual void Move (Vector3 deltaPosition) {
|
||||
accumulatedMovementDelta += deltaPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the agent to a position.
|
||||
///
|
||||
/// This is used if you want to override how the agent moves. For example if you are using
|
||||
/// root motion with Mecanim.
|
||||
///
|
||||
/// This will use a CharacterController, Rigidbody, Rigidbody2D or the Transform component depending on what options
|
||||
/// are available.
|
||||
///
|
||||
/// The agent will be clamped to the navmesh after the movement (if such information is available, generally this is only done by the RichAI component).
|
||||
///
|
||||
/// See: <see cref="MovementUpdate"/> for some example code.
|
||||
/// See: <see cref="controller"/>, <see cref="rigid"/>, <see cref="rigid2D"/>
|
||||
/// </summary>
|
||||
/// <param name="nextPosition">New position of the agent.</param>
|
||||
/// <param name="nextRotation">New rotation of the agent. If #enableRotation is false then this parameter will be ignored.</param>
|
||||
public virtual void FinalizeMovement (Vector3 nextPosition, Quaternion nextRotation) {
|
||||
if (enableRotation) FinalizeRotation(nextRotation);
|
||||
FinalizePosition(nextPosition);
|
||||
}
|
||||
|
||||
void FinalizeRotation (Quaternion nextRotation) {
|
||||
simulatedRotation = nextRotation;
|
||||
if (updateRotation) {
|
||||
if (rigid != null) rigid.MoveRotation(nextRotation);
|
||||
else if (rigid2D != null) rigid2D.MoveRotation(nextRotation.eulerAngles.z);
|
||||
else tr.rotation = nextRotation;
|
||||
}
|
||||
}
|
||||
|
||||
void FinalizePosition (Vector3 nextPosition) {
|
||||
// Use a local variable, it is significantly faster
|
||||
Vector3 currentPosition = simulatedPosition;
|
||||
bool positionDirty1 = false;
|
||||
|
||||
if (controller != null && controller.enabled && updatePosition) {
|
||||
// Use CharacterController
|
||||
// The Transform may not be at #position if it was outside the navmesh and had to be moved to the closest valid position
|
||||
tr.position = currentPosition;
|
||||
controller.Move((nextPosition - currentPosition) + accumulatedMovementDelta);
|
||||
// Grab the position after the movement to be able to take physics into account
|
||||
// TODO: Add this into the clampedPosition calculation below to make RVO better respond to physics
|
||||
currentPosition = tr.position;
|
||||
if (controller.isGrounded) verticalVelocity = 0;
|
||||
} else {
|
||||
// Use Transform, Rigidbody, Rigidbody2D or nothing at all (if updatePosition = false)
|
||||
float lastElevation;
|
||||
movementPlane.ToPlane(currentPosition, out lastElevation);
|
||||
currentPosition = nextPosition + accumulatedMovementDelta;
|
||||
|
||||
// Position the character on the ground
|
||||
if (usingGravity) currentPosition = RaycastPosition(currentPosition, lastElevation);
|
||||
positionDirty1 = true;
|
||||
}
|
||||
|
||||
// Clamp the position to the navmesh after movement is done
|
||||
bool positionDirty2 = false;
|
||||
currentPosition = ClampToNavmesh(currentPosition, out positionDirty2);
|
||||
|
||||
// Assign the final position to the character if we haven't already set it (mostly for performance, setting the position can be slow)
|
||||
if ((positionDirty1 || positionDirty2) && updatePosition) {
|
||||
// Note that rigid.MovePosition may or may not move the character immediately.
|
||||
// Check the Unity documentation for the special cases.
|
||||
if (rigid != null) rigid.MovePosition(currentPosition);
|
||||
else if (rigid2D != null) rigid2D.MovePosition(currentPosition);
|
||||
else tr.position = currentPosition;
|
||||
}
|
||||
|
||||
accumulatedMovementDelta = Vector3.zero;
|
||||
simulatedPosition = currentPosition;
|
||||
UpdateVelocity();
|
||||
}
|
||||
|
||||
protected void UpdateVelocity () {
|
||||
prevPosition2 = prevPosition1;
|
||||
prevPosition1 = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constrains the character's position to lie on the navmesh.
|
||||
/// Not all movement scripts have support for this.
|
||||
///
|
||||
/// Returns: New position of the character that has been clamped to the navmesh.
|
||||
/// </summary>
|
||||
/// <param name="position">Current position of the character.</param>
|
||||
/// <param name="positionChanged">True if the character's position was modified by this method.</param>
|
||||
protected virtual Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) {
|
||||
positionChanged = false;
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hit info from the last raycast done for ground placement.
|
||||
/// Will not update unless gravity is used (if no gravity is used, then raycasts are disabled).
|
||||
///
|
||||
/// See: <see cref="RaycastPosition"/>
|
||||
/// </summary>
|
||||
protected RaycastHit lastRaycastHit;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the character is grounded and prevents ground penetration.
|
||||
///
|
||||
/// Sets <see cref="verticalVelocity"/> to zero if the character is grounded.
|
||||
///
|
||||
/// Returns: The new position of the character.
|
||||
/// </summary>
|
||||
/// <param name="position">Position of the character in the world.</param>
|
||||
/// <param name="lastElevation">Elevation coordinate before the agent was moved. This is along the 'up' axis of the #movementPlane.</param>
|
||||
protected Vector3 RaycastPosition (Vector3 position, float lastElevation) {
|
||||
float elevation;
|
||||
|
||||
movementPlane.ToPlane(position, out elevation);
|
||||
float rayLength = tr.localScale.y * height * 0.5f + Mathf.Max(0, lastElevation-elevation);
|
||||
Vector3 rayOffset = movementPlane.ToWorld(Vector2.zero, rayLength);
|
||||
|
||||
if (Physics.Raycast(position + rayOffset, -rayOffset, out lastRaycastHit, rayLength, groundMask, QueryTriggerInteraction.Ignore)) {
|
||||
// Grounded
|
||||
// Make the vertical velocity fall off exponentially. This is reasonable from a physical standpoint as characters
|
||||
// are not completely stiff and touching the ground will not immediately negate all velocity downwards. The AI will
|
||||
// stop moving completely due to the raycast penetration test but it will still *try* to move downwards. This helps
|
||||
// significantly when moving down along slopes as if the vertical velocity would be set to zero when the character
|
||||
// was grounded it would lead to a kind of 'bouncing' behavior (try it, it's hard to explain). Ideally this should
|
||||
// use a more physically correct formula but this is a good approximation and is much more performant. The constant
|
||||
// '5' in the expression below determines how quickly it converges but high values can lead to too much noise.
|
||||
verticalVelocity *= System.Math.Max(0, 1 - 5 * lastDeltaTime);
|
||||
return lastRaycastHit.point;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
protected virtual void OnDrawGizmosSelected () {
|
||||
// When selected in the Unity inspector it's nice to make the component react instantly if
|
||||
// any other components are attached/detached or enabled/disabled.
|
||||
// We don't want to do this normally every frame because that would be expensive.
|
||||
if (Application.isPlaying) FindComponents();
|
||||
}
|
||||
|
||||
public static readonly Color ShapeGizmoColor = new Color(240/255f, 213/255f, 30/255f);
|
||||
|
||||
public override void DrawGizmos () {
|
||||
if (!Application.isPlaying || !enabled || tr == null) FindComponents();
|
||||
|
||||
var color = ShapeGizmoColor;
|
||||
if (rvoController != null && rvoController.locked) color *= 0.5f;
|
||||
if (orientation == OrientationMode.YAxisForward) {
|
||||
Draw.WireCylinder(position, Vector3.forward, 0, radius * tr.localScale.x, color);
|
||||
} else {
|
||||
Draw.WireCylinder(position, rotation * Vector3.up, tr.localScale.y * height, radius * tr.localScale.x, color);
|
||||
}
|
||||
|
||||
if (!float.IsPositiveInfinity(destination.x) && Application.isPlaying) Draw.Circle(destination, movementPlane.rotation * Vector3.up, 0.2f, Color.blue);
|
||||
|
||||
autoRepath.DrawGizmos(Draw.editor, position, radius, new NativeMovementPlane(movementPlane.rotation));
|
||||
}
|
||||
|
||||
protected override void Reset () {
|
||||
ResetShape();
|
||||
base.Reset();
|
||||
}
|
||||
|
||||
void ResetShape () {
|
||||
if (TryGetComponent(out CharacterController cc)) {
|
||||
radius = cc.radius;
|
||||
height = Mathf.Max(radius*2, cc.height);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) {
|
||||
if (migrations.TryMigrateFromLegacyFormat(out var legacyVersion)) {
|
||||
// Version == 5 is the 4.2 branch.
|
||||
if (legacyVersion <= 2 || legacyVersion == 5) rvoDensityBehavior.enabled = false;
|
||||
|
||||
if (legacyVersion <= 3) {
|
||||
repathRate = repathRateCompatibility;
|
||||
canSearch = canSearchCompability;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Packages/com.arongranberg.astar/Core/AI/AIBase.cs.meta
Normal file
12
Packages/com.arongranberg.astar/Core/AI/AIBase.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a08f67bbe580e4ddfaebd06363c9cc97
|
||||
timeCreated: 1496932372
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 100
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
718
Packages/com.arongranberg.astar/Core/AI/AILerp.cs
Normal file
718
Packages/com.arongranberg.astar/Core/AI/AILerp.cs
Normal file
@@ -0,0 +1,718 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Linearly interpolating movement script.
|
||||
///
|
||||
/// This movement script will follow the path exactly, it uses linear interpolation to move between the waypoints in the path.
|
||||
/// This is desirable for some types of games.
|
||||
/// It also works in 2D.
|
||||
///
|
||||
/// See: You can see an example of this script in action in the example scene called Example15_2D.
|
||||
///
|
||||
/// \section rec Configuration
|
||||
/// \subsection rec-snapped Recommended setup for movement along connections
|
||||
///
|
||||
/// This depends on what type of movement you are aiming for.
|
||||
/// If you are aiming for movement where the unit follows the path exactly and move only along the graph connections on a grid/point graph.
|
||||
/// I recommend that you adjust the StartEndModifier on the Seeker component: set the 'Start Point Snapping' field to 'NodeConnection' and the 'End Point Snapping' field to 'SnapToNode'.
|
||||
/// [Open online documentation to see images]
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// \subsection rec-smooth Recommended setup for smooth movement
|
||||
/// If you on the other hand want smoother movement I recommend setting 'Start Point Snapping' and 'End Point Snapping' to 'ClosestOnNode' and to add the Simple Smooth Modifier to the GameObject as well.
|
||||
/// Alternatively you can use the <see cref="Pathfinding.FunnelModifier Funnel"/> which works better on navmesh/recast graphs or the <see cref="Pathfinding.RaycastModifier"/>.
|
||||
///
|
||||
/// You should not combine the Simple Smooth Modifier or the Funnel Modifier with the NodeConnection snapping mode. This may lead to very odd behavior.
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
/// [Open online documentation to see images]
|
||||
/// You may also want to tweak the <see cref="rotationSpeed"/>.
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Seeker))]
|
||||
[AddComponentMenu("Pathfinding/AI/AILerp (2D,3D)")]
|
||||
[UniqueComponent(tag = "ai")]
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/ailerp.html")]
|
||||
public class AILerp : VersionedMonoBehaviour, IAstarAI {
|
||||
/// <summary>
|
||||
/// Determines how often it will search for new paths.
|
||||
/// If you have fast moving targets or AIs, you might want to set it to a lower value.
|
||||
/// The value is in seconds between path requests.
|
||||
///
|
||||
/// Deprecated: This has been renamed to <see cref="autoRepath.period"/>.
|
||||
/// See: <see cref="AutoRepathPolicy"/>
|
||||
/// </summary>
|
||||
public float repathRate {
|
||||
get {
|
||||
return this.autoRepath.period;
|
||||
}
|
||||
set {
|
||||
this.autoRepath.period = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// \copydoc Pathfinding::IAstarAI::canSearch
|
||||
/// Deprecated: This has been superseded by <see cref="autoRepath.mode"/>.
|
||||
/// </summary>
|
||||
public bool canSearch {
|
||||
get {
|
||||
return this.autoRepath.mode != AutoRepathPolicy.Mode.Never;
|
||||
}
|
||||
set {
|
||||
this.autoRepath.mode = value ? AutoRepathPolicy.Mode.EveryNSeconds : AutoRepathPolicy.Mode.Never;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines how the agent recalculates its path automatically.
|
||||
/// This corresponds to the settings under the "Recalculate Paths Automatically" field in the inspector.
|
||||
/// </summary>
|
||||
public AutoRepathPolicy autoRepath = new AutoRepathPolicy();
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary>
|
||||
public bool canMove = true;
|
||||
|
||||
/// <summary>Speed in world units</summary>
|
||||
public float speed = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Determines which direction the agent moves in.
|
||||
/// For 3D games you most likely want the ZAxisIsForward option as that is the convention for 3D games.
|
||||
/// For 2D games you most likely want the YAxisIsForward option as that is the convention for 2D games.
|
||||
///
|
||||
/// Using the YAxisForward option will also allow the agent to assume that the movement will happen in the 2D (XY) plane instead of the XZ plane
|
||||
/// if it does not know. This is important only for the point graph which does not have a well defined up direction. The other built-in graphs (e.g the grid graph)
|
||||
/// will all tell the agent which movement plane it is supposed to use.
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
/// </summary>
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("rotationIn2D")]
|
||||
public OrientationMode orientation = OrientationMode.ZAxisForward;
|
||||
|
||||
/// <summary>
|
||||
/// If true, the AI will rotate to face the movement direction.
|
||||
/// See: <see cref="orientation"/>
|
||||
/// </summary>
|
||||
public bool enableRotation = true;
|
||||
|
||||
/// <summary>How quickly to rotate</summary>
|
||||
public float rotationSpeed = 10;
|
||||
|
||||
/// <summary>
|
||||
/// If true, some interpolation will be done when a new path has been calculated.
|
||||
/// This is used to avoid short distance teleportation.
|
||||
/// See: <see cref="switchPathInterpolationSpeed"/>
|
||||
/// </summary>
|
||||
public bool interpolatePathSwitches = true;
|
||||
|
||||
/// <summary>
|
||||
/// How quickly to interpolate to the new path.
|
||||
/// See: <see cref="interpolatePathSwitches"/>
|
||||
/// </summary>
|
||||
public float switchPathInterpolationSpeed = 5;
|
||||
|
||||
/// <summary>True if the end of the current path has been reached</summary>
|
||||
public bool reachedEndOfPath { get; private set; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary>
|
||||
public bool reachedDestination {
|
||||
get {
|
||||
if (!reachedEndOfPath || !interpolator.valid) return false;
|
||||
// Note: distanceToSteeringTarget is the distance to the end of the path when approachingPathEndpoint is true
|
||||
var dir = destination - interpolator.endPoint;
|
||||
// Ignore either the y or z coordinate depending on if we are using 2D mode or not
|
||||
if (orientation == OrientationMode.YAxisForward) dir.z = 0;
|
||||
else dir.y = 0;
|
||||
|
||||
// Check against using a very small margin
|
||||
// In theory a check against 0 should be done, but this will be a bit more resilient against targets that move slowly or maybe jitter around due to floating point errors.
|
||||
if (remainingDistance + dir.magnitude >= 0.05f) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 destination { get; set; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::movementPlane</summary>
|
||||
public NativeMovementPlane movementPlane {
|
||||
get {
|
||||
if (path != null && path.path.Count > 0) {
|
||||
var graph = path.path[0].Graph;
|
||||
if (graph is NavmeshBase navmeshBase) {
|
||||
return new NativeMovementPlane(navmeshBase.transform.ToSimpleMovementPlane());
|
||||
} else if (graph is GridGraph gg) {
|
||||
return new NativeMovementPlane(gg.transform.ToSimpleMovementPlane());
|
||||
}
|
||||
}
|
||||
return new NativeMovementPlane(Unity.Mathematics.quaternion.identity);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the character's position should be coupled to the Transform's position.
|
||||
/// If false then all movement calculations will happen as usual, but the object that this component is attached to will not move
|
||||
/// instead only the <see cref="position"/> property will change.
|
||||
///
|
||||
/// See: <see cref="canMove"/> which in contrast to this field will disable all movement calculations.
|
||||
/// See: <see cref="updateRotation"/>
|
||||
/// </summary>
|
||||
public bool updatePosition { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the character's rotation should be coupled to the Transform's rotation.
|
||||
/// If false then all movement calculations will happen as usual, but the object that this component is attached to will not rotate
|
||||
/// instead only the <see cref="rotation"/> property will change.
|
||||
///
|
||||
/// See: <see cref="updatePosition"/>
|
||||
/// </summary>
|
||||
public bool updateRotation { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Cached delegate for the <see cref="OnPathComplete"/> method.
|
||||
///
|
||||
/// Caching this avoids allocating a new one every time a path is calculated, which reduces GC pressure.
|
||||
/// </summary>
|
||||
protected OnPathDelegate onPathComplete;
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::position</summary>
|
||||
public Vector3 position { get { return updatePosition ? tr.position : simulatedPosition; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::rotation</summary>
|
||||
public Quaternion rotation {
|
||||
get { return updateRotation ? tr.rotation : simulatedRotation; }
|
||||
set {
|
||||
if (updateRotation) {
|
||||
tr.rotation = value;
|
||||
} else {
|
||||
simulatedRotation = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::endOfPath</summary>
|
||||
public Vector3 endOfPath {
|
||||
get {
|
||||
if (interpolator.valid) return interpolator.endPoint;
|
||||
if (float.IsFinite(destination.x)) return destination;
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
#region IAstarAI implementation
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::Move</summary>
|
||||
void IAstarAI.Move (Vector3 deltaPosition) {
|
||||
// This script does not know the concept of being away from the path that it is following
|
||||
// so this call will be ignored (as is also mentioned in the documentation).
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::radius</summary>
|
||||
float IAstarAI.radius { get { return 0; } set {} }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::height</summary>
|
||||
float IAstarAI.height { get { return 0; } set {} }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary>
|
||||
float IAstarAI.maxSpeed { get { return speed; } set { speed = value; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::canSearch</summary>
|
||||
bool IAstarAI.canSearch { get { return canSearch; } set { canSearch = value; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary>
|
||||
bool IAstarAI.canMove { get { return canMove; } set { canMove = value; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::velocity</summary>
|
||||
public Vector3 velocity {
|
||||
get {
|
||||
return Time.deltaTime > 0.00001f ? (previousPosition1 - previousPosition2) / Time.deltaTime : Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 IAstarAI.desiredVelocity {
|
||||
get {
|
||||
// The AILerp script sets the position every frame. It does not take into account physics
|
||||
// or other things. So the velocity should always be the same as the desired velocity.
|
||||
return (this as IAstarAI).velocity;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 IAstarAI.desiredVelocityWithoutLocalAvoidance {
|
||||
get {
|
||||
// The AILerp script sets the position every frame. It does not take into account physics
|
||||
// or other things. So the velocity should always be the same as the desired velocity.
|
||||
return (this as IAstarAI).velocity;
|
||||
}
|
||||
set {
|
||||
throw new System.InvalidOperationException("The AILerp component does not support setting the desiredVelocityWithoutLocalAvoidance property since it does not make sense for how its movement works.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary>
|
||||
Vector3 IAstarAI.steeringTarget {
|
||||
get {
|
||||
// AILerp doesn't use steering at all, so we will just return a point ahead of the agent in the direction it is moving.
|
||||
return interpolator.valid ? interpolator.position + interpolator.tangent : simulatedPosition;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::remainingDistance</summary>
|
||||
public float remainingDistance {
|
||||
get {
|
||||
return interpolator.valid ? Mathf.Max(interpolator.remainingDistance, 0) : float.PositiveInfinity;
|
||||
}
|
||||
set {
|
||||
if (!interpolator.valid) throw new System.InvalidOperationException("Cannot set the remaining distance on the AILerp component because it doesn't have a path to follow.");
|
||||
interpolator.remainingDistance = Mathf.Max(value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary>
|
||||
public bool hasPath {
|
||||
get {
|
||||
return interpolator.valid;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary>
|
||||
public bool pathPending {
|
||||
get {
|
||||
return !canSearchAgain;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::isStopped</summary>
|
||||
public bool isStopped { get; set; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::onSearchPath</summary>
|
||||
public System.Action onSearchPath { get; set; }
|
||||
|
||||
/// <summary>Cached Seeker component</summary>
|
||||
protected Seeker seeker;
|
||||
|
||||
/// <summary>Cached Transform component</summary>
|
||||
protected Transform tr;
|
||||
|
||||
/// <summary>Current path which is followed</summary>
|
||||
protected ABPath path;
|
||||
|
||||
/// <summary>Only when the previous path has been returned should a search for a new path be done</summary>
|
||||
protected bool canSearchAgain = true;
|
||||
|
||||
/// <summary>
|
||||
/// When a new path was returned, the AI was moving along this ray.
|
||||
/// Used to smoothly interpolate between the previous movement and the movement along the new path.
|
||||
/// The speed is equal to movement direction.
|
||||
/// </summary>
|
||||
protected Vector3 previousMovementOrigin;
|
||||
protected Vector3 previousMovementDirection;
|
||||
|
||||
/// <summary>
|
||||
/// Time since the path was replaced by a new path.
|
||||
/// See: <see cref="interpolatePathSwitches"/>
|
||||
/// </summary>
|
||||
protected float pathSwitchInterpolationTime = 0;
|
||||
|
||||
protected PathInterpolator.Cursor interpolator;
|
||||
protected PathInterpolator interpolatorPath = new PathInterpolator();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Holds if the Start function has been run.
|
||||
/// Used to test if coroutines should be started in OnEnable to prevent calculating paths
|
||||
/// in the awake stage (or rather before start on frame 0).
|
||||
/// </summary>
|
||||
bool startHasRun = false;
|
||||
|
||||
Vector3 previousPosition1, previousPosition2, simulatedPosition;
|
||||
Quaternion simulatedRotation;
|
||||
|
||||
[SerializeField]
|
||||
[HideInInspector]
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("repathRate")]
|
||||
float repathRateCompatibility = float.NaN;
|
||||
|
||||
[SerializeField]
|
||||
[HideInInspector]
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("canSearch")]
|
||||
bool canSearchCompability = false;
|
||||
|
||||
protected AILerp () {
|
||||
// Note that this needs to be set here in the constructor and not in e.g Awake
|
||||
// because it is possible that other code runs and sets the destination property
|
||||
// before the Awake method on this script runs.
|
||||
destination = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes reference variables.
|
||||
/// If you override this function you should in most cases call base.Awake () at the start of it.
|
||||
/// </summary>
|
||||
protected override void Awake () {
|
||||
base.Awake();
|
||||
//This is a simple optimization, cache the transform component lookup
|
||||
tr = transform;
|
||||
|
||||
seeker = GetComponent<Seeker>();
|
||||
|
||||
// Tell the StartEndModifier to ask for our exact position when post processing the path This
|
||||
// is important if we are using prediction and requesting a path from some point slightly ahead
|
||||
// of us since then the start point in the path request may be far from our position when the
|
||||
// path has been calculated. This is also good because if a long path is requested, it may take
|
||||
// a few frames for it to be calculated so we could have moved some distance during that time
|
||||
seeker.startEndModifier.adjustStartPoint = () => simulatedPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts searching for paths.
|
||||
/// If you override this function you should in most cases call base.Start () at the start of it.
|
||||
/// See: <see cref="Init"/>
|
||||
/// </summary>
|
||||
protected virtual void Start () {
|
||||
startHasRun = true;
|
||||
Init();
|
||||
}
|
||||
|
||||
/// <summary>Called when the component is enabled</summary>
|
||||
protected virtual void OnEnable () {
|
||||
onPathComplete = OnPathComplete;
|
||||
Init();
|
||||
}
|
||||
|
||||
void Init () {
|
||||
if (startHasRun) {
|
||||
// The Teleport call will make sure some variables are properly initialized (like #prevPosition1 and #prevPosition2)
|
||||
Teleport(position, false);
|
||||
autoRepath.Reset();
|
||||
if (shouldRecalculatePath) SearchPath();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDisable () {
|
||||
ClearPath();
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,bool)}</summary>
|
||||
public void GetRemainingPath (List<Vector3> buffer, out bool stale) {
|
||||
buffer.Clear();
|
||||
if (!interpolator.valid) {
|
||||
buffer.Add(position);
|
||||
stale = true;
|
||||
return;
|
||||
}
|
||||
|
||||
stale = false;
|
||||
interpolator.GetRemainingPath(buffer);
|
||||
// The agent is almost always at interpolation.position (which is buffer[0])
|
||||
// but sometimes - in particular when interpolating between two paths - the agent might at a slightly different position.
|
||||
// So we replace the first point with the actual position of the agent.
|
||||
buffer[0] = position;
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,List<PathPartWithLinkInfo>,bool)}</summary>
|
||||
public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale) {
|
||||
GetRemainingPath(buffer, out stale);
|
||||
// This movement script doesn't keep track of path parts, so we just add the whole path as a single part
|
||||
if (partsBuffer != null) {
|
||||
partsBuffer.Clear();
|
||||
partsBuffer.Add(new PathPartWithLinkInfo { startIndex = 0, endIndex = buffer.Count - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
public void Teleport (Vector3 position, bool clearPath = true) {
|
||||
if (clearPath) ClearPath();
|
||||
simulatedPosition = previousPosition1 = previousPosition2 = position;
|
||||
if (updatePosition) tr.position = position;
|
||||
reachedEndOfPath = false;
|
||||
if (clearPath) SearchPath();
|
||||
}
|
||||
|
||||
/// <summary>True if the path should be automatically recalculated as soon as possible</summary>
|
||||
protected virtual bool shouldRecalculatePath {
|
||||
get {
|
||||
return canSearchAgain && autoRepath.ShouldRecalculatePath(position, 0.0f, destination, Time.time);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Requests a path to the target.</summary>
|
||||
public virtual void SearchPath () {
|
||||
if (float.IsPositiveInfinity(destination.x)) return;
|
||||
if (onSearchPath != null) onSearchPath();
|
||||
|
||||
// This is where the path should start to search from
|
||||
var currentPosition = GetFeetPosition();
|
||||
|
||||
// If we are following a path, start searching from the node we will
|
||||
// reach next this can prevent odd turns right at the start of the path
|
||||
/*if (interpolator.valid) {
|
||||
var prevDist = interpolator.distance;
|
||||
// Move to the end of the current segment
|
||||
interpolator.MoveToSegment(interpolator.segmentIndex, 1);
|
||||
currentPosition = interpolator.position;
|
||||
// Move back to the original position
|
||||
interpolator.distance = prevDist;
|
||||
}*/
|
||||
|
||||
canSearchAgain = false;
|
||||
|
||||
// Create a new path request
|
||||
// The OnPathComplete method will later be called with the result
|
||||
SetPath(ABPath.Construct(currentPosition, destination, null), false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The end of the path has been reached.
|
||||
/// If you want custom logic for when the AI has reached it's destination
|
||||
/// add it here.
|
||||
/// You can also create a new script which inherits from this one
|
||||
/// and override the function in that script.
|
||||
///
|
||||
/// Deprecated: Avoid overriding this method. Instead poll the <see cref="reachedDestination"/> or <see cref="reachedEndOfPath"/> properties.
|
||||
/// </summary>
|
||||
public virtual void OnTargetReached () {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a requested path has finished calculation.
|
||||
/// A path is first requested by <see cref="SearchPath"/>, it is then calculated, probably in the same or the next frame.
|
||||
/// Finally it is returned to the seeker which forwards it to this function.
|
||||
/// </summary>
|
||||
protected virtual void OnPathComplete (Path _p) {
|
||||
ABPath p = _p as ABPath;
|
||||
|
||||
if (p == null) throw new System.Exception("This function only handles ABPaths, do not use special path types");
|
||||
|
||||
canSearchAgain = true;
|
||||
|
||||
// Increase the reference count on the path.
|
||||
// This is used for path pooling
|
||||
p.Claim(this);
|
||||
|
||||
// Path couldn't be calculated of some reason.
|
||||
// More info in p.errorLog (debug string)
|
||||
if (p.error) {
|
||||
p.Release(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (interpolatePathSwitches) {
|
||||
ConfigurePathSwitchInterpolation();
|
||||
}
|
||||
|
||||
|
||||
// Replace the old path
|
||||
var oldPath = path;
|
||||
|
||||
path = p;
|
||||
reachedEndOfPath = false;
|
||||
|
||||
// The RandomPath and MultiTargetPath do not have a well defined destination that could have been
|
||||
// set before the paths were calculated. So we instead set the destination here so that some properties
|
||||
// like #reachedDestination and #remainingDistance work correctly.
|
||||
if (path is RandomPath rpath) {
|
||||
destination = rpath.originalEndPoint;
|
||||
} else if (path is MultiTargetPath mpath) {
|
||||
destination = mpath.originalEndPoint;
|
||||
}
|
||||
|
||||
// Just for the rest of the code to work, if there
|
||||
// is only one waypoint in the path add another one
|
||||
if (path.vectorPath != null && path.vectorPath.Count == 1) {
|
||||
path.vectorPath.Insert(0, GetFeetPosition());
|
||||
}
|
||||
|
||||
// Reset some variables
|
||||
ConfigureNewPath();
|
||||
|
||||
// Release the previous path
|
||||
// This is used for path pooling.
|
||||
// This is done after the interpolator has been configured in the ConfigureNewPath method
|
||||
// as this method would otherwise invalidate the interpolator
|
||||
// since the vectorPath list (which the interpolator uses) will be pooled.
|
||||
if (oldPath != null) oldPath.Release(this);
|
||||
|
||||
if (interpolator.remainingDistance < 0.0001f && !reachedEndOfPath) {
|
||||
reachedEndOfPath = true;
|
||||
OnTargetReached();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the current path of the agent.
|
||||
///
|
||||
/// Usually invoked using <see cref="SetPath"/>(null)
|
||||
///
|
||||
/// See: <see cref="SetPath"/>
|
||||
/// See: <see cref="isStopped"/>
|
||||
/// </summary>
|
||||
protected virtual void ClearPath () {
|
||||
// Abort any calculations in progress
|
||||
if (seeker != null) seeker.CancelCurrentPathRequest();
|
||||
canSearchAgain = true;
|
||||
reachedEndOfPath = false;
|
||||
|
||||
// Release current path so that it can be pooled
|
||||
if (path != null) path.Release(this);
|
||||
path = null;
|
||||
interpolatorPath.SetPath(null);
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::SetPath</summary>
|
||||
public void SetPath (Path path, bool updateDestinationFromPath = true) {
|
||||
if (updateDestinationFromPath && path is ABPath abPath && !(path is RandomPath)) {
|
||||
this.destination = abPath.originalEndPoint;
|
||||
}
|
||||
|
||||
if (path == null) {
|
||||
ClearPath();
|
||||
} else if (path.PipelineState == PathState.Created) {
|
||||
// Path has not started calculation yet
|
||||
canSearchAgain = false;
|
||||
seeker.CancelCurrentPathRequest();
|
||||
seeker.StartPath(path, onPathComplete);
|
||||
autoRepath.DidRecalculatePath(destination, Time.time);
|
||||
} else if (path.PipelineState >= PathState.Returning) {
|
||||
// Path has already been calculated
|
||||
|
||||
// We might be calculating another path at the same time, and we don't want that path to override this one. So cancel it.
|
||||
if (seeker.GetCurrentPath() != path) seeker.CancelCurrentPathRequest();
|
||||
|
||||
OnPathComplete(path);
|
||||
} else {
|
||||
// Path calculation has been started, but it is not yet complete. Cannot really handle this.
|
||||
throw new System.ArgumentException("You must call the SetPath method with a path that either has been completely calculated or one whose path calculation has not been started at all. It looks like the path calculation for the path you tried to use has been started, but is not yet finished.");
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void ConfigurePathSwitchInterpolation () {
|
||||
bool reachedEndOfPreviousPath = interpolator.valid && interpolator.remainingDistance < 0.0001f;
|
||||
|
||||
if (interpolator.valid && !reachedEndOfPreviousPath) {
|
||||
previousMovementOrigin = interpolator.position;
|
||||
previousMovementDirection = interpolator.tangent.normalized * interpolator.remainingDistance;
|
||||
pathSwitchInterpolationTime = 0;
|
||||
} else {
|
||||
previousMovementOrigin = Vector3.zero;
|
||||
previousMovementDirection = Vector3.zero;
|
||||
pathSwitchInterpolationTime = float.PositiveInfinity;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Vector3 GetFeetPosition () {
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>Finds the closest point on the current path and configures the <see cref="interpolator"/></summary>
|
||||
protected virtual void ConfigureNewPath () {
|
||||
var hadValidPath = interpolator.valid;
|
||||
var prevTangent = hadValidPath ? interpolator.tangent : Vector3.zero;
|
||||
|
||||
interpolatorPath.SetPath(path.vectorPath);
|
||||
interpolator = interpolatorPath.start;
|
||||
interpolator.MoveToClosestPoint(GetFeetPosition());
|
||||
|
||||
if (interpolatePathSwitches && switchPathInterpolationSpeed > 0.01f && hadValidPath) {
|
||||
var correctionFactor = Mathf.Max(-Vector3.Dot(prevTangent.normalized, interpolator.tangent.normalized), 0);
|
||||
interpolator.distance -= speed*correctionFactor*(1f/switchPathInterpolationSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Update () {
|
||||
if (shouldRecalculatePath) SearchPath();
|
||||
if (canMove) {
|
||||
Vector3 nextPosition;
|
||||
Quaternion nextRotation;
|
||||
MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);
|
||||
FinalizeMovement(nextPosition, nextRotation);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::MovementUpdate</summary>
|
||||
public void MovementUpdate (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) {
|
||||
if (updatePosition) simulatedPosition = tr.position;
|
||||
if (updateRotation) simulatedRotation = tr.rotation;
|
||||
|
||||
Vector3 direction;
|
||||
|
||||
nextPosition = CalculateNextPosition(out direction, isStopped ? 0f : deltaTime);
|
||||
|
||||
if (enableRotation) nextRotation = SimulateRotationTowards(direction, deltaTime);
|
||||
else nextRotation = simulatedRotation;
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::FinalizeMovement</summary>
|
||||
public void FinalizeMovement (Vector3 nextPosition, Quaternion nextRotation) {
|
||||
previousPosition2 = previousPosition1;
|
||||
previousPosition1 = simulatedPosition = nextPosition;
|
||||
simulatedRotation = nextRotation;
|
||||
if (updatePosition) tr.position = nextPosition;
|
||||
if (updateRotation) tr.rotation = nextRotation;
|
||||
}
|
||||
|
||||
Quaternion SimulateRotationTowards (Vector3 direction, float deltaTime) {
|
||||
// Rotate unless we are really close to the target
|
||||
if (direction != Vector3.zero) {
|
||||
Quaternion targetRotation = Quaternion.LookRotation(direction, orientation == OrientationMode.YAxisForward ? Vector3.back : Vector3.up);
|
||||
// This causes the character to only rotate around the Z axis
|
||||
if (orientation == OrientationMode.YAxisForward) targetRotation *= Quaternion.Euler(90, 0, 0);
|
||||
return Quaternion.Slerp(simulatedRotation, targetRotation, deltaTime * rotationSpeed);
|
||||
}
|
||||
return simulatedRotation;
|
||||
}
|
||||
|
||||
/// <summary>Calculate the AI's next position (one frame in the future).</summary>
|
||||
/// <param name="direction">The tangent of the segment the AI is currently traversing. Not normalized.</param>
|
||||
/// <param name="deltaTime">The time to simulate into the future.</param>
|
||||
protected virtual Vector3 CalculateNextPosition (out Vector3 direction, float deltaTime) {
|
||||
if (!interpolator.valid) {
|
||||
direction = Vector3.zero;
|
||||
return simulatedPosition;
|
||||
}
|
||||
|
||||
interpolator.distance += deltaTime * speed;
|
||||
|
||||
if (interpolator.remainingDistance < 0.0001f && !reachedEndOfPath) {
|
||||
reachedEndOfPath = true;
|
||||
OnTargetReached();
|
||||
}
|
||||
|
||||
direction = interpolator.tangent;
|
||||
pathSwitchInterpolationTime += deltaTime;
|
||||
var alpha = switchPathInterpolationSpeed * pathSwitchInterpolationTime;
|
||||
|
||||
if (interpolatePathSwitches && alpha < 1f) {
|
||||
// Find the approximate position we would be at if we
|
||||
// would have continued to follow the previous path
|
||||
Vector3 positionAlongPreviousPath = previousMovementOrigin + Vector3.ClampMagnitude(previousMovementDirection, speed * pathSwitchInterpolationTime);
|
||||
|
||||
// Interpolate between the position on the current path and the position
|
||||
// we would have had if we would have continued along the previous path.
|
||||
return Vector3.Lerp(positionAlongPreviousPath, interpolator.position, alpha);
|
||||
} else {
|
||||
return interpolator.position;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) {
|
||||
if (migrations.TryMigrateFromLegacyFormat(out var legacyVersion)) {
|
||||
if (legacyVersion <= 3) {
|
||||
repathRate = repathRateCompatibility;
|
||||
canSearch = canSearchCompability;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void DrawGizmos () {
|
||||
tr = transform;
|
||||
autoRepath.DrawGizmos(Pathfinding.Drawing.Draw.editor, this.position, 0.0f, new NativeMovementPlane(orientation == OrientationMode.YAxisForward ? Quaternion.Euler(-90, 0, 0) : Quaternion.identity));
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/com.arongranberg.astar/Core/AI/AILerp.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Core/AI/AILerp.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 847a14d4dc9cc43679ab34fc78e0182f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
543
Packages/com.arongranberg.astar/Core/AI/AIPath.cs
Normal file
543
Packages/com.arongranberg.astar/Core/AI/AIPath.cs
Normal file
@@ -0,0 +1,543 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.Util;
|
||||
using Pathfinding.Drawing;
|
||||
|
||||
/// <summary>
|
||||
/// AI for following paths.
|
||||
///
|
||||
/// This AI is the default movement script which comes with the A* Pathfinding Project.
|
||||
/// It is in no way required by the rest of the system, so feel free to write your own. But I hope this script will make it easier
|
||||
/// to set up movement for the characters in your game.
|
||||
/// This script works well for many types of units, but if you need the highest performance (for example if you are moving hundreds of characters) you
|
||||
/// may want to customize this script or write a custom movement script to be able to optimize it specifically for your game.
|
||||
///
|
||||
/// This script will try to move to a given <see cref="destination"/>. At <see cref="repathRate regular"/>, the path to the destination will be recalculated.
|
||||
/// If you want to make the AI to follow a particular object you can attach the <see cref="Pathfinding.AIDestinationSetter"/> component.
|
||||
/// Take a look at the getstarted (view in online documentation for working links) tutorial for more instructions on how to configure this script.
|
||||
///
|
||||
/// Here is a video of this script being used move an agent around (it also uses the <see cref="MineBotAnimation"/> component to drive the animations):
|
||||
/// [Open online documentation to see videos]
|
||||
///
|
||||
/// \section variables Quick overview of the variables
|
||||
/// In the inspector in Unity, you will see a bunch of variables. You can view detailed information further down, but here's a quick overview.
|
||||
///
|
||||
/// The <see cref="repathRate"/> determines how often it will search for new paths, if you have fast moving targets, you might want to set it to a lower value.
|
||||
/// The <see cref="destination"/> field is where the AI will try to move, it can be a point on the ground where the player has clicked in an RTS for example.
|
||||
/// Or it can be the player object in a zombie game.
|
||||
/// The <see cref="maxSpeed"/> is self-explanatory, as is <see cref="rotationSpeed"/>. however <see cref="slowdownDistance"/> might require some explanation:
|
||||
/// It is the approximate distance from the target where the AI will start to slow down. Setting it to a large value will make the AI slow down very gradually.
|
||||
/// <see cref="pickNextWaypointDist"/> determines the distance to the point the AI will move to (see image below).
|
||||
///
|
||||
/// Below is an image illustrating several variables that are exposed by this class (<see cref="pickNextWaypointDist"/>, <see cref="steeringTarget"/>, <see cref="desiredVelocity)"/>
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// This script has many movement fallbacks.
|
||||
/// If it finds an RVOController attached to the same GameObject as this component, it will use that. If it finds a character controller it will also use that.
|
||||
/// If it finds a rigidbody it will use that. Lastly it will fall back to simply modifying Transform.position which is guaranteed to always work and is also the most performant option.
|
||||
///
|
||||
/// \section how-aipath-works How it works
|
||||
/// In this section I'm going to go over how this script is structured and how information flows.
|
||||
/// This is useful if you want to make changes to this script or if you just want to understand how it works a bit more deeply.
|
||||
/// However you do not need to read this section if you are just going to use the script as-is.
|
||||
///
|
||||
/// This script inherits from the <see cref="AIBase"/> class. The movement happens either in Unity's standard Update or FixedUpdate method.
|
||||
/// They are both defined in the AIBase class. Which one is actually used depends on if a rigidbody is used for movement or not.
|
||||
/// Rigidbody movement has to be done inside the FixedUpdate method while otherwise it is better to do it in Update.
|
||||
///
|
||||
/// From there a call is made to the <see cref="MovementUpdate"/> method (which in turn calls <see cref="MovementUpdateInternal)"/>.
|
||||
/// This method contains the main bulk of the code and calculates how the AI *wants* to move. However it doesn't do any movement itself.
|
||||
/// Instead it returns the position and rotation it wants the AI to move to have at the end of the frame.
|
||||
/// The Update (or FixedUpdate) method then passes these values to the <see cref="FinalizeMovement"/> method which is responsible for actually moving the character.
|
||||
/// That method also handles things like making sure the AI doesn't fall through the ground using raycasting.
|
||||
///
|
||||
/// The AI recalculates its path regularly. This happens in the Update method which checks <see cref="shouldRecalculatePath"/>, and if that returns true it will call <see cref="SearchPath"/>.
|
||||
/// The <see cref="SearchPath"/> method will prepare a path request and send it to the <see cref="Seeker"/> component, which should be attached to the same GameObject as this script.
|
||||
/// </summary>
|
||||
[AddComponentMenu("Pathfinding/AI/AIPath (2D,3D)")]
|
||||
[UniqueComponent(tag = "ai")]
|
||||
[DisallowMultipleComponent]
|
||||
public partial class AIPath : AIBase, IAstarAI {
|
||||
/// <summary>
|
||||
/// How quickly the agent accelerates.
|
||||
/// Positive values represent an acceleration in world units per second squared.
|
||||
/// Negative values are interpreted as an inverse time of how long it should take for the agent to reach its max speed.
|
||||
/// For example if it should take roughly 0.4 seconds for the agent to reach its max speed then this field should be set to -1/0.4 = -2.5.
|
||||
/// For a negative value the final acceleration will be: -acceleration*maxSpeed.
|
||||
/// This behaviour exists mostly for compatibility reasons.
|
||||
///
|
||||
/// In the Unity inspector there are two modes: Default and Custom. In the Default mode this field is set to -2.5 which means that it takes about 0.4 seconds for the agent to reach its top speed.
|
||||
/// In the Custom mode you can set the acceleration to any positive value.
|
||||
/// </summary>
|
||||
public float maxAcceleration = -2.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Rotation speed in degrees per second.
|
||||
/// Rotation is calculated using Quaternion.RotateTowards. This variable represents the rotation speed in degrees per second.
|
||||
/// The higher it is, the faster the character will be able to rotate.
|
||||
/// </summary>
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("turningSpeed")]
|
||||
public float rotationSpeed = 360;
|
||||
|
||||
/// <summary>Distance from the end of the path where the AI will start to slow down</summary>
|
||||
public float slowdownDistance = 0.6F;
|
||||
|
||||
/// <summary>
|
||||
/// How far the AI looks ahead along the path to determine the point it moves to.
|
||||
/// In world units.
|
||||
/// If you enable the <see cref="alwaysDrawGizmos"/> toggle this value will be visualized in the scene view as a blue circle around the agent.
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// Here are a few example videos showing some typical outcomes with good values as well as how it looks when this value is too low and too high.
|
||||
/// <table>
|
||||
/// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-danger">Too low</span><br/></verbatim>\endxmlonly A too low value and a too low acceleration will result in the agent overshooting a lot and not managing to follow the path well.</td></tr>
|
||||
/// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-warning">Ok</span><br/></verbatim>\endxmlonly A low value but a high acceleration works decently to make the AI follow the path more closely. Note that the <see cref="Pathfinding.AILerp"/> component is better suited if you want the agent to follow the path without any deviations.</td></tr>
|
||||
/// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-success">Ok</span><br/></verbatim>\endxmlonly A reasonable value in this example.</td></tr>
|
||||
/// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-success">Ok</span><br/></verbatim>\endxmlonly A reasonable value in this example, but the path is followed slightly more loosely than in the previous video.</td></tr>
|
||||
/// <tr><td>[Open online documentation to see videos]</td><td>\xmlonly <verbatim><span class="label label-danger">Too high</span><br/></verbatim>\endxmlonly A too high value will make the agent follow the path too loosely and may cause it to try to move through obstacles.</td></tr>
|
||||
/// </table>
|
||||
/// </summary>
|
||||
public float pickNextWaypointDist = 2;
|
||||
|
||||
/// <summary>Draws detailed gizmos constantly in the scene view instead of only when the agent is selected and settings are being modified</summary>
|
||||
public bool alwaysDrawGizmos;
|
||||
|
||||
/// <summary>
|
||||
/// Slow down when not facing the target direction.
|
||||
/// Incurs at a small performance overhead.
|
||||
///
|
||||
/// This setting only has an effect if <see cref="enableRotation"/> is enabled.
|
||||
/// </summary>
|
||||
public bool slowWhenNotFacingTarget = true;
|
||||
|
||||
/// <summary>
|
||||
/// Prevent the velocity from being too far away from the forward direction of the character.
|
||||
/// If the character is ordered to move in the opposite direction from where it is facing
|
||||
/// then enabling this will cause it to make a small loop instead of turning on the spot.
|
||||
///
|
||||
/// This setting only has an effect if <see cref="slowWhenNotFacingTarget"/> is enabled.
|
||||
/// </summary>
|
||||
public bool preventMovingBackwards = false;
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that the character is always on the traversable surface of the navmesh.
|
||||
/// When this option is enabled a <see cref="AstarPath.GetNearest"/> query will be done every frame to find the closest node that the agent can walk on
|
||||
/// and if the agent is not inside that node, then the agent will be moved to it.
|
||||
///
|
||||
/// This is especially useful together with local avoidance in order to avoid agents pushing each other into walls.
|
||||
/// See: local-avoidance (view in online documentation for working links) for more info about this.
|
||||
///
|
||||
/// This option also integrates with local avoidance so that if the agent is say forced into a wall by other agents the local avoidance
|
||||
/// system will be informed about that wall and can take that into account.
|
||||
///
|
||||
/// Enabling this has some performance impact depending on the graph type (pretty fast for grid graphs, slightly slower for navmesh/recast graphs).
|
||||
/// If you are using a navmesh/recast graph you may want to switch to the <see cref="Pathfinding.RichAI"/> movement script which is specifically written for navmesh/recast graphs and
|
||||
/// does this kind of clamping out of the box. In many cases it can also follow the path more smoothly around sharp bends in the path.
|
||||
///
|
||||
/// It is not recommended that you use this option together with the funnel modifier on grid graphs because the funnel modifier will make the path
|
||||
/// go very close to the border of the graph and this script has a tendency to try to cut corners a bit. This may cause it to try to go slightly outside the
|
||||
/// traversable surface near corners and that will look bad if this option is enabled.
|
||||
///
|
||||
/// Warning: This option makes no sense to use on point graphs because point graphs do not have a surface.
|
||||
/// Enabling this option when using a point graph will lead to the agent being snapped to the closest node every frame which is likely not what you want.
|
||||
///
|
||||
/// Below you can see an image where several agents using local avoidance were ordered to go to the same point in a corner.
|
||||
/// When not constraining the agents to the graph they are easily pushed inside obstacles.
|
||||
/// [Open online documentation to see images]
|
||||
/// </summary>
|
||||
public bool constrainInsideGraph = false;
|
||||
|
||||
/// <summary>Current path which is followed</summary>
|
||||
protected Path path;
|
||||
|
||||
/// <summary>Represents the current steering target for the agent</summary>
|
||||
protected PathInterpolator.Cursor interpolator;
|
||||
/// <summary>Helper which calculates points along the current path</summary>
|
||||
protected PathInterpolator interpolatorPath = new PathInterpolator();
|
||||
|
||||
#region IAstarAI implementation
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::Teleport</summary>
|
||||
public override void Teleport (Vector3 newPosition, bool clearPath = true) {
|
||||
reachedEndOfPath = false;
|
||||
base.Teleport(newPosition, clearPath);
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::remainingDistance</summary>
|
||||
public float remainingDistance => interpolator.valid ? interpolator.remainingDistance + movementPlane.ToPlane(interpolator.position - position).magnitude : float.PositiveInfinity;
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary>
|
||||
public override bool reachedDestination {
|
||||
get {
|
||||
if (!reachedEndOfPath) return false;
|
||||
if (!interpolator.valid || remainingDistance + movementPlane.ToPlane(destination - interpolator.endPoint).magnitude > endReachedDistance) return false;
|
||||
|
||||
// Don't do height checks in 2D mode
|
||||
if (orientation != OrientationMode.YAxisForward) {
|
||||
// Check if the destination is above the head of the character or far below the feet of it
|
||||
movementPlane.ToPlane(destination - position, out float yDifference);
|
||||
var h = tr.localScale.y * height;
|
||||
if (yDifference > h || yDifference < -h*0.5) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::reachedEndOfPath</summary>
|
||||
public bool reachedEndOfPath { get; protected set; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary>
|
||||
public bool hasPath => interpolator.valid;
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary>
|
||||
public bool pathPending => waitingForPathCalculation;
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary>
|
||||
public Vector3 steeringTarget => interpolator.valid ? interpolator.position : position;
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::endOfPath</summary>
|
||||
public override Vector3 endOfPath {
|
||||
get {
|
||||
if (interpolator.valid) return interpolator.endPoint;
|
||||
if (float.IsFinite(destination.x)) return destination;
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::radius</summary>
|
||||
float IAstarAI.radius { get => radius; set => radius = value; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::height</summary>
|
||||
float IAstarAI.height { get => height; set => height = value; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary>
|
||||
float IAstarAI.maxSpeed { get => maxSpeed; set => maxSpeed = value; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::canSearch</summary>
|
||||
bool IAstarAI.canSearch { get => canSearch; set => canSearch = value; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary>
|
||||
bool IAstarAI.canMove { get => canMove; set => canMove = value; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::movementPlane</summary>
|
||||
NativeMovementPlane IAstarAI.movementPlane => new NativeMovementPlane(movementPlane);
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,bool)}</summary>
|
||||
public void GetRemainingPath (List<Vector3> buffer, out bool stale) {
|
||||
buffer.Clear();
|
||||
buffer.Add(position);
|
||||
if (!interpolator.valid) {
|
||||
stale = true;
|
||||
return;
|
||||
}
|
||||
|
||||
stale = false;
|
||||
interpolator.GetRemainingPath(buffer);
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,List<PathPartWithLinkInfo>,bool)}</summary>
|
||||
public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale) {
|
||||
GetRemainingPath(buffer, out stale);
|
||||
// This movement script doesn't keep track of path parts, so we just add the whole path as a single part
|
||||
if (partsBuffer != null) {
|
||||
partsBuffer.Clear();
|
||||
partsBuffer.Add(new PathPartWithLinkInfo { startIndex = 0, endIndex = buffer.Count - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDisable () {
|
||||
// This will, among other things call ClearPath
|
||||
base.OnDisable();
|
||||
rotationFilterState = Vector2.zero;
|
||||
rotationFilterState2 = Vector2.zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The end of the path has been reached.
|
||||
/// If you want custom logic for when the AI has reached it's destination add it here. You can
|
||||
/// also create a new script which inherits from this one and override the function in that script.
|
||||
///
|
||||
/// This method will be called again if a new path is calculated as the destination may have changed.
|
||||
/// So when the agent is close to the destination this method will typically be called every <see cref="repathRate"/> seconds.
|
||||
///
|
||||
/// Deprecated: Avoid overriding this method. Instead poll the <see cref="reachedDestination"/> or <see cref="reachedEndOfPath"/> properties.
|
||||
/// </summary>
|
||||
public virtual void OnTargetReached () {
|
||||
}
|
||||
|
||||
protected virtual void UpdateMovementPlane () {
|
||||
if (path.path == null || path.path.Count == 0) return;
|
||||
var graph = AstarData.GetGraph(path.path[0]) as ITransformedGraph;
|
||||
IMovementPlane graphTransform = graph != null ? graph.transform : (orientation == OrientationMode.YAxisForward ? new GraphTransform(Matrix4x4.TRS(Vector3.zero, Quaternion.Euler(-90, 270, 90), Vector3.one)) : GraphTransform.identityTransform);
|
||||
|
||||
movementPlane = graphTransform.ToSimpleMovementPlane();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a requested path has been calculated.
|
||||
/// A path is first requested by <see cref="SearchPath"/>, it is then calculated, probably in the same or the next frame.
|
||||
/// Finally it is returned to the seeker which forwards it to this function.
|
||||
/// </summary>
|
||||
protected override void OnPathComplete (Path newPath) {
|
||||
ABPath p = newPath as ABPath;
|
||||
|
||||
if (p == null) throw new System.Exception("This function only handles ABPaths, do not use special path types");
|
||||
|
||||
waitingForPathCalculation = false;
|
||||
|
||||
// Increase the reference count on the new path.
|
||||
// This is used for object pooling to reduce allocations.
|
||||
p.Claim(this);
|
||||
|
||||
// Path couldn't be calculated of some reason.
|
||||
// More info in p.errorLog (debug string)
|
||||
if (p.error) {
|
||||
p.Release(this);
|
||||
SetPath(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Release the previous path.
|
||||
if (path != null) path.Release(this);
|
||||
|
||||
// Replace the old path
|
||||
path = p;
|
||||
|
||||
// The RandomPath and MultiTargetPath do not have a well defined destination that could have been
|
||||
// set before the paths were calculated. So we instead set the destination here so that some properties
|
||||
// like #reachedDestination and #remainingDistance work correctly.
|
||||
if (!p.endPointKnownBeforeCalculation) {
|
||||
destination = p.originalEndPoint;
|
||||
}
|
||||
|
||||
// Make sure the path contains at least 2 points
|
||||
if (path.vectorPath.Count == 1) path.vectorPath.Add(path.vectorPath[0]);
|
||||
interpolatorPath.SetPath(path.vectorPath);
|
||||
interpolator = interpolatorPath.start;
|
||||
|
||||
UpdateMovementPlane();
|
||||
|
||||
// Reset some variables
|
||||
reachedEndOfPath = false;
|
||||
|
||||
// Simulate movement from the point where the path was requested
|
||||
// to where we are right now. This reduces the risk that the agent
|
||||
// gets confused because the first point in the path is far away
|
||||
// from the current position (possibly behind it which could cause
|
||||
// the agent to turn around, and that looks pretty bad).
|
||||
interpolator.MoveToLocallyClosestPoint((GetFeetPosition() + p.originalStartPoint) * 0.5f);
|
||||
interpolator.MoveToLocallyClosestPoint(GetFeetPosition());
|
||||
|
||||
// Update which point we are moving towards.
|
||||
// Note that we need to do this here because otherwise the remainingDistance field might be incorrect for 1 frame.
|
||||
// (due to interpolator.remainingDistance being incorrect).
|
||||
interpolator.MoveToCircleIntersection2D(position, pickNextWaypointDist, movementPlane);
|
||||
|
||||
var distanceToEnd = remainingDistance;
|
||||
|
||||
if (distanceToEnd <= endReachedDistance) {
|
||||
reachedEndOfPath = true;
|
||||
OnTargetReached();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ClearPath () {
|
||||
CancelCurrentPathRequest();
|
||||
// Release current path so that it can be pooled
|
||||
if (path != null) path.Release(this);
|
||||
path = null;
|
||||
interpolatorPath.SetPath(null);
|
||||
reachedEndOfPath = false;
|
||||
}
|
||||
|
||||
/// <summary>Called during either Update or FixedUpdate depending on if rigidbodies are used for movement or not</summary>
|
||||
protected override void MovementUpdateInternal (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) {
|
||||
float currentAcceleration = maxAcceleration;
|
||||
|
||||
// If negative, calculate the acceleration from the max speed
|
||||
if (currentAcceleration < 0) currentAcceleration *= -maxSpeed;
|
||||
|
||||
if (updatePosition) {
|
||||
// Get our current position. We read from transform.position as few times as possible as it is relatively slow
|
||||
// (at least compared to a local variable)
|
||||
simulatedPosition = tr.position;
|
||||
}
|
||||
if (updateRotation) simulatedRotation = tr.rotation;
|
||||
|
||||
var currentPosition = simulatedPosition;
|
||||
|
||||
// Normalized direction of where the agent is looking
|
||||
var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward));
|
||||
|
||||
// Check if we have a valid path to follow and some other script has not stopped the character
|
||||
bool stopped = isStopped || (reachedDestination && whenCloseToDestination == CloseToDestinationMode.Stop);
|
||||
|
||||
if (rvoController != null) rvoDensityBehavior.Update(rvoController.enabled, reachedDestination, ref stopped, ref rvoController.priorityMultiplier, ref rvoController.flowFollowingStrength, currentPosition);
|
||||
|
||||
float speedLimitFactor = 0;
|
||||
float distanceToEnd;
|
||||
// Check if we have a path to follow
|
||||
if (interpolator.valid) {
|
||||
// Update which point we are moving towards
|
||||
interpolator.MoveToCircleIntersection2D(currentPosition, pickNextWaypointDist, movementPlane);
|
||||
var dir = movementPlane.ToPlane(steeringTarget - currentPosition);
|
||||
|
||||
// Calculate the distance to the end of the path
|
||||
distanceToEnd = dir.magnitude + Mathf.Max(0, interpolator.remainingDistance);
|
||||
|
||||
// Check if we have reached the target
|
||||
var prevTargetReached = reachedEndOfPath;
|
||||
reachedEndOfPath = distanceToEnd <= endReachedDistance;
|
||||
if (!prevTargetReached && reachedEndOfPath) OnTargetReached();
|
||||
|
||||
if (!stopped) {
|
||||
// How fast to move depending on the distance to the destination.
|
||||
// Move slower as the character gets closer to the destination.
|
||||
// This is always a value between 0 and 1.
|
||||
speedLimitFactor = distanceToEnd < slowdownDistance? Mathf.Sqrt(distanceToEnd / slowdownDistance) : 1;
|
||||
velocity2D += MovementUtilities.CalculateAccelerationToReachPoint(dir, dir.normalized*maxSpeed, velocity2D, currentAcceleration, rotationSpeed, maxSpeed, forwards) * deltaTime;
|
||||
}
|
||||
} else {
|
||||
reachedEndOfPath = false;
|
||||
distanceToEnd = float.PositiveInfinity;
|
||||
}
|
||||
|
||||
if (!interpolator.valid || stopped) {
|
||||
// Slow down as quickly as possible
|
||||
velocity2D -= Vector2.ClampMagnitude(velocity2D, currentAcceleration * deltaTime);
|
||||
// We are already slowing down as quickly as possible. Avoid limiting the speed in other ways.
|
||||
speedLimitFactor = 1;
|
||||
}
|
||||
|
||||
velocity2D = MovementUtilities.ClampVelocity(velocity2D, maxSpeed, speedLimitFactor, slowWhenNotFacingTarget && enableRotation, preventMovingBackwards, forwards);
|
||||
|
||||
ApplyGravity(deltaTime);
|
||||
bool avoidingOtherAgents = false;
|
||||
|
||||
if (rvoController != null && rvoController.enabled) {
|
||||
// Send a message to the RVOController that we want to move
|
||||
// with this velocity. In the next simulation step, this
|
||||
// velocity will be processed and it will be fed back to the
|
||||
// rvo controller and finally it will be used by this script
|
||||
// when calling the CalculateMovementDelta method below
|
||||
|
||||
// Make sure that we don't move further than to the end point
|
||||
// of the path. If the RVO simulation FPS is low and we did
|
||||
// not do this, the agent might overshoot the target a lot.
|
||||
var rvoTarget = currentPosition + movementPlane.ToWorld(Vector2.ClampMagnitude(velocity2D, distanceToEnd), 0f);
|
||||
rvoController.SetTarget(rvoTarget, velocity2D.magnitude, maxSpeed, endOfPath);
|
||||
avoidingOtherAgents = rvoController.AvoidingAnyAgents;
|
||||
}
|
||||
|
||||
// Set how much the agent wants to move during this frame
|
||||
var delta2D = lastDeltaPosition = CalculateDeltaToMoveThisFrame(currentPosition, distanceToEnd, deltaTime);
|
||||
nextPosition = currentPosition + movementPlane.ToWorld(delta2D, verticalVelocity * deltaTime);
|
||||
CalculateNextRotation(speedLimitFactor, avoidingOtherAgents, out nextRotation);
|
||||
}
|
||||
|
||||
Vector2 rotationFilterState, rotationFilterState2;
|
||||
|
||||
protected virtual void CalculateNextRotation (float slowdown, bool avoidingOtherAgents, out Quaternion nextRotation) {
|
||||
if (lastDeltaTime > 0.00001f && enableRotation) {
|
||||
// Rotate towards the direction we are moving in
|
||||
// Filter out noise in the movement direction
|
||||
// This is especially important when the agent is almost standing still and when using local avoidance
|
||||
float noiseThreshold = radius * tr.localScale.x * 0.2f;
|
||||
float rotationSpeedFactor = MovementUtilities.FilterRotationDirection(ref rotationFilterState, ref rotationFilterState2, lastDeltaPosition, noiseThreshold, lastDeltaTime, avoidingOtherAgents);
|
||||
nextRotation = SimulateRotationTowards(rotationFilterState, rotationSpeed * lastDeltaTime * rotationSpeedFactor, rotationSpeed * lastDeltaTime);
|
||||
} else {
|
||||
// TODO: simulatedRotation
|
||||
nextRotation = rotation;
|
||||
}
|
||||
}
|
||||
|
||||
static NNConstraint cachedNNConstraint = NNConstraint.Walkable;
|
||||
protected override Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) {
|
||||
if (constrainInsideGraph) {
|
||||
cachedNNConstraint.tags = seeker.traversableTags;
|
||||
cachedNNConstraint.graphMask = seeker.graphMask;
|
||||
cachedNNConstraint.distanceMetric = DistanceMetric.ClosestAsSeenFromAboveSoft();
|
||||
// Note: We don't want to set nn.constrainDistance = false (i.e. allow finding nodes arbitrarily far away), because that can lead to harsh
|
||||
// performance cliffs if agents for example fall through the ground or get thrown off the map, or something like that (it's bound to happen in some games).
|
||||
var nearestOnNavmesh = AstarPath.active.GetNearest(position, cachedNNConstraint);
|
||||
|
||||
if (nearestOnNavmesh.node == null) {
|
||||
// Found no valid node to constrain to. This can happen if there are no valid nodes close enough to the agent.
|
||||
positionChanged = false;
|
||||
return position;
|
||||
}
|
||||
|
||||
var clampedPosition = nearestOnNavmesh.position;
|
||||
|
||||
if (rvoController != null && rvoController.enabled) {
|
||||
// Inform the RVO system about the edges of the navmesh which will allow
|
||||
// it to better keep inside the navmesh in the first place.
|
||||
rvoController.SetObstacleQuery(nearestOnNavmesh.node);
|
||||
}
|
||||
|
||||
// We cannot simply check for equality because some precision may be lost
|
||||
// if any coordinate transformations are used.
|
||||
var difference = movementPlane.ToPlane(clampedPosition - position);
|
||||
float sqrDifference = difference.sqrMagnitude;
|
||||
if (sqrDifference > 0.001f*0.001f) {
|
||||
// The agent was outside the navmesh. Remove that component of the velocity
|
||||
// so that the velocity only goes along the direction of the wall, not into it
|
||||
velocity2D -= difference * Vector2.Dot(difference, velocity2D) / sqrDifference;
|
||||
|
||||
positionChanged = true;
|
||||
// Return the new position, but ignore any changes in the y coordinate from the ClampToNavmesh method as the y coordinates in the navmesh are rarely very accurate
|
||||
return position + movementPlane.ToWorld(difference);
|
||||
}
|
||||
}
|
||||
|
||||
positionChanged = false;
|
||||
return position;
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
[System.NonSerialized]
|
||||
int gizmoHash = 0;
|
||||
|
||||
[System.NonSerialized]
|
||||
float lastChangedTime = float.NegativeInfinity;
|
||||
|
||||
protected static readonly Color GizmoColor = new Color(46.0f/255, 104.0f/255, 201.0f/255);
|
||||
|
||||
public override void DrawGizmos () {
|
||||
base.DrawGizmos();
|
||||
|
||||
// If alwaysDrawGizmos is false, gizmos are only visible for a short while after the user changes any settings on this component
|
||||
var newGizmoHash = pickNextWaypointDist.GetHashCode() ^ slowdownDistance.GetHashCode() ^ endReachedDistance.GetHashCode();
|
||||
|
||||
if (newGizmoHash != gizmoHash && gizmoHash != 0) lastChangedTime = Time.realtimeSinceStartup;
|
||||
gizmoHash = newGizmoHash;
|
||||
float alpha = alwaysDrawGizmos ? 1 : Mathf.SmoothStep(1, 0, (Time.realtimeSinceStartup - lastChangedTime - 5f)/0.5f) * (GizmoContext.selectionSize == 1 ? 1 : 0);
|
||||
|
||||
if (alpha > 0) {
|
||||
// Make sure the scene view is repainted while the gizmos are visible
|
||||
if (!alwaysDrawGizmos) UnityEditor.SceneView.RepaintAll();
|
||||
Draw.Line(position, steeringTarget, GizmoColor * new Color(1, 1, 1, alpha));
|
||||
using (Draw.WithMatrix(Matrix4x4.TRS(position, transform.rotation * (orientation == OrientationMode.YAxisForward ? Quaternion.Euler(-90, 0, 0) : Quaternion.identity), Vector3.one))) {
|
||||
Draw.xz.Circle(Vector3.zero, pickNextWaypointDist, GizmoColor * new Color(1, 1, 1, alpha));
|
||||
Draw.xz.Circle(Vector3.zero, slowdownDistance, Color.Lerp(GizmoColor, Color.red, 0.5f) * new Color(1, 1, 1, alpha));
|
||||
Draw.xz.Circle(Vector3.zero, endReachedDistance, Color.Lerp(GizmoColor, Color.red, 0.8f) * new Color(1, 1, 1, alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) {
|
||||
if (migrations.IsLegacyFormat) {
|
||||
// Approximately convert from a damping value to a degrees per second value.
|
||||
if (migrations.LegacyVersion < 1) rotationSpeed *= 90;
|
||||
// The base call will migrate the legacy format further
|
||||
}
|
||||
base.OnUpgradeSerializedData(ref migrations, unityThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/com.arongranberg.astar/Core/AI/AIPath.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Core/AI/AIPath.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6eb1402c17e84a9282a7f0f62eb584f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
2061
Packages/com.arongranberg.astar/Core/AI/FollowerEntity.cs
Normal file
2061
Packages/com.arongranberg.astar/Core/AI/FollowerEntity.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cfe9431ea8ad072f2aecd3041b1524dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
501
Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs
Normal file
501
Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs
Normal file
@@ -0,0 +1,501 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using Pathfinding.Util;
|
||||
|
||||
namespace Pathfinding {
|
||||
/// <summary>
|
||||
/// Common interface for all movement scripts in the A* Pathfinding Project.
|
||||
/// See: <see cref="Pathfinding.AIPath"/>
|
||||
/// See: <see cref="Pathfinding.RichAI"/>
|
||||
/// See: <see cref="Pathfinding.AILerp"/>
|
||||
/// </summary>
|
||||
public interface IAstarAI {
|
||||
/// <summary>
|
||||
/// Radius of the agent in world units.
|
||||
/// This is visualized in the scene view as a yellow cylinder around the character.
|
||||
///
|
||||
/// Note that this does not affect pathfinding in any way.
|
||||
/// The graph used completely determines where the agent can move.
|
||||
///
|
||||
/// Note: The <see cref="Pathfinding.AILerp"/> script doesn't really have any use of knowing the radius or the height of the character, so this property will always return 0 in that script.
|
||||
/// </summary>
|
||||
float radius { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Height of the agent in world units.
|
||||
/// This is visualized in the scene view as a yellow cylinder around the character.
|
||||
///
|
||||
/// This value is currently only used if an RVOController is attached to the same GameObject, otherwise it is only used for drawing nice gizmos in the scene view.
|
||||
/// However since the height value is used for some things, the radius field is always visible for consistency and easier visualization of the character.
|
||||
/// That said, it may be used for something in a future release.
|
||||
///
|
||||
/// Note: The <see cref="Pathfinding.AILerp"/> script doesn't really have any use of knowing the radius or the height of the character, so this property will always return 0 in that script.
|
||||
/// </summary>
|
||||
float height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Position of the agent.
|
||||
/// In world space.
|
||||
/// See: <see cref="rotation"/>
|
||||
///
|
||||
/// If you want to move the agent you may use <see cref="Teleport"/> or <see cref="Move"/>.
|
||||
/// </summary>
|
||||
Vector3 position { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Rotation of the agent.
|
||||
/// In world space.
|
||||
/// See: <see cref="position"/>
|
||||
/// </summary>
|
||||
Quaternion rotation { get; set; }
|
||||
|
||||
/// <summary>Max speed in world units per second</summary>
|
||||
float maxSpeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Actual velocity that the agent is moving with.
|
||||
/// In world units per second.
|
||||
///
|
||||
/// See: <see cref="desiredVelocity"/>
|
||||
/// </summary>
|
||||
Vector3 velocity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Velocity that this agent wants to move with.
|
||||
/// Includes gravity and local avoidance if applicable.
|
||||
/// In world units per second.
|
||||
///
|
||||
/// See: <see cref="velocity"/>
|
||||
///
|
||||
/// Note: The <see cref="Pathfinding.AILerp"/> movement script doesn't use local avoidance or gravity so this property will always be identical to <see cref="velocity"/> on that component.
|
||||
/// </summary>
|
||||
Vector3 desiredVelocity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Velocity that this agent wants to move with before taking local avoidance into account.
|
||||
///
|
||||
/// Includes gravity.
|
||||
/// In world units per second.
|
||||
///
|
||||
/// Setting this property will set the current velocity that the agent is trying to move with, including gravity.
|
||||
/// This can be useful if you want to make the agent come to a complete stop in a single frame or if you want to modify the velocity in some way.
|
||||
///
|
||||
/// <code>
|
||||
/// // Set the velocity to zero, but keep the current gravity
|
||||
/// var newVelocity = new Vector3(0, ai.desiredVelocityWithoutLocalAvoidance.y, 0);
|
||||
///
|
||||
/// ai.desiredVelocityWithoutLocalAvoidance = newVelocity;
|
||||
/// </code>
|
||||
///
|
||||
/// Note: The <see cref="Pathfinding.AILerp"/> movement script doesn't use local avoidance so this property will always be identical to <see cref="velocity"/> on that component.
|
||||
///
|
||||
/// Warning: Trying to set this property on an AILerp component will throw an exception since its velocity cannot meaningfully be changed abitrarily.
|
||||
///
|
||||
/// If you are not using local avoidance then this property will in almost all cases be identical to <see cref="desiredVelocity"/> plus some noise due to floating point math.
|
||||
///
|
||||
/// See: <see cref="velocity"/>
|
||||
/// See: <see cref="desiredVelocity"/>
|
||||
/// See: <see cref="Move"/>
|
||||
/// See: <see cref="MovementUpdate"/>
|
||||
/// </summary>
|
||||
Vector3 desiredVelocityWithoutLocalAvoidance { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Approximate remaining distance along the current path to the end of the path.
|
||||
/// The RichAI movement script approximates this distance since it is quite expensive to calculate the real distance.
|
||||
/// However it will be accurate when the agent is within 1 corner of the destination.
|
||||
/// You can use <see cref="GetRemainingPath"/> to calculate the actual remaining path more precisely.
|
||||
///
|
||||
/// The AIPath and AILerp scripts use a more accurate distance calculation at all times.
|
||||
///
|
||||
/// If the agent does not currently have a path, then positive infinity will be returned.
|
||||
///
|
||||
/// Note: This is the distance to the end of the path, which may or may not be at the <see cref="destination"/>. If the character cannot reach the destination it will try to move as close as possible to it.
|
||||
///
|
||||
/// Warning: Since path requests are asynchronous, there is a small delay between a path request being sent and this value being updated with the new calculated path.
|
||||
///
|
||||
/// See: <see cref="reachedDestination"/>
|
||||
/// See: <see cref="reachedEndOfPath"/>
|
||||
/// See: <see cref="pathPending"/>
|
||||
/// </summary>
|
||||
float remainingDistance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the ai has reached the <see cref="destination"/>.
|
||||
/// This is a best effort calculation to see if the <see cref="destination"/> has been reached.
|
||||
/// For the AIPath/RichAI scripts, this is when the character is within <see cref="AIPath.endReachedDistance"/> world units from the <see cref="destination"/>.
|
||||
/// For the AILerp script it is when the character is at the destination (±a very small margin).
|
||||
///
|
||||
/// This value will be updated immediately when the <see cref="destination"/> is changed (in contrast to <see cref="reachedEndOfPath)"/>, however since path requests are asynchronous
|
||||
/// it will use an approximation until it sees the real path result. What this property does is to check the distance to the end of the current path, and add to that the distance
|
||||
/// from the end of the path to the <see cref="destination"/> (i.e. is assumes it is possible to move in a straight line between the end of the current path to the destination) and then checks if that total
|
||||
/// distance is less than <see cref="AIPath.endReachedDistance"/>. This property is therefore only a best effort, but it will work well for almost all use cases.
|
||||
///
|
||||
/// Furthermore it will not report that the destination is reached if the destination is above the head of the character or more than half the <see cref="height"/> of the character below its feet
|
||||
/// (so if you have a multilevel building, it is important that you configure the <see cref="height"/> of the character correctly).
|
||||
///
|
||||
/// The cases which could be problematic are if an agent is standing next to a very thin wall and the destination suddenly changes to the other side of that thin wall.
|
||||
/// During the time that it takes for the path to be calculated the agent may see itself as alredy having reached the destination because the destination only moved a very small distance (the wall was thin),
|
||||
/// even though it may actually be quite a long way around the wall to the other side.
|
||||
///
|
||||
/// In contrast to <see cref="reachedEndOfPath"/>, this property is immediately updated when the <see cref="destination"/> is changed.
|
||||
///
|
||||
/// <code>
|
||||
/// IEnumerator Start () {
|
||||
/// ai.destination = somePoint;
|
||||
/// // Start to search for a path to the destination immediately
|
||||
/// ai.SearchPath();
|
||||
/// // Wait until the agent has reached the destination
|
||||
/// while (!ai.reachedDestination) {
|
||||
/// yield return null;
|
||||
/// }
|
||||
/// // The agent has reached the destination now
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// See: <see cref="AIPath.endReachedDistance"/>
|
||||
/// See: <see cref="remainingDistance"/>
|
||||
/// See: <see cref="reachedEndOfPath"/>
|
||||
/// </summary>
|
||||
bool reachedDestination { get; }
|
||||
|
||||
/// <summary>
|
||||
/// True if the agent has reached the end of the current path.
|
||||
///
|
||||
/// Note that setting the <see cref="destination"/> does not immediately update the path, nor is there any guarantee that the
|
||||
/// AI will actually be able to reach the destination that you set. The AI will try to get as close as possible.
|
||||
/// Often you want to use <see cref="reachedDestination"/> instead which is easier to work with.
|
||||
///
|
||||
/// It is very hard to provide a method for detecting if the AI has reached the <see cref="destination"/> that works across all different games
|
||||
/// because the destination may not even lie on the navmesh and how that is handled differs from game to game (see also the code snippet in the docs for <see cref="destination"/>).
|
||||
///
|
||||
/// See: <see cref="remainingDistance"/>
|
||||
/// See: <see cref="reachedDestination"/>
|
||||
/// </summary>
|
||||
bool reachedEndOfPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// End point of path the agent is currently following.
|
||||
/// If the agent has no path (or it might not be calculated yet), this will return the <see cref="destination"/> instead.
|
||||
/// If the agent has no destination it will return the agent's current position.
|
||||
///
|
||||
/// The end of the path is usually identical or very close to the <see cref="destination"/>, but it may differ
|
||||
/// if the path for example was blocked by a wall so that the agent couldn't get any closer.
|
||||
///
|
||||
/// This is only updated when the path is recalculated.
|
||||
/// </summary>
|
||||
Vector3 endOfPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Position in the world that this agent should move to.
|
||||
///
|
||||
/// If no destination has been set yet, then (+infinity, +infinity, +infinity) will be returned.
|
||||
///
|
||||
/// Note that setting this property does not immediately cause the agent to recalculate its path.
|
||||
/// So it may take some time before the agent starts to move towards this point.
|
||||
/// Most movement scripts have a repathRate field which indicates how often the agent looks
|
||||
/// for a new path. You can also call the <see cref="SearchPath"/> method to immediately
|
||||
/// start to search for a new path. Paths are calculated asynchronously so when an agent starts to
|
||||
/// search for path it may take a few frames (usually 1 or 2) until the result is available.
|
||||
/// During this time the <see cref="pathPending"/> property will return true.
|
||||
///
|
||||
/// If you are setting a destination and then want to know when the agent has reached that destination
|
||||
/// then you could either use <see cref="reachedDestination"/> (recommended) or check both <see cref="pathPending"/> and <see cref="reachedEndOfPath"/>.
|
||||
/// Check the documentation for the respective fields to learn about their differences.
|
||||
///
|
||||
/// <code>
|
||||
/// IEnumerator Start () {
|
||||
/// ai.destination = somePoint;
|
||||
/// // Start to search for a path to the destination immediately
|
||||
/// ai.SearchPath();
|
||||
/// // Wait until the agent has reached the destination
|
||||
/// while (!ai.reachedDestination) {
|
||||
/// yield return null;
|
||||
/// }
|
||||
/// // The agent has reached the destination now
|
||||
/// }
|
||||
/// </code>
|
||||
/// <code>
|
||||
/// IEnumerator Start () {
|
||||
/// ai.destination = somePoint;
|
||||
/// // Start to search for a path to the destination immediately
|
||||
/// // Note that the result may not become available until after a few frames
|
||||
/// // ai.pathPending will be true while the path is being calculated
|
||||
/// ai.SearchPath();
|
||||
/// // Wait until we know for sure that the agent has calculated a path to the destination we set above
|
||||
/// while (ai.pathPending || !ai.reachedEndOfPath) {
|
||||
/// yield return null;
|
||||
/// }
|
||||
/// // The agent has reached the destination now
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
Vector3 destination { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables recalculating the path at regular intervals.
|
||||
/// Setting this to false does not stop any active path requests from being calculated or stop it from continuing to follow the current path.
|
||||
///
|
||||
/// Note that this only disables automatic path recalculations. If you call the <see cref="SearchPath()"/> method a path will still be calculated.
|
||||
///
|
||||
/// See: <see cref="canMove"/>
|
||||
/// See: <see cref="isStopped"/>
|
||||
/// </summary>
|
||||
bool canSearch { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables movement completely.
|
||||
/// If you want the agent to stand still, but still react to local avoidance and use gravity: use <see cref="isStopped"/> instead.
|
||||
///
|
||||
/// This is also useful if you want to have full control over when the movement calculations run.
|
||||
/// Take a look at <see cref="MovementUpdate"/>
|
||||
///
|
||||
/// See: <see cref="canSearch"/>
|
||||
/// See: <see cref="isStopped"/>
|
||||
/// </summary>
|
||||
bool canMove { get; set; }
|
||||
|
||||
/// <summary>True if this agent currently has a path that it follows</summary>
|
||||
bool hasPath { get; }
|
||||
|
||||
/// <summary>True if a path is currently being calculated</summary>
|
||||
bool pathPending { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the character's position should be coupled to the Transform's position.
|
||||
/// If false then all movement calculations will happen as usual, but the GameObject that this component is attached to will not move.
|
||||
/// Instead, only the <see cref="position"/> property will change.
|
||||
///
|
||||
/// This is useful if you want to control the movement of the character using some other means, such
|
||||
/// as root motion, but still want the AI to move freely.
|
||||
///
|
||||
/// See: <see cref="canMove"/> which in contrast to this field will disable all movement calculations.
|
||||
/// See: <see cref="updateRotation"/>
|
||||
/// </summary>
|
||||
bool updatePosition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the character's rotation should be coupled to the Transform's rotation.
|
||||
/// If false then all movement calculations will happen as usual, but the GameObject that this component is attached to will not rotate.
|
||||
/// Instead, only the <see cref="rotation"/> property will change.
|
||||
///
|
||||
/// This is particularly useful for 2D games where you want the Transform to stay in the same orientation, and instead swap out the displayed
|
||||
/// sprite to indicate the direction the character is facing.
|
||||
///
|
||||
/// See: <see cref="updatePosition"/>
|
||||
/// See: <see cref="rotation"/>
|
||||
/// </summary>
|
||||
bool updateRotation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets if the agent should stop moving.
|
||||
/// If this is set to true the agent will immediately start to slow down as quickly as it can to come to a full stop.
|
||||
/// The agent will still react to local avoidance and gravity (if applicable), but it will not try to move in any particular direction.
|
||||
///
|
||||
/// The current path of the agent will not be cleared, so when this is set
|
||||
/// to false again the agent will continue moving along the previous path.
|
||||
///
|
||||
/// This is a purely user-controlled parameter, so for example it is not set automatically when the agent stops
|
||||
/// moving because it has reached the target. Use <see cref="reachedEndOfPath"/> for that.
|
||||
///
|
||||
/// If this property is set to true while the agent is traversing an off-mesh link (RichAI script only), then the agent will
|
||||
/// continue traversing the link and stop once it has completed it.
|
||||
///
|
||||
/// Note: This is not the same as the <see cref="canMove"/> setting which some movement scripts have. The <see cref="canMove"/> setting
|
||||
/// disables movement calculations completely (which among other things makes it not be affected by local avoidance or gravity).
|
||||
/// For the AILerp movement script which doesn't use gravity or local avoidance anyway changing this property is very similar to
|
||||
/// changing <see cref="canMove"/>.
|
||||
///
|
||||
/// The <see cref="steeringTarget"/> property will continue to indicate the point which the agent would move towards if it would not be stopped.
|
||||
/// </summary>
|
||||
bool isStopped { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Point on the path which the agent is currently moving towards.
|
||||
/// This is usually a point a small distance ahead of the agent
|
||||
/// or the end of the path.
|
||||
///
|
||||
/// If the agent does not have a path at the moment, then the agent's current position will be returned.
|
||||
/// </summary>
|
||||
Vector3 steeringTarget { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the agent recalculates its path.
|
||||
/// This is called both for automatic path recalculations (see <see cref="canSearch)"/> and manual ones (see <see cref="SearchPath)"/>.
|
||||
///
|
||||
/// See: Take a look at the <see cref="Pathfinding.AIDestinationSetter"/> source code for an example of how it can be used.
|
||||
/// </summary>
|
||||
System.Action onSearchPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The plane the agent is moving in.
|
||||
///
|
||||
/// This is typically the ground plane, which will be the XZ plane in a 3D game, and the XY plane in a 2D game.
|
||||
/// Ultimately it depends on the graph orientation.
|
||||
///
|
||||
/// If you are doing pathfinding on a spherical world (see spherical) (view in online documentation for working links), the the movement plane will be the tangent plane of the sphere at the agent's position.
|
||||
/// </summary>
|
||||
NativeMovementPlane movementPlane { get; }
|
||||
/// <summary>
|
||||
/// Fills buffer with the remaining path.
|
||||
///
|
||||
/// <code>
|
||||
/// var buffer = new List<Vector3>();
|
||||
///
|
||||
/// ai.GetRemainingPath(buffer, out bool stale);
|
||||
/// for (int i = 0; i < buffer.Count - 1; i++) {
|
||||
/// Debug.DrawLine(buffer[i], buffer[i+1], Color.red);
|
||||
/// }
|
||||
/// </code>
|
||||
/// [Open online documentation to see images]
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer will be cleared and replaced with the path. The first point is the current position of the agent.</param>
|
||||
/// <param name="stale">May be true if the path is invalid in some way. For example if the agent has no path or (for the RichAI/FollowerEntity components only) if the agent has detected that some nodes in the path have been destroyed.</param>
|
||||
void GetRemainingPath(List<Vector3> buffer, out bool stale);
|
||||
|
||||
/// <summary>
|
||||
/// Fills buffer with the remaining path.
|
||||
///
|
||||
/// <code>
|
||||
/// var buffer = new List<Vector3>();
|
||||
/// var parts = new List<PathPartWithLinkInfo>();
|
||||
///
|
||||
/// ai.GetRemainingPath(buffer, parts, out bool stale);
|
||||
/// foreach (var part in parts) {
|
||||
/// for (int i = part.startIndex; i < part.endIndex; i++) {
|
||||
/// Debug.DrawLine(buffer[i], buffer[i+1], part.type == Funnel.PartType.NodeSequence ? Color.red : Color.green);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// Note: The <see cref="AIPath"/> and <see cref="AILerp"/> movement scripts do not know about off-mesh links, so the partsBuffer will always be filled with a single node-sequence part.
|
||||
/// </summary>
|
||||
/// <param name="buffer">The buffer will be cleared and replaced with the path. The first point is the current position of the agent.</param>
|
||||
/// <param name="partsBuffer">If not null, this list will be cleared and filled with information about the different parts of the path. A part is a sequence of nodes or an off-mesh link.</param>
|
||||
/// <param name="stale">May be true if the path is invalid in some way. For example if the agent has no path or (for the RichAI/FollowerEntity components only) if the agent has detected that some nodes in the path have been destroyed.</param>
|
||||
void GetRemainingPath(List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale);
|
||||
|
||||
/// <summary>
|
||||
/// Recalculate the current path.
|
||||
/// You can for example use this if you want very quick reaction times when you have changed the <see cref="destination"/>
|
||||
/// so that the agent does not have to wait until the next automatic path recalculation (see <see cref="canSearch)"/>.
|
||||
///
|
||||
/// If there is an ongoing path calculation, it will be canceled, so make sure you leave time for the paths to get calculated before calling this function again.
|
||||
/// A canceled path will show up in the log with the message "Canceled by script" (see <see cref="Seeker.CancelCurrentPathRequest"/>).
|
||||
///
|
||||
/// If no <see cref="destination"/> has been set yet then nothing will be done.
|
||||
///
|
||||
/// Note: The path result may not become available until after a few frames.
|
||||
/// During the calculation time the <see cref="pathPending"/> property will return true.
|
||||
///
|
||||
/// See: <see cref="pathPending"/>
|
||||
/// </summary>
|
||||
void SearchPath();
|
||||
|
||||
/// <summary>
|
||||
/// Make the AI follow the specified path.
|
||||
///
|
||||
/// In case the path has not been calculated, the script will call seeker.StartPath to calculate it.
|
||||
/// This means the AI may not actually start to follow the path until in a few frames when the path has been calculated.
|
||||
/// The <see cref="pathPending"/> field will as usual return true while the path is being calculated.
|
||||
///
|
||||
/// In case the path has already been calculated it will immediately replace the current path the AI is following.
|
||||
/// This is useful if you want to replace how the AI calculates its paths.
|
||||
///
|
||||
/// If you pass null as a parameter then the current path will be cleared and the agent will stop moving.
|
||||
/// Note than unless you have also disabled <see cref="canSearch"/> then the agent will soon recalculate its path and start moving again.
|
||||
///
|
||||
/// You can disable the automatic path recalculation by setting the <see cref="canSearch"/> field to false.
|
||||
///
|
||||
/// Note: This call will be ignored if the agent is currently traversing an off-mesh link. Furthermore, if the agent starts traversing an off-mesh link, the current path request will be canceled (if one is currently in progress).
|
||||
///
|
||||
/// <code>
|
||||
/// // Disable the automatic path recalculation
|
||||
/// ai.canSearch = false;
|
||||
/// var pointToAvoid = enemy.position;
|
||||
/// // Make the AI flee from the enemy.
|
||||
/// // The path will be about 20 world units long (the default cost of moving 1 world unit is 1000).
|
||||
/// var path = FleePath.Construct(ai.position, pointToAvoid, 1000 * 20);
|
||||
/// ai.SetPath(path);
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="path">The path to follow.</param>
|
||||
/// <param name="updateDestinationFromPath">If true, the \reflink{destination} property will be set to the end point of the path. If false, the previous destination value will be kept.</param>
|
||||
void SetPath(Path path, bool updateDestinationFromPath = true);
|
||||
|
||||
/// <summary>
|
||||
/// Instantly move the agent to a new position.
|
||||
/// This will trigger a path recalculation (if clearPath is true, which is the default) so if you want to teleport the agent and change its <see cref="destination"/>
|
||||
/// it is recommended that you set the <see cref="destination"/> before calling this method.
|
||||
///
|
||||
/// The current path will be cleared by default.
|
||||
///
|
||||
/// This method is preferred for long distance teleports. If you only move the agent a very small distance (so that it is reasonable that it can keep its current path),
|
||||
/// then setting the <see cref="position"/> property is preferred.
|
||||
/// When using the <see cref="FollowerEntity"/> movement script, setting the <see cref="position"/> property when using a long distance teleport could cause the agent to fail to move the full distance, as it can get blocked by the navmesh.
|
||||
///
|
||||
/// See: Works similarly to Unity's NavmeshAgent.Warp.
|
||||
/// See: <see cref="SearchPath"/>
|
||||
/// </summary>
|
||||
void Teleport(Vector3 newPosition, bool clearPath = true);
|
||||
|
||||
/// <summary>
|
||||
/// Move the agent.
|
||||
///
|
||||
/// This is intended for external movement forces such as those applied by wind, conveyor belts, knockbacks etc.
|
||||
///
|
||||
/// Some movement scripts may ignore this completely (notably the <see cref="AILerp"/> script) if it does not have
|
||||
/// any concept of being moved externally.
|
||||
///
|
||||
/// For the <see cref="AIPath"/> and <see cref="RichAI"/> movement scripts, the agent will not be moved immediately when calling this method. Instead this offset will be stored and then
|
||||
/// applied the next time the agent runs its movement calculations (which is usually later this frame or the next frame).
|
||||
/// If you want to move the agent immediately then call:
|
||||
/// <code>
|
||||
/// ai.Move(someVector);
|
||||
/// ai.FinalizeMovement(ai.position, ai.rotation);
|
||||
/// </code>
|
||||
///
|
||||
/// The <see cref="FollowerEntity"/> movement script will, on the other hand, move the agent immediately.
|
||||
/// </summary>
|
||||
/// <param name="deltaPosition">Direction and distance to move the agent in world space.</param>
|
||||
void Move(Vector3 deltaPosition);
|
||||
|
||||
/// <summary>
|
||||
/// Calculate how the character wants to move during this frame.
|
||||
///
|
||||
/// Note that this does not actually move the character. You need to call <see cref="FinalizeMovement"/> for that.
|
||||
/// This is called automatically unless <see cref="canMove"/> is false.
|
||||
///
|
||||
/// To handle movement yourself you can disable <see cref="canMove"/> and call this method manually.
|
||||
/// This code will replicate the normal behavior of the component:
|
||||
/// <code>
|
||||
/// void Update () {
|
||||
/// // Disable the AIs own movement code
|
||||
/// ai.canMove = false;
|
||||
/// Vector3 nextPosition;
|
||||
/// Quaternion nextRotation;
|
||||
/// // Calculate how the AI wants to move
|
||||
/// ai.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);
|
||||
/// // Modify nextPosition and nextRotation in any way you wish
|
||||
/// // Actually move the AI
|
||||
/// ai.FinalizeMovement(nextPosition, nextRotation);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">time to simulate movement for. Usually set to Time.deltaTime.</param>
|
||||
/// <param name="nextPosition">the position that the agent wants to move to during this frame.</param>
|
||||
/// <param name="nextRotation">the rotation that the agent wants to rotate to during this frame.</param>
|
||||
void MovementUpdate(float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation);
|
||||
|
||||
/// <summary>
|
||||
/// Move the agent.
|
||||
/// To be called as the last step when you are handling movement manually.
|
||||
///
|
||||
/// The movement will be clamped to the navmesh if applicable (this is done for the RichAI movement script).
|
||||
///
|
||||
/// See: <see cref="MovementUpdate"/> for a code example.
|
||||
/// </summary>
|
||||
void FinalizeMovement(Vector3 nextPosition, Quaternion nextRotation);
|
||||
}
|
||||
}
|
||||
12
Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs.meta
Normal file
12
Packages/com.arongranberg.astar/Core/AI/IAstarAI.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7438f3f6b9404f05ab7f584f92aa7d5
|
||||
timeCreated: 1495013922
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
73
Packages/com.arongranberg.astar/Core/AI/LocalSpaceRichAI.cs
Normal file
73
Packages/com.arongranberg.astar/Core/AI/LocalSpaceRichAI.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using UnityEngine;
|
||||
namespace Pathfinding.Examples {
|
||||
using Pathfinding.Util;
|
||||
|
||||
/// <summary>
|
||||
/// RichAI for local space (pathfinding on moving graphs).
|
||||
///
|
||||
/// What this script does is that it fakes graph movement.
|
||||
/// It can be seen in the example scene called 'Moving' where
|
||||
/// a character is pathfinding on top of a moving ship.
|
||||
/// The graph does not actually move in that example
|
||||
/// instead there is some 'cheating' going on.
|
||||
///
|
||||
/// When requesting a path, we first transform
|
||||
/// the start and end positions of the path request
|
||||
/// into local space for the object we are moving on
|
||||
/// (e.g the ship in the example scene), then when we get the
|
||||
/// path back, they will still be in these local coordinates.
|
||||
/// When following the path, we will every frame transform
|
||||
/// the coordinates of the waypoints in the path to global
|
||||
/// coordinates so that we can follow them.
|
||||
///
|
||||
/// At the start of the game (when the graph is scanned) the
|
||||
/// object we are moving on should be at a valid position on the graph and
|
||||
/// you should attach the <see cref="Pathfinding.LocalSpaceGraph"/> component to it. The <see cref="Pathfinding.LocalSpaceGraph"/>
|
||||
/// component will store the position and orientation of the object right there are the start
|
||||
/// and then we can use that information to transform coordinates back to that region of the graph
|
||||
/// as if the object had not moved at all.
|
||||
///
|
||||
/// This functionality is only implemented for the RichAI
|
||||
/// script, however it should not be hard to
|
||||
/// use the same approach for other movement scripts.
|
||||
/// </summary>
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/localspacerichai.html")]
|
||||
public class LocalSpaceRichAI : RichAI {
|
||||
/// <summary>Root of the object we are moving on</summary>
|
||||
public LocalSpaceGraph graph;
|
||||
|
||||
protected override Vector3 ClampPositionToGraph (Vector3 newPosition) {
|
||||
RefreshTransform();
|
||||
// Clamp the new position to the navmesh
|
||||
// First we need to transform the position to the same space that the graph is in though.
|
||||
var nearest = AstarPath.active != null? AstarPath.active.GetNearest(graph.transformation.InverseTransform(newPosition)) : new NNInfo();
|
||||
float elevation;
|
||||
|
||||
movementPlane.ToPlane(newPosition, out elevation);
|
||||
return movementPlane.ToWorld(movementPlane.ToPlane(nearest.node != null ? graph.transformation.Transform(nearest.position) : newPosition), elevation);
|
||||
}
|
||||
|
||||
void RefreshTransform () {
|
||||
graph.Refresh();
|
||||
richPath.transform = graph.transformation;
|
||||
movementPlane = graph.transformation.ToSimpleMovementPlane();
|
||||
}
|
||||
|
||||
protected override void Start () {
|
||||
RefreshTransform();
|
||||
base.Start();
|
||||
}
|
||||
|
||||
protected override void CalculatePathRequestEndpoints (out Vector3 start, out Vector3 end) {
|
||||
RefreshTransform();
|
||||
base.CalculatePathRequestEndpoints(out start, out end);
|
||||
start = graph.transformation.InverseTransform(start);
|
||||
end = graph.transformation.InverseTransform(end);
|
||||
}
|
||||
|
||||
protected override void OnUpdate (float dt) {
|
||||
RefreshTransform();
|
||||
base.OnUpdate(dt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e342e9f54c9d04f05b77eff70a36605e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
646
Packages/com.arongranberg.astar/Core/AI/RichAI.cs
Normal file
646
Packages/com.arongranberg.astar/Core/AI/RichAI.cs
Normal file
@@ -0,0 +1,646 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.RVO;
|
||||
using Pathfinding.Util;
|
||||
using Pathfinding.Drawing;
|
||||
|
||||
[AddComponentMenu("Pathfinding/AI/RichAI (3D, for navmesh)")]
|
||||
[UniqueComponent(tag = "ai")]
|
||||
[DisallowMultipleComponent]
|
||||
/// <summary>
|
||||
/// Advanced AI for navmesh based graphs.
|
||||
///
|
||||
/// See: movementscripts (view in online documentation for working links)
|
||||
/// </summary>
|
||||
public partial class RichAI : AIBase, IAstarAI {
|
||||
/// <summary>
|
||||
/// Max acceleration of the agent.
|
||||
/// In world units per second per second.
|
||||
/// </summary>
|
||||
public float acceleration = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Max rotation speed of the agent.
|
||||
/// In degrees per second.
|
||||
/// </summary>
|
||||
public float rotationSpeed = 360;
|
||||
|
||||
/// <summary>
|
||||
/// How long before reaching the end of the path to start to slow down.
|
||||
/// A lower value will make the agent stop more abruptly.
|
||||
///
|
||||
/// Note: The agent may require more time to slow down if
|
||||
/// its maximum <see cref="acceleration"/> is not high enough.
|
||||
///
|
||||
/// If set to zero the agent will not even attempt to slow down.
|
||||
/// This can be useful if the target point is not a point you want the agent to stop at
|
||||
/// but it might for example be the player and you want the AI to slam into the player.
|
||||
///
|
||||
/// Note: A value of zero will behave differently from a small but non-zero value (such as 0.0001).
|
||||
/// When it is non-zero the agent will still respect its <see cref="acceleration"/> when determining if it needs
|
||||
/// to slow down, but if it is zero it will disable that check.
|
||||
/// This is useful if the <see cref="destination"/> is not a point where you want the agent to stop.
|
||||
///
|
||||
/// \htmlonly <video class="tinyshadow" controls="true" loop="true"><source src="images/richai_slowdown_time.mp4" type="video/mp4" /></video> \endhtmlonly
|
||||
/// </summary>
|
||||
public float slowdownTime = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Force to avoid walls with.
|
||||
/// The agent will try to steer away from walls slightly.
|
||||
///
|
||||
/// See: <see cref="wallDist"/>
|
||||
/// </summary>
|
||||
public float wallForce = 3;
|
||||
|
||||
/// <summary>
|
||||
/// Walls within this range will be used for avoidance.
|
||||
/// Setting this to zero disables wall avoidance and may improve performance slightly
|
||||
///
|
||||
/// See: <see cref="wallForce"/>
|
||||
/// </summary>
|
||||
public float wallDist = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Use funnel simplification.
|
||||
/// On tiled navmesh maps, but sometimes on normal ones as well, it can be good to simplify
|
||||
/// the funnel as a post-processing step to make the paths straighter.
|
||||
///
|
||||
/// This has a moderate performance impact during frames when a path calculation is completed.
|
||||
///
|
||||
/// The RichAI script uses its own internal funnel algorithm, so you never
|
||||
/// need to attach the FunnelModifier component.
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// See: <see cref="Pathfinding.FunnelModifier"/>
|
||||
/// </summary>
|
||||
public bool funnelSimplification = false;
|
||||
|
||||
/// <summary>
|
||||
/// Slow down when not facing the target direction.
|
||||
/// Incurs at a small performance overhead.
|
||||
///
|
||||
/// This setting only has an effect if <see cref="enableRotation"/> is enabled.
|
||||
/// </summary>
|
||||
public bool slowWhenNotFacingTarget = true;
|
||||
|
||||
/// <summary>
|
||||
/// Prevent the velocity from being too far away from the forward direction of the character.
|
||||
/// If the character is ordered to move in the opposite direction from where it is facing
|
||||
/// then enabling this will cause it to make a small loop instead of turning on the spot.
|
||||
///
|
||||
/// This setting only has an effect if <see cref="slowWhenNotFacingTarget"/> is enabled.
|
||||
/// </summary>
|
||||
public bool preventMovingBackwards = false;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the agent starts to traverse an off-mesh link.
|
||||
/// Register to this callback to handle off-mesh links in a custom way.
|
||||
///
|
||||
/// If this event is set to null then the agent will fall back to traversing
|
||||
/// off-mesh links using a very simple linear interpolation.
|
||||
///
|
||||
/// <code>
|
||||
/// void OnEnable () {
|
||||
/// ai = GetComponent<RichAI>();
|
||||
/// if (ai != null) ai.onTraverseOffMeshLink += TraverseOffMeshLink;
|
||||
/// }
|
||||
///
|
||||
/// void OnDisable () {
|
||||
/// if (ai != null) ai.onTraverseOffMeshLink -= TraverseOffMeshLink;
|
||||
/// }
|
||||
///
|
||||
/// IEnumerator TraverseOffMeshLink (RichSpecial link) {
|
||||
/// // Traverse the link over 1 second
|
||||
/// float startTime = Time.time;
|
||||
///
|
||||
/// while (Time.time < startTime + 1) {
|
||||
/// transform.position = Vector3.Lerp(link.first.position, link.second.position, Time.time - startTime);
|
||||
/// yield return null;
|
||||
/// }
|
||||
/// transform.position = link.second.position;
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public System.Func<RichSpecial, IEnumerator> onTraverseOffMeshLink;
|
||||
|
||||
/// <summary>Holds the current path that this agent is following</summary>
|
||||
protected readonly RichPath richPath = new RichPath();
|
||||
|
||||
protected bool delayUpdatePath;
|
||||
protected bool lastCorner;
|
||||
|
||||
/// <summary>Internal state used for filtering out noise in the agent's rotation</summary>
|
||||
Vector2 rotationFilterState;
|
||||
Vector2 rotationFilterState2;
|
||||
|
||||
/// <summary>Distance to <see cref="steeringTarget"/> in the movement plane</summary>
|
||||
protected float distanceToSteeringTarget = float.PositiveInfinity;
|
||||
|
||||
protected readonly List<Vector3> nextCorners = new List<Vector3>();
|
||||
protected readonly List<Vector3> wallBuffer = new List<Vector3>();
|
||||
|
||||
public bool traversingOffMeshLink { get; protected set; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::remainingDistance</summary>
|
||||
public float remainingDistance {
|
||||
get {
|
||||
return distanceToSteeringTarget + Vector3.Distance(steeringTarget, richPath.Endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::reachedEndOfPath</summary>
|
||||
public bool reachedEndOfPath { get { return approachingPathEndpoint && distanceToSteeringTarget < endReachedDistance; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::reachedDestination</summary>
|
||||
public override bool reachedDestination {
|
||||
get {
|
||||
if (!reachedEndOfPath) return false;
|
||||
// Distance from our position to the current steering target +
|
||||
// Distance from the steering target to the end of the path +
|
||||
// distance from the end of the path to the destination.
|
||||
// Note that most distance checks are done only in the movement plane (which means in most cases that the y coordinate differences are discarded).
|
||||
// This is because those coordinates are often not very accurate.
|
||||
// A separate check is done below to make sure that the destination y coordinate is correct
|
||||
if (distanceToSteeringTarget + movementPlane.ToPlane(steeringTarget - richPath.Endpoint).magnitude + movementPlane.ToPlane(destination - richPath.Endpoint).magnitude > endReachedDistance) return false;
|
||||
|
||||
// Don't do height checks in 2D mode
|
||||
if (orientation != OrientationMode.YAxisForward) {
|
||||
// Check if the destination is above the head of the character or far below the feet of it
|
||||
float yDifference;
|
||||
movementPlane.ToPlane(destination - position, out yDifference);
|
||||
var h = tr.localScale.y * height;
|
||||
if (yDifference > h || yDifference < -h*0.5) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::hasPath</summary>
|
||||
public bool hasPath { get { return richPath.GetCurrentPart() != null; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::pathPending</summary>
|
||||
public bool pathPending { get { return waitingForPathCalculation || delayUpdatePath; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::steeringTarget</summary>
|
||||
public Vector3 steeringTarget { get; protected set; }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::radius</summary>
|
||||
float IAstarAI.radius { get { return radius; } set { radius = value; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::height</summary>
|
||||
float IAstarAI.height { get { return height; } set { height = value; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::maxSpeed</summary>
|
||||
float IAstarAI.maxSpeed { get { return maxSpeed; } set { maxSpeed = value; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::canSearch</summary>
|
||||
bool IAstarAI.canSearch { get { return canSearch; } set { canSearch = value; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::canMove</summary>
|
||||
bool IAstarAI.canMove { get { return canMove; } set { canMove = value; } }
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::movementPlane</summary>
|
||||
NativeMovementPlane IAstarAI.movementPlane => new NativeMovementPlane(movementPlane);
|
||||
|
||||
/// <summary>
|
||||
/// True if approaching the last waypoint in the current part of the path.
|
||||
/// Path parts are separated by off-mesh links.
|
||||
///
|
||||
/// See: <see cref="approachingPathEndpoint"/>
|
||||
/// </summary>
|
||||
public bool approachingPartEndpoint {
|
||||
get {
|
||||
return lastCorner && nextCorners.Count == 1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if approaching the last waypoint of all parts in the current path.
|
||||
/// Path parts are separated by off-mesh links.
|
||||
///
|
||||
/// See: <see cref="approachingPartEndpoint"/>
|
||||
/// </summary>
|
||||
public bool approachingPathEndpoint {
|
||||
get {
|
||||
return approachingPartEndpoint && richPath.IsLastPart;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydoc Pathfinding::IAstarAI::endOfPath</summary>
|
||||
public override Vector3 endOfPath {
|
||||
get {
|
||||
if (hasPath) return richPath.Endpoint;
|
||||
if (float.IsFinite(destination.x)) return destination;
|
||||
return position;
|
||||
}
|
||||
}
|
||||
|
||||
public override Quaternion rotation {
|
||||
get {
|
||||
return base.rotation;
|
||||
}
|
||||
set {
|
||||
base.rotation = value;
|
||||
// Make the agent keep this rotation instead of just rotating back to whatever it used before
|
||||
rotationFilterState = Vector2.zero;
|
||||
rotationFilterState2 = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// \copydoc Pathfinding::IAstarAI::Teleport
|
||||
///
|
||||
/// When setting transform.position directly the agent
|
||||
/// will be clamped to the part of the navmesh it can
|
||||
/// reach, so it may not end up where you wanted it to.
|
||||
/// This ensures that the agent can move to any part of the navmesh.
|
||||
/// </summary>
|
||||
public override void Teleport (Vector3 newPosition, bool clearPath = true) {
|
||||
base.Teleport(ClampPositionToGraph(newPosition), clearPath);
|
||||
}
|
||||
|
||||
protected virtual Vector3 ClampPositionToGraph (Vector3 newPosition) {
|
||||
// Clamp the new position to the navmesh
|
||||
var nearest = AstarPath.active != null? AstarPath.active.GetNearest(newPosition) : new NNInfo();
|
||||
float elevation;
|
||||
|
||||
movementPlane.ToPlane(newPosition, out elevation);
|
||||
return movementPlane.ToWorld(movementPlane.ToPlane(nearest.node != null ? nearest.position : newPosition), elevation);
|
||||
}
|
||||
|
||||
/// <summary>Called when the component is disabled</summary>
|
||||
protected override void OnDisable () {
|
||||
base.OnDisable();
|
||||
traversingOffMeshLink = false;
|
||||
// Stop the off mesh link traversal coroutine
|
||||
StopAllCoroutines();
|
||||
rotationFilterState = Vector2.zero;
|
||||
rotationFilterState2 = Vector2.zero;
|
||||
}
|
||||
|
||||
protected override bool shouldRecalculatePath {
|
||||
get {
|
||||
// Don't automatically recalculate the path in the middle of an off-mesh link
|
||||
return base.shouldRecalculatePath && !traversingOffMeshLink;
|
||||
}
|
||||
}
|
||||
|
||||
public override void SearchPath () {
|
||||
// Calculate paths after the current off-mesh link has been completed
|
||||
if (traversingOffMeshLink) {
|
||||
delayUpdatePath = true;
|
||||
} else {
|
||||
base.SearchPath();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnPathComplete (Path p) {
|
||||
waitingForPathCalculation = false;
|
||||
p.Claim(this);
|
||||
|
||||
if (p.error) {
|
||||
p.Release(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (traversingOffMeshLink) {
|
||||
delayUpdatePath = true;
|
||||
} else {
|
||||
// The RandomPath and MultiTargetPath do not have a well defined destination that could have been
|
||||
// set before the paths were calculated. So we instead set the destination here so that some properties
|
||||
// like #reachedDestination and #remainingDistance work correctly.
|
||||
if (p is ABPath abPath && !abPath.endPointKnownBeforeCalculation) {
|
||||
destination = abPath.originalEndPoint;
|
||||
}
|
||||
|
||||
richPath.Initialize(seeker, p, true, funnelSimplification);
|
||||
|
||||
// Check if we have already reached the end of the path
|
||||
// We need to do this here to make sure that the #reachedEndOfPath
|
||||
// property is up to date.
|
||||
var part = richPath.GetCurrentPart() as RichFunnel;
|
||||
if (part != null) {
|
||||
if (updatePosition) simulatedPosition = tr.position;
|
||||
|
||||
// Note: UpdateTarget has some side effects like setting the nextCorners list and the lastCorner field
|
||||
var localPosition = movementPlane.ToPlane(UpdateTarget(part));
|
||||
|
||||
// Target point
|
||||
steeringTarget = nextCorners[0];
|
||||
Vector2 targetPoint = movementPlane.ToPlane(steeringTarget);
|
||||
distanceToSteeringTarget = (targetPoint - localPosition).magnitude;
|
||||
|
||||
if (lastCorner && nextCorners.Count == 1 && distanceToSteeringTarget <= endReachedDistance) {
|
||||
NextPart();
|
||||
}
|
||||
}
|
||||
}
|
||||
p.Release(this);
|
||||
}
|
||||
|
||||
protected override void ClearPath () {
|
||||
CancelCurrentPathRequest();
|
||||
richPath.Clear();
|
||||
lastCorner = false;
|
||||
delayUpdatePath = false;
|
||||
distanceToSteeringTarget = float.PositiveInfinity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Declare that the AI has completely traversed the current part.
|
||||
/// This will skip to the next part, or call OnTargetReached if this was the last part
|
||||
/// </summary>
|
||||
protected void NextPart () {
|
||||
if (!richPath.CompletedAllParts) {
|
||||
if (!richPath.IsLastPart) lastCorner = false;
|
||||
richPath.NextPart();
|
||||
if (richPath.CompletedAllParts) {
|
||||
OnTargetReached();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,bool)}</summary>
|
||||
public void GetRemainingPath (List<Vector3> buffer, out bool stale) {
|
||||
richPath.GetRemainingPath(buffer, null, simulatedPosition, out stale);
|
||||
}
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.GetRemainingPath(List<Vector3>,List<PathPartWithLinkInfo>,bool)}</summary>
|
||||
public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, out bool stale) {
|
||||
richPath.GetRemainingPath(buffer, partsBuffer, simulatedPosition, out stale);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the end of the path is reached.
|
||||
///
|
||||
/// Deprecated: Avoid overriding this method. Instead poll the <see cref="reachedDestination"/> or <see cref="reachedEndOfPath"/> properties.
|
||||
/// </summary>
|
||||
protected virtual void OnTargetReached () {
|
||||
}
|
||||
|
||||
protected virtual Vector3 UpdateTarget (RichFunnel fn) {
|
||||
nextCorners.Clear();
|
||||
|
||||
// This method assumes simulatedPosition is up to date as our current position.
|
||||
// We read and write to tr.position as few times as possible since doing so
|
||||
// is much slower than to read and write from/to a local/member variable.
|
||||
bool requiresRepath;
|
||||
Vector3 position = fn.Update(simulatedPosition, nextCorners, 2, out lastCorner, out requiresRepath);
|
||||
|
||||
if (requiresRepath && !waitingForPathCalculation && canSearch) {
|
||||
// TODO: What if canSearch is false? How do we notify other scripts that might be handling the path calculation that a new path needs to be calculated?
|
||||
SearchPath();
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>Called during either Update or FixedUpdate depending on if rigidbodies are used for movement or not</summary>
|
||||
protected override void MovementUpdateInternal (float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) {
|
||||
if (updatePosition) simulatedPosition = tr.position;
|
||||
if (updateRotation) simulatedRotation = tr.rotation;
|
||||
|
||||
RichPathPart currentPart = richPath.GetCurrentPart();
|
||||
|
||||
if (currentPart is RichSpecial) {
|
||||
// Start traversing the off mesh link if we haven't done it yet
|
||||
if (!traversingOffMeshLink && !richPath.CompletedAllParts) {
|
||||
StartCoroutine(TraverseSpecial(currentPart as RichSpecial));
|
||||
}
|
||||
|
||||
nextPosition = steeringTarget = simulatedPosition;
|
||||
nextRotation = rotation;
|
||||
} else {
|
||||
var funnel = currentPart as RichFunnel;
|
||||
|
||||
// Check if we have a valid path to follow and some other script has not stopped the character
|
||||
bool stopped = isStopped || (reachedDestination && whenCloseToDestination == CloseToDestinationMode.Stop);
|
||||
if (rvoController != null) rvoDensityBehavior.Update(rvoController.enabled, reachedDestination, ref stopped, ref rvoController.priorityMultiplier, ref rvoController.flowFollowingStrength, simulatedPosition);
|
||||
|
||||
if (funnel != null && !stopped) {
|
||||
TraverseFunnel(funnel, deltaTime, out nextPosition, out nextRotation);
|
||||
} else {
|
||||
// Unknown, null path part, or the character is stopped
|
||||
// Slow down as quickly as possible
|
||||
velocity2D -= Vector2.ClampMagnitude(velocity2D, acceleration * deltaTime);
|
||||
FinalMovement(simulatedPosition, deltaTime, float.PositiveInfinity, 1f, out nextPosition, out nextRotation);
|
||||
if (funnel == null || isStopped) {
|
||||
steeringTarget = simulatedPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TraverseFunnel (RichFunnel fn, float deltaTime, out Vector3 nextPosition, out Quaternion nextRotation) {
|
||||
// Clamp the current position to the navmesh
|
||||
// and update the list of upcoming corners in the path
|
||||
// and store that in the 'nextCorners' field
|
||||
var position3D = UpdateTarget(fn);
|
||||
float elevation;
|
||||
Vector2 position = movementPlane.ToPlane(position3D, out elevation);
|
||||
|
||||
// Only find nearby walls every 5th frame to improve performance
|
||||
if (Time.frameCount % 5 == 0 && wallForce > 0 && wallDist > 0) {
|
||||
wallBuffer.Clear();
|
||||
fn.FindWalls(wallBuffer, wallDist);
|
||||
}
|
||||
|
||||
// Target point
|
||||
steeringTarget = nextCorners[0];
|
||||
Vector2 targetPoint = movementPlane.ToPlane(steeringTarget);
|
||||
// Direction to target
|
||||
Vector2 dir = targetPoint - position;
|
||||
|
||||
// Normalized direction to the target
|
||||
Vector2 normdir = VectorMath.Normalize(dir, out distanceToSteeringTarget);
|
||||
// Calculate force from walls
|
||||
Vector2 wallForceVector = CalculateWallForce(position, elevation, normdir);
|
||||
Vector2 targetVelocity;
|
||||
|
||||
if (approachingPartEndpoint) {
|
||||
targetVelocity = slowdownTime > 0 ? Vector2.zero : normdir * maxSpeed;
|
||||
|
||||
// Reduce the wall avoidance force as we get closer to our target
|
||||
wallForceVector *= System.Math.Min(distanceToSteeringTarget/0.5f, 1);
|
||||
|
||||
if (distanceToSteeringTarget <= endReachedDistance) {
|
||||
// Reached the end of the path or an off mesh link
|
||||
NextPart();
|
||||
}
|
||||
} else {
|
||||
var nextNextCorner = nextCorners.Count > 1 ? movementPlane.ToPlane(nextCorners[1]) : position + 2*dir;
|
||||
targetVelocity = (nextNextCorner - targetPoint).normalized * maxSpeed;
|
||||
}
|
||||
|
||||
var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward));
|
||||
|
||||
// Update the velocity using the acceleration
|
||||
Vector2 accel = MovementUtilities.CalculateAccelerationToReachPoint(targetPoint - position, targetVelocity, velocity2D, acceleration, rotationSpeed, maxSpeed, forwards);
|
||||
velocity2D += (accel + wallForceVector*wallForce)*deltaTime;
|
||||
|
||||
// Distance to the end of the path (almost as the crow flies)
|
||||
var distanceToEndOfPath = distanceToSteeringTarget + Vector3.Distance(steeringTarget, fn.exactEnd);
|
||||
|
||||
// How fast to move depending on the distance to the destination.
|
||||
// Move slower as the character gets closer to the destination.
|
||||
// This is always a value between 0 and 1.
|
||||
var speedLimitFactor = distanceToEndOfPath < maxSpeed * slowdownTime? Mathf.Sqrt(distanceToEndOfPath / (maxSpeed * slowdownTime)) : 1;
|
||||
|
||||
FinalMovement(position3D, deltaTime, distanceToEndOfPath, speedLimitFactor, out nextPosition, out nextRotation);
|
||||
}
|
||||
|
||||
void FinalMovement (Vector3 position3D, float deltaTime, float distanceToEndOfPath, float speedLimitFactor, out Vector3 nextPosition, out Quaternion nextRotation) {
|
||||
var forwards = movementPlane.ToPlane(simulatedRotation * (orientation == OrientationMode.YAxisForward ? Vector3.up : Vector3.forward));
|
||||
|
||||
ApplyGravity(deltaTime);
|
||||
|
||||
velocity2D = MovementUtilities.ClampVelocity(velocity2D, maxSpeed, speedLimitFactor, slowWhenNotFacingTarget && enableRotation, preventMovingBackwards, forwards);
|
||||
bool avoidingAnyAgents = false;
|
||||
|
||||
if (rvoController != null && rvoController.enabled) {
|
||||
// Send a message to the RVOController that we want to move
|
||||
// with this velocity. In the next simulation step, this
|
||||
// velocity will be processed and it will be fed back to the
|
||||
// rvo controller and finally it will be used by this script
|
||||
// when calling the CalculateMovementDelta method below
|
||||
|
||||
// Make sure that we don't move further than to the end point
|
||||
// of the path. If the RVO simulation FPS is low and we did
|
||||
// not do this, the agent might overshoot the target a lot.
|
||||
var rvoTarget = position3D + movementPlane.ToWorld(Vector2.ClampMagnitude(velocity2D, distanceToEndOfPath));
|
||||
rvoController.SetTarget(rvoTarget, velocity2D.magnitude, maxSpeed, endOfPath);
|
||||
avoidingAnyAgents = rvoController.AvoidingAnyAgents;
|
||||
}
|
||||
|
||||
// Direction and distance to move during this frame
|
||||
var deltaPosition = lastDeltaPosition = CalculateDeltaToMoveThisFrame(position3D, distanceToEndOfPath, deltaTime);
|
||||
|
||||
if (enableRotation) {
|
||||
// Rotate towards the direction we are moving in
|
||||
// Filter out noise in the movement direction
|
||||
// This is especially important when the agent is almost standing still and when using local avoidance
|
||||
float noiseThreshold = radius * tr.localScale.x * 0.2f;
|
||||
float rotationSpeedFactor = MovementUtilities.FilterRotationDirection(ref rotationFilterState, ref rotationFilterState2, deltaPosition, noiseThreshold, deltaTime, avoidingAnyAgents);
|
||||
nextRotation = SimulateRotationTowards(rotationFilterState, rotationSpeed * deltaTime * rotationSpeedFactor, rotationSpeed * deltaTime);
|
||||
} else {
|
||||
nextRotation = simulatedRotation;
|
||||
}
|
||||
|
||||
nextPosition = position3D + movementPlane.ToWorld(deltaPosition, verticalVelocity * deltaTime);
|
||||
}
|
||||
|
||||
protected override Vector3 ClampToNavmesh (Vector3 position, out bool positionChanged) {
|
||||
if (richPath != null) {
|
||||
var funnel = richPath.GetCurrentPart() as RichFunnel;
|
||||
if (funnel != null) {
|
||||
var clampedPosition = funnel.ClampToNavmesh(position);
|
||||
|
||||
// Inform the RVO system about the edges of the navmesh which will allow
|
||||
// it to better keep inside the navmesh in the first place.
|
||||
if (rvoController != null && rvoController.enabled) rvoController.SetObstacleQuery(funnel.CurrentNode);
|
||||
|
||||
// We cannot simply check for equality because some precision may be lost
|
||||
// if any coordinate transformations are used.
|
||||
var difference = movementPlane.ToPlane(clampedPosition - position);
|
||||
float sqrDifference = difference.sqrMagnitude;
|
||||
if (sqrDifference > 0.001f*0.001f) {
|
||||
// The agent was outside the navmesh. Remove that component of the velocity
|
||||
// so that the velocity only goes along the direction of the wall, not into it
|
||||
velocity2D -= difference * Vector2.Dot(difference, velocity2D) / sqrDifference;
|
||||
positionChanged = true;
|
||||
// Return the new position, but ignore any changes in the y coordinate from the ClampToNavmesh method as the y coordinates in the navmesh are rarely very accurate
|
||||
return position + movementPlane.ToWorld(difference);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
positionChanged = false;
|
||||
return position;
|
||||
}
|
||||
|
||||
Vector2 CalculateWallForce (Vector2 position, float elevation, Vector2 directionToTarget) {
|
||||
if (wallForce <= 0 || wallDist <= 0) return Vector2.zero;
|
||||
|
||||
float wLeft = 0;
|
||||
float wRight = 0;
|
||||
|
||||
var position3D = movementPlane.ToWorld(position, elevation);
|
||||
for (int i = 0; i < wallBuffer.Count; i += 2) {
|
||||
Vector3 closest = VectorMath.ClosestPointOnSegment(wallBuffer[i], wallBuffer[i+1], position3D);
|
||||
float dist = (closest-position3D).sqrMagnitude;
|
||||
|
||||
if (dist > wallDist*wallDist) continue;
|
||||
|
||||
Vector2 tang = movementPlane.ToPlane(wallBuffer[i+1]-wallBuffer[i]).normalized;
|
||||
|
||||
// Using the fact that all walls are laid out clockwise (looking from inside the obstacle)
|
||||
// Then left and right (ish) can be figured out like this
|
||||
float dot = Vector2.Dot(directionToTarget, tang);
|
||||
float weight = 1 - System.Math.Max(0, (2*(dist / (wallDist*wallDist))-1));
|
||||
if (dot > 0) wRight = System.Math.Max(wRight, dot * weight);
|
||||
else wLeft = System.Math.Max(wLeft, -dot * weight);
|
||||
}
|
||||
|
||||
Vector2 normal = new Vector2(directionToTarget.y, -directionToTarget.x);
|
||||
return normal*(wRight-wLeft);
|
||||
}
|
||||
|
||||
/// <summary>Traverses an off-mesh link</summary>
|
||||
protected virtual IEnumerator TraverseSpecial (RichSpecial link) {
|
||||
traversingOffMeshLink = true;
|
||||
// The current path part is a special part, for example a link
|
||||
// Movement during this part of the path is handled by the TraverseSpecial coroutine
|
||||
velocity2D = Vector3.zero;
|
||||
var offMeshLinkCoroutine = onTraverseOffMeshLink != null? onTraverseOffMeshLink(link) : TraverseOffMeshLinkFallback(link);
|
||||
yield return StartCoroutine(offMeshLinkCoroutine);
|
||||
|
||||
// Off-mesh link traversal completed
|
||||
traversingOffMeshLink = false;
|
||||
NextPart();
|
||||
|
||||
// If a path completed during the time we traversed the special connection, we need to recalculate it
|
||||
if (delayUpdatePath) {
|
||||
delayUpdatePath = false;
|
||||
// TODO: What if canSearch is false? How do we notify other scripts that might be handling the path calculation that a new path needs to be calculated?
|
||||
if (canSearch) SearchPath();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fallback for traversing off-mesh links in case <see cref="onTraverseOffMeshLink"/> is not set.
|
||||
/// This will do a simple linear interpolation along the link.
|
||||
/// </summary>
|
||||
protected IEnumerator TraverseOffMeshLinkFallback (RichSpecial link) {
|
||||
float duration = maxSpeed > 0 ? Vector3.Distance(link.second.position, link.first.position) / maxSpeed : 1;
|
||||
float startTime = Time.time;
|
||||
|
||||
while (true) {
|
||||
var pos = Vector3.Lerp(link.first.position, link.second.position, Mathf.InverseLerp(startTime, startTime + duration, Time.time));
|
||||
if (updatePosition) tr.position = pos;
|
||||
else simulatedPosition = pos;
|
||||
|
||||
if (Time.time >= startTime + duration) break;
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected static readonly Color GizmoColorPath = new Color(8.0f/255, 78.0f/255, 194.0f/255);
|
||||
|
||||
public override void DrawGizmos () {
|
||||
base.DrawGizmos();
|
||||
|
||||
if (tr != null) {
|
||||
Vector3 lastPosition = position;
|
||||
for (int i = 0; i < nextCorners.Count; lastPosition = nextCorners[i], i++) {
|
||||
Draw.Line(lastPosition, nextCorners[i], GizmoColorPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/com.arongranberg.astar/Core/AI/RichAI.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Core/AI/RichAI.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce11ea984e202491d9271f53021d8b89
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: f2e81a0445323b64f973d2f5b5c56e15, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
799
Packages/com.arongranberg.astar/Core/AI/RichPath.cs
Normal file
799
Packages/com.arongranberg.astar/Core/AI/RichPath.cs
Normal file
@@ -0,0 +1,799 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using Pathfinding.Util;
|
||||
using Pathfinding.Pooling;
|
||||
using UnityEngine.Assertions;
|
||||
|
||||
namespace Pathfinding {
|
||||
public class RichPath {
|
||||
int currentPart;
|
||||
readonly List<RichPathPart> parts = new List<RichPathPart>();
|
||||
|
||||
public Seeker seeker;
|
||||
|
||||
/// <summary>
|
||||
/// Transforms points from path space to world space.
|
||||
/// If null the identity transform will be used.
|
||||
///
|
||||
/// This is used when the world position of the agent does not match the
|
||||
/// corresponding position on the graph. This is the case in the example
|
||||
/// scene called 'Moving'.
|
||||
///
|
||||
/// See: <see cref="Pathfinding.Examples.LocalSpaceRichAI"/>
|
||||
/// </summary>
|
||||
public ITransform transform;
|
||||
|
||||
public RichPath () {
|
||||
Clear();
|
||||
}
|
||||
|
||||
public void Clear () {
|
||||
parts.Clear();
|
||||
currentPart = 0;
|
||||
Endpoint = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
|
||||
}
|
||||
|
||||
/// <summary>Use this for initialization.</summary>
|
||||
/// <param name="seeker">Optionally provide in order to take tag penalties into account. May be null if you do not use a Seeker\</param>
|
||||
/// <param name="path">Path to follow</param>
|
||||
/// <param name="mergePartEndpoints">If true, then adjacent parts that the path is split up in will
|
||||
/// try to use the same start/end points. For example when using a link on a navmesh graph
|
||||
/// Instead of first following the path to the center of the node where the link is and then
|
||||
/// follow the link, the path will be adjusted to go to the exact point where the link starts
|
||||
/// which usually makes more sense.</param>
|
||||
/// <param name="simplificationMode">The path can optionally be simplified. This can be a bit expensive for long paths.</param>
|
||||
public void Initialize (Seeker seeker, Path path, bool mergePartEndpoints, bool simplificationMode) {
|
||||
if (path.error) throw new System.ArgumentException("Path has an error");
|
||||
|
||||
List<GraphNode> nodes = path.path;
|
||||
if (nodes.Count == 0) throw new System.ArgumentException("Path traverses no nodes");
|
||||
|
||||
this.seeker = seeker;
|
||||
// Release objects back to object pool
|
||||
// Yeah, I know, it's casting... but this won't be called much
|
||||
for (int i = 0; i < parts.Count; i++) {
|
||||
var funnelPart = parts[i] as RichFunnel;
|
||||
var specialPart = parts[i] as RichSpecial;
|
||||
if (funnelPart != null) ObjectPool<RichFunnel>.Release(ref funnelPart);
|
||||
else if (specialPart != null) ObjectPool<RichSpecial>.Release(ref specialPart);
|
||||
}
|
||||
|
||||
Clear();
|
||||
|
||||
// Initialize new
|
||||
Endpoint = path.vectorPath[path.vectorPath.Count-1];
|
||||
|
||||
//Break path into parts
|
||||
for (int i = 0; i < nodes.Count; i++) {
|
||||
if (nodes[i] is TriangleMeshNode) {
|
||||
var graph = AstarData.GetGraph(nodes[i]) as NavmeshBase;
|
||||
if (graph == null) throw new System.Exception("Found a TriangleMeshNode that was not in a NavmeshBase graph");
|
||||
|
||||
RichFunnel f = ObjectPool<RichFunnel>.Claim().Initialize(this, graph);
|
||||
|
||||
f.funnelSimplification = simplificationMode;
|
||||
|
||||
int sIndex = i;
|
||||
uint currentGraphIndex = nodes[sIndex].GraphIndex;
|
||||
|
||||
|
||||
for (; i < nodes.Count; i++) {
|
||||
if (nodes[i].GraphIndex != currentGraphIndex && !(nodes[i] is NodeLink3Node)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
i--;
|
||||
|
||||
if (sIndex == 0) {
|
||||
f.exactStart = path.vectorPath[0];
|
||||
} else {
|
||||
f.exactStart = (Vector3)nodes[mergePartEndpoints ? sIndex-1 : sIndex].position;
|
||||
}
|
||||
|
||||
if (i == nodes.Count-1) {
|
||||
f.exactEnd = path.vectorPath[path.vectorPath.Count-1];
|
||||
} else {
|
||||
f.exactEnd = (Vector3)nodes[mergePartEndpoints ? i+1 : i].position;
|
||||
}
|
||||
|
||||
f.BuildFunnelCorridor(nodes, sIndex, i);
|
||||
|
||||
parts.Add(f);
|
||||
} else if (nodes[i] is LinkNode nl) {
|
||||
int sIndex = i;
|
||||
uint currentGraphIndex = nodes[sIndex].GraphIndex;
|
||||
|
||||
while (i < nodes.Count && nodes[i].GraphIndex == currentGraphIndex) i++;
|
||||
i--;
|
||||
|
||||
if (i - sIndex > 1) {
|
||||
throw new System.Exception("NodeLink2 path length greater than two (2) nodes. " + (i - sIndex));
|
||||
} else if (i - sIndex == 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
|
||||
continue;
|
||||
}
|
||||
Assert.AreEqual(nl.linkConcrete, (nodes[i] as LinkNode).linkConcrete);
|
||||
|
||||
RichSpecial rps = ObjectPool<RichSpecial>.Claim().Initialize(nl.linkConcrete.GetTracer(nl));
|
||||
parts.Add(rps);
|
||||
} else if (!(nodes[i] is PointNode)) {
|
||||
// Some other graph type which we do not have support for
|
||||
throw new System.InvalidOperationException("The RichAI movment script can only be used on recast/navmesh graphs. A node of type " + nodes[i].GetType().Name + " was in the path.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 Endpoint { get; private set; }
|
||||
|
||||
/// <summary>True if we have completed (called NextPart for) the last part in the path</summary>
|
||||
public bool CompletedAllParts {
|
||||
get {
|
||||
return currentPart >= parts.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>True if we are traversing the last part of the path</summary>
|
||||
public bool IsLastPart {
|
||||
get {
|
||||
return currentPart >= parts.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
public void NextPart () {
|
||||
currentPart = Mathf.Min(currentPart + 1, parts.Count);
|
||||
}
|
||||
|
||||
public RichPathPart GetCurrentPart () {
|
||||
if (parts.Count == 0) return null;
|
||||
return currentPart < parts.Count ? parts[currentPart] : parts[parts.Count - 1];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the buffer with the remaining path.
|
||||
/// See: <see cref="Pathfinding.IAstarAI.GetRemainingPath"/>
|
||||
/// </summary>
|
||||
public void GetRemainingPath (List<Vector3> buffer, List<PathPartWithLinkInfo> partsBuffer, Vector3 currentPosition, out bool requiresRepath) {
|
||||
buffer.Clear();
|
||||
buffer.Add(currentPosition);
|
||||
requiresRepath = false;
|
||||
for (int i = currentPart; i < parts.Count; i++) {
|
||||
var part = parts[i];
|
||||
if (part is RichFunnel funnel) {
|
||||
bool lastCorner;
|
||||
var startIndex = buffer.Count;
|
||||
if (i != 0) buffer.Add(funnel.exactStart);
|
||||
funnel.Update(i == 0 ? currentPosition : funnel.exactStart, buffer, int.MaxValue, out lastCorner, out requiresRepath);
|
||||
if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo(startIndex, buffer.Count-1));
|
||||
if (requiresRepath) {
|
||||
return;
|
||||
}
|
||||
} else if (part is RichSpecial rs) {
|
||||
// By adding all points above the link will look like just a stright line, which is reasonable
|
||||
// The part's start/end indices refer to the last point in previous part and first point in the next part, respectively
|
||||
if (partsBuffer != null) partsBuffer.Add(new PathPartWithLinkInfo(buffer.Count-1, buffer.Count, rs.nodeLink));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class RichPathPart : IAstarPooledObject {
|
||||
public abstract void OnEnterPool();
|
||||
}
|
||||
|
||||
public class RichFunnel : RichPathPart {
|
||||
readonly List<Vector3> left;
|
||||
readonly List<Vector3> right;
|
||||
List<TriangleMeshNode> nodes;
|
||||
public Vector3 exactStart;
|
||||
public Vector3 exactEnd;
|
||||
NavmeshBase graph;
|
||||
int currentNode;
|
||||
Vector3 currentPosition;
|
||||
int checkForDestroyedNodesCounter;
|
||||
RichPath path;
|
||||
int[] triBuffer = new int[3];
|
||||
|
||||
/// <summary>Post process the funnel corridor or not</summary>
|
||||
public bool funnelSimplification = true;
|
||||
|
||||
public RichFunnel () {
|
||||
left = Pathfinding.Pooling.ListPool<Vector3>.Claim();
|
||||
right = Pathfinding.Pooling.ListPool<Vector3>.Claim();
|
||||
nodes = new List<TriangleMeshNode>();
|
||||
this.graph = null;
|
||||
}
|
||||
|
||||
/// <summary>Works like a constructor, but can be used even for pooled objects. Returns this for easy chaining</summary>
|
||||
public RichFunnel Initialize (RichPath path, NavmeshBase graph) {
|
||||
if (graph == null) throw new System.ArgumentNullException("graph");
|
||||
if (this.graph != null) throw new System.InvalidOperationException("Trying to initialize an already initialized object. " + graph);
|
||||
|
||||
this.graph = graph;
|
||||
this.path = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public override void OnEnterPool () {
|
||||
left.Clear();
|
||||
right.Clear();
|
||||
nodes.Clear();
|
||||
graph = null;
|
||||
currentNode = 0;
|
||||
checkForDestroyedNodesCounter = 0;
|
||||
}
|
||||
|
||||
public TriangleMeshNode CurrentNode {
|
||||
get {
|
||||
var node = nodes[currentNode];
|
||||
if (!node.Destroyed) {
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build a funnel corridor from a node list slice.
|
||||
/// The nodes are assumed to be of type TriangleMeshNode.
|
||||
/// </summary>
|
||||
/// <param name="nodes">Nodes to build the funnel corridor from</param>
|
||||
/// <param name="start">Start index in the nodes list</param>
|
||||
/// <param name="end">End index in the nodes list, this index is inclusive</param>
|
||||
public void BuildFunnelCorridor (List<GraphNode> nodes, int start, int end) {
|
||||
//Make sure start and end points are on the correct nodes
|
||||
exactStart = (nodes[start] as MeshNode).ClosestPointOnNode(exactStart);
|
||||
exactEnd = (nodes[end] as MeshNode).ClosestPointOnNode(exactEnd);
|
||||
|
||||
left.Clear();
|
||||
right.Clear();
|
||||
left.Add(exactStart);
|
||||
right.Add(exactStart);
|
||||
|
||||
this.nodes.Clear();
|
||||
|
||||
if (funnelSimplification) {
|
||||
List<GraphNode> tmp = ListPool<GraphNode>.Claim(end-start);
|
||||
|
||||
var tagPenalties = path.seeker != null ? path.seeker.tagPenalties : Path.ZeroTagPenalties;
|
||||
var traversableTags = path.seeker != null ? path.seeker.traversableTags : -1;
|
||||
|
||||
Funnel.Simplify(new Funnel.PathPart {
|
||||
startIndex = start,
|
||||
endIndex = end,
|
||||
startPoint = exactStart,
|
||||
endPoint = exactEnd,
|
||||
type = Funnel.PartType.NodeSequence,
|
||||
}, graph, nodes, tmp, tagPenalties, traversableTags);
|
||||
|
||||
if (this.nodes.Capacity < tmp.Count) this.nodes.Capacity = tmp.Count;
|
||||
|
||||
for (int i = 0; i < tmp.Count; i++) {
|
||||
// Guaranteed to be TriangleMeshNodes since they are all in the same graph
|
||||
var node = tmp[i] as TriangleMeshNode;
|
||||
if (node != null) this.nodes.Add(node);
|
||||
}
|
||||
|
||||
ListPool<GraphNode>.Release(ref tmp);
|
||||
} else {
|
||||
if (this.nodes.Capacity < end-start) this.nodes.Capacity = (end-start);
|
||||
for (int i = start; i <= end; i++) {
|
||||
// Guaranteed to be TriangleMeshNodes since they are all in the same graph
|
||||
var node = nodes[i] as TriangleMeshNode;
|
||||
if (node != null) this.nodes.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.nodes.Count-1; i++) {
|
||||
if (this.nodes[i].GetPortal(this.nodes[i+1], out var leftP, out var rightP)) {
|
||||
left.Add(leftP);
|
||||
right.Add(rightP);
|
||||
} else {
|
||||
// Can happen in case custom connections have been added
|
||||
left.Add((Vector3)this.nodes[i].position);
|
||||
right.Add((Vector3)this.nodes[i].position);
|
||||
left.Add((Vector3)this.nodes[i+1].position);
|
||||
right.Add((Vector3)this.nodes[i+1].position);
|
||||
}
|
||||
}
|
||||
|
||||
left.Add(exactEnd);
|
||||
right.Add(exactEnd);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Split funnel at node index splitIndex and throw the nodes up to that point away and replace with prefix.
|
||||
/// Used when the AI has happened to get sidetracked and entered a node outside the funnel.
|
||||
/// </summary>
|
||||
void UpdateFunnelCorridor (int splitIndex, List<TriangleMeshNode> prefix) {
|
||||
nodes.RemoveRange(0, splitIndex);
|
||||
nodes.InsertRange(0, prefix);
|
||||
|
||||
left.Clear();
|
||||
right.Clear();
|
||||
left.Add(exactStart);
|
||||
right.Add(exactStart);
|
||||
|
||||
for (int i = 0; i < nodes.Count-1; i++) {
|
||||
if (nodes[i].GetPortal(nodes[i+1], out var leftP, out var rightP)) {
|
||||
left.Add(leftP);
|
||||
right.Add(rightP);
|
||||
}
|
||||
}
|
||||
|
||||
left.Add(exactEnd);
|
||||
right.Add(exactEnd);
|
||||
}
|
||||
|
||||
/// <summary>True if any node in the path is destroyed</summary>
|
||||
bool CheckForDestroyedNodes () {
|
||||
// Loop through all nodes and check if they are destroyed
|
||||
// If so, we really need a recalculation of our path quickly
|
||||
// since there might be an obstacle blocking our path after
|
||||
// a graph update or something similar
|
||||
for (int i = 0, t = nodes.Count; i < t; i++) {
|
||||
if (nodes[i].Destroyed) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Approximate distance (as the crow flies) to the endpoint of this path part.
|
||||
/// See: <see cref="exactEnd"/>
|
||||
/// </summary>
|
||||
public float DistanceToEndOfPath {
|
||||
get {
|
||||
var currentNode = CurrentNode;
|
||||
Vector3 closestOnNode = currentNode != null? currentNode.ClosestPointOnNode(currentPosition) : currentPosition;
|
||||
return (exactEnd - closestOnNode).magnitude;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the position to the navmesh and repairs the path if the agent has moved slightly outside it.
|
||||
/// You should not call this method with anything other than the agent's position.
|
||||
/// </summary>
|
||||
public Vector3 ClampToNavmesh (Vector3 position) {
|
||||
if (path.transform != null) position = path.transform.InverseTransform(position);
|
||||
UnityEngine.Assertions.Assert.IsFalse(float.IsNaN(position.x));
|
||||
ClampToNavmeshInternal(ref position);
|
||||
if (path.transform != null) position = path.transform.Transform(position);
|
||||
UnityEngine.Assertions.Assert.IsFalse(float.IsNaN(position.x));
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Find the next points to move towards and clamp the position to the navmesh.
|
||||
///
|
||||
/// Returns: The position of the agent clamped to make sure it is inside the navmesh.
|
||||
/// </summary>
|
||||
/// <param name="position">The position of the agent.</param>
|
||||
/// <param name="buffer">Will be filled with up to numCorners points which are the next points in the path towards the target.</param>
|
||||
/// <param name="numCorners">See buffer.</param>
|
||||
/// <param name="lastCorner">True if the buffer contains the end point of the path.</param>
|
||||
/// <param name="requiresRepath">True if nodes along the path have been destroyed and a path recalculation is necessary.</param>
|
||||
public Vector3 Update (Vector3 position, List<Vector3> buffer, int numCorners, out bool lastCorner, out bool requiresRepath) {
|
||||
if (path.transform != null) position = path.transform.InverseTransform(position);
|
||||
UnityEngine.Assertions.Assert.IsFalse(float.IsNaN(position.x));
|
||||
|
||||
lastCorner = false;
|
||||
requiresRepath = false;
|
||||
|
||||
// Only check for destroyed nodes every 10 frames
|
||||
if (checkForDestroyedNodesCounter >= 10) {
|
||||
checkForDestroyedNodesCounter = 0;
|
||||
requiresRepath |= CheckForDestroyedNodes();
|
||||
} else {
|
||||
checkForDestroyedNodesCounter++;
|
||||
}
|
||||
|
||||
bool nodesDestroyed = ClampToNavmeshInternal(ref position);
|
||||
|
||||
currentPosition = position;
|
||||
|
||||
if (nodesDestroyed) {
|
||||
// Some nodes on the path have been destroyed
|
||||
// we need to recalculate the path immediately
|
||||
requiresRepath = true;
|
||||
lastCorner = false;
|
||||
buffer.Add(position);
|
||||
} else if (!FindNextCorners(position, currentNode, buffer, numCorners, out lastCorner)) {
|
||||
Debug.LogError("Failed to find next corners in the path");
|
||||
buffer.Add(position);
|
||||
}
|
||||
|
||||
if (path.transform != null) {
|
||||
for (int i = 0; i < buffer.Count; i++) {
|
||||
buffer[i] = path.transform.Transform(buffer[i]);
|
||||
}
|
||||
|
||||
position = path.transform.Transform(position);
|
||||
UnityEngine.Assertions.Assert.IsFalse(float.IsNaN(position.x));
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
/// <summary>Cached object to avoid unnecessary allocations</summary>
|
||||
static Queue<TriangleMeshNode> navmeshClampQueue = new Queue<TriangleMeshNode>();
|
||||
/// <summary>Cached object to avoid unnecessary allocations</summary>
|
||||
static List<TriangleMeshNode> navmeshClampList = new List<TriangleMeshNode>();
|
||||
/// <summary>Cached object to avoid unnecessary allocations</summary>
|
||||
static Dictionary<TriangleMeshNode, TriangleMeshNode> navmeshClampDict = new Dictionary<TriangleMeshNode, TriangleMeshNode>();
|
||||
|
||||
/// <summary>
|
||||
/// Searches for the node the agent is inside.
|
||||
/// This will also clamp the position to the navmesh
|
||||
/// and repair the funnel cooridor if the agent moves slightly outside it.
|
||||
///
|
||||
/// Returns: True if nodes along the path have been destroyed so that a path recalculation is required
|
||||
/// </summary>
|
||||
bool ClampToNavmeshInternal (ref Vector3 position) {
|
||||
var previousNode = nodes[currentNode];
|
||||
|
||||
if (previousNode.Destroyed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if we are in the same node as we were in during the last frame and otherwise do a more extensive search
|
||||
if (previousNode.ContainsPoint(position)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This part of the code is relatively seldom called
|
||||
// Most of the time we are still on the same node as during the previous frame
|
||||
|
||||
var que = navmeshClampQueue;
|
||||
var allVisited = navmeshClampList;
|
||||
var parent = navmeshClampDict;
|
||||
previousNode.TemporaryFlag1 = true;
|
||||
parent[previousNode] = null;
|
||||
que.Enqueue(previousNode);
|
||||
allVisited.Add(previousNode);
|
||||
|
||||
float bestDistance = float.PositiveInfinity;
|
||||
Vector3 bestPoint = position;
|
||||
TriangleMeshNode bestNode = null;
|
||||
|
||||
while (que.Count > 0) {
|
||||
var node = que.Dequeue();
|
||||
|
||||
// Snap to the closest point in XZ space (keep the Y coordinate)
|
||||
// If we would have snapped to the closest point in 3D space, the agent
|
||||
// might slow down when traversing slopes
|
||||
var closest = node.ClosestPointOnNodeXZ(position);
|
||||
var dist = VectorMath.MagnitudeXZ(closest - position);
|
||||
|
||||
// Check if this node is any closer than the previous best node.
|
||||
// Allow for a small margin to both avoid floating point errors and to allow
|
||||
// moving past very small local minima.
|
||||
if (dist <= bestDistance * 1.05f + 0.001f) {
|
||||
if (dist < bestDistance) {
|
||||
bestDistance = dist;
|
||||
bestPoint = closest;
|
||||
bestNode = node;
|
||||
}
|
||||
|
||||
for (int i = 0; i < node.connections.Length; i++) {
|
||||
if (!node.connections[i].isOutgoing) continue;
|
||||
var neighbour = node.connections[i].node as TriangleMeshNode;
|
||||
if (neighbour != null && !neighbour.TemporaryFlag1) {
|
||||
neighbour.TemporaryFlag1 = true;
|
||||
parent[neighbour] = node;
|
||||
que.Enqueue(neighbour);
|
||||
allVisited.Add(neighbour);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UnityEngine.Assertions.Assert.IsNotNull(bestNode);
|
||||
|
||||
for (int i = 0; i < allVisited.Count; i++) allVisited[i].TemporaryFlag1 = false;
|
||||
allVisited.ClearFast();
|
||||
|
||||
var closestNodeInPath = nodes.IndexOf(bestNode);
|
||||
|
||||
// Move the x and z coordinates of the chararacter but not the y coordinate
|
||||
// because the navmesh surface may not line up with the ground
|
||||
position.x = bestPoint.x;
|
||||
position.z = bestPoint.z;
|
||||
|
||||
// Check if the closest node
|
||||
// was on the path already or if we need to adjust it
|
||||
if (closestNodeInPath == -1) {
|
||||
// Reuse this list, because why not.
|
||||
var prefix = navmeshClampList;
|
||||
|
||||
while (closestNodeInPath == -1) {
|
||||
prefix.Add(bestNode);
|
||||
bestNode = parent[bestNode];
|
||||
closestNodeInPath = nodes.IndexOf(bestNode);
|
||||
}
|
||||
|
||||
// We have found a node containing the position, but it is outside the funnel
|
||||
// Recalculate the funnel to include this node
|
||||
exactStart = position;
|
||||
UpdateFunnelCorridor(closestNodeInPath, prefix);
|
||||
|
||||
prefix.ClearFast();
|
||||
|
||||
// Restart from the first node in the updated path
|
||||
currentNode = 0;
|
||||
} else {
|
||||
currentNode = closestNodeInPath;
|
||||
}
|
||||
|
||||
parent.Clear();
|
||||
// Do a quick check to see if the next node in the path has been destroyed
|
||||
// If that is the case then we should plan a new path immediately
|
||||
return currentNode + 1 < nodes.Count && nodes[currentNode+1].Destroyed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fill wallBuffer with all navmesh wall segments close to the current position.
|
||||
/// A wall segment is a node edge which is not shared by any other neighbour node, i.e an outer edge on the navmesh.
|
||||
/// </summary>
|
||||
public void FindWalls (List<Vector3> wallBuffer, float range) {
|
||||
FindWalls(currentNode, wallBuffer, currentPosition, range);
|
||||
}
|
||||
|
||||
void FindWalls (int nodeIndex, List<Vector3> wallBuffer, Vector3 position, float range) {
|
||||
if (range <= 0) return;
|
||||
|
||||
bool negAbort = false;
|
||||
bool posAbort = false;
|
||||
|
||||
range *= range;
|
||||
|
||||
//Looping as 0,-1,1,-2,2,-3,3,-4,4 etc. Avoids code duplication by keeping it to one loop instead of two
|
||||
for (int i = 0; !negAbort || !posAbort; i = i < 0 ? -i : -i-1) {
|
||||
if (i < 0 && negAbort) continue;
|
||||
if (i > 0 && posAbort) continue;
|
||||
|
||||
if (i < 0 && nodeIndex+i < 0) {
|
||||
negAbort = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i > 0 && nodeIndex+i >= nodes.Count) {
|
||||
posAbort = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
TriangleMeshNode prev = nodeIndex+i-1 < 0 ? null : nodes[nodeIndex+i-1];
|
||||
TriangleMeshNode node = nodes[nodeIndex+i];
|
||||
TriangleMeshNode next = nodeIndex+i+1 >= nodes.Count ? null : nodes[nodeIndex+i+1];
|
||||
|
||||
if (node.Destroyed) {
|
||||
break;
|
||||
}
|
||||
|
||||
var dir = node.ClosestPointOnNodeXZ(position)-position;
|
||||
dir.y = 0;
|
||||
if (dir.sqrMagnitude > range) {
|
||||
if (i < 0) negAbort = true;
|
||||
else posAbort = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int j = 0; j < 3; j++) triBuffer[j] = 0;
|
||||
|
||||
for (int j = 0; j < node.connections.Length; j++) {
|
||||
var other = node.connections[j].node as TriangleMeshNode;
|
||||
if (other == null) continue;
|
||||
|
||||
int va = -1;
|
||||
for (int a = 0; a < 3; a++) {
|
||||
for (int b = 0; b < 3; b++) {
|
||||
if (node.GetVertex(a) == other.GetVertex((b+1) % 3) && node.GetVertex((a+1) % 3) == other.GetVertex(b)) {
|
||||
va = a;
|
||||
a = 3;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (va == -1) {
|
||||
//No direct connection
|
||||
} else {
|
||||
triBuffer[va] = other == prev || other == next ? 2 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
//Tribuffer values
|
||||
// 0 : Navmesh border, outer edge
|
||||
// 1 : Inner edge, to node inside funnel
|
||||
// 2 : Inner edge, to node outside funnel
|
||||
if (triBuffer[j] == 0) {
|
||||
//Add edge to list of walls
|
||||
wallBuffer.Add((Vector3)node.GetVertex(j));
|
||||
wallBuffer.Add((Vector3)node.GetVertex((j+1) % 3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (path.transform != null) {
|
||||
for (int i = 0; i < wallBuffer.Count; i++) {
|
||||
wallBuffer[i] = path.transform.Transform(wallBuffer[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool FindNextCorners (Vector3 origin, int startIndex, List<Vector3> funnelPath, int numCorners, out bool lastCorner) {
|
||||
lastCorner = false;
|
||||
|
||||
if (left == null) throw new System.Exception("left list is null");
|
||||
if (right == null) throw new System.Exception("right list is null");
|
||||
if (funnelPath == null) throw new System.ArgumentNullException("funnelPath");
|
||||
|
||||
if (left.Count != right.Count) throw new System.ArgumentException("left and right lists must have equal length");
|
||||
|
||||
int diagonalCount = left.Count;
|
||||
|
||||
if (diagonalCount == 0) throw new System.ArgumentException("no diagonals");
|
||||
|
||||
if (diagonalCount-startIndex < 3) {
|
||||
//Direct path
|
||||
funnelPath.Add(left[diagonalCount-1]);
|
||||
lastCorner = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
#if ASTARDEBUG
|
||||
for (int i = startIndex; i < left.Count-1; i++) {
|
||||
Debug.DrawLine(left[i], left[i+1], Color.red);
|
||||
Debug.DrawLine(right[i], right[i+1], Color.magenta);
|
||||
Debug.DrawRay(right[i], Vector3.up, Color.magenta);
|
||||
}
|
||||
for (int i = 0; i < left.Count; i++) {
|
||||
Debug.DrawLine(right[i], left[i], Color.cyan);
|
||||
}
|
||||
#endif
|
||||
|
||||
//Remove identical vertices
|
||||
while (left[startIndex+1] == left[startIndex+2] && right[startIndex+1] == right[startIndex+2]) {
|
||||
//System.Console.WriteLine ("Removing identical left and right");
|
||||
//left.RemoveAt (1);
|
||||
//right.RemoveAt (1);
|
||||
startIndex++;
|
||||
|
||||
if (diagonalCount-startIndex <= 3) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 swPoint = left[startIndex+2];
|
||||
if (swPoint == left[startIndex+1]) {
|
||||
swPoint = right[startIndex+2];
|
||||
}
|
||||
|
||||
|
||||
//Test
|
||||
while (VectorMath.IsColinearXZ(origin, left[startIndex+1], right[startIndex+1]) || VectorMath.RightOrColinearXZ(left[startIndex+1], right[startIndex+1], swPoint) == VectorMath.RightOrColinearXZ(left[startIndex+1], right[startIndex+1], origin)) {
|
||||
#if ASTARDEBUG
|
||||
Debug.DrawLine(left[startIndex+1], right[startIndex+1], new Color(0, 0, 0, 0.5F));
|
||||
Debug.DrawLine(origin, swPoint, new Color(0, 0, 0, 0.5F));
|
||||
#endif
|
||||
//left.RemoveAt (1);
|
||||
//right.RemoveAt (1);
|
||||
startIndex++;
|
||||
|
||||
if (diagonalCount-startIndex < 3) {
|
||||
//Debug.Log ("#2 " + left.Count + " - " + startIndex + " = " + (left.Count-startIndex));
|
||||
//Direct path
|
||||
funnelPath.Add(left[diagonalCount-1]);
|
||||
lastCorner = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
swPoint = left[startIndex+2];
|
||||
if (swPoint == left[startIndex+1]) {
|
||||
swPoint = right[startIndex+2];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//funnelPath.Add (origin);
|
||||
|
||||
Vector3 portalApex = origin;
|
||||
Vector3 portalLeft = left[startIndex+1];
|
||||
Vector3 portalRight = right[startIndex+1];
|
||||
|
||||
int apexIndex = startIndex+0;
|
||||
int rightIndex = startIndex+1;
|
||||
int leftIndex = startIndex+1;
|
||||
|
||||
for (int i = startIndex+2; i < diagonalCount; i++) {
|
||||
if (funnelPath.Count >= numCorners) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (funnelPath.Count > 2000) {
|
||||
Debug.LogWarning("Avoiding infinite loop. Remove this check if you have this long paths.");
|
||||
break;
|
||||
}
|
||||
|
||||
Vector3 pLeft = left[i];
|
||||
Vector3 pRight = right[i];
|
||||
|
||||
/*Debug.DrawLine (portalApex,portalLeft,Color.red);
|
||||
* Debug.DrawLine (portalApex,portalRight,Color.yellow);
|
||||
* Debug.DrawLine (portalApex,left,Color.cyan);
|
||||
* Debug.DrawLine (portalApex,right,Color.cyan);*/
|
||||
|
||||
if (VectorMath.SignedTriangleAreaTimes2XZ(portalApex, portalRight, pRight) >= 0) {
|
||||
if (portalApex == portalRight || VectorMath.SignedTriangleAreaTimes2XZ(portalApex, portalLeft, pRight) <= 0) {
|
||||
portalRight = pRight;
|
||||
rightIndex = i;
|
||||
} else {
|
||||
funnelPath.Add(portalLeft);
|
||||
portalApex = portalLeft;
|
||||
apexIndex = leftIndex;
|
||||
|
||||
portalLeft = portalApex;
|
||||
portalRight = portalApex;
|
||||
|
||||
leftIndex = apexIndex;
|
||||
rightIndex = apexIndex;
|
||||
|
||||
i = apexIndex;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (VectorMath.SignedTriangleAreaTimes2XZ(portalApex, portalLeft, pLeft) <= 0) {
|
||||
if (portalApex == portalLeft || VectorMath.SignedTriangleAreaTimes2XZ(portalApex, portalRight, pLeft) >= 0) {
|
||||
portalLeft = pLeft;
|
||||
leftIndex = i;
|
||||
} else {
|
||||
funnelPath.Add(portalRight);
|
||||
portalApex = portalRight;
|
||||
apexIndex = rightIndex;
|
||||
|
||||
portalLeft = portalApex;
|
||||
portalRight = portalApex;
|
||||
|
||||
leftIndex = apexIndex;
|
||||
rightIndex = apexIndex;
|
||||
|
||||
i = apexIndex;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lastCorner = true;
|
||||
funnelPath.Add(left[diagonalCount-1]);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public struct FakeTransform {
|
||||
public Vector3 position;
|
||||
public Quaternion rotation;
|
||||
}
|
||||
|
||||
public class RichSpecial : RichPathPart {
|
||||
public OffMeshLinks.OffMeshLinkTracer nodeLink;
|
||||
public FakeTransform first => new FakeTransform { position = nodeLink.relativeStart, rotation = nodeLink.isReverse ? nodeLink.link.end.rotation : nodeLink.link.start.rotation };
|
||||
public FakeTransform second => new FakeTransform { position = nodeLink.relativeEnd, rotation = nodeLink.isReverse ? nodeLink.link.start.rotation : nodeLink.link.end.rotation };
|
||||
public bool reverse => nodeLink.isReverse;
|
||||
|
||||
public override void OnEnterPool () {
|
||||
nodeLink = default;
|
||||
}
|
||||
|
||||
/// <summary>Works like a constructor, but can be used even for pooled objects. Returns this for easy chaining</summary>
|
||||
public RichSpecial Initialize (OffMeshLinks.OffMeshLinkTracer nodeLink) {
|
||||
this.nodeLink = nodeLink;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
Packages/com.arongranberg.astar/Core/AI/RichPath.cs.meta
Normal file
8
Packages/com.arongranberg.astar/Core/AI/RichPath.cs.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6865b0d3859fe4641ad0839b829e00d2
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
772
Packages/com.arongranberg.astar/Core/AI/Seeker.cs
Normal file
772
Packages/com.arongranberg.astar/Core/AI/Seeker.cs
Normal file
@@ -0,0 +1,772 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
using Pathfinding.Util;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.Drawing;
|
||||
|
||||
/// <summary>
|
||||
/// Handles path calls for a single unit.
|
||||
///
|
||||
/// This is a component which is meant to be attached to a single unit (AI, Robot, Player, whatever) to handle its pathfinding calls.
|
||||
/// It also handles post-processing of paths using modifiers.
|
||||
///
|
||||
/// See: calling-pathfinding (view in online documentation for working links)
|
||||
/// See: modifiers (view in online documentation for working links)
|
||||
/// </summary>
|
||||
[AddComponentMenu("Pathfinding/Seeker")]
|
||||
[DisallowMultipleComponent]
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/seeker.html")]
|
||||
public class Seeker : VersionedMonoBehaviour {
|
||||
/// <summary>
|
||||
/// Enables drawing of the last calculated path using Gizmos.
|
||||
/// The path will show up in green.
|
||||
///
|
||||
/// See: OnDrawGizmos
|
||||
/// </summary>
|
||||
public bool drawGizmos = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enables drawing of the non-postprocessed path using Gizmos.
|
||||
/// The path will show up in orange.
|
||||
///
|
||||
/// Requires that <see cref="drawGizmos"/> is true.
|
||||
///
|
||||
/// This will show the path before any post processing such as smoothing is applied.
|
||||
///
|
||||
/// See: drawGizmos
|
||||
/// See: OnDrawGizmos
|
||||
/// </summary>
|
||||
public bool detailedGizmos;
|
||||
|
||||
/// <summary>Path modifier which tweaks the start and end points of a path</summary>
|
||||
[HideInInspector]
|
||||
public StartEndModifier startEndModifier = new StartEndModifier();
|
||||
|
||||
/// <summary>
|
||||
/// The tags which the Seeker can traverse.
|
||||
///
|
||||
/// Note: This field is a bitmask.
|
||||
/// See: bitmasks (view in online documentation for working links)
|
||||
/// </summary>
|
||||
[HideInInspector]
|
||||
public int traversableTags = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Penalties for each tag.
|
||||
/// Tag 0 which is the default tag, will have added a penalty of tagPenalties[0].
|
||||
/// These should only be positive values since the A* algorithm cannot handle negative penalties.
|
||||
///
|
||||
/// The length of this array should be exactly 32, one for each tag.
|
||||
///
|
||||
/// See: Pathfinding.Path.tagPenalties
|
||||
/// </summary>
|
||||
[HideInInspector]
|
||||
public int[] tagPenalties = new int[32];
|
||||
|
||||
/// <summary>
|
||||
/// Graphs that this Seeker can use.
|
||||
/// This field determines which graphs will be considered when searching for the start and end nodes of a path.
|
||||
/// It is useful in numerous situations, for example if you want to make one graph for small units and one graph for large units.
|
||||
///
|
||||
/// This is a bitmask so if you for example want to make the agent only use graph index 3 then you can set this to:
|
||||
/// <code> seeker.graphMask = 1 << 3; </code>
|
||||
///
|
||||
/// See: bitmasks (view in online documentation for working links)
|
||||
///
|
||||
/// Note that this field only stores which graph indices that are allowed. This means that if the graphs change their ordering
|
||||
/// then this mask may no longer be correct.
|
||||
///
|
||||
/// If you know the name of the graph you can use the <see cref="Pathfinding.GraphMask.FromGraphName"/> method:
|
||||
/// <code>
|
||||
/// GraphMask mask1 = GraphMask.FromGraphName("My Grid Graph");
|
||||
/// GraphMask mask2 = GraphMask.FromGraphName("My Other Grid Graph");
|
||||
///
|
||||
/// NNConstraint nn = NNConstraint.Walkable;
|
||||
///
|
||||
/// nn.graphMask = mask1 | mask2;
|
||||
///
|
||||
/// // Find the node closest to somePoint which is either in 'My Grid Graph' OR in 'My Other Grid Graph'
|
||||
/// var info = AstarPath.active.GetNearest(somePoint, nn);
|
||||
/// </code>
|
||||
///
|
||||
/// Some overloads of the <see cref="StartPath"/> methods take a graphMask parameter. If those overloads are used then they
|
||||
/// will override the graph mask for that path request.
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// See: multiple-agent-types (view in online documentation for working links)
|
||||
/// </summary>
|
||||
[HideInInspector]
|
||||
public GraphMask graphMask = GraphMask.everything;
|
||||
|
||||
/// <summary>
|
||||
/// Custom traversal provider to calculate which nodes are traversable and their penalties.
|
||||
///
|
||||
/// This can be used to override the built-in pathfinding logic.
|
||||
///
|
||||
/// <code>
|
||||
/// seeker.traversalProvider = new MyCustomTraversalProvider();
|
||||
/// </code>
|
||||
///
|
||||
/// See: traversal_provider (view in online documentation for working links)
|
||||
/// </summary>
|
||||
public ITraversalProvider traversalProvider;
|
||||
|
||||
/// <summary>Used for serialization backwards compatibility</summary>
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("graphMask")]
|
||||
int graphMaskCompatibility = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Callback for when a path is completed.
|
||||
/// Movement scripts should register to this delegate.
|
||||
/// A temporary callback can also be set when calling StartPath, but that delegate will only be called for that path
|
||||
///
|
||||
/// <code>
|
||||
/// public void Start () {
|
||||
/// // Assumes a Seeker component is attached to the GameObject
|
||||
/// Seeker seeker = GetComponent<Seeker>();
|
||||
///
|
||||
/// // seeker.pathCallback is a OnPathDelegate, we add the function OnPathComplete to it so it will be called whenever a path has finished calculating on that seeker
|
||||
/// seeker.pathCallback += OnPathComplete;
|
||||
/// }
|
||||
///
|
||||
/// public void OnPathComplete (Path p) {
|
||||
/// Debug.Log("This is called when a path is completed on the seeker attached to this GameObject");
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// Deprecated: Pass a callback every time to the StartPath method instead, or use ai.SetPath+ai.pathPending on the movement script. You can cache it in your own script if you want to avoid the GC allocation of creating a new delegate.
|
||||
/// </summary>
|
||||
[System.Obsolete("Pass a callback every time to the StartPath method instead, or use ai.SetPath+ai.pathPending on the movement script. You can cache it in your own script if you want to avoid the GC allocation of creating a new delegate.")]
|
||||
public OnPathDelegate pathCallback;
|
||||
|
||||
/// <summary>Called before pathfinding is started</summary>
|
||||
public OnPathDelegate preProcessPath;
|
||||
|
||||
/// <summary>Called after a path has been calculated, right before modifiers are executed.</summary>
|
||||
public OnPathDelegate postProcessPath;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>Used for drawing gizmos</summary>
|
||||
[System.NonSerialized]
|
||||
List<Vector3> lastCompletedVectorPath;
|
||||
|
||||
/// <summary>Used for drawing gizmos</summary>
|
||||
[System.NonSerialized]
|
||||
List<GraphNode> lastCompletedNodePath;
|
||||
#endif
|
||||
|
||||
/// <summary>The current path</summary>
|
||||
[System.NonSerialized]
|
||||
protected Path path;
|
||||
|
||||
/// <summary>Previous path. Used to draw gizmos</summary>
|
||||
[System.NonSerialized]
|
||||
private Path prevPath;
|
||||
|
||||
/// <summary>Cached delegate to avoid allocating one every time a path is started</summary>
|
||||
private readonly OnPathDelegate onPathDelegate;
|
||||
/// <summary>Cached delegate to avoid allocating one every time a path is started</summary>
|
||||
private readonly OnPathDelegate onPartialPathDelegate;
|
||||
|
||||
/// <summary>Temporary callback only called for the current path. This value is set by the StartPath functions</summary>
|
||||
private OnPathDelegate tmpPathCallback;
|
||||
|
||||
/// <summary>The path ID of the last path queried</summary>
|
||||
protected uint lastPathID;
|
||||
|
||||
/// <summary>Internal list of all modifiers</summary>
|
||||
readonly List<IPathModifier> modifiers = new List<IPathModifier>();
|
||||
|
||||
public enum ModifierPass {
|
||||
PreProcess,
|
||||
// An obsolete item occupied index 1 previously
|
||||
PostProcess = 2,
|
||||
}
|
||||
|
||||
public Seeker () {
|
||||
onPathDelegate = OnPathComplete;
|
||||
onPartialPathDelegate = OnPartialPathComplete;
|
||||
}
|
||||
|
||||
/// <summary>Initializes a few variables</summary>
|
||||
protected override void Awake () {
|
||||
base.Awake();
|
||||
startEndModifier.Awake(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Path that is currently being calculated or was last calculated.
|
||||
/// You should rarely have to use this. Instead get the path when the path callback is called.
|
||||
///
|
||||
/// See: <see cref="StartPath"/>
|
||||
/// </summary>
|
||||
public Path GetCurrentPath() => path;
|
||||
|
||||
/// <summary>
|
||||
/// Stop calculating the current path request.
|
||||
/// If this Seeker is currently calculating a path it will be canceled.
|
||||
/// The callback (usually to a method named OnPathComplete) will soon be called
|
||||
/// with a path that has the 'error' field set to true.
|
||||
///
|
||||
/// This does not stop the character from moving, it just aborts
|
||||
/// the path calculation.
|
||||
/// </summary>
|
||||
/// <param name="pool">If true then the path will be pooled when the pathfinding system is done with it.</param>
|
||||
public void CancelCurrentPathRequest (bool pool = true) {
|
||||
if (!IsDone()) {
|
||||
path.FailWithError("Canceled by script (Seeker.CancelCurrentPathRequest)");
|
||||
if (pool) {
|
||||
// Make sure the path has had its reference count incremented and decremented once.
|
||||
// If this is not done the system will think no pooling is used at all and will not pool the path.
|
||||
// The particular object that is used as the parameter (in this case 'path') doesn't matter at all
|
||||
// it just has to be *some* object.
|
||||
path.Claim(path);
|
||||
path.Release(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans up some variables.
|
||||
/// Releases any eventually claimed paths.
|
||||
/// Calls OnDestroy on the <see cref="startEndModifier"/>.
|
||||
///
|
||||
/// See: <see cref="ReleaseClaimedPath"/>
|
||||
/// See: <see cref="startEndModifier"/>
|
||||
/// </summary>
|
||||
void OnDestroy () {
|
||||
ReleaseClaimedPath();
|
||||
startEndModifier.OnDestroy(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases the path used for gizmos (if any).
|
||||
/// The seeker keeps the latest path claimed so it can draw gizmos.
|
||||
/// In some cases this might not be desireable and you want it released.
|
||||
/// In that case, you can call this method to release it (not that path gizmos will then not be drawn).
|
||||
///
|
||||
/// If you didn't understand anything from the description above, you probably don't need to use this method.
|
||||
///
|
||||
/// See: pooling (view in online documentation for working links)
|
||||
/// </summary>
|
||||
void ReleaseClaimedPath () {
|
||||
if (prevPath != null) {
|
||||
prevPath.Release(this, true);
|
||||
prevPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Called by modifiers to register themselves</summary>
|
||||
public void RegisterModifier (IPathModifier modifier) {
|
||||
modifiers.Add(modifier);
|
||||
|
||||
// Sort the modifiers based on their specified order
|
||||
modifiers.Sort((a, b) => a.Order.CompareTo(b.Order));
|
||||
}
|
||||
|
||||
/// <summary>Called by modifiers when they are disabled or destroyed</summary>
|
||||
public void DeregisterModifier (IPathModifier modifier) {
|
||||
modifiers.Remove(modifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Post-processes a path.
|
||||
/// This will run any modifiers attached to this GameObject on the path.
|
||||
/// This is identical to calling RunModifiers(ModifierPass.PostProcess, path)
|
||||
/// See: <see cref="RunModifiers"/>
|
||||
/// </summary>
|
||||
public void PostProcess (Path path) {
|
||||
RunModifiers(ModifierPass.PostProcess, path);
|
||||
}
|
||||
|
||||
/// <summary>Runs modifiers on a path</summary>
|
||||
public void RunModifiers (ModifierPass pass, Path path) {
|
||||
if (pass == ModifierPass.PreProcess) {
|
||||
if (preProcessPath != null) preProcessPath(path);
|
||||
|
||||
for (int i = 0; i < modifiers.Count; i++) modifiers[i].PreProcess(path);
|
||||
} else if (pass == ModifierPass.PostProcess) {
|
||||
Profiler.BeginSample("Running Path Modifiers");
|
||||
// Call delegates if they exist
|
||||
if (postProcessPath != null) postProcessPath(path);
|
||||
|
||||
// Loop through all modifiers and apply post processing
|
||||
for (int i = 0; i < modifiers.Count; i++) modifiers[i].Apply(path);
|
||||
Profiler.EndSample();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the current path done calculating.
|
||||
/// Returns true if the current <see cref="path"/> has been returned or if the <see cref="path"/> is null.
|
||||
///
|
||||
/// Note: Do not confuse this with Pathfinding.Path.IsDone. They usually return the same value, but not always.
|
||||
/// The path might be completely calculated, but has not yet been processed by the Seeker.
|
||||
///
|
||||
/// Inside the OnPathComplete callback this method will return true.
|
||||
///
|
||||
/// Version: Before version 4.2.13 this would return false inside the OnPathComplete callback. However, this behaviour was unintuitive.
|
||||
/// </summary>
|
||||
public bool IsDone() => path == null || path.PipelineState >= PathState.Returning;
|
||||
|
||||
/// <summary>Called when a path has completed</summary>
|
||||
void OnPathComplete (Path path) {
|
||||
OnPathComplete(path, true, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a path has completed.
|
||||
/// Will post process it and return it by calling <see cref="tmpPathCallback"/>
|
||||
/// </summary>
|
||||
void OnPathComplete (Path p, bool runModifiers, bool sendCallbacks) {
|
||||
if (p != null && p != path && sendCallbacks) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this == null || p == null || p != path)
|
||||
return;
|
||||
|
||||
if (!path.error && runModifiers) {
|
||||
// This will send the path for post processing to modifiers attached to this Seeker
|
||||
RunModifiers(ModifierPass.PostProcess, path);
|
||||
}
|
||||
|
||||
if (sendCallbacks) {
|
||||
p.Claim(this);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
lastCompletedNodePath = p.path;
|
||||
lastCompletedVectorPath = p.vectorPath;
|
||||
#endif
|
||||
|
||||
#pragma warning disable 618
|
||||
if (tmpPathCallback == null && pathCallback == null) {
|
||||
#if UNITY_EDITOR
|
||||
// This checks for a common error that people make when they upgrade from an older version
|
||||
// This will be removed in a future version to avoid the slight performance cost.
|
||||
if (TryGetComponent<IAstarAI>(out var ai)) {
|
||||
Debug.LogWarning("A path was calculated, but no callback was specified when calling StartPath. If you wanted a movement script to use this path, use <b>ai.SetPath</b> instead of calling StartPath on the Seeker directly. The path will be forwarded to the attached movement script, but this behavior will be removed in the future.", this);
|
||||
ai.SetPath(p);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
// This will send the path to the callback (if any) specified when calling StartPath
|
||||
if (tmpPathCallback != null) {
|
||||
tmpPathCallback(p);
|
||||
}
|
||||
|
||||
// This will send the path to any script which has registered to the callback
|
||||
if (pathCallback != null) {
|
||||
pathCallback(p);
|
||||
}
|
||||
}
|
||||
#pragma warning restore 618
|
||||
|
||||
// Note: it is important that #prevPath is kept alive (i.e. not pooled)
|
||||
// if we are drawing gizmos.
|
||||
// It is also important that #path is kept alive since it can be returned
|
||||
// from the GetCurrentPath method.
|
||||
// Since #path will be copied to #prevPath it is sufficient that #prevPath
|
||||
// is kept alive until it is replaced.
|
||||
|
||||
// Recycle the previous path to reduce the load on the GC
|
||||
if (prevPath != null) {
|
||||
prevPath.Release(this, true);
|
||||
}
|
||||
|
||||
prevPath = p;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called for each path in a MultiTargetPath.
|
||||
/// Only post processes the path, does not return it.
|
||||
/// </summary>
|
||||
void OnPartialPathComplete (Path p) {
|
||||
OnPathComplete(p, true, false);
|
||||
}
|
||||
|
||||
/// <summary>Called once for a MultiTargetPath. Only returns the path, does not post process.</summary>
|
||||
void OnMultiPathComplete (Path p) {
|
||||
OnPathComplete(p, false, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a path to be calculated.
|
||||
/// Since this method does not take a callback parameter, you should set the <see cref="pathCallback"/> field before calling this method.
|
||||
///
|
||||
/// <code>
|
||||
/// void Start () {
|
||||
/// // Get the seeker component attached to this GameObject
|
||||
/// var seeker = GetComponent<Seeker>();
|
||||
///
|
||||
/// // Schedule a new path request from the current position to a position 10 units forward.
|
||||
/// // When the path has been calculated, the OnPathComplete method will be called, unless it was canceled by another path request
|
||||
/// seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete);
|
||||
///
|
||||
/// // Note that the path is NOT calculated at this point
|
||||
/// // It has just been queued for calculation
|
||||
/// }
|
||||
///
|
||||
/// void OnPathComplete (Path path) {
|
||||
/// // The path is now calculated!
|
||||
///
|
||||
/// if (path.error) {
|
||||
/// Debug.LogError("Path failed: " + path.errorLog);
|
||||
/// return;
|
||||
/// }
|
||||
///
|
||||
/// // Cast the path to the path type we were using
|
||||
/// var abPath = path as ABPath;
|
||||
///
|
||||
/// // Draw the path in the scene view for 10 seconds
|
||||
/// for (int i = 0; i < abPath.vectorPath.Count - 1; i++) {
|
||||
/// Debug.DrawLine(abPath.vectorPath[i], abPath.vectorPath[i+1], Color.red, 10);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// Deprecated: Use <see cref="StartPath(Vector3,Vector3,OnPathDelegate)"/> instead.
|
||||
/// </summary>
|
||||
/// <param name="start">The start point of the path</param>
|
||||
/// <param name="end">The end point of the path</param>
|
||||
[System.Obsolete("Use the overload that takes a callback instead")]
|
||||
public Path StartPath (Vector3 start, Vector3 end) {
|
||||
return StartPath(start, end, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a path to be calculated.
|
||||
///
|
||||
/// The callback will be called when the path has been calculated (which may be several frames into the future).
|
||||
/// Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed)
|
||||
///
|
||||
/// <code>
|
||||
/// void Start () {
|
||||
/// // Get the seeker component attached to this GameObject
|
||||
/// var seeker = GetComponent<Seeker>();
|
||||
///
|
||||
/// // Schedule a new path request from the current position to a position 10 units forward.
|
||||
/// // When the path has been calculated, the OnPathComplete method will be called, unless it was canceled by another path request
|
||||
/// seeker.StartPath(transform.position, transform.position + Vector3.forward * 10, OnPathComplete);
|
||||
///
|
||||
/// // Note that the path is NOT calculated at this point
|
||||
/// // It has just been queued for calculation
|
||||
/// }
|
||||
///
|
||||
/// void OnPathComplete (Path path) {
|
||||
/// // The path is now calculated!
|
||||
///
|
||||
/// if (path.error) {
|
||||
/// Debug.LogError("Path failed: " + path.errorLog);
|
||||
/// return;
|
||||
/// }
|
||||
///
|
||||
/// // Cast the path to the path type we were using
|
||||
/// var abPath = path as ABPath;
|
||||
///
|
||||
/// // Draw the path in the scene view for 10 seconds
|
||||
/// for (int i = 0; i < abPath.vectorPath.Count - 1; i++) {
|
||||
/// Debug.DrawLine(abPath.vectorPath[i], abPath.vectorPath[i+1], Color.red, 10);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="start">The start point of the path</param>
|
||||
/// <param name="end">The end point of the path</param>
|
||||
/// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param>
|
||||
public Path StartPath (Vector3 start, Vector3 end, OnPathDelegate callback) {
|
||||
return StartPath(ABPath.Construct(start, end, null), callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a path to be calculated.
|
||||
///
|
||||
/// The callback will be called when the path has been calculated (which may be several frames into the future).
|
||||
/// Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed)
|
||||
///
|
||||
/// <code>
|
||||
/// // Schedule a path search that will only start searching the graphs with index 0 and 3
|
||||
/// seeker.StartPath(startPoint, endPoint, null, 1 << 0 | 1 << 3);
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="start">The start point of the path</param>
|
||||
/// <param name="end">The end point of the path</param>
|
||||
/// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param>
|
||||
/// <param name="graphMask">Mask used to specify which graphs should be searched for close nodes. See #Pathfinding.NNConstraint.graphMask. This will override the #graphMask on this Seeker.</param>
|
||||
public Path StartPath (Vector3 start, Vector3 end, OnPathDelegate callback, GraphMask graphMask) {
|
||||
return StartPath(ABPath.Construct(start, end, null), callback, graphMask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a path to be calculated.
|
||||
///
|
||||
/// Version: Since 4.1.x this method will no longer overwrite the graphMask on the path unless it is explicitly passed as a parameter (see other overloads of this method).
|
||||
///
|
||||
/// This overload takes no callback parameter. Instead, it is expected that you poll the path for completion, or block until it is completed.
|
||||
///
|
||||
/// See: <see cref="IsDone"/>
|
||||
/// See: <see cref="Path.WaitForPath"/>
|
||||
/// See: <see cref="Path.BlockUntilCalculated"/>
|
||||
///
|
||||
/// However, <see cref="Path.IsDone"/> should not be used with the Seeker component. This is because while the path itself may be calculated, the Seeker may not have had time to run post processing modifiers on the path yet.
|
||||
/// </summary>
|
||||
/// <param name="p">The path to start calculating</param>
|
||||
public Path StartPath (Path p) {
|
||||
return StartPath(p, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a path to be calculated.
|
||||
///
|
||||
/// The callback will be called when the path has been calculated (which may be several frames into the future).
|
||||
/// The callback will not be called if a new path request is started before this path request has been calculated.
|
||||
/// </summary>
|
||||
/// <param name="p">The path to start calculating</param>
|
||||
/// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param>
|
||||
public Path StartPath (Path p, OnPathDelegate callback) {
|
||||
// Set the graph mask only if the user has not changed it from the default value.
|
||||
// This is not perfect as the user may have wanted it to be precisely -1
|
||||
// however it is the best detection that I can do.
|
||||
// The non-default check is primarily for compatibility reasons to avoid breaking peoples existing code.
|
||||
// The StartPath overloads with an explicit graphMask field should be used instead to set the graphMask.
|
||||
if (p.nnConstraint.graphMask == -1) p.nnConstraint.graphMask = graphMask;
|
||||
StartPathInternal(p, callback);
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queue a path to be calculated.
|
||||
///
|
||||
/// The callback will be called when the path has been calculated (which may be several frames into the future).
|
||||
/// The callback will not be called if a new path request is started before this path request has been calculated.
|
||||
/// </summary>
|
||||
/// <param name="p">The path to start calculating</param>
|
||||
/// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param>
|
||||
/// <param name="graphMask">Mask used to specify which graphs should be searched for close nodes. See \reflink{GraphMask}. This will override the #graphMask on this Seeker.</param>
|
||||
public Path StartPath (Path p, OnPathDelegate callback, GraphMask graphMask) {
|
||||
p.nnConstraint.graphMask = graphMask;
|
||||
StartPathInternal(p, callback);
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>Internal method to start a path and mark it as the currently active path</summary>
|
||||
void StartPathInternal (Path p, OnPathDelegate callback) {
|
||||
var mtp = p as MultiTargetPath;
|
||||
if (mtp != null) {
|
||||
// TODO: Allocation, cache
|
||||
var callbacks = new OnPathDelegate[mtp.targetPoints.Length];
|
||||
|
||||
for (int i = 0; i < callbacks.Length; i++) {
|
||||
callbacks[i] = onPartialPathDelegate;
|
||||
}
|
||||
|
||||
mtp.callbacks = callbacks;
|
||||
p.callback += OnMultiPathComplete;
|
||||
} else {
|
||||
p.callback += onPathDelegate;
|
||||
}
|
||||
|
||||
p.enabledTags = traversableTags;
|
||||
p.tagPenalties = tagPenalties;
|
||||
if (traversalProvider != null) p.traversalProvider = traversalProvider;
|
||||
|
||||
// Cancel a previously requested path is it has not been processed yet and also make sure that it has not been recycled and used somewhere else
|
||||
if (path != null && path.PipelineState <= PathState.Processing && path.CompleteState != PathCompleteState.Error && lastPathID == path.pathID) {
|
||||
path.FailWithError("Canceled path because a new one was requested.\n"+
|
||||
"This happens when a new path is requested from the seeker when one was already being calculated.\n" +
|
||||
"For example if a unit got a new order, you might request a new path directly instead of waiting for the now" +
|
||||
" invalid path to be calculated. Which is probably what you want.\n" +
|
||||
"If you are getting this a lot, you might want to consider how you are scheduling path requests.");
|
||||
// No callback will be sent for the canceled path
|
||||
}
|
||||
|
||||
// Set p as the active path
|
||||
path = p;
|
||||
tmpPathCallback = callback;
|
||||
|
||||
// Save the path id so we can make sure that if we cancel a path (see above) it should not have been recycled yet.
|
||||
lastPathID = path.pathID;
|
||||
|
||||
// Pre process the path
|
||||
RunModifiers(ModifierPass.PreProcess, path);
|
||||
|
||||
// Send the request to the pathfinder
|
||||
AstarPath.StartPath(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a Multi Target Path from one start point to multiple end points.
|
||||
/// A Multi Target Path will search for all the end points in one search and will return all paths if pathsForAll is true, or only the shortest one if pathsForAll is false.
|
||||
///
|
||||
/// callback will be called when the path has completed. Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed)
|
||||
///
|
||||
/// See: Pathfinding.MultiTargetPath
|
||||
/// See: MultiTargetPathExample.cs (view in online documentation for working links) "Example of how to use multi-target-paths"
|
||||
///
|
||||
/// <code>
|
||||
/// var endPoints = new Vector3[] {
|
||||
/// transform.position + Vector3.forward * 5,
|
||||
/// transform.position + Vector3.right * 10,
|
||||
/// transform.position + Vector3.back * 15
|
||||
/// };
|
||||
/// // Start a multi target path, where endPoints is a Vector3[] array.
|
||||
/// // The pathsForAll parameter specifies if a path to every end point should be searched for
|
||||
/// // or if it should only try to find the shortest path to any end point.
|
||||
/// var path = seeker.StartMultiTargetPath(transform.position, endPoints, pathsForAll: true, callback: null);
|
||||
/// path.BlockUntilCalculated();
|
||||
///
|
||||
/// if (path.error) {
|
||||
/// Debug.LogError("Error calculating path: " + path.errorLog);
|
||||
/// return;
|
||||
/// }
|
||||
///
|
||||
/// Debug.Log("The closest target was index " + path.chosenTarget);
|
||||
///
|
||||
/// // Draw the path to all targets
|
||||
/// foreach (var subPath in path.vectorPaths) {
|
||||
/// for (int i = 0; i < subPath.Count - 1; i++) {
|
||||
/// Debug.DrawLine(subPath[i], subPath[i+1], Color.green, 10);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Draw the path to the closest target
|
||||
/// for (int i = 0; i < path.vectorPath.Count - 1; i++) {
|
||||
/// Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="start">The start point of the path</param>
|
||||
/// <param name="endPoints">The end points of the path</param>
|
||||
/// <param name="pathsForAll">Indicates whether or not a path to all end points should be searched for or only to the closest one</param>
|
||||
/// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param>
|
||||
/// <param name="graphMask">Mask used to specify which graphs should be searched for close nodes. See Pathfinding.NNConstraint.graphMask. This will override the #graphMask on this Seeker.</param>
|
||||
public MultiTargetPath StartMultiTargetPath (Vector3 start, Vector3[] endPoints, bool pathsForAll, OnPathDelegate callback, GraphMask graphMask) {
|
||||
MultiTargetPath p = MultiTargetPath.Construct(start, endPoints, null, null);
|
||||
|
||||
p.pathsForAll = pathsForAll;
|
||||
StartPath(p, callback, graphMask);
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a Multi Target Path from one start point to multiple end points.
|
||||
/// A Multi Target Path will search for all the end points in one search and will return all paths if pathsForAll is true, or only the shortest one if pathsForAll is false.
|
||||
///
|
||||
/// callback will be called when the path has completed. Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed)
|
||||
///
|
||||
/// See: Pathfinding.MultiTargetPath
|
||||
/// See: MultiTargetPathExample.cs (view in online documentation for working links) "Example of how to use multi-target-paths"
|
||||
///
|
||||
/// <code>
|
||||
/// var endPoints = new Vector3[] {
|
||||
/// transform.position + Vector3.forward * 5,
|
||||
/// transform.position + Vector3.right * 10,
|
||||
/// transform.position + Vector3.back * 15
|
||||
/// };
|
||||
/// // Start a multi target path, where endPoints is a Vector3[] array.
|
||||
/// // The pathsForAll parameter specifies if a path to every end point should be searched for
|
||||
/// // or if it should only try to find the shortest path to any end point.
|
||||
/// var path = seeker.StartMultiTargetPath(transform.position, endPoints, pathsForAll: true, callback: null);
|
||||
/// path.BlockUntilCalculated();
|
||||
///
|
||||
/// if (path.error) {
|
||||
/// Debug.LogError("Error calculating path: " + path.errorLog);
|
||||
/// return;
|
||||
/// }
|
||||
///
|
||||
/// Debug.Log("The closest target was index " + path.chosenTarget);
|
||||
///
|
||||
/// // Draw the path to all targets
|
||||
/// foreach (var subPath in path.vectorPaths) {
|
||||
/// for (int i = 0; i < subPath.Count - 1; i++) {
|
||||
/// Debug.DrawLine(subPath[i], subPath[i+1], Color.green, 10);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// // Draw the path to the closest target
|
||||
/// for (int i = 0; i < path.vectorPath.Count - 1; i++) {
|
||||
/// Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.red, 10);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </summary>
|
||||
/// <param name="start">The start point of the path</param>
|
||||
/// <param name="endPoints">The end points of the path</param>
|
||||
/// <param name="pathsForAll">Indicates whether or not a path to all end points should be searched for or only to the closest one</param>
|
||||
/// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param>
|
||||
public MultiTargetPath StartMultiTargetPath(Vector3 start, Vector3[] endPoints, bool pathsForAll, OnPathDelegate callback) => StartMultiTargetPath(start, endPoints, pathsForAll, callback, graphMask);
|
||||
|
||||
/// <summary>
|
||||
/// Starts a Multi Target Path from multiple start points to a single target point.
|
||||
/// A Multi Target Path will search from all start points to the target point in one search and will return all paths if pathsForAll is true, or only the shortest one if pathsForAll is false.
|
||||
///
|
||||
/// callback will be called when the path has completed. Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed)
|
||||
///
|
||||
/// See: Pathfinding.MultiTargetPath
|
||||
/// See: MultiTargetPathExample.cs (view in online documentation for working links) "Example of how to use multi-target-paths"
|
||||
/// </summary>
|
||||
/// <param name="startPoints">The start points of the path</param>
|
||||
/// <param name="end">The end point of the path</param>
|
||||
/// <param name="pathsForAll">Indicates whether or not a path from all start points should be searched for or only to the closest one</param>
|
||||
/// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param>
|
||||
/// <param name="graphMask">Mask used to specify which graphs should be searched for close nodes. See Pathfinding.NNConstraint.graphMask. This will override the #graphMask on this Seeker.</param>
|
||||
public MultiTargetPath StartMultiTargetPath (Vector3[] startPoints, Vector3 end, bool pathsForAll, OnPathDelegate callback, GraphMask graphMask) {
|
||||
MultiTargetPath p = MultiTargetPath.Construct(startPoints, end, null, null);
|
||||
|
||||
p.pathsForAll = pathsForAll;
|
||||
StartPath(p, callback, graphMask);
|
||||
return p;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Starts a Multi Target Path from multiple start points to a single target point.
|
||||
/// A Multi Target Path will search from all start points to the target point in one search and will return all paths if pathsForAll is true, or only the shortest one if pathsForAll is false.
|
||||
///
|
||||
/// callback will be called when the path has completed. Callback will not be called if the path is canceled (e.g when a new path is requested before the previous one has completed)
|
||||
///
|
||||
/// See: Pathfinding.MultiTargetPath
|
||||
/// See: MultiTargetPathExample.cs (view in online documentation for working links) "Example of how to use multi-target-paths"
|
||||
/// </summary>
|
||||
/// <param name="startPoints">The start points of the path</param>
|
||||
/// <param name="end">The end point of the path</param>
|
||||
/// <param name="pathsForAll">Indicates whether or not a path from all start points should be searched for or only to the closest one</param>
|
||||
/// <param name="callback">The function to call when the path has been calculated. If you don't want a callback (e.g. if you instead poll path.IsDone or use a similar method) you can set this to null.</param>
|
||||
public MultiTargetPath StartMultiTargetPath(Vector3[] startPoints, Vector3 end, bool pathsForAll, OnPathDelegate callback) => StartMultiTargetPath(startPoints, end, pathsForAll, callback, graphMask);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>Draws gizmos for the Seeker</summary>
|
||||
public override void DrawGizmos () {
|
||||
if (lastCompletedNodePath == null || !drawGizmos) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (detailedGizmos && lastCompletedNodePath != null) {
|
||||
using (Draw.WithColor(new Color(0.7F, 0.5F, 0.1F, 0.5F))) {
|
||||
for (int i = 0; i < lastCompletedNodePath.Count-1; i++) {
|
||||
Draw.Line((Vector3)lastCompletedNodePath[i].position, (Vector3)lastCompletedNodePath[i+1].position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastCompletedVectorPath != null) {
|
||||
using (Draw.WithColor(new Color(0, 1F, 0, 1F))) {
|
||||
for (int i = 0; i < lastCompletedVectorPath.Count-1; i++) {
|
||||
Draw.Line(lastCompletedVectorPath[i], lastCompletedVectorPath[i+1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
protected override void OnUpgradeSerializedData (ref Serialization.Migrations migrations, bool unityThread) {
|
||||
if (graphMaskCompatibility != -1) {
|
||||
graphMask = graphMaskCompatibility;
|
||||
graphMaskCompatibility = -1;
|
||||
}
|
||||
base.OnUpgradeSerializedData(ref migrations, unityThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Packages/com.arongranberg.astar/Core/AI/Seeker.cs.meta
Normal file
11
Packages/com.arongranberg.astar/Core/AI/Seeker.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 373b52eb9bf8c40f785bb6947a1aee66
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {fileID: 2800000, guid: 2c41d544537d6ee4a8ecd487b2ac9724, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
25
Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs
Normal file
25
Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pathfinding.Examples {
|
||||
/// <summary>Helper script in the example scene 'Turn Based'</summary>
|
||||
[HelpURL("https://arongranberg.com/astar/documentation/stable/turnbasedai.html")]
|
||||
public class TurnBasedAI : VersionedMonoBehaviour {
|
||||
public int movementPoints = 2;
|
||||
public BlockManager blockManager;
|
||||
public SingleNodeBlocker blocker;
|
||||
public GraphNode targetNode;
|
||||
public BlockManager.TraversalProvider traversalProvider;
|
||||
|
||||
void Start () {
|
||||
blocker.BlockAtCurrentPosition();
|
||||
}
|
||||
|
||||
protected override void Awake () {
|
||||
base.Awake();
|
||||
// Set the traversal provider to block all nodes that are blocked by a SingleNodeBlocker
|
||||
// except the SingleNodeBlocker owned by this AI (we don't want to be blocked by ourself)
|
||||
traversalProvider = new BlockManager.TraversalProvider(blockManager, BlockManager.BlockMode.AllExceptSelector, new List<SingleNodeBlocker>() { blocker });
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs.meta
Normal file
12
Packages/com.arongranberg.astar/Core/AI/TurnBasedAI.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f95b80c439d6408b9afac9d013922e4
|
||||
timeCreated: 1453035991
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
715
Packages/com.arongranberg.astar/Core/AstarData.cs
Normal file
715
Packages/com.arongranberg.astar/Core/AstarData.cs
Normal file
@@ -0,0 +1,715 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Pathfinding.WindowsStore;
|
||||
using Pathfinding.Serialization;
|
||||
using Pathfinding.Util;
|
||||
using Pathfinding.Sync;
|
||||
|
||||
namespace Pathfinding {
|
||||
[System.Serializable]
|
||||
/// <summary>
|
||||
/// Stores the navigation graphs for the A* Pathfinding System.
|
||||
///
|
||||
/// An instance of this class is assigned to <see cref="AstarPath.data"/>. From it you can access all graphs loaded through the <see cref="graphs"/> variable.
|
||||
/// This class also handles a lot of the high level serialization.
|
||||
/// </summary>
|
||||
public class AstarData {
|
||||
/// <summary>The AstarPath component which owns this AstarData</summary>
|
||||
AstarPath active;
|
||||
|
||||
#region Fields
|
||||
/// <summary>
|
||||
/// Shortcut to the first <see cref="NavMeshGraph"/>
|
||||
///
|
||||
/// Deprecated: Use <see cref="navmeshGraph"/> instead
|
||||
/// </summary>
|
||||
[System.Obsolete("Use navmeshGraph instead")]
|
||||
public NavMeshGraph navmesh => navmeshGraph;
|
||||
|
||||
/// <summary>Shortcut to the first <see cref="NavMeshGraph"/></summary>
|
||||
public NavMeshGraph navmeshGraph { get; private set; }
|
||||
|
||||
#if !ASTAR_NO_GRID_GRAPH
|
||||
/// <summary>Shortcut to the first <see cref="GridGraph"/></summary>
|
||||
public GridGraph gridGraph { get; private set; }
|
||||
|
||||
/// <summary>Shortcut to the first <see cref="LayerGridGraph"/>.</summary>
|
||||
public LayerGridGraph layerGridGraph { get; private set; }
|
||||
#endif
|
||||
|
||||
#if !ASTAR_NO_POINT_GRAPH
|
||||
/// <summary>Shortcut to the first <see cref="PointGraph"/>.</summary>
|
||||
public PointGraph pointGraph { get; private set; }
|
||||
#endif
|
||||
|
||||
/// <summary>Shortcut to the first <see cref="RecastGraph"/>.</summary>
|
||||
public RecastGraph recastGraph { get; private set; }
|
||||
|
||||
/// <summary>Shortcut to the first <see cref="LinkGraph"/>.</summary>
|
||||
public LinkGraph linkGraph { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// All supported graph types.
|
||||
/// Populated through reflection search
|
||||
/// </summary>
|
||||
public static System.Type[] graphTypes { get; private set; }
|
||||
|
||||
#if ASTAR_FAST_NO_EXCEPTIONS || UNITY_WINRT || UNITY_WEBGL
|
||||
/// <summary>
|
||||
/// Graph types to use when building with Fast But No Exceptions for iPhone.
|
||||
/// If you add any custom graph types, you need to add them to this hard-coded list.
|
||||
/// </summary>
|
||||
public static readonly System.Type[] DefaultGraphTypes = new System.Type[] {
|
||||
#if !ASTAR_NO_GRID_GRAPH
|
||||
typeof(GridGraph),
|
||||
typeof(LayerGridGraph),
|
||||
#endif
|
||||
#if !ASTAR_NO_POINT_GRAPH
|
||||
typeof(PointGraph),
|
||||
#endif
|
||||
typeof(NavMeshGraph),
|
||||
typeof(RecastGraph),
|
||||
typeof(LinkGraph),
|
||||
};
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// All graphs.
|
||||
/// This will be filled only after deserialization has completed.
|
||||
/// May contain null entries if graph have been removed.
|
||||
/// </summary>
|
||||
[System.NonSerialized]
|
||||
public NavGraph[] graphs = new NavGraph[0];
|
||||
|
||||
/// <summary>
|
||||
/// Serialized data for all graphs and settings.
|
||||
/// Stored as a base64 encoded string because otherwise Unity's Undo system would sometimes corrupt the byte data (because it only stores deltas).
|
||||
///
|
||||
/// This can be accessed as a byte array from the <see cref="data"/> property.
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
string dataString;
|
||||
|
||||
/// <summary>Serialized data for all graphs and settings</summary>
|
||||
private byte[] data {
|
||||
get {
|
||||
var d = dataString != null? System.Convert.FromBase64String(dataString) : null;
|
||||
// Unity can initialize the dataString to an empty string, but that's not a valid zip file
|
||||
if (d != null && d.Length == 0) return null;
|
||||
return d;
|
||||
}
|
||||
set {
|
||||
dataString = value != null? System.Convert.ToBase64String(value) : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serialized data for cached startup.
|
||||
/// If set, and <see cref="cacheStartup"/> is enabled, graphs will be deserialized from this file when the game starts.
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
/// </summary>
|
||||
public TextAsset file_cachedStartup;
|
||||
|
||||
/// <summary>
|
||||
/// Should graph-data be cached.
|
||||
/// Caching the startup means saving the whole graphs - not only the settings - to a file (<see cref="file_cachedStartup)"/> which can
|
||||
/// be loaded when the game starts. This is usually much faster than scanning the graphs when the game starts. This is configured from the editor under the "Save & Load" tab.
|
||||
///
|
||||
/// [Open online documentation to see images]
|
||||
///
|
||||
/// See: save-load-graphs (view in online documentation for working links)
|
||||
/// </summary>
|
||||
[SerializeField]
|
||||
public bool cacheStartup;
|
||||
|
||||
List<bool> graphStructureLocked = new List<bool>();
|
||||
|
||||
static readonly Unity.Profiling.ProfilerMarker MarkerLoadFromCache = new Unity.Profiling.ProfilerMarker("LoadFromCache");
|
||||
static readonly Unity.Profiling.ProfilerMarker MarkerDeserializeGraphs = new Unity.Profiling.ProfilerMarker("DeserializeGraphs");
|
||||
static readonly Unity.Profiling.ProfilerMarker MarkerSerializeGraphs = new Unity.Profiling.ProfilerMarker("SerializeGraphs");
|
||||
static readonly Unity.Profiling.ProfilerMarker MarkerFindGraphTypes = new Unity.Profiling.ProfilerMarker("FindGraphTypes");
|
||||
|
||||
#endregion
|
||||
|
||||
internal AstarData (AstarPath active) {
|
||||
this.active = active;
|
||||
}
|
||||
|
||||
/// <summary>Get the serialized data for all graphs and their settings</summary>
|
||||
public byte[] GetData() => data;
|
||||
|
||||
/// <summary>
|
||||
/// Set the serialized data for all graphs and their settings.
|
||||
///
|
||||
/// During runtime you usually want to deserialize the graphs immediately, in which case you should use <see cref="DeserializeGraphs(byte"/>[]) instead.
|
||||
/// </summary>
|
||||
public void SetData (byte[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/// <summary>Loads the graphs from memory, will load cached graphs if any exists</summary>
|
||||
public void OnEnable () {
|
||||
FindGraphTypes();
|
||||
|
||||
if (graphs == null) graphs = new NavGraph[0];
|
||||
|
||||
if (cacheStartup && file_cachedStartup != null && Application.isPlaying) {
|
||||
LoadFromCache();
|
||||
} else {
|
||||
DeserializeGraphs();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prevent the graph structure from changing during the time this lock is held.
|
||||
/// This prevents graphs from being added or removed and also prevents graphs from being serialized or deserialized.
|
||||
/// This is used when e.g an async scan is happening to ensure that for example a graph that is being scanned is not destroyed.
|
||||
///
|
||||
/// Each call to this method *must* be paired with exactly one call to <see cref="UnlockGraphStructure"/>.
|
||||
/// The calls may be nested.
|
||||
/// </summary>
|
||||
internal void LockGraphStructure (bool allowAddingGraphs = false) {
|
||||
graphStructureLocked.Add(allowAddingGraphs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the graph structure to change again.
|
||||
/// See: <see cref="LockGraphStructure"/>
|
||||
/// </summary>
|
||||
internal void UnlockGraphStructure () {
|
||||
if (graphStructureLocked.Count == 0) throw new System.InvalidOperationException();
|
||||
graphStructureLocked.RemoveAt(graphStructureLocked.Count - 1);
|
||||
}
|
||||
|
||||
PathProcessor.GraphUpdateLock AssertSafe (bool onlyAddingGraph = false) {
|
||||
if (graphStructureLocked.Count > 0) {
|
||||
bool allowAdding = true;
|
||||
for (int i = 0; i < graphStructureLocked.Count; i++) allowAdding &= graphStructureLocked[i];
|
||||
if (!(onlyAddingGraph && allowAdding)) throw new System.InvalidOperationException("Graphs cannot be added, removed or serialized while the graph structure is locked. This is the case when a graph is currently being scanned and when executing graph updates and work items.\nHowever as a special case, graphs can be added inside work items.");
|
||||
}
|
||||
|
||||
// Pause the pathfinding threads
|
||||
var graphLock = active.PausePathfinding();
|
||||
if (!active.IsInsideWorkItem) {
|
||||
// Make sure all graph updates and other callbacks are done
|
||||
// Only do this if this code is not being called from a work item itself as that would cause a recursive wait that could never complete.
|
||||
// There are some valid cases when this can happen. For example it may be necessary to add a new graph inside a work item.
|
||||
active.FlushWorkItems();
|
||||
|
||||
// Paths that are already calculated and waiting to be returned to the Seeker component need to be
|
||||
// processed immediately as their results usually depend on graphs that currently exist. If this was
|
||||
// not done then after destroying a graph one could get a path result with destroyed nodes in it.
|
||||
active.pathReturnQueue.ReturnPaths(false);
|
||||
}
|
||||
return graphLock;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the callback with every node in all graphs.
|
||||
/// This is the easiest way to iterate through every existing node.
|
||||
///
|
||||
/// <code>
|
||||
/// AstarPath.active.data.GetNodes(node => {
|
||||
/// Debug.Log("I found a node at position " + (Vector3)node.position);
|
||||
/// });
|
||||
/// </code>
|
||||
///
|
||||
/// See: <see cref="Pathfinding.NavGraph.GetNodes"/> for getting the nodes of a single graph instead of all.
|
||||
/// See: graph-updates (view in online documentation for working links)
|
||||
/// </summary>
|
||||
public void GetNodes (System.Action<GraphNode> callback) {
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
if (graphs[i] != null) graphs[i].GetNodes(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates shortcuts to the first graph of different types.
|
||||
/// Hard coding references to some graph types is not really a good thing imo. I want to keep it dynamic and flexible.
|
||||
/// But these references ease the use of the system, so I decided to keep them.
|
||||
/// </summary>
|
||||
public void UpdateShortcuts () {
|
||||
navmeshGraph = (NavMeshGraph)FindGraphOfType(typeof(NavMeshGraph));
|
||||
|
||||
#if !ASTAR_NO_GRID_GRAPH
|
||||
gridGraph = (GridGraph)FindGraphOfType(typeof(GridGraph));
|
||||
layerGridGraph = (LayerGridGraph)FindGraphOfType(typeof(LayerGridGraph));
|
||||
#endif
|
||||
|
||||
#if !ASTAR_NO_POINT_GRAPH
|
||||
pointGraph = (PointGraph)FindGraphOfType(typeof(PointGraph));
|
||||
#endif
|
||||
|
||||
recastGraph = (RecastGraph)FindGraphOfType(typeof(RecastGraph));
|
||||
linkGraph = (LinkGraph)FindGraphOfType(typeof(LinkGraph));
|
||||
}
|
||||
|
||||
/// <summary>Load from data from <see cref="file_cachedStartup"/></summary>
|
||||
public void LoadFromCache () {
|
||||
using var _ = MarkerLoadFromCache.Auto();
|
||||
using (AssertSafe()) {
|
||||
if (file_cachedStartup != null) {
|
||||
var bytes = file_cachedStartup.bytes;
|
||||
DeserializeGraphs(bytes);
|
||||
|
||||
GraphModifier.TriggerEvent(GraphModifier.EventType.PostCacheLoad);
|
||||
} else {
|
||||
Debug.LogError("Can't load from cache since the cache is empty");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Serialization
|
||||
|
||||
/// <summary>
|
||||
/// Serializes all graphs settings to a byte array.
|
||||
/// See: DeserializeGraphs(byte[])
|
||||
/// </summary>
|
||||
public byte[] SerializeGraphs () {
|
||||
return SerializeGraphs(SerializeSettings.Settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serializes all graphs settings and optionally node data to a byte array.
|
||||
/// See: DeserializeGraphs(byte[])
|
||||
/// See: Pathfinding.Serialization.SerializeSettings
|
||||
/// </summary>
|
||||
public byte[] SerializeGraphs (SerializeSettings settings) {
|
||||
return SerializeGraphs(settings, out var _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Main serializer function.
|
||||
/// Serializes all graphs to a byte array
|
||||
/// A similar function exists in the AstarPathEditor.cs script to save additional info
|
||||
/// </summary>
|
||||
public byte[] SerializeGraphs (SerializeSettings settings, out uint checksum) {
|
||||
return SerializeGraphs(settings, out checksum, graphs);
|
||||
}
|
||||
|
||||
byte[] SerializeGraphs (SerializeSettings settings, out uint checksum, NavGraph[] graphs) {
|
||||
MarkerSerializeGraphs.Begin();
|
||||
using (AssertSafe()) {
|
||||
var sr = new AstarSerializer(this, settings, active.gameObject);
|
||||
|
||||
sr.OpenSerialize();
|
||||
sr.SerializeGraphs(graphs);
|
||||
sr.SerializeExtraInfo();
|
||||
byte[] bytes = sr.CloseSerialize();
|
||||
checksum = sr.GetChecksum();
|
||||
#if ASTARDEBUG
|
||||
Debug.Log("Got a whole bunch of data, "+bytes.Length+" bytes");
|
||||
#endif
|
||||
|
||||
MarkerSerializeGraphs.End();
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Deserializes graphs from <see cref="data"/></summary>
|
||||
public void DeserializeGraphs () {
|
||||
var dataBytes = data;
|
||||
if (dataBytes != null) {
|
||||
DeserializeGraphs(dataBytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destroys all graphs and sets <see cref="graphs"/> to null.
|
||||
/// See: <see cref="RemoveGraph"/>
|
||||
/// </summary>
|
||||
public void ClearGraphs () {
|
||||
using (AssertSafe()) {
|
||||
ClearGraphsInternal();
|
||||
}
|
||||
}
|
||||
|
||||
void ClearGraphsInternal () {
|
||||
if (graphs == null) return;
|
||||
using (AssertSafe()) {
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
if (graphs[i] != null) {
|
||||
active.DirtyBounds(graphs[i].bounds);
|
||||
((IGraphInternals)graphs[i]).OnDestroy();
|
||||
graphs[i].active = null;
|
||||
}
|
||||
}
|
||||
graphs = new NavGraph[0];
|
||||
UpdateShortcuts();
|
||||
}
|
||||
}
|
||||
|
||||
public void DisposeUnmanagedData () {
|
||||
if (graphs == null) return;
|
||||
using (AssertSafe()) {
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
if (graphs[i] != null) {
|
||||
((IGraphInternals)graphs[i]).DisposeUnmanagedData();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Makes all graphs become unscanned</summary>
|
||||
internal void DestroyAllNodes () {
|
||||
if (graphs == null) return;
|
||||
using (AssertSafe()) {
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
if (graphs[i] != null) {
|
||||
((IGraphInternals)graphs[i]).DestroyAllNodes();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnDestroy () {
|
||||
ClearGraphsInternal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes and loads graphs from the specified byte array.
|
||||
/// An error will be logged if deserialization fails.
|
||||
///
|
||||
/// Returns: The deserialized graphs
|
||||
/// </summary>
|
||||
public NavGraph[] DeserializeGraphs (byte[] bytes) {
|
||||
using (AssertSafe()) {
|
||||
ClearGraphs();
|
||||
return DeserializeGraphsAdditive(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes and loads graphs from the specified byte array additively.
|
||||
/// An error will be logged if deserialization fails.
|
||||
/// This function will add loaded graphs to the current ones.
|
||||
///
|
||||
/// Returns: The deserialized graphs
|
||||
/// </summary>
|
||||
public NavGraph[] DeserializeGraphsAdditive (byte[] bytes) {
|
||||
using (AssertSafe()) {
|
||||
try {
|
||||
MarkerDeserializeGraphs.Begin();
|
||||
NavGraph[] result;
|
||||
if (bytes != null) {
|
||||
var sr = new AstarSerializer(this, active.gameObject);
|
||||
|
||||
if (sr.OpenDeserialize(bytes)) {
|
||||
result = DeserializeGraphsPartAdditive(sr);
|
||||
sr.CloseDeserialize();
|
||||
} else {
|
||||
throw new System.ArgumentException("Invalid data file (cannot read zip).\nThe data is either corrupt or it was saved using a 3.0.x or earlier version of the system");
|
||||
}
|
||||
} else {
|
||||
throw new System.ArgumentNullException(nameof(bytes));
|
||||
}
|
||||
UpdateShortcuts();
|
||||
GraphModifier.TriggerEvent(GraphModifier.EventType.PostGraphLoad);
|
||||
return result;
|
||||
} catch (System.Exception e) {
|
||||
Debug.LogError(new System.Exception("Caught exception while deserializing data.", e));
|
||||
graphs = new NavGraph[0];
|
||||
UpdateShortcuts();
|
||||
throw;
|
||||
} finally {
|
||||
MarkerDeserializeGraphs.End();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Helper function for deserializing graphs</summary>
|
||||
NavGraph[] DeserializeGraphsPartAdditive (AstarSerializer sr) {
|
||||
if (graphs == null) graphs = new NavGraph[0];
|
||||
|
||||
var gr = new List<NavGraph>(graphs);
|
||||
|
||||
// Trim nulls at the end
|
||||
while (gr.Count > 0 && gr[gr.Count-1] == null) gr.RemoveAt(gr.Count-1);
|
||||
|
||||
// Set an offset so that the deserializer will load
|
||||
// the graphs with the correct graph indexes
|
||||
sr.SetGraphIndexOffset(gr.Count);
|
||||
|
||||
FindGraphTypes();
|
||||
// This may be false if the user is editing a prefab, for example.
|
||||
// If it is false, we must not try to load any nodes
|
||||
bool astarInitialized = active == AstarPath.active;
|
||||
var newGraphs = sr.DeserializeGraphs(graphTypes, astarInitialized);
|
||||
gr.AddRange(newGraphs);
|
||||
|
||||
if (gr.Count > GraphNode.MaxGraphIndex + 1) {
|
||||
throw new System.InvalidOperationException("Graph Count Limit Reached. You cannot have more than " + GraphNode.MaxGraphIndex + " graphs.");
|
||||
}
|
||||
|
||||
graphs = gr.ToArray();
|
||||
|
||||
// Assign correct graph indices.
|
||||
bool anyScanned = false;
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
if (graphs[i] == null) continue;
|
||||
graphs[i].GetNodes(node => node.GraphIndex = (uint)i);
|
||||
anyScanned |= graphs[i].isScanned;
|
||||
}
|
||||
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
for (int j = i+1; j < graphs.Length; j++) {
|
||||
if (graphs[i] != null && graphs[j] != null && graphs[i].guid == graphs[j].guid) {
|
||||
Debug.LogWarning("Guid Conflict when importing graphs additively. Imported graph will get a new Guid.\nThis message is (relatively) harmless.");
|
||||
graphs[i].guid = Pathfinding.Util.Guid.NewGuid();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sr.PostDeserialization();
|
||||
|
||||
if (anyScanned) {
|
||||
// This will refresh off-mesh links,
|
||||
// and also recalculate the hierarchical graph if necessary.
|
||||
//
|
||||
// It's important that this does not run if no graphs are scanned,
|
||||
// which is the case when just deserializing graph settings in the editor.
|
||||
// This is because we may be in a prefab, and prefabs should never be able
|
||||
// to actually load graphs with nodes.
|
||||
active.AddWorkItem(ctx => {
|
||||
for (int i = 0; i < newGraphs.Length; i++) {
|
||||
if (newGraphs[i].isScanned) {
|
||||
ctx.DirtyBounds(newGraphs[i].bounds);
|
||||
}
|
||||
}
|
||||
});
|
||||
active.FlushWorkItems();
|
||||
}
|
||||
return newGraphs;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Find all graph types supported in this build.
|
||||
/// Using reflection, the assembly is searched for types which inherit from NavGraph.
|
||||
/// </summary>
|
||||
public void FindGraphTypes () {
|
||||
if (graphTypes != null) return;
|
||||
|
||||
MarkerFindGraphTypes.Begin();
|
||||
#if !ASTAR_FAST_NO_EXCEPTIONS && !UNITY_WINRT && !UNITY_WEBGL
|
||||
graphTypes = AssemblySearcher.FindTypesInheritingFrom<NavGraph>().ToArray();
|
||||
#else
|
||||
graphTypes = DefaultGraphTypes;
|
||||
#endif
|
||||
MarkerFindGraphTypes.End();
|
||||
}
|
||||
|
||||
#region GraphCreation
|
||||
|
||||
/// <summary>Creates a new graph instance of type type</summary>
|
||||
internal NavGraph CreateGraph (System.Type type) {
|
||||
var graph = System.Activator.CreateInstance(type) as NavGraph;
|
||||
|
||||
graph.active = active;
|
||||
return graph;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a graph of type T to the <see cref="graphs"/> array.
|
||||
/// See: runtime-graphs (view in online documentation for working links)
|
||||
/// </summary>
|
||||
public T AddGraph<T> () where T : NavGraph => AddGraph(typeof(T)) as T;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a graph of type type to the <see cref="graphs"/> array.
|
||||
/// See: runtime-graphs (view in online documentation for working links)
|
||||
/// </summary>
|
||||
public NavGraph AddGraph (System.Type type) {
|
||||
NavGraph graph = null;
|
||||
|
||||
for (int i = 0; i < graphTypes.Length; i++) {
|
||||
if (System.Type.Equals(graphTypes[i], type)) {
|
||||
graph = CreateGraph(graphTypes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (graph == null) {
|
||||
Debug.LogError("No NavGraph of type '"+type+"' could be found, "+graphTypes.Length+" graph types are avaliable");
|
||||
return null;
|
||||
}
|
||||
|
||||
AddGraph(graph);
|
||||
|
||||
return graph;
|
||||
}
|
||||
|
||||
/// <summary>Adds the specified graph to the <see cref="graphs"/> array</summary>
|
||||
void AddGraph (NavGraph graph) {
|
||||
// Make sure to not interfere with pathfinding
|
||||
using (AssertSafe(true)) {
|
||||
// Try to fill in an empty position
|
||||
int graphIndex = System.Array.IndexOf(graphs, null);
|
||||
|
||||
if (graphIndex == -1) {
|
||||
if (graphs.Length >= GraphNode.MaxGraphIndex) {
|
||||
throw new System.Exception($"Graph Count Limit Reached. You cannot have more than {GraphNode.MaxGraphIndex} graphs.");
|
||||
}
|
||||
|
||||
// Add a new entry
|
||||
Memory.Realloc(ref graphs, graphs.Length + 1);
|
||||
graphIndex = graphs.Length-1;
|
||||
}
|
||||
graphs[graphIndex] = graph;
|
||||
graph.graphIndex = (uint)graphIndex;
|
||||
graph.active = active;
|
||||
|
||||
UpdateShortcuts();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the specified graph from the <see cref="graphs"/> array and Destroys it in a safe manner.
|
||||
/// To avoid changing graph indices for the other graphs, the graph is simply nulled in the array instead
|
||||
/// of actually removing it from the array.
|
||||
/// The empty position will be reused if a new graph is added.
|
||||
///
|
||||
/// Returns: True if the graph was sucessfully removed (i.e it did exist in the <see cref="graphs"/> array). False otherwise.
|
||||
///
|
||||
/// See: <see cref="ClearGraphs"/>
|
||||
/// </summary>
|
||||
public bool RemoveGraph (NavGraph graph) {
|
||||
// Make sure the pathfinding threads are paused
|
||||
using (AssertSafe()) {
|
||||
active.DirtyBounds(graph.bounds);
|
||||
((IGraphInternals)graph).OnDestroy();
|
||||
graph.active = null;
|
||||
|
||||
int i = System.Array.IndexOf(graphs, graph);
|
||||
if (i != -1) graphs[i] = null;
|
||||
|
||||
UpdateShortcuts();
|
||||
active.AddWorkItem(() => active.offMeshLinks.Refresh());
|
||||
active.FlushWorkItems();
|
||||
return i != -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Duplicates the given graph and adds the duplicate to the <see cref="graphs"/> array.
|
||||
///
|
||||
/// Note: Only graph settings are duplicated, not the nodes in the graph. You may want to scan the graph after duplicating it.
|
||||
///
|
||||
/// Returns: The duplicated graph.
|
||||
/// </summary>
|
||||
public NavGraph DuplicateGraph (NavGraph graph) {
|
||||
if (graph == null) throw new System.ArgumentNullException(nameof(graph));
|
||||
|
||||
int i = System.Array.IndexOf(graphs, graph);
|
||||
if (i == -1) throw new System.ArgumentException("Graph doesn't exist");
|
||||
|
||||
var bytes = SerializeGraphs(SerializeSettings.Settings, out var _, new NavGraph[] { graph });
|
||||
var newGraphs = DeserializeGraphsAdditive(bytes);
|
||||
UnityEngine.Assertions.Assert.AreEqual(1, newGraphs.Length);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
foreach (var g in newGraphs) {
|
||||
var existingNames = new string[graphs.Length];
|
||||
for (int j = 0; j < graphs.Length; j++) existingNames[j] = graphs[j].name;
|
||||
g.name = UnityEditor.ObjectNames.GetUniqueName(existingNames, g.name);
|
||||
}
|
||||
#endif
|
||||
return newGraphs[0];
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GraphUtility
|
||||
|
||||
/// <summary>
|
||||
/// Graph which contains the specified node.
|
||||
/// The graph must be in the <see cref="graphs"/> array.
|
||||
///
|
||||
/// Returns: Returns the graph which contains the node. Null if the graph wasn't found
|
||||
/// </summary>
|
||||
public static NavGraph GetGraph (GraphNode node) {
|
||||
if (node == null || node.Destroyed) return null;
|
||||
|
||||
AstarPath script = AstarPath.active;
|
||||
if (System.Object.ReferenceEquals(script, null)) return null;
|
||||
|
||||
AstarData data = script.data;
|
||||
if (data == null || data.graphs == null) return null;
|
||||
|
||||
uint graphIndex = node.GraphIndex;
|
||||
return data.graphs[(int)graphIndex];
|
||||
}
|
||||
|
||||
/// <summary>Returns the first graph which satisfies the predicate. Returns null if no graph was found.</summary>
|
||||
public NavGraph FindGraph (System.Func<NavGraph, bool> predicate) {
|
||||
if (graphs != null) {
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
if (graphs[i] != null && predicate(graphs[i])) {
|
||||
return graphs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Returns the first graph of type type found in the <see cref="graphs"/> array. Returns null if no graph was found.</summary>
|
||||
public NavGraph FindGraphOfType (System.Type type) {
|
||||
return FindGraph(graph => System.Type.Equals(graph.GetType(), type));
|
||||
}
|
||||
|
||||
/// <summary>Returns the first graph which inherits from the type type. Returns null if no graph was found.</summary>
|
||||
public NavGraph FindGraphWhichInheritsFrom (System.Type type) {
|
||||
return FindGraph(graph => WindowsStoreCompatibility.GetTypeInfo(type).IsAssignableFrom(WindowsStoreCompatibility.GetTypeInfo(graph.GetType())));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loop through this function to get all graphs of type 'type'
|
||||
/// <code>
|
||||
/// foreach (GridGraph graph in AstarPath.data.FindGraphsOfType (typeof(GridGraph))) {
|
||||
/// //Do something with the graph
|
||||
/// }
|
||||
/// </code>
|
||||
/// See: <see cref="AstarPath.AddWorkItem"/>
|
||||
/// </summary>
|
||||
public IEnumerable FindGraphsOfType (System.Type type) {
|
||||
if (graphs == null) yield break;
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
if (graphs[i] != null && System.Type.Equals(graphs[i].GetType(), type)) {
|
||||
yield return graphs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All graphs which implements the UpdateableGraph interface
|
||||
/// <code> foreach (IUpdatableGraph graph in AstarPath.data.GetUpdateableGraphs ()) {
|
||||
/// //Do something with the graph
|
||||
/// } </code>
|
||||
/// See: <see cref="AstarPath.AddWorkItem"/>
|
||||
/// See: <see cref="IUpdatableGraph"/>
|
||||
/// </summary>
|
||||
public IEnumerable GetUpdateableGraphs () {
|
||||
if (graphs == null) yield break;
|
||||
for (int i = 0; i < graphs.Length; i++) {
|
||||
if (graphs[i] is IUpdatableGraph) {
|
||||
yield return graphs[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the index of the graph in the <see cref="graphs"/> array</summary>
|
||||
public int GetGraphIndex (NavGraph graph) {
|
||||
if (graph == null) throw new System.ArgumentNullException("graph");
|
||||
if (graphs == null) throw new System.ArgumentException("No graphs exist");
|
||||
|
||||
var index = System.Array.IndexOf(graphs, graph);
|
||||
if (index == -1) throw new System.ArgumentException("Graph doesn't exist");
|
||||
return index;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
7
Packages/com.arongranberg.astar/Core/AstarData.cs.meta
Normal file
7
Packages/com.arongranberg.astar/Core/AstarData.cs.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 38d211caa07cb44ef886481aa1cf755c
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
2110
Packages/com.arongranberg.astar/Core/AstarMath.cs
Normal file
2110
Packages/com.arongranberg.astar/Core/AstarMath.cs
Normal file
File diff suppressed because it is too large
Load Diff
7
Packages/com.arongranberg.astar/Core/AstarMath.cs.meta
Normal file
7
Packages/com.arongranberg.astar/Core/AstarMath.cs.meta
Normal file
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 960fd9020b1f74f939fee737c3c0f491
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
2450
Packages/com.arongranberg.astar/Core/AstarPath.cs
Normal file
2450
Packages/com.arongranberg.astar/Core/AstarPath.cs
Normal file
File diff suppressed because it is too large
Load Diff
16
Packages/com.arongranberg.astar/Core/AstarPath.cs.meta
Normal file
16
Packages/com.arongranberg.astar/Core/AstarPath.cs.meta
Normal file
@@ -0,0 +1,16 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 78396926cbbfc4ac3b48fc5fc34a87d1
|
||||
labels:
|
||||
- Pathfinder
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- gizmoSurfaceMaterial: {fileID: 2100000, guid: 5ce51318bbfb1466188b929a68a6bd3a,
|
||||
type: 2}
|
||||
- gizmoLineMaterial: {fileID: 2100000, guid: 91035448860ba4e708919485c73f7edc, type: 2}
|
||||
executionOrder: -10000
|
||||
icon: {fileID: 2800000, guid: 1620f833be5302149a071c06944d3e9b, type: 3}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.arongranberg.astar/Core/Collections.meta
Normal file
8
Packages/com.arongranberg.astar/Core/Collections.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b2120e89e2185f4bbaa86f8099157df
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using Unity.Mathematics;
|
||||
using Pathfinding.Pooling;
|
||||
|
||||
namespace Pathfinding.Util {
|
||||
/// <summary>Various utilities for handling arrays and memory</summary>
|
||||
public static class Memory {
|
||||
/// <summary>
|
||||
/// Returns a new array with at most length newLength.
|
||||
/// The array will contain a copy of all elements of arr up to but excluding the index newLength.
|
||||
/// </summary>
|
||||
public static T[] ShrinkArray<T>(T[] arr, int newLength) {
|
||||
newLength = Math.Min(newLength, arr.Length);
|
||||
var shrunkArr = new T[newLength];
|
||||
Array.Copy(arr, shrunkArr, newLength);
|
||||
return shrunkArr;
|
||||
}
|
||||
|
||||
/// <summary>Swaps the variables a and b</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public static void Swap<T>(ref T a, ref T b) {
|
||||
T tmp = a;
|
||||
|
||||
a = b;
|
||||
b = tmp;
|
||||
}
|
||||
|
||||
public static void Realloc<T>(ref NativeArray<T> arr, int newSize, Allocator allocator, NativeArrayOptions options = NativeArrayOptions.ClearMemory) where T : struct {
|
||||
if (arr.IsCreated && arr.Length >= newSize) return;
|
||||
|
||||
var newArr = new NativeArray<T>(newSize, allocator, options);
|
||||
if (arr.IsCreated) {
|
||||
// Copy over old data
|
||||
NativeArray<T>.Copy(arr, newArr, arr.Length);
|
||||
arr.Dispose();
|
||||
}
|
||||
arr = newArr;
|
||||
}
|
||||
|
||||
public static void Realloc<T>(ref T[] arr, int newSize) {
|
||||
if (arr == null) {
|
||||
arr = new T[newSize];
|
||||
} else if (newSize > arr.Length) {
|
||||
var newArr = new T[newSize];
|
||||
arr.CopyTo(newArr, 0);
|
||||
arr = newArr;
|
||||
}
|
||||
}
|
||||
|
||||
public static T[] UnsafeAppendBufferToArray<T>(UnsafeAppendBuffer src) where T : unmanaged {
|
||||
var elementCount = src.Length / UnsafeUtility.SizeOf<T>();
|
||||
var dst = new T[elementCount];
|
||||
|
||||
unsafe {
|
||||
var gCHandle = System.Runtime.InteropServices.GCHandle.Alloc(dst, System.Runtime.InteropServices.GCHandleType.Pinned);
|
||||
System.IntPtr value = gCHandle.AddrOfPinnedObject();
|
||||
UnsafeUtility.MemCpy((byte*)(void*)value, src.Ptr, (long)elementCount * (long)UnsafeUtility.SizeOf<T>());
|
||||
gCHandle.Free();
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
public static void Rotate3DArray<T>(T[] arr, int3 size, int dx, int dz) {
|
||||
int width = size.x;
|
||||
int height = size.y;
|
||||
int depth = size.z;
|
||||
dx = dx % width;
|
||||
dz = dz % depth;
|
||||
if (dx != 0) {
|
||||
if (dx < 0) dx = width + dx;
|
||||
var tmp = ArrayPool<T>.Claim(dx);
|
||||
for (int y = 0; y < height; y++) {
|
||||
var offset = y * width * depth;
|
||||
for (int z = 0; z < depth; z++) {
|
||||
Array.Copy(arr, offset + z * width + width - dx, tmp, 0, dx);
|
||||
Array.Copy(arr, offset + z * width, arr, offset + z * width + dx, width - dx);
|
||||
Array.Copy(tmp, 0, arr, offset + z * width, dx);
|
||||
}
|
||||
}
|
||||
ArrayPool<T>.Release(ref tmp);
|
||||
}
|
||||
|
||||
if (dz != 0) {
|
||||
if (dz < 0) dz = depth + dz;
|
||||
var tmp = ArrayPool<T>.Claim(dz * width);
|
||||
for (int y = 0; y < height; y++) {
|
||||
var offset = y * width * depth;
|
||||
Array.Copy(arr, offset + (depth - dz) * width, tmp, 0, dz * width);
|
||||
Array.Copy(arr, offset, arr, offset + dz * width, (depth - dz) * width);
|
||||
Array.Copy(tmp, 0, arr, offset, dz * width);
|
||||
}
|
||||
ArrayPool<T>.Release(ref tmp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9bdecddfdfec947eb8ed96282e4b1fe1
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
359
Packages/com.arongranberg.astar/Core/Collections/BinaryHeap.cs
Normal file
359
Packages/com.arongranberg.astar/Core/Collections/BinaryHeap.cs
Normal file
@@ -0,0 +1,359 @@
|
||||
// #define VALIDATE_BINARY_HEAP
|
||||
#pragma warning disable 162
|
||||
#pragma warning disable 429
|
||||
using Unity.Mathematics;
|
||||
using Unity.Collections;
|
||||
using Unity.Burst;
|
||||
using Unity.Burst.CompilerServices;
|
||||
|
||||
namespace Pathfinding {
|
||||
using Pathfinding.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Binary heap implementation.
|
||||
/// Binary heaps are really fast for ordering nodes in a way that
|
||||
/// makes it possible to get the node with the lowest F score.
|
||||
/// Also known as a priority queue.
|
||||
///
|
||||
/// This has actually been rewritten as a 4-ary heap
|
||||
/// for performance, but it's the same principle.
|
||||
///
|
||||
/// See: http://en.wikipedia.org/wiki/Binary_heap
|
||||
/// See: https://en.wikipedia.org/wiki/D-ary_heap
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
public struct BinaryHeap {
|
||||
/// <summary>Number of items in the tree</summary>
|
||||
public int numberOfItems;
|
||||
|
||||
/// <summary>The tree will grow by at least this factor every time it is expanded</summary>
|
||||
public const float GrowthFactor = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Number of children of each node in the tree.
|
||||
/// Different values have been tested and 4 has been empirically found to perform the best.
|
||||
/// See: https://en.wikipedia.org/wiki/D-ary_heap
|
||||
/// </summary>
|
||||
const int D = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Sort nodes by G score if there is a tie when comparing the F score.
|
||||
/// Disabling this will improve pathfinding performance with around 2.5%
|
||||
/// but may break ties between paths that have the same length in a less
|
||||
/// desirable manner (only relevant for grid graphs).
|
||||
/// </summary>
|
||||
const bool SortGScores = true;
|
||||
|
||||
public const ushort NotInHeap = 0xFFFF;
|
||||
|
||||
/// <summary>Internal backing array for the heap</summary>
|
||||
private UnsafeSpan<HeapNode> heap;
|
||||
|
||||
/// <summary>True if the heap does not contain any elements</summary>
|
||||
public bool isEmpty => numberOfItems <= 0;
|
||||
|
||||
/// <summary>Item in the heap</summary>
|
||||
private struct HeapNode {
|
||||
public uint pathNodeIndex;
|
||||
/// <summary>Bitpacked F and G scores</summary>
|
||||
public ulong sortKey;
|
||||
|
||||
public HeapNode (uint pathNodeIndex, uint g, uint f) {
|
||||
this.pathNodeIndex = pathNodeIndex;
|
||||
this.sortKey = ((ulong)f << 32) | (ulong)g;
|
||||
}
|
||||
|
||||
public uint F {
|
||||
get => (uint)(sortKey >> 32);
|
||||
set => sortKey = (sortKey & 0xFFFFFFFFUL) | ((ulong)value << 32);
|
||||
}
|
||||
|
||||
public uint G => (uint)sortKey;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rounds up v so that it has remainder 1 when divided by D.
|
||||
/// I.e it is of the form n*D + 1 where n is any non-negative integer.
|
||||
/// </summary>
|
||||
static int RoundUpToNextMultipleMod1 (int v) {
|
||||
// I have a feeling there is a nicer way to do this
|
||||
return v + (4 - ((v-1) % D)) % D;
|
||||
}
|
||||
|
||||
/// <summary>Create a new heap with the specified initial capacity</summary>
|
||||
public BinaryHeap (int capacity) {
|
||||
// Make sure the size has remainder 1 when divided by D
|
||||
// This allows us to always guarantee that indices used in the Remove method
|
||||
// will never throw out of bounds exceptions
|
||||
capacity = RoundUpToNextMultipleMod1(capacity);
|
||||
|
||||
heap = new UnsafeSpan<HeapNode>(Unity.Collections.Allocator.Persistent, capacity);
|
||||
numberOfItems = 0;
|
||||
}
|
||||
|
||||
public void Dispose () {
|
||||
unsafe {
|
||||
AllocatorManager.Free<HeapNode>(Allocator.Persistent, heap.ptr, heap.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes all elements from the heap</summary>
|
||||
public void Clear (UnsafeSpan<PathNode> pathNodes) {
|
||||
// Clear all heap indices, and references to the heap nodes
|
||||
for (int i = 0; i < numberOfItems; i++) {
|
||||
pathNodes[heap[i].pathNodeIndex].heapIndex = NotInHeap;
|
||||
}
|
||||
|
||||
numberOfItems = 0;
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public uint GetPathNodeIndex(int heapIndex) => heap[heapIndex].pathNodeIndex;
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public uint GetG(int heapIndex) => heap[heapIndex].G;
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public uint GetF(int heapIndex) => heap[heapIndex].F;
|
||||
|
||||
public void SetH (int heapIndex, uint h) {
|
||||
heap[heapIndex].F = heap[heapIndex].G + h;
|
||||
}
|
||||
|
||||
/// <summary>Expands to a larger backing array when the current one is too small</summary>
|
||||
static void Expand (ref UnsafeSpan<HeapNode> heap) {
|
||||
// 65533 == 1 mod 4 and slightly smaller than 1<<16 = 65536
|
||||
int newSize = math.max(heap.Length+4, math.min(65533, (int)math.round(heap.Length*GrowthFactor)));
|
||||
|
||||
// Make sure the size has remainder 1 when divided by D
|
||||
// This allows us to always guarantee that indices used in the Remove method
|
||||
// will never throw out of bounds exceptions
|
||||
newSize = RoundUpToNextMultipleMod1(newSize);
|
||||
|
||||
// Check if the heap is really large
|
||||
// Also note that heaps larger than this are not supported
|
||||
// since PathNode.heapIndex is a ushort and can only store
|
||||
// values up to 65535 (NotInHeap = 65535 is reserved however)
|
||||
if (newSize > (1<<16) - 2) {
|
||||
throw new System.Exception("Binary Heap Size really large (>65534). A heap size this large is probably the cause of pathfinding running in an infinite loop. ");
|
||||
}
|
||||
|
||||
var newHeap = new UnsafeSpan<HeapNode>(Unity.Collections.Allocator.Persistent, newSize);
|
||||
newHeap.CopyFrom(heap);
|
||||
unsafe {
|
||||
AllocatorManager.Free<HeapNode>(Allocator.Persistent, heap.ptr, heap.Length);
|
||||
}
|
||||
#if ASTARDEBUG
|
||||
UnityEngine.Debug.Log("Resizing binary heap to "+newSize);
|
||||
#endif
|
||||
heap = newHeap;
|
||||
}
|
||||
|
||||
/// <summary>Adds a node to the heap</summary>
|
||||
public void Add (UnsafeSpan<PathNode> nodes, uint pathNodeIndex, uint g, uint f) {
|
||||
Add(ref this, ref nodes, pathNodeIndex, g, f);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
static void Add (ref BinaryHeap binaryHeap, ref UnsafeSpan<PathNode> nodes, uint pathNodeIndex, uint g, uint f) {
|
||||
ref var numberOfItems = ref binaryHeap.numberOfItems;
|
||||
ref var heap = ref binaryHeap.heap;
|
||||
|
||||
// Check if node is already in the heap
|
||||
ref var node = ref nodes[pathNodeIndex];
|
||||
if (node.heapIndex != NotInHeap) {
|
||||
var activeNode = new HeapNode(pathNodeIndex, g, f);
|
||||
UnityEngine.Assertions.Assert.AreEqual(activeNode.pathNodeIndex, pathNodeIndex);
|
||||
DecreaseKey(heap, nodes, activeNode, node.heapIndex);
|
||||
Validate(ref nodes, ref binaryHeap);
|
||||
} else {
|
||||
if (numberOfItems == heap.Length) {
|
||||
Expand(ref heap);
|
||||
}
|
||||
Validate(ref nodes, ref binaryHeap);
|
||||
|
||||
DecreaseKey(heap, nodes, new HeapNode(pathNodeIndex, g, f), (ushort)numberOfItems);
|
||||
numberOfItems++;
|
||||
Validate(ref nodes, ref binaryHeap);
|
||||
}
|
||||
}
|
||||
|
||||
static void DecreaseKey (UnsafeSpan<HeapNode> heap, UnsafeSpan<PathNode> nodes, HeapNode node, ushort index) {
|
||||
// This is where 'obj' is in the binary heap logically speaking
|
||||
// (for performance reasons we don't actually store it there until
|
||||
// we know the final index, that's just a waste of CPU cycles)
|
||||
uint bubbleIndex = index;
|
||||
|
||||
while (bubbleIndex != 0) {
|
||||
// Parent node of the bubble node
|
||||
uint parentIndex = (bubbleIndex-1) / D;
|
||||
|
||||
Hint.Assume(parentIndex < heap.length);
|
||||
Hint.Assume(bubbleIndex < heap.length);
|
||||
if (node.sortKey < heap[parentIndex].sortKey) {
|
||||
// Swap the bubble node and parent node
|
||||
// (we don't really need to store the bubble node until we know the final index though
|
||||
// so we do that after the loop instead)
|
||||
heap[bubbleIndex] = heap[parentIndex];
|
||||
nodes[heap[bubbleIndex].pathNodeIndex].heapIndex = (ushort)bubbleIndex;
|
||||
bubbleIndex = parentIndex;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Hint.Assume(bubbleIndex < heap.length);
|
||||
heap[bubbleIndex] = node;
|
||||
nodes[node.pathNodeIndex].heapIndex = (ushort)bubbleIndex;
|
||||
}
|
||||
|
||||
/// <summary>Returns the node with the lowest F score from the heap</summary>
|
||||
public uint Remove (UnsafeSpan<PathNode> nodes, out uint g, out uint f) {
|
||||
return Remove(ref nodes, ref this, out g, out f);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
static uint Remove (ref UnsafeSpan<PathNode> nodes, ref BinaryHeap binaryHeap, [NoAlias] out uint removedG, [NoAlias] out uint removedF) {
|
||||
ref var numberOfItems = ref binaryHeap.numberOfItems;
|
||||
var heap = binaryHeap.heap;
|
||||
|
||||
if (numberOfItems == 0) {
|
||||
throw new System.InvalidOperationException("Removing item from empty heap");
|
||||
}
|
||||
|
||||
// This is the smallest item in the heap.
|
||||
// Mark it as removed from the heap.
|
||||
Hint.Assume(0UL < heap.length);
|
||||
uint returnIndex = heap[0].pathNodeIndex;
|
||||
nodes[returnIndex].heapIndex = NotInHeap;
|
||||
removedG = heap[0].G;
|
||||
removedF = heap[0].F;
|
||||
|
||||
numberOfItems--;
|
||||
if (numberOfItems == 0) {
|
||||
return returnIndex;
|
||||
}
|
||||
|
||||
// Last item in the heap array
|
||||
Hint.Assume((uint)numberOfItems < heap.length);
|
||||
var swapItem = heap[numberOfItems];
|
||||
uint swapIndex = 0;
|
||||
ulong comparisonKey = swapItem.sortKey;
|
||||
|
||||
// Trickle upwards
|
||||
while (true) {
|
||||
var parent = swapIndex;
|
||||
uint pd = parent * D + 1;
|
||||
|
||||
// If this holds, then the indices used
|
||||
// below are guaranteed to not throw an index out of bounds
|
||||
// exception since we choose the size of the array in that way
|
||||
if (pd < numberOfItems) {
|
||||
// Find the child node with the smallest F score, or if equal, the smallest G score.
|
||||
// The sorting key is the tuple (F,G).
|
||||
// However, to be able to easily get the smallest index, we steal the lowest 2 bits of G
|
||||
// and use it for the child index (0..3) instead.
|
||||
// This means that tie-breaking will not be perfect, but in all practical cases it will
|
||||
// yield exactly the same result since G scores typically differ by more than 4 anyway.
|
||||
Hint.Assume(pd+0 < heap.length);
|
||||
ulong l0 = (heap[pd+0].sortKey & ~0x3UL) | 0;
|
||||
Hint.Assume(pd+1 < heap.length);
|
||||
ulong l1 = (heap[pd+1].sortKey & ~0x3UL) | 1;
|
||||
Hint.Assume(pd+2 < heap.length);
|
||||
ulong l2 = (heap[pd+2].sortKey & ~0x3UL) | 2;
|
||||
Hint.Assume(pd+3 < heap.length);
|
||||
ulong l3 = (heap[pd+3].sortKey & ~0x3UL) | 3;
|
||||
|
||||
ulong smallest = l0;
|
||||
// Not all children may exist, so we need to check that the index is valid
|
||||
if (pd+1 < numberOfItems) smallest = math.min(smallest, l1);
|
||||
if (pd+2 < numberOfItems) smallest = math.min(smallest, l2);
|
||||
if (pd+3 < numberOfItems) smallest = math.min(smallest, l3);
|
||||
|
||||
if (smallest < comparisonKey) {
|
||||
swapIndex = pd + (uint)(smallest & 0x3UL);
|
||||
|
||||
// One if the parent's children are smaller or equal, swap them
|
||||
// (actually we are just pretenting we swapped them, we hold the swapItem
|
||||
// in local variable and only assign it once we know the final index)
|
||||
Hint.Assume(parent < heap.length);
|
||||
Hint.Assume(swapIndex < heap.length);
|
||||
heap[parent] = heap[swapIndex];
|
||||
Hint.Assume(swapIndex < heap.length);
|
||||
nodes[heap[swapIndex].pathNodeIndex].heapIndex = (ushort)parent;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign element to the final position
|
||||
Hint.Assume(swapIndex < heap.length);
|
||||
heap[swapIndex] = swapItem;
|
||||
nodes[swapItem.pathNodeIndex].heapIndex = (ushort)swapIndex;
|
||||
|
||||
// For debugging
|
||||
Validate(ref nodes, ref binaryHeap);
|
||||
|
||||
return returnIndex;
|
||||
}
|
||||
|
||||
[System.Diagnostics.Conditional("VALIDATE_BINARY_HEAP")]
|
||||
static void Validate (ref UnsafeSpan<PathNode> nodes, ref BinaryHeap binaryHeap) {
|
||||
for (int i = 1; i < binaryHeap.numberOfItems; i++) {
|
||||
int parentIndex = (i-1)/D;
|
||||
if (binaryHeap.heap[parentIndex].F > binaryHeap.heap[i].F) {
|
||||
throw new System.Exception("Invalid state at " + i + ":" + parentIndex + " ( " + binaryHeap.heap[parentIndex].F + " > " + binaryHeap.heap[i].F + " ) ");
|
||||
}
|
||||
|
||||
if (binaryHeap.heap[parentIndex].sortKey > binaryHeap.heap[i].sortKey) {
|
||||
throw new System.Exception("Invalid state at " + i + ":" + parentIndex + " ( " + binaryHeap.heap[parentIndex].F + " > " + binaryHeap.heap[i].F + " ) ");
|
||||
}
|
||||
|
||||
if (nodes[binaryHeap.heap[i].pathNodeIndex].heapIndex != i) {
|
||||
throw new System.Exception("Invalid heap index");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rebuilds the heap by trickeling down all items.
|
||||
/// Usually called after the hTarget on a path has been changed
|
||||
/// </summary>
|
||||
public void Rebuild (UnsafeSpan<PathNode> nodes) {
|
||||
#if ASTARDEBUG
|
||||
int changes = 0;
|
||||
#endif
|
||||
|
||||
for (int i = 2; i < numberOfItems; i++) {
|
||||
int bubbleIndex = i;
|
||||
var node = heap[i];
|
||||
uint nodeF = node.F;
|
||||
while (bubbleIndex != 1) {
|
||||
int parentIndex = bubbleIndex / D;
|
||||
|
||||
if (nodeF < heap[parentIndex].F) {
|
||||
heap[bubbleIndex] = heap[parentIndex];
|
||||
nodes[heap[bubbleIndex].pathNodeIndex].heapIndex = (ushort)bubbleIndex;
|
||||
|
||||
heap[parentIndex] = node;
|
||||
nodes[heap[parentIndex].pathNodeIndex].heapIndex = (ushort)parentIndex;
|
||||
|
||||
bubbleIndex = parentIndex;
|
||||
#if ASTARDEBUG
|
||||
changes++;
|
||||
#endif
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if ASTARDEBUG
|
||||
UnityEngine.Debug.Log("+++ Rebuilt Heap - "+changes+" changes +++");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb4299e8747f44ad2b4e086752108ea3
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
@@ -0,0 +1,241 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Pathfinding.Pooling;
|
||||
|
||||
namespace Pathfinding.Collections {
|
||||
/// <summary>
|
||||
/// Implements an efficient circular buffer that can be appended to in both directions.
|
||||
///
|
||||
/// See: <see cref="NativeCircularBuffer"/>
|
||||
/// </summary>
|
||||
public struct CircularBuffer<T> : IReadOnlyList<T>, IReadOnlyCollection<T> {
|
||||
internal T[] data;
|
||||
internal int head;
|
||||
int length;
|
||||
|
||||
/// <summary>Number of items in the buffer</summary>
|
||||
public readonly int Length => length;
|
||||
/// <summary>Absolute index of the first item in the buffer, may be negative or greater than <see cref="Length"/></summary>
|
||||
public readonly int AbsoluteStartIndex => head;
|
||||
/// <summary>Absolute index of the last item in the buffer, may be negative or greater than <see cref="Length"/></summary>
|
||||
public readonly int AbsoluteEndIndex => head + length - 1;
|
||||
|
||||
/// <summary>First item in the buffer, throws if the buffer is empty</summary>
|
||||
public readonly ref T First => ref data[head & (data.Length-1)];
|
||||
|
||||
/// <summary>Last item in the buffer, throws if the buffer is empty</summary>
|
||||
public readonly ref T Last => ref data[(head+length-1) & (data.Length-1)];
|
||||
|
||||
readonly int IReadOnlyCollection<T>.Count {
|
||||
get {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Create a new buffer with the given capacity</summary>
|
||||
public CircularBuffer(int initialCapacity) {
|
||||
data = ArrayPool<T>.Claim(initialCapacity);
|
||||
head = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new buffer using the given array as an internal store.
|
||||
/// This will take ownership of the given array.
|
||||
/// </summary>
|
||||
public CircularBuffer(T[] backingArray) {
|
||||
data = backingArray;
|
||||
head = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
/// <summary>Resets the buffer's length to zero. Does not clear the current allocation</summary>
|
||||
public void Clear () {
|
||||
length = 0;
|
||||
head = 0;
|
||||
}
|
||||
|
||||
/// <summary>Appends a list of items to the end of the buffer</summary>
|
||||
public void AddRange (List<T> items) {
|
||||
// TODO: Can be optimized
|
||||
for (int i = 0; i < items.Count; i++) PushEnd(items[i]);
|
||||
}
|
||||
|
||||
/// <summary>Pushes a new item to the start of the buffer</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public void PushStart (T item) {
|
||||
if (data == null || length >= data.Length) Grow();
|
||||
length += 1;
|
||||
head -= 1;
|
||||
this[0] = item;
|
||||
}
|
||||
|
||||
/// <summary>Pushes a new item to the end of the buffer</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public void PushEnd (T item) {
|
||||
if (data == null || length >= data.Length) Grow();
|
||||
length += 1;
|
||||
this[length-1] = item;
|
||||
}
|
||||
|
||||
/// <summary>Pushes a new item to the start or the end of the buffer</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public void Push (bool toStart, T item) {
|
||||
if (toStart) PushStart(item);
|
||||
else PushEnd(item);
|
||||
}
|
||||
|
||||
/// <summary>Removes and returns the first element</summary>
|
||||
public T PopStart () {
|
||||
if (length == 0) throw new System.InvalidOperationException();
|
||||
var r = this[0];
|
||||
head++;
|
||||
length--;
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>Removes and returns the last element</summary>
|
||||
public T PopEnd () {
|
||||
if (length == 0) throw new System.InvalidOperationException();
|
||||
var r = this[length-1];
|
||||
length--;
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>Pops either from the start or from the end of the buffer</summary>
|
||||
public T Pop (bool fromStart) {
|
||||
if (fromStart) return PopStart();
|
||||
else return PopEnd();
|
||||
}
|
||||
|
||||
/// <summary>Return either the first element or the last element</summary>
|
||||
public readonly T GetBoundaryValue (bool start) {
|
||||
return GetAbsolute(start ? AbsoluteStartIndex : AbsoluteEndIndex);
|
||||
}
|
||||
|
||||
/// <summary>Inserts an item at the given absolute index</summary>
|
||||
public void InsertAbsolute (int index, T item) {
|
||||
SpliceUninitializedAbsolute(index, 0, 1);
|
||||
data[index & (data.Length - 1)] = item;
|
||||
}
|
||||
|
||||
/// <summary>Removes toRemove items from the buffer, starting at startIndex, and then inserts the toInsert items at startIndex</summary>
|
||||
public void Splice (int startIndex, int toRemove, List<T> toInsert) {
|
||||
SpliceAbsolute(startIndex + head, toRemove, toInsert);
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="Splice"/>, but startIndex is an absolute index</summary>
|
||||
public void SpliceAbsolute (int startIndex, int toRemove, List<T> toInsert) {
|
||||
if (toInsert == null) {
|
||||
SpliceUninitializedAbsolute(startIndex, toRemove, 0);
|
||||
} else {
|
||||
SpliceUninitializedAbsolute(startIndex, toRemove, toInsert.Count);
|
||||
for (int i = 0; i < toInsert.Count; i++) data[(startIndex + i) & (data.Length - 1)] = toInsert[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="Splice"/>, but the newly inserted items are left in an uninitialized state</summary>
|
||||
public void SpliceUninitialized (int startIndex, int toRemove, int toInsert) {
|
||||
SpliceUninitializedAbsolute(startIndex + head, toRemove, toInsert);
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="SpliceUninitialized"/>, but startIndex is an absolute index</summary>
|
||||
public void SpliceUninitializedAbsolute (int startIndex, int toRemove, int toInsert) {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (startIndex - head < 0 || startIndex + toRemove - head > length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
var itemsToAdd = toInsert - toRemove;
|
||||
while (this.length + itemsToAdd > this.data.Length) Grow();
|
||||
|
||||
// move items [startIndex+length .. end] itemsToAdd steps forward in the array
|
||||
MoveAbsolute(startIndex + toRemove, AbsoluteEndIndex, itemsToAdd);
|
||||
this.length += itemsToAdd;
|
||||
}
|
||||
|
||||
void MoveAbsolute (int startIndex, int endIndex, int deltaIndex) {
|
||||
if (deltaIndex > 0) {
|
||||
for (int i = endIndex; i >= startIndex; i--) data[(i+deltaIndex) & (data.Length-1)] = data[i & (data.Length-1)];
|
||||
} else if (deltaIndex < 0) {
|
||||
for (int i = startIndex; i <= endIndex; i++) data[(i+deltaIndex) & (data.Length-1)] = data[i & (data.Length-1)];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Indexes the buffer, with index 0 being the first element</summary>
|
||||
public T this[int index] {
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
readonly get {
|
||||
#if UNITY_EDITOR
|
||||
if ((uint)index >= length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
return data[(index+head) & (data.Length-1)];
|
||||
}
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
set {
|
||||
#if UNITY_EDITOR
|
||||
if ((uint)index >= length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
data[(index+head) & (data.Length-1)] = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indexes the buffer using absolute indices.
|
||||
/// When pushing to and popping from the buffer, the absolute indices do not change.
|
||||
/// So e.g. after doing PushStart(x) on an empty buffer, GetAbsolute(-1) will get the newly pushed element.
|
||||
/// </summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public readonly T GetAbsolute (int index) {
|
||||
#if UNITY_EDITOR
|
||||
if ((uint)(index - head) >= length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
return data[index & (data.Length-1)];
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public readonly void SetAbsolute (int index, T value) {
|
||||
#if UNITY_EDITOR
|
||||
if ((uint)(index - head) >= length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
data[index & (data.Length-1)] = value;
|
||||
}
|
||||
|
||||
void Grow () {
|
||||
var newData = ArrayPool<T>.Claim(System.Math.Max(4, data != null ? data.Length*2 : 0));
|
||||
if (data != null) {
|
||||
var inOrderItems = data.Length - (head & (data.Length-1));
|
||||
System.Array.Copy(data, head & (data.Length-1), newData, head & (newData.Length - 1), inOrderItems);
|
||||
var wraparoundItems = length - inOrderItems;
|
||||
if (wraparoundItems > 0) System.Array.Copy(data, 0, newData, (head + inOrderItems) & (newData.Length - 1), wraparoundItems);
|
||||
ArrayPool<T>.Release(ref data);
|
||||
}
|
||||
data = newData;
|
||||
}
|
||||
|
||||
/// <summary>Release the backing array of this buffer back into an array pool</summary>
|
||||
public void Pool () {
|
||||
ArrayPool<T>.Release(ref data);
|
||||
length = 0;
|
||||
head = 0;
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator () {
|
||||
for (int i = 0; i < length; i++) {
|
||||
yield return this[i];
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator () {
|
||||
for (int i = 0; i < length; i++) {
|
||||
yield return this[i];
|
||||
}
|
||||
}
|
||||
|
||||
public CircularBuffer<T> Clone () {
|
||||
return new CircularBuffer<T> {
|
||||
data = data != null ? (T[])data.Clone() : null,
|
||||
length = length,
|
||||
head = head
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2c76961280d816f42a709dc7150208c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,288 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine.Assertions;
|
||||
|
||||
namespace Pathfinding.Collections {
|
||||
/// <summary>
|
||||
/// Thread-safe hierarchical bitset.
|
||||
///
|
||||
/// Stores an array of bits. Each bit can be set or cleared individually from any thread.
|
||||
///
|
||||
/// Note: Setting the capacity is not thread-safe, nor is iterating over the bitset while it is being modified.
|
||||
/// </summary>
|
||||
[BurstCompile]
|
||||
public struct HierarchicalBitset {
|
||||
UnsafeSpan<ulong> l1;
|
||||
UnsafeSpan<ulong> l2;
|
||||
UnsafeSpan<ulong> l3;
|
||||
Allocator allocator;
|
||||
|
||||
const int Log64 = 6;
|
||||
|
||||
public HierarchicalBitset (int size, Allocator allocator) {
|
||||
this.allocator = allocator;
|
||||
l1 = new UnsafeSpan<ulong>(allocator, (size + 64 - 1) >> Log64);
|
||||
l2 = new UnsafeSpan<ulong>(allocator, (size + (64*64 - 1)) >> Log64 >> Log64);
|
||||
l3 = new UnsafeSpan<ulong>(allocator, (size + (64*64*64 - 1)) >> Log64 >> Log64 >> Log64);
|
||||
l1.FillZeros();
|
||||
l2.FillZeros();
|
||||
l3.FillZeros();
|
||||
}
|
||||
|
||||
public bool IsCreated => Capacity > 0;
|
||||
|
||||
public void Dispose () {
|
||||
l1.Free(allocator);
|
||||
l2.Free(allocator);
|
||||
l3.Free(allocator);
|
||||
this = default;
|
||||
}
|
||||
|
||||
public int Capacity {
|
||||
get {
|
||||
return l1.Length << Log64;
|
||||
}
|
||||
set {
|
||||
if (value < Capacity) throw new System.ArgumentException("Shrinking the bitset is not supported");
|
||||
if (value == Capacity) return;
|
||||
var b = new HierarchicalBitset(value, allocator);
|
||||
|
||||
// Copy the old data
|
||||
l1.CopyTo(b.l1);
|
||||
l2.CopyTo(b.l2);
|
||||
l3.CopyTo(b.l3);
|
||||
|
||||
Dispose();
|
||||
this = b;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Number of set bits in the bitset</summary>
|
||||
public int Count () {
|
||||
int count = 0;
|
||||
for (int i = 0; i < l1.Length; i++) {
|
||||
count += math.countbits(l1[i]);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>True if the bitset is empty</summary>
|
||||
public bool IsEmpty {
|
||||
get {
|
||||
for (int i = 0; i < l3.Length; i++) {
|
||||
if (l3[i] != 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Clear all bits</summary>
|
||||
public void Clear () {
|
||||
// TODO: Optimize?
|
||||
l1.FillZeros();
|
||||
l2.FillZeros();
|
||||
l3.FillZeros();
|
||||
}
|
||||
|
||||
public void GetIndices (NativeList<int> result) {
|
||||
var buffer = new NativeArray<int>(256, Allocator.Temp);
|
||||
var iter = GetIterator(buffer.AsUnsafeSpan());
|
||||
while (iter.MoveNext()) {
|
||||
var span = iter.Current;
|
||||
for (int i = 0; i < span.Length; i++) {
|
||||
result.Add(span[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
static bool SetAtomic (ref UnsafeSpan<ulong> span, int index) {
|
||||
var cellIndex = index >> Log64;
|
||||
var currentValue = span[cellIndex];
|
||||
// Note: 1 << index will only use the lower 6 bits of index
|
||||
if ((currentValue & (1UL << index)) != 0) {
|
||||
// Bit already set
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Use Interlocked.Or in newer .net versions
|
||||
while (true) {
|
||||
var actualValue = (ulong)System.Threading.Interlocked.CompareExchange(ref UnsafeUtility.As<ulong, long>(ref span[cellIndex]), (long)(currentValue | (1UL << index)), (long)currentValue);
|
||||
if (actualValue != currentValue) currentValue = actualValue;
|
||||
else break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
static bool ResetAtomic (ref UnsafeSpan<ulong> span, int index) {
|
||||
var cellIndex = index >> Log64;
|
||||
var currentValue = span[cellIndex];
|
||||
// Note: 1 << index will only use the lower 6 bits of index
|
||||
if ((currentValue & (1UL << index)) == 0) {
|
||||
// Bit already cleared
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: Use Interlocked.Or in newer .net versions
|
||||
while (true) {
|
||||
var actualValue = (ulong)System.Threading.Interlocked.CompareExchange(ref UnsafeUtility.As<ulong, long>(ref span[cellIndex]), (long)(currentValue & ~(1UL << index)), (long)currentValue);
|
||||
if (actualValue != currentValue) currentValue = actualValue;
|
||||
else break;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Get the value of a bit</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public bool Get (int index) {
|
||||
// Note: 1 << index will only use the lower 6 bits of index
|
||||
return (l1[index >> Log64] & (1UL << index)) != 0;
|
||||
}
|
||||
|
||||
/// <summary>Set a given bit to 1</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public void Set (int index) {
|
||||
if (SetAtomic(ref l1, index)) return;
|
||||
SetAtomic(ref l2, index >> Log64);
|
||||
SetAtomic(ref l3, index >> (2*Log64));
|
||||
}
|
||||
|
||||
/// <summary>Set a given bit to 0</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public void Reset (int index) {
|
||||
if (ResetAtomic(ref l1, index)) return;
|
||||
if (l1[index >> Log64] == 0) ResetAtomic(ref l2, index >> Log64);
|
||||
if (l2[index >> (2*Log64)] == 0) ResetAtomic(ref l3, index >> (2*Log64));
|
||||
}
|
||||
|
||||
/// <summary>Get an iterator over all set bits.</summary>
|
||||
/// <param name="scratchBuffer">A buffer to use for temporary storage. A slice of this buffer will be returned on each iteration, filled with the indices of the set bits.</param>
|
||||
public Iterator GetIterator (UnsafeSpan<int> scratchBuffer) {
|
||||
return new Iterator(this, scratchBuffer);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public struct Iterator : IEnumerator<UnsafeSpan<int> >, IEnumerable<UnsafeSpan<int> > {
|
||||
HierarchicalBitset bitSet;
|
||||
UnsafeSpan<int> result;
|
||||
int resultCount;
|
||||
int l3index;
|
||||
int l3bitIndex;
|
||||
int l2bitIndex;
|
||||
|
||||
public UnsafeSpan<int> Current => result.Slice(0, resultCount);
|
||||
|
||||
object IEnumerator.Current => throw new System.NotImplementedException();
|
||||
|
||||
public void Reset() => throw new System.NotImplementedException();
|
||||
|
||||
public void Dispose () {}
|
||||
|
||||
public IEnumerator<UnsafeSpan<int> > GetEnumerator() => this;
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => throw new System.NotImplementedException();
|
||||
|
||||
static int l2index(int l3index, int l3bitIndex) => (l3index << Log64) + l3bitIndex;
|
||||
static int l1index(int l2index, int l2bitIndex) => (l2index << Log64) + l2bitIndex;
|
||||
|
||||
public Iterator (HierarchicalBitset bitSet, UnsafeSpan<int> result) {
|
||||
this.bitSet = bitSet;
|
||||
this.result = result;
|
||||
resultCount = 0;
|
||||
l3index = 0;
|
||||
l3bitIndex = 0;
|
||||
l2bitIndex = 0;
|
||||
if (result.Length < 128) {
|
||||
// Minimum is actually 64, but that can be very inefficient
|
||||
throw new System.ArgumentException("Result array must be at least 128 elements long");
|
||||
}
|
||||
}
|
||||
|
||||
public bool MoveNext () {
|
||||
return MoveNextBurst(ref this);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public static bool MoveNextBurst (ref Iterator iter) {
|
||||
return iter.MoveNextInternal();
|
||||
}
|
||||
|
||||
// Inline
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
bool MoveNextInternal () {
|
||||
// Store various data in local variables to avoid writing them to memory every time they are updated
|
||||
uint resultCount = 0;
|
||||
int l3index = this.l3index;
|
||||
int l3bitIndex = this.l3bitIndex;
|
||||
int l2bitIndex = this.l2bitIndex;
|
||||
Assert.IsTrue(l2bitIndex < 64 && l3bitIndex < 64);
|
||||
|
||||
for (; l3index < bitSet.l3.length; l3index++) {
|
||||
// Get the L3 cell, and mask out all bits we have already visited
|
||||
var l3cell = bitSet.l3[l3index] & (~0UL << l3bitIndex);
|
||||
if (l3cell == 0) continue;
|
||||
|
||||
while (l3cell != 0) {
|
||||
// Find the next set bit in the L3 cell
|
||||
l3bitIndex = math.tzcnt(l3cell);
|
||||
|
||||
// Nest check for level 2
|
||||
int l2index = Iterator.l2index(l3index, l3bitIndex);
|
||||
// The l2 cell is guaranteed to be non-zero, even after masking out the bits we have already visited
|
||||
var l2cell = bitSet.l2[l2index] & (~0UL << l2bitIndex);
|
||||
Assert.AreNotEqual(0, l2cell);
|
||||
|
||||
while (l2cell != 0) {
|
||||
l2bitIndex = math.tzcnt(l2cell);
|
||||
// Stop the loop if we have almost filled the result array
|
||||
// Each L1 cell may contain up to 64 set bits
|
||||
if (resultCount + 64 > result.Length) {
|
||||
this.resultCount = (int)resultCount;
|
||||
this.l3index = l3index;
|
||||
this.l3bitIndex = l3bitIndex;
|
||||
this.l2bitIndex = l2bitIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
int l1index = Iterator.l1index(l2index, l2bitIndex);
|
||||
var l1cell = bitSet.l1[l1index];
|
||||
int l1indexStart = l1index << Log64;
|
||||
Assert.AreNotEqual(0, l1cell);
|
||||
|
||||
while (l1cell != 0) {
|
||||
var l1bitIndex = math.tzcnt(l1cell);
|
||||
l1cell &= l1cell - 1UL; // clear lowest bit
|
||||
int index = l1indexStart + l1bitIndex;
|
||||
Unity.Burst.CompilerServices.Hint.Assume(resultCount < (uint)result.Length);
|
||||
result[resultCount++] = index;
|
||||
}
|
||||
|
||||
l2cell &= l2cell - 1UL;
|
||||
}
|
||||
|
||||
// Skip a bit at the L3 level
|
||||
l3cell &= l3cell - 1UL; // clear lowest bit
|
||||
// Enter new L2 level
|
||||
l2bitIndex = 0;
|
||||
}
|
||||
|
||||
l2bitIndex = 0;
|
||||
l3bitIndex = 0;
|
||||
}
|
||||
|
||||
this.resultCount = (int)resultCount;
|
||||
this.l3index = l3index;
|
||||
this.l3bitIndex = l3bitIndex;
|
||||
this.l2bitIndex = l2bitIndex;
|
||||
return resultCount > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c9a4ed21e5f87d40aba2fcc4e01146d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,320 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine.Assertions;
|
||||
|
||||
namespace Pathfinding.Collections {
|
||||
/// <summary>
|
||||
/// Implements an efficient circular buffer that can be appended to in both directions.
|
||||
///
|
||||
/// See: <see cref="CircularBuffer"/>
|
||||
/// </summary>
|
||||
public struct NativeCircularBuffer<T> : IReadOnlyList<T>, IReadOnlyCollection<T> where T : unmanaged {
|
||||
[NativeDisableUnsafePtrRestriction]
|
||||
internal unsafe T* data;
|
||||
internal int head;
|
||||
int length;
|
||||
/// <summary>Capacity of the allocation minus 1. Invariant: (a power of two) minus 1</summary>
|
||||
int capacityMask;
|
||||
|
||||
/// <summary>The allocator used to create the internal buffer.</summary>
|
||||
public AllocatorManager.AllocatorHandle Allocator;
|
||||
/// <summary>Number of items in the buffer</summary>
|
||||
public readonly int Length => length;
|
||||
/// <summary>Absolute index of the first item in the buffer, may be negative or greater than <see cref="Length"/></summary>
|
||||
public readonly int AbsoluteStartIndex => head;
|
||||
/// <summary>Absolute index of the last item in the buffer, may be negative or greater than <see cref="Length"/></summary>
|
||||
public readonly int AbsoluteEndIndex => head + length - 1;
|
||||
|
||||
/// <summary>First item in the buffer throws if the buffer is empty</summary>
|
||||
public readonly ref T First {
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
get {
|
||||
unsafe {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (length == 0) throw new System.InvalidOperationException();
|
||||
#endif
|
||||
return ref data[head & capacityMask];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Last item in the buffer, throws if the buffer is empty</summary>
|
||||
public readonly ref T Last {
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
get {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (length == 0) throw new System.InvalidOperationException();
|
||||
#endif
|
||||
unsafe { return ref data[(head+length-1) & capacityMask]; }
|
||||
}
|
||||
}
|
||||
|
||||
readonly int IReadOnlyCollection<T>.Count => Length;
|
||||
|
||||
public readonly bool IsCreated {
|
||||
get {
|
||||
unsafe {
|
||||
return data != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Create a new empty buffer</summary>
|
||||
|
||||
public NativeCircularBuffer(AllocatorManager.AllocatorHandle allocator) {
|
||||
unsafe {
|
||||
data = null;
|
||||
}
|
||||
Allocator = allocator;
|
||||
capacityMask = -1;
|
||||
head = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
/// <summary>Create a new buffer with the given capacity</summary>
|
||||
public NativeCircularBuffer(int initialCapacity, AllocatorManager.AllocatorHandle allocator) {
|
||||
initialCapacity = math.ceilpow2(initialCapacity);
|
||||
unsafe {
|
||||
data = AllocatorManager.Allocate<T>(allocator, initialCapacity);
|
||||
capacityMask = initialCapacity - 1;
|
||||
}
|
||||
Allocator = allocator;
|
||||
head = 0;
|
||||
length = 0;
|
||||
}
|
||||
|
||||
unsafe public NativeCircularBuffer(CircularBuffer<T> buffer, out ulong gcHandle) : this(buffer.data, buffer.head, buffer.Length, out gcHandle) {}
|
||||
|
||||
unsafe public NativeCircularBuffer(T[] data, int head, int length, out ulong gcHandle) {
|
||||
Assert.IsTrue((data.Length & (data.Length - 1)) == 0);
|
||||
Assert.IsTrue(length <= data.Length);
|
||||
unsafe {
|
||||
this.data = (T*)UnsafeUtility.PinGCArrayAndGetDataAddress(data, out gcHandle);
|
||||
}
|
||||
this.capacityMask = data.Length - 1;
|
||||
this.head = head;
|
||||
this.length = length;
|
||||
Allocator = Unity.Collections.Allocator.None;
|
||||
}
|
||||
|
||||
/// <summary>Resets the buffer's length to zero. Does not clear the current allocation</summary>
|
||||
public void Clear () {
|
||||
length = 0;
|
||||
head = 0;
|
||||
}
|
||||
|
||||
/// <summary>Appends a list of items to the end of the buffer</summary>
|
||||
public void AddRange (List<T> items) {
|
||||
// TODO: Can be optimized
|
||||
for (int i = 0; i < items.Count; i++) PushEnd(items[i]);
|
||||
}
|
||||
|
||||
/// <summary>Pushes a new item to the start of the buffer</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public void PushStart (T item) {
|
||||
if (length > capacityMask) Grow();
|
||||
length += 1;
|
||||
head -= 1;
|
||||
this[0] = item;
|
||||
}
|
||||
|
||||
/// <summary>Pushes a new item to the end of the buffer</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public void PushEnd (T item) {
|
||||
if (length > capacityMask) Grow();
|
||||
length += 1;
|
||||
this[length-1] = item;
|
||||
}
|
||||
|
||||
/// <summary>Pushes a new item to the start or the end of the buffer</summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public void Push (bool toStart, T item) {
|
||||
if (toStart) PushStart(item);
|
||||
else PushEnd(item);
|
||||
}
|
||||
|
||||
/// <summary>Removes and returns the first element</summary>
|
||||
public T PopStart () {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (length == 0) throw new System.InvalidOperationException();
|
||||
#endif
|
||||
var r = this[0];
|
||||
head++;
|
||||
length--;
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>Removes and returns the last element</summary>
|
||||
public T PopEnd () {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (length == 0) throw new System.InvalidOperationException();
|
||||
#endif
|
||||
var r = this[length-1];
|
||||
length--;
|
||||
return r;
|
||||
}
|
||||
|
||||
/// <summary>Pops either from the start or from the end of the buffer</summary>
|
||||
public T Pop (bool fromStart) {
|
||||
if (fromStart) return PopStart();
|
||||
else return PopEnd();
|
||||
}
|
||||
|
||||
/// <summary>Return either the first element or the last element</summary>
|
||||
public readonly T GetBoundaryValue (bool start) {
|
||||
return start ? GetAbsolute(AbsoluteStartIndex) : GetAbsolute(AbsoluteEndIndex);
|
||||
}
|
||||
|
||||
/// <summary>Lowers the length of the buffer to the given value, and does nothing if the given value is greater or equal to the current length</summary>
|
||||
|
||||
public void TrimTo (int length) {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (length < 0) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
this.length = math.min(this.length, length);
|
||||
}
|
||||
|
||||
/// <summary>Removes toRemove items from the buffer, starting at startIndex, and then inserts the toInsert items at startIndex</summary>
|
||||
|
||||
public void Splice (int startIndex, int toRemove, List<T> toInsert) {
|
||||
SpliceAbsolute(startIndex + head, toRemove, toInsert);
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="Splice"/>, but startIndex is an absolute index</summary>
|
||||
|
||||
public void SpliceAbsolute (int startIndex, int toRemove, List<T> toInsert) {
|
||||
SpliceUninitializedAbsolute(startIndex, toRemove, toInsert.Count);
|
||||
unsafe {
|
||||
for (int i = 0; i < toInsert.Count; i++) data[(startIndex + i) & capacityMask] = toInsert[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="Splice"/>, but the newly inserted items are left in an uninitialized state</summary>
|
||||
public void SpliceUninitialized (int startIndex, int toRemove, int toInsert) {
|
||||
SpliceUninitializedAbsolute(startIndex + head, toRemove, toInsert);
|
||||
}
|
||||
|
||||
/// <summary>Like <see cref="SpliceUninitialized"/>, but startIndex is an absolute index</summary>
|
||||
public void SpliceUninitializedAbsolute (int startIndex, int toRemove, int toInsert) {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (startIndex - head < 0 || startIndex + toRemove - head > length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
var itemsToAdd = toInsert - toRemove;
|
||||
while (this.length + itemsToAdd > capacityMask + 1) Grow();
|
||||
|
||||
// move items [startIndex+length .. end] itemsToAdd steps forward in the array
|
||||
MoveAbsolute(startIndex + toRemove, AbsoluteEndIndex, itemsToAdd);
|
||||
this.length += itemsToAdd;
|
||||
}
|
||||
|
||||
void MoveAbsolute (int startIndex, int endIndex, int deltaIndex) {
|
||||
unsafe {
|
||||
if (deltaIndex > 0) {
|
||||
for (int i = endIndex; i >= startIndex; i--) data[(i+deltaIndex) & capacityMask] = data[i & capacityMask];
|
||||
} else if (deltaIndex < 0) {
|
||||
for (int i = startIndex; i <= endIndex; i++) data[(i+deltaIndex) & capacityMask] = data[i & capacityMask];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Indexes the buffer, with index 0 being the first element</summary>
|
||||
public T this[int index] {
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
readonly get {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if ((uint)index >= length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
unsafe {
|
||||
return data[(index+head) & capacityMask];
|
||||
}
|
||||
}
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
set {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if ((uint)index >= length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
unsafe {
|
||||
data[(index+head) & capacityMask] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indexes the buffer using absolute indices.
|
||||
/// When pushing to and popping from the buffer, the absolute indices do not change.
|
||||
/// So e.g. after doing PushStart(x) on an empty buffer, GetAbsolute(-1) will get the newly pushed element.
|
||||
/// </summary>
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
public readonly T GetAbsolute (int index) {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if ((uint)(index - head) >= length) throw new System.ArgumentOutOfRangeException();
|
||||
#endif
|
||||
unsafe {
|
||||
return data[index & capacityMask];
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
|
||||
void Grow () {
|
||||
unsafe {
|
||||
// Note: Will always be a power of 2 since capacity is a power of 2
|
||||
var capacity = capacityMask + 1;
|
||||
var newCapacity = math.max(4, capacity*2);
|
||||
var newData = AllocatorManager.Allocate<T>(this.Allocator, newCapacity);
|
||||
if (data != null) {
|
||||
var inOrderItems = capacity - (head & capacityMask);
|
||||
UnsafeUtility.MemCpy(newData + (head & (newCapacity - 1)), data + (head & capacityMask), inOrderItems * sizeof(T));
|
||||
var wraparoundItems = length - inOrderItems;
|
||||
if (wraparoundItems > 0) {
|
||||
UnsafeUtility.MemCpy(newData + ((head + inOrderItems) & (newCapacity - 1)), data, wraparoundItems * sizeof(T));
|
||||
}
|
||||
AllocatorManager.Free(Allocator, data);
|
||||
}
|
||||
capacityMask = newCapacity - 1;
|
||||
data = newData;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Releases the unmanaged memory held by this container</summary>
|
||||
public void Dispose () {
|
||||
capacityMask = -1;
|
||||
length = 0;
|
||||
head = 0;
|
||||
unsafe {
|
||||
AllocatorManager.Free(Allocator, data);
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator () {
|
||||
for (int i = 0; i < length; i++) {
|
||||
yield return this[i];
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator () {
|
||||
for (int i = 0; i < length; i++) {
|
||||
yield return this[i];
|
||||
}
|
||||
}
|
||||
|
||||
public NativeCircularBuffer<T> Clone () {
|
||||
unsafe {
|
||||
if (!IsCreated) return default;
|
||||
|
||||
var newData = AllocatorManager.Allocate<T>(this.Allocator, capacityMask + 1);
|
||||
UnsafeUtility.MemCpy(newData, data, length * sizeof(T));
|
||||
return new NativeCircularBuffer<T> {
|
||||
data = newData,
|
||||
head = head,
|
||||
length = length,
|
||||
capacityMask = capacityMask,
|
||||
Allocator = this.Allocator
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c126a7f5fd26b684984ddef8030409f9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,323 @@
|
||||
// #define DEBUG_ALLOCATOR
|
||||
namespace Pathfinding.Collections {
|
||||
using Unity.Mathematics;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
|
||||
/// <summary>
|
||||
/// A tiny slab allocator.
|
||||
/// Allocates spans of type T in power-of-two sized blocks.
|
||||
///
|
||||
/// Note: This allocator has no support for merging adjacent freed blocks.
|
||||
/// Therefore it is best suited for similarly sized allocations which are relatively small.
|
||||
///
|
||||
/// Can be used in burst jobs.
|
||||
///
|
||||
/// This is faster than allocating NativeArrays using the Temp allocator, and significantly faster
|
||||
/// than allocating them using the Persistent allocator.
|
||||
/// </summary>
|
||||
public struct SlabAllocator<T> where T : unmanaged {
|
||||
/// <summary>Allocation which is always invalid</summary>
|
||||
public const int InvalidAllocation = -2;
|
||||
/// <summary>Allocation representing a zero-length array</summary>
|
||||
public const int ZeroLengthArray = -1;
|
||||
|
||||
// The max number of items we are likely to need to allocate comes from the connections array of each hierarchical node.
|
||||
// If you have a ton (thousands) of off-mesh links next to each other, then that array can get large.
|
||||
public const int MaxAllocationSizeIndex = 12;
|
||||
public const int MaxAllocationSize = 1 << MaxAllocationSizeIndex;
|
||||
|
||||
internal static int SizeIndexToElements (int sizeIndex) {
|
||||
return 1 << sizeIndex;
|
||||
}
|
||||
|
||||
internal static int ElementsToSizeIndex (int nElements) {
|
||||
if (nElements < 0) throw new System.Exception("SlabAllocator cannot allocate less than 1 element");
|
||||
if (nElements == 0) return 0;
|
||||
int sizeIndex = CollectionHelper.Log2Ceil(nElements);
|
||||
if (sizeIndex > MaxAllocationSizeIndex) throw new System.Exception("SlabAllocator cannot allocate more than MaxAllocationSize elements.");
|
||||
return sizeIndex;
|
||||
}
|
||||
|
||||
const uint UsedBit = 1u << 31;
|
||||
const uint AllocatedBit = 1u << 30;
|
||||
const uint LengthMask = AllocatedBit - 1;
|
||||
public bool IsDebugAllocator => false;
|
||||
|
||||
[NativeDisableUnsafePtrRestriction]
|
||||
unsafe AllocatorData* data;
|
||||
|
||||
struct AllocatorData {
|
||||
public UnsafeList<byte> mem;
|
||||
public unsafe fixed int freeHeads[MaxAllocationSizeIndex+1];
|
||||
}
|
||||
|
||||
struct Header {
|
||||
public uint length;
|
||||
}
|
||||
|
||||
struct NextBlock {
|
||||
public int next;
|
||||
}
|
||||
|
||||
public bool IsCreated {
|
||||
get {
|
||||
unsafe {
|
||||
return data != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ByteSize {
|
||||
get {
|
||||
unsafe {
|
||||
return data->mem.Length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SlabAllocator(int initialCapacityBytes, AllocatorManager.AllocatorHandle allocator) {
|
||||
unsafe {
|
||||
data = AllocatorManager.Allocate<AllocatorData>(allocator);
|
||||
data->mem = new UnsafeList<byte>(initialCapacityBytes, allocator);
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees all existing allocations.
|
||||
/// Does not free the underlaying unmanaged memory. Use <see cref="Dispose"/> for that.
|
||||
/// </summary>
|
||||
public void Clear () {
|
||||
CheckDisposed();
|
||||
unsafe {
|
||||
data->mem.Clear();
|
||||
for (int i = 0; i < MaxAllocationSizeIndex + 1; i++) {
|
||||
data->freeHeads[i] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Get the span representing the given allocation.
|
||||
/// The returned array does not need to be disposed.
|
||||
/// It is only valid until the next call to <see cref="Allocate"/>, <see cref="Free"/> or <see cref="Dispose"/>.
|
||||
/// </summary>
|
||||
public UnsafeSpan<T> GetSpan (int allocatedIndex) {
|
||||
CheckDisposed();
|
||||
unsafe {
|
||||
if (allocatedIndex == ZeroLengthArray) return new UnsafeSpan<T>(null, 0);
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException($"Invalid allocation {allocatedIndex}");
|
||||
#endif
|
||||
var ptr = data->mem.Ptr + allocatedIndex;
|
||||
var header = (Header*)(ptr - sizeof(Header));
|
||||
var length = header->length & LengthMask;
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception($"Invalid allocation {allocatedIndex}");
|
||||
if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to get a span for an unallocated index");
|
||||
#endif
|
||||
return new UnsafeSpan<T>(ptr, (int)length);
|
||||
}
|
||||
}
|
||||
|
||||
public void Realloc (ref int allocatedIndex, int nElements) {
|
||||
CheckDisposed();
|
||||
if (allocatedIndex == ZeroLengthArray) {
|
||||
allocatedIndex = Allocate(nElements);
|
||||
return;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException();
|
||||
#endif
|
||||
var ptr = data->mem.Ptr + allocatedIndex;
|
||||
var header = (Header*)(ptr - sizeof(Header));
|
||||
var length = header->length & LengthMask;
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception("Invalid index");
|
||||
if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to get a span for an unallocated index");
|
||||
#endif
|
||||
var capacityIndex = ElementsToSizeIndex((int)length);
|
||||
var newCapacityIndex = ElementsToSizeIndex((int)nElements);
|
||||
if (capacityIndex == newCapacityIndex) {
|
||||
header->length = (uint)nElements | AllocatedBit | UsedBit;
|
||||
} else {
|
||||
int newAllocation = Allocate(nElements);
|
||||
var oldSpan = GetSpan(allocatedIndex);
|
||||
var newSpan = GetSpan(newAllocation);
|
||||
oldSpan.Slice(0, math.min((int)length, nElements)).CopyTo(newSpan);
|
||||
Free(allocatedIndex);
|
||||
allocatedIndex = newAllocation;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates an array big enough to fit the given values and copies them to the new allocation.
|
||||
/// Returns: An ID for the new allocation.
|
||||
/// </summary>
|
||||
public int Allocate (System.Collections.Generic.List<T> values) {
|
||||
var index = Allocate(values.Count);
|
||||
var span = GetSpan(index);
|
||||
for (int i = 0; i < span.Length; i++) span[i] = values[i];
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates an array big enough to fit the given values and copies them to the new allocation.
|
||||
/// Returns: An ID for the new allocation.
|
||||
/// </summary>
|
||||
public int Allocate (NativeList<T> values) {
|
||||
var index = Allocate(values.Length);
|
||||
GetSpan(index).CopyFrom(values.AsArray());
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates an array of type T with length nElements.
|
||||
/// Must later be freed using <see cref="Free"/> (or <see cref="Dispose)"/>.
|
||||
///
|
||||
/// Returns: An ID for the new allocation.
|
||||
/// </summary>
|
||||
public int Allocate (int nElements) {
|
||||
CheckDisposed();
|
||||
if (nElements == 0) return ZeroLengthArray;
|
||||
var sizeIndex = ElementsToSizeIndex(nElements);
|
||||
unsafe {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (sizeIndex < 0 || sizeIndex > MaxAllocationSizeIndex) throw new System.Exception("Invalid size index " + sizeIndex);
|
||||
#endif
|
||||
int head = data->freeHeads[sizeIndex];
|
||||
if (head != -1) {
|
||||
var ptr = data->mem.Ptr;
|
||||
data->freeHeads[sizeIndex] = ((NextBlock*)(ptr + head))->next;
|
||||
*(Header*)(ptr + head - sizeof(Header)) = new Header { length = (uint)nElements | UsedBit | AllocatedBit };
|
||||
return head;
|
||||
}
|
||||
|
||||
int headerStart = data->mem.Length;
|
||||
int requiredSize = headerStart + sizeof(Header) + SizeIndexToElements(sizeIndex)*sizeof(T);
|
||||
if (Unity.Burst.CompilerServices.Hint.Unlikely(requiredSize > data->mem.Capacity)) {
|
||||
data->mem.SetCapacity(math.max(data->mem.Capacity*2, requiredSize));
|
||||
}
|
||||
|
||||
// Set the length field directly because we know we don't have to resize the list,
|
||||
// and we do not care about zeroing the memory.
|
||||
data->mem.m_length = requiredSize;
|
||||
*(Header*)(data->mem.Ptr + headerStart) = new Header { length = (uint)nElements | UsedBit | AllocatedBit };
|
||||
return headerStart + sizeof(Header);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Frees a single allocation</summary>
|
||||
public void Free (int allocatedIndex) {
|
||||
CheckDisposed();
|
||||
if (allocatedIndex == ZeroLengthArray) return;
|
||||
unsafe {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (allocatedIndex < sizeof(Header) || allocatedIndex >= data->mem.Length) throw new System.IndexOutOfRangeException();
|
||||
#endif
|
||||
var ptr = data->mem.Ptr;
|
||||
var header = (Header*)(ptr + allocatedIndex - sizeof(Header));
|
||||
var length = (int)(header->length & LengthMask);
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (length < 0 || length > SizeIndexToElements(MaxAllocationSizeIndex)) throw new System.Exception("Invalid index");
|
||||
if ((header->length & AllocatedBit) == 0) throw new System.Exception("Trying to free an already freed index");
|
||||
#endif
|
||||
|
||||
var sizeIndex = ElementsToSizeIndex(length);
|
||||
|
||||
*(NextBlock*)(ptr + allocatedIndex) = new NextBlock {
|
||||
next = data->freeHeads[sizeIndex]
|
||||
};
|
||||
data->freeHeads[sizeIndex] = allocatedIndex;
|
||||
// Mark as not allocated
|
||||
header->length &= ~(AllocatedBit | UsedBit);
|
||||
}
|
||||
}
|
||||
|
||||
public void CopyTo (SlabAllocator<T> other) {
|
||||
CheckDisposed();
|
||||
other.CheckDisposed();
|
||||
unsafe {
|
||||
other.data->mem.CopyFrom(data->mem);
|
||||
for (int i = 0; i < MaxAllocationSizeIndex + 1; i++) {
|
||||
other.data->freeHeads[i] = data->freeHeads[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
void CheckDisposed () {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
unsafe {
|
||||
if (data == null) throw new System.InvalidOperationException("SlabAllocator is already disposed or not initialized");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>Frees all unmanaged memory associated with this container</summary>
|
||||
public void Dispose () {
|
||||
unsafe {
|
||||
if (data == null) return;
|
||||
var allocator = data->mem.Allocator;
|
||||
data->mem.Dispose();
|
||||
AllocatorManager.Free(allocator, data);
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
|
||||
public List GetList (int allocatedIndex) {
|
||||
return new List(this, allocatedIndex);
|
||||
}
|
||||
|
||||
public ref struct List {
|
||||
public UnsafeSpan<T> span;
|
||||
SlabAllocator<T> allocator;
|
||||
// TODO: Can be derived from span
|
||||
public int allocationIndex;
|
||||
|
||||
public List(SlabAllocator<T> allocator, int allocationIndex) {
|
||||
this.span = allocator.GetSpan(allocationIndex);
|
||||
this.allocator = allocator;
|
||||
this.allocationIndex = allocationIndex;
|
||||
}
|
||||
|
||||
public void Add (T value) {
|
||||
allocator.Realloc(ref allocationIndex, span.Length + 1);
|
||||
span = allocator.GetSpan(allocationIndex);
|
||||
span[span.Length - 1] = value;
|
||||
}
|
||||
|
||||
public void RemoveAt (int index) {
|
||||
span.Slice(index + 1).CopyTo(span.Slice(index, span.Length - index - 1));
|
||||
allocator.Realloc(ref allocationIndex, span.Length - 1);
|
||||
span = allocator.GetSpan(allocationIndex);
|
||||
}
|
||||
|
||||
public void Clear () {
|
||||
allocator.Realloc(ref allocationIndex, 0);
|
||||
span = allocator.GetSpan(allocationIndex);
|
||||
}
|
||||
|
||||
public int Length => span.Length;
|
||||
|
||||
public ref T this[int index] {
|
||||
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||
get {
|
||||
return ref span[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SlabListExtensions {
|
||||
public static void Remove<T>(ref this SlabAllocator<T>.List list, T value) where T : unmanaged, System.IEquatable<T> {
|
||||
int idx = list.span.IndexOf(value);
|
||||
if (idx != -1) list.RemoveAt(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 020cb204e780440f8987b23cd21c02d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
441
Packages/com.arongranberg.astar/Core/Collections/Span.cs
Normal file
441
Packages/com.arongranberg.astar/Core/Collections/Span.cs
Normal file
@@ -0,0 +1,441 @@
|
||||
using Unity.Mathematics;
|
||||
using Unity.Burst;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Pathfinding.Collections {
|
||||
/// <summary>
|
||||
/// Replacement for System.Span which is compatible with earlier versions of C#.
|
||||
///
|
||||
/// Warning: These spans do not in any way guarantee that the memory they refer to is valid. It is up to the user to make sure
|
||||
/// the memory is not deallocated before usage. It should never be used to refer to managed heap memory without pinning it, since unpinned managed memory can be moved by some runtimes.
|
||||
///
|
||||
/// This has several benefits over e.g. UnsafeList:
|
||||
/// - It is faster to index into a span than into an UnsafeList, especially from C#. In fact, indexing into an UnsafeSpan is as fast as indexing into a native C# array.
|
||||
/// - As a comparison, indexing into a NativeArray can easily be 10x slower, and indexing into an UnsafeList is at least a few times slower.
|
||||
/// - You can create a UnsafeSpan from a C# array by pinning it.
|
||||
/// - It can be sliced efficiently.
|
||||
/// - It supports ref returns for the indexing operations.
|
||||
/// </summary>
|
||||
public readonly struct UnsafeSpan<T> where T : unmanaged {
|
||||
[NativeDisableUnsafePtrRestriction]
|
||||
internal readonly unsafe T* ptr;
|
||||
internal readonly uint length;
|
||||
|
||||
/// <summary>Number of elements in this span</summary>
|
||||
public int Length => (int)length;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe UnsafeSpan(void* ptr, int length) {
|
||||
if (length < 0) throw new System.ArgumentOutOfRangeException();
|
||||
if (length > 0 && ptr == null) throw new System.ArgumentNullException();
|
||||
this.ptr = (T*)ptr;
|
||||
this.length = (uint)length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new UnsafeSpan from a C# array.
|
||||
/// The array is pinned to ensure it does not move while the span is in use.
|
||||
///
|
||||
/// You must unpin the pinned memory using UnsafeUtility.ReleaseGCObject when you are done with the span.
|
||||
/// </summary>
|
||||
public unsafe UnsafeSpan(T[] data, out ulong gcHandle) {
|
||||
unsafe {
|
||||
this.ptr = (T*)UnsafeUtility.PinGCArrayAndGetDataAddress(data, out gcHandle);
|
||||
}
|
||||
this.length = (uint)data.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new UnsafeSpan from a 2D C# array.
|
||||
/// The array is pinned to ensure it does not move while the span is in use.
|
||||
///
|
||||
/// You must unpin the pinned memory using UnsafeUtility.ReleaseGCObject when you are done with the span.
|
||||
/// </summary>
|
||||
public unsafe UnsafeSpan(T[,] data, out ulong gcHandle) {
|
||||
unsafe {
|
||||
this.ptr = (T*)UnsafeUtility.PinGCArrayAndGetDataAddress(data, out gcHandle);
|
||||
}
|
||||
this.length = (uint)data.Length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a new UnsafeSpan with the specified length.
|
||||
/// The memory is not initialized.
|
||||
///
|
||||
/// You are responsible for freeing the memory using the same allocator when you are done with it.
|
||||
/// </summary>
|
||||
public UnsafeSpan(Allocator allocator, int length) {
|
||||
unsafe {
|
||||
if (length < 0) throw new System.ArgumentOutOfRangeException();
|
||||
if (length > 0) this.ptr = (T*)UnsafeUtility.MallocTracked(length * (long)UnsafeUtility.SizeOf<T>(), UnsafeUtility.AlignOf<T>(), allocator, 1);
|
||||
else this.ptr = null;
|
||||
this.length = (uint)length;
|
||||
}
|
||||
}
|
||||
|
||||
public ref T this[int index] {
|
||||
// With aggressive inlining the performance of indexing is essentially the same as indexing into a native C# array
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get {
|
||||
unsafe {
|
||||
if ((uint)index >= length) throw new System.IndexOutOfRangeException();
|
||||
Unity.Burst.CompilerServices.Hint.Assume(ptr != null);
|
||||
return ref *(ptr + index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ref T this[uint index] {
|
||||
// With aggressive inlining the performance of indexing is essentially the same as indexing into a native C# array
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
get {
|
||||
unsafe {
|
||||
if (index >= length) throw new System.IndexOutOfRangeException();
|
||||
Unity.Burst.CompilerServices.Hint.Assume(ptr != null);
|
||||
Unity.Burst.CompilerServices.Hint.Assume(ptr + index != null);
|
||||
return ref *(ptr + index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of this span, but with a different data-type.
|
||||
/// The new data-type must have the same size as the old one.
|
||||
///
|
||||
/// In burst, this should effectively be a no-op, except possibly a branch.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public UnsafeSpan<U> Reinterpret<U> () where U : unmanaged {
|
||||
unsafe {
|
||||
if (sizeof(T) != sizeof(U)) throw new System.InvalidOperationException("Cannot reinterpret span because the size of the types do not match");
|
||||
return new UnsafeSpan<U>(ptr, (int)length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of this span, but with a different data-type.
|
||||
/// The new data-type does not need to have the same size as the old one.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public UnsafeSpan<U> Reinterpret<U>(int expectedOriginalTypeSize) where U : unmanaged {
|
||||
unsafe {
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
if (sizeof(T) != expectedOriginalTypeSize) throw new System.InvalidOperationException("Cannot reinterpret span because sizeof(T) != expectedOriginalTypeSize");
|
||||
#endif
|
||||
return new UnsafeSpan<U>(ptr, (int)length * sizeof(T) / sizeof(U));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new span which is a slice of this span.
|
||||
/// The new span will start at the specified index and have the specified length.
|
||||
/// </summary>
|
||||
public UnsafeSpan<T> Slice (int start, int length) {
|
||||
if (start < 0 || length < 0 || start + length > this.length) throw new System.ArgumentOutOfRangeException();
|
||||
unsafe {
|
||||
return new UnsafeSpan<T>(ptr + start, length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new span which is a slice of this span.
|
||||
/// The new span will start at the specified index and continue to the end of this span.
|
||||
/// </summary>
|
||||
public UnsafeSpan<T> Slice (int start) {
|
||||
return Slice(start, (int)this.length - start);
|
||||
}
|
||||
|
||||
/// <summary>Copy the range [startIndex,startIndex+count) to [toIndex,toIndex+count)</summary>
|
||||
public void Move (int startIndex, int toIndex, int count) {
|
||||
unsafe {
|
||||
if (count < 0) throw new System.ArgumentOutOfRangeException();
|
||||
if (startIndex < 0 || startIndex + count > length) throw new System.ArgumentOutOfRangeException();
|
||||
if (toIndex < 0 || toIndex + count > length) throw new System.ArgumentOutOfRangeException();
|
||||
// If length is zero, the pointers may be null, which is technically undefined behavior (but in practice usually fine)
|
||||
if (count == 0) return;
|
||||
UnsafeUtility.MemMove(ptr + toIndex, ptr + startIndex, (long)sizeof(T) * (long)count);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the memory of this span to another span.
|
||||
/// The other span must be large enough to hold the contents of this span.
|
||||
///
|
||||
/// Note: Assumes the other span does not alias this one.
|
||||
/// </summary>
|
||||
public void CopyTo (UnsafeSpan<T> other) {
|
||||
if (other.length < length) throw new System.ArgumentException();
|
||||
unsafe {
|
||||
// If length is zero, the pointers may be null, which is technically undefined behavior (but in practice usually fine)
|
||||
if (length > 0) UnsafeUtility.MemCpy(other.ptr, ptr, (long)sizeof(T) * (long)length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Appends all elements in this span to the given list</summary>
|
||||
public void CopyTo (List<T> buffer) {
|
||||
if (buffer.Capacity < buffer.Count + Length) buffer.Capacity = buffer.Count + Length;
|
||||
for (int i = 0; i < Length; i++) buffer.Add(this[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new copy of the span allocated using the given allocator.
|
||||
///
|
||||
/// You are responsible for freeing this memory using the same allocator when you are done with it.
|
||||
/// </summary>
|
||||
public UnsafeSpan<T> Clone (Allocator allocator) {
|
||||
unsafe {
|
||||
var clone = new UnsafeSpan<T>(allocator, (int)length);
|
||||
CopyTo(clone);
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Converts the span to a managed array</summary>
|
||||
public T[] ToArray () {
|
||||
var arr = new T[length];
|
||||
if (length > 0) {
|
||||
unsafe {
|
||||
fixed (T* ptr = arr) {
|
||||
UnsafeUtility.MemCpy(ptr, this.ptr, (long)sizeof(T) * (long)length);
|
||||
}
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves this data to a new NativeArray.
|
||||
///
|
||||
/// This transfers ownership of the memory to the NativeArray, without any copying.
|
||||
/// The NativeArray must be disposed when you are done with it.
|
||||
///
|
||||
/// Warning: This span must have been allocated using the specified allocator.
|
||||
/// </summary>
|
||||
public unsafe NativeArray<T> MoveToNativeArray (Allocator allocator) {
|
||||
var arr = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<T>(ptr, Length, allocator);
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref arr, AtomicSafetyHandle.Create());
|
||||
#endif
|
||||
return arr;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees the underlaying memory.
|
||||
///
|
||||
/// Warning: The span must have been allocated using the specified allocator.
|
||||
///
|
||||
/// Warning: You must never use this span (or any other span referencing the same memory) again after calling this method.
|
||||
/// </summary>
|
||||
public unsafe void Free (Allocator allocator) {
|
||||
if (length > 0) UnsafeUtility.FreeTracked(ptr, allocator);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a new span with a different size, copies the current data over to it, and frees this span.
|
||||
///
|
||||
/// The new span may be larger or smaller than the current span. If it is larger, the new elements will be uninitialized.
|
||||
///
|
||||
/// Warning: The span must have been allocated using the specified allocator.
|
||||
///
|
||||
/// Warning: You must never use the old span (or any other span referencing the same memory) again after calling this method.
|
||||
///
|
||||
/// Returns: The new span.
|
||||
/// </summary>
|
||||
public unsafe UnsafeSpan<T> Reallocate (Allocator allocator, int newSize) {
|
||||
var newSpan = new UnsafeSpan<T>(allocator, newSize);
|
||||
Slice(0, System.Math.Min(newSize, Length)).CopyTo(newSpan);
|
||||
Free(allocator);
|
||||
return newSpan;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SpanExtensions {
|
||||
public static void FillZeros<T>(this UnsafeSpan<T> span) where T : unmanaged {
|
||||
unsafe {
|
||||
if (span.length > 0) UnsafeUtility.MemSet(span.ptr, 0, (long)sizeof(T) * (long)span.length);
|
||||
}
|
||||
}
|
||||
|
||||
public static void Fill<T>(this UnsafeSpan<T> span, T value) where T : unmanaged {
|
||||
unsafe {
|
||||
// This is wayy faster than a C# for loop (easily 10x faster).
|
||||
// It is also faster than a burst loop (at least as long as the span is reasonably large).
|
||||
// It also generates a lot less code than a burst for loop.
|
||||
if (span.length > 0) {
|
||||
// If this is too big, unity seems to overflow and crash internally
|
||||
if ((long)sizeof(T) * (long)span.length > (long)int.MaxValue) throw new System.ArgumentException("Span is too large to fill");
|
||||
UnsafeUtility.MemCpyReplicate(span.ptr, &value, sizeof(T), (int)span.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the contents of a NativeArray to this span.
|
||||
/// The span must be large enough to hold the contents of the array.
|
||||
/// </summary>
|
||||
public static void CopyFrom<T>(this UnsafeSpan<T> span, NativeArray<T> array) where T : unmanaged {
|
||||
array.AsUnsafeReadOnlySpan().CopyTo(span);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the contents of another span to this span.
|
||||
/// The span must be large enough to hold the contents of the array.
|
||||
/// </summary>
|
||||
public static void CopyFrom<T>(this UnsafeSpan<T> span, UnsafeSpan<T> other) where T : unmanaged {
|
||||
other.CopyTo(span);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies the contents of an array to this span.
|
||||
/// The span must be large enough to hold the contents of the array.
|
||||
/// </summary>
|
||||
public static void CopyFrom<T>(this UnsafeSpan<T> span, T[] array) where T : unmanaged {
|
||||
if (array.Length > span.Length) throw new System.InvalidOperationException();
|
||||
if (array.Length == 0) return;
|
||||
unsafe {
|
||||
var ptr = UnsafeUtility.PinGCArrayAndGetDataAddress(array, out var gcHandle);
|
||||
UnsafeUtility.MemCpy(span.ptr, ptr, (long)sizeof(T) * (long)array.Length);
|
||||
UnsafeUtility.ReleaseGCObject(gcHandle);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an UnsafeAppendBuffer to a span.
|
||||
/// The buffer must be a multiple of the element size.
|
||||
///
|
||||
/// The span is a view of the buffer memory, so do not dispose the buffer while the span is in use.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UnsafeSpan<T> AsUnsafeSpan<T>(this UnsafeAppendBuffer buffer) where T : unmanaged {
|
||||
unsafe {
|
||||
var items = buffer.Length / UnsafeUtility.SizeOf<T>();
|
||||
if (items * UnsafeUtility.SizeOf<T>() != buffer.Length) throw new System.ArgumentException("Buffer length is not a multiple of the element size");
|
||||
return new UnsafeSpan<T>(buffer.Ptr, items);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a NativeList to a span.
|
||||
///
|
||||
/// The span is a view of the list memory, so do not dispose the list while the span is in use.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UnsafeSpan<T> AsUnsafeSpan<T>(this NativeList<T> list) where T : unmanaged {
|
||||
unsafe {
|
||||
return new UnsafeSpan<T>(list.GetUnsafePtr(), list.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a NativeArray to a span.
|
||||
///
|
||||
/// The span is a view of the array memory, so do not dispose the array while the span is in use.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UnsafeSpan<T> AsUnsafeSpan<T>(this NativeArray<T> arr) where T : unmanaged {
|
||||
unsafe {
|
||||
return new UnsafeSpan<T>(arr.GetUnsafePtr(), arr.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a NativeArray to a span without performing any checks.
|
||||
///
|
||||
/// The span is a view of the array memory, so do not dispose the array while the span is in use.
|
||||
/// This method does not perform any checks to ensure that the array is safe to write to or read from.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UnsafeSpan<T> AsUnsafeSpanNoChecks<T>(this NativeArray<T> arr) where T : unmanaged {
|
||||
unsafe {
|
||||
return new UnsafeSpan<T>(NativeArrayUnsafeUtility.GetUnsafeBufferPointerWithoutChecks(arr), arr.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a NativeArray to a span, assuming it will only be read.
|
||||
///
|
||||
/// The span is a view of the array memory, so do not dispose the array while the span is in use.
|
||||
///
|
||||
/// Warning: No checks are done to ensure that you only read from the array. You are responsible for ensuring that you do not write to the span.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UnsafeSpan<T> AsUnsafeReadOnlySpan<T>(this NativeArray<T> arr) where T : unmanaged {
|
||||
unsafe {
|
||||
return new UnsafeSpan<T>(arr.GetUnsafeReadOnlyPtr(), arr.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts an UnsafeList to a span.
|
||||
///
|
||||
/// The span is a view of the list memory, so do not dispose the list while the span is in use.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UnsafeSpan<T> AsUnsafeSpan<T>(this UnsafeList<T> arr) where T : unmanaged {
|
||||
unsafe {
|
||||
return new UnsafeSpan<T>(arr.Ptr, arr.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a NativeSlice to a span.
|
||||
///
|
||||
/// The span is a view of the slice memory, so do not dispose the underlaying memory allocation while the span is in use.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static UnsafeSpan<T> AsUnsafeSpan<T>(this NativeSlice<T> slice) where T : unmanaged {
|
||||
unsafe {
|
||||
return new UnsafeSpan<T>(slice.GetUnsafePtr(), slice.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns true if the value exists in the span</summary>
|
||||
public static bool Contains<T>(this UnsafeSpan<T> span, T value) where T : unmanaged, System.IEquatable<T> {
|
||||
return IndexOf(span, value) != -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the first occurrence of a value in the span.
|
||||
/// If the value is not found, -1 is returned.
|
||||
/// </summary>
|
||||
public static int IndexOf<T>(this UnsafeSpan<T> span, T value) where T : unmanaged, System.IEquatable<T> {
|
||||
unsafe {
|
||||
return System.MemoryExtensions.IndexOf(new System.ReadOnlySpan<T>(span.ptr, (int)span.length), value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sorts the span in ascending order</summary>
|
||||
public static void Sort<T>(this UnsafeSpan<T> span) where T : unmanaged, System.IComparable<T> {
|
||||
unsafe {
|
||||
NativeSortExtension.Sort<T>(span.ptr, span.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sorts the span in ascending order</summary>
|
||||
public static void Sort<T, U>(this UnsafeSpan<T> span, U comp) where T : unmanaged where U : System.Collections.Generic.IComparer<T> {
|
||||
unsafe {
|
||||
NativeSortExtension.Sort<T, U>(span.ptr, span.Length, comp);
|
||||
}
|
||||
}
|
||||
|
||||
#if !MODULE_COLLECTIONS_2_4_0_OR_NEWER
|
||||
/// <summary>Shifts elements toward the end of this list, increasing its length</summary>
|
||||
public static void InsertRange<T>(this NativeList<T> list, int index, int count) where T : unmanaged {
|
||||
list.ResizeUninitialized(list.Length + count);
|
||||
list.AsUnsafeSpan().Move(index, index + count, list.Length - (index + count));
|
||||
}
|
||||
#endif
|
||||
|
||||
#if !MODULE_COLLECTIONS_2_1_0_OR_NEWER
|
||||
/// <summary>Appends value count times to the end of this list</summary>
|
||||
public static void AddReplicate<T>(this NativeList<T> list, T value, int count) where T : unmanaged {
|
||||
var origLength = list.Length;
|
||||
list.ResizeUninitialized(origLength + count);
|
||||
list.AsUnsafeSpan().Slice(origLength).Fill(value);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa5a1e493f0bf8946bc5fe477ef4710b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.arongranberg.astar/Core/Control.meta
Normal file
8
Packages/com.arongranberg.astar/Core/Control.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 756b69de94c66c508a7195b291178ac7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,208 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Pathfinding.Util {
|
||||
public static class MovementUtilities {
|
||||
public static float FilterRotationDirection (ref Vector2 state, ref Vector2 state2, Vector2 deltaPosition, float threshold, float deltaTime, bool avoidingOtherAgents) {
|
||||
const float Decay = 0.5f;
|
||||
|
||||
var lastState = state;
|
||||
|
||||
if (!avoidingOtherAgents) {
|
||||
// When not avoiding other agents, we can be a bit more aggressive with rotating towards the target.
|
||||
// This is because in that case, the velocity is much less noisy.
|
||||
state += deltaPosition * 10;
|
||||
} else {
|
||||
state += deltaPosition;
|
||||
}
|
||||
|
||||
// Decay the state slowly (has the most effect if the agent is standing still)
|
||||
state *= Mathf.Clamp01(1.0f - deltaTime*Decay);
|
||||
float stateLength = state.magnitude;
|
||||
|
||||
if (stateLength > threshold*2f) {
|
||||
state = state * (threshold*2f/stateLength);
|
||||
stateLength = threshold*2f;
|
||||
}
|
||||
|
||||
// TODO: Figure out what to do with
|
||||
state2 += (state - lastState) * Decay;
|
||||
state2 *= Mathf.Clamp01(1.0f - deltaTime*Decay);
|
||||
|
||||
// Prevent rotation if the agent doesn't move much
|
||||
float speed = stateLength > threshold ? 1.0f : 0.0f;
|
||||
return speed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the velocity to the max speed and optionally the forwards direction.
|
||||
///
|
||||
/// Note that all vectors are 2D vectors, not 3D vectors.
|
||||
///
|
||||
/// Returns: The clamped velocity in world units per second.
|
||||
/// </summary>
|
||||
/// <param name="velocity">Desired velocity of the character. In world units per second.</param>
|
||||
/// <param name="maxSpeed">Max speed of the character. In world units per second.</param>
|
||||
/// <param name="speedLimitFactor">Value between 0 and 1 which determines how much slower the character should move than normal.
|
||||
/// Normally 1 but should go to 0 when the character approaches the end of the path.</param>
|
||||
/// <param name="slowWhenNotFacingTarget">Slow the character down if the desired velocity is not in the same direction as the forward vector.</param>
|
||||
/// <param name="preventMovingBackwards">Prevent the velocity from being too far away from the forward direction of the character.</param>
|
||||
/// <param name="forward">Forward direction of the character. Used together with the slowWhenNotFacingTarget parameter.</param>
|
||||
public static Vector2 ClampVelocity (Vector2 velocity, float maxSpeed, float speedLimitFactor, bool slowWhenNotFacingTarget, bool preventMovingBackwards, Vector2 forward) {
|
||||
// Max speed to use for this frame
|
||||
var currentMaxSpeed = maxSpeed * speedLimitFactor;
|
||||
|
||||
// Check if the agent should slow down in case it is not facing the direction it wants to move in
|
||||
if (slowWhenNotFacingTarget && (forward.x != 0 || forward.y != 0)) {
|
||||
float currentSpeed;
|
||||
var normalizedVelocity = VectorMath.Normalize(velocity, out currentSpeed);
|
||||
float dot = Vector2.Dot(normalizedVelocity, forward);
|
||||
|
||||
// Lower the speed when the character's forward direction is not pointing towards the desired velocity
|
||||
// 1 when velocity is in the same direction as forward
|
||||
// 0.2 when they point in the opposite directions
|
||||
float directionSpeedFactor = Mathf.Clamp(dot+0.707f, 0.2f, 1.0f);
|
||||
currentMaxSpeed *= directionSpeedFactor;
|
||||
currentSpeed = Mathf.Min(currentSpeed, currentMaxSpeed);
|
||||
|
||||
if (preventMovingBackwards) {
|
||||
// Angle between the forwards direction of the character and our desired velocity
|
||||
float angle = Mathf.Acos(Mathf.Clamp(dot, -1, 1));
|
||||
|
||||
// Clamp the angle to 20 degrees
|
||||
// We cannot keep the velocity exactly in the forwards direction of the character
|
||||
// because we use the rotation to determine in which direction to rotate and if
|
||||
// the velocity would always be in the forwards direction of the character then
|
||||
// the character would never rotate.
|
||||
// Allow larger angles when near the end of the path to prevent oscillations.
|
||||
angle = Mathf.Min(angle, (20f + 180f*(1 - speedLimitFactor*speedLimitFactor))*Mathf.Deg2Rad);
|
||||
|
||||
float sin = Mathf.Sin(angle);
|
||||
float cos = Mathf.Cos(angle);
|
||||
|
||||
// Determine if we should rotate clockwise or counter-clockwise to move towards the current velocity
|
||||
sin *= Mathf.Sign(normalizedVelocity.x*forward.y - normalizedVelocity.y*forward.x);
|
||||
// Rotate the #forward vector by #angle radians
|
||||
// The rotation is done using an inlined rotation matrix.
|
||||
// See https://en.wikipedia.org/wiki/Rotation_matrix
|
||||
return new Vector2(forward.x*cos + forward.y*sin, forward.y*cos - forward.x*sin) * currentSpeed;
|
||||
} else {
|
||||
return normalizedVelocity * currentSpeed;
|
||||
}
|
||||
} else {
|
||||
return Vector2.ClampMagnitude(velocity, currentMaxSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Calculate an acceleration to move deltaPosition units and get there with approximately a velocity of targetVelocity</summary>
|
||||
public static Vector2 CalculateAccelerationToReachPoint (Vector2 deltaPosition, Vector2 targetVelocity, Vector2 currentVelocity, float forwardsAcceleration, float rotationSpeed, float maxSpeed, Vector2 forwardsVector) {
|
||||
// Guard against div by zero
|
||||
if (forwardsAcceleration <= 0) return Vector2.zero;
|
||||
|
||||
float currentSpeed = currentVelocity.magnitude;
|
||||
|
||||
// Convert rotation speed to an acceleration
|
||||
// See https://en.wikipedia.org/wiki/Centripetal_force
|
||||
var sidewaysAcceleration = currentSpeed * rotationSpeed * Mathf.Deg2Rad;
|
||||
|
||||
// To avoid weird behaviour when the rotation speed is very low we allow the agent to accelerate sideways without rotating much
|
||||
// if the rotation speed is very small. Also guards against division by zero.
|
||||
sidewaysAcceleration = Mathf.Max(sidewaysAcceleration, forwardsAcceleration);
|
||||
|
||||
// Transform coordinates to local space where +X is the forwards direction
|
||||
// This is essentially equivalent to Transform.InverseTransformDirection.
|
||||
deltaPosition = VectorMath.ComplexMultiplyConjugate(deltaPosition, forwardsVector);
|
||||
targetVelocity = VectorMath.ComplexMultiplyConjugate(targetVelocity, forwardsVector);
|
||||
currentVelocity = VectorMath.ComplexMultiplyConjugate(currentVelocity, forwardsVector);
|
||||
float ellipseSqrFactorX = 1 / (forwardsAcceleration*forwardsAcceleration);
|
||||
float ellipseSqrFactorY = 1 / (sidewaysAcceleration*sidewaysAcceleration);
|
||||
|
||||
// If the target velocity is zero we can use a more fancy approach
|
||||
// and calculate a nicer path.
|
||||
// In particular, this is the case at the end of the path.
|
||||
if (targetVelocity == Vector2.zero) {
|
||||
// Run a binary search over the time to get to the target point.
|
||||
float mn = 0.01f;
|
||||
float mx = 10;
|
||||
while (mx - mn > 0.01f) {
|
||||
var time = (mx + mn) * 0.5f;
|
||||
|
||||
// Given that we want to move deltaPosition units from out current position, that our current velocity is given
|
||||
// and that when we reach the target we want our velocity to be zero. Also assume that our acceleration will
|
||||
// vary linearly during the slowdown. Then we can calculate what our acceleration should be during this frame.
|
||||
|
||||
//{ t = time
|
||||
//{ deltaPosition = vt + at^2/2 + qt^3/6
|
||||
//{ 0 = v + at + qt^2/2
|
||||
//{ solve for a
|
||||
// a = acceleration vector
|
||||
// q = derivative of the acceleration vector
|
||||
var a = (6*deltaPosition - 4*time*currentVelocity)/(time*time);
|
||||
var q = 6*(time*currentVelocity - 2*deltaPosition)/(time*time*time);
|
||||
|
||||
// Make sure the acceleration is not greater than our maximum allowed acceleration.
|
||||
// If it is we increase the time we want to use to get to the target
|
||||
// and if it is not, we decrease the time to get there faster.
|
||||
// Since the acceleration is described by acceleration = a + q*t
|
||||
// we only need to check at t=0 and t=time.
|
||||
// Note that the acceleration limit is described by an ellipse, not a circle.
|
||||
var nextA = a + q*time;
|
||||
if (a.x*a.x*ellipseSqrFactorX + a.y*a.y*ellipseSqrFactorY > 1.0f || nextA.x*nextA.x*ellipseSqrFactorX + nextA.y*nextA.y*ellipseSqrFactorY > 1.0f) {
|
||||
mn = time;
|
||||
} else {
|
||||
mx = time;
|
||||
}
|
||||
}
|
||||
|
||||
var finalAcceleration = (6*deltaPosition - 4*mx*currentVelocity)/(mx*mx);
|
||||
|
||||
// Boosting
|
||||
{
|
||||
// The trajectory calculated above has a tendency to use very wide arcs
|
||||
// and that does unfortunately not look particularly good in some cases.
|
||||
// Here we amplify the component of the acceleration that is perpendicular
|
||||
// to our current velocity. This will make the agent turn towards the
|
||||
// target quicker.
|
||||
// How much amplification to use. Value is unitless.
|
||||
const float Boost = 1;
|
||||
finalAcceleration.y *= 1 + Boost;
|
||||
|
||||
// Clamp the velocity to the maximum acceleration.
|
||||
// Note that the maximum acceleration constraint is shaped like an ellipse, not like a circle.
|
||||
float ellipseMagnitude = finalAcceleration.x*finalAcceleration.x*ellipseSqrFactorX + finalAcceleration.y*finalAcceleration.y*ellipseSqrFactorY;
|
||||
if (ellipseMagnitude > 1.0f) finalAcceleration /= Mathf.Sqrt(ellipseMagnitude);
|
||||
}
|
||||
|
||||
return VectorMath.ComplexMultiply(finalAcceleration, forwardsVector);
|
||||
} else {
|
||||
// Here we try to move towards the next waypoint which has been modified slightly using our
|
||||
// desired velocity at that point so that the agent will more smoothly round the corner.
|
||||
|
||||
// How much to strive for making sure we reach the target point with the target velocity. Unitless.
|
||||
const float TargetVelocityWeight = 0.5f;
|
||||
|
||||
// Limit to how much to care about the target velocity. Value is in seconds.
|
||||
// This prevents the character from moving away from the path too much when the target point is far away
|
||||
const float TargetVelocityWeightLimit = 1.5f;
|
||||
float targetSpeed;
|
||||
var normalizedTargetVelocity = VectorMath.Normalize(targetVelocity, out targetSpeed);
|
||||
|
||||
var distance = deltaPosition.magnitude;
|
||||
var targetPoint = deltaPosition - normalizedTargetVelocity * System.Math.Min(TargetVelocityWeight * distance * targetSpeed / (currentSpeed + targetSpeed), maxSpeed*TargetVelocityWeightLimit);
|
||||
|
||||
// How quickly the agent will try to reach the velocity that we want it to have.
|
||||
// We need this to prevent oscillations and jitter which is what happens if
|
||||
// we let the constant go towards zero. Value is in seconds.
|
||||
const float TimeToReachDesiredVelocity = 0.1f;
|
||||
// TODO: Clamp to ellipse using more accurate acceleration (use rotation speed as well)
|
||||
var finalAcceleration = (targetPoint.normalized*maxSpeed - currentVelocity) * (1f/TimeToReachDesiredVelocity);
|
||||
|
||||
// Clamp the velocity to the maximum acceleration.
|
||||
// Note that the maximum acceleration constraint is shaped like an ellipse, not like a circle.
|
||||
float ellipseMagnitude = finalAcceleration.x*finalAcceleration.x*ellipseSqrFactorX + finalAcceleration.y*finalAcceleration.y*ellipseSqrFactorY;
|
||||
if (ellipseMagnitude > 1.0f) finalAcceleration /= Mathf.Sqrt(ellipseMagnitude);
|
||||
|
||||
return VectorMath.ComplexMultiply(finalAcceleration, forwardsVector);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a6cffd9895f94907aa43f18b0904587
|
||||
timeCreated: 1490097740
|
||||
licenseType: Store
|
||||
MonoImporter:
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1115
Packages/com.arongranberg.astar/Core/Control/PIDMovement.cs
Normal file
1115
Packages/com.arongranberg.astar/Core/Control/PIDMovement.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2d19693db79b5eedab18d1362b58bca1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
29
Packages/com.arongranberg.astar/Core/Control/PIDUtilities.cs
Normal file
29
Packages/com.arongranberg.astar/Core/Control/PIDUtilities.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.PID {
|
||||
public struct AnglePIDControlOutput2D {
|
||||
/// <summary>How much to rotate in a single time-step. In radians.</summary>
|
||||
public float rotationDelta;
|
||||
public float targetRotation;
|
||||
/// <summary>How much to move in a single time-step. In world units.</summary>
|
||||
public float2 positionDelta;
|
||||
|
||||
public AnglePIDControlOutput2D(float currentRotation, float targetRotation, float rotationDelta, float moveDistance) {
|
||||
var midpointRotation = currentRotation + rotationDelta * 0.5f;
|
||||
math.sincos(midpointRotation, out float s, out float c);
|
||||
this.rotationDelta = rotationDelta;
|
||||
this.positionDelta = new float2(c, s) * moveDistance;
|
||||
this.targetRotation = targetRotation;
|
||||
}
|
||||
|
||||
public static AnglePIDControlOutput2D WithMovementAtEnd (float currentRotation, float targetRotation, float rotationDelta, float moveDistance) {
|
||||
var finalRotation = currentRotation + rotationDelta;
|
||||
math.sincos(finalRotation, out float s, out float c);
|
||||
return new AnglePIDControlOutput2D {
|
||||
rotationDelta = rotationDelta,
|
||||
targetRotation = targetRotation,
|
||||
positionDelta = new float2(c, s) * moveDistance,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3e01131736f2ac179398e1f408f94eb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.arongranberg.astar/Core/ECS.meta
Normal file
8
Packages/com.arongranberg.astar/Core/ECS.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4551b5f18d9b68d428cf336b3ab3c38b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Packages/com.arongranberg.astar/Core/ECS/Components.meta
Normal file
8
Packages/com.arongranberg.astar/Core/ECS/Components.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c496a3fafab9fab4a8c139fc7295f219
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding;
|
||||
using Pathfinding.ECS.RVO;
|
||||
|
||||
/// <summary>An agent's shape represented as a cylinder</summary>
|
||||
[System.Serializable]
|
||||
public struct AgentCylinderShape : IComponentData {
|
||||
/// <summary>Radius of the agent in world units</summary>
|
||||
public float radius;
|
||||
|
||||
/// <summary>Height of the agent in world units</summary>
|
||||
public float height;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9dd6a4018eb50a48b69d83cb69a09b9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,34 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding;
|
||||
using Pathfinding.Util;
|
||||
using Unity.Mathematics;
|
||||
|
||||
/// <summary>Holds an agent's movement plane</summary>
|
||||
[System.Serializable]
|
||||
public struct AgentMovementPlane : IComponentData {
|
||||
/// <summary>
|
||||
/// The movement plane for the agent.
|
||||
///
|
||||
/// The movement plane determines what the "up" direction of the agent is.
|
||||
/// For most typical 3D games, this will be aligned with the Y axis, but there are
|
||||
/// games in which the agent needs to navigate on walls, or on spherical worlds.
|
||||
/// For those games this movement plane will track the plane in which the agent is currently moving.
|
||||
///
|
||||
/// See: spherical (view in online documentation for working links)
|
||||
/// </summary>
|
||||
public NativeMovementPlane value;
|
||||
|
||||
/// <summary>Create a movement plane aligned with the XZ plane of the specified rotation</summary>
|
||||
public AgentMovementPlane (quaternion rotation) {
|
||||
value = new NativeMovementPlane(rotation);
|
||||
}
|
||||
|
||||
public AgentMovementPlane (NativeMovementPlane plane) {
|
||||
value = plane;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b7137ab6e696b37428b1bec8e09e78ad
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,15 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
/// <summary>
|
||||
/// The movement plane source for an agent.
|
||||
///
|
||||
/// See: <see cref="MovementPlaneSource"/>
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct AgentMovementPlaneSource : ISharedComponentData {
|
||||
public MovementPlaneSource value;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a2198aa10fd82b94db223e3dbec9352b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,388 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.Transforms;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding;
|
||||
using Pathfinding.Util;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Holds unmanaged information about an off-mesh link that the agent is currently traversing.
|
||||
/// This component is added to the agent when it starts traversing an off-mesh link.
|
||||
/// It is removed when the agent has finished traversing the link.
|
||||
///
|
||||
/// See: <see cref="ManagedAgentOffMeshLinkTraversal"/>
|
||||
/// </summary>
|
||||
public struct AgentOffMeshLinkTraversal : IComponentData {
|
||||
/// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.relativeStart}</summary>
|
||||
public float3 relativeStart;
|
||||
|
||||
/// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.relativeEnd}</summary>
|
||||
public float3 relativeEnd;
|
||||
|
||||
/// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.relativeStart}. Deprecated: Use relativeStart instead</summary>
|
||||
[System.Obsolete("Use relativeStart instead")]
|
||||
public float3 firstPosition => relativeStart;
|
||||
|
||||
/// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.relativeEnd}. Deprecated: Use relativeEnd instead</summary>
|
||||
[System.Obsolete("Use relativeEnd instead")]
|
||||
public float3 secondPosition => relativeEnd;
|
||||
|
||||
/// <summary>\copydocref{OffMeshLinks.OffMeshLinkTracer.isReverse}</summary>
|
||||
public bool isReverse;
|
||||
|
||||
public AgentOffMeshLinkTraversal (OffMeshLinks.OffMeshLinkTracer linkInfo) {
|
||||
relativeStart = linkInfo.relativeStart;
|
||||
relativeEnd = linkInfo.relativeEnd;
|
||||
isReverse = linkInfo.isReverse;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holds managed information about an off-mesh link that the agent is currently traversing.
|
||||
/// This component is added to the agent when it starts traversing an off-mesh link.
|
||||
/// It is removed when the agent has finished traversing the link.
|
||||
///
|
||||
/// See: <see cref="AgentOffMeshLinkTraversal"/>
|
||||
/// </summary>
|
||||
public class ManagedAgentOffMeshLinkTraversal : IComponentData, System.ICloneable, ICleanupComponentData {
|
||||
/// <summary>Internal context used to pass component data to the coroutine</summary>
|
||||
public AgentOffMeshLinkTraversalContext context;
|
||||
|
||||
/// <summary>Coroutine which is used to traverse the link</summary>
|
||||
public System.Collections.IEnumerator coroutine;
|
||||
public IOffMeshLinkHandler handler;
|
||||
public IOffMeshLinkStateMachine stateMachine;
|
||||
|
||||
public ManagedAgentOffMeshLinkTraversal() {}
|
||||
|
||||
public ManagedAgentOffMeshLinkTraversal (AgentOffMeshLinkTraversalContext context, IOffMeshLinkHandler handler) {
|
||||
this.context = context;
|
||||
this.handler = handler;
|
||||
this.coroutine = null;
|
||||
this.stateMachine = null;
|
||||
}
|
||||
|
||||
public object Clone () {
|
||||
// This will set coroutine and stateMachine to null.
|
||||
// This is correct, as the coroutine cannot be cloned, and the state machine may be unique for a specific agent
|
||||
return new ManagedAgentOffMeshLinkTraversal((AgentOffMeshLinkTraversalContext)context.Clone(), handler);
|
||||
}
|
||||
}
|
||||
|
||||
public struct MovementTarget {
|
||||
internal bool isReached;
|
||||
public bool reached => isReached;
|
||||
|
||||
public MovementTarget (bool isReached) {
|
||||
this.isReached = isReached;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context with helpers for traversing an off-mesh link.
|
||||
///
|
||||
/// This will be passed to the code that is responsible for traversing the off-mesh link.
|
||||
///
|
||||
/// Warning: This context should never be accessed outside of an implementation of the <see cref="IOffMeshLinkStateMachine"/> interface.
|
||||
/// </summary>
|
||||
public class AgentOffMeshLinkTraversalContext : System.ICloneable {
|
||||
internal unsafe AgentOffMeshLinkTraversal* linkInfoPtr;
|
||||
internal unsafe MovementControl* movementControlPtr;
|
||||
internal unsafe MovementSettings* movementSettingsPtr;
|
||||
internal unsafe LocalTransform* transformPtr;
|
||||
internal unsafe AgentMovementPlane* movementPlanePtr;
|
||||
|
||||
/// <summary>The entity that is traversing the off-mesh link</summary>
|
||||
public Entity entity;
|
||||
|
||||
/// <summary>Some internal state of the agent</summary>
|
||||
[Unity.Properties.DontCreateProperty]
|
||||
public ManagedState managedState;
|
||||
|
||||
/// <summary>
|
||||
/// The off-mesh link that is being traversed.
|
||||
///
|
||||
/// See: <see cref="link"/>
|
||||
/// </summary>
|
||||
[Unity.Properties.DontCreateProperty]
|
||||
internal OffMeshLinks.OffMeshLinkConcrete concreteLink;
|
||||
|
||||
protected bool disabledRVO;
|
||||
protected float backupRotationSmoothing = float.NaN;
|
||||
|
||||
/// <summary>
|
||||
/// Delta time since the last link simulation.
|
||||
///
|
||||
/// During high time scales, the simulation may run multiple substeps per frame.
|
||||
///
|
||||
/// This is not the same as Time.deltaTime. Inside the link coroutine, you should always use this field instead of Time.deltaTime.
|
||||
/// </summary>
|
||||
public float deltaTime;
|
||||
|
||||
protected GameObject gameObjectCache;
|
||||
|
||||
/// <summary>
|
||||
/// GameObject associated with the agent.
|
||||
///
|
||||
/// In most cases, an agent is associated with an agent, but this is not always the case.
|
||||
/// For example, if you have created an entity without using the <see cref="FollowerEntity"/> component, this property may return null.
|
||||
///
|
||||
/// Note: When directly modifying the agent's transform during a link traversal, you should use the <see cref="transform"/> property instead of modifying the GameObject's transform.
|
||||
/// </summary>
|
||||
public virtual GameObject gameObject {
|
||||
get {
|
||||
if (gameObjectCache == null) {
|
||||
var follower = BatchedEvents.Find<FollowerEntity, Entity>(entity, (follower, entity) => follower.entity == entity);
|
||||
if (follower != null) gameObjectCache = follower.gameObject;
|
||||
}
|
||||
return gameObjectCache;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>ECS LocalTransform component attached to the agent</summary>
|
||||
public ref LocalTransform transform {
|
||||
get {
|
||||
unsafe {
|
||||
return ref *transformPtr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The movement settings for the agent</summary>
|
||||
public ref MovementSettings movementSettings {
|
||||
get {
|
||||
unsafe {
|
||||
return ref *movementSettingsPtr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// How the agent should move.
|
||||
///
|
||||
/// The agent will move according to this data, every frame.
|
||||
/// </summary>
|
||||
public ref MovementControl movementControl {
|
||||
get {
|
||||
unsafe {
|
||||
return ref *movementControlPtr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Information about the off-mesh link that the agent is traversing</summary>
|
||||
public OffMeshLinks.OffMeshLinkTracer link {
|
||||
get {
|
||||
unsafe {
|
||||
return new OffMeshLinks.OffMeshLinkTracer(concreteLink, linkInfoPtr->relativeStart, linkInfoPtr->relativeEnd, linkInfoPtr->isReverse);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Information about the off-mesh link that the agent is traversing.
|
||||
///
|
||||
/// Deprecated: Use the <see cref="link"/> property instead
|
||||
/// </summary>
|
||||
[System.Obsolete("Use the link property instead")]
|
||||
public AgentOffMeshLinkTraversal linkInfo {
|
||||
get {
|
||||
unsafe {
|
||||
return *linkInfoPtr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The plane in which the agent is moving.
|
||||
///
|
||||
/// In a 3D game, this will typically be the XZ plane, but in a 2D game
|
||||
/// it will typically be the XY plane. Games on spherical planets could have planes that are aligned with the surface of the planet.
|
||||
/// </summary>
|
||||
public ref NativeMovementPlane movementPlane {
|
||||
get {
|
||||
unsafe {
|
||||
return ref movementPlanePtr->value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AgentOffMeshLinkTraversalContext (OffMeshLinks.OffMeshLinkConcrete link) {
|
||||
this.concreteLink = link;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method to set the data of the context.
|
||||
///
|
||||
/// This is used by the job system to set the data of the context.
|
||||
/// You should almost never need to use this.
|
||||
/// </summary>
|
||||
public virtual unsafe void SetInternalData (Entity entity, ref LocalTransform transform, ref AgentMovementPlane movementPlane, ref MovementControl movementControl, ref MovementSettings movementSettings, ref AgentOffMeshLinkTraversal linkInfo, ManagedState state, float deltaTime) {
|
||||
this.linkInfoPtr = (AgentOffMeshLinkTraversal*)UnsafeUtility.AddressOf(ref linkInfo);
|
||||
this.movementControlPtr = (MovementControl*)UnsafeUtility.AddressOf(ref movementControl);
|
||||
this.movementSettingsPtr = (MovementSettings*)UnsafeUtility.AddressOf(ref movementSettings);
|
||||
this.transformPtr = (LocalTransform*)UnsafeUtility.AddressOf(ref transform);
|
||||
this.movementPlanePtr = (AgentMovementPlane*)UnsafeUtility.AddressOf(ref movementPlane);
|
||||
this.managedState = state;
|
||||
this.deltaTime = deltaTime;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables local avoidance for the agent.
|
||||
///
|
||||
/// Agents that traverse links are already marked as 'unstoppable' by the local avoidance system,
|
||||
/// but calling this method will make other agents ignore them completely while traversing the link.
|
||||
/// </summary>
|
||||
public void DisableLocalAvoidance () {
|
||||
if (managedState.enableLocalAvoidance) {
|
||||
disabledRVO = true;
|
||||
managedState.enableLocalAvoidance = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables rotation smoothing for the agent.
|
||||
///
|
||||
/// This disables the effect of <see cref="MovementSettings.rotationSmoothing"/> while the agent is traversing the link.
|
||||
/// Having rotation smoothing enabled can make the agent rotate towards its target rotation more slowly,
|
||||
/// which is sometimes not desirable.
|
||||
///
|
||||
/// Rotation smoothing will automatically be restored when the agent finishes traversing the link (if it was enabled before).
|
||||
///
|
||||
/// The <see cref="MoveTowards"/> method automatically disables rotation smoothing when called.
|
||||
/// </summary>
|
||||
public void DisableRotationSmoothing () {
|
||||
if (float.IsNaN(backupRotationSmoothing) && movementSettings.rotationSmoothing > 0) {
|
||||
backupRotationSmoothing = movementSettings.rotationSmoothing;
|
||||
movementSettings.rotationSmoothing = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores the agent's settings to what it was before the link traversal started.
|
||||
///
|
||||
/// This undos the changes made by <see cref="DisableLocalAvoidance"/> and <see cref="DisableRotationSmoothing"/>.
|
||||
///
|
||||
/// This method is automatically called when the agent finishes traversing the link.
|
||||
/// </summary>
|
||||
public virtual void Restore () {
|
||||
if (disabledRVO) {
|
||||
managedState.enableLocalAvoidance = true;
|
||||
disabledRVO = false;
|
||||
}
|
||||
if (!float.IsNaN(backupRotationSmoothing)) {
|
||||
movementSettings.rotationSmoothing = backupRotationSmoothing;
|
||||
backupRotationSmoothing = float.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Teleports the agent to the given position</summary>
|
||||
public virtual void Teleport (float3 position) {
|
||||
transform.Position = position;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when the off-mesh link traversal should be aborted.
|
||||
///
|
||||
/// See: <see cref="AgentOffMeshLinkTraversalContext.Abort"/>
|
||||
/// </summary>
|
||||
public class AbortOffMeshLinkTraversal : System.Exception {}
|
||||
|
||||
/// <summary>
|
||||
/// Aborts traversing the off-mesh link.
|
||||
///
|
||||
/// This will immediately stop your off-mesh link traversal coroutine.
|
||||
///
|
||||
/// This is useful if your agent was traversing an off-mesh link, but you have detected that it cannot continue.
|
||||
/// Maybe the ladder it was climbing was destroyed, or the bridge it was walking on collapsed.
|
||||
///
|
||||
/// Note: If you instead want to immediately make the agent move to the end of the link, you can call <see cref="Teleport"/>, and then use 'yield break;' from your coroutine.
|
||||
/// </summary>
|
||||
/// <param name="teleportToStart">If true, the agent will be teleported back to the start of the link (from the perspective of the agent). Its rotation will remain unchanged.</param>
|
||||
public virtual void Abort (bool teleportToStart = true) {
|
||||
if (teleportToStart) Teleport(link.relativeStart);
|
||||
// Cancel the current path, as otherwise the agent will instantly try to traverse the off-mesh link again.
|
||||
managedState.pathTracer.RemoveAllButFirstNode(movementPlane, managedState.pathfindingSettings.traversalProvider);
|
||||
throw new AbortOffMeshLinkTraversal();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Move towards a point while ignoring the navmesh.
|
||||
/// This method should be called repeatedly until the returned <see cref="MovementTarget.reached"/> property is true.
|
||||
///
|
||||
/// Returns: A <see cref="MovementTarget"/> struct which can be used to check if the target has been reached.
|
||||
///
|
||||
/// Note: This method completely ignores the navmesh. It also overrides local avoidance, if enabled (other agents will still avoid it, but this agent will not avoid other agents).
|
||||
///
|
||||
/// TODO: The gravity property is not yet implemented. Gravity is always applied.
|
||||
/// </summary>
|
||||
/// <param name="position">The position to move towards.</param>
|
||||
/// <param name="rotation">The rotation to rotate towards.</param>
|
||||
/// <param name="gravity">If true, gravity will be applied to the agent.</param>
|
||||
/// <param name="slowdown">If true, the agent will slow down as it approaches the target.</param>
|
||||
public virtual MovementTarget MoveTowards (float3 position, quaternion rotation, bool gravity, bool slowdown) {
|
||||
// If rotation smoothing was enabled, it could cause a very slow convergence to the target rotation.
|
||||
// Therefore, we disable it here.
|
||||
// The agent will try to remove its remaining rotation smoothing offset as quickly as possible.
|
||||
// After the off-mesh link is traversed, the rotation smoothing will be automatically restored.
|
||||
DisableRotationSmoothing();
|
||||
|
||||
var dirInPlane = movementPlane.ToPlane(position - transform.Position);
|
||||
var remainingDistance = math.length(dirInPlane);
|
||||
var maxSpeed = movementSettings.follower.Speed(slowdown ? remainingDistance : float.PositiveInfinity);
|
||||
var speed = movementSettings.follower.Accelerate(movementControl.speed, movementSettings.follower.slowdownTime, deltaTime);
|
||||
speed = math.min(speed, maxSpeed);
|
||||
|
||||
var targetRot = movementPlane.ToPlane(rotation);
|
||||
var currentRot = movementPlane.ToPlane(transform.Rotation);
|
||||
var remainingRot = Mathf.Abs(AstarMath.DeltaAngle(currentRot, targetRot));
|
||||
movementControl = new MovementControl {
|
||||
targetPoint = position,
|
||||
endOfPath = position,
|
||||
speed = speed,
|
||||
maxSpeed = speed * 1.1f,
|
||||
hierarchicalNodeIndex = -1,
|
||||
overrideLocalAvoidance = true,
|
||||
targetRotation = targetRot,
|
||||
targetRotationHint = targetRot,
|
||||
targetRotationOffset = 0,
|
||||
rotationSpeed = math.radians(movementSettings.follower.rotationSpeed),
|
||||
};
|
||||
|
||||
return new MovementTarget {
|
||||
isReached = remainingDistance <= (slowdown ? 0.01f : speed * (1/30f)) && remainingRot < math.radians(1),
|
||||
};
|
||||
}
|
||||
|
||||
public virtual object Clone () {
|
||||
var clone = (AgentOffMeshLinkTraversalContext)MemberwiseClone();
|
||||
clone.entity = Entity.Null;
|
||||
clone.gameObjectCache = null;
|
||||
clone.managedState = null;
|
||||
unsafe {
|
||||
linkInfoPtr = null;
|
||||
movementControlPtr = null;
|
||||
movementSettingsPtr = null;
|
||||
transformPtr = null;
|
||||
movementPlanePtr = null;
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ctx.MoveTowards (position, rotation, rvo = Auto | Disabled | AutoUnstoppable, gravity = auto|disabled) -> { reached() }
|
||||
|
||||
// MovementTarget { ... }
|
||||
// while (!movementTarget.reached) {
|
||||
// ctx.SetMovementTarget(movementTarget);
|
||||
// yield return null;
|
||||
// }
|
||||
// yield return ctx.MoveTo(position, rotation)
|
||||
// ctx.TeleportTo(position, rotation)
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e7b7b15e5b39fc142a7dd409c4c3a18d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,97 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
/// <summary>
|
||||
/// Policy for how often to recalculate an agent's path.
|
||||
///
|
||||
/// See: <see cref="FollowerEntity.autoRepath"/>
|
||||
///
|
||||
/// This is the unmanaged equivalent of <see cref="Pathfinding.AutoRepathPolicy"/>.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct AutoRepathPolicy : IComponentData {
|
||||
/// <summary>
|
||||
/// How sensitive the agent should be to changes in its destination for Mode.Dynamic.
|
||||
/// A higher value means the destination has to move less for the path to be recalculated.
|
||||
///
|
||||
/// See: <see cref="AutoRepathPolicy.Mode"/>
|
||||
/// </summary>
|
||||
public const float Sensitivity = 10.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Policy to use when recalculating paths.
|
||||
///
|
||||
/// See: <see cref="Pathfinding.AutoRepathPolicy.Mode"/> for more details.
|
||||
/// </summary>
|
||||
public Pathfinding.AutoRepathPolicy.Mode mode;
|
||||
|
||||
/// <summary>Number of seconds between each automatic path recalculation for Mode.EveryNSeconds, and the maximum interval for Mode.Dynamic</summary>
|
||||
public float period;
|
||||
|
||||
float3 lastDestination;
|
||||
float lastRepathTime;
|
||||
|
||||
public static AutoRepathPolicy Default => new AutoRepathPolicy {
|
||||
mode = Pathfinding.AutoRepathPolicy.Mode.Dynamic,
|
||||
period = 2,
|
||||
lastDestination = float.PositiveInfinity,
|
||||
lastRepathTime = float.NegativeInfinity
|
||||
};
|
||||
|
||||
public AutoRepathPolicy (Pathfinding.AutoRepathPolicy policy) {
|
||||
mode = policy.mode;
|
||||
period = policy.mode == Pathfinding.AutoRepathPolicy.Mode.Dynamic ? policy.maximumPeriod : policy.period;
|
||||
lastDestination = float.PositiveInfinity;
|
||||
lastRepathTime = float.NegativeInfinity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the path should be recalculated according to the policy
|
||||
///
|
||||
/// The above parameters are relevant only if <see cref="mode"/> is <see cref="Mode.Dynamic"/>.
|
||||
/// </summary>
|
||||
/// <param name="position">The current position of the agent.</param>
|
||||
/// <param name="radius">The radius of the agent. You may pass 0.0 if the agent doesn't have a radius.</param>
|
||||
/// <param name="destination">The goal of the agent right now</param>
|
||||
/// <param name="time">The current time in seconds</param>
|
||||
public bool ShouldRecalculatePath (float3 position, float radius, float3 destination, float time) {
|
||||
if (mode == Pathfinding.AutoRepathPolicy.Mode.Never || float.IsPositiveInfinity(destination.x)) return false;
|
||||
|
||||
float timeSinceLast = time - lastRepathTime;
|
||||
if (mode == Pathfinding.AutoRepathPolicy.Mode.EveryNSeconds) {
|
||||
return timeSinceLast >= period;
|
||||
} else {
|
||||
// cost = change in destination / max(distance to destination, radius)
|
||||
float squaredCost = math.lengthsq(destination - lastDestination) / math.max(math.lengthsq(position - lastDestination), radius*radius);
|
||||
float fraction = squaredCost * (Sensitivity*Sensitivity);
|
||||
if (float.IsNaN(fraction)) {
|
||||
// The agent's radius is zero, and the destination is precisely at the agent's position, which is also the destination of the last calculated path
|
||||
// This is a special case. It happens sometimes for the AILerp component when it reaches its
|
||||
// destination, as the AILerp component has no radius.
|
||||
// In this case we just use the maximum period.
|
||||
fraction = 0;
|
||||
}
|
||||
|
||||
return timeSinceLast >= period*(1 - math.sqrt(fraction));
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset () {
|
||||
lastDestination = float.PositiveInfinity;
|
||||
lastRepathTime = float.NegativeInfinity;
|
||||
}
|
||||
|
||||
/// <summary>Must be called when a path request has been scheduled</summary>
|
||||
public void DidRecalculatePath (float3 destination, float time) {
|
||||
lastRepathTime = time;
|
||||
lastDestination = destination;
|
||||
// Randomize the repath time slightly so that all agents don't request a path at the same time
|
||||
// in the future. This is useful when there are a lot of agents instantiated at exactly the same time.
|
||||
const float JITTER_AMOUNT = 0.3f;
|
||||
lastRepathTime -= (UnityEngine.Random.value - 0.5f) * JITTER_AMOUNT * period;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 49e745654af51f043a68a105c85e2bae
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,26 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
/// <summary>Holds an agent's destination point</summary>
|
||||
public struct DestinationPoint : IComponentData {
|
||||
/// <summary>
|
||||
/// The destination point that the agent is moving towards.
|
||||
///
|
||||
/// This is the point that the agent is trying to reach, but it may not always be possible to reach it.
|
||||
///
|
||||
/// See: <see cref="AIDestinationSetter"/>
|
||||
/// See: <see cref="IAstarAI.destination"/>
|
||||
/// </summary>
|
||||
public float3 destination;
|
||||
|
||||
/// <summary>
|
||||
/// The direction the agent should face when it reaches the destination.
|
||||
///
|
||||
/// If zero, the agent will not try to face any particular direction when reaching the destination.
|
||||
/// </summary>
|
||||
public float3 facingDirection;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87fc1fca9dfafa64b98ec33b24a358fa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,16 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
/// <summary>Agent state related to gravity</summary>
|
||||
public struct GravityState : IComponentData, IEnableableComponent {
|
||||
/// <summary>
|
||||
/// Current vertical velocity of the agent.
|
||||
/// This is the velocity that the agent is moving with due to gravity.
|
||||
/// It is not necessarily the same as the Y component of the estimated velocity.
|
||||
/// </summary>
|
||||
public float verticalVelocity;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ed943911778141b4988cbdcd7f5b3a07
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,91 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Unity.Transforms;
|
||||
|
||||
public delegate void BeforeControlDelegate(Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings);
|
||||
public delegate void AfterControlDelegate(Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl);
|
||||
public delegate void BeforeMovementDelegate(Entity entity, float dt, ref LocalTransform localTransform, ref AgentCylinderShape shape, ref AgentMovementPlane movementPlane, ref DestinationPoint destination, ref MovementState movementState, ref MovementSettings movementSettings, ref MovementControl movementControl, ref ResolvedMovement resolvedMovement);
|
||||
|
||||
/// <summary>
|
||||
/// Helper for adding and removing hooks to the FollowerEntity component.
|
||||
/// This is used to allow other systems to override the movement of the agent.
|
||||
///
|
||||
/// See: <see cref="FollowerEntity.movementOverrides"/>
|
||||
/// </summary>
|
||||
public ref struct ManagedMovementOverrides {
|
||||
Entity entity;
|
||||
World world;
|
||||
|
||||
public ManagedMovementOverrides (Entity entity, World world) {
|
||||
this.entity = entity;
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
public void AddBeforeControlCallback (BeforeControlDelegate value) {
|
||||
AddCallback<ManagedMovementOverrideBeforeControl, BeforeControlDelegate>(value);
|
||||
}
|
||||
public void RemoveBeforeControlCallback (BeforeControlDelegate value) {
|
||||
RemoveCallback<ManagedMovementOverrideBeforeControl, BeforeControlDelegate>(value);
|
||||
}
|
||||
|
||||
public void AddAfterControlCallback (AfterControlDelegate value) {
|
||||
AddCallback<ManagedMovementOverrideAfterControl, AfterControlDelegate>(value);
|
||||
}
|
||||
public void RemoveAfterControlCallback (AfterControlDelegate value) {
|
||||
RemoveCallback<ManagedMovementOverrideAfterControl, AfterControlDelegate>(value);
|
||||
}
|
||||
|
||||
public void AddBeforeMovementCallback (BeforeMovementDelegate value) {
|
||||
AddCallback<ManagedMovementOverrideBeforeMovement, BeforeMovementDelegate>(value);
|
||||
}
|
||||
public void RemoveBeforeMovementCallback (BeforeMovementDelegate value) {
|
||||
RemoveCallback<ManagedMovementOverrideBeforeMovement, BeforeMovementDelegate>(value);
|
||||
}
|
||||
|
||||
void AddCallback<C, T>(T callback) where T : System.Delegate where C : ManagedMovementOverride<T>, IComponentData, new() {
|
||||
if (callback == null) throw new System.ArgumentNullException(nameof(callback));
|
||||
if (world == null || !world.EntityManager.Exists(entity)) throw new System.InvalidOperationException("The entity does not exist. You can only set a callback when the FollowerEntity is active and has been enabled. If you are trying to set this during Awake or OnEnable, try setting it during Start instead.");
|
||||
if (!world.EntityManager.HasComponent<C>(entity)) world.EntityManager.AddComponentData(entity, new C());
|
||||
world.EntityManager.GetComponentData<C>(entity).AddCallback(callback);
|
||||
}
|
||||
|
||||
void RemoveCallback<C, T>(T callback) where T : System.Delegate where C : ManagedMovementOverride<T>, IComponentData, new() {
|
||||
if (callback == null) throw new System.ArgumentNullException(nameof(callback));
|
||||
if (world == null || !world.EntityManager.Exists(entity)) return;
|
||||
if (!world.EntityManager.HasComponent<C>(entity)) return;
|
||||
|
||||
var comp = world.EntityManager.GetComponentData<C>(entity);
|
||||
if (!comp.RemoveCallback(callback)) {
|
||||
world.EntityManager.RemoveComponent<C>(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores a delegate that can be used to override movement control and movement settings for a specific entity.
|
||||
/// This is used by the FollowerEntity to allow other systems to override the movement of the entity.
|
||||
///
|
||||
/// See: <see cref="FollowerEntity.movementOverrides"/>
|
||||
/// </summary>
|
||||
public class ManagedMovementOverride<T> : IComponentData where T : class, System.Delegate {
|
||||
public T callback;
|
||||
|
||||
public void AddCallback(T callback) => this.callback = (T)System.Delegate.Combine(this.callback, callback);
|
||||
public bool RemoveCallback(T callback) => (this.callback = (T)System.Delegate.Remove(this.callback, callback)) != null;
|
||||
}
|
||||
|
||||
// IJobEntity does not support generic jobs yet, so we have to make concrete component types for each delegate type
|
||||
public class ManagedMovementOverrideBeforeControl : ManagedMovementOverride<BeforeControlDelegate>, System.ICloneable {
|
||||
// No fields in this class can be cloned safely
|
||||
public object Clone() => new ManagedMovementOverrideBeforeControl();
|
||||
}
|
||||
public class ManagedMovementOverrideAfterControl : ManagedMovementOverride<AfterControlDelegate> {
|
||||
public object Clone() => new ManagedMovementOverrideAfterControl();
|
||||
}
|
||||
public class ManagedMovementOverrideBeforeMovement : ManagedMovementOverride<BeforeMovementDelegate> {
|
||||
public object Clone() => new ManagedMovementOverrideBeforeMovement();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce6a314668bdcdd498d5d9d3ebf753c2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,222 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding;
|
||||
using Pathfinding.ECS.RVO;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Settings for agent movement that cannot be put anywhere else.
|
||||
///
|
||||
/// The Unity ECS in general wants everything in components to be unmanaged types.
|
||||
/// However, some things cannot be unmanaged types, for example delegates and interfaces.
|
||||
/// There are also other things like path references and node references which are not unmanaged types at the moment.
|
||||
///
|
||||
/// This component is used to store those things.
|
||||
///
|
||||
/// It can also be used for things that are not used often, and so are best kept out-of-band to avoid bloating the ECS chunks too much.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class ManagedState : IComponentData, System.IDisposable, System.ICloneable {
|
||||
/// <summary>
|
||||
/// Settings for when to recalculate the path.
|
||||
///
|
||||
/// Deprecated: Use <see cref="FollowerEntity.autoRepath"/>, or the <see cref="Pathfinding.ECS.AutoRepathPolicy"/> component instead.
|
||||
/// </summary>
|
||||
[System.Obsolete("Use FollowerEntity.autoRepath, or the Pathfinding.ECS.AutoRepathPolicy component instead", true)]
|
||||
public Pathfinding.AutoRepathPolicy autoRepath => null;
|
||||
|
||||
/// <summary>Calculates in which direction to move to follow the path</summary>
|
||||
public PathTracer pathTracer;
|
||||
|
||||
/// <summary>
|
||||
/// Local avoidance settings.
|
||||
///
|
||||
/// When the agent has local avoidance enabled, these settings will be copied into a <see cref="Pathfinding.ECS.RVO.RVOAgent"/> component which is attached to the agent.
|
||||
///
|
||||
/// See: <see cref="enableLocalAvoidance"/>
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("rvoAgent")]
|
||||
public RVOAgent rvoSettings = RVOAgent.Default;
|
||||
|
||||
/// <summary>Callback for when the agent starts to traverse an off-mesh link</summary>
|
||||
[System.NonSerialized]
|
||||
public IOffMeshLinkHandler onTraverseOffMeshLink;
|
||||
|
||||
public PathRequestSettings pathfindingSettings = PathRequestSettings.Default;
|
||||
|
||||
/// <summary>
|
||||
/// True if local avoidance is enabled for this agent.
|
||||
///
|
||||
/// Enabling this will automatically add a <see cref="Pathfinding.ECS.RVO.RVOAgent"/> component to the entity.
|
||||
///
|
||||
/// See: local-avoidance (view in online documentation for working links)
|
||||
/// </summary>
|
||||
[FormerlySerializedAs("rvoEnabled")]
|
||||
public bool enableLocalAvoidance;
|
||||
|
||||
/// <summary>
|
||||
/// True if gravity is enabled for this agent.
|
||||
///
|
||||
/// The agent will always fall down according to its own movement plane.
|
||||
/// The gravity applied is Physics.gravity.y.
|
||||
///
|
||||
/// Enabling this will add the <see cref="GravityState"/> component to the entity.
|
||||
///
|
||||
/// This has no effect if the agent's orientation is set to YAxisForward (2D mode).
|
||||
/// Gravity does not really make sense for top-down 2D games. The gravity setting is also hidden from the inspector in this mode.
|
||||
/// </summary>
|
||||
public bool enableGravity = true;
|
||||
|
||||
/// <summary>Path that is being calculated, if any</summary>
|
||||
// Do not create a property visitor for this field, as otherwise the ECS infrastructure will try to patch entities inside it, and get very confused.
|
||||
// I haven't been able to replicate this issue recently, but it has caused problems in the past.
|
||||
// [Unity.Properties.DontCreateProperty]
|
||||
public Path pendingPath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Path that is being followed, if any.
|
||||
///
|
||||
/// The agent may have moved away from this path since it was calculated. So it may not be up to date.
|
||||
/// </summary>
|
||||
// Do not create a property visitor for this field, as otherwise the ECS infrastructure will try to patch entities inside it, and get very confused.
|
||||
// [Unity.Properties.DontCreateProperty]
|
||||
public Path activePath { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// \copydocref{IAstarAI.SetPath}.
|
||||
///
|
||||
/// Warning: In almost all cases you should use <see cref="FollowerEntity.SetPath"/> instead of this method.
|
||||
/// </summary>
|
||||
public static void SetPath (Path path, ManagedState state, in AgentMovementPlane movementPlane, ref DestinationPoint destination) {
|
||||
if (path == null) {
|
||||
state.CancelCurrentPathRequest();
|
||||
state.ClearPath();
|
||||
} else if (path.PipelineState == PathState.Created) {
|
||||
// Path has not started calculation yet
|
||||
state.CancelCurrentPathRequest();
|
||||
state.pendingPath = path;
|
||||
path.Claim(state);
|
||||
AstarPath.StartPath(path);
|
||||
} else if (path.PipelineState >= PathState.ReturnQueue) {
|
||||
// Path has already been calculated
|
||||
|
||||
if (state.pendingPath == path) {
|
||||
// The pending path is now obviously no longer pending
|
||||
state.pendingPath = null;
|
||||
} else {
|
||||
// We might be calculating another path at the same time, and we don't want that path to override this one. So cancel it.
|
||||
state.CancelCurrentPathRequest();
|
||||
|
||||
// Increase the refcount on the path.
|
||||
// If the path was already our pending path, then the refcount will have already been incremented
|
||||
path.Claim(state);
|
||||
}
|
||||
|
||||
var abPath = path as ABPath;
|
||||
if (abPath == null) throw new System.ArgumentException("This function only works with ABPaths, or paths inheriting from ABPath");
|
||||
|
||||
if (!abPath.error) {
|
||||
try {
|
||||
state.pathTracer.SetPath(abPath, movementPlane.value);
|
||||
|
||||
// Release the previous path back to the pool, to reduce GC pressure
|
||||
if (state.activePath != null) state.activePath.Release(state);
|
||||
|
||||
state.activePath = abPath;
|
||||
} catch (System.Exception e) {
|
||||
// If the path was so invalid that the path tracer throws an exception, then we should not use it.
|
||||
abPath.Release(state);
|
||||
state.ClearPath();
|
||||
UnityEngine.Debug.LogException(e);
|
||||
}
|
||||
|
||||
// If a RandomPath or MultiTargetPath have just been calculated, then we need
|
||||
// to patch our destination point, to ensure the agent continues to move towards the end of the path.
|
||||
// For these path types, the end point of the path is not known before the calculation starts.
|
||||
if (!abPath.endPointKnownBeforeCalculation) {
|
||||
destination = new DestinationPoint { destination = abPath.originalEndPoint, facingDirection = default };
|
||||
}
|
||||
|
||||
// Right now, the pathTracer is almost fully up to date.
|
||||
// To make it fully up to date, we'd also have to call pathTracer.UpdateStart and pathTracer.UpdateEnd after this function.
|
||||
// During normal path recalculations, the JobRepairPath will be scheduled right after this function, and it will
|
||||
// call those functions. The incomplete state will not be observable outside the system.
|
||||
// When called from FollowerEntity, the SetPath method on that component will ensure that these methods are called.
|
||||
} else {
|
||||
abPath.Release(state);
|
||||
}
|
||||
} else {
|
||||
// Path calculation has been started, but it is not yet complete. Cannot really handle this.
|
||||
throw new System.ArgumentException("You must call the SetPath method with a path that either has been completely calculated or one whose path calculation has not been started at all. It looks like the path calculation for the path you tried to use has been started, but is not yet finished.");
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearPath () {
|
||||
pathTracer.Clear();
|
||||
if (activePath != null) {
|
||||
activePath.Release(this);
|
||||
activePath = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void CancelCurrentPathRequest () {
|
||||
if (pendingPath != null) {
|
||||
pendingPath.FailWithError("Canceled by script");
|
||||
pendingPath.Release(this);
|
||||
pendingPath = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose () {
|
||||
pathTracer.Dispose();
|
||||
if (pendingPath != null) {
|
||||
pendingPath.FailWithError("Canceled because entity was destroyed");
|
||||
pendingPath.Release(this);
|
||||
pendingPath = null;
|
||||
}
|
||||
if (activePath != null) {
|
||||
activePath.Release(this);
|
||||
activePath = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pops the current part, and the next part from the start of the path.
|
||||
///
|
||||
/// It is assumed that the agent is currently on a normal NodeSequence part, and that the next part in the path is an off-mesh link.
|
||||
/// </summary>
|
||||
public void PopNextLinkFromPath () {
|
||||
if (pathTracer.partCount < 2 && pathTracer.GetPartType(1) != Funnel.PartType.OffMeshLink) {
|
||||
throw new System.InvalidOperationException("The next part in the path is not an off-mesh link.");
|
||||
}
|
||||
pathTracer.PopParts(2, pathfindingSettings.traversalProvider, activePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the managed state for when an entity is duplicated.
|
||||
///
|
||||
/// Some fields are cleared instead of being cloned, such as the pending path,
|
||||
/// which cannot reasonably be cloned.
|
||||
/// </summary>
|
||||
object System.ICloneable.Clone () {
|
||||
return new ManagedState {
|
||||
pathTracer = pathTracer.Clone(),
|
||||
rvoSettings = rvoSettings,
|
||||
pathfindingSettings = new PathRequestSettings {
|
||||
graphMask = pathfindingSettings.graphMask,
|
||||
tagPenalties = pathfindingSettings.tagPenalties != null ? (int[])pathfindingSettings.tagPenalties.Clone() : null,
|
||||
traversableTags = pathfindingSettings.traversableTags,
|
||||
traversalProvider = null, // Cannot be safely cloned or copied
|
||||
},
|
||||
enableLocalAvoidance = enableLocalAvoidance,
|
||||
enableGravity = enableGravity,
|
||||
onTraverseOffMeshLink = null, // Cannot be safely cloned or copied
|
||||
pendingPath = null, // Cannot be safely cloned or copied
|
||||
activePath = null, // Cannot be safely cloned or copied
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 33d1c95731798be41b90302b91409645
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,85 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding;
|
||||
using Pathfinding.Util;
|
||||
|
||||
/// <summary>
|
||||
/// Desired movement for an agent.
|
||||
/// This data will be fed to the local avoidance system to calculate the final movement of the agent.
|
||||
/// If no local avoidance is used, it will be directly copied to <see cref="ResolvedMovement"/>.
|
||||
///
|
||||
/// See: <see cref="ResolvedMovement"/>
|
||||
/// </summary>
|
||||
public struct MovementControl : IComponentData {
|
||||
/// <summary>The point the agent should move towards</summary>
|
||||
public float3 targetPoint;
|
||||
|
||||
/// <summary>
|
||||
/// The end of the current path.
|
||||
///
|
||||
/// This informs the local avoidance system about the final desired destination for the agent.
|
||||
/// This is used to make agents stop if the destination is crowded and it cannot reach its destination.
|
||||
///
|
||||
/// If this is not set, agents will often move forever around a crowded destination, always trying to find
|
||||
/// some way to get closer, but never finding it.
|
||||
/// </summary>
|
||||
public float3 endOfPath;
|
||||
|
||||
/// <summary>The speed at which the agent should move towards <see cref="targetPoint"/>, in meters per second</summary>
|
||||
public float speed;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum speed at which the agent may move, in meters per second.
|
||||
///
|
||||
/// It is recommended to keep this slightly above <see cref="speed"/>, to allow the local avoidance system to move agents around more efficiently when necessary.
|
||||
/// </summary>
|
||||
public float maxSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// The index of the hierarchical node that the agent is currently in.
|
||||
/// Will be -1 if the hierarchical node index is not known.
|
||||
/// See: <see cref="HierarchicalGraph"/>
|
||||
/// </summary>
|
||||
public int hierarchicalNodeIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The desired rotation of the agent, in radians, relative to the current movement plane.
|
||||
/// See: <see cref="NativeMovementPlane.ToWorldRotation"/>
|
||||
/// </summary>
|
||||
public float targetRotation;
|
||||
|
||||
/// <summary>
|
||||
/// The desired rotation of the agent, in radians, over a longer time horizon, relative to the current movement plane.
|
||||
///
|
||||
/// The <see cref="targetRotation"/> is usually only over a very short time-horizon, usually a single simulation time step.
|
||||
/// This variable is used to provide a hint of where the agent wants to rotate to over a slightly longer time scale (on the order of a second or so).
|
||||
/// It is not used to control movement directly, but it may be used to guide animations, or rotation smoothing.
|
||||
///
|
||||
/// If no better hint is available, this should be set to the same value as <see cref="targetRotation"/>.
|
||||
///
|
||||
/// See: <see cref="NativeMovementPlane.ToWorldRotation"/>
|
||||
/// </summary>
|
||||
public float targetRotationHint;
|
||||
|
||||
/// <summary>
|
||||
/// Additive modifier to <see cref="targetRotation"/>, in radians.
|
||||
/// This is used by the local avoidance system to rotate the agent, without this causing a feedback loop.
|
||||
/// This extra rotation will be ignored by the control system which decides how the agent *wants* to move.
|
||||
/// It will instead be directly applied to the agent.
|
||||
/// </summary>
|
||||
public float targetRotationOffset;
|
||||
|
||||
/// <summary>The speed at which the agent should rotate towards <see cref="targetRotation"/> + <see cref="targetRotationOffset"/>, in radians per second</summary>
|
||||
public float rotationSpeed;
|
||||
|
||||
/// <summary>
|
||||
/// If true, this agent will ignore other agents during local avoidance, but other agents will still avoid this one.
|
||||
/// This is useful for example for a player character which should not avoid other agents, but other agents should avoid the player.
|
||||
/// </summary>
|
||||
public bool overrideLocalAvoidance;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 067b0510e83c84e43b21eb81fb804132
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,127 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding.PID;
|
||||
|
||||
/// <summary>How to calculate which direction is "up" for the agent</summary>
|
||||
public enum MovementPlaneSource : byte {
|
||||
/// <summary>
|
||||
/// The graph's natural up direction will be used to align the agent.
|
||||
/// This is the most common option.
|
||||
/// </summary>
|
||||
Graph,
|
||||
/// <summary>
|
||||
/// The agent will be aligned with the normal of the navmesh.
|
||||
///
|
||||
/// This is useful when you have a spherical world, or some other strange shape.
|
||||
///
|
||||
/// The agent will look at the normal of the navmesh around the point it is currently standing on to determine which way is up.
|
||||
/// The radius of the agent will be used to determine the size of the area to sample the normal from.
|
||||
/// A bit of smoothing is done to make sure sharp changes in the normal do not cause the agent to rotate too fast.
|
||||
///
|
||||
/// Note: If you have a somewhat flat world, and you want to align the agent to the ground, this is not the option you want.
|
||||
/// Instead, you might want to disable <see cref="FollowerEntity.updateRotation"/> and then align the transform using a custom script.
|
||||
///
|
||||
/// Warning: Using this option has a performance penalty.
|
||||
///
|
||||
/// [Open online documentation to see videos]
|
||||
///
|
||||
/// See: spherical (view in online documentation for working links)
|
||||
/// </summary>
|
||||
NavmeshNormal,
|
||||
/// <summary>
|
||||
/// The agent will be aligned with the ground normal.
|
||||
///
|
||||
/// This is useful when you have a spherical world, or some other strange shape.
|
||||
///
|
||||
/// You may want to use this instead of the NavmeshNormal option if your collider is smoother than your navmesh.
|
||||
/// For example, if you have a spherical world with a sphere collider, you may want to use this option instead of the NavmeshNormal option.
|
||||
///
|
||||
/// Note: If you have a somewhat flat world, and you want to align the agent to the ground, this is not the option you want.
|
||||
/// Instead, you might want to disable <see cref="FollowerEntity.updateRotation"/> and then align the transform using a custom script.
|
||||
///
|
||||
/// Warning: Using this option has a performance penalty.
|
||||
/// </summary>
|
||||
Raycast,
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public struct MovementSettings : IComponentData {
|
||||
/// <summary>Additional movement settings</summary>
|
||||
public PIDMovement follower;
|
||||
|
||||
/// <summary>Flags for enabling debug rendering in the scene view</summary>
|
||||
public PIDMovement.DebugFlags debugFlags;
|
||||
|
||||
/// <summary>
|
||||
/// How far away from the destination should the agent aim to stop, in world units.
|
||||
///
|
||||
/// If the agent is within this distance from the destination point it will be considered to have reached the destination.
|
||||
///
|
||||
/// Even if you want the agent to stop precisely at a given point, it is recommended to keep this slightly above zero.
|
||||
/// If it is exactly zero, the agent may have a hard time deciding that it
|
||||
/// has actually reached the end of the path, due to floating point errors and such.
|
||||
///
|
||||
/// Note: This will not be multiplied the agent's scale.
|
||||
/// </summary>
|
||||
public float stopDistance;
|
||||
|
||||
/// <summary>
|
||||
/// How much to smooth the visual rotation of the agent.
|
||||
///
|
||||
/// This does not affect movement, but smoothes out how the agent rotates visually.
|
||||
///
|
||||
/// Recommended values are between 0.0 and 0.5.
|
||||
/// A value of zero will disable smoothing completely.
|
||||
///
|
||||
/// The smoothing is done primarily using an exponential moving average, but with
|
||||
/// a small linear term to make the rotation converge faster when the agent is almost facing the desired direction.
|
||||
///
|
||||
/// Adding smoothing will make the visual rotation of the agent lag a bit behind the actual rotation.
|
||||
/// Too much smoothing may make the agent seem sluggish, and appear to move sideways.
|
||||
///
|
||||
/// The unit for this field is seconds.
|
||||
/// </summary>
|
||||
public float rotationSmoothing;
|
||||
|
||||
/// <summary>
|
||||
/// How much to smooth the visual position of the agent.
|
||||
///
|
||||
/// This does not affect movement, but smoothes out the position of the agent visually.
|
||||
///
|
||||
/// Recommended values are between 0.0 and 0.5.
|
||||
/// A value of zero will disable smoothing completely.
|
||||
///
|
||||
/// This will make the agent seem to lag slightly behind the internal position of the agent.
|
||||
/// It may also cut corners slightly.
|
||||
///
|
||||
/// The unit for this field is seconds.
|
||||
/// </summary>
|
||||
public float positionSmoothing;
|
||||
|
||||
/// <summary>
|
||||
/// Layer mask to use for ground placement.
|
||||
/// Make sure this does not include the layer of any colliders attached to this gameobject.
|
||||
///
|
||||
/// See: <see cref="GravityState"/>
|
||||
/// See: https://docs.unity3d.com/Manual/Layers.html
|
||||
/// </summary>
|
||||
public LayerMask groundMask;
|
||||
|
||||
/// <summary>
|
||||
/// How to calculate which direction is "up" for the agent.
|
||||
/// See: <see cref="MovementPlaneSource"/>
|
||||
///
|
||||
/// Deprecated: Use the AgentMovementPlaneSource component instead, or the movementPlaneSource property on the FollowerEntity component
|
||||
/// </summary>
|
||||
[System.Obsolete("Use the AgentMovementPlaneSource component instead, or the movementPlaneSource property on the FollowerEntity component", true)]
|
||||
public MovementPlaneSource movementPlaneSource { get; set; }
|
||||
|
||||
/// <summary>\copydocref{IAstarAI.isStopped}</summary>
|
||||
public bool isStopped;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4fafdd860735074e8ca2abad75c3992
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,196 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding;
|
||||
using Pathfinding.PID;
|
||||
|
||||
public struct MovementState : IComponentData {
|
||||
/// <summary>State of the PID controller for the movement</summary>
|
||||
public PIDMovement.PersistentState followerState;
|
||||
|
||||
/// <summary>The next corner in the path</summary>
|
||||
public float3 nextCorner;
|
||||
|
||||
/// <summary>
|
||||
/// The end of the current path.
|
||||
/// Note that the agent may be heading towards an off-mesh link which is not the same as this point.
|
||||
/// </summary>
|
||||
public float3 endOfPath;
|
||||
|
||||
/// <summary>
|
||||
/// The closest point on the navmesh to the agent.
|
||||
/// The agent will be snapped to this point.
|
||||
/// </summary>
|
||||
public float3 closestOnNavmesh;
|
||||
|
||||
/// <summary>
|
||||
/// Offset from the agent's internal position to its visual position.
|
||||
///
|
||||
/// This is used when position smoothing is enabled. Otherwise it is zero.
|
||||
/// </summary>
|
||||
public float3 positionOffset;
|
||||
|
||||
/// <summary>
|
||||
/// The index of the hierarchical node that the agent is currently in.
|
||||
/// Will be -1 if the hierarchical node index is not known.
|
||||
///
|
||||
/// This field is valid during all system updates in the <see cref="AIMovementSystemGroup"/>. It is not guaranteed to be valid after that group has finished running, as graph updates may have changed the graph.
|
||||
///
|
||||
/// See: <see cref="HierarchicalGraph"/>
|
||||
/// </summary>
|
||||
public int hierarchicalNodeIndex;
|
||||
|
||||
/// <summary>The remaining distance until the end of the path, or the next off-mesh link</summary>
|
||||
public float remainingDistanceToEndOfPart;
|
||||
|
||||
/// <summary>
|
||||
/// The current additional rotation that is applied to the agent.
|
||||
/// This is used by the local avoidance system to rotate the agent, without this causing a feedback loop.
|
||||
///
|
||||
/// See: <see cref="ResolvedMovement.targetRotationOffset"/>
|
||||
/// </summary>
|
||||
public float rotationOffset;
|
||||
|
||||
/// <summary>
|
||||
/// An additional, purely visual, rotation offset.
|
||||
/// This is used for rotation smoothing, but does not affect the movement of the agent.
|
||||
/// </summary>
|
||||
public float rotationOffset2;
|
||||
|
||||
/// <summary>
|
||||
/// Version number of <see cref="PathTracer.version"/> when the movement state was last updated.
|
||||
/// In particular, <see cref="closestOnNavmesh"/>, <see cref="nextCorner"/>, <see cref="endOfPath"/>, <see cref="remainingDistanceToEndOfPart"/>, <see cref="reachedDestination"/> and <see cref="reachedEndOfPath"/> will only
|
||||
/// be considered up to date if this is equal to the current version number of the path tracer.
|
||||
/// </summary>
|
||||
public ushort pathTracerVersion;
|
||||
|
||||
/// <summary>Bitmask for various flags</summary>
|
||||
ushort flags;
|
||||
|
||||
const int ReachedDestinationFlag = 1 << 0;
|
||||
const int reachedDestinationAndOrientationFlag = 1 << 1;
|
||||
const int ReachedEndOfPathFlag = 1 << 2;
|
||||
const int reachedEndOfPathAndOrientationFlag = 1 << 3;
|
||||
const int ReachedEndOfPartFlag = 1 << 4;
|
||||
const int TraversingLastPartFlag = 1 << 5;
|
||||
|
||||
/// <summary>
|
||||
/// True if the agent has reached its destination.
|
||||
/// The destination will be considered reached if all of these conditions are met:
|
||||
/// - The agent has a path
|
||||
/// - The path is not stale
|
||||
/// - The destination is not significantly below the agent's feet.
|
||||
/// - The destination is not significantly above the agent's head.
|
||||
/// - The agent is on the last part of the path (there are no more remaining off-mesh links).
|
||||
/// - The remaining distance to the end of the path + the distance from the end of the path to the destination is less than <see cref="MovementSettings.stopDistance"/>.
|
||||
/// </summary>
|
||||
public bool reachedDestination {
|
||||
get => (flags & ReachedDestinationFlag) != 0;
|
||||
set => flags = (ushort)((flags & ~ReachedDestinationFlag) | (value ? ReachedDestinationFlag : 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the agent has reached its destination and is facing the desired orientation.
|
||||
/// This will become true if all of these conditions are met:
|
||||
/// - <see cref="reachedDestination"/> is true
|
||||
/// - The agent is facing the desired facing direction as specified in <see cref="DestinationPoint.facingDirection"/>.
|
||||
/// </summary>
|
||||
public bool reachedDestinationAndOrientation {
|
||||
get => (flags & reachedDestinationAndOrientationFlag) != 0;
|
||||
set => flags = (ushort)((flags & ~reachedDestinationAndOrientationFlag) | (value ? reachedDestinationAndOrientationFlag : 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the agent has reached the end of the path.
|
||||
/// The end of the path will be considered reached if all of these conditions are met:
|
||||
/// - The agent has a path
|
||||
/// - The path is not stale
|
||||
/// - The end of the path is not significantly below the agent's feet.
|
||||
/// - The end of the path is not significantly above the agent's head.
|
||||
/// - The agent is on the last part of the path (there are no more remaining off-mesh links).
|
||||
/// - The remaining distance to the end of the path is less than <see cref="MovementSettings.stopDistance"/>.
|
||||
/// </summary>
|
||||
public bool reachedEndOfPath {
|
||||
get => (flags & ReachedEndOfPathFlag) != 0;
|
||||
set => flags = (ushort)((flags & ~ReachedEndOfPathFlag) | (value ? ReachedEndOfPathFlag : 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the agent has reached its destination and is facing the desired orientation.
|
||||
/// This will become true if all of these conditions are met:
|
||||
/// - <see cref="reachedEndOfPath"/> is true
|
||||
/// - The agent is facing the desired facing direction as specified in <see cref="DestinationPoint.facingDirection"/>.
|
||||
/// </summary>
|
||||
public bool reachedEndOfPathAndOrientation {
|
||||
get => (flags & reachedEndOfPathAndOrientationFlag) != 0;
|
||||
set => flags = (ushort)((flags & ~reachedEndOfPathAndOrientationFlag) | (value ? reachedEndOfPathAndOrientationFlag : 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the agent has reached the end of the current part in the path.
|
||||
/// The end of the current part will be considered reached if all of these conditions are met:
|
||||
/// - The agent has a path
|
||||
/// - The path is not stale
|
||||
/// - The end of the current part is not significantly below the agent's feet.
|
||||
/// - The end of the current part is not significantly above the agent's head.
|
||||
/// - The remaining distance to the end of the part is not significantly larger than the agent's radius.
|
||||
/// </summary>
|
||||
public bool reachedEndOfPart {
|
||||
get => (flags & ReachedEndOfPartFlag) != 0;
|
||||
set => flags = (ushort)((flags & ~ReachedEndOfPartFlag) | (value ? ReachedEndOfPartFlag : 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the agent is traversing the last part of the path.
|
||||
///
|
||||
/// If false, the agent will have to traverse at least one off-mesh link before it gets to its destination.
|
||||
/// </summary>
|
||||
public bool traversingLastPart {
|
||||
get => (flags & TraversingLastPartFlag) != 0;
|
||||
set => flags = (ushort)((flags & ~TraversingLastPartFlag) | (value ? TraversingLastPartFlag : 0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The index of the graph that the agent is currently traversing.
|
||||
///
|
||||
/// Will be <see cref="GraphNode.InvalidGraphIndex"/> if the agent has no path, or the node that the agent is traversing has been destroyed.
|
||||
/// </summary>
|
||||
public uint graphIndex {
|
||||
get => (uint)(flags >> 8);
|
||||
internal set => flags = (ushort)((flags & 0xFF) | (ushort)(value << 8));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// True if the agent is currently on a valid node.
|
||||
///
|
||||
/// This is true if the agent has a path, and the node that the agent is traversing is walkable and not destroyed.
|
||||
///
|
||||
/// If false, the <see cref="hierarchicalNodeIndex"/> and <see cref="graphIndex"/> fields are invalid.
|
||||
/// </summary>
|
||||
public bool isOnValidNode => hierarchicalNodeIndex != -1;
|
||||
|
||||
public MovementState(UnityEngine.Vector3 agentPosition) {
|
||||
this = default;
|
||||
SetPathIsEmpty(agentPosition);
|
||||
}
|
||||
|
||||
/// <summary>Sets the appropriate fields to indicate that the agent has no path</summary>
|
||||
public void SetPathIsEmpty (UnityEngine.Vector3 agentPosition) {
|
||||
nextCorner = agentPosition;
|
||||
endOfPath = agentPosition;
|
||||
closestOnNavmesh = agentPosition;
|
||||
hierarchicalNodeIndex = -1;
|
||||
remainingDistanceToEndOfPart = float.PositiveInfinity;
|
||||
reachedEndOfPath = false;
|
||||
reachedDestination = false;
|
||||
reachedEndOfPart = false;
|
||||
reachedDestinationAndOrientation = false;
|
||||
reachedEndOfPathAndOrientation = false;
|
||||
traversingLastPart = true;
|
||||
graphIndex = GraphNode.InvalidGraphIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd27960b09d0034419af8c9451a551fb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,17 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
public struct MovementStatistics : IComponentData {
|
||||
/// <summary>
|
||||
/// The estimated velocity that the agent is moving with.
|
||||
/// This includes all form of movement, including local avoidance and gravity.
|
||||
/// </summary>
|
||||
public float3 estimatedVelocity;
|
||||
|
||||
/// <summary>The position of the agent at the end of the last movement simulation step</summary>
|
||||
public float3 lastPosition;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40111712788bfc3409f6b39341f91e2a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 233cdeb50c94c714ab4c82711f977368
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,91 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
#endif
|
||||
|
||||
namespace Pathfinding.ECS.RVO {
|
||||
using Pathfinding.RVO;
|
||||
using Unity.Collections;
|
||||
|
||||
/// <summary>
|
||||
/// Index of an RVO agent in the local avoidance simulation.
|
||||
///
|
||||
/// If this component is present, that indicates that the agent is part of a local avoidance simulation.
|
||||
/// The <see cref="RVOSystem"/> is responsible for adding and removing this component as necessary.
|
||||
/// Any other systems should only concern themselves with the <see cref="RVOAgent"/> component.
|
||||
///
|
||||
/// Warning: This component does not support cloning. You must not clone entities that use this component.
|
||||
/// There doesn't seem to be any way to make this work with the Unity.Entities API at the moment.
|
||||
/// </summary>
|
||||
#if MODULE_ENTITIES
|
||||
[WriteGroup(typeof(ResolvedMovement))]
|
||||
#endif
|
||||
public readonly struct AgentIndex
|
||||
#if MODULE_ENTITIES
|
||||
: Unity.Entities.ICleanupComponentData
|
||||
#endif
|
||||
{
|
||||
const int DeletedBit = 1 << 31;
|
||||
const int IndexMask = (1 << 24) - 1;
|
||||
const int VersionOffset = 24;
|
||||
const int VersionMask = 0b1111_111 << VersionOffset;
|
||||
|
||||
readonly int packedAgentIndex;
|
||||
|
||||
/// <summary>
|
||||
/// Index of the agent in the simulation's data arrays.
|
||||
///
|
||||
/// See: <see cref="TryGetIndex"/>
|
||||
/// </summary>
|
||||
internal int Index => packedAgentIndex & IndexMask;
|
||||
int Version => packedAgentIndex & VersionMask;
|
||||
internal bool Valid => (packedAgentIndex & DeletedBit) == 0;
|
||||
|
||||
internal AgentIndex(int packedAgentIndex) {
|
||||
this.packedAgentIndex = packedAgentIndex;
|
||||
}
|
||||
|
||||
internal AgentIndex(int version, int index) {
|
||||
version <<= VersionOffset;
|
||||
UnityEngine.Assertions.Assert.IsTrue((index & IndexMask) == index);
|
||||
packedAgentIndex = (version & VersionMask) | (index & IndexMask);
|
||||
}
|
||||
|
||||
internal readonly AgentIndex WithIncrementedVersion () {
|
||||
return new AgentIndex((((packedAgentIndex & VersionMask) + (1 << VersionOffset)) & VersionMask) | Index);
|
||||
}
|
||||
|
||||
internal readonly AgentIndex WithDeleted () {
|
||||
return new AgentIndex(packedAgentIndex | DeletedBit);
|
||||
}
|
||||
|
||||
/// <summary>True if the agent exists in the simulation</summary>
|
||||
public readonly bool Exists (ref SimulatorBurst.AgentData agentData) {
|
||||
return TryGetIndex(ref agentData, out _);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the agent in the simulation's data arrays, if the agent exists.
|
||||
///
|
||||
/// If the agent does not exist, the index will be set to -1 and the method returns false.
|
||||
/// </summary>
|
||||
public readonly bool TryGetIndex (ref SimulatorBurst.AgentData agentData, out int index) {
|
||||
return TryGetIndex(ref agentData.version, out index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the agent in the simulation's data arrays, if the agent exists.
|
||||
///
|
||||
/// If the agent does not exist, the index will be set to -1 and the method returns false.
|
||||
/// </summary>
|
||||
public readonly bool TryGetIndex (ref NativeArray<AgentIndex> agentDataVersions, out int index) {
|
||||
var tmpIndex = Index;
|
||||
index = -1;
|
||||
if (!agentDataVersions.IsCreated) return false;
|
||||
if (tmpIndex >= agentDataVersions.Length) return false;
|
||||
if (agentDataVersions[tmpIndex].Version != Version) return false;
|
||||
|
||||
index = tmpIndex;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd00f859416fc5c4f984c5680d19fc7d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,94 @@
|
||||
#if MODULE_ENTITIES
|
||||
using Pathfinding.RVO;
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
using Unity.Transforms;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace Pathfinding.ECS.RVO {
|
||||
using Pathfinding.RVO;
|
||||
|
||||
/// <summary>
|
||||
/// Agent data for the local avoidance system.
|
||||
///
|
||||
/// See: local-avoidance (view in online documentation for working links)
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public struct RVOAgent : IComponentData {
|
||||
/// <summary>How far into the future to look for collisions with other agents (in seconds)</summary>
|
||||
[Tooltip("How far into the future to look for collisions with other agents (in seconds)")]
|
||||
public float agentTimeHorizon;
|
||||
|
||||
/// <summary>How far into the future to look for collisions with obstacles (in seconds)</summary>
|
||||
[Tooltip("How far into the future to look for collisions with obstacles (in seconds)")]
|
||||
public float obstacleTimeHorizon;
|
||||
|
||||
/// <summary>
|
||||
/// Max number of other agents to take into account.
|
||||
/// A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality.
|
||||
/// </summary>
|
||||
[Tooltip("Max number of other agents to take into account.\n" +
|
||||
"A smaller value can reduce CPU load, a higher value can lead to better local avoidance quality.")]
|
||||
public int maxNeighbours;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the avoidance layer for this agent.
|
||||
/// The <see cref="collidesWith"/> mask on other agents will determine if they will avoid this agent.
|
||||
/// </summary>
|
||||
public RVOLayer layer;
|
||||
|
||||
/// <summary>
|
||||
/// Layer mask specifying which layers this agent will avoid.
|
||||
/// You can set it as CollidesWith = RVOLayer.DefaultAgent | RVOLayer.Layer3 | RVOLayer.Layer6 ...
|
||||
///
|
||||
/// This can be very useful in games which have multiple teams of some sort. For example you usually
|
||||
/// want the agents in one team to avoid each other, but you do not want them to avoid the enemies.
|
||||
///
|
||||
/// This field only affects which other agents that this agent will avoid, it does not affect how other agents
|
||||
/// react to this agent.
|
||||
///
|
||||
/// See: bitmasks (view in online documentation for working links)
|
||||
/// See: http://en.wikipedia.org/wiki/Mask_(computing)
|
||||
/// </summary>
|
||||
[Pathfinding.EnumFlag]
|
||||
public RVOLayer collidesWith;
|
||||
|
||||
/// <summary>\copydocref{Pathfinding.RVO.IAgent.Priority}</summary>
|
||||
[Tooltip("How strongly other agents will avoid this agent")]
|
||||
[UnityEngine.Range(0, 1)]
|
||||
public float priority;
|
||||
|
||||
/// <summary>
|
||||
/// Priority multiplier.
|
||||
/// This functions identically to the <see cref="priority"/>, however it is not exposed in the Unity inspector.
|
||||
/// It is primarily used by the <see cref="Pathfinding.RVO.RVODestinationCrowdedBehavior"/>.
|
||||
/// </summary>
|
||||
[System.NonSerialized]
|
||||
public float priorityMultiplier;
|
||||
|
||||
[System.NonSerialized]
|
||||
public float flowFollowingStrength;
|
||||
|
||||
/// <summary>Enables drawing debug information in the scene view</summary>
|
||||
public AgentDebugFlags debug;
|
||||
|
||||
/// <summary>A locked unit cannot move. Other units will still avoid it but avoidance quality is not the best.</summary>
|
||||
[Tooltip("A locked unit cannot move. Other units will still avoid it. But avoidance quality is not the best")]
|
||||
public bool locked;
|
||||
|
||||
/// <summary>Good default settings for an RVO agent</summary>
|
||||
public static readonly RVOAgent Default = new RVOAgent {
|
||||
locked = false,
|
||||
agentTimeHorizon = 1.0f,
|
||||
obstacleTimeHorizon = 0.5f,
|
||||
maxNeighbours = 10,
|
||||
layer = RVOLayer.DefaultAgent,
|
||||
collidesWith = (RVOLayer)(-1),
|
||||
priority = 0.5f,
|
||||
priorityMultiplier = 1.0f,
|
||||
flowFollowingStrength = 0.0f,
|
||||
debug = AgentDebugFlags.Nothing,
|
||||
};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 910690cbba23a2745a85046d13e5c03b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user