diff options
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors')
8 files changed, 414 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIDestinationSetter.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIDestinationSetter.cs new file mode 100644 index 0000000..5834f6f --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIDestinationSetter.cs @@ -0,0 +1,110 @@ +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. + /// 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"/> + /// + /// [Open online documentation to see images] + /// </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; + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIDestinationSetter.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIDestinationSetter.cs.meta new file mode 100644 index 0000000..b0a55d0 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIDestinationSetter.cs.meta @@ -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: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIPathAlignedToSurface.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIPathAlignedToSurface.cs new file mode 100644 index 0000000..c6a58c6 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIPathAlignedToSurface.cs @@ -0,0 +1,141 @@ +using UnityEngine; +using System.Collections.Generic; +using UnityEngine.Profiling; + +namespace Pathfinding { + using Pathfinding.Util; + 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 + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIPathAlignedToSurface.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIPathAlignedToSurface.cs.meta new file mode 100644 index 0000000..91969e5 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/AIPathAlignedToSurface.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4cf55afa704da4bada4b28145f5f4685 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/MoveInCircle.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/MoveInCircle.cs new file mode 100644 index 0000000..9d5c464 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/MoveInCircle.cs @@ -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> +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/MoveInCircle.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/MoveInCircle.cs.meta new file mode 100644 index 0000000..0572f6b --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/MoveInCircle.cs.meta @@ -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: diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/Patrol.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/Patrol.cs new file mode 100644 index 0000000..47f4c00 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/Patrol.cs @@ -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; + } + } + } +} diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/Patrol.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/Patrol.cs.meta new file mode 100644 index 0000000..7d5bfd6 --- /dev/null +++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Behaviors/Patrol.cs.meta @@ -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: |