summaryrefslogtreecommitdiff
path: root/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems
diff options
context:
space:
mode:
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems')
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs268
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs194
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs51
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs322
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs188
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs84
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs254
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs28
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs.meta11
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs91
-rw-r--r--Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs.meta11
18 files changed, 1579 insertions, 0 deletions
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs
new file mode 100644
index 0000000..c68ad7f
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs
@@ -0,0 +1,268 @@
+#pragma warning disable CS0282
+#if MODULE_ENTITIES
+using Unity.Entities;
+using Unity.Transforms;
+using Unity.Burst;
+using Unity.Jobs;
+using Unity.Collections;
+using UnityEngine;
+using UnityEngine.Jobs;
+using GCHandle = System.Runtime.InteropServices.GCHandle;
+
+namespace Pathfinding.ECS {
+ using Pathfinding;
+ using Pathfinding.ECS.RVO;
+ using Pathfinding.Drawing;
+ using Pathfinding.Util;
+ using Unity.Profiling;
+ using UnityEngine.Profiling;
+
+ [BurstCompile]
+ [UpdateAfter(typeof(FollowerControlSystem))]
+ [UpdateAfter(typeof(RVOSystem))]
+ [UpdateAfter(typeof(FallbackResolveMovementSystem))]
+ [UpdateInGroup(typeof(AIMovementSystemGroup))]
+ [RequireMatchingQueriesForUpdate]
+ public partial struct AIMoveSystem : ISystem {
+ EntityQuery entityQueryPrepareMovement;
+ EntityQuery entityQueryWithGravity;
+ EntityQuery entityQueryMove;
+ EntityQuery entityQueryRotation;
+ EntityQuery entityQueryGizmos;
+ EntityQuery entityQueryMovementOverride;
+ JobRepairPath.Scheduler jobRepairPathScheduler;
+ ComponentTypeHandle<MovementState> MovementStateTypeHandleRO;
+ ComponentTypeHandle<ResolvedMovement> ResolvedMovementHandleRO;
+
+ public static EntityQueryBuilder EntityQueryPrepareMovement () {
+ return new EntityQueryBuilder(Allocator.Temp)
+ .WithAllRW<MovementState>()
+ .WithAllRW<ManagedState>()
+ .WithAllRW<LocalTransform>()
+ .WithAll<MovementSettings, DestinationPoint, AgentMovementPlane, AgentCylinderShape>()
+ // .WithAny<ReadyToTraverseOffMeshLink>() // TODO: Use WithPresent in newer versions
+ .WithAbsent<AgentOffMeshLinkTraversal>();
+ }
+
+ public void OnCreate (ref SystemState state) {
+ jobRepairPathScheduler = new JobRepairPath.Scheduler(ref state);
+ MovementStateTypeHandleRO = state.GetComponentTypeHandle<MovementState>(true);
+ ResolvedMovementHandleRO = state.GetComponentTypeHandle<ResolvedMovement>(true);
+
+ entityQueryRotation = state.GetEntityQuery(
+ ComponentType.ReadWrite<LocalTransform>(),
+ ComponentType.ReadOnly<MovementSettings>(),
+ ComponentType.ReadOnly<MovementState>(),
+ ComponentType.ReadOnly<AgentCylinderShape>(),
+ ComponentType.ReadOnly<AgentMovementPlane>(),
+ ComponentType.ReadOnly<MovementControl>(),
+ ComponentType.ReadWrite<ResolvedMovement>(),
+ ComponentType.ReadOnly<SimulateMovement>(),
+ ComponentType.ReadOnly<SimulateMovementFinalize>()
+ );
+
+ entityQueryMove = state.GetEntityQuery(
+ ComponentType.ReadWrite<LocalTransform>(),
+ ComponentType.ReadOnly<AgentCylinderShape>(),
+ ComponentType.ReadOnly<AgentMovementPlane>(),
+ ComponentType.ReadWrite<MovementState>(),
+ ComponentType.ReadOnly<MovementSettings>(),
+ ComponentType.ReadOnly<ResolvedMovement>(),
+ ComponentType.ReadWrite<MovementStatistics>(),
+
+ ComponentType.ReadOnly<SimulateMovement>(),
+ ComponentType.ReadOnly<SimulateMovementFinalize>()
+ );
+
+ entityQueryWithGravity = state.GetEntityQuery(
+ ComponentType.ReadWrite<LocalTransform>(),
+ ComponentType.ReadOnly<AgentCylinderShape>(),
+ ComponentType.ReadWrite<AgentMovementPlane>(),
+ ComponentType.ReadWrite<MovementState>(),
+ ComponentType.ReadOnly<MovementSettings>(),
+ ComponentType.ReadWrite<ResolvedMovement>(),
+ ComponentType.ReadWrite<MovementStatistics>(),
+ ComponentType.ReadOnly<MovementControl>(),
+ ComponentType.ReadWrite<GravityState>(),
+
+ ComponentType.ReadOnly<AgentMovementPlaneSource>(),
+ ComponentType.ReadOnly<SimulateMovement>(),
+ ComponentType.ReadOnly<SimulateMovementFinalize>()
+ );
+
+ entityQueryPrepareMovement = jobRepairPathScheduler.GetEntityQuery(Allocator.Temp).WithAll<SimulateMovement, SimulateMovementRepair>().Build(ref state);
+
+ entityQueryGizmos = state.GetEntityQuery(
+ ComponentType.ReadOnly<LocalTransform>(),
+ ComponentType.ReadOnly<AgentCylinderShape>(),
+ ComponentType.ReadOnly<MovementSettings>(),
+ ComponentType.ReadOnly<AgentMovementPlane>(),
+ ComponentType.ReadOnly<ManagedState>(),
+ ComponentType.ReadOnly<MovementState>(),
+ ComponentType.ReadOnly<ResolvedMovement>(),
+
+ ComponentType.ReadOnly<SimulateMovement>()
+ );
+
+ entityQueryMovementOverride = state.GetEntityQuery(
+ ComponentType.ReadWrite<ManagedMovementOverrideBeforeMovement>(),
+
+ ComponentType.ReadWrite<LocalTransform>(),
+ ComponentType.ReadWrite<AgentCylinderShape>(),
+ ComponentType.ReadWrite<AgentMovementPlane>(),
+ ComponentType.ReadWrite<DestinationPoint>(),
+ ComponentType.ReadWrite<MovementState>(),
+ ComponentType.ReadWrite<MovementStatistics>(),
+ ComponentType.ReadWrite<ManagedState>(),
+ ComponentType.ReadWrite<MovementSettings>(),
+ ComponentType.ReadWrite<ResolvedMovement>(),
+ ComponentType.ReadWrite<MovementControl>(),
+
+ ComponentType.Exclude<AgentOffMeshLinkTraversal>(),
+ ComponentType.ReadOnly<SimulateMovement>(),
+ ComponentType.ReadOnly<SimulateMovementControl>()
+ );
+ }
+
+ static readonly ProfilerMarker MarkerMovementOverride = new ProfilerMarker("MovementOverrideBeforeMovement");
+
+ public void OnDestroy (ref SystemState state) {
+ jobRepairPathScheduler.Dispose();
+ }
+
+ public void OnUpdate (ref SystemState systemState) {
+ var draw = DrawingManager.GetBuilder();
+
+ // This system is executed at least every frame to make sure the agent is moving smoothly even at high fps.
+ // The control loop and local avoidance may be running less often.
+ // So this is designated a "cheap" system, and we use the corresponding delta time for that.
+ var dt = AIMovementSystemGroup.TimeScaledRateManager.CheapStepDeltaTime;
+
+ systemState.Dependency = new JobAlignAgentWithMovementDirection {
+ dt = dt,
+ }.Schedule(entityQueryRotation, systemState.Dependency);
+
+ RunMovementOverrideBeforeMovement(ref systemState, dt);
+
+ // Move all agents which do not have a GravityState component
+ systemState.Dependency = new JobMoveAgent {
+ dt = dt,
+ }.ScheduleParallel(entityQueryMove, systemState.Dependency);
+
+ ScheduleApplyGravity(ref systemState, draw, dt);
+ var gizmosDependency = systemState.Dependency;
+
+ UpdateTypeHandles(ref systemState);
+
+ systemState.Dependency = ScheduleRepairPaths(ref systemState, systemState.Dependency);
+
+ // Draw gizmos only in the editor, and at most once per frame.
+ // The movement calculations may run multiple times per frame when using high time-scales,
+ // but rendering gizmos more than once would just lead to clutter.
+ if (Application.isEditor && AIMovementSystemGroup.TimeScaledRateManager.IsLastSubstep) {
+ gizmosDependency = ScheduleDrawGizmos(draw, systemState.Dependency);
+ }
+
+ // Render gizmos as soon as all relevant jobs are done
+ draw.DisposeAfter(gizmosDependency);
+ systemState.Dependency = ScheduleSyncEntitiesToTransforms(ref systemState, systemState.Dependency);
+ systemState.Dependency = JobHandle.CombineDependencies(systemState.Dependency, gizmosDependency);
+ }
+
+ void ScheduleApplyGravity (ref SystemState systemState, CommandBuilder draw, float dt) {
+ Profiler.BeginSample("Gravity");
+ // Note: We cannot use CalculateEntityCountWithoutFiltering here, because the GravityState component can be disabled
+ var count = entityQueryWithGravity.CalculateEntityCount();
+ var raycastCommands = CollectionHelper.CreateNativeArray<RaycastCommand>(count, systemState.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory);
+ var raycastHits = CollectionHelper.CreateNativeArray<RaycastHit>(count, systemState.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory);
+
+ // Prepare raycasts for all entities that have a GravityState component
+ systemState.Dependency = new JobPrepareAgentRaycasts {
+ raycastQueryParameters = new QueryParameters(-1, false, QueryTriggerInteraction.Ignore, false),
+ raycastCommands = raycastCommands,
+ draw = draw,
+ dt = dt,
+ gravity = Physics.gravity.y,
+ }.ScheduleParallel(entityQueryWithGravity, systemState.Dependency);
+
+ var raycastJob = RaycastCommand.ScheduleBatch(raycastCommands, raycastHits, 32, 1, systemState.Dependency);
+
+ // Apply gravity and move all agents that have a GravityState component
+ systemState.Dependency = new JobApplyGravity {
+ raycastHits = raycastHits,
+ raycastCommands = raycastCommands,
+ draw = draw,
+ dt = dt,
+ }.ScheduleParallel(entityQueryWithGravity, JobHandle.CombineDependencies(systemState.Dependency, raycastJob));
+
+ Profiler.EndSample();
+ }
+
+ void RunMovementOverrideBeforeMovement (ref SystemState systemState, float dt) {
+ if (!entityQueryMovementOverride.IsEmptyIgnoreFilter) {
+ MarkerMovementOverride.Begin();
+ // The movement overrides always run on the main thread.
+ // This adds a sync point, but only if people actually add a movement override (which is rare).
+ systemState.CompleteDependency();
+ new JobManagedMovementOverrideBeforeMovement {
+ dt = dt,
+ // TODO: Add unit test to make sure it fires/not fires when it should
+ }.Run(entityQueryMovementOverride);
+ MarkerMovementOverride.End();
+ }
+ }
+
+ void UpdateTypeHandles (ref SystemState systemState) {
+ MovementStateTypeHandleRO.Update(ref systemState);
+ ResolvedMovementHandleRO.Update(ref systemState);
+ }
+
+ JobHandle ScheduleRepairPaths (ref SystemState systemState, JobHandle dependency) {
+ Profiler.BeginSample("RepairPaths");
+ // This job accesses graph data, but this is safe because the AIMovementSystemGroup
+ // holds a read lock on the graph data while its subsystems are running.
+ dependency = jobRepairPathScheduler.ScheduleParallel(ref systemState, entityQueryPrepareMovement, dependency);
+ Profiler.EndSample();
+ return dependency;
+ }
+
+ JobHandle ScheduleDrawGizmos (CommandBuilder commandBuilder, JobHandle dependency) {
+ // Note: The ScheduleRepairPaths job runs right before this, so those handles are still valid
+ return new JobDrawFollowerGizmos {
+ draw = commandBuilder,
+ entityManagerHandle = jobRepairPathScheduler.entityManagerHandle,
+ LocalTransformTypeHandleRO = jobRepairPathScheduler.LocalTransformTypeHandleRO,
+ AgentCylinderShapeHandleRO = jobRepairPathScheduler.AgentCylinderShapeTypeHandleRO,
+ MovementSettingsHandleRO = jobRepairPathScheduler.MovementSettingsTypeHandleRO,
+ AgentMovementPlaneHandleRO = jobRepairPathScheduler.AgentMovementPlaneTypeHandleRO,
+ ManagedStateHandleRW = jobRepairPathScheduler.ManagedStateTypeHandleRW,
+ MovementStateHandleRO = MovementStateTypeHandleRO,
+ ResolvedMovementHandleRO = ResolvedMovementHandleRO,
+ }.ScheduleParallel(entityQueryGizmos, dependency);
+ }
+
+ JobHandle ScheduleSyncEntitiesToTransforms (ref SystemState systemState, JobHandle dependency) {
+ Profiler.BeginSample("SyncEntitiesToTransforms");
+ int numComponents = BatchedEvents.GetComponents<FollowerEntity>(BatchedEvents.Event.None, out var transforms, out var components);
+ if (numComponents == 0) {
+ Profiler.EndSample();
+ return dependency;
+ }
+
+ var entities = CollectionHelper.CreateNativeArray<Entity>(numComponents, systemState.WorldUpdateAllocator);
+ for (int i = 0; i < numComponents; i++) entities[i] = components[i].entity;
+
+ dependency = new JobSyncEntitiesToTransforms {
+ entities = entities,
+ syncPositionWithTransform = SystemAPI.GetComponentLookup<SyncPositionWithTransform>(true),
+ syncRotationWithTransform = SystemAPI.GetComponentLookup<SyncRotationWithTransform>(true),
+ orientationYAxisForward = SystemAPI.GetComponentLookup<OrientationYAxisForward>(true),
+ entityPositions = SystemAPI.GetComponentLookup<LocalTransform>(true),
+ movementState = SystemAPI.GetComponentLookup<MovementState>(true),
+ }.Schedule(transforms, dependency);
+ Profiler.EndSample();
+ return dependency;
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs.meta
new file mode 100644
index 0000000..31a2d28
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f857e04ce9382d74989b3d469a0b956e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs
new file mode 100644
index 0000000..a97172c
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs
@@ -0,0 +1,194 @@
+#if MODULE_ENTITIES
+using Unity.Entities;
+using Unity.Transforms;
+using UnityEngine;
+using Unity.Collections;
+using Unity.Core;
+using Unity.Jobs;
+
+namespace Pathfinding.ECS {
+ [UpdateAfter(typeof(TransformSystemGroup))]
+ public partial class AIMovementSystemGroup : ComponentSystemGroup {
+ /// <summary>Rate manager which runs a system group multiple times if the delta time is higher than desired, but always executes the group at least once per frame</summary>
+ public class TimeScaledRateManager : IRateManager, System.IDisposable {
+ int numUpdatesThisFrame;
+ int updateIndex;
+ float stepDt;
+ float maximumDt = 1.0f / 30.0f;
+ NativeList<TimeData> cheapTimeDataQueue;
+ NativeList<TimeData> timeDataQueue;
+ double lastFullSimulation;
+ double lastCheapSimulation;
+ static bool cheapSimulationOnly;
+ static bool isLastSubstep;
+ static bool inGroup;
+ static TimeData cheapTimeData;
+
+ /// <summary>
+ /// True if it was determined that zero substeps should be simulated.
+ /// In this case all systems will get an opportunity to run a single update,
+ /// but they should avoid systems that don't have to run every single frame.
+ /// </summary>
+ public static bool CheapSimulationOnly {
+ get {
+ if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager");
+ return cheapSimulationOnly;
+ }
+ }
+
+ public static float CheapStepDeltaTime {
+ get {
+ if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager");
+ return cheapTimeData.DeltaTime;
+ }
+ }
+
+ /// <summary>True when this is the last substep of the current simulation</summary>
+ public static bool IsLastSubstep {
+ get {
+ if (!inGroup) throw new System.InvalidOperationException("Cannot call this method outside of a simulation group using TimeScaledRateManager");
+ return isLastSubstep;
+ }
+ }
+
+ public TimeScaledRateManager () {
+ cheapTimeDataQueue = new NativeList<TimeData>(Allocator.Persistent);
+ timeDataQueue = new NativeList<TimeData>(Allocator.Persistent);
+ }
+
+ public void Dispose () {
+ cheapTimeDataQueue.Dispose();
+ timeDataQueue.Dispose();
+ }
+
+ public bool ShouldGroupUpdate (ComponentSystemGroup group) {
+ // if this is true, means we're being called a second or later time in a loop.
+ if (inGroup) {
+ group.World.PopTime();
+ updateIndex++;
+ if (updateIndex >= numUpdatesThisFrame) {
+ inGroup = false;
+ return false;
+ }
+ } else {
+ cheapTimeDataQueue.Clear();
+ timeDataQueue.Clear();
+
+ if (inGroup) throw new System.InvalidOperationException("Cannot nest simulation groups using TimeScaledRateManager");
+ var fullDt = (float)(group.World.Time.ElapsedTime - lastFullSimulation);
+
+ // It has been observed that the time move backwards.
+ // Not quite sure when it happens, but we need to guard against it.
+ if (fullDt < 0) fullDt = 0;
+
+ // If the delta time is large enough we may want to perform multiple simulation sub-steps per frame.
+ // This is done to improve simulation stability. In particular at high time scales, but it also
+ // helps at low fps, or if the game has a sudden long stutter.
+ // We raise the value to a power slightly smaller than 1 to make the number of sub-steps increase
+ // more slowly as the delta time increases. This is important to avoid the edge case when
+ // the time it takes to run the simulation is longer than maximumDt. Otherwise the number of
+ // simulation sub-steps would increase without bound. However, the simulation quality
+ // may decrease a bit as the number of sub-steps increases.
+ numUpdatesThisFrame = Mathf.FloorToInt(Mathf.Pow(fullDt / maximumDt, 0.8f));
+ var currentTime = group.World.Time.ElapsedTime;
+ cheapSimulationOnly = numUpdatesThisFrame == 0;
+ if (cheapSimulationOnly) {
+ timeDataQueue.Add(new TimeData(
+ lastFullSimulation,
+ 0.0f
+ ));
+ cheapTimeDataQueue.Add(new TimeData(
+ currentTime,
+ (float)(currentTime - lastCheapSimulation)
+ ));
+ lastCheapSimulation = currentTime;
+ } else {
+ stepDt = fullDt / numUpdatesThisFrame;
+ // Push the time for each sub-step
+ for (int i = 0; i < numUpdatesThisFrame; i++) {
+ var stepTime = lastFullSimulation + (i+1) * stepDt;
+ timeDataQueue.Add(new TimeData(
+ stepTime,
+ stepDt
+ ));
+ cheapTimeDataQueue.Add(new TimeData(
+ stepTime,
+ (float)(stepTime - lastCheapSimulation)
+ ));
+ lastCheapSimulation = stepTime;
+ }
+ lastFullSimulation = currentTime;
+ }
+ numUpdatesThisFrame = Mathf.Max(1, numUpdatesThisFrame);
+ inGroup = true;
+ updateIndex = 0;
+ }
+
+ group.World.PushTime(timeDataQueue[updateIndex]);
+ cheapTimeData = cheapTimeDataQueue[updateIndex];
+ isLastSubstep = updateIndex + 1 >= numUpdatesThisFrame;
+
+ return true;
+ }
+
+ public float Timestep {
+ get => maximumDt;
+ set => maximumDt = value;
+ }
+ }
+
+ protected override void OnUpdate () {
+ // Various jobs (e.g. the JobRepairPath) in this system group may use graph data,
+ // and they also need the graph data to be consistent during the whole update.
+ // For example the MovementState.hierarchicalNodeIndex field needs to be valid
+ // during the whole group update, as it may be used by the RVOSystem and FollowerControlSystem.
+ // Locking the graph data as read-only here means that no graph updates will be performed
+ // while these jobs are running.
+ var readLock = AstarPath.active != null? AstarPath.active.LockGraphDataForReading() : default;
+
+ // And here I thought the entities package reaching 1.0 would mean that they wouldn't just rename
+ // properties without any compatibility code... but nope...
+#if MODULE_ENTITIES_1_0_8_OR_NEWER
+ var systems = this.GetUnmanagedSystems();
+ for (int i = 0; i < systems.Length; i++) {
+ ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i]);
+ state.Dependency = JobHandle.CombineDependencies(state.Dependency, readLock.dependency);
+ }
+#else
+ var systems = this.Systems;
+ for (int i = 0; i < systems.Count; i++) {
+ ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i].SystemHandle);
+ state.Dependency = JobHandle.CombineDependencies(state.Dependency, readLock.dependency);
+ }
+#endif
+
+ base.OnUpdate();
+
+ JobHandle readDependency = default;
+#if MODULE_ENTITIES_1_0_8_OR_NEWER
+ for (int i = 0; i < systems.Length; i++) {
+ ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i]);
+ readDependency = JobHandle.CombineDependencies(readDependency, state.Dependency);
+ }
+ systems.Dispose();
+#else
+ for (int i = 0; i < systems.Count; i++) {
+ ref var state = ref this.World.Unmanaged.ResolveSystemStateRef(systems[i].SystemHandle);
+ readDependency = JobHandle.CombineDependencies(readDependency, state.Dependency);
+ }
+#endif
+ readLock.UnlockAfter(readDependency);
+ }
+
+ protected override void OnDestroy () {
+ base.OnDestroy();
+ (this.RateManager as TimeScaledRateManager).Dispose();
+ }
+
+ protected override void OnCreate () {
+ base.OnCreate();
+ this.RateManager = new TimeScaledRateManager();
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs.meta
new file mode 100644
index 0000000..da24779
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/AIMovementSystemGroup.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 84577891deac65d458d801d960c6fcee
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs
new file mode 100644
index 0000000..a4d590a
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs
@@ -0,0 +1,51 @@
+#pragma warning disable CS0282
+#if MODULE_ENTITIES
+using Unity.Entities;
+using Unity.Burst;
+using Unity.Collections;
+
+namespace Pathfinding.ECS {
+ using Pathfinding;
+ using Pathfinding.ECS.RVO;
+
+ /// <summary>Copies <see cref="MovementControl"/> to <see cref="ResolvedMovement"/> when no local avoidance is used</summary>
+ [BurstCompile]
+ [UpdateAfter(typeof(FollowerControlSystem))]
+ [UpdateAfter(typeof(RVOSystem))] // Has to execute after RVOSystem in case that system detects that some agents should not be simulated using the RVO system anymore.
+ [UpdateInGroup(typeof(AIMovementSystemGroup))]
+ [RequireMatchingQueriesForUpdate]
+ public partial struct FallbackResolveMovementSystem : ISystem {
+ EntityQuery entityQuery;
+
+ public void OnCreate (ref SystemState state) {
+ entityQuery = state.GetEntityQuery(new EntityQueryDesc {
+ All = new ComponentType[] {
+ ComponentType.ReadWrite<ResolvedMovement>(),
+ ComponentType.ReadOnly<MovementControl>(),
+ ComponentType.ReadOnly<SimulateMovement>()
+ },
+ Options = EntityQueryOptions.FilterWriteGroup
+ });
+ }
+
+ public void OnDestroy (ref SystemState state) { }
+
+ public void OnUpdate (ref SystemState systemState) {
+ new CopyJob {}.Schedule(entityQuery);
+ }
+
+ [BurstCompile]
+ public partial struct CopyJob : IJobEntity {
+ public void Execute (in MovementControl control, ref ResolvedMovement resolved) {
+ resolved.targetPoint = control.targetPoint;
+ resolved.speed = control.speed;
+ resolved.turningRadiusMultiplier = 1.0f;
+ resolved.targetRotation = control.targetRotation;
+ resolved.targetRotationHint = control.targetRotationHint;
+ resolved.targetRotationOffset = control.targetRotationOffset;
+ resolved.rotationSpeed = control.rotationSpeed;
+ }
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs.meta
new file mode 100644
index 0000000..fbac068
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FallbackResolveMovementSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5502c9f6a3e7fc448803d8b0607c6eac
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs
new file mode 100644
index 0000000..bc50184
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs
@@ -0,0 +1,322 @@
+#pragma warning disable CS0282
+#if MODULE_ENTITIES
+using Unity.Entities;
+using UnityEngine.Profiling;
+using Unity.Profiling;
+using Unity.Transforms;
+using Unity.Burst;
+using Unity.Jobs;
+using GCHandle = System.Runtime.InteropServices.GCHandle;
+
+namespace Pathfinding.ECS {
+ using Pathfinding;
+ using Pathfinding.ECS.RVO;
+ using Pathfinding.Drawing;
+ using Pathfinding.RVO;
+ using Unity.Collections;
+ using Unity.Burst.Intrinsics;
+ using System.Diagnostics;
+
+ [UpdateInGroup(typeof(AIMovementSystemGroup))]
+ [BurstCompile]
+ public partial struct FollowerControlSystem : ISystem {
+ EntityQuery entityQueryPrepare;
+ EntityQuery entityQueryControl;
+ EntityQuery entityQueryControlManaged;
+ EntityQuery entityQueryControlManaged2;
+ EntityQuery entityQueryOffMeshLink;
+ EntityQuery entityQueryOffMeshLinkCleanup;
+ public JobRepairPath.Scheduler jobRepairPathScheduler;
+ RedrawScope redrawScope;
+
+ static readonly ProfilerMarker MarkerMovementOverrideBeforeControl = new ProfilerMarker("MovementOverrideBeforeControl");
+ static readonly ProfilerMarker MarkerMovementOverrideAfterControl = new ProfilerMarker("MovementOverrideAfterControl");
+
+ public void OnCreate (ref SystemState state) {
+ jobRepairPathScheduler = new JobRepairPath.Scheduler(ref state);
+
+ redrawScope = DrawingManager.GetRedrawScope();
+
+ entityQueryPrepare = jobRepairPathScheduler.GetEntityQuery(Unity.Collections.Allocator.Temp).WithAll<SimulateMovement, SimulateMovementRepair>().Build(ref state);
+ entityQueryControl = state.GetEntityQuery(
+ ComponentType.ReadWrite<LocalTransform>(),
+ ComponentType.ReadOnly<AgentCylinderShape>(),
+ ComponentType.ReadOnly<AgentMovementPlane>(),
+ ComponentType.ReadOnly<DestinationPoint>(),
+ ComponentType.ReadWrite<MovementState>(),
+ ComponentType.ReadOnly<MovementStatistics>(),
+ ComponentType.ReadWrite<ManagedState>(),
+ ComponentType.ReadOnly<MovementSettings>(),
+ ComponentType.ReadOnly<ResolvedMovement>(),
+ ComponentType.ReadWrite<MovementControl>(),
+
+ ComponentType.Exclude<AgentOffMeshLinkTraversal>(),
+ ComponentType.ReadOnly<SimulateMovement>(),
+ ComponentType.ReadOnly<SimulateMovementControl>()
+ );
+
+ entityQueryControlManaged = state.GetEntityQuery(
+ ComponentType.ReadWrite<ManagedMovementOverrideBeforeControl>(),
+
+ ComponentType.ReadWrite<LocalTransform>(),
+ ComponentType.ReadWrite<AgentCylinderShape>(),
+ ComponentType.ReadWrite<AgentMovementPlane>(),
+ ComponentType.ReadWrite<DestinationPoint>(),
+ ComponentType.ReadWrite<MovementState>(),
+ ComponentType.ReadWrite<MovementStatistics>(),
+ ComponentType.ReadWrite<ManagedState>(),
+ ComponentType.ReadWrite<MovementSettings>(),
+ ComponentType.ReadWrite<ResolvedMovement>(),
+ ComponentType.ReadWrite<MovementControl>(),
+
+ ComponentType.Exclude<AgentOffMeshLinkTraversal>(),
+ ComponentType.ReadOnly<SimulateMovement>(),
+ ComponentType.ReadOnly<SimulateMovementControl>()
+ );
+
+ entityQueryControlManaged2 = state.GetEntityQuery(
+ ComponentType.ReadWrite<ManagedMovementOverrideAfterControl>(),
+
+ ComponentType.ReadWrite<LocalTransform>(),
+ ComponentType.ReadWrite<AgentCylinderShape>(),
+ ComponentType.ReadWrite<AgentMovementPlane>(),
+ ComponentType.ReadWrite<DestinationPoint>(),
+ ComponentType.ReadWrite<MovementState>(),
+ ComponentType.ReadWrite<MovementStatistics>(),
+ ComponentType.ReadWrite<ManagedState>(),
+ ComponentType.ReadWrite<MovementSettings>(),
+ ComponentType.ReadWrite<ResolvedMovement>(),
+ ComponentType.ReadWrite<MovementControl>(),
+
+ ComponentType.Exclude<AgentOffMeshLinkTraversal>(),
+ ComponentType.ReadOnly<SimulateMovement>(),
+ ComponentType.ReadOnly<SimulateMovementControl>()
+ );
+
+ entityQueryOffMeshLink = state.GetEntityQuery(
+ ComponentType.ReadWrite<LocalTransform>(),
+ ComponentType.ReadOnly<AgentCylinderShape>(),
+ ComponentType.ReadWrite<AgentMovementPlane>(),
+ ComponentType.ReadOnly<DestinationPoint>(),
+ ComponentType.ReadWrite<MovementState>(),
+ ComponentType.ReadOnly<MovementStatistics>(),
+ ComponentType.ReadWrite<ManagedState>(),
+ ComponentType.ReadWrite<MovementSettings>(),
+ ComponentType.ReadOnly<ResolvedMovement>(),
+ ComponentType.ReadWrite<MovementControl>(),
+ ComponentType.ReadWrite<AgentOffMeshLinkTraversal>(),
+ ComponentType.ReadWrite<ManagedAgentOffMeshLinkTraversal>(),
+ ComponentType.ReadOnly<SimulateMovement>()
+ );
+
+ entityQueryOffMeshLinkCleanup = state.GetEntityQuery(
+ // ManagedAgentOffMeshLinkTraversal is a cleanup component.
+ // If it exists, but the AgentOffMeshLinkTraversal does not exist,
+ // then the agent must have been destroyed while traversing the off-mesh link.
+ ComponentType.ReadWrite<ManagedAgentOffMeshLinkTraversal>(),
+ ComponentType.Exclude<AgentOffMeshLinkTraversal>()
+ );
+ }
+
+ public void OnDestroy (ref SystemState state) {
+ redrawScope.Dispose();
+ jobRepairPathScheduler.Dispose();
+ }
+
+ public void OnUpdate (ref SystemState systemState) {
+ if (AstarPath.active == null) return;
+
+ var commandBuffer = new EntityCommandBuffer(systemState.WorldUpdateAllocator);
+
+ SyncLocalAvoidanceComponents(ref systemState, commandBuffer);
+ SchedulePaths(ref systemState);
+ StartOffMeshLinkTraversal(ref systemState, commandBuffer);
+
+ commandBuffer.Playback(systemState.EntityManager);
+ commandBuffer.Dispose();
+
+ ProcessActiveOffMeshLinkTraversal(ref systemState);
+ RepairPaths(ref systemState);
+
+ // The full movement calculations do not necessarily need to be done every frame if the fps is high
+ if (!AIMovementSystemGroup.TimeScaledRateManager.CheapSimulationOnly) {
+ ProcessControlLoop(ref systemState, SystemAPI.Time.DeltaTime);
+ }
+ }
+
+ void SyncLocalAvoidanceComponents (ref SystemState systemState, EntityCommandBuffer commandBuffer) {
+ var simulator = RVOSimulator.active?.GetSimulator();
+ // First check if we have a simulator. If not, we can skip handling RVO components
+ if (simulator == null) return;
+
+ Profiler.BeginSample("AddRVOComponents");
+ foreach (var(managedState, entity) in SystemAPI.Query<ManagedState>().WithNone<RVOAgent>().WithEntityAccess()) {
+ if (managedState.enableLocalAvoidance) {
+ commandBuffer.AddComponent<RVOAgent>(entity, managedState.rvoSettings);
+ }
+ }
+ Profiler.EndSample();
+ Profiler.BeginSample("CopyRVOSettings");
+ foreach (var(managedState, rvoAgent, entity) in SystemAPI.Query<ManagedState, RefRW<RVOAgent> >().WithEntityAccess()) {
+ rvoAgent.ValueRW = managedState.rvoSettings;
+ if (!managedState.enableLocalAvoidance) {
+ commandBuffer.RemoveComponent<RVOAgent>(entity);
+ }
+ }
+
+ Profiler.EndSample();
+ }
+
+ void RepairPaths (ref SystemState systemState) {
+ Profiler.BeginSample("RepairPaths");
+ // This job accesses managed component data in a somewhat unsafe way.
+ // It should be safe to run it in parallel with other systems, but I'm not 100% sure.
+ // This job also accesses graph data, but this is safe because the AIMovementSystemGroup
+ // holds a read lock on the graph data while its subsystems are running.
+ systemState.Dependency = jobRepairPathScheduler.ScheduleParallel(ref systemState, entityQueryPrepare, systemState.Dependency);
+ Profiler.EndSample();
+ }
+
+ [BurstCompile]
+ [WithAbsent(typeof(ManagedAgentOffMeshLinkTraversal))] // Do not recalculate the path of agents that are currently traversing an off-mesh link.
+ partial struct JobShouldRecalculatePaths : IJobEntity {
+ public float time;
+ public NativeBitArray shouldRecalculatePath;
+ int index;
+
+ public void Execute (ref ECS.AutoRepathPolicy autoRepathPolicy, in LocalTransform transform, in AgentCylinderShape shape, in DestinationPoint destination) {
+ if (index >= shouldRecalculatePath.Length) {
+ shouldRecalculatePath.Resize(shouldRecalculatePath.Length * 2, NativeArrayOptions.ClearMemory);
+ }
+ shouldRecalculatePath.Set(index++, autoRepathPolicy.ShouldRecalculatePath(transform.Position, shape.radius, destination.destination, time));
+ }
+ }
+
+ [WithAbsent(typeof(ManagedAgentOffMeshLinkTraversal))] // Do not recalculate the path of agents that are currently traversing an off-mesh link.
+ public partial struct JobRecalculatePaths : IJobEntity {
+ public float time;
+ public NativeBitArray shouldRecalculatePath;
+ int index;
+
+ public void Execute (ManagedState state, ref ECS.AutoRepathPolicy autoRepathPolicy, ref LocalTransform transform, ref DestinationPoint destination, ref AgentMovementPlane movementPlane) {
+ MaybeRecalculatePath(state, ref autoRepathPolicy, ref transform, ref destination, ref movementPlane, time, shouldRecalculatePath.IsSet(index++));
+ }
+
+ public static void MaybeRecalculatePath (ManagedState state, ref ECS.AutoRepathPolicy autoRepathPolicy, ref LocalTransform transform, ref DestinationPoint destination, ref AgentMovementPlane movementPlane, float time, bool wantsToRecalculatePath) {
+ if ((state.pathTracer.isStale || wantsToRecalculatePath) && state.pendingPath == null) {
+ if (autoRepathPolicy.mode != Pathfinding.AutoRepathPolicy.Mode.Never && float.IsFinite(destination.destination.x)) {
+ var path = ABPath.Construct(transform.Position, destination.destination, null);
+ path.UseSettings(state.pathfindingSettings);
+ path.nnConstraint.distanceMetric = DistanceMetric.ClosestAsSeenFromAboveSoft(movementPlane.value.up);
+ ManagedState.SetPath(path, state, in movementPlane, ref destination);
+ autoRepathPolicy.DidRecalculatePath(destination.destination, time);
+ }
+ }
+ }
+ }
+
+ void SchedulePaths (ref SystemState systemState) {
+ Profiler.BeginSample("Schedule search");
+ // Block the pathfinding threads from starting new path calculations while this loop is running.
+ // This is done to reduce lock contention and significantly improve performance.
+ // If we did not do this, all pathfinding threads would immediately wake up when a path was pushed to the queue.
+ // Immediately when they wake up they will try to acquire a lock on the path queue.
+ // If we are scheduling a lot of paths, this causes significant contention, and can make this loop take 100 times
+ // longer to complete, compared to if we block the pathfinding threads.
+ // TODO: Switch to a lock-free queue to avoid this issue altogether.
+ var bits = new NativeBitArray(512, Allocator.TempJob);
+ systemState.CompleteDependency();
+ var pathfindingLock = AstarPath.active.PausePathfindingSoon();
+ // Calculate which agents want to recalculate their path (using burst)
+ new JobShouldRecalculatePaths {
+ time = (float)SystemAPI.Time.ElapsedTime,
+ shouldRecalculatePath = bits,
+ }.Run();
+ // Schedule the path calculations
+ new JobRecalculatePaths {
+ time = (float)SystemAPI.Time.ElapsedTime,
+ shouldRecalculatePath = bits,
+ }.Run();
+ pathfindingLock.Release();
+ bits.Dispose();
+ Profiler.EndSample();
+ }
+
+ void StartOffMeshLinkTraversal (ref SystemState systemState, EntityCommandBuffer commandBuffer) {
+ Profiler.BeginSample("Start off-mesh link traversal");
+ foreach (var(state, entity) in SystemAPI.Query<ManagedState>().WithAll<ReadyToTraverseOffMeshLink>()
+ .WithEntityAccess()
+ // Do not try to add another off-mesh link component to agents that already have one.
+ .WithNone<AgentOffMeshLinkTraversal>()) {
+ // UnityEngine.Assertions.Assert.IsTrue(movementState.ValueRO.reachedEndOfPart && state.pathTracer.isNextPartValidLink);
+ var linkInfo = NextLinkToTraverse(state);
+ var ctx = new AgentOffMeshLinkTraversalContext(linkInfo.link);
+ // Add the AgentOffMeshLinkTraversal and ManagedAgentOffMeshLinkTraversal components when the agent should start traversing an off-mesh link.
+ commandBuffer.AddComponent(entity, new AgentOffMeshLinkTraversal(linkInfo));
+ commandBuffer.AddComponent(entity, new ManagedAgentOffMeshLinkTraversal(ctx, ResolveOffMeshLinkHandler(state, ctx)));
+ }
+ Profiler.EndSample();
+ }
+
+ public static OffMeshLinks.OffMeshLinkTracer NextLinkToTraverse (ManagedState state) {
+ return state.pathTracer.GetLinkInfo(1);
+ }
+
+ public static IOffMeshLinkHandler ResolveOffMeshLinkHandler (ManagedState state, AgentOffMeshLinkTraversalContext ctx) {
+ var handler = state.onTraverseOffMeshLink ?? ctx.concreteLink.handler;
+ return handler;
+ }
+
+ void ProcessActiveOffMeshLinkTraversal (ref SystemState systemState) {
+ var commandBuffer = new EntityCommandBuffer(systemState.WorldUpdateAllocator);
+ systemState.CompleteDependency();
+ new JobManagedOffMeshLinkTransition {
+ commandBuffer = commandBuffer,
+ deltaTime = AIMovementSystemGroup.TimeScaledRateManager.CheapStepDeltaTime,
+ }.Run(entityQueryOffMeshLink);
+
+ new JobManagedOffMeshLinkTransitionCleanup().Run(entityQueryOffMeshLinkCleanup);
+#if MODULE_ENTITIES_1_0_8_OR_NEWER
+ commandBuffer.RemoveComponent<ManagedAgentOffMeshLinkTraversal>(entityQueryOffMeshLinkCleanup, EntityQueryCaptureMode.AtPlayback);
+#else
+ commandBuffer.RemoveComponent<ManagedAgentOffMeshLinkTraversal>(entityQueryOffMeshLinkCleanup);
+#endif
+ commandBuffer.Playback(systemState.EntityManager);
+ commandBuffer.Dispose();
+ }
+
+ void ProcessControlLoop (ref SystemState systemState, float dt) {
+ // This is a hook for other systems to modify the movement of agents.
+ // Normally it is not used.
+ if (!entityQueryControlManaged.IsEmpty) {
+ MarkerMovementOverrideBeforeControl.Begin();
+ systemState.Dependency.Complete();
+ new JobManagedMovementOverrideBeforeControl {
+ dt = dt,
+ }.Run(entityQueryControlManaged);
+ MarkerMovementOverrideBeforeControl.End();
+ }
+
+ redrawScope.Rewind();
+ var draw = DrawingManager.GetBuilder(redrawScope);
+ var navmeshEdgeData = AstarPath.active.GetNavmeshBorderData(out var readLock);
+ systemState.Dependency = new JobControl {
+ navmeshEdgeData = navmeshEdgeData,
+ draw = draw,
+ dt = dt,
+ }.ScheduleParallel(entityQueryControl, JobHandle.CombineDependencies(systemState.Dependency, readLock.dependency));
+ readLock.UnlockAfter(systemState.Dependency);
+ draw.DisposeAfter(systemState.Dependency);
+
+ if (!entityQueryControlManaged2.IsEmpty) {
+ MarkerMovementOverrideAfterControl.Begin();
+ systemState.Dependency.Complete();
+ new JobManagedMovementOverrideAfterControl {
+ dt = dt,
+ }.Run(entityQueryControlManaged2);
+ MarkerMovementOverrideAfterControl.End();
+ }
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs.meta
new file mode 100644
index 0000000..653caa6
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/FollowerControlSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ce754b44fa448624dac9bbefe12d03e9
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs
new file mode 100644
index 0000000..de40655
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs
@@ -0,0 +1,188 @@
+#pragma warning disable CS0282
+#if MODULE_ENTITIES
+using Unity.Entities;
+using Unity.Mathematics;
+using Unity.Burst;
+using Unity.Collections;
+
+namespace Pathfinding.ECS {
+ using System.Collections.Generic;
+ using System.Runtime.InteropServices;
+ using Pathfinding;
+ using Pathfinding.Drawing;
+ using Pathfinding.Util;
+ using Unity.Transforms;
+ using UnityEngine.Profiling;
+
+ [UpdateBefore(typeof(FollowerControlSystem))]
+ [UpdateInGroup(typeof(AIMovementSystemGroup))]
+ [RequireMatchingQueriesForUpdate]
+ [BurstCompile]
+ public partial struct MovementPlaneFromGraphSystem : ISystem {
+ public EntityQuery entityQueryGraph;
+ public EntityQuery entityQueryNormal;
+ NativeArray<float3> sphereSamplePoints;
+ // Store the queue in a GCHandle to avoid restrictions on ISystem
+ GCHandle graphNodeQueue;
+
+ public void OnCreate (ref SystemState state) {
+ entityQueryGraph = state.GetEntityQuery(ComponentType.ReadOnly<MovementState>(), ComponentType.ReadWrite<AgentMovementPlane>(), ComponentType.ReadOnly<AgentMovementPlaneSource>());
+ entityQueryGraph.SetSharedComponentFilter(new AgentMovementPlaneSource { value = MovementPlaneSource.Graph });
+ entityQueryNormal = state.GetEntityQuery(
+ ComponentType.ReadWrite<ManagedState>(),
+ ComponentType.ReadOnly<LocalTransform>(),
+ ComponentType.ReadWrite<AgentMovementPlane>(),
+ ComponentType.ReadOnly<AgentCylinderShape>(),
+ ComponentType.ReadOnly<AgentMovementPlaneSource>()
+ );
+ entityQueryNormal.AddSharedComponentFilter(new AgentMovementPlaneSource { value = MovementPlaneSource.NavmeshNormal });
+
+ // Number of samples to use when approximating the normal, when using the NavmeshNormal mode.
+ const int Samples = 16;
+ sphereSamplePoints = new NativeArray<float3>(Samples, Allocator.Persistent);
+ UnityEngine.Random.InitState(0);
+ for (int i = 0; i < Samples; i++) {
+ sphereSamplePoints[i] = (float3)UnityEngine.Random.insideUnitSphere;
+ }
+
+ graphNodeQueue = GCHandle.Alloc(new List<GraphNode>(32));
+ }
+
+ public void OnDestroy (ref SystemState state) {
+ sphereSamplePoints.Dispose();
+ graphNodeQueue.Free();
+ }
+
+ public void OnUpdate (ref SystemState systemState) {
+ var graphs = AstarPath.active?.data.graphs;
+ if (graphs == null) return;
+
+ var movementPlanes = CollectionHelper.CreateNativeArray<AgentMovementPlane>(graphs.Length, systemState.WorldUpdateAllocator, NativeArrayOptions.UninitializedMemory);
+ for (int i = 0; i < graphs.Length; i++) {
+ var graph = graphs[i];
+ var plane = new NativeMovementPlane(quaternion.identity);
+ if (graph is NavmeshBase navmesh) {
+ plane = new NativeMovementPlane(navmesh.transform.rotation);
+ } else if (graph is GridGraph grid) {
+ plane = new NativeMovementPlane(grid.transform.rotation);
+ }
+ movementPlanes[i] = new AgentMovementPlane {
+ value = plane,
+ };
+ }
+
+ if (!entityQueryNormal.IsEmpty) {
+ systemState.CompleteDependency();
+ var vertices = new NativeList<float3>(16, Allocator.Temp);
+ new JobMovementPlaneFromNavmeshNormal {
+ dt = AIMovementSystemGroup.TimeScaledRateManager.CheapStepDeltaTime,
+ sphereSamplePoints = sphereSamplePoints,
+ vertices = vertices,
+ que = (List<GraphNode>)graphNodeQueue.Target,
+ }.Run(entityQueryNormal);
+ }
+
+ systemState.Dependency = new JobMovementPlaneFromGraph {
+ movementPlanes = movementPlanes,
+ }.Schedule(entityQueryGraph, systemState.Dependency);
+ }
+
+ partial struct JobMovementPlaneFromNavmeshNormal : IJobEntity {
+ public float dt;
+ [ReadOnly]
+ public NativeArray<float3> sphereSamplePoints;
+ public NativeList<float3> vertices;
+ public List<GraphNode> que;
+
+ public void Execute (ManagedState managedState, in LocalTransform localTransform, ref AgentMovementPlane agentMovementPlane, in AgentCylinderShape shape) {
+ var sphereSamplePointsSpan = sphereSamplePoints.AsUnsafeSpan();
+ var node = managedState.pathTracer.startNode;
+ // TODO: Expose these parameters?
+ float size = shape.radius * 1.5f;
+ const float InverseSmoothness = 20f;
+ if (node != null) {
+ vertices.Clear();
+ que.Clear();
+ var position = localTransform.Position;
+ var bounds = new UnityEngine.Bounds(position, new float3(size, size, size));
+ int queStart = 0;
+ node.TemporaryFlag1 = true;
+ que.Add(node);
+
+ while (queStart < que.Count) {
+ var current = que[queStart++] as TriangleMeshNode;
+
+ current.GetVertices(out var v0, out var v1, out var v2);
+ var p0 = (float3)v0;
+ var p1 = (float3)v1;
+ var p2 = (float3)v2;
+ Polygon.ClosestPointOnTriangleByRef(in p0, in p1, in p2, in position, out var closest);
+ if (math.lengthsq(closest - position) < size*size) {
+ vertices.Add(p0);
+ vertices.Add(p1);
+ vertices.Add(p2);
+ current.GetConnections((GraphNode con, ref List<GraphNode> que) => {
+ if (!con.TemporaryFlag1) {
+ con.TemporaryFlag1 = true;
+ que.Add(con);
+ }
+ }, ref que);
+ }
+ }
+
+ // Reset temporary flags
+ for (int i = 0; i < que.Count; i++) {
+ que[i].TemporaryFlag1 = false;
+ }
+
+ var verticesSpan = vertices.AsUnsafeSpan();
+ SampleTriangleNormals(ref sphereSamplePointsSpan, ref position, size, ref verticesSpan, ref agentMovementPlane, dt * InverseSmoothness);
+ }
+ }
+ }
+
+ [BurstCompile]
+ partial struct JobMovementPlaneFromGraph : IJobEntity {
+ [ReadOnly]
+ public NativeArray<AgentMovementPlane> movementPlanes;
+
+ public void Execute (in MovementState movementState, ref AgentMovementPlane movementPlane) {
+ if (movementState.graphIndex < (uint)movementPlanes.Length) {
+ movementPlane = movementPlanes[(int)movementState.graphIndex];
+ } else {
+ // This can happen if the agent has no path, or if the path is stale.
+ // Potentially also if a graph has been removed.
+ }
+ }
+ }
+
+ [BurstCompile(FloatMode = FloatMode.Fast)]
+ static void SampleTriangleNormals (ref UnsafeSpan<float3> samplePoints, ref float3 sampleOrigin, float sampleScale, ref UnsafeSpan<float3> triangleVertices, ref AgentMovementPlane agentMovementPlane, float alpha) {
+ var targetNormal = float3.zero;
+ int normalWeight = 0;
+ for (int i = 0; i < triangleVertices.Length; i += 3) {
+ var p0 = triangleVertices[i + 0];
+ var p1 = triangleVertices[i + 1];
+ var p2 = triangleVertices[i + 2];
+ var triangleNormal = math.normalizesafe(math.cross(p1 - p0, p2 - p0));
+
+ for (int j = 0; j < samplePoints.Length; j++) {
+ var p = samplePoints[j] * sampleScale + sampleOrigin;
+ if (Polygon.ClosestPointOnTriangleByRef(in p0, in p1, in p2, in p, out var closest) && math.lengthsq(closest - sampleOrigin) < sampleScale*sampleScale) {
+ targetNormal += triangleNormal;
+ normalWeight++;
+ }
+ }
+ }
+
+ if (normalWeight > 0) {
+ targetNormal = math.normalizesafe(targetNormal / normalWeight);
+
+ var currentNormal = agentMovementPlane.value.up;
+ var nextNormal = math.lerp(currentNormal, targetNormal, math.clamp(0, 1, alpha));
+ JobApplyGravity.UpdateMovementPlaneFromNormal(nextNormal, ref agentMovementPlane);
+ }
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs.meta
new file mode 100644
index 0000000..f8563f3
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/MovementPlaneFromGraphSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e03b8b6eb150263419cf52d753bc4bc5
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs
new file mode 100644
index 0000000..35ea267
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs
@@ -0,0 +1,84 @@
+#pragma warning disable CS0282
+#if MODULE_ENTITIES
+using Unity.Entities;
+using Unity.Burst;
+using GCHandle = System.Runtime.InteropServices.GCHandle;
+using Unity.Transforms;
+
+namespace Pathfinding.ECS {
+ /// <summary>
+ /// Checks if paths have been calculated, and updates the agent's paths if they have.
+ ///
+ /// This is essentially a replacement for <see cref="Path.callback"/> for ECS agents.
+ ///
+ /// This system is a bit different in that it doesn't run in the normal update loop,
+ /// but instead it will run when the <see cref="AstarPath.OnPathsCalculated"/> event fires.
+ /// This is to avoid having to call a separate callback for every agent, since that
+ /// would result in excessive overhead as it would have to synchronize with the ECS world
+ /// on every such call.
+ ///
+ /// See: <see cref="AstarPath.OnPathsCalculated"/>
+ /// </summary>
+ [BurstCompile]
+ public partial struct PollPendingPathsSystem : ISystem {
+ GCHandle onPathsCalculated;
+ static bool anyPendingPaths;
+
+ JobRepairPath.Scheduler jobRepairPathScheduler;
+ EntityQuery entityQueryPrepare;
+
+ public void OnCreate (ref SystemState state) {
+ jobRepairPathScheduler = new JobRepairPath.Scheduler(ref state) {
+ onlyApplyPendingPaths = true,
+ };
+ entityQueryPrepare = jobRepairPathScheduler.GetEntityQuery(Unity.Collections.Allocator.Temp).Build(ref state);
+
+ var world = state.WorldUnmanaged;
+ System.Action onPathsCalculated = () => {
+ // Allow the system to run
+ anyPendingPaths = true;
+ try {
+ // Update the system manually
+ world.GetExistingUnmanagedSystem<PollPendingPathsSystem>().Update(world);
+ } finally {
+ anyPendingPaths = false;
+ }
+ };
+ AstarPath.OnPathsCalculated += onPathsCalculated;
+ // Store the callback in a GCHandle to get around limitations on unmanaged systems.
+ this.onPathsCalculated = GCHandle.Alloc(onPathsCalculated);
+ }
+
+ public void OnDestroy (ref SystemState state) {
+ AstarPath.OnPathsCalculated -= (System.Action)onPathsCalculated.Target;
+ onPathsCalculated.Free();
+ jobRepairPathScheduler.Dispose();
+ }
+
+ void OnUpdate (ref SystemState systemState) {
+ // Only run the system when we have triggered it manually
+ if (!anyPendingPaths) return;
+
+ // During an off-mesh link traversal, we shouldn't calculate any paths, because it's somewhat undefined where they should start.
+ // Paths are already cancelled when the off-mesh link traversal starts, but just in case it has been started by a user manually in some way, we also cancel them every frame.
+ foreach (var state in SystemAPI.Query<ManagedState>().WithAll<AgentOffMeshLinkTraversal>()) state.CancelCurrentPathRequest();
+
+ // The JobRepairPath may access graph data, so we need to lock it for reading.
+ // Otherwise a graph update could start while the job was running, which could cause all kinds of problems.
+ var readLock = AstarPath.active.LockGraphDataForReading();
+
+ // Iterate over all agents and check if they have any pending paths, and if they have been calculated.
+ // If they have, we update the agent's current path to the newly calculated one.
+ //
+ // We do this by running the JobRepairPath for all agents that have just had their path calculated.
+ // This ensures that all properties like remainingDistance are up to date immediately after
+ // a path recalculation.
+ // This may seem wasteful, but during the next update, the regular JobRepairPath job
+ // will most likely be able to early out, because we did most of the work here.
+ systemState.Dependency = jobRepairPathScheduler.ScheduleParallel(ref systemState, entityQueryPrepare, systemState.Dependency);
+
+ readLock.UnlockAfter(systemState.Dependency);
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs.meta
new file mode 100644
index 0000000..6f6c2c7
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/PollPendingPathsSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 12ab30eb86c3d4841b72f49aa252574c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs
new file mode 100644
index 0000000..3f6b217
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs
@@ -0,0 +1,254 @@
+#pragma warning disable CS0282
+#if MODULE_ENTITIES
+using Unity.Mathematics;
+using Unity.Burst;
+using Unity.Entities;
+using Unity.Transforms;
+using Unity.Collections;
+using GCHandle = System.Runtime.InteropServices.GCHandle;
+
+namespace Pathfinding.ECS.RVO {
+ using Pathfinding.RVO;
+
+ [BurstCompile]
+ [UpdateAfter(typeof(FollowerControlSystem))]
+ [UpdateInGroup(typeof(AIMovementSystemGroup))]
+ public partial struct RVOSystem : ISystem {
+ EntityQuery entityQuery;
+ /// <summary>
+ /// Keeps track of the last simulator that this RVOSystem saw.
+ /// This is a weak GCHandle to allow it to be stored in an ISystem.
+ /// </summary>
+ GCHandle lastSimulator;
+ EntityQuery withAgentIndex;
+ EntityQuery shouldBeAddedToSimulation;
+ EntityQuery shouldBeRemovedFromSimulation;
+ ComponentLookup<AgentOffMeshLinkTraversal> agentOffMeshLinkTraversalLookup;
+
+ public void OnCreate (ref SystemState state) {
+ entityQuery = state.GetEntityQuery(
+ ComponentType.ReadOnly<AgentCylinderShape>(),
+ ComponentType.ReadOnly<LocalTransform>(),
+ ComponentType.ReadOnly<RVOAgent>(),
+ ComponentType.ReadOnly<AgentIndex>(),
+ ComponentType.ReadOnly<AgentMovementPlane>(),
+ ComponentType.ReadOnly<MovementControl>(),
+ ComponentType.ReadWrite<ResolvedMovement>(),
+ ComponentType.ReadOnly<SimulateMovement>()
+ );
+ withAgentIndex = state.GetEntityQuery(
+ ComponentType.ReadWrite<AgentIndex>()
+ );
+ shouldBeAddedToSimulation = state.GetEntityQuery(
+ ComponentType.ReadOnly<RVOAgent>(),
+ ComponentType.Exclude<AgentIndex>()
+ );
+ shouldBeRemovedFromSimulation = state.GetEntityQuery(
+ ComponentType.ReadOnly<AgentIndex>(),
+ ComponentType.Exclude<RVOAgent>()
+ );
+ lastSimulator = GCHandle.Alloc(null, System.Runtime.InteropServices.GCHandleType.Weak);
+ agentOffMeshLinkTraversalLookup = state.GetComponentLookup<AgentOffMeshLinkTraversal>(true);
+ }
+
+ public void OnDestroy (ref SystemState state) {
+ lastSimulator.Free();
+ }
+
+ public void OnUpdate (ref SystemState systemState) {
+ var simulator = RVOSimulator.active?.GetSimulator();
+
+ if (simulator != lastSimulator.Target) {
+ // If the simulator has been destroyed, we need to remove all AgentIndex components
+ RemoveAllAgentsFromSimulation(ref systemState);
+ lastSimulator.Target = simulator;
+ }
+ if (simulator == null) return;
+
+ AddAndRemoveAgentsFromSimulation(ref systemState, simulator);
+
+ // The full movement calculations do not necessarily need to be done every frame if the fps is high
+ if (AIMovementSystemGroup.TimeScaledRateManager.CheapSimulationOnly) {
+ return;
+ }
+
+ CopyFromEntitiesToRVOSimulator(ref systemState, simulator, SystemAPI.Time.DeltaTime);
+
+ // Schedule RVO update
+ systemState.Dependency = simulator.Update(
+ systemState.Dependency,
+ SystemAPI.Time.DeltaTime,
+ AIMovementSystemGroup.TimeScaledRateManager.IsLastSubstep,
+ systemState.WorldUpdateAllocator
+ );
+
+ CopyFromRVOSimulatorToEntities(ref systemState, simulator);
+ simulator.LockSimulationDataReadOnly(systemState.Dependency);
+ }
+
+ void RemoveAllAgentsFromSimulation (ref SystemState systemState) {
+ var buffer = new EntityCommandBuffer(Allocator.Temp);
+ var entities = withAgentIndex.ToEntityArray(systemState.WorldUpdateAllocator);
+ buffer.RemoveComponent<AgentIndex>(entities);
+ buffer.Playback(systemState.EntityManager);
+ buffer.Dispose();
+ }
+
+ void AddAndRemoveAgentsFromSimulation (ref SystemState systemState, SimulatorBurst simulator) {
+ // Remove all agents from the simulation that do not have an RVOAgent component, but have an AgentIndex
+ var indicesToRemove = shouldBeRemovedFromSimulation.ToComponentDataArray<AgentIndex>(systemState.WorldUpdateAllocator);
+ // Add all agents to the simulation that have an RVOAgent component, but not AgentIndex component
+ var entitiesToAdd = shouldBeAddedToSimulation.ToEntityArray(systemState.WorldUpdateAllocator);
+ // Avoid a sync point in the common case
+ if (indicesToRemove.Length > 0 || entitiesToAdd.Length > 0) {
+ var buffer = new EntityCommandBuffer(Allocator.Temp);
+#if MODULE_ENTITIES_1_0_8_OR_NEWER
+ buffer.RemoveComponent<AgentIndex>(shouldBeRemovedFromSimulation, EntityQueryCaptureMode.AtPlayback);
+#else
+ buffer.RemoveComponent<AgentIndex>(shouldBeRemovedFromSimulation);
+#endif
+ for (int i = 0; i < indicesToRemove.Length; i++) {
+ simulator.RemoveAgent(indicesToRemove[i]);
+ }
+ for (int i = 0; i < entitiesToAdd.Length; i++) {
+ buffer.AddComponent<AgentIndex>(entitiesToAdd[i], simulator.AddAgentBurst(UnityEngine.Vector3.zero));
+ }
+
+ buffer.Playback(systemState.EntityManager);
+ buffer.Dispose();
+ }
+ }
+
+ void CopyFromEntitiesToRVOSimulator (ref SystemState systemState, SimulatorBurst simulator, float dt) {
+ agentOffMeshLinkTraversalLookup.Update(ref systemState);
+ systemState.Dependency = new JobCopyFromEntitiesToRVOSimulator {
+ agentData = simulator.simulationData,
+ agentOutputData = simulator.outputData,
+ movementPlaneMode = simulator.movementPlane,
+ agentOffMeshLinkTraversalLookup = agentOffMeshLinkTraversalLookup,
+ dt = dt,
+ }.ScheduleParallel(entityQuery, systemState.Dependency);
+ }
+
+ void CopyFromRVOSimulatorToEntities (ref SystemState systemState, SimulatorBurst simulator) {
+ systemState.Dependency = new JobCopyFromRVOSimulatorToEntities {
+ quadtree = simulator.quadtree,
+ agentData = simulator.simulationData,
+ agentOutputData = simulator.outputData,
+ }.ScheduleParallel(entityQuery, systemState.Dependency);
+ }
+
+ [BurstCompile]
+ public partial struct JobCopyFromEntitiesToRVOSimulator : IJobEntity {
+ [NativeDisableParallelForRestriction]
+ public SimulatorBurst.AgentData agentData;
+ [ReadOnly]
+ public SimulatorBurst.AgentOutputData agentOutputData;
+ public MovementPlane movementPlaneMode;
+ [ReadOnly]
+ public ComponentLookup<AgentOffMeshLinkTraversal> agentOffMeshLinkTraversalLookup;
+ public float dt;
+
+ public void Execute (Entity entity, in LocalTransform transform, in AgentCylinderShape shape, in AgentMovementPlane movementPlane, in AgentIndex agentIndex, in RVOAgent controller, in MovementControl target) {
+ var scale = math.abs(transform.Scale);
+ var index = agentIndex.Index;
+
+ if (agentData.version[index].Version != agentIndex.Version) throw new System.InvalidOperationException("RVOAgent has an invalid entity index");
+
+ // Actual infinity is not handled well by some algorithms, but very large values are ok.
+ // This should be larger than any reasonable value a user might want to use.
+ const float VERY_LARGE = 100000;
+
+ // Copy all fields to the rvo simulator, and clamp them to reasonable values
+ agentData.radius[index] = math.clamp(shape.radius * scale, 0.001f, VERY_LARGE);
+ agentData.agentTimeHorizon[index] = math.clamp(controller.agentTimeHorizon, 0, VERY_LARGE);
+ agentData.obstacleTimeHorizon[index] = math.clamp(controller.obstacleTimeHorizon, 0, VERY_LARGE);
+ agentData.locked[index] = controller.locked;
+ agentData.maxNeighbours[index] = math.max(controller.maxNeighbours, 0);
+ agentData.debugFlags[index] = controller.debug;
+ agentData.layer[index] = controller.layer;
+ agentData.collidesWith[index] = controller.collidesWith;
+ agentData.targetPoint[index] = target.targetPoint;
+ agentData.desiredSpeed[index] = math.clamp(target.speed, 0, VERY_LARGE);
+ agentData.maxSpeed[index] = math.clamp(target.maxSpeed, 0, VERY_LARGE);
+ agentData.manuallyControlled[index] = target.overrideLocalAvoidance;
+ agentData.endOfPath[index] = target.endOfPath;
+ agentData.hierarchicalNodeIndex[index] = target.hierarchicalNodeIndex;
+ // control.endOfPath // TODO
+ agentData.movementPlane[index] = movementPlane.value;
+
+ // Use the position from the movement script if one is attached
+ // as the movement script's position may not be the same as the transform's position
+ // (in particular if IAstarAI.updatePosition is false).
+ var pos = movementPlane.value.ToPlane(transform.Position, out float elevation);
+ var center = 0.5f * shape.height;
+ if (movementPlaneMode == MovementPlane.XY) {
+ // In 2D it is assumed the Z coordinate differences of agents is ignored.
+ agentData.height[index] = 1;
+ agentData.position[index] = movementPlane.value.ToWorld(pos, 0);
+ } else {
+ agentData.height[index] = math.clamp(shape.height * scale, 0, VERY_LARGE);
+ agentData.position[index] = movementPlane.value.ToWorld(pos, elevation + (center - 0.5f * shape.height) * scale);
+ }
+
+
+ // TODO: Move this to a separate file
+ var reached = agentOutputData.effectivelyReachedDestination[index];
+ var prio = math.clamp(controller.priority * controller.priorityMultiplier, 0, VERY_LARGE);
+ var flow = math.clamp(controller.flowFollowingStrength, 0, 1);
+ if (reached == ReachedEndOfPath.Reached) {
+ flow = math.lerp(agentData.flowFollowingStrength[index], 1.0f, 6.0f * dt);
+ prio *= 0.3f;
+ } else if (reached == ReachedEndOfPath.ReachedSoon) {
+ flow = math.lerp(agentData.flowFollowingStrength[index], 1.0f, 6.0f * dt);
+ prio *= 0.45f;
+ }
+ agentData.priority[index] = prio;
+ agentData.flowFollowingStrength[index] = flow;
+
+ if (agentOffMeshLinkTraversalLookup.HasComponent(entity)) {
+ // Agents traversing off-mesh links should not avoid other agents,
+ // but other agents may still avoid them.
+ agentData.manuallyControlled[index] = true;
+ }
+ }
+ }
+
+ [BurstCompile]
+ public partial struct JobCopyFromRVOSimulatorToEntities : IJobEntity {
+ [ReadOnly]
+ public SimulatorBurst.AgentData agentData;
+ [ReadOnly]
+ public RVOQuadtreeBurst quadtree;
+ [ReadOnly]
+ public SimulatorBurst.AgentOutputData agentOutputData;
+
+ /// <summary>See https://en.wikipedia.org/wiki/Circle_packing</summary>
+ const float MaximumCirclePackingDensity = 0.9069f;
+
+ public void Execute (in LocalTransform transform, in AgentCylinderShape shape, in AgentIndex agentIndex, in RVOAgent controller, in MovementControl control, ref ResolvedMovement resolved) {
+ var index = agentIndex.Index;
+
+ if (agentData.version[index].Version != agentIndex.Version) return;
+
+ var scale = math.abs(transform.Scale);
+ var r = shape.radius * scale * 3f;
+ var area = quadtree.QueryArea(transform.Position, r);
+ var density = area / (MaximumCirclePackingDensity * math.PI * r * r);
+
+
+ resolved.targetPoint = agentOutputData.targetPoint[index];
+ resolved.speed = agentOutputData.speed[index];
+ var rnd = 1.0f; // (agentIndex.Index % 1024) / 1024f;
+ resolved.turningRadiusMultiplier = math.max(1f, math.pow(density * 2.0f, 4.0f) * rnd);
+
+ // Pure copy
+ resolved.targetRotation = control.targetRotation;
+ resolved.targetRotationHint = control.targetRotationHint;
+ resolved.targetRotationOffset = control.targetRotationOffset;
+ resolved.rotationSpeed = control.rotationSpeed;
+ }
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs.meta
new file mode 100644
index 0000000..a291705
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4ab994574a30005439b0db78c01279f7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs
new file mode 100644
index 0000000..30c0205
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs
@@ -0,0 +1,28 @@
+#pragma warning disable CS0282
+#if MODULE_ENTITIES
+using Unity.Entities;
+using UnityEngine;
+
+namespace Pathfinding.ECS {
+ using Pathfinding;
+
+ [UpdateBefore(typeof(FollowerControlSystem))]
+ [UpdateInGroup(typeof(AIMovementSystemGroup))]
+ [RequireMatchingQueriesForUpdate]
+ public partial struct SyncDestinationTransformSystem : ISystem {
+ public void OnCreate (ref SystemState state) {}
+ public void OnDestroy (ref SystemState state) {}
+
+ public void OnUpdate (ref SystemState systemState) {
+ foreach (var(point, destinationSetter) in SystemAPI.Query<RefRW<DestinationPoint>, AIDestinationSetter>()) {
+ if (destinationSetter.target != null) {
+ point.ValueRW = new DestinationPoint {
+ destination = destinationSetter.target.position,
+ facingDirection = destinationSetter.useRotation ? destinationSetter.target.forward : Vector3.zero
+ };
+ }
+ }
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs.meta
new file mode 100644
index 0000000..274915b
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncDestinationTransformSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f6ec5674da0fa5043bc982e1a4afed11
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs
new file mode 100644
index 0000000..0fcbf60
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs
@@ -0,0 +1,91 @@
+#pragma warning disable CS0282
+#if MODULE_ENTITIES
+using Unity.Entities;
+using Unity.Mathematics;
+using UnityEngine.Profiling;
+using Unity.Transforms;
+using Unity.Burst;
+using Unity.Jobs;
+using Unity.Collections;
+using UnityEngine.Jobs;
+
+namespace Pathfinding.ECS {
+ using Pathfinding;
+ using Pathfinding.Util;
+
+ [UpdateBefore(typeof(TransformSystemGroup))]
+ [UpdateBefore(typeof(AIMovementSystemGroup))]
+ [UpdateInGroup(typeof(SimulationSystemGroup))]
+ public partial struct SyncTransformsToEntitiesSystem : ISystem {
+ public static readonly quaternion ZAxisForwardToYAxisForward = quaternion.Euler(math.PI / 2, 0, 0);
+ public static readonly quaternion YAxisForwardToZAxisForward = quaternion.Euler(-math.PI / 2, 0, 0);
+
+ public void OnCreate (ref SystemState state) {}
+ public void OnDestroy (ref SystemState state) {}
+
+ public void OnUpdate (ref SystemState systemState) {
+ int numComponents = BatchedEvents.GetComponents<FollowerEntity>(BatchedEvents.Event.None, out var transforms, out var components);
+ if (numComponents > 0) {
+ var entities = new NativeArray<Entity>(numComponents, Allocator.TempJob);
+
+ for (int i = 0; i < numComponents; i++) entities[i] = components[i].entity;
+
+ systemState.Dependency = new SyncTransformsToEntitiesJob {
+ entities = entities,
+ entityPositions = SystemAPI.GetComponentLookup<LocalTransform>(),
+ syncPositionWithTransform = SystemAPI.GetComponentLookup<SyncPositionWithTransform>(true),
+ syncRotationWithTransform = SystemAPI.GetComponentLookup<SyncRotationWithTransform>(true),
+ orientationYAxisForward = SystemAPI.GetComponentLookup<OrientationYAxisForward>(true),
+ movementState = SystemAPI.GetComponentLookup<MovementState>(true),
+ }.Schedule(transforms, systemState.Dependency);
+ }
+ }
+
+ [BurstCompile]
+ struct SyncTransformsToEntitiesJob : IJobParallelForTransform {
+ [ReadOnly]
+ [DeallocateOnJobCompletion]
+ public NativeArray<Entity> entities;
+
+ // Safety: All entities are unique
+ [NativeDisableParallelForRestriction]
+ public ComponentLookup<LocalTransform> entityPositions;
+ [ReadOnly]
+ public ComponentLookup<SyncPositionWithTransform> syncPositionWithTransform;
+ [ReadOnly]
+ public ComponentLookup<SyncRotationWithTransform> syncRotationWithTransform;
+ [ReadOnly]
+ public ComponentLookup<OrientationYAxisForward> orientationYAxisForward;
+ [ReadOnly]
+ public ComponentLookup<MovementState> movementState;
+
+ public void Execute (int index, TransformAccess transform) {
+ var entity = entities[index];
+ if (entityPositions.HasComponent(entity)) {
+#if MODULE_ENTITIES_1_0_8_OR_NEWER
+ ref var tr = ref entityPositions.GetRefRW(entity).ValueRW;
+#else
+ ref var tr = ref entityPositions.GetRefRW(entity, false).ValueRW;
+#endif
+
+ float3 offset = float3.zero;
+ if (movementState.TryGetComponent(entity, out var ms)) {
+ offset = ms.positionOffset;
+ }
+
+ if (syncPositionWithTransform.HasComponent(entity)) tr.Position = (float3)transform.position - offset;
+ if (syncRotationWithTransform.HasComponent(entity)) {
+ if (orientationYAxisForward.HasComponent(entity)) {
+ tr.Rotation = math.mul(transform.rotation, YAxisForwardToZAxisForward);
+ } else {
+ // Z axis forward
+ tr.Rotation = transform.rotation;
+ }
+ }
+ tr.Scale = transform.localScale.y;
+ }
+ }
+ }
+ }
+}
+#endif
diff --git a/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs.meta b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs.meta
new file mode 100644
index 0000000..70b3391
--- /dev/null
+++ b/Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems/SyncTransformsToEntitiesSystem.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: ea4380d40e1bfa745a1ddb6638ed46b8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: