update
This commit is contained in:
271
Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs
Normal file
271
Packages/com.arongranberg.astar/Core/ECS/Systems/AIMoveSystem.cs
Normal file
@@ -0,0 +1,271 @@
|
||||
#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>(),
|
||||
|
||||
// When in 2D mode, gravity is always disabled
|
||||
ComponentType.Exclude<OrientationYAxisForward>(),
|
||||
|
||||
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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f857e04ce9382d74989b3d469a0b956e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84577891deac65d458d801d960c6fcee
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5502c9f6a3e7fc448803d8b0607c6eac
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,135 @@
|
||||
#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 entityQueryControl;
|
||||
EntityQuery entityQueryControlManaged;
|
||||
EntityQuery entityQueryControlManaged2;
|
||||
RedrawScope redrawScope;
|
||||
|
||||
static readonly ProfilerMarker MarkerMovementOverrideBeforeControl = new ProfilerMarker("MovementOverrideBeforeControl");
|
||||
static readonly ProfilerMarker MarkerMovementOverrideAfterControl = new ProfilerMarker("MovementOverrideAfterControl");
|
||||
|
||||
public void OnCreate (ref SystemState state) {
|
||||
redrawScope = DrawingManager.GetRedrawScope();
|
||||
|
||||
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>()
|
||||
);
|
||||
}
|
||||
|
||||
public void OnDestroy (ref SystemState state) {
|
||||
redrawScope.Dispose();
|
||||
}
|
||||
|
||||
public void OnUpdate (ref SystemState systemState) {
|
||||
// The full movement calculations do not necessarily need to be done every frame if the fps is high
|
||||
if (AstarPath.active != null && !AIMovementSystemGroup.TimeScaledRateManager.CheapSimulationOnly) {
|
||||
ProcessControlLoop(ref systemState, SystemAPI.Time.DeltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce754b44fa448624dac9bbefe12d03e9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,247 @@
|
||||
#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.Util;
|
||||
using Pathfinding.Collections;
|
||||
using Unity.Transforms;
|
||||
using UnityEngine.Profiling;
|
||||
|
||||
[UpdateBefore(typeof(RepairPathSystem))]
|
||||
[UpdateInGroup(typeof(AIMovementSystemGroup))]
|
||||
[RequireMatchingQueriesForUpdate]
|
||||
[BurstCompile]
|
||||
public partial struct MovementPlaneFromGraphSystem : ISystem {
|
||||
public EntityQuery entityQueryGraph;
|
||||
public EntityQuery entityQueryNormal;
|
||||
// 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 });
|
||||
|
||||
graphNodeQueue = GCHandle.Alloc(new List<GraphNode>(32));
|
||||
}
|
||||
|
||||
public void OnDestroy (ref SystemState state) {
|
||||
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++) {
|
||||
movementPlanes[i] = new AgentMovementPlane(MovementPlaneFromGraph(graphs[i]));
|
||||
}
|
||||
|
||||
if (!entityQueryNormal.IsEmpty) {
|
||||
Profiler.BeginSample("MovementPlaneSource.NavmeshNormal");
|
||||
systemState.CompleteDependency();
|
||||
var vertices = new NativeList<Int3>(16, Allocator.Temp);
|
||||
new JobMovementPlaneFromNavmeshNormal {
|
||||
dt = AIMovementSystemGroup.TimeScaledRateManager.CheapStepDeltaTime,
|
||||
vertices = vertices,
|
||||
que = (List<GraphNode>)graphNodeQueue.Target,
|
||||
}.Run(entityQueryNormal);
|
||||
Profiler.EndSample();
|
||||
}
|
||||
|
||||
systemState.Dependency = new JobMovementPlaneFromGraph {
|
||||
movementPlanes = movementPlanes,
|
||||
}.Schedule(entityQueryGraph, systemState.Dependency);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Natural movement plane for a graph traversing a given graph.
|
||||
///
|
||||
/// This is the movement plane used for <see cref="MovementPlaneSource"/>.Graph.
|
||||
///
|
||||
/// See: <see cref="FollowerEntity.movementPlaneSource"/>
|
||||
/// </summary>
|
||||
public static NativeMovementPlane MovementPlaneFromGraph (NavGraph graph) {
|
||||
if (graph is NavmeshBase navmesh) {
|
||||
return new NativeMovementPlane(navmesh.transform.rotation);
|
||||
} else if (graph is GridGraph grid) {
|
||||
return new NativeMovementPlane(grid.transform.rotation);
|
||||
} else {
|
||||
return new NativeMovementPlane(quaternion.identity);
|
||||
}
|
||||
}
|
||||
|
||||
partial struct JobMovementPlaneFromNavmeshNormal : IJobEntity {
|
||||
public float dt;
|
||||
public NativeList<Int3> vertices;
|
||||
public List<GraphNode> que;
|
||||
|
||||
public void Execute (ManagedState managedState, in LocalTransform localTransform, ref AgentMovementPlane agentMovementPlane, in AgentCylinderShape shape) {
|
||||
var node = managedState.pathTracer.startNode as TriangleMeshNode;
|
||||
if (node != null) {
|
||||
// TODO: Expose this parameter?
|
||||
const float InverseSmoothness = 20f;
|
||||
var radius = math.max(0.01f, shape.radius);
|
||||
SampleSmoothNavmeshNormal(node, que, vertices, localTransform.Position, radius, 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SampleSmoothNavmeshNormal (TriangleMeshNode node, List<GraphNode> scratchList, NativeList<Int3> scratchBuffer, float3 position, float agentRadius, ref AgentMovementPlane agentMovementPlane, float alpha) {
|
||||
var vertices = scratchBuffer;
|
||||
var que = scratchList;
|
||||
vertices.Clear();
|
||||
que.Clear();
|
||||
int queStart = 0;
|
||||
node.TemporaryFlag1 = true;
|
||||
que.Add(node);
|
||||
var i0 = node.v0;
|
||||
var i1 = node.v1;
|
||||
var i2 = node.v2;
|
||||
|
||||
while (queStart < que.Count) {
|
||||
var current = que[queStart++] as TriangleMeshNode;
|
||||
if (current == null) continue;
|
||||
var anyVertex = current.v0 == i0 | current.v1 == i0 | current.v2 == i0 | current.v0 == i1 | current.v1 == i1 | current.v2 == i1 | current.v0 == i2 | current.v1 == i2 | current.v2 == i2;
|
||||
if (anyVertex) {
|
||||
current.GetVertices(out var v0, out var v1, out var v2);
|
||||
vertices.Add(v0);
|
||||
vertices.Add(v1);
|
||||
vertices.Add(v2);
|
||||
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();
|
||||
SampleSmoothTriangleNormal(ref position, ref verticesSpan, ref agentMovementPlane, agentRadius, alpha);
|
||||
}
|
||||
|
||||
static float Square (float x) {
|
||||
return x * x;
|
||||
}
|
||||
|
||||
/// <summary>Sine of the angle ABC</summary>
|
||||
static float SinAngle (float3 a, float3 b, float3 c) {
|
||||
return math.sqrt(1 - Square(math.dot(math.normalizesafe(a - b), math.normalizesafe(c - b))));
|
||||
}
|
||||
|
||||
[BurstCompile(FloatMode = FloatMode.Fast)]
|
||||
static void SampleSmoothTriangleNormal (ref float3 position, ref UnsafeSpan<Int3> _triangleVertices, ref AgentMovementPlane agentMovementPlane, float agentRadius, float alpha) {
|
||||
var triangleVertices = _triangleVertices.Reinterpret<int3>();
|
||||
if (triangleVertices.Length < 3) throw new System.ArgumentException("triangleVertices.Length < 3");
|
||||
unsafe {
|
||||
// First 3 vertices represent the triangle we start on
|
||||
var sourceVertices = triangleVertices.ptr;
|
||||
var normals = stackalloc float3[3];
|
||||
var weights = stackalloc float[3];
|
||||
normals[0] = normals[1] = normals[2] = float3.zero;
|
||||
weights[0] = weights[1] = weights[2] = 0;
|
||||
var currentNormal = agentMovementPlane.value.up;
|
||||
|
||||
for (uint i = 0; i < triangleVertices.length; i += 3) {
|
||||
var p0 = triangleVertices[i + 0];
|
||||
var p1 = triangleVertices[i + 1];
|
||||
var p2 = triangleVertices[i + 2];
|
||||
var f0 = (float3)p0 * Int3.PrecisionFactor;
|
||||
var f1 = (float3)p1 * Int3.PrecisionFactor;
|
||||
var f2 = (float3)p2 * Int3.PrecisionFactor;
|
||||
|
||||
var triangleNormal = math.normalizesafe(math.cross(f1 - f0, f2 - f0));
|
||||
|
||||
const float COS_SMOOTH_ANGLE_LIMIT = 0.86f;
|
||||
float weight = 1;
|
||||
var cosAngle = math.dot(triangleNormal, currentNormal);
|
||||
if (cosAngle < COS_SMOOTH_ANGLE_LIMIT) {
|
||||
// Hard angle. Lower the weight of this triangle to avoid starting to rotate too early.
|
||||
Polygon.ClosestPointOnTriangleByRef(in f0, in f1, in f2, in position, out var closest);
|
||||
var distance = math.lengthsq(closest - position) / Square(1.5f * agentRadius);
|
||||
var distanceWeight = math.max(0.1f, 1 - distance);
|
||||
var angleWeight = (COS_SMOOTH_ANGLE_LIMIT - math.max(0, cosAngle)) / COS_SMOOTH_ANGLE_LIMIT;
|
||||
weight = math.lerp(1, distanceWeight, angleWeight);
|
||||
}
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (math.all(p0 == sourceVertices[j])) {
|
||||
// When calculating smooth normals, we ideally want to weigh the contributions from
|
||||
// differnt triangles by the angle of the triangle at the vertex.
|
||||
// We use the sine of that angle instead, which is a decent approximation.
|
||||
var w = weight * SinAngle(p2, p0, p1);
|
||||
weights[j] += w;
|
||||
normals[j] += w * triangleNormal;
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (math.all(p1 == sourceVertices[j])) {
|
||||
var w = weight * SinAngle(p0, p1, p2);
|
||||
weights[j] += w;
|
||||
normals[j] += w * triangleNormal;
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (math.all(p2 == sourceVertices[j])) {
|
||||
var w = weight * SinAngle(p1, p2, p0);
|
||||
weights[j] += w;
|
||||
normals[j] += w * triangleNormal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < 3; j++) {
|
||||
if (weights[j] > 0) normals[j] /= weights[j];
|
||||
}
|
||||
|
||||
var v0 = (float3)sourceVertices[0] * Int3.PrecisionFactor;
|
||||
var v1 = (float3)sourceVertices[1] * Int3.PrecisionFactor;
|
||||
var v2 = (float3)sourceVertices[2] * Int3.PrecisionFactor;
|
||||
var barycentric = Polygon.ClosestPointOnTriangleBarycentric(v0, v1, v2, position);
|
||||
var targetNormal = math.normalizesafe(normals[0] * barycentric.x + normals[1] * barycentric.y + normals[2] * barycentric.z);
|
||||
|
||||
var nextNormal = math.lerp(currentNormal, targetNormal, math.clamp(alpha, 0, 1));
|
||||
agentMovementPlane.value = agentMovementPlane.value.MatchUpDirection(nextNormal);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e03b8b6eb150263419cf52d753bc4bc5
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12ab30eb86c3d4841b72f49aa252574c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
253
Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs
Normal file
253
Packages/com.arongranberg.astar/Core/ECS/Systems/RVOSystem.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
#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;
|
||||
using Unity.Jobs;
|
||||
|
||||
[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
|
||||
simulator.Update(
|
||||
systemState.Dependency,
|
||||
SystemAPI.Time.DeltaTime,
|
||||
AIMovementSystemGroup.TimeScaledRateManager.IsLastSubstep,
|
||||
systemState.WorldUpdateAllocator
|
||||
);
|
||||
|
||||
CopyFromRVOSimulatorToEntities(ref systemState, simulator);
|
||||
}
|
||||
|
||||
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);
|
||||
var writeLock = simulator.LockSimulationDataReadWrite();
|
||||
systemState.Dependency = new JobCopyFromEntitiesToRVOSimulator {
|
||||
agentData = simulator.simulationData,
|
||||
agentOutputData = simulator.outputData,
|
||||
movementPlaneMode = simulator.movementPlane,
|
||||
agentOffMeshLinkTraversalLookup = agentOffMeshLinkTraversalLookup,
|
||||
dt = dt,
|
||||
}.ScheduleParallel(JobHandle.CombineDependencies(writeLock.dependency, systemState.Dependency));
|
||||
writeLock.UnlockAfter(systemState.Dependency);
|
||||
}
|
||||
|
||||
void CopyFromRVOSimulatorToEntities (ref SystemState systemState, SimulatorBurst simulator) {
|
||||
var writeLock = simulator.LockSimulationDataReadWrite();
|
||||
systemState.Dependency = new JobCopyFromRVOSimulatorToEntities {
|
||||
quadtree = simulator.quadtree,
|
||||
agentDataVersions = simulator.simulationData.version,
|
||||
agentOutputData = simulator.outputData,
|
||||
}.ScheduleParallel(JobHandle.CombineDependencies(writeLock.dependency, systemState.Dependency));
|
||||
writeLock.UnlockAfter(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);
|
||||
if (!agentIndex.TryGetIndex(ref agentData, out var index)) 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;
|
||||
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 NativeArray<AgentIndex> agentDataVersions;
|
||||
[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) {
|
||||
if (!agentIndex.TryGetIndex(ref agentDataVersions, out var index)) 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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ab994574a30005439b0db78c01279f7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,215 @@
|
||||
#pragma warning disable CS0282
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using UnityEngine.Profiling;
|
||||
using Unity.Transforms;
|
||||
using Unity.Burst;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding;
|
||||
using Pathfinding.ECS.RVO;
|
||||
using Pathfinding.RVO;
|
||||
using Unity.Collections;
|
||||
|
||||
[UpdateInGroup(typeof(AIMovementSystemGroup))]
|
||||
[UpdateBefore(typeof(FollowerControlSystem))]
|
||||
[BurstCompile]
|
||||
public partial struct RepairPathSystem : ISystem {
|
||||
EntityQuery entityQueryPrepare;
|
||||
EntityQuery entityQueryOffMeshLink;
|
||||
EntityQuery entityQueryOffMeshLinkCleanup;
|
||||
public JobRepairPath.Scheduler jobRepairPathScheduler;
|
||||
|
||||
public void OnCreate (ref SystemState state) {
|
||||
jobRepairPathScheduler = new JobRepairPath.Scheduler(ref state);
|
||||
|
||||
entityQueryPrepare = jobRepairPathScheduler.GetEntityQuery(Unity.Collections.Allocator.Temp).WithAll<SimulateMovement, SimulateMovementRepair>().Build(ref state);
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b48a3bd9c8f676b4bbb586cb85b4acaf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma warning disable CS0282
|
||||
#if MODULE_ENTITIES
|
||||
using Unity.Entities;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Pathfinding.ECS {
|
||||
using Pathfinding;
|
||||
|
||||
[UpdateBefore(typeof(RepairPathSystem))]
|
||||
[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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6ec5674da0fa5043bc982e1a4afed11
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea4380d40e1bfa745a1ddb6638ed46b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user