[C++/UE] UE5中AI開發的一點功能梳理(五) – AI Perception 感知系統 Sense部分Programming Notes, DevNotes / By LoneliNerd / 2024 年 3 月 30 日 2024 年 3 月 30 日 感知系統AI Perception使用的AI Controller -> Add ComponentAIPerception => 刺激源接收器 AIPerception Stimuli Source => 刺激源發生器在非AI Actor上添加並注冊對應事件,從而被AI所「感知」到增加具體要被哪些感官所能感知到所有Pawn都會默認加上Sight,所以多用於添加其他類型的刺激或者非Pawn的Actor也能發生刺激的場景上 AI Perception使用及參數說明在AI Perception組件中,可以配置相應的Sense Config項 Sight 視覺感知Detection by Affiliation 決定誰能觸發事件(敵人/路人/友軍)Affiliation目前只能在C++裡定義在blueprint裡可以勾上Dectect Neutrals(路人)來檢測所有ActorAuto Success Range from Last Seen Location 對象必然會被重新看見的距離if value > 0,對象如果離上次被看見的距離 < value,則仍然算是被看見if value < 0,每次失蹤後都要重新做一次完整的視覺感知例子:解決恐怖遊戲的當面入櫃情況具體視野計算Sight Radius / Lose Sight Radius 視野/ 失蹤距離,會被轉化成Square值 //AISense_Sight.cpp UAISense_Sight::FDigestedSightProperties::FDigestedSightProperties(const UAISenseConfig_Sight& SenseConfig) { SightRadiusSq = FMath::Square(SenseConfig.SightRadius + SenseConfig.PointOfViewBackwardOffset); LoseSightRadiusSq = FMath::Square(SenseConfig.LoseSightRadius + SenseConfig.PointOfViewBackwardOffset); } Peripheral Vision Half Angle Degrees 可視範圍的半角,會被轉成弧度加入計算Point of View Backward Offset 如果不接近於0,就會用來調整視野的起點位置== 0:起點位置 = AI自身的位置!= 0:起點位置 = 正後方方向normalized * Point of View Backward OffsetNear clipping Radius:從新起點計算的近平面半徑,會被轉化為Square值 //AISense_Sight.cpp UAISense_Sight::FDigestedSightProperties::FDigestedSightProperties(const UAISenseConfig_Sight& SenseConfig) { NearClippingRadiusSq = FMath::Square(SenseConfig.NearClippingRadius); } 具體視野示意圖如下(視野檢測會調用CheckIsTargetInSightCone): //AIHelpers.cpp //----------------------------------------------------------------------// // CheckIsTargetInSightCone // F // ***** // * * // * * // * * // * * // * * // \ / // \ / // \ / // \ X / // \ / // \ *** / // \ * N * / // \ * * / // N N // // // // // // // B // // X = StartLocation // B = Backward offset // N = Near Clipping Radius (from the StartLocation adjusted by Backward offset) // F = Far Clipping Radius (from the StartLocation adjusted by Backward offset) //----------------------------------------------------------------------// bool CheckIsTargetInSightCone(const FVector& StartLocation, const FVector& ConeDirectionNormal, float PeripheralVisionAngleCos, float ConeDirectionBackwardOffset, float NearClippingRadiusSq, float const FarClippingRadiusSq, const FVector& TargetLocation) { const FVector BaseLocation = FMath::IsNearlyZero(ConeDirectionBackwardOffset) ? StartLocation : StartLocation - ConeDirectionNormal * ConeDirectionBackwardOffset; const FVector ActorToTarget = TargetLocation - BaseLocation; const FVector::FReal DistToTargetSq = ActorToTarget.SizeSquared(); if (DistToTargetSq <= FarClippingRadiusSq && DistToTargetSq >= NearClippingRadiusSq) { // Will return true if squared distance to Target is smaller than SMALL_NUMBER if (DistToTargetSq < SMALL_NUMBER) { return true; } // Calculate the normal here instead of calling GetUnsafeNormal as we already have the DistToTargetSq (optim) const FVector DirectionToTargetNormal = ActorToTarget * FMath::InvSqrt(DistToTargetSq); return FVector::DotProduct(DirectionToTargetNormal, ConeDirectionNormal) > PeripheralVisionAngleCos; } return false; } //AISense_Sight.cpp UAISense_Sight::EVisibilityResult UAISense_Sight::ComputeVisibility(UWorld* World, FAISightQuery& SightQuery, FPerceptionListener& Listener, const AActor* ListenerActor, FAISightTarget& Target, AActor* TargetActor, const FDigestedSightProperties& PropDigest, float& OutStimulusStrength, FVector& OutSeenLocation, int32& OutNumberOfLoSChecksPerformed, int32& OutNumberOfAsyncLosCheckRequested) const { //... const FVector TargetLocation = TargetActor->GetActorLocation(); const float SightRadiusSq = SightQuery.GetLastResult() ? PropDigest.LoseSightRadiusSq : PropDigest.SightRadiusSq; if (!FAISystem::CheckIsTargetInSightCone(Listener.CachedLocation, Listener.CachedDirection, PropDigest.PeripheralVisionAngleCos, PropDigest.PointOfViewBackwardOffset, PropDigest.NearClippingRadiusSq, SightRadiusSq, TargetLocation)) { return EVisibilityResult::NotVisible; } //... } Hearing 聽覺感知響應Report Noise Eventblueprint裡可以拖出相關節點 Hearing Range 聽覺距離Detection by Affiliation 決定誰能觸發事件(敵人/路人/友軍)Affiliation目前只能在C++裡定義在blueprint裡可以勾上Dectect Neutrals(路人)來檢測所有Actor Touch 接觸感知應在被甚麼碰到/碰到甚麼時觸發響應Report Touch Eventblueprint裡可以拖出相關節點 Team Sense 隊友感知同陣營接近的時候會通知需要配合C++食用 AI Prediction Sense 預測感知目的是為了讓AI發現玩家後,在跟蹤的過程中跟丟之後,依然會對玩家的可能移動結果進行一個以時間為單位的推算,並得出一個新的目標位置需要配合其他Sense使用,如以下視野檢測: 當Stimulus Successfully Sensed為false時(看不見了),接入Request Pawn/Character Prediction Event節點,請求做一次Prediction Sense然後會觸發Perception Update事件,再在該事件裡增加對Prediction Sense的處理 Damage Sense 傷害感知響應 Report Damage Event / Apply Any Damage / Apply Radial Damage / Apply Point Damageblueprint裡可以拖出相關節點 共同參數Dominant Sense => 最優先感知,應該是已配置的感知其中之一StartsEnabled:該感知是否要手動啟動,true為不需要手動啟動流程:調用SetSenseEnabled,指定具體的Sense Class MaxAge:感知持續時間,0代表永久持續單位是秒Age會每幀按一定的增長率遞增,一旦 Age >= MaxAge(ExpirationAge),感知就會「過期」增長率可以在UAIPerceptionSystem的初始化列表裡修改 //AIPerceptionSystem UAIPerceptionSystem::UAIPerceptionSystem(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) , PerceptionAgingRate(0.3f) , bHandlePawnNotification(false) , NextStimuliAgingTick(0.) , CurrentTime(0.) { StimuliSourceEndPlayDelegate.BindDynamic(this, &UAIPerceptionSystem::OnPerceptionStimuliSourceEndPlay); } void UAIPerceptionSystem::Tick(float DeltaSeconds) { SCOPE_CYCLE_COUNTER(STAT_AI_PerceptionSys); SCOPE_CYCLE_COUNTER(STAT_AI_Overall); CSV_SCOPED_TIMING_STAT_EXCLUSIVE(AIPerception); // if no new stimuli // and it's not time to remove stimuli from "know events" UWorld* World = GEngine->GetWorldFromContextObjectChecked(GetOuter()); check(World); if (World->bPlayersOnly == false) { // cache it CurrentTime = World->GetTimeSeconds(); if (SourcesToRegister.Num() > 0) { PerformSourceRegistration(); } bool bSomeListenersNeedUpdateDueToStimuliAging = false; if (NextStimuliAgingTick <= CurrentTime) { constexpr double Precision = 1./64.; const float AgingDt = FloatCastChecked<float>(CurrentTime - NextStimuliAgingTick, Precision); bSomeListenersNeedUpdateDueToStimuliAging = AgeStimuli(PerceptionAgingRate + AgingDt); NextStimuliAgingTick = CurrentTime + PerceptionAgingRate; } //... } } bool UAIPerceptionSystem::AgeStimuli(const float Amount) { ensure(Amount >= 0.f); bool bTagged = false; for (AIPerception::FListenerMap::TIterator ListenerIt(ListenerContainer); ListenerIt; ++ListenerIt) { FPerceptionListener& Listener = ListenerIt->Value; if (Listener.Listener.IsValid()) { // AgeStimuli will return true if this listener requires an update after stimuli aging if (Listener.Listener->AgeStimuli(Amount)) { Listener.MarkForStimulusProcessing(); bTagged = true; } } } return bTagged; } //AIPerceptionComponent.cpp bool UAIPerceptionComponent::AgeStimuli(const float ConstPerceptionAgingRate) { bool bExpiredStimuli = false; for (FActorPerceptionContainer::TIterator It(PerceptualData); It; ++It) { FActorPerceptionInfo& ActorPerceptionInfo = It->Value; for (FAIStimulus& Stimulus : ActorPerceptionInfo.LastSensedStimuli) { // Age the stimulus. If it is active but has just expired, mark it as such if (Stimulus.AgeStimulus(ConstPerceptionAgingRate) == false && (Stimulus.IsActive() || Stimulus.WantsToNotifyOnlyOnPerceptionChange()) && Stimulus.IsExpired() == false) { AActor* TargetActor = ActorPerceptionInfo.Target.Get(); if (TargetActor) { Stimulus.MarkExpired(); RegisterStimulus(TargetActor, Stimulus); bExpiredStimuli = true; } } } } return bExpiredStimuli; } //AIPerceptionTypes.h /** @return false when this stimulus is no longer valid, when it is Expired */ FORCEINLINE bool AgeStimulus(float ConstPerceptionAgingRate) { Age += ConstPerceptionAgingRate; return Age < ExpirationAge; }