[C++/UE] UE5中AI開發的一點功能梳理(六) – AI Perception 感知系統 Event部分

Events

  • AI Perception可以為所附的AI Controller blueprint提供一些感知更新時觸發的事件,AI Controller可就著這些事件觸發的時機,編寫AI角色在感知更新時的處理
    • OnPerceptionUpdated
      • 只要有對象更新感知系統就調用,無論有沒有綁定該函數至藍圖內
      • 每次調用都把所有對象作為參數傳入(Actor列表)
    • OnTargetPerceptionForgotten
      • 各感知系統可配置MaxLife指定對象被感知到的持續時間
      • 當這些對象被「遺忘」時,觸發事件並傳入該對象的Actor
    • OnTargetPerceptionUpdated
      • 感知系統接收到更新信號時觸發,傳入發出更新信號的某個對象Actor的強引用及感知細節(Stimuli)
    • OnTargetPerceptionInfoUpdated
      • 感知系統接收到更新信號時觸發,傳入發出更新信號的UpdateInfo,UpdateInfo包含了以下3個東西
        • 對象指針的Hash
        • 對象的弱引用
        • 感知細節(Stimuli)
  • 流程
    • AIPerceptionSystem每幀遍歷全部AIPerception組件並執行ProcessStimuli()
    • 具體的AIPerceptionComponent裡的ProcessStimuli()主要會進行以下跟上述事件相關的工作
      • 檢查OnTargetPerceptionUpdated事件有沒有綁定
      • 檢查OnTargetPerceptionInfoUpdated事件有沒有綁定
      • 遍歷全部感知列表,假設每個元素為s
        • ProcessingStimuli -> 各感知項每幀檢測,在感知有更新時,會實例化一個Stimuli,裡面包含必要信息,並將它加到列表裡,如下面的示例(從AISense_Damage開始)
//AISense_Damage.cpp
float UAISense_Damage::Update()
{
    AIPerception::FListenerMap& ListenersMap = *GetListeners();

    for (const FAIDamageEvent& Event : RegisteredEvents)
    {
        IAIPerceptionListenerInterface* PerceptionListener = Event.GetDamagedActorAsPerceptionListener();
        if (PerceptionListener != nullptr)
        {
            UAIPerceptionComponent* PerceptionComponent = PerceptionListener->GetPerceptionComponent();
            if (PerceptionComponent != nullptr && PerceptionComponent->GetListenerId().IsValid())
            {
                // this has to succeed, will assert a failure
                FPerceptionListener& Listener = ListenersMap[PerceptionComponent->GetListenerId()];

                if (Listener.HasSense(GetSenseID()))
                {
                    Listener.RegisterStimulus(Event.Instigator, FAIStimulus(*this, Event.Amount, Event.Location, Event.HitLocation, FAIStimulus::SensingSucceeded, Event.Tag));
                }
            }
        }
    }

    RegisteredEvents.Reset();

    // return decides when next tick is going to happen
    return SuspendNextUpdate;
}

//AIPerceptionTypes.cpp
void FPerceptionListener::RegisterStimulus(AActor* Source, const FAIStimulus& Stimulus)
{
    bHasStimulusToProcess = true;
    Listener->RegisterStimulus(Source, Stimulus);
}

//AIPerceptionComponent.cpp
void UAIPerceptionComponent::RegisterStimulus(AActor* Source, const FAIStimulus& Stimulus)
{
    FStimulusToProcess& StimulusToProcess = StimuliToProcess.Add_GetRef(FStimulusToProcess(Source, Stimulus));
    StimulusToProcess.Stimulus.SetExpirationAge(MaxActiveAge[int32(Stimulus.Type)]);
}
    • 一些可行性檢測,確保感知成功,並嘗試把感知對象轉成Actor,失敗直接continue
    • 根據上次該感官感知到的對象跟當前感知到的對象以及WantsToNotifyOnlyOnValueChange配置項來確定標記位bActorInfoUpdated(ActorInfo信息是否有變動),下稱「需要更新」
    • 根據具體的感知情況,作出以下分支處理
      • 如果某個感官感知到新對象,且感知更強或更年輕,就使新對象取代老對象
      • 如果沒有新對象且老對象沒有「過期」,並需要更新,標記對象不需要再被重新感知,Age設置為0
      • 老對象已過期,把對象加到「待遺忘Actor」列表中
    • 如需要更新
      • 如有綁定OnTaregetPerceptionUpdated事件,就會調用OnTargetPerceptionUpdated,把對象的強引用和感知的信息細節廣播出去
      • 如有綁定OnTargetPerceptionInfoUpdated事件,就會調用OnTargetPerceptionInfoUpdated,並創建一個UpdateInfo對象,把相關信息廣播出去,UpdateInfo包含以下資訊
        • TargetId:對象指針的Hash
        • 對象的弱引用
        • 感知信息細節
      • 把所有觸發感知更新的Actor都走OnPerceptionUpdated事件廣播出去
      • 如果綁了OnTargetPerceptionForgotten事件,就遍歷「待遺忘Actor」列表,把每個Actor都走這個事件廣播出去,每個Actor廣播一次
//AIPerceptionSystem.cpp
void UAIPerceptionSystem::Tick(float DeltaSeconds)
{
    //...
    if(ListenerIt->Value.HasAnyNewStimuli())
    {
        ListenerIt->Value.ProcessStimuli();
    }
    //...
}

//AIPerceptionTypes.cpp
void FPerceptionListener::ProcessStimuli()
{
    ensure(bHasStimulusToProcess);
    Listener->ProcessStimuli();
    bHasStimulusToProcess = false;
}

//AIPerceptionComponent
void UAIPerceptionComponent::ProcessStimuli()
{
    SCOPE_CYCLE_COUNTER(STAT_AI_PercepComp_ProcessStimuli);
    
    if(StimuliToProcess.Num() == 0)
    {
        UE_VLOG(GetOwner(), LogAIPerception, Warning, TEXT("UAIPerceptionComponent::ProcessStimuli called without any Stimuli to process"));
        return;
    }

    const bool bBroadcastEveryTargetUpdate = OnTargetPerceptionUpdated.IsBound();
    const bool bBroadcastEveryTargetInfoUpdate = OnTargetPerceptionInfoUpdated.IsBound();
    
    TArray<FStimulusToProcess> ProcessingStimuli = MoveTemp(StimuliToProcess);
    TArray<AActor*> UpdatedActors;
    UpdatedActors.Reserve(ProcessingStimuli.Num());
    TArray<AActor*> ActorsToForget;
    ActorsToForget.Reserve(ProcessingStimuli.Num());
    TArray<TObjectKey<AActor>, TInlineAllocator<8>> DataToRemove;

    for (FStimulusToProcess& SourcedStimulus : ProcessingStimuli)
    {
        const TObjectKey<AActor>& SourceKey = SourcedStimulus.Source;

        FActorPerceptionInfo* PerceptualInfo = PerceptualData.Find(SourceKey);
        AActor* SourceActor = nullptr;

        if (PerceptualInfo == NULL)
        {
            if (SourcedStimulus.Stimulus.WasSuccessfullySensed() == false)
            {
                // this means it's a failed perception of an actor our owner is not aware of
                // at all so there's no point in creating perceptual data for a failed stimulus
                continue;
            }
            else
            {
                SourceActor = CastChecked<AActor>(SourceKey.ResolveObjectPtr(), ECastCheckedType::NullAllowed);

                // no existing perceptual data and source no longer valid: nothing to do with this stimulus
                if (SourceActor == nullptr)
                {
                    continue;
                }
                
                // create an entry
                PerceptualInfo = &PerceptualData.Add(SourceKey, FActorPerceptionInfo(SourceActor));
                // tell it what's our dominant sense
                PerceptualInfo->DominantSense = DominantSenseID;

                PerceptualInfo->bIsHostile = (FGenericTeamId::GetAttitude(GetOwner(), SourceActor) == ETeamAttitude::Hostile);
            }
        }

        if (PerceptualInfo->LastSensedStimuli.Num() <= SourcedStimulus.Stimulus.Type)
        {
            const int32 NumberToAdd = SourcedStimulus.Stimulus.Type - PerceptualInfo->LastSensedStimuli.Num() + 1;
            PerceptualInfo->LastSensedStimuli.AddDefaulted(NumberToAdd);
        }

        check(SourcedStimulus.Stimulus.Type.IsValid());

        FAIStimulus& StimulusStore = PerceptualInfo->LastSensedStimuli[SourcedStimulus.Stimulus.Type];
        const bool bActorInfoUpdated = SourcedStimulus.Stimulus.WantsToNotifyOnlyOnPerceptionChange() == false 
            || SourcedStimulus.Stimulus.WasSuccessfullySensed() != StimulusStore.WasSuccessfullySensed();

        if (SourcedStimulus.Stimulus.WasSuccessfullySensed())
        {
            RefreshStimulus(StimulusStore, SourcedStimulus.Stimulus);
        }
        else if (StimulusStore.IsExpired() == false)
        {    
            if (bActorInfoUpdated)
            {
                // @note there some more valid info in SourcedStimulus->Stimulus regarding test that failed
                // may be useful in future
                StimulusStore.MarkNoLongerSensed();
                StimulusStore.SetStimulusAge(0);
            }
        }
        else
        {
            HandleExpiredStimulus(StimulusStore);

            if (bForgetStaleActors && !PerceptualInfo->HasAnyCurrentStimulus())
            {
                if (AActor* ActorToForget = PerceptualInfo->Target.Get())
                {
                    ActorsToForget.Add(ActorToForget);
                }
            }
        }

        // if the new stimulus is "valid" or it's info that "no longer sensed" and it used to be sensed successfully
        if (bActorInfoUpdated)
        {
            // Source Actor is only resolved from SourceKey when required but might already have been resolved for new entry
            SourceActor = (SourceActor == nullptr) ? CastChecked<AActor>(SourceKey.ResolveObjectPtr(), ECastCheckedType::NullAllowed) : SourceActor;
            if (SourceActor == nullptr)
            {
                DataToRemove.Add(SourceKey);
            }
            else
            {
                UpdatedActors.AddUnique(SourceActor);
                if (bBroadcastEveryTargetUpdate)
                {
                    OnTargetPerceptionUpdated.Broadcast(SourceActor, StimulusStore);
                }
            }

            if (bBroadcastEveryTargetInfoUpdate)
            {
                OnTargetPerceptionInfoUpdated.Broadcast(FActorPerceptionUpdateInfo(GetTypeHash(SourceKey), PerceptualInfo->Target, StimulusStore));
            }
        }
    }

    if (UpdatedActors.Num() > 0)
    {
        if (AIOwner != NULL)
        {
            AIOwner->ActorsPerceptionUpdated(UpdatedActors);
        }

        OnPerceptionUpdated.Broadcast(UpdatedActors);
    }

    // forget actors that are no longer perceived
    for (AActor* ActorToForget : ActorsToForget)
    {
        ForgetActor(ActorToForget);
    }

    // notify anyone interested
    if (OnTargetPerceptionForgotten.IsBound())
    {
        for (AActor* ActorToForget : ActorsToForget)
        {
            OnTargetPerceptionForgotten.Broadcast(ActorToForget);
        }
    }

    // remove perceptual info related to stale actors
    for (const TObjectKey<AActor>& SourceKey : DataToRemove)
    {
        PerceptualData.Remove(SourceKey);
    }
}