using UnityEngine; using Unity.Collections; using Unity.Mathematics; using Unity.Burst; namespace Pathfinding.RVO { /// /// Controls if the agent slows down to a stop if the area around the destination is crowded. /// The main idea for this script is to /// - Reduce the local avoidance priority for agents that have reached their destination once. /// - Make agents stop if there is a high density of units around its destination. /// /// 'High density' is defined as: /// Take the circle with the center at the AI's destination and a radius such that the AI's current position /// is touching its border. Let 'A' be the area of that circle. Further let 'a' be the total area of all /// individual agents inside that circle. /// The agent should stop if a > A*0.6 or something like that. I.e if the agents inside the circle cover /// over 60% of the surface of the circle. The 60% figure can be modified (see . /// /// This script was inspired by how Starcraft 2 does its local avoidance. /// /// See: /// [System.Serializable] public struct RVODestinationCrowdedBehavior { /// Enables or disables this module public bool enabled; /// /// The threshold for when to stop. /// See the class description for more info. /// [Range(0, 1)] public float densityThreshold; /// /// If true, the agent will start to move to the destination again if it determines that it is now less crowded. /// If false and the destination becomes less crowded (or if the agent is pushed away from the destination in some way), then the agent will still stay put. /// public bool returnAfterBeingPushedAway; public float progressAverage; bool wasEnabled; float timer1; float shouldStopDelayTimer; bool lastShouldStopResult; Vector3 lastShouldStopDestination; Vector3 reachedDestinationPoint; public bool lastJobDensityResult; /// See https://en.wikipedia.org/wiki/Circle_packing const float MaximumCirclePackingDensity = 0.9069f; [BurstCompile(CompileSynchronously = false, FloatMode = FloatMode.Fast)] public struct JobDensityCheck : Pathfinding.Jobs.IJobParallelForBatched { [ReadOnly] RVOQuadtreeBurst quadtree; [ReadOnly] public NativeArray data; [ReadOnly] public NativeArray agentPosition; [ReadOnly] NativeArray agentTargetPoint; [ReadOnly] NativeArray agentRadius; [ReadOnly] NativeArray agentDesiredSpeed; [ReadOnly] NativeArray agentOutputTargetPoint; [ReadOnly] NativeArray agentOutputSpeed; [WriteOnly] public NativeArray outThresholdResult; public NativeArray progressAverage; public float deltaTime; public bool allowBoundsChecks => false; public struct QueryData { public float3 agentDestination; public int agentIndex; public float densityThreshold; } public JobDensityCheck(int size, float deltaTime) { var simulator = RVOSimulator.active.GetSimulator() as SimulatorBurst; agentPosition = simulator.simulationData.position; agentTargetPoint = simulator.simulationData.targetPoint; agentRadius = simulator.simulationData.radius; agentDesiredSpeed = simulator.simulationData.desiredSpeed; agentOutputTargetPoint = simulator.outputData.targetPoint; agentOutputSpeed = simulator.outputData.speed; quadtree = simulator.quadtree; data = new NativeArray(size, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); outThresholdResult = new NativeArray(size, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); progressAverage = new NativeArray(size, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); this.deltaTime = deltaTime; } public void Dispose () { data.Dispose(); outThresholdResult.Dispose(); progressAverage.Dispose(); } public void Set (int index, int rvoAgentIndex, float3 destination, float densityThreshold, float progressAverage) { data[index] = new QueryData { agentDestination = destination, densityThreshold = densityThreshold, agentIndex = rvoAgentIndex, }; this.progressAverage[index] = progressAverage; } void Pathfinding.Jobs.IJobParallelForBatched.Execute (int start, int count) { for (int i = start; i < start + count; i++) { Execute(i); } } float AgentDensityInCircle (float3 position, float radius) { return quadtree.QueryArea(position, radius) / (radius * radius * math.PI); } void Execute (int i) { var query = data[i]; var position = agentPosition[query.agentIndex]; var radius = agentRadius[query.agentIndex]; var desiredDirection = math.normalizesafe(agentTargetPoint[query.agentIndex] - position); float delta; if (agentDesiredSpeed[query.agentIndex] > 0.01f) { // How quickly the agent can move var speedTowardsTarget = math.dot(desiredDirection, math.normalizesafe(agentOutputTargetPoint[query.agentIndex] - position) * agentOutputSpeed[query.agentIndex]); // Make it relative to how quickly it wants to move // So 0.0 means it is stuck // 1.0 means it is moving as quickly as it wants // Cap the desired speed by the agent's radius. This avoids making agents that want to move very quickly // but are slowed down to a still reasonable speed get a very low progressAverage. delta = speedTowardsTarget / math.max(0.001f, math.min(agentDesiredSpeed[query.agentIndex], agentRadius[query.agentIndex])); // Clamp between -1 and 1 delta = math.clamp(delta, -1.0f, 1.0f); } else { // If the agent doesn't want to move anywhere then it has 100% progress delta = 1.0f; } // Exponentially decaying average of the deltas (in seconds^-1) const float FilterConvergenceSpeed = 2.0f; progressAverage[i] = math.lerp(progressAverage[i], delta, FilterConvergenceSpeed * deltaTime); // If no destination has been set, then always stop if (math.any(math.isinf(query.agentDestination))) { outThresholdResult[i] = true; return; } var checkRadius = math.length(query.agentDestination - position); var checkRadius2 = radius*5; if (checkRadius > checkRadius2) { // TODO: Change center to be slightly biased towards agent position // If the agent is far away from the destination then do a faster check around the destination first. if (AgentDensityInCircle(query.agentDestination, checkRadius2) < MaximumCirclePackingDensity*query.densityThreshold) { outThresholdResult[i] = false; return; } } outThresholdResult[i] = AgentDensityInCircle(query.agentDestination, checkRadius) > MaximumCirclePackingDensity*query.densityThreshold; } } public void ReadJobResult (ref JobDensityCheck jobResult, int index) { bool shouldStop = jobResult.outThresholdResult[index]; progressAverage = jobResult.progressAverage[index]; lastJobDensityResult = shouldStop; shouldStopDelayTimer = Mathf.Lerp(shouldStopDelayTimer, shouldStop ? 1 : 0, Time.deltaTime); shouldStop = shouldStop && shouldStopDelayTimer > 0.1f; lastShouldStopResult = shouldStop; lastShouldStopDestination = jobResult.data[index].agentDestination; } public RVODestinationCrowdedBehavior (bool enabled, float densityFraction, bool returnAfterBeingPushedAway) { this.enabled = wasEnabled = enabled; this.densityThreshold = densityFraction; this.returnAfterBeingPushedAway = returnAfterBeingPushedAway; this.lastJobDensityResult = false; this.progressAverage = 0; this.wasStopped = false; this.lastShouldStopDestination = new Vector3(float.NaN, float.NaN, float.NaN); this.reachedDestinationPoint = new Vector3(float.NaN, float.NaN, float.NaN); timer1 = 0; shouldStopDelayTimer = 0; reachedDestination = false; lastShouldStopResult = false; } /// /// Marks the destination as no longer being reached. /// /// If the agent had stopped because the destination was crowded, this will make it immediately try again /// to move forwards if it can. If the destination is still crowded it will soon stop again. /// /// This is useful to call when a user gave an agent an explicit order to ensure it doesn't /// just stay in the same location without even trying to move forwards. /// public void ClearDestinationReached () { wasStopped = false; progressAverage = 1.0f; reachedDestination = false; } public void OnDestinationChanged (Vector3 newDestination, bool reachedDestination) { timer1 = float.PositiveInfinity; // TODO: Check previous ShouldStop result. Check how much the circles overlap. // With significant overlap we may want to keep reachedCurrentDestination = true this.reachedDestination = reachedDestination; // (ideal: || ShouldStop(ai, rvo)) } /// /// True if the agent has reached its destination. /// If the agents destination changes this may return false until the next frame. /// Note that changing the destination every frame may cause this value to never return true. /// /// True will be returned if the agent has stopped due to being close enough to the destination. /// This may be quite some distance away if there are many other agents around the destination. /// /// See: /// public bool reachedDestination { get; private set; } bool wasStopped; const float DefaultPriority = 1.0f; const float StoppedPriority = 0.1f; const float MoveBackPriority = 0.5f; public void Update (bool rvoControllerEnabled, bool reachedDestination, ref bool isStopped, ref float rvoPriorityMultiplier, ref float rvoFlowFollowingStrength, Vector3 agentPosition) { if (!(enabled && rvoControllerEnabled)) { if (wasEnabled) { wasEnabled = false; // Reset to default values rvoPriorityMultiplier = DefaultPriority; rvoFlowFollowingStrength = 0; timer1 = float.PositiveInfinity; progressAverage = 1.0f; } return; } wasEnabled = true; if (reachedDestination) { var validRange = (agentPosition - this.reachedDestinationPoint).sqrMagnitude; if ((lastShouldStopDestination - this.reachedDestinationPoint).sqrMagnitude > validRange) { // The reachedDestination bool is no longer valid. // The destination has moved significantly from the last point where we detected that it was crowded. // It may end up being set to true immediately afterwards though if // the parameter reachedDestination (not this.reachedDestination) is true. this.reachedDestination = false; } } if (reachedDestination || lastShouldStopResult) { // We have reached the destination the destination is crowded enough that we should stop here anyway timer1 = 0f; this.reachedDestination = true; this.reachedDestinationPoint = this.lastShouldStopDestination; rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, StoppedPriority, Time.deltaTime * 2); rvoFlowFollowingStrength = Mathf.Lerp(rvoFlowFollowingStrength, 1.0f, Time.deltaTime * 4); wasStopped |= math.abs(progressAverage) < 0.1f; isStopped |= wasStopped; // false && rvoPriorityMultiplier > 0.9f; } else if (isStopped) { // We have not reached the destination, but a separate script is telling is to stop timer1 = 0f; this.reachedDestination = false; rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, StoppedPriority, Time.deltaTime * 2); rvoFlowFollowingStrength = Mathf.Lerp(rvoFlowFollowingStrength, 1.0f, Time.deltaTime * 4); wasStopped |= math.abs(progressAverage) < 0.1f; } else { // Check if we had reached the current destination previously (but it is not reached any longer) // TODO: Rename variable, confusing if (this.reachedDestination) { timer1 += Time.deltaTime; if (timer1 > 3 && returnAfterBeingPushedAway) { // Make the agent try to move back to the destination // Use a slightly higher priority than agents that are just standing still, but lower than regular agents rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, MoveBackPriority, Time.deltaTime * 2); rvoFlowFollowingStrength = 0; isStopped = false; wasStopped = false; } else { rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, StoppedPriority, Time.deltaTime * 2); rvoFlowFollowingStrength = Mathf.Lerp(rvoFlowFollowingStrength, 1.0f, Time.deltaTime * 4); wasStopped |= math.abs(progressAverage) < 0.1f; isStopped = wasStopped; //isStopped = false && rvoPriorityMultiplier > 0.9f; } } else { // This is the common case: the agent is just on its way to the destination rvoPriorityMultiplier = Mathf.Lerp(rvoPriorityMultiplier, DefaultPriority, Time.deltaTime * 4); rvoFlowFollowingStrength = 0f; isStopped = false; wasStopped = false; } } } } }