Browse Source

feat: 添加 Hearing 的触发逻辑

nicetry12138 5 months ago
parent
commit
d908870362
4 changed files with 426 additions and 74 deletions
  1. BIN
      UE5/AI/感知/Image/003.png
  2. BIN
      UE5/AI/感知/Image/004.png
  3. BIN
      UE5/AI/感知/Image/005.png
  4. 426 74
      UE5/AI/感知/RAEDME.md

BIN
UE5/AI/感知/Image/003.png


BIN
UE5/AI/感知/Image/004.png


BIN
UE5/AI/感知/Image/005.png


+ 426 - 74
UE5/AI/感知/RAEDME.md

@@ -5,11 +5,178 @@
 AI 感知系统分为五个部分
 
 1. AIPerceptionSystem: AI感知系统 核心数据处理 信息传递
-2. AISense: 提供具体 AI 感知
+2. AISense: 提供具体 AI 感知的执行逻辑
 3. AISenseConfig: 配置 AISense 所需数据
 4. AIStimulus: 感知系统中的刺激
 5. AIPerceptionComponent: 配置 AISenseConfig
 
+![](Image/003.png)
+
+大概内容如上图所展示
+
+在场景中存在一个唯一的 `AIPerceptionSystem` 作为 感知系统 的管理器
+
+每个 `Actor` 上都可以绑定一个 `AIPerceptionComponent` 用于配置 **感知** 数据
+
+在 `AIPerceptionComponent` 的 `OnRegister` 的时候,会将 **自己** 和配置的 `AISenseConfig` 注册给 `AIPerceptionSystem`
+
+> `AISenseConfig` 与 `AISense` 对应,通过 `AISenseConfig` 可以获取对应的 `AISense` 的 `Class`
+
+相同类型的 `AISense` 不会重复创建,这是通过 `AISense` 中 `AISenseId` 属性来判断的
+
+在 `AIPerceptionSystem` 的 `Tick` 中
+
+1. 更新 Listener 的信息,也就是 `AIPerceptionComponent` 的位置信息
+2. 执行每个 AISense 的 Tick 函数,会在里面根据收集到的各种信息,来判读是否触发 **刺激**,触发刺激时调用 Listener.RegisterStimulus 将信息注册保存起来
+3. 判断 Listener 是否需要触发刺激,调用 Listener.ProcessStimuli
+
+## 通用结构体
+
+### UAISenseID
+
+```cpp
+struct FAISenseCounter : FAIBasicCounter<uint8>
+{};
+typedef FAINamedID<FAISenseCounter> FAISenseID;
+```
+
+这里出现两个结构体 `FAINamedID` 和 `FAIBasicCounter`
+
+```cpp
+template<typename TCounterType>
+struct FAIBasicCounter
+{
+	typedef TCounterType Type;
+protected:
+	Type NextAvailableID;
+public:
+	FAIBasicCounter() : NextAvailableID(Type(0)) {}
+	Type GetNextAvailableID() { return NextAvailableID++; }
+	uint32 GetSize() const { return uint32(NextAvailableID); }
+    // 其他工具函数
+}
+```
+
+`FAIBasicCounter` 就是维护一个 TCounterType 类型的属性
+
+> TCounterType 可能是 uint8、uint32 等,这里使用的是 uint8
+
+```cpp
+template<typename TCounter>
+struct FAINamedID
+{
+	const typename TCounter::Type Index;
+	const FName Name;
+private:
+	static AIMODULE_API TCounter Counter;
+public:
+	FAINamedID(const FName& InName)
+		: Index(GetCounter().GetNextAvailableID()), Name(InName)
+	{}
+
+    // 其他函数
+}
+```
+
+`FAINamedID` 的内容,总共有三个属性
+
+1. Index 用于表示 ID
+2. Name 用于表示对应的 AISense 的名称
+3. `Counter` 就是 `FAIBasicCounter`
+
+注意,`Counter` 属性是 `static` 的,也就是所有的 `AISenseID` 通用的
+
+每次调用 `GetCounter().GetNextAvailableID()` 给 `FAISenseID` 的 `Index` 赋值的时候都会让 `NextAvailableID` 的值 +1
+
+综上所述,简单来说
+
+`FAIBasicCounter`,就是一个 uint8 的整数,每次创建一个全新的 `FAISenseID` 的时候都会让这个值 +1 
+
+但是,不是每次都会创建 `FAISenseID` 
+
+在 `UAIPerceptionSystem::RegisterSenseClass` 函数中可以看到如何获取 AISense 的 AISenseID 的
+
+```cpp
+FAISenseID SenseID = UAISense::GetSenseID(SenseClass);
+
+static FAISenseID GetSenseID(const TSubclassOf<UAISense> SenseClass) { return SenseClass ? ((const UAISense*)SenseClass->GetDefaultObject())->SenseID : FAISenseID::InvalidID(); }
+```
+
+没错,是从 CDO 中获取 AISenseID 的
+
+这也就意味着,每种类型的 FAISence 对应的 FAISenseID 是相同的
+
+如果是一个全新类型的 FAISense,它没有有效的 SenseID,那么会通过来创建一个新的 FAISenseID
+
+```cpp
+FAISenseID UAISense::UpdateSenseID()
+{
+	if (SenseID.IsValid() == false)
+	{
+		SenseID = FAISenseID(GetFName());
+	}
+	return SenseID;
+}
+```
+ 
+> 注意,这里的构造函数,使得 Counter.NextAvailableID 的数值 +1
+
+所以说,总共有多少个 FAISense,那么其对应的 AISenseID 最大值就是多少
+
+这也是为什么 `PerceptionFilter` 可以只用一个 int32 来过滤 FAISense,毕竟一个项目一般不会超过 32 中类型的 FAISense
+
+> 注意 Counter.NextAvailableID 是 uint8 类型的,也就是 2^8 = 32
+
+### FPerceptionChannelAllowList
+
+
+```cpp
+struct FPerceptionChannelAllowList
+{
+	typedef int32 FFlagsContainer;
+	FFlagsContainer AcceptedChannelsMask;
+
+	FORCEINLINE_DEBUGGABLE FPerceptionChannelAllowList& AcceptChannel(FAISenseID Channel)
+	{
+		AcceptedChannelsMask |= (1 << Channel);
+		return *this;
+	}
+
+	FORCEINLINE bool ShouldRespondToChannel(FAISenseID Channel) const
+	{
+		return (AcceptedChannelsMask & (1 << Channel)) != 0;
+	}
+    // 其他功能函数
+}
+```
+
+结构非常简单, `FPerceptionChannelAllowList` 用于维护一个 int32 的属性,该属性用于做位运算
+
+参考 `AcceptChannel` 函数实现,每个 `AISenseID` 都对应 `AcceptedChannelsMask` 32 位中的一个位
+
+那么,使用位运算就可以很方便的对 AISense 进行过滤,或者判断是否需要对该 AISense 做出反应
+
+> `ShouldRespondToChannel` 函数的作用就是过滤
+
+### FPerceptionListener
+
+用于存储一个 Listener 的相关信息
+
+```cpp
+struct FPerceptionListener
+{
+	TWeakObjectPtr<UAIPerceptionComponent> Listener;	// Listener 的感知组件
+	FPerceptionChannelAllowList Filter;					// Listener 的过滤器
+	FVector CachedLocation;					// 缓存 Listener 的坐标
+	FVector CachedDirection;				// 缓存 Listener 的朝向
+	FGenericTeamId TeamIdentifier;			// Listener 的队伍信息
+private:
+	uint32 bHasStimulusToProcess : 1;		// 是否由新的刺激
+	FPerceptionListenerID ListenerID;		// 
+	// 其他函数
+}
+```
+
 ## 感知配置 UAISenseConfig
 
 ![](Image/002.png)
@@ -159,137 +326,322 @@ SetMaxStimulusAge(SenseID, SenseConfig.GetMaxAge());
 
 `PerceptionFilter` 是什么?
 
+`PerceptionFilter` 就是 `FPerceptionChannelAllowList`,一个用于判断是否需要对指定 `AISenseID` 做出反应的结构体
+
+### 事件
+
+所有感知事件的触发都是通过事件的方式,传递出去的
+
+| 事件 | 作用 |
+| --- | --- |
+| OnPerceptionUpdated | 全局感知更新事件,所有在本批处理中状态变化的Actor |
+| OnTargetPerceptionForgotten | 特定目标被完全遗忘时触发。表示系统不再跟踪该目标的任何感知信息 |
+| OnTargetPerceptionUpdated | 特定目标的感知状态变化通知。提供源Actor和具体刺激数据,但要求源Actor必须有效 |
+| OnTargetPerceptionInfoUpdated | 更可靠的目标感知状态变化通知。使用FActorPerceptionUpdateInfo结构传递数据,即使源Actor已销毁仍会触发 |
+
+### RegisterStimulus
+
+对外接口,用于注册一个 刺激 信息
+
 ```cpp
-struct FPerceptionChannelAllowList
+void UAIPerceptionComponent::RegisterStimulus(AActor* Source, const FAIStimulus& Stimulus)
 {
-	typedef int32 FFlagsContainer;
-	FFlagsContainer AcceptedChannelsMask;
+	FStimulusToProcess& StimulusToProcess = StimuliToProcess.Add_GetRef(FStimulusToProcess(Source, Stimulus));
+	StimulusToProcess.Stimulus.SetExpirationAge(MaxActiveAge[int32(Stimulus.Type)]);
+}
+```
 
-	FORCEINLINE_DEBUGGABLE FPerceptionChannelAllowList& AcceptChannel(FAISenseID Channel)
-	{
-		AcceptedChannelsMask |= (1 << Channel);
-		return *this;
-	}
+### ProcessStimuli
 
-    // 其他功能函数
+当有 Stimuli 刺激的时候,会由 `AIPerceptionSystem` 调用该函数
+
+用于处理在 `RegisterStimulus` 中添加到 `StimuliToProcess` 的数据
+
+
+
+## AIPerceptionSystem
+
+### 成员属性
+
+- SourcesToRegister 
+
+`FPerceptionSourceRegistration` 结构体中, `SenseID` 表示是哪个 `AISense` 在监听, `Source` 表示被监听的对象
+
+如果一个 `AISense` 的 `bAutoRegisterAllPawnsAsSources` 为 true,那么会在 `OnNewPawn` 的时候将 `NewPawn` 和该 `AISense` 注册到 `SourceToRegister` 中
+
+```cpp
+struct FPerceptionSourceRegistration
+{
+	FAISenseID SenseID;
+	TWeakObjectPtr<AActor> Source;
+	// 其他函数
 }
+TArray<FPerceptionSourceRegistration> SourcesToRegister;
 ```
 
-结构非常简单, `FPerceptionChannelAllowList` 用于维护一个 int32 的属性,该属性用于做位运算
+- RegisteredStimuliSources
 
+`FPerceptionStimuliSource` 结构体中存储这被监听的对象,以及该对象会触发哪些 `AISense`
 
-参考 `AcceptChannel` 函数实现,每个 `AISenseID` 都对应 `AcceptedChannelsMask` 32 位中的一个
+> `FPerceptionChannelAllowList` 维护着一个 uint32,每一位代表着一种 AISense
 
-那么,使用位运算就可以很方便的对 AISense 进行过滤,或者判断是否需要对该 AISense 做出反应
+`RegisteredStimuliSources` 存储着一个 `Actor` 和所有关心这个 `Actor` 的 `AISense`
 
-> `ShouldRespondToChannel` 函数的作用就是过滤
+```cpp
+struct FPerceptionStimuliSource
+{
+	TWeakObjectPtr<AActor> SourceActor;
+	FPerceptionChannelAllowList RelevantSenses;
+};
 
-### 事件
+TMap<const AActor*, FPerceptionStimuliSource> RegisteredStimuliSources;
+```
 
-所有感知事件的触发都是通过事件的方式,传递出去的
+- DelayedStimuli
 
-| 事件 | 作用 |
-| --- | --- |
-| OnPerceptionUpdated |  |
-| OnTargetPerceptionForgotten |  |
-| OnTargetPerceptionUpdated |  |
-| OnTargetPerceptionInfoUpdated |  |
+```cpp
+struct FDelayedStimulus
+{
+	double DeliveryTimestamp;				// 交付时间戳 并不是立刻触发刺激
+	FPerceptionListenerID ListenerId;		// Linsener 的 ID
+	TWeakObjectPtr<AActor> Instigator;		// 刺激源 Actor
+	FAIStimulus Stimulus;					// 刺激信息
+};
 
-## AIPerceptionSystem
+TArray<FDelayedStimulus> DelayedStimuli;
+```
 
+以 `UAISense_Hearing::Update` 为例,`DistToSoundSquared` 表示生源与接收者之间距离的平方,`SpeedOfSoundSqScalar` 表示声音速度平方的倒数
 
+通过 `DistToSoundSquared * SpeedOfSoundSqScalar` 计算得到从声音发出到目标听到声音所需要的时间间隔
 
+> `SpeedOfSoundSqScalar` 默认值为 0,表示无时间间隔
 
+```cpp
+const float Delay = FloatCastChecked<float>(FMath::Sqrt(DistToSoundSquared * SpeedOfSoundSqScalar), UE::LWC::DefaultFloatPrecision);
+```
+
+> `SpeedOfSoundSqScalar` 通过 `SpeedOfSoundSq` 初始化, `SpeedOfSoundSq` 在 `BaseGame.ini` 文件中配置,默认值为 0
 
-## 感知行为实现 UAISense
 
-### UAISenseID
+
+### RegisterSenseClass
+
+由 `AIPerceptionComponent` 在 `OnRegister` 的时候调用
 
 ```cpp
-struct FAISenseCounter : FAIBasicCounter<uint8>
-{};
-typedef FAINamedID<FAISenseCounter> FAISenseID;
+if (Senses[SenseID] == nullptr)
+{
+	Senses[SenseID] = NewObject<UAISense>(this, SenseClass);
+	
+	if (Senses[SenseID]->ShouldAutoRegisterAllPawnsAsSources())
+	{
+		UWorld* World = GetWorld();
+		if (World->HasBegunPlay())
+		{
+			World->GetTimerManager().SetTimerForNextTick(FTimerDelegate::CreateUObject(this, &UAIPerceptionSystem::RegisterAllPawnsAsSourcesForSense, SenseID));
+		}
+	}
+}
 ```
 
-这里出现两个结构体 `FAINamedID` 和 `FAIBasicCounter`
+`RegisterAllPawnsAsSourcesForSense` 在做的事情,就是遍历场景中所有的 Pawn 并将其添加到 `SourcesToRegister` 中
 
 ```cpp
-template<typename TCounterType>
-struct FAIBasicCounter
+UWorld* World = GetWorld();
+for (TActorIterator<APawn> PawnIt(World); PawnIt; ++PawnIt)
 {
-	typedef TCounterType Type;
-protected:
-	Type NextAvailableID;
-public:
-	FAIBasicCounter() : NextAvailableID(Type(0)) {}
-	Type GetNextAvailableID() { return NextAvailableID++; }
-	uint32 GetSize() const { return uint32(NextAvailableID); }
-    // 其他工具函数
+	SourcesToRegister.AddUnique(FPerceptionSourceRegistration(SenseID, &SourceActor));
 }
 ```
 
-`FAIBasicCounter` 就是维护一个 int 类型的属性
+### UpdateListener
+
+当传入一个新的 Listener 时,如果这 Listener 没有 ListenerID,需要生成一个 ListenerID 并设置给 Linstener
+
+> `Lisnter` 是 `UAIPerceptionComponent`
 
 ```cpp
-template<typename TCounter>
-struct FAINamedID
+const FPerceptionListenerID NewListenerId = FPerceptionListenerID::GetNextID();
+Listener.StoreListenerId(NewListenerId);
+FPerceptionListener& ListenerEntry = ListenerContainer.Add(NewListenerId, FPerceptionListener(Listener));
+ListenerEntry.CacheLocation();
+
+// 通知现有的 Sense 有新的 Listerner
+OnNewListener(ListenerContainer[NewListenerId]);
+```
+
+> `FPerceptionListenerID::GetNextID()` 可以直接理解为一个 static 的全局 int32 变量执行 +1 操作
+
+缓存 `ListenerID` 和 `Listener` 缓存到 `ListenerContainer` 容器中
+
+然后通知所有现有的 `AISense` 有新的 `Listener` 了
+
+### OnNewPawn
+
+在 `UAISystem` 的 `PostInitProperties` 的时候,监听了 `Pawn` 的 BeginPlay 事件
+
+```cpp
+PawnBeginPlayDelegateHandle = APawn::OnPawnBeginPlay.AddUObject(this, &UAISystem::OnPawnBeginPlay);
+```
+
+在 `OnPawnBeginPlay` 的时候调用 `AIPerceptionSystem` 的 `OnNewPawn`
+
+```cpp
+if (PawnWorld == GetWorld())
 {
-	const typename TCounter::Type Index;
-	const FName Name;
-private:
-	static AIMODULE_API TCounter Counter;
-public:
-	FAINamedID(const FName& InName)
-		: Index(GetCounter().GetNextAvailableID()), Name(InName)
-	{}
+	PerceptionSystem->OnNewPawn(*Pawn);
+}
+```
 
-    // 其他函数
+在 `AIPerceptionSystem` 的 `OnNewPawn` 主要是为了通知 `AISense`,顺便增量更新 `SourcesToRegister` 的内容
+
+```cpp
+for (UAISense* Sense : Senses)
+{
+	if (Sense->WantsNewPawnNotification())
+	{
+		Sense->OnNewPawn(Pawn);
+	}
+
+	if (Sense->ShouldAutoRegisterAllPawnsAsSources())
+	{
+		FAISenseID SenseID = Sense->GetSenseID();
+		SourcesToRegister.AddUnique(FPerceptionSourceRegistration(SenseID, &SourceActor));
+	}
 }
 ```
 
-`FAINamedID` 的内容,总共有三个属性
+> 毕竟不可能总是遍历 World 内所有的 Pawn 来更新 SourcesToRegister 的值
 
-1. Index 用于表示 ID
-2. Name 用于表示对应的 AISense 的名称
-3. `Counter` 就是 `FAIBasicCounter`
+### Tick
 
-注意,`Counter` 属性是 `static` 的,也就是所有的 `AISenseID` 通用的
+整体流程大概如下
 
-每次调用 `GetCounter().GetNextAvailableID()` 给 `FAISenseID` 的 `Index` 赋值的时候都会让 `NextAvailableID` 的值 +1
+1. 更新 `RegisteredStimuliSources` 数据
+2. 更新所有 Listener 的数据
+3. 调用每个 AISense 的 Tick
+4. 处理所有 DelayedStimulus 刺激,注册给 Listener
+5. 通知 Listener 处理刺激
 
-综上所述,简单来说
 
-`FAIBasicCounter`,就是一个 uint8 的整数,每次创建一个全新的 `FAISenseID` 的时候都会让这个值 +1 
+- 执行 `PerformSourceRegistration`
 
-但是,不是每次都会创建 `FAISenseID` 
+将 `SourcesToRegister` 中存储的 Source Actor 注册到对应的 AISense 中
 
-在 `UAIPerceptionSystem::RegisterSenseClass` 函数中可以看到如何获取 AISense 的 AISenseID 的
+将 `SourcesToRegister` 中的数据转换到 `RegisteredStimuliSources` 中,并清空 `SourcesToRegister`
 
 ```cpp
-FAISenseID SenseID = UAISense::GetSenseID(SenseClass);
+// 注册 Source Actor 到 AISense 中
+Senses[PercSource.SenseID]->RegisterSource(*SourceActor);
 
-static FAISenseID GetSenseID(const TSubclassOf<UAISense> SenseClass) { return SenseClass ? ((const UAISense*)SenseClass->GetDefaultObject())->SenseID : FAISenseID::InvalidID(); }
+// 将 Source Actor 转存到 RegisteredStimuliSources 中
+FPerceptionStimuliSource& StimuliSource = RegisteredStimuliSources.FindOrAdd(SourceActor);
+StimuliSource.SourceActor = SourceActor;
+StimuliSource.RelevantSenses.AcceptChannel(PercSource.SenseID);
 ```
 
-没错,是从 CDO 中获取 AISenseID 的
+- 更新 Listener 的缓存信息,清理无效 Listener
 
-这也就意味着,每种类型的 FAISence 对应的 FAISenseID 是相同的
+```cpp
+for (AIPerception::FListenerMap::TIterator ListenerIt(ListenerContainer); ListenerIt; ++ListenerIt)
+{
+	if (ListenerIt->Value.Listener.IsValid())
+	{
+		ListenerIt->Value.CacheLocation();
+	}
+	else
+	{
+		OnListenerRemoved(ListenerIt->Value);
+		ListenerIt.RemoveCurrent();
+	}
+}
+```
 
-如果是一个全新类型的 FAISense,它没有有效的 SenseID,那么会通过来创建一个新的 FAISenseID
+- 执行每个 AISense 的 Tick 
 
 ```cpp
-FAISenseID UAISense::UpdateSenseID()
+for (UAISense* const SenseInstance : Senses)
 {
-	if (SenseID.IsValid() == false)
+	if (SenseInstance != nullptr)
 	{
-		SenseID = FAISenseID(GetFName());
+		SenseInstance->Tick();
+	}
+}
+```
+
+> `UAISense::Tick` 去调用 `UAISense::Update` 函数,积累都是重写 `Update` 函数
+
+- 执行 `DeliverDelayedStimuli`
+
+将 `DelayedStimuli` 按 刺激 触发时间进行排序,将所有在 `CurrentTime` 之前的 刺激 全部注册给 `Listener`
+
+```cpp
+ListenerEntry.RegisterStimulus(DelayedStimulus.Instigator.Get(), DelayedStimulus.Stimulus);
+```
+
+> 触发 `AIPerceptionComponent` 的 `RegisterStimulus`
+
+- 遍历所有的 Listener,如果存在新的刺激(Stimuli),通知其执行
+
+```cpp
+for (AIPerception::FListenerMap::TIterator ListenerIt(ListenerContainer); ListenerIt; ++ListenerIt)
+{
+	if (ListenerIt->Value.HasAnyNewStimuli())
+	{
+		ListenerIt->Value.ProcessStimuli();
+	}
+}
+```
+
+`ProcessStimuli` 的内容就是设置 `bHasStimulusToProcess` 标记为为 false,清除刺激状态,调用 `Listener->ProcessStimuli()` 通知 `AIPerceptionComponent` 执行逻辑
+
+
+## 感知行为实现 UAISense
+
+以 `AISense_Hearing` 为例
+
+如果想要触发一次 Hearing 的刺激,在蓝图中调用 `Report Noise Event`
+
+![](Image/004.png)
+
+```cpp
+template<typename FEventClass, typename FSenseClass = typename FEventClass::FSenseClass>
+void OnEvent(const FEventClass& Event)
+{
+	const FAISenseID SenseID = UAISense::GetSenseID<FSenseClass>();
+	if (Senses.IsValidIndex(SenseID) && Senses[SenseID] != nullptr)
+	{
+		((FSenseClass*)Senses[SenseID])->RegisterEvent(Event);
+	}
+}
+```
+
+![](Image/005.png)
+
+在 `UAISense_Hearing::Update` 函数中,处理注册到 `NoiseEvents` 中的事件
+
+获取 `AIPerceptionSystem` 中所有的 Listeners
+
+双重遍历,首先遍历 Listeners,然后对每个 Listener 遍历 NoiseEvents 判断该 Noise 能否激活
+
+对于能够激活的 刺激,收集其信息,并通过 `RegisterDelayedStimulus` 注册到 `AIPerceptionSystem` 中 
+
+```cpp
+auto ListenersMap = PerceptionSystemInstance->GetListenersMap();
+for (AIPerception::FListenerMap::TIterator ListenerIt(ListenersMap); ListenerIt; ++ListenerIt)
+{
+	for (const FAINoiseEvent& Event : NoiseEvents)
+	{
+		// 距离 时间 判断
+		
+		// 符合条件的
+		const float Delay = FloatCastChecked<float>(FMath::Sqrt(DistToSoundSquared * SpeedOfSoundSqScalar), UE::LWC::DefaultFloatPrecision);
+		PerseptionSys->RegisterDelayedStimulus(Listener.GetListenerID(), Delay, Event.Instigator
+			, FAIStimulus(*this, ClampedLoudness, Event.NoiseLocation, Listener.CachedLocation, FAIStimulus::SensingSucceeded, Event.Tag) );
+
 	}
-	return SenseID;
 }
 ```
- 
-> 注意,这里的构造函数,使得 Counter.NextAvailableID 的数值 +1
 
-所以说,总共有多少个 FAISense,那么其对应的 AISenseID 最大值就是多少
 
-这也是为什么 `PerceptionFilter` 可以只用一个 int32 来过滤 FAISense,毕竟一个项目不可能创建 2^32 中类型的 FAISense