Events
- AI Perception可以為所附的AI Controller blueprint提供一些感知更新時觸發的事件,AI Controller可就著這些事件觸發的時機,編寫AI角色在感知更新時的處理
-
- OnPerceptionUpdated
- 只要有對象更新感知系統就調用,無論有沒有綁定該函數至藍圖內
- 每次調用都把所有對象作為參數傳入(Actor列表)
- OnTargetPerceptionForgotten
- 各感知系統可配置MaxLife指定對象被感知到的持續時間
- 當這些對象被「遺忘」時,觸發事件並傳入該對象的Actor
- OnTargetPerceptionUpdated
- 感知系統接收到更新信號時觸發,傳入發出更新信號的某個對象Actor的強引用及感知細節(Stimuli)
- OnTargetPerceptionInfoUpdated
- 感知系統接收到更新信號時觸發,傳入發出更新信號的UpdateInfo,UpdateInfo包含了以下3個東西
- 對象指針的Hash
- 對象的弱引用
- 感知細節(Stimuli)
- 感知系統接收到更新信號時觸發,傳入發出更新信號的UpdateInfo,UpdateInfo包含了以下3個東西
- OnPerceptionUpdated
- 流程
- 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);
}
}