第五章 Enemy AI
2-Perparing Enemy For Combat(为战斗准备敌人)
- 现在我们主要解决两件事,一件是AI控制器,一件是行为树
- AI拥有AI避障、AI感官、设置团队ID区分敌我
- 行为树要讨论的事件
- AI控制器要讨论的事件
- 在讨论AI回避之前,为了让它正常工作,我们必须获取其中的一个属性
- 为了回避功能正常,我们需要准确反应胶囊体半径,将之前调过胶囊体的大小恢复成默认,网格尺寸调整就行,调胶囊体尺寸要调整他的高与宽,不要使用缩放
3-Crowd Following Component(人群跟随组件) ps.这集理论避障
- AI回避的构建方法两种
- RVO(反向速度障碍)避障的两种方法
- 使用代理远离彼此:使用次方法会忽略障碍物和导航网格,因此当在移动的时候,AI很可能脱离导航网格边界体积,导致卡在某个位置
- 使用代理远离彼此:使用次方法会忽略障碍物和导航网格,因此当在移动的时候,AI很可能脱离导航网格边界体积,导致卡在某个位置
- Detour Crowd Avoidance(绕道人群回避)
- 该方法知道其他的代理,会生成一条新的路径来绕过彼此,而且还知道我们导航网格的大小
- 该方法知道其他的代理,会生成一条新的路径来绕过彼此,而且还知道我们导航网格的大小
- 新建一个AI控制器,然后重写这个组件类,因此我们不需要使用这个新的路径跟踪组件,而是需要使用的他的子类,这意味着,我们需要重写WarriorAIController类中的此类
- 然后创建一个子蓝图添加到敌人身上
4-AI Perception
- 添加感官系统
- 头文件
- #include “Perception/AIPerceptionComponent.h”
- #include “Perception/AISenseConfig_Sight.h”
- 委托函数的参数变量是从这里来的
5-Generic Team ID
- 设置团队编号,重写IGenericTeamAgentInterface接口中的GetTeamAttitudeTowards函数,这里这个函数记得加Override
- 然后在角色控制器中设置角色的团队属性,这里这个函数记得加Override
- 最后在AI感官更新信息的回调函数中测试一下信息
6-Behavior Tree
- 行为树,这节内容不怎么多,看看即可
7-Configure AI Avoidance(配置AI避障)ps.这集需要多研究
- 使用
AI.Crowd.DebugSelectedActors 1
命令可以查看避障调试绘图 - 新建三个变量用来控制AI避障状态
- 在BeginPlay中设置,避障的参数
- 这几个参数与回避质量最相关的值,具体每个参数的作用看视频,上面这个数组对应我们代码中的那四个质量,我们使用最高质量所以调节Index[3],这里设置为0,当AI到达不了角色位置就会停下来
8-Behavior Tree Node Types
- 虽然绕道回避设置很不错了,但它仍然不能神奇地解决我们在战斗中可能遇到的所有问题。
- 这节课就写了一个获取玩家距离敌人的服务蓝图任务
9-Observer Aborts(观察中止 )
- 首先讨论两个问题,一个是是否进入扫射,一个是判断自己是否离角色远了
- None:无
- Self:当结果为假时中止自己
- Lower Priority:当结果为真时,中止其他优先级低的分支
- Both:以上两点都有
- 具体看视频
10-Orient To Target Actor(瞄准目标Actor)
- 新建一个BTService类来瞄准目标Actor
- UBTService_OrientToTargetActor.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "AI/BTService_OrientToTargetActor.h"
UBTService_OrientToTargetActor::UBTService_OrientToTargetActor()
{
// 设置蓝图中显示的服务节点名称
NodeName = TEXT("Native Orient Rotation To Target Actor");
// 初始化服务节点的通知标志,确保该行为树服务能够正常工作并响应事件
INIT_SERVICE_NODE_NOTIFY_FLAGS();
RotationInterpSpeed = 5.f;//设置旋转插值速度,默认为5.0f。这个值控制角色朝向目标时的旋转速率
Interval = 0.f;//这是父类中的变量,定义服务的后续节拍之间的时间跨度
RandomDeviation = 0.f;//这是父类中的变量,为服务的间隔添加随机范围
// 添加对象过滤器,确保InTargetActorKey只能选择AActor类型的对象
InTargetActorKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(ThisClass, InTargetActorKey),
AActor::StaticClass());
}
void UBTService_OrientToTargetActor::InitializeFromAsset(UBehaviorTree& Asset)
{
Super::InitializeFromAsset(Asset);
// 获取行为树关联的黑板资产
if (UBlackboardData* BBAsset = GetBlackboardAsset())
{
// 解析所选黑板键,确保InTargetActorKey指向正确的黑板键
InTargetActorKey.ResolveSelectedKey(*BBAsset);
}
}
FString UBTService_OrientToTargetActor::GetStaticDescription() const
{
// 获取选定黑板键的名称描述
const FString KeyDescription = InTargetActorKey.SelectedKeyName.ToString();
// 返回静态描述字符串,包括选定黑板键和默认服务描述
return FString::Printf(TEXT("Orient Rotation To %s Key %s"), *KeyDescription,
*GetStaticServiceDescription());
}
- 重写TickNode函数
- 设置朝目标转向逻辑
- 行为树
11-Environment Query System
- 这集将EQS理论知识,可以多看看
12-Custom Query Context
- EQS使用知识,可以多看看
13-Toggle Strafing State
- 新建一个基类任务,后续敌人的任务可以继承这个,方便修改
- 基础这个基类任务,重写执行任务的函数,进行修改AI的扫射状态,当打开这个状态的的时候,就为其设置移动速度添加拥有其标签,否则就当前最大速度与黑板键的速度是否一样,进行设置速度,然后删除其拥有标签
- 在控制器中设置黑板键的默认速度
- 行为树逻辑
14-Calculate Direction(动画蓝图方面的C++硬编码)
- 计算运动的方向,首先添加这个模块
- 添加变量计算方向
- 在基类动画实例中创建一个检测是否拥有具体指定的此标签的函数
- 在敌人的基类动画蓝图中进行判断,有没有拥有扫射的标签进行切换不同的混合空间姿势
15-Strafing Blend Space
16-Compute Success Chance(计算成功几率)
- 流程
17-Dot Product Test(这集是关于避障的)
- 点积测试:点积背后的概念很简单,因此它比较两个向量的方向,然后您将返回这两个向量的差异程度,如果它们完全相反,则点积将返回-1的值,如果它们垂直,则返回值为0,如果它们平行,则返回值1
18-Enemy Melee Ability
- 看看就行
19-Activate Ability By Tag
- 在UWarriorAbilitySystemComponent类中新建一个用来激活能力的函数
- 激活能力
- 注意的问题
20-Is Target Pawn Hostile
- 在UWarriorFunctionLibrary类中添加一个辅助函数,用来判断目标是否有敌意
- 然后在AWarriorWeaponBase类中修改一下之前的碰撞检测执行委托的事件函数,因为之前的会伤害到同伴,这次加了个检查敌意的辅助函数就不会攻击到同伴了
- 在UEnemyCombatComponent中重写父类的碰撞到目标函数,进行测试
- 结果
21-Notify Pawn Hostile
- 通知攻击碰撞
- 测试
22-Make Enemy Damage Effect Spec Handle
- 设置敌人的伤害效果规范处理,和角色的差不多的
- 然后在敌人的攻击GA里面创建一个函数用来造成伤害使用
23-Apply Enemy Damage
- 看看就行
24-Motion Warping(运动变形)
- 运动扭曲:动态调整角色的根部运动以与目标对齐
- 首先我们需要在项目里面打开Motion Warping这个插件
- 添加这个模块
MotionWarping
- 创建这个组件
- 在蒙太奇中创建这个运动扭曲的状态通知,然后设置转向目标名字,使用运动转向时在动画序列中一定要打开跟运动
25-Update Motion Warp Target
- 创建一个服务进行频率更新运动扭曲目标,跟着视频做有些小问题,下面是修改过的
- 注意点:行为树的这个内置节点可以让敌人先旋转后再进行下一步攻击任务,但是这个节点必须开启控制器旋转与关闭方向旋转
- 也就说这里是相反的,但是由于我们AI要进行扫射行走,所以我们不能使用这个内置节点进行旋转,此方案在这不可用
26-Construct Native BT Task(ps.定义任务节点的内存结构)
- 构建原生BT任务
- UBTTask_RotateToFaceTarget:
- 为什么要为任务节点结构分配内存:是为了确保每个行为树实例在运行时有独立的状态数据,这在多线程或多实例环境中尤为重要。
- 结果
27-Rotate Enemy In Task
- 重写Execute Task与TickTask函数,添加一个用来判断是否到达旋转精度的辅助函数
- 总结
BTTask_RotateToFaceTarget.h
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_RotateToFaceTarget.generated.h"
// 定义任务节点的内存结构
struct FRotateToFaceTargetTaskMemory
{
// 使用弱对象指针存储拥有者 Pawn 和目标演员,避免循环引用导致的内存泄漏
TWeakObjectPtr<APawn> OwningPawn;
TWeakObjectPtr<AActor> TargetActor;
// 检查两个指针是否都有效
bool IsValid() const
{
return OwningPawn.IsValid() && TargetActor.IsValid();
}
// 重置两个指针
void Reset()
{
OwningPawn.Reset();
TargetActor.Reset();
}
};
/**
*
*/
UCLASS()
class WARRIOR_API UBTTask_RotateToFaceTarget : public UBTTaskNode
{
GENERATED_BODY()
UBTTask_RotateToFaceTarget();
//~ Begin UBTNode Interface
virtual void InitializeFromAsset(UBehaviorTree& Asset) override;
virtual uint16 GetInstanceMemorySize() const override;
virtual FString GetStaticDescription() const override;
//~ End UBTNode Interface
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
//判断是否到达精度
bool HasReachedAnglePercision(APawn* QueryPawn, AActor* TargetActor) const;
// 角度精度,当角色朝向与目标之间的角度差小于该值时认为已正确朝向
UPROPERTY(EditAnywhere,Category = "Face Target")
float AnglePrecision;
// 旋转插值速度,控制角色旋转到目标方向的速度
UPROPERTY(EditAnywhere, Category = "Face Target")
float RotationInterpSpeed;
// 黑板键选择器,用于从黑板中获取目标演员
UPROPERTY(EditAnywhere, Category = "Face Target")
FBlackboardKeySelector InTargetToFaceKey;
};
BTTask_RotateToFaceTarget.cpp
// Fill out your copyright notice in the Description page of Project Settings.
#include "AI/BTTask_RotateToFaceTarget.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "AIController.h"
#include "Kismet/KismetMathLibrary.h"
UBTTask_RotateToFaceTarget::UBTTask_RotateToFaceTarget()
{
NodeName = TEXT("Native Rotate To Face Target Actor");// 设置蓝图中显示的任务节点名称
// 设置默认的角度精度和旋转插值速度
AnglePrecision = 10.f;
RotationInterpSpeed = 5.f;
bNotifyTick = true; // 启用 Tick 通知,使得任务可以在每帧更新
bNotifyTaskFinished = true; // 启用任务完成通知,以便在任务完成后收到通知
bCreateNodeInstance = false; // 确保不会创建节点实例(通常不需要更改)
// 初始化任务节点的通知标志,确保该行为树服务能够正常工作并响应事件
INIT_TASK_NODE_NOTIFY_FLAGS();
// 添加对象过滤器,确保InTargetActorKey只能选择AActor类型的对象
InTargetToFaceKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(ThisClass, InTargetToFaceKey),
AActor::StaticClass());
}
void UBTTask_RotateToFaceTarget::InitializeFromAsset(UBehaviorTree& Asset)
{
Super::InitializeFromAsset(Asset);
// 获取行为树关联的黑板资产,并解析所选黑板键
if (UBlackboardData* BBAsset = GetBlackboardAsset())
{
InTargetToFaceKey.ResolveSelectedKey(*BBAsset);
}
}
uint16 UBTTask_RotateToFaceTarget::GetInstanceMemorySize() const
{
return sizeof(FRotateToFaceTargetTaskMemory);// 返回任务节点内存结构的大小,用于分配内存
}
FString UBTTask_RotateToFaceTarget::GetStaticDescription() const
{
// 获取选定黑板键的名称描述
const FString KeyDescription = InTargetToFaceKey.SelectedKeyName.ToString();
// 返回静态描述字符串,包括选定黑板键和角度精度信息
return FString::Printf(TEXT("Smoothly rotates to face %s Key until the angle precision %s is reached"),
*KeyDescription, *FString::SanitizeFloat(AnglePrecision));
}
EBTNodeResult::Type UBTTask_RotateToFaceTarget::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
// 从黑板组件中获取目标演员的对象引用
UObject* ActorObject = OwnerComp.GetBlackboardComponent()->GetValueAsObject(InTargetToFaceKey.SelectedKeyName);
AActor* TargetActor = Cast<AActor>(ActorObject);
APawn* OwningPawn = OwnerComp.GetAIOwner()->GetPawn();
// 将节点内存转换为任务特定的内存结构
FRotateToFaceTargetTaskMemory* Memory = CastInstanceNodeMemory<FRotateToFaceTargetTaskMemory>(NodeMemory);
check(Memory);
// 设置内存中的拥有者 Pawn 和目标演员
Memory->OwningPawn = OwningPawn;
Memory->TargetActor = TargetActor;
if (!Memory->IsValid())
{
return EBTNodeResult::Failed;
}
if (HasReachedAnglePercision(OwningPawn, TargetActor))// 如果已经达到了角度精度要求,则重置内存并返回成功结果
{
Memory->Reset();
return EBTNodeResult::Succeeded;
}
return EBTNodeResult::InProgress;// 否则,返回正在执行的结果,表示任务还在进行中
}
void UBTTask_RotateToFaceTarget::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
// 将节点内存转换为任务特定的内存结构
FRotateToFaceTargetTaskMemory* Memory = CastInstanceNodeMemory<FRotateToFaceTargetTaskMemory>(NodeMemory);
if (!Memory->IsValid())
{
FinishLatentTask(OwnerComp, EBTNodeResult::Failed);
}
// 如果已经达到了角度精度要求,则重置内存并结束任务,返回成功结果
if (HasReachedAnglePercision(Memory->OwningPawn.Get(), Memory->TargetActor.Get()))
{
Memory->Reset();
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
}
else
{
// 使用插值函数平滑地将当前旋转调整为目标朝向旋转
const FRotator LookAtRot = UKismetMathLibrary::FindLookAtRotation(Memory->OwningPawn->GetActorLocation(),
Memory->TargetActor->GetActorLocation());
const FRotator TargetRot = FMath::RInterpTo(Memory->OwningPawn->GetActorRotation(), LookAtRot,
DeltaSeconds, RotationInterpSpeed);
Memory->OwningPawn->SetActorRotation(TargetRot);
}
}
bool UBTTask_RotateToFaceTarget::HasReachedAnglePercision(APawn* QueryPawn, AActor* TargetActor) const
{
const FVector OwnerForward = QueryPawn->GetActorForwardVector();
// 获取从查询对象到目标演员的归一化方向向量
const FVector OwnerToTargetNormalized =
(TargetActor->GetActorLocation() - QueryPawn->GetActorLocation()).GetSafeNormal();
// 计算两个向量之间的点积
const float DotResult = FVector::DotProduct(OwnerForward, OwnerToTargetNormalized);
// 通过反余弦函数计算角度差(以度为单位)
const float AngleDiff = UKismetMathLibrary::DegAcos(DotResult);
// 返回角度差是否小于等于设定的角度精度
return AngleDiff <= AnglePrecision;
}
测试结果
- 行为树逻辑
28-Melee Attack Branch
- 关于为什么是这个逻辑,多看看视频
- 行为树逻辑
29-Does Actor Have Tag Decorator
- 添加一个被攻击状态下的的标签
- 新建一个装饰器,用来判断当前Actor有没有标签
30-Duration Gameplay Effect(虚幻5.4与虚幻5.3有改动位置)
- 这集版本之间有改动位置,可以多看看视频
- 这节课讨论向敌人添加受到攻击的状态标签,用GE进行添加,因为在敌人受到伤害的时候就添加标签,不在受到伤害的时候就将其删除
- 在受击的GA中应用此GE
- 行为树逻辑
- 运行结果
31-Should Abort All Logic
- 添加一个装饰器用来判断玩家或者AI是否死亡,死亡就不在执行AI行为树逻辑
32-Guardian Attack Sound FX
- 添加音效,看看就行