AI behavior: Patrol
This is a super simple patrolling behavior, where ai will walk to random points within their starting area. It was created as a base to build a more complex system on top of. It is written in the exp language created by John Haggerty.
Exp Cheatsheet:
- aslongas - keeps the current scope alive "as long as" the expression remains true
- whenever - Runs the following statement repeatedly, once each time the expression is true, ending the statement when it becomes false, and waiting for it to become true again when the statement ends
- meantime - Whenever may be paired with "meantime" to provide a statement that will be run when the expression is false
- thread - Exp "threads" are asynchronous function calls. They are akin to coroutines in some other languages: they take turns executing and so are not truly asynchronous.
- localthread - A localthread is a thread that is guaranteed to end when the scope it is defined in is exited. You can customize the scope it is attached to with the @ symbol.
- cleanup - After a cleanup block is passed during execution, the statement (or block of statements) after cleanup is guaranteed to run when the scope is exited.
ctconst int MAX_ATTEMPTS_PER_FRAME = 6 ctconst float PATROL_RANGE = 1000.0 ctconst float MIN_DIST = 500.0 shared maywait fn ABaseAI::PatrolBehavior( watchobj< bool > shouldRun ) { aslongas @fn shouldRun localthread ScopedDebugString( "PatrolBehavior" ) aslongas !NavmeshInPlay { IdleLoop() } //Initial delay so everyone isn't synced waitfor WatchTimer( RandomFloat(0, 3) ) whenever ShouldIdlePatrol { vector refLocation = GetActorLocation() bool useNpcFacing = true int findPointAttempts = 0 loop { // find a point auto result = GetPointToPatrolTo( refLocation, useNpcFacing ) if ( !result.ReturnValue ) { findPointAttempts++ if ( findPointAttempts > 2 ) useNpcFacing = false WaitFrame() continue } useNpcFacing = false findPointAttempts = 0 vector patrolPoint = result.RandomLocation // move! bool moveResult = MoveTo( patrolPoint ) if ( !moveResult ) { WaitFrame() continue } // idle at destination aslongas WatchTimer( RandomFloat(3, 5) ) { IdleLoop() } } } meantime { loop IdleLoop() } } UNavigationSystemV1_GetRandomReachablePointInRadius_Output fn ABaseAI::GetPointToPatrolTo( vector refLocation, bool useNpcFacing ) { vector feetLoc = GetCharacterFeetPosition() for ( int i = 0; i < MAX_ATTEMPTS_PER_FRAME; i++ ) { auto result = TryFindReachablePoint( refLocation, PATROL_RANGE ) if ( !result.ReturnValue ) continue // choose a point within our initial direction if ( useNpcFacing ) { auto myEyesData = GetActorEyesViewPoint() vector eyeLoc = myEyesData.OutLocation vector eyeForward = myEyesData.OutRotation.GetForwardVector() vector eyeToNavPoint = FlattenVector(result.RandomLocation - eyeLoc).normalized() float dot = Dot( eyeForward, eyeToNavPoint ) if ( dot > 0.6 ) { return result } } else { // make sure the point is at least this far away from us if ( DistanceSqr( feetLoc, result.RandomLocation.ToVector() ) > sqr(MIN_DIST) ) return result } } UNavigationSystemV1_GetRandomReachablePointInRadius_Output badResult return badResult }