1
0
KF2-Dev-Scripts/GameFramework/Classes/GameCrowdDestination.uc
2020-12-13 18:01:13 +03:00

762 lines
22 KiB
Ucode

/**
*
* Copyright 1998-2013 Epic Games, Inc. All Rights Reserved.
*
* Where crowd agent is going. Destinations can kill agents that reach them or route them to another destination
*
*/
class GameCrowdDestination extends GameCrowdInteractionPoint
implements(GameCrowdSpawnInterface)
implements(EditorLinkSelectionInterface)
dependsOn(GameCrowdAgent)
native;
/** If TRUE, kill crowd members when they reach this destination. */
var() bool bKillWhenReached;
// randomly pick from this list of active destinations
var() duplicatetransient array<GameCrowdDestination> NextDestinations;
/** queue point to use if this destination is at capacity */
var() duplicatetransient GameCrowdDestinationQueuePoint QueueHead;
// whether agents previous destination can be used as a destination if in list of NextDestinations
var() bool bAllowAsPreviousDestination;
/** How many agents can simultaneously have this as a destination */
var() int Capacity;
/** Adjusts the likelihood of agents to select this destination from list at previous destination*/
var() float Frequency;
/** Current number of agents using this destination */
var private int CustomerCount;
/** if set, only agents of this class can use this destination */
var(Restrictions) array<class<GameCrowdAgent> > SupportedAgentClasses;
/** if set, agents from this archetype can use this destination */
var(Restrictions) array<object> SupportedArchetypes;
/** if set, agents of this class cannot use this destination */
var(Restrictions) array<class<GameCrowdAgent> > RestrictedAgentClasses;
/** if set, agents from this archetype cannot use this destination */
var(Restrictions) array<object> RestrictedArchetypes;
/** Don't go to this destination if panicked */
var() bool bAvoidWhenPanicked;
/** Don't perform kismet or custom behavior at this destination if panicked */
var() bool bSkipBehaviorIfPanicked;
/** Always run toward this destination */
var() bool bFleeDestination;
/** Must reach this destination exactly - will force movement when close */
var() bool bMustReachExactly;
var float ExactReachTolerance;
/** True if has supported class/archetype restrictions */
var bool bHasRestrictions;
/** Type of interaction */
var() Name InteractionTag;
/** Time before an agent is allowed to attempt this sort of interaction again */
var() float InteractionDelay;
/** True if spawning permitted at this node */
var(Spawning) bool bAllowsSpawning;
var(Spawning) bool bAllowCloudSpawning;
var(Spawning) bool bAllowVisibleSpawning;
/** Spawn in a line rather than in a circle. */
var(Spawning) bool bLineSpawner;
/** Whether to spawn agents only at the edge of the circle, or at any point within the circle. */
var(Spawning) bool bSpawnAtEdge;
/** Agents reaching this destination will pick a behavior from this list */
var() array<BehaviorEntry> ReachedBehaviors;
/** Whether agent should stop on reach edge of destination radius (if not reach exactly), or have a "soft" perimeter */
var() bool bSoftPerimeter;
/** Agent currently coming to this destination. Not guaranteed to be correct/exhaustive. Used to allow agents to trade places with nearer agent for destinations with queueing */
var GameCrowdAgent AgentEnRoute;
//=========================================================
/** The following properties are set and used by the GameCrowdPopulationManager class for selecting at which destinations to spawn agents */
/** True if currently in line of sight of a player (may not be within view frustrum) */
var bool bIsVisible;
/** True if will become visible shortly based on player's current velocity */
var bool bWillBeVisible;
/** This destination is currently available for spawning */
var bool bCanSpawnHereNow;
/** This destination is beyond the maximum spawn distance */
var bool bIsBeyondSpawnDistance;
/** Cache that node is currently adjacent to a visible node */
var bool bAdjacentToVisibleNode;
/** Whether there is a valid NavigationMesh around this destination */
var bool bHasNavigationMesh;
/** Priority for spawning agents at this destination */
var float Priority;
/** Most recent time at which agent was spawned at this destination */
var float LastSpawnTime;
/** Population manager with which this destination is associated */
var transient GameCrowdPopulationManager MyPopMgr;
cpptext
{
/** EditorLinkSelectionInterface */
virtual void LinkSelection(USelection* SelectedActors);
virtual void UnLinkSelection(USelection* SelectedActors);
/**
* Function that gets called from within Map_Check to allow this actor to check itself
* for any potential errors and register them with map check dialog.
*/
#if WITH_EDITOR
virtual void CheckForErrors();
#endif
};
/**
* @PARAM Agent is the agent being checked
@PARAM Testposition is the position to be tested
@PARAM bTestExactly if true and GameCrowdDestination.bMustReachExactly is true means ReachedByAgent() only returns true if right on the destination
* @RETURNS TRUE if Agent has reached this destination
*/
native simulated function bool ReachedByAgent(GameCrowdAgent Agent, vector TestPosition, bool bTestExactly);
simulated function PostBeginPlay()
{
local int i;
local GameCrowdPopulationManager PopMgr;
super.PostBeginPlay();
bHasRestrictions = (SupportedAgentClasses.Length > 0) || (SupportedArchetypes.Length > 0) || (RestrictedAgentClasses.Length > 0) || (RestrictedArchetypes.Length > 0);
// don't allow automatic agent spawning at destinations with Queues, or small capacities
if( QueueHead != None || bKillWhenReached )
{
bAllowsSpawning = false;
}
// verify behavior lists
for ( i=0; i< ReachedBehaviors.Length; i++ )
{
if ( ReachedBehaviors[i].BehaviorArchetype == None )
{
`warn(self$" missing BehaviorArchetype at ReachedBehavior "$i);
ReachedBehaviors.remove(i,1);
i--;
}
}
// Add self to population manager list
PopMgr = GameCrowdPopulationManager(WorldInfo.PopulationManager);
if ( PopMgr != None )
{
PopMgr.AddSpawnPoint(self);
}
}
simulated function Destroyed()
{
super.Destroyed();
if ( MyPopMgr != None )
{
MyPopMgr.RemoveSpawnPoint(self);
}
}
/**
* Called after Agent reaches this destination
* Will be called every tick as long as ReachedByAgent() is true and agent is not idle, so should change
* Agent to avoid this (change current destination, make agent idle, or kill agent) )
*
* @PARAM Agent is the crowd agent that just reached this destination
* @PARAM bIgnoreKismet skips generating Kismet event if true.
*/
simulated event ReachedDestination(GameCrowdAgent Agent)
{
local int i,j;
local SeqEvent_CrowdAgentReachedDestination ReachedEvent;
local bool bEventActivated;
// check if kismet event on reaching this destination
for( i = 0; i < GeneratedEvents.Length; i++ )
{
ReachedEvent = SeqEvent_CrowdAgentReachedDestination(GeneratedEvents[i]);
for( j = 0; j < ReachedEvent.OutputLinks[0].Links.Length; j++ )
{
// HACKY - clear bActive on output ops so this agent can get in on an already active latent action
ReachedEvent.OutputLinks[0].Links[j].LinkedOp.bActive = FALSE;
}
bEventActivated = ReachedEvent.CheckActivate( self, Agent );
break;
}
// kill agent that reached me?
if( bKillWhenReached )
{
// If desired, kill actor when it reaches an attractor
DecrementCustomerCount(Agent);
Agent.CurrentDestination = None;
Agent.KillAgent();
return;
}
// mark the interaction if tagged
if( InteractionTag != '' )
{
i = Agent.RecentInteractions.Add(1);
Agent.RecentInteractions[i].InteractionTag = InteractionTag;
if( InteractionDelay > 0.f )
{
// mark the time to remove this interaction from history
Agent.RecentInteractions[i].InteractionDelay = WorldInfo.TimeSeconds + InteractionDelay;
}
}
// Can agent perform a custom behavior here
if( Agent.BehaviorDestination != self && (Agent.CurrentBehavior == None || Agent.CurrentBehavior.AllowBehaviorAt(self)) )
{
// Assign a reachedbehavior to the agent
if( ReachedBehaviors.Length > 0 )
{
Agent.PickBehaviorFrom(ReachedBehaviors);
}
if( ReachedEvent != None )
{
Agent.BehaviorDestination = self;
}
}
// choose next destination
if( !bEventActivated && NextDestinations.Length > 0 )
{
PickNewDestinationFor( Agent, FALSE );
if( Agent.CurrentDestination == None )
{
// if haven't been visible for a while, just kill
if( Agent.NotVisibleLifeSpan > 0.f && `TimeSince(Agent.LastRenderTime) > Agent.NotVisibleLifeSpan )
{
Agent.KillAgent();
}
else
{
// failed with restrictions, so pick any - FIXMESTEVE probably want more refined fallback
PickNewDestinationFor( Agent, TRUE );
}
}
}
// first in group to get new destination should update others
if( Agent.MyGroup != None )
{
Agent.MyGroup.UpdateDestinations(Agent.CurrentDestination);
}
}
/**
* Pick a new destination from this one for agent.
*/
simulated function PickNewDestinationFor(GameCrowdAgent Agent, bool bIgnoreRestrictions )
{
local int i;
local float DestinationFrequencySum, DestinationPickValue;
local array<GameCrowdDestination> DestOptions;
// Pick a new destination from available list
DecrementCustomerCount(Agent);
Agent.CurrentDestination = None;
Agent.BehaviorDestination = None;
// init DestinationFrequencySum
for( i=0; i< NextDestinations.Length; i++ )
{
if ( (NextDestinations[i] != None) && NextDestinations[i].bHasNavigationMesh && (bIgnoreRestrictions || NextDestinations[i].AllowableDestinationFor(Agent)) )
{
// bonus to this potential destination's frequency if current destination is not visible, and destination is, and agent prefers visible destinations
DestinationFrequencySum += NextDestinations[i].Frequency * ((!bIsVisible && Agent.bPreferVisibleDestination && (NextDestinations[i].bIsVisible || NextDestinations[i].bWillBeVisible)) ? 2.0 : 1.0);
DestOptions.AddItem( NextDestinations[i] );
}
}
DestinationPickValue = DestinationFrequencySum * FRand();
DestinationFrequencySum = 0.0;
for( i = 0; i < DestOptions.Length; i++ )
{
if( DestOptions[i] != None && DestOptions[i].bHasNavigationMesh )
{
// bonus to this potential destination's frequency if current destination is not visible, and destination is, and agent prefers visible destinations
DestinationFrequencySum += DestOptions[i].Frequency * ((!bIsVisible && Agent.bPreferVisibleDestination && (DestOptions[i].bIsVisible || DestOptions[i].bWillBeVisible)) ? 2.0 : 1.0);
if( DestinationPickValue < DestinationFrequencySum )
{
Agent.SetCurrentDestination(DestOptions[i]);
Agent.PreviousDestination = self;
Agent.UpdateIntermediatePoint();
break;
}
}
}
Agent.PreviousDestination = self;
}
/**
* Decrement customer count. Update Queue if have one
* Be sure to decrement customer count from old destination before setting a new one!
* FIXMESTEVE - should probably wrap decrement old customercount into GameCrowdAgent.SetDestination()
*/
simulated event DecrementCustomerCount(GameCrowdAgent DepartingAgent)
{
local GameCrowdDestinationQueuePoint QP;
local bool bIsInQueue;
// Check to make sure that the current destination is ourself, to prevent double decrementing
if( DepartingAgent.CurrentDestination == self )
{
// check if departing agent is in queue
for( QP = QueueHead; QP != None; QP = QP.NextQueuePosition )
{
if ( QP.QueuedAgent == DepartingAgent )
{
bIsInQueue = true;
QP.ClearQueue(DepartingAgent);
break;
}
}
if( !bIsInQueue )
{
// agent was customer, so clear him out
CustomerCount--;
if( QueueHead != None && QueueHead.HasCustomer() )
{
QueueHead.AdvanceCustomerTo(self);
}
}
}
}
/**
* Increment customer count, or add agent to queue if needed
*/
simulated event IncrementCustomerCount(GameCrowdAgent ArrivingAgent)
{
// if at capacity, or queue is about to move forward, add to queue rather than directly
if( AtCapacity() || (Queuehead != None && Queuehead.bPendingAdvance) )
{
// add to queue
if( QueueHead != None && QueueHead.HasSpace() )
{
// maybe switch with agent currently in route, if ArrivingAgent is closer
if ( (AgentEnRoute != None) && (AgentEnRoute.CurrentBehavior == None) && !ReachedByAgent(AgentEnRoute, AgentEnRoute.Location, false)
&& (VSizeSq(ArrivingAgent.Location - Location) < VSizeSq(AgentEnRoute.Location - Location)) )
{
// switch places
//`log("Switching "$ArrivingAgent$" for "$AgentEnRoute);
QueueHead.AddCustomer(AgentEnRoute,self);
AgentEnRoute = ArrivingAgent;
}
else
{
QueueHead.AddCustomer(ArrivingAgent,self);
}
}
else
{
if( QueueHead != None )
{
`warn(self$" added customer "$ArrivingAgent$" beyond capacity with queue "$QueueHead);
}
}
}
else
{
AgentEnRoute = ArrivingAgent;
CustomerCount++;
}
}
simulated function bool AtCapacity( optional byte CheckCnt )
{
return (CustomerCount + CheckCnt) >= Capacity;
}
/**
* Returns true if this destination is valid for Agent
*/
simulated event bool AllowableDestinationFor(GameCrowdAgent Agent)
{
local int i;
local bool bSupported;
if( !bHasNavigationMesh || !bIsEnabled )
{
return FALSE;
}
if( bIsBeyondSpawnDistance )
{
// FIXMESTEVE - maybe allow moving to beyond max spawn distance destination if currently close enough
return FALSE;
}
if( !bAllowAsPreviousDestination && Agent.PreviousDestination == self )
{
return FALSE;
}
// check if allowed by agent's behavior
if( Agent.CurrentBehavior != None && !Agent.CurrentBehavior.AllowThisDestination(self) )
{
return FALSE;
}
// check if destination has room - make sure there is room for the whole group
if( (Agent.MyGroup != None && AtCapacity(Agent.MyGroup.Members.Length-1)) || AtCapacity() || (QueueHead != None && !QueueHead.HasSpace()) )
{
return FALSE;
}
// check if this interaction is tagged
if( InteractionTag != '' )
{
i = Agent.RecentInteractions.Find('InteractionTag',InteractionTag);
if( i != INDEX_NONE && (Agent.RecentInteractions[i].InteractionDelay == 0.f || WorldInfo.TimeSeconds < Agent.RecentInteractions[i].InteractionDelay) )
{
return FALSE;
}
else if( i != INDEX_NONE )
{
// clear out the old interaction
Agent.RecentInteractions.Remove(i,1);
}
}
if( bHasRestrictions )
{
// make sure the agent class/archetype is supported
if( SupportedAgentClasses.Length > 0 || SupportedArchetypes.Length > 0 )
{
bSupported = FALSE;
for( i = 0; i < SupportedAgentClasses.Length; i++ )
{
if( ClassIsChildOf( Agent.Class, SupportedAgentClasses[i] ) )
{
bSupported = TRUE;
break;
}
}
// only check against supported archetypes if failed supported classes list
if( !bSupported )
{
for( i = 0; i < SupportedArchetypes.Length; i++ )
{
if( SupportedArchetypes[i] == Agent.MyArchetype )
{
bSupported = TRUE;
break;
}
}
}
if( !bSupported )
{
return FALSE;
}
}
// if passed the supported test, make sure not in restricted classes list
for( i = 0; i < RestrictedAgentClasses.Length; i++ )
{
if( ClassIsChildOf( Agent.Class, RestrictedAgentClasses[i] ) )
{
return FALSE;
}
}
for( i = 0; i < RestrictedArchetypes.Length; i++ )
{
if( RestrictedArchetypes[i] == Agent.MyArchetype )
{
return FALSE;
}
}
}
return TRUE;
}
simulated function float GetSpawnRadius()
{
return CylinderComponent.CollisionRadius;
}
// FIXMESTEVE - natively show the spawn line in the editor if bLineSpawner
simulated function GetSpawnPosition(SeqAct_GameCrowdSpawner Spawner, out vector SpawnPos, out rotator SpawnRot)
{
local vector SpawnLine;
local float RandScale;
// LINE SPAWN
if(bLineSpawner)
{
// Scale between -1.0 and 1.0
RandScale = -1.0 + (2.0 * FRand());
// Get line along which to spawn.
SpawnLine = vect(0,1,0) >> Rotation;
// Now make the position
SpawnPos = Location + (RandScale * SpawnLine * GetSpawnRadius());
// Always face same way as spawn location
SpawnRot.Yaw = Rotation.Yaw;
}
else
{
// CIRCLE SPAWN
SpawnRot = RotRand(false);
SpawnRot.Pitch = 0;
if(bSpawnAtEdge)
{
SpawnPos = Location + ((vect(1,0,0) * GetSpawnRadius()) >> SpawnRot);
}
else
{
SpawnPos = Location + ((vect(1,0,0) * FRand() * GetSpawnRadius()) >> SpawnRot);
}
}
}
simulated function bool AnalyzeSpawnPoint( const out array<CrowdSpawnerPlayerInfo> PlayerInfo, float MaxSpawnDistSq, bool bForceNavMeshPathing, NavigationHandle NavHandle )
{
local Actor HitActor;
local vector HitLocation, HitNormal;
local int NextIdx, PlayerIdx;
local GameCrowdDestination NextGCD;
local float DistFromView, DistFromPred;
bIsVisible = TRUE;
bAdjacentToVisibleNode = FALSE;
bWillBeVisible = FALSE;
Priority = 0.f;
bCanSpawnHereNow = FALSE;
bHasNavigationMesh = TRUE;
bIsBeyondSpawnDistance = TRUE;
for( PlayerIdx = 0; PlayerIdx < PlayerInfo.Length; PlayerIdx++ )
{
DistFromView = VSizeSq(PlayerInfo[PlayerIdx].ViewLocation - Location);
DistFromPred = VSizeSq(PlayerInfo[PlayerIdx].PredictLocation - Location);
if( FMin( DistFromView, DistFromPred ) < MaxSpawnDistSq )
{
bIsBeyondSpawnDistance = FALSE;
break;
}
}
if( bIsEnabled && bAllowsSpawning )
{
if( bForceNavMeshPathing && NavHandle.LineCheck(Location, Location - vect(0,0,3)* CylinderComponent.CollisionHeight, vect(0,0,0)) )
{
// no nav mesh streamed in, so can't use for spawning
bHasNavigationMesh = FALSE;
}
else
{
if( !bIsBeyondSpawnDistance )
{
bCanSpawnHereNow = TRUE;
bIsVisible = FALSE;
for( PlayerIdx = 0; PlayerIdx < PlayerInfo.Length; PlayerIdx++ )
{
HitActor = Trace(HitLocation, HitNormal, Location, PlayerInfo[PlayerIdx].ViewLocation, FALSE);
if( HitActor == None )
{
bIsVisible = TRUE;
break;
}
}
if( !bIsVisible )
{
for( PlayerIdx = 0; PlayerIdx < PlayerInfo.Length; PlayerIdx++ )
{
HitActor = Trace(HitLocation, HitNormal, Location, PlayerInfo[PlayerIdx].PredictLocation, FALSE);
if( HitActor == None )
{
bWillBeVisible = TRUE;
break;
}
}
}
}
if( bIsVisible )
{
// Allow spawning at destinations beyond the max spawn dist if connected to visible destinations inside the spawn dist
for( NextIdx = 0; NextIdx < NextDestinations.Length; NextIdx++ )
{
NextGCD = NextDestinations[NextIdx];
if( NextGCD != None && NextGCD.bIsVisible && NextGCD.bCanSpawnHereNow && !NextGCD.bIsBeyondSpawnDistance )
{
bAdjacentToVisibleNode = TRUE;
if( bIsBeyondSpawnDistance )
{
bCanSpawnHereNow = TRUE;
}
}
}
}
}
return TRUE;
}
return FALSE;
}
simulated function PrioritizeSpawnPoint( const out array<CrowdSpawnerPlayerInfo> PlayerInfo, float MaxSpawnDist )
{
local float DistToSpawn;
local int PlayerIdx;
// Priority based on inverse of distance
DistToSpawn = 999999.f;
for( PlayerIdx = 0; PlayerIdx < PlayerInfo.Length; PlayerIdx++ )
{
DistToSpawn = FMin( DistToSpawn, VSize(Location - PlayerInfo[PlayerIdx].ViewLocation));
}
Priority = 1.f - ((MaxSpawnDist - DistToSpawn) / MaxSpawnDist);
// prefer destinations that are about to become visible
if( bWillBeVisible )
{
Priority *= 10.f;
}
else if( bAdjacentToVisibleNode )
{
Priority *= 5.f;
}
// prefer destinations that haven't been used recently
Priority *= FMin(`TimeSince(LastSpawnTime), 10.0);
}
function float GetDestinationRadius()
{
return CylinderComponent.CollisionRadius;
}
simulated function DrawDebug( const out array<CrowdSpawnerPlayerInfo> PlayerInfo, optional bool bPresistent )
{
local int PlayerIdx;
local Vector Extent;
Extent.X = CylinderComponent.CollisionRadius;
Extent.Y = CylinderComponent.CollisionRadius;
Extent.Z = CylinderComponent.CollisionHeight * 2.f;
for( PlayerIdx = 0; PlayerIdx < PlayerInfo.Length; PlayerIdx++ )
{
if( bIsBeyondSpawnDistance )
{
DrawDebugLine( Location, PlayerInfo[PlayerIdx].ViewLocation, 255, 0, 0, bPresistent );
if( PlayerIdx == 0 )
{
DrawDebugSphere( Location, 20, 20, 255, 0, 0, bPresistent );
}
}
else if( !bIsEnabled || !bAllowsSpawning )
{
if( PlayerIdx == 0 )
{
DrawDebugLine( Location, PlayerInfo[PlayerIdx].ViewLocation, 128, 0, 0, bPresistent );
}
DrawDebugSphere( Location, 20, 20, 128, 0, 0, bPresistent );
}
else if( bIsVisible )
{
if( PlayerIdx == 0 )
{
DrawDebugLine( Location, PlayerInfo[PlayerIdx].ViewLocation, 255, 0, 0, bPresistent );
}
DrawDebugBox( Location, Extent, 255, 0, 0, bPresistent );
}
else
{
if( PlayerIdx == 0 )
{
DrawDebugLine( Location, PlayerInfo[PlayerIdx].ViewLocation, 0, 255, 0, bPresistent );
}
DrawDebugBox( Location, Extent, 0, 255, 0, bPresistent );
}
}
if( bAdjacentToVisibleNode )
{
DrawDebugStar( Location, 8, 0, 255, 0, bPresistent );
}
if( bWillBeVisible )
{
DrawDebugStar( Location + vect(0,0,8), 8, 0, 0, 255, bPresistent );
}
if( bCanSpawnHereNow )
{
DrawDebugStar( Location + vect(0,0,16), 8, 255, 255, 255, bPresistent );
}
// `log( self@"Pri:"@Priority );
}
defaultproperties
{
Begin Object Name=Sprite
Sprite=Texture2D'EditorResources.Crowd.T_Crowd_Destination'
Scale=0.5
End Object
bAllowAsPreviousDestination=false
bAvoidWhenPanicked=false
bSkipBehaviorIfPanicked=true
Capacity=1000
Frequency=1.0
ExactReachTolerance=3.0
bAllowsSpawning=TRUE
bAllowCloudSpawning=TRUE
bSoftPerimeter=true
bStatic=true
bForceAllowKismetModification=true
bHasNavigationMesh=true
SupportedEvents.Empty
SupportedEvents(0)=class'SeqEvent_CrowdAgentReachedDestination'
Begin Object Class=GameDestinationConnRenderingComponent Name=ConnectionRenderer
End Object
Components.Add(ConnectionRenderer)
}