AI Perception: HEARING
This is a sample of the logic that sends audio perception events to AI, 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.
// Most events that an ai will 'hear' use this. Character movement, muzzle gunshots, deaths, etc. shared fn EmitAiSound_ByType( AActor sourceActor, vector soundLocation, float soundRadius, AiSoundType type ) { assert( type != AiSoundType.Bullet, "Use EmitAiSound_Bullet() instead." ) array<ABaseAI> hearingNpcArray foreach ( npc in aliveAiArray ) { if ( npc.ShouldIgnorePerceivedActor_Hearing(sourceActor) ) continue bool wasSoundFromAlly = !npc.IsHostileTo( sourceActor ) && sourceActor is ABaseAI if ( wasSoundFromAlly ) { assert( sourceActor is ABaseAI ) if ( type == AiSoundType.Gunshot ) { thread npc.OnDetectAllyGunshot( sourceActor, soundLocation, soundRadius ) continue } if ( ![AiSoundType.OtherDeathAny, AiSoundType.OtherDamagedAny].contains(type) ) continue } // distance check vector npcEyeLocation = npc.GetActorEyesViewPoint().OutLocation if ( DistanceSqr(npcEyeLocation, soundLocation) > sqr(soundRadius) ) continue #if DEV if ( npc.debugFlagsServer && aiDebugInfo == 2 ) DrawDebugSphere( soundLocation, soundRadius, 12, FLinearColor_Yellow, 4.0, 6.0 ) #endif // DEV hearingNpcArray.push( npc ) } foreach ( npc in hearingNpcArray ) thread npc.HearSound_ByType( sourceActor, soundLocation, type, hearingNpcArray ) } maywait fn ABaseAI::HearSound_ByType( AActor sourceActor, vector soundLocation, AiSoundType type, array<ABaseAI> otherNpcsHearingSound ) { aslongas @fn IsAlive() // get data for use later... timeval timeOfKnowledge = GetTime() UpdateThreatKnowledgeFromHearing( sourceActor, type, soundLocation, timeOfKnowledge ) // React to the sound if ( this isnt AVillain || AiSoundMap[type].battleChatterEvent == EBarkEvent.None ) return float delay = RandomFloat( AiSoundMap[type].battleChatterDelayMin, AiSoundMap[type].battleChatterDelayMax ) wait delay DoHeardBattleChatter( sourceActor, type, soundLocation, timeOfKnowledge, otherNpcsHearingSound ) } maywait fn ABaseAI::UpdateThreatKnowledgeFromHearing( AActor threat, AiSoundType soundType, vector soundLocation, timeval timeOfKnowledge ) { // wait delay for knowledge float delay = RandomFloat( AiSoundMap[soundType].durationUntilNoticeMin, AiSoundMap[soundType].durationUntilNoticeMax ) wait delay // Update Knowledge if ( IsHostileTo(threat) ) { float valueToAdd = GetPerceptionSensitivity( EThreatPerceptionType.Sound ) ThreatKnowledge? knowledge = AddThreatStimulus( threat, valueToAdd, soundLocation, timeOfKnowledge, true ) if ( knowledge && (soundType == AiSoundType.Gunshot || soundType == AiSoundType.Bullet) ) { knowledge.threatScore += ThreatScore_NearMissIncrease } } else { thread AddThreatLocationWithoutInstigator( soundLocation ) } } maywait fn AVillain::DoHeardBattleChatter( AActor sourceActor, AiSoundType soundType, vector threatLocation, timeval timeOfKnowledge, array<ABaseAI> otherNpcsHearingSound ) { // don't comment on someone that isnt our enemy, unless we're hearing them die... if ( GetCurrentState() == AIState.COMBAT && IsInPlay(enemy) && enemy != sourceActor ) { if ( AiSoundMap[soundType].battleChatterEvent != EBarkEvent.DeathHeard ) return } // we already know about this threat if ( IsInPlay(sourceActor) && senseInfo.threatMap.contains(sourceActor) ) { // .. and they're seen, so ignore this if ( IsThreatSeen(sourceActor) ) return // they're not seen, but they might as well be float distDontCare = RADIUS_AISOUND_MOVEMENT + 500.0 if ( DistanceSqr(GetActorLocation(), sourceActor.GetActorLocation()) <= sqr(distDontCare) ) return } // prevents spamming every frame of heard footsteps bool isCooledDown = true if ( soundType == AiSoundType.Movement ) { if ( IsBarkRequestCooledDown(LastTimeHeardFootsteps, BATTLE_CHATTER_HEARD_FOOTSTEPS_COOLDOWN) ) LastTimeHeardFootsteps = GetTime() else isCooledDown = false } if ( isCooledDown ) { EBarkEvent battleChatterEvent = AiSoundMap[soundType].battleChatterEvent thread RequestBattleChatter( battleChatterEvent, otherActor = sourceActor ) } if ( !IsHostileTo(sourceActor) ) return // Alert other ai about what we just heard UpdateThreatKnowledge_ForFactionFromHeardThreat( sourceActor, threatLocation, timeOfKnowledge, otherNpcsHearingSound ) }