BlueRoseNote/03-UnrealEngine/Gameplay/AI/Ue4 AI相关功能笔记.md
2023-06-29 11:55:02 +08:00

350 lines
18 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: Ue4 AI相关功能笔记
date: 2021-09-07  19:57:53
tags: AI
rating: ⭐️
---
## 其他文档
UE4场景询问系统浅析EQS与行为树
http://www.uejoy.com/?p=500
UE4 AIModule源码阅读之AI感知系统
https://zhuanlan.zhihu.com/p/356716030
## AI感知系统AI Perception System
文档https://docs.unrealengine.com/4.26/zh-CN/InteractiveExperiences/ArtificialIntelligence/AIPerception/
给角色类添加AIPerception组件并且添加所需要类型的感知即可实现感知效果。
- AI伤害对Event Any Damage、Event Point Damage或Event Radial Damage作出反应
- AI听觉可用于检测由 报告噪点事件Report Noise Event 产生的声音
- AI感知报告在PredictionTime秒内向请求者提供PredictedActor的预计位置。
- AI视觉当一个Actor进入视觉半径后AI感知系统将发出更新信号并穿过被看到的Actor举例而言一个玩家进入该半径并被具备视觉感知的AI所察觉
- AI团队这会通知感知组件的拥有者同团队中有人处在附近发送该事件的游戏代码也会发送半径距离
- AI触觉:配置能够检测到AI与物体发生主动碰撞、或是与物体发生被动碰撞。。举例而言在潜入类型的游戏中您可能希望玩家在不接触敌方AI的情况下偷偷绕过他们。使用此感官可以确定玩家与AI发生接触并能用不同逻辑做出响应。
除了可以使用绑定FActorPerceptionUpdatedDelegate委托来实现获取目标Actor外还可以使用GetHostileActorsBySense()蓝图版本GetCurrentlyPerceivedActors()
处理函数ProcessStimuli()是从UAIPerceptionSystem::Tick()调用的所以只需要修改Tick频率即可。PrimaryActorTick.TickInterval即可。
UAISense_Prediction一般与UAISense_Sight连用在UAISense_Sight丢失目标后若干秒使用计时器,调用RequestControllerPredictionEvent其中PredictionTime为预测时间长短。之后在处理函数中将事件Location传递给黑板变量。
### UPawnSensingComponent
除了UAIPerceptionComponent还可以使用UPawnSensingComponent有OnSeePawn与OnHearNoise委托。不过UPawnSensingComponent有个问题即它只能“看到”或者“听到”Pawn。
案例代码:
```
ARPGAICharacter::ARPGAICharacter(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer.SetDefaultSubobjectClass<ULCMCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
AIControllerClass = ARPGAIController::StaticClass();
bUseControllerRotationYaw = true;
SensingComponent = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("SensingComponent"));
SensingComponent->SetPeripheralVisionAngle(90.f);
}
void ARPGAICharacter::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
if (SensingComponent)
{
SensingComponent->OnSeePawn.AddDynamic(this, &ARPGAICharacter::OnSeeTarget);
SensingComponent->OnHearNoise.AddDynamic(this, &ARPGAICharacter::OnHearNoise);
}
}
void ARPGAICharacter::OnSeeTarget(APawn* Pawn)
{
ARPGAIController* AIController = Cast<ARPGAIController>(GetController());
//Set the seen target on the blackboard
if (AIController)
{
GLog->Log("Oh hello there");
AIController->SetSeenTarget(Pawn);
}
}
void ARPGAICharacter::OnHearNoise(APawn* Pawn,const FVector& Location, float Volume)
{
ARPGAIController* AIController = Cast<ARPGAIController>(GetController());
//Set the seen target on the blackboard
if (AIController)
{
GLog->Log(FString::Printf(TEXT("Hear Noise:Vec3(%f,%f,%f) Volume:%f"),Location.X,Location.Y,Location.Z,Volume));
AIController->SetHearTarget(Pawn,Location,Volume);
}
}
```
控制类中一般是将这些数据添加到黑板中。对应HearNoise则需要在角色中添加UPawnNoiseEmitterComponent
之后调用MakeNoise实现。
```
PawnNoiseEmitterComponent->MakeNoise(this, 1.f, GetActorLocation());
```
## 刺激源Stimuli Source
该组件封装了场景中可以感知的内容以及可以感知的内容。它通常添加在AI需要感知的Actor中。
## 调试方法
您可以使用AI调试工具调试AI感知。操作方法是在游戏运行时按下撇号'然后按数字键4调出感知信息。
## 设计思路
对于ARPG游戏来说我们只需AI听觉、AI感知报告、AI视觉与AI团队。
AI听觉让AI确定TargetLocation让AI去最后听到声音的位置查看。
AI视觉让AI发现TargetActor。
AI团队让AI将自己的TargetActor发送给其他AI。
## UE4 AI阻挡AI解决策略
CharacterMovement->bUseRVOAvoidance = true
## RAMA的插件
### nav修改Volume
https://nerivec.github.io/old-ue4-wiki/pages/ai-custom-pathing-how-to-use-nav-modifiers-query-filters.html
## 随机Task实现蓝图实现
https://answers.unrealengine.com/questions/658501/random-task-in-ai.html
使用Service节点随机给黑板随机生成一个数字在下面连接Tasks上加上一个BlackboardBasedCondition限制节点来实现。
## BT节点
### 黑板相关
可以在构造函数中给FBlackboardKeySelector设置过滤
```
//筛选Actor类型
BlackboardKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UBTService_DefaultFocus, BlackboardKey), AActor::StaticClass());
//筛选Vector类型
BlackboardKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UBTService_DefaultFocus, BlackboardKey));
MyVectorKey.AddVectorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyVectorKey));
MyObjectKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyObjectKey), AActor::StaticClass());
MySpecialActorClassKey.AddClassFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MySpecialActorClassKey), AMySpecialActor::StaticClass());
MyEnumKey.AddEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), StaticEnum<EMyEnum>());
MyEnumKey.AddNativeEnumFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyEnumKey), "MyEnum");
MyIntKey.AddIntFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyIntKey));
MyFloatKey.AddFloatFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyFloatKey));
MyBoolKey.AddBoolFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyBoolKey));
MyRotatorKey.AddRotatorFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyRotatorKey));
MyStringKey.AddStringFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyStringKey));
MyNameKey.AddNameFilter(this, GET_MEMBER_NAME_CHECKED(UMyBTTask, MyNameKey));
```
```
//通过行为树组件获取到黑板组件
UBlackboardComponent* MyBlackboard = OwnerComp.GetBlackboardComponent();
//获取黑板键值
UObject* KeyValue = MyBlackboard->GetValue<UBlackboardKeyType_Object>(BlackboardKey.GetSelectedKeyID());
//设置黑板键值
UBlackboardComponent->SetValueAsObject(TargetActor.SelectedKeyName, nullptr);
```
如果设置了FBlackboardKeySelector变量还需要在重写InitializeFromAsset(UBehaviorTree& Asset)
```
void UMyBTTask::InitializeFromAsset(UBehaviorTree& Asset)
{
Super::InitializeFromAsset(Asset);
UBlackboardData* BBAsset = GetBlackboardAsset();
if (ensure(BBAsset))
{
MySpecialKey.ResolveSelectedKey(*BBAsset);
}
}
```
### 实例化
Service节点是默认不被实例化的这代表这个节点会共用内存。实例化Service的两种方法:
#### 第一种: 使用内存块.
优点: 更加节省,不需要实例化节点,小巧方便.
缺点: 内存块的大小是固定的,如果涉及到动态分配内存的类型,例如数组,则无法使用此方法.
在源码BTService.h文件中可以看到一段二十行的英文注释关于实例化的描述翻译如下。
>由于其中一些可以针对特定的AI进行实例化因此以下虚拟函数未标记为const
OnBecomeRelevant来自UBTAuxiliaryNode
OnCeaseRelevant来自UBTAuxiliaryNode
TickNode来自UBTAuxiliaryNode
OnSearchStart
如果未实例化您的节点(默认行为),请不要在这些函数内更改对象的任何属性!
模板节点使用相同的树资产在所有行为树组件之间共享并且必须将其运行时属性存储在提供的NodeMemory块中分配大小由GetInstanceMemorySize确定
GetInstanceMemorySize函数的返回值决定了Service节点可以被分配到用来存储自身属性的内存大小。
而此函数是在UBTAuxiliaryNode的父类UBTNode中声明的并且默认返回值为0
>UBTNode不光是UBTAuxiliaryNode的父类也是UBTTaskNode任务节点的父类。
也就是说,行为树中 Service服务节点Decorator装饰器节点Task任务节点都是默认不被实例化的
至于如何实例化我们完全可以直接参考源码提供的BTService_RunEQS的写法。
首先在自定义Service头文件中声明一个结构体将需要的变量都放结构体里。
在类中声明一个此结构体的变量并重写GetInstanceMemorySize函数返回结构体所需内存大小。在InitializeMemory函数中可以对结构体变量进行初始化。
#### 第二种: 直接实例化节点.
优点: 可以使用任何复杂类型的属性.
缺点: 会为每个节点创建一个新对象.
这种只要在构造函数中设置标志位 bCreateNodeInstance = true; ,你就可以在头文件中声明任意变量使用了.
### 更改描述及命名
重写GetStaticDescription函数。
NodeName可以从Details面板修改也可以在类构造函数中直接 NodeName = “ ”
但这里的NodeName并不会修改在AddService时列表中显示的名称。
列表中的名称是由类名决定的除了修改类名也可以用类说明符DisplayName修改。
```
UClass(DisplayName="xxxx")
```
### 蓝图节点为啥会是双本版设计
源码逻辑:
当AIOwner为AIController时优先调用带AI后缀的函数。若只实现了不带AI后缀的也会调用成功
当AIOwner不为AIController时只调用不带AI后缀的函数。
```
void UBTService_BlueprintBase::SetOwner(AActor* InActorOwner)
{
ActorOwner = InActorOwner;
AIOwner = Cast<AAIController>(InActorOwner);
}
void UBTService_BlueprintBase::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
//判断是否Owner为AIController并且有任一版本函数被实现
if (AIOwner != nullptr && (ReceiveTickImplementations & FBTNodeBPImplementationHelper::AISpecific))
{
ReceiveTickAI(AIOwner, AIOwner->GetPawn(), DeltaSeconds);
}
//是否实现了不带AI后缀的函数
else if (ReceiveTickImplementations & FBTNodeBPImplementationHelper::Generic)
{
ReceiveTick(ActorOwner, DeltaSeconds);
}
}
```
## 只有附加在Composites合成节点上的Service才会调用OnSearchStart。
Composites合成节点也就是指SequenceSelectorSimpleParallel节点。
还有种特殊情况当Service附加在与Root相连的第一个合成节点上时SearchStart会被触发两次。
## Compisiton
复合节点:此类节点定义分支的根以及执行该分支的基本规则。
## Decorator
![](https://docs.unrealengine.com/4.26/Images/InteractiveExperiences/ArtificialIntelligence/BehaviorTrees/BehaviorTreeNodeReference/BehaviorTreeNodeReferenceDecorators/CustomDecorator.webp)
可以理解为条件节点连接Composite或者Task节点决定树中的分支、甚至单个节点能否被执行。
c++实现可以参考:
- UBTDecorator_BlueprintBase
- UBTDecorator_BlackboardBase
- UBTDecorator_CheckGameplayTagsOnActor
- UBTDecorator_Loop
蓝图节点中可重写的函数有:
- ReceiveTick:通用版本的tick函数。如果条件符合会在TickNode()中调用。
- ReceiveTickAI:AI版本的tick函数。如果条件符合会在TickNode()中调用。
- ReceiveExecutionStart:通用版本的Start函数。如果条件符合会在OnNodeActivation()中调用。
- ReceiveExecutionStartAI:AI版本的Start函数。如果条件符合会在OnNodeActivation()中调用。
- ReceiveExecutionFinish:通用版本的Finish函数。如果条件符合会在OnNodeDeactivation()中调用。
- ReceiveExecutionFinishAI:AI版本的Finish函数。如果条件符合会在OnNodeDeactivation()中调用。
- ReceiveObserverActivated:通用版本的Activated函数。如果条件符合会在OnBecomeRelevant()中调用。
- ReceiveObserverActivatedAI:AI版本的Activated函数。如果条件符合会在OnBecomeRelevant()中调用。
- ReceiveObserverDeactivated:通用版本的Deactivated函数。如果条件符合会在OnCeaseRelevant()中调用。
- ReceiveObserverDeactivatedAI:AI版本的Deactivated函数。如果条件符合会在OnCeaseRelevant()中调用。
- PerformConditionCheck:通用版本的ConditionCheck函数。如果条件符合会在CalculateRawConditionValueImpl()中调用。
- PerformConditionCheckAI:AI版本的ConditionCheck函数。如果条件符合会在CalculateRawConditionValueImpl()中调用。
## Service
![](https://docs.unrealengine.com/4.26/Images/InteractiveExperiences/ArtificialIntelligence/BehaviorTrees/BehaviorTreeNodeReference/BehaviorTreeNodeReferenceServices/NewCustomService_01.webp)
可以理解为并行逻辑节点连接连接Composite或者Task节点,运行到当前分支时,Service节点将按照之前设定的频率执行。通常是使用这些Service节点检查、更新黑板数据。
c++可以参考:
- UBTService_BlueprintBase
- UBTService_BlackboardBase
- UBTService_RunEQS
蓝图节点中可重写的函数有:
- ReceiveTick
- ReceiveTickAI
- ReceiveSearchStart
- ReceiveSearchStartAI
- ReceiveActivation
- ReceiveActivationAI
- ReceiveDeactivation
- ReceiveDeactivationAI
### 相关事件
- OnSearchStart所挂靠的合成节点被激活时调用的函数。
- TickNodeTick执行的函数间隔自定义。可在构造函数设置 bNotifyTick 来决定是否调用。
- OnBecomeRelevant节点被激活时调用等同于蓝图Service里的Receive Activation 需要在构造函数中设置 bNotifyBecomeRelevant = true来开启调用。
- OnCeaseRelevant节点被激活时调用等同于蓝图Service里的Receive Deactivation 需要在构造函数中设置 bNotifyCeaseRelevant = true来开启调用。
- TickNodeOnBecomeRelevant和OnCeaseRelevant并不是UBTService类的虚函数而是UBTService的父类UBTAuxiliaryNode的。装饰器UBTDecorator也继承此类。
- GetInstanceMemorySize当节点被实例化时分配的内存大小关于实例化后面会详谈。
- Tick函数执行间隔 = RandomInterval-RandomDeviation,Interval+RandomDeviation)
在这也就是 = Random(0.5-0.1 , 0.5+0.1) 每次调用都会计算一次。
- CallTickonSearchStart若为True则会在SearchStart函数触发时调用一次Tick函数。 若为False则即使节点已被激活也要等待一次间隔才能执行第一次Tick函数。
- RestartTimeronEachActivation若为True则每次节点被激活都会从0开始计算间隔。 若为False则会记录下上一次已经等待的间隔在下一次激活时沿用。
## Task
可以理解为具体执行的任务(逻辑)。此类节点是行为树的叶。它们是可执行的操作,没有输出连接。
UBTTask_BlueprintBase与UBTTask的区别在于
以UBTTask_PlaySound这种简单UBTTask为例主要实现了ExecuteTask()其他是显示图标与描述的GetStaticDescription()与GetNodeIconName()
蓝图节点中可重写的函数有:
- ReceiveTick:通用版本的tick函数。如果条件符合会在TickTask()中调用。
- ReceiveTickAI:AI版本的tick函数。如果条件符合会在TickTask()中调用。
- ReceiveExecute:通用版本的Execute函数。如果条件符合会在ExecuteTask()中调用。
- ReceiveAbort:通用版本的Abort函数。如果条件符合会在AbortTask()中调用。
- ReceiveExecuteAI:AI版本的Execute函数。如果条件符合会在ExecuteTask()中调用。
- ReceiveAbortAI:AI版本的Abort函数。如果条件符合会在AbortTask()中调用。
### RunBehavior 与 RunBehaviorDynamic
可以加载其他行为树并且运行。
- RunBehavior的一个限制是在运行时不能改变该子树资源。存在该限制的原因是给子树的根等级装饰器节点提供支持这些装饰器节点将注入到父树中。此外在运行时不能修改运行树的结构。
- RunBehaviorDynamic使用行为树组件上的SetDynamicSubtree 函数即可以在运行时分配子树资源。本函数不会为子树的根等级装饰器节点提供支持。
## EQS
大致流程:
通过生成器在场景生成检测点获取当前位置的各种数据之后再通过条件判断获取想要的查询结果。EQS的一些使用范例包括找到最近的回复剂或弹药、判断出威胁最大的敌人或者找到能看到玩家的视线下面就会显示这样的一个示例
### 在Actor类中使用EQS
结合原生代码使用EQS
虽然EQS查询通常是在行为树中运行但也可以直接从原生代码使用它。以下示例展示了一个虚构的查询要在指定区域内为角色或物品寻找安全生成地点
```
// 以下名称必须与查询中使用的变量名一致
static const FName SafeZoneIndexName = FName(TEXT("SafeZoneIndex"));
static const FName SafeZoneRadiusName = FName(TEXT("SafeZoneRadius"));
// 运行查询,根据区域索引和安全半径寻找安全的生成点
bool AMyActor::RunPlacementQuery(const UEnvQuery* PlacementQuery)
{
if (PlacementQuery)
{
// 设置查询请求
FEnvQueryRequest QueryRequest(PlacementQuery, this);
// 设置查询参数
QueryRequest.SetIntParam(SafeZoneIndexName, SafeZoneIndexValue);
QueryRequest.SetFloatParam(SafeZoneRadiusName, SafeZoneRadius);
// 执行查询
QueryRequest.Execute(EEnvQueryRunMode::RandomBest25Pct, this, &AFortAthenaMutator_SpawningPolicyBase::OnEQSSpawnLocationFinished);
// 返回true说明查询已开始
return true;
}
// 返回false说明查询未能开始
return false;
}
```