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 )
}