#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; /// /// Keeps track of the last simulator that this RVOSystem saw. /// This is a weak GCHandle to allow it to be stored in an ISystem. /// GCHandle lastSimulator; EntityQuery withAgentIndex; EntityQuery shouldBeAddedToSimulation; EntityQuery shouldBeRemovedFromSimulation; ComponentLookup agentOffMeshLinkTraversalLookup; public void OnCreate (ref SystemState state) { entityQuery = state.GetEntityQuery( ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadOnly(), ComponentType.ReadWrite(), ComponentType.ReadOnly() ); withAgentIndex = state.GetEntityQuery( ComponentType.ReadWrite() ); shouldBeAddedToSimulation = state.GetEntityQuery( ComponentType.ReadOnly(), ComponentType.Exclude() ); shouldBeRemovedFromSimulation = state.GetEntityQuery( ComponentType.ReadOnly(), ComponentType.Exclude() ); lastSimulator = GCHandle.Alloc(null, System.Runtime.InteropServices.GCHandleType.Weak); agentOffMeshLinkTraversalLookup = state.GetComponentLookup(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(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(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(shouldBeRemovedFromSimulation, EntityQueryCaptureMode.AtPlayback); #else buffer.RemoveComponent(shouldBeRemovedFromSimulation); #endif for (int i = 0; i < indicesToRemove.Length; i++) { simulator.RemoveAgent(indicesToRemove[i]); } for (int i = 0; i < entitiesToAdd.Length; i++) { buffer.AddComponent(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 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; /// See https://en.wikipedia.org/wiki/Circle_packing 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