diff options
Diffstat (limited to 'Other/AstarPathfindingDemo/Packages/com.arongranberg.astar/Core/ECS/Systems')
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: |