2025-07-08 10:46:31 +00:00
using UnityEngine ;
using Unity.Collections ;
using Unity.Mathematics ;
using Unity.Burst ;
namespace Pathfinding.RVO {
/// <summary>
/// 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 <see cref="densityThreshold)"/>.
///
/// This script was inspired by how Starcraft 2 does its local avoidance.
///
/// See: <see cref="Pathfinding.AIBase.rvoDensityBehavior"/>
/// </summary>
[System.Serializable]
public struct RVODestinationCrowdedBehavior {
/// <summary>Enables or disables this module</summary>
public bool enabled ;
/// <summary>
/// The threshold for when to stop.
/// See the class description for more info.
/// </summary>
[Range(0, 1)]
public float densityThreshold ;
/// <summary>
/// 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.
/// </summary>
public bool returnAfterBeingPushedAway ;
public float progressAverage ;
bool wasEnabled ;
float timer1 ;
float shouldStopDelayTimer ;
bool lastShouldStopResult ;
Vector3 lastShouldStopDestination ;
Vector3 reachedDestinationPoint ;
public bool lastJobDensityResult ;
/// <summary>See https://en.wikipedia.org/wiki/Circle_packing</summary>
const float MaximumCirclePackingDensity = 0.9069f ;
[BurstCompile(CompileSynchronously = false, FloatMode = FloatMode.Fast)]
public struct JobDensityCheck : Pathfinding . Jobs . IJobParallelForBatched {
[ReadOnly]
RVOQuadtreeBurst quadtree ;
[ReadOnly]
public NativeArray < QueryData > data ;
[ReadOnly]
public NativeArray < float3 > agentPosition ;
[ReadOnly]
NativeArray < float3 > agentTargetPoint ;
[ReadOnly]
NativeArray < float > agentRadius ;
[ReadOnly]
NativeArray < float > agentDesiredSpeed ;
[ReadOnly]
NativeArray < float3 > agentOutputTargetPoint ;
[ReadOnly]
NativeArray < float > agentOutputSpeed ;
[WriteOnly]
public NativeArray < bool > outThresholdResult ;
public NativeArray < float > 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 , SimulatorBurst simulator ) {
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 < QueryData > ( size , Allocator . TempJob , NativeArrayOptions . UninitializedMemory ) ;
outThresholdResult = new NativeArray < bool > ( size , Allocator . TempJob , NativeArrayOptions . UninitializedMemory ) ;
progressAverage = new NativeArray < float > ( 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 ;
}
/// <summary>
/// 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.
/// </summary>
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))
}
/// <summary>
/// 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: <see cref="Pathfinding.IAstarAI.destination"/>
/// </summary>
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 ;
}
}
}
}
}