虚幻c++ 敌人AI
xiuxiu学习视频地址:5-1. 敌人AI_哔哩哔哩_bilibili
章节内容
5.2 Preparing Enemy For Combat

- 敌人AI控制器
- 行为树

- AI避让
- 感知
- 通用团队ID

调整胶囊体半径为真实半径

之前胶囊体进行了1.5倍数缩放,调整为1,然后放大网格体倍数,从1改为1.5

重新调整胶囊体高度和半径,改为97.5和67.5

移动网格体使胶囊体覆盖

虚幻引擎中为什么胶囊体一定要是真实半径,而不能进行scale缩放(AI回答)
在虚幻引擎(Unreal Engine)中,胶囊体碰撞体(Capsule Component) 的设计确实限制了不能通过 非均匀缩放(non-uniform scale) 或甚至 均匀缩放(uniform scale) 来改变其物理尺寸,而必须通过直接修改其 半径(Radius) 和 高度(Half Height) 属性来调整大小。这背后有几个关键原因:
1. 物理模拟的稳定性与准确性
- 胶囊体常用于角色的碰撞检测(如 Character Movement Component),是物理系统(如 PhysX)中的基本几何形状之一。
- 如果允许通过缩放(尤其是非均匀缩放)改变胶囊体,会导致:
- 碰撞形状不再是标准胶囊体(可能变成椭球或其他奇怪形状);
- 物理计算变得复杂甚至不可靠;
- 可能引发穿透、抖动或移动异常等问题。
2. 性能优化考虑
- 虚幻引擎和底层物理引擎(如 Chaos 或 PhysX)对胶囊体做了大量优化,假设其始终是“规范形式”(canonical form)。
- 如果允许缩放,引擎就需要在每次物理更新时将缩放矩阵应用到胶囊体上,再进行碰撞检测——这会显著降低性能,并破坏缓存友好性。
3. 角色移动组件(CharacterMovementComponent)的依赖
-
在角色类(Character)中,默认使用 CapsuleComponent 作为根组件,CharacterMovementComponent 直接读取其 CapsuleRadius 和 CapsuleHalfHeight 来进行地面检测、台阶攀爬、斜坡处理等逻辑。
-
这些逻辑代码 不读取组件的缩放(Scale),只信任 Radius/Height 属性。
-
如果你通过缩放改变了视觉大小,但没改 Radius/Height,角色可能会“穿模”、卡住、无法正确跳跃或落地。
如果你想改变胶囊体的大小,请直接修改其属性或在蓝图中使用 Set Capsule Size 节点:
// C++
MyCharacter->GetCapsuleComponent()->SetCapsuleSize(NewRadius, NewHalfHeight);
5.3 Crowd Following Component
AI避让

- RVO Avoidance
对每个邻居算一个“会撞上的速度锥” → 平移成互惠版本(RVO)→ 在速度圆里找一个不进任何锥的最优速度 → 移动
关键特点:
- 分布式计算:每个智能体只根据周围邻居的位置和速度做局部决策,不需要全局规划。
- 实时高效:计算量小,适合大量单位同时避障(如 RTS 游戏中的百人军队)。
- 无中心协调:不需要中央控制器,天然支持动态增减智能体。
- 避免死锁:在对称场景(如两人对冲)中也能自然解耦。
- Detour Crowd Avoidance
在已有导航网格(NavMesh)基础上,让多个 AI 智能体在移动过程中动态避开彼此,避免“穿模”或“挤成一团”
在沿着全局路径移动的过程中,每个智能体每帧(或每几帧)都会检测附近的其他智能体;
使用 Detour Crowd 的速度采样+评分机制 动态调整自己的瞬时速度,以避开邻居;

以AIController为父类创建C++类WarriorAIController,AIController中有PathFollowingComponent组件,需要重写该组件,用子组件CrowdFollowingComponent

#include "CoreMinimal.h"
#include "AIController.h"
#include "WarriorAIController.generated.h"
/*
*
*/
UCLASS()
class WARRIOR_API AWarriorAIController public AAIController
{
GENERATED_BODY()
public:
AWarriorAIController(const FObjectInitializer&ObjectInitializer);
}
#include "Controllers/WarriorAIController.h"
#include "Navigation/CrowdFollowingComponent.h"
#include "WarriorDebugHelper.h"
AWarriorAIController:AWarriorAIController(const FobjectInitializer&ObjectInitializer)
:Super(ObjectInitializer.SetDefaultSubobjectclass<UCrowdFollowingComponent("PathFollowingComponent"))
{
if (UCrowdFoLlowingComponent*CrowdComp Cast<uCrowdFollowingComponent>(GetPathFoLlowingComponent())){
Debug:Print(TEXT("CrowdFollowingComponent valid"),FColor:Green);
}
}
AAIController 的构造函数(即 Super())内部会创建所有默认子对象,如果你不在传给 Super() 的 ObjectInitializer 中提前注册替换规则,父类就会用自己的默认类型(UPathFollowingComponent)创建组件,一旦创建完成,就无法再替换了
SetDefaultSubobjectClass,这是 Unreal Engine 官方推荐且唯一可靠的方式来替换父类的默认子对象类型。
创建WarriorAIController子类蓝图AIC_Enemy_Base,AIC_Enemy_Base创建子类蓝图AIC_Guardian并赋给BP_Gruntling_Guardian进行测试,屏幕会打印两条调试信息



5.4 AI Perception
在AWarriorAIController中添加属性UAIPerceptionComponent、UAISenseConfig_Sight
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "WarriorAIController.generated.h"
class UAIPerceptionComponent;
class UAISenseConfig_Sight;
/**
*
*/
UCLASS()
class WARRIOR_API AWarriorAIController : public AAIController
{
GENERATED_BODY()
public:
AWarriorAIController(const FObjectInitializer& ObjectInitializer);
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere,BlueprintReadOnly)
UAIPerceptionComponent* EnemyPerceptionComponent;
UPROPERTY(VisibleAnywhere,BlueprintReadOnly)
UAISenseConfig_Sight* AISenseConfig_Sight;
};
UAIPerceptionComponent 和 UAISenseConfig_Sight 是 AI 感知系统(AI Perception System)中的两个核心类,用于实现 AI 角色对周围环境的“感知”能力,比如“看到”、“听到”其他角色
-
UAIPerceptionComponent
这是挂载在 AI 控制器(AIController)或 Pawn 上的一个组件,用于管理 AI 的所有感知能力。它充当“感官中枢”,可以配置多种感知类型(如视觉、听觉等),并接收来自这些感官的刺激,然后触发相应的事件(如发现目标、失去目标等)。
主要功能:
注册和管理不同类型的感知配置(如视觉、听觉)。
监听环境中由 UAISense(如 UAISense_Sight)发出的感知事件。
提供委托(Delegates)让开发者响应感知变化,例如:
OnPerceptionUpdated:当感知信息更新时触发。
OnTargetPerceptionUpdated:当特定目标的感知状态改变时触发。
可以查询当前 AI “感知到了谁”以及感知的强度、位置等信息。
-
UAISenseConfig_Sight
这是专门用于配置 AI 视觉感知(Sight)行为的类。它定义了 AI 的“视力”参数,比如视野角度、最大/最小可视距离、是否忽略障碍物、是否只感知可见的敌人等。
#include "Controllers/WarriorAIController.h"
#include "Navigation/CrowdFollowingComponent.h"
#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig_Sight.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "WarriorDebugHelper.h"
AWarriorAIController::AWarriorAIController(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UCrowdFollowingComponent>("PathFollowingComponent"))
{
AISenseConfig_Sight = CreateDefaultSubobject<UAISenseConfig_Sight>("EnemySenseConfig_Sight");
AISenseConfig_Sight->DetectionByAffiliation.bDetectEnemies = true;
AISenseConfig_Sight->DetectionByAffiliation.bDetectFriendlies = false;
AISenseConfig_Sight->DetectionByAffiliation.bDetectNeutrals = false;
AISenseConfig_Sight->SightRadius = 5000.f;
AISenseConfig_Sight->LoseSightRadius = 0.f;
AISenseConfig_Sight->PeripheralVisionAngleDegrees = 360.f;
EnemyPerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>("EnemyPerceptionComponent");
EnemyPerceptionComponent->ConfigureSense(*AISenseConfig_Sight);
EnemyPerceptionComponent->SetDominantSense(UAISenseConfig_Sight::StaticClass());
EnemyPerceptionComponent->OnTargetPerceptionUpdated.AddUniqueDynamic(this,&ThisClass::OnEnemyPerceptionUpdated);
}
AISenseConfig_Sight关键属性:
1. DetectionByAffiliation.bDetectEnemies = true;
作用:允许 AI 感知到 敌对单位(Enemies)
2. DetectionByAffiliation.bDetectFriendlies = false;
作用:禁止 AI 感知 友军(Friendlies)
3. DetectionByAffiliation.bDetectNeutrals = false;
作用:禁止 AI 感知 中立单位(Neutrals)
4. SightRadius = 5000.f;
作用:AI 的最大可视距离为 5000 单位
5. LoseSightRadius = 0.f;
作用:当目标离开 SightRadius 后,立即丢失视野
6. PeripheralVisionAngleDegrees = 360.f;
作用:AI 拥有 360° 全向视野
SetDomainantSense是指定 AI 的 “主导感知类型”
在引擎源码中,UAIPerceptionComponent::OnTargetPerceptionUpdated 的声明是
// Engine Source
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FPerceptionUpdatedDelegate, AActor*, Actor, FAIStimulus, Stimulus);
UPROPERTY(BlueprintAssignable)
FPerceptionUpdatedDelegate OnTargetPerceptionUpdated;
所以在AWarriorAIController中添加
virtual void OnEnemyPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)用于绑定
UFUNCTION()
virtual void OnEnemyPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
1. AActor* Actor
- 含义:触发感知事件的目标 Actor(即“被看见/被听到”的那个对象)
2. FAIStimulus Stimulus
这是一个结构体,包含 感知事件的详细信息。关键字段如下:
| 成员 | 说明 |
|---|---|
Stimulus.Type |
感知类型(如 UAISense_Sight::StaticClass()) |
Stimulus.WasSuccessfullySensed() |
是否成功感知到? ✅ true = 刚看到 / 听到❌ false = 刚丢失目标 |
Stimulus.Strength |
刺激强度(视觉通常为 1.0;听觉随距离衰减) |
Stimulus.Location |
刺激发生的位置(比如声音来源点,或目标当前位置) |
Stimulus.Tag |
可选标签(可用于自定义分类,如 "Gunshot", "Footstep") |
Stimulus.ExpirationAge |
刺激多久后过期(用于记忆衰减) |

感知组件基础设置完成
5.5 Generic TeamID
WarriorAIController重写AAIController接口IGenericTeamAgentInterface的GetTeamAttitudeTowards方法
//~ Begin IGenericTeamAgentInterface Interface.
virtual ETeamAttitude::Type GetTeamAttitudeTowards(const AActor& Other) const override;
//~ End IGenericTeamAgentInterface Interface
ETeamAttitude::Type AWarriorAIController::GetTeamAttitudeTowards(const AActor& Other) const
{
const APawn* PawnToCheck = Cast<const APawn>(&Other);
const IGenericTeamAgentInterface* OtherTeamAgent = Cast<const IGenericTeamAgentInterface>(PawnToCheck->GetController());
if (OtherTeamAgent && OtherTeamAgent->GetGenericTeamId() != GetGenericTeamId())
{
return ETeamAttitude::Hostile;
}
return ETeamAttitude::Friendly;
}
IGenericTeamAgentInterface::GetTeamAttitudeTowards 的作用
该方法用于 定义当前 AI 代理(Agent)对另一个代理的“团队态度”,即:
“我对这个其他角色是友方、敌方,还是中立?”
在AWarriorAIController构造函数中添加GenericTeamID
SetGenericTeamId(FGenericTeamId(1));
在AWarriorHeroController上添加接口IGenericTeamAgentInterface,创建构造函数,HeroTeamID属性,重写GetGenericTeamId()
UCLASS()
class WARRIOR_API AWarriorHeroController : public APlayerController, public IGenericTeamAgentInterface
{
GENERATED_BODY()
public:
AWarriorHeroController();
//~ Begin IGenericTeamAgentInterface Interface.
virtual FGenericTeamId GetGenericTeamId() const override;
//~ End IGenericTeamAgentInterface Interface
private:
FGenericTeamId HeroTeamID;
};
AWarriorHeroController::AWarriorHeroController()
{
HeroTeamID = FGenericTeamId(0);
}
FGenericTeamId AWarriorHeroController::GetGenericTeamId() const
{
return HeroTeamID;
}
测试
void AWarriorAIController::OnEnemyPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
if (Stimulus.WasSuccessfullySensed() && Actor)
{
Debug::Print(Acyor->GetActorNameOrLabel() + TEXT(" was sensed "), FColor::Green);
}
}
屏幕打印调试信息
按;,1、2禁用AI、行为树,5开启感知功能,可以看到敌人能看到玩家角色

5.6 Behavior Tree

一、行为树(Behavior Tree)
✅ 是什么?
行为树是一种 层次化的 AI 决策结构,用于控制 AI 如何根据当前环境和状态做出反应。它由一系列 节点(Nodes) 组成,通过树形结构从根节点向下执行,最终决定 AI 的行为(如巡逻、追击、攻击、逃跑等)。
核心节点类型
| 节点类型 | 作用 | 示例 |
|---|---|---|
| Composite(复合节点) | 控制子节点的执行逻辑 | Selector(选择器)、Sequence(序列) |
| Decorator(装饰器) | 附加条件,决定是否允许子节点运行 | “如果看见敌人”、“如果血量 < 50%” |
| Service(服务) | 在后台周期性运行的轻量逻辑 | 每秒更新最近敌人位置 |
| Task(任务) | 执行具体动作 | “移动到目标”、“播放攻击动画” |
二、黑板(Blackboard)
✅ 是什么?
黑板是一个 键值对(Key-Value)数据容器,用于在行为树、AI Controller、感知系统等模块之间 共享数据。
首先创建黑板BB_Enemy_Base,所有敌人共享一个黑板

创建行为树BT_Guardian

BB_Enemy_Base创建TargetActor键值对
行为树里添加序列Sequence节点、Move To任务节点,记得改BlackboardKey、Acceptable Radius,
当 AI 与目标的距离小于或等于Acceptable Radius时,Move To 任务即视为 成功完成

在AIC_Enemy_Base中添加运行行为树

在AIC_Guardian指定要运行的行为树BT_Guardian

接下来还需要设置TargetActor的值,在C++中完成
void AWarriorAIController::OnEnemyPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
if (Stimulus.WasSuccessfullySensed() && Actor)
{
if(UBlackboardComponent* BlackboardComponent = GetBlackboardComponent())
{
BlackboardComponent->SetValueAsObject(FName("TargetActor"), Actor);
}
}
}
最后在场景中添加导航网格体,按键P可视化导航网格体

5.7 Configure AI Avoidance
配置AI回避
控制台输入下面命令可以可视化避障过程(算法如何采样周围环境),需要按下F8键选中某个敌人
ai crowd DebugSelectedActors 1

AIC_Guardian中有PathFollowingComponent,但组件内的变量没有暴露给蓝图,只能在c++代码中调整,在BeginPlay()中调整
protected:
virtual void BeginPlay() override;
UPROPERTY(VisibleAnywhere,BlueprintReadOnly)
UAIPerceptionComponent* EnemyPerceptionComponent;
UPROPERTY(VisibleAnywhere,BlueprintReadOnly)
UAISenseConfig_Sight* AISenseConfig_Sight;
UFUNCTION()
virtual void OnEnemyPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus);
另外加了三个变量控制
UPROPERTY(EditDefaultsOnly, Category = "Detour Crowd Avoidance Config")
bool bEnableDetourCrowdAvoidance = true;
UPROPERTY(EditDefaultsOnly, Category = "Detour Crowd Avoidance Config", meta = (EditCondition = "bEnableDetourCrowdAvoidance",UIMin = "1",UIMax = "4"))
int32 DetourCrowdAvoidanceQuality = 4;
UPROPERTY(EditDefaultsOnly, Category = "Detour Crowd Avoidance Config", meta = (EditCondition = "bEnableDetourCrowdAvoidance"))
float CollisionQueryRange = 600.f;
void AWarriorAIController::BeginPlay()
{
Super::BeginPlay();
if (UCrowdFollowingComponent* CrowdComp = Cast<UCrowdFollowingComponent>(GetPathFollowingComponent()))
{
CrowdComp->SetCrowdSimulationState(bEnableDetourCrowdAvoidance? ECrowdSimulationState::Enabled : ECrowdSimulationState::Disabled);
switch (DetourCrowdAvoidanceQuality)
{
case 1: CrowdComp->SetCrowdAvoidanceQuality(ECrowdAvoidanceQuality::Low); break;
case 2: CrowdComp->SetCrowdAvoidanceQuality(ECrowdAvoidanceQuality::Medium); break;
case 3: CrowdComp->SetCrowdAvoidanceQuality(ECrowdAvoidanceQuality::Good); break;
case 4: CrowdComp->SetCrowdAvoidanceQuality(ECrowdAvoidanceQuality::High); break;
default:
break;
}
CrowdComp->SetAvoidanceGroup(1);
CrowdComp->SetGroupsToAvoid(1);
CrowdComp->SetCrowdCollisionQueryRange(CollisionQueryRange);
}
}
1.
SetCrowdSimulationState(ECrowdSimulationState)作用:启用或禁用 群体模拟(Crowd Simulation) 功能。
说明:
- 当设为
ECrowdSimulationState::Enabled,该 AI 会参与 Recast & Detour 的 Crowd Manager 管理的动态避障系统。- 禁用后,AI 仍可寻路(使用普通
PathFollowingComponent行为),但不会与其他 Crowd Agent 实时避让,也不会被其他 Agent 避让。2.
SetCrowdAvoidanceQuality(ECrowdAvoidanceQuality)作用:设置当前 Agent 所属的 避障分组(Avoidance Group)。
SetAvoidanceGroup(uint8 Group)作用:设置当前 Agent 所属的 避障分组
4.
SetGroupsToAvoid(uint8 Groups)作用:设置当前 Agent 需要避让的其他避障分组
5.
SetCrowdCollisionQueryRange(float Range)作用:设置 避障检测的感知半径(单位:厘米)
调整PathFollowingComponent组件变量为什么在BeginPlay中更好,而不是在构造函数中?
1. 在构造函数中PathFollowingComponent 可能尚未创建或初始化
2. BeginPlay() 保证运行时上下文完整
3.BeginPlay()执行在所有初始化(包括蓝图)之后,能覆盖最终生效的值,或基于蓝图配置做进一步调整。4. 避免编辑器中的副作用
BeginPlay()仅在游戏运行时调用,完全隔离编辑器环境,更安全
避障设置已启用,可以改设置看不同效果
但是如果有第三个AI被两个AI挡住,会在那旋转
为什么会这样?
UE 的 Crowd 避障基于 Reciprocal Velocity Obstacles (RVO) 或其变种(如 HRVO)。
它的核心思想是:“在不与邻居碰撞的前提下,选择一个最接近目标方向的速度。”
但当 多个 Agent 形成包围/夹击 时:
- 当前 AI 的所有“安全速度”都远离目标方向;
- 它可能被迫选择 向左绕 → 但左边 AI 也想动 → 又被挡;
- 下一帧又尝试 向右绕 → 右边也被挡;
- 结果:左右横跳、原地转圈、或缓慢旋转调整朝向(因为移动速度为0,但 Rotation 仍在更新)
在项目设置中还有参数可以调整

5.8 Behavior Tree Node
希望敌人分散靠近玩家

行为树四种类型节点

复合节点

最常用的选择节点和顺序节点

顺序节点(Sequence)
✅ 执行逻辑:
从左到右依次执行子节点,一旦某个子节点返回
Failure,立即停止并返回Failure;只有所有子节点都成功,整体才成功。
选择节点(Selector)
✅ 执行逻辑:
从左到右依次执行子节点,一旦某个子节点返回
Success(成功),立即停止并返回Success;如果子节点返回Failure(失败),则继续尝试下一个。
装饰节点

可以附加到复合节点上,定义分支能否执行
服务节点

| 节点类型 | 主要作用 |
|---|---|
| 装饰器(Decorator) | 控制“是否允许”某个子节点执行 —— 相当于 if 条件判断 |
| 服务(Service) | 在节点激活期间“持续运行后台逻辑” —— 相当于 while (active) { ... } |
任务节点

叶子节点,执行具体行为

在这里可以自定义节点,创建BTService文件夹和BTService_GetDistToTarget

| 函数 | 调用时机 | 是否提供 AIController | 推荐使用场景 |
|---|---|---|---|
ReceiveActivation |
节点激活时(一次) | ❌ 需手动获取 | 通用行为树 |
ReceiveActivationAI |
节点激活时(一次) | ✅ 直接提供 | 标准 AI 开发(推荐) |
ReceiveDeactivation |
节点停用时(一次) | ❌ | 通用清理 |
ReceiveDeactivationAI |
节点停用时(一次) | ✅ | AI 相关清理(推荐) |
ReceiveTick |
Service 每次 tick | ❌ | 通用后台逻辑 |
ReceiveTickAI |
Service 每次 tick | ✅ | AI 感知/更新(推荐) |
💡 最佳实践:
只要你的 AI 是基于
AAIController的(99% 的情况都是),就优先使用带AI后缀的函数。
根据输入的TargetActor的Key(黑板中存储有TargetActor)获取与目标Actor的距离,然后保存在黑板中
添加服务,修改键值,记得要在黑板中添加DistToTarget来存储距离,还可以设置执行频率

在场景中移动可以看到值的变化

5.9 Observer Aborts
添加一个顺序节点,并在上面加一个装饰节点,判断距离条件(距离大于等于600)

后面再加一个任务节点,移动至TargetActor

再添加一个分支,使用选择节点,并在上面加一个装饰节点,判断距离条件(距离小于600),添加一个等待任务来验证分支有效性


| 选项 | 触发条件 |
|---|---|
| On Value Change | 只要黑板键的值变了,就触发重新评估(不管条件结果是否变化) |
| On Result Change | 只有当装饰器的判断结果(True/False)发生变化时,才触发重新评估 |
| 选项 | 说明 |
|---|---|
| None | 默认。只在尝试执行该节点时检查条件(不会中断) |
| Self | 当条件变为 true 时,中断当前正在执行的子树(包括自己),重新评估 |
| Lower Priority | 当条件变为 true 时,中断所有优先级比它低的兄弟节点(最常用!) |
| Both | 两者都启用 |
可以调整Observer Oborts在项目中看效果
5.10 Orient To Target Actor
添加两个顺序节点,一个是攻击,一个是侧移

因为需要每帧调整敌人朝向,高密集型用c++创建,创建一个BTService类(BTService_OrientToTargetActor),BTService_BlackboardBase提供了一些代码黑板专用基类,BTService_BlueprintBase不能被继承

UCLASS()
class WARRIOR_API UBTService_OrientToTargetActor : public UBTService
{
GENERATED_BODY()
UBTService_OrientToTargetActor();
//~ Begin UBTNode Interface
virtual void InitializeFromAsset(UBehaviorTree& Asset) override;
virtual FString GetStaticDescription() const override;
//~ End UBTNode Interface
UPROPERTY(EditAnywhere, Category = "Target")
FBlackboardKeySelector InTargetActorKey;
UPROPERTY(EditAnywhere, Category = "Target")
float RotationInterpSpeed;
};
UBTService_OrientToTargetActor::UBTService_OrientToTargetActor()
{
NodeName = TEXT("Native Orient Rotation To Target Actor");
INIT_SERVICE_NODE_NOTIFY_FLAGS();
RotationInterpSpeed = 5.f;
Interval = 0.f; //UBTService 的成员变量,每帧执行
RandomDeviation = 0.f;
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.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());
}
✅ 1.
INIT_SERVICE_NODE_NOTIFY_FLAGS();是什么?这是一个 宏(Macro),用于设置 Service 节点的 通知标志(Notify Flags)。
🔧 它的作用是:
告诉行为树系统:“这个 Service 需要在哪些生命周期事件中被通知”。
展开后大致等价于:
bNotifyTick = true; // 需要 Tick(因为你要重写 TickNode) bNotifyBecomeRelevant = false; bNotifyCeaseRelevant = false; // ... 其他标志// 如果你只写了 TickNode,但没设置 bNotifyTick = true virtual void TickNode(...) override { /* 这段代码永远不会运行! */ }3.
InTargetActorKey.AddObjectFilter(...)是做什么的?这是为了在 行为树编辑器(Blueprint Editor)中限制黑板键的选择范围。
🔍 背景:
FBlackboardKeySelector是一个结构体,用于让用户在编辑器中选择一个黑板键但黑板可能有多种类型的键:
Vector、Bool、Object、Class等你希望 只允许选择类型为
AActor*(或其子类)的黑板键🛠️
AddObjectFilter的作用:InTargetActorKey.AddObjectFilter( this, // 当前 UObject(用于反射) GET_MEMBER_NAME_CHECKED(ThisClass, InTargetActorKey), // 成员名(用于属性绑定) AActor::StaticClass() // 只允许 Object 类型且是 AActor 或其子类 );
除此之外还要重写TickNode方法
virtual void TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
void UBTService_OrientToTargetActor::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
UObject* ActorObject = OwnerComp.GetBlackboardComponent()->GetValueAsObject(InTargetActorKey.SelectedKeyName);
AActor* TargetActor = Cast<AActor>(ActorObject);
APawn* OwningPawn = OwnerComp.GetAIOwner()->GetPawn();
if (OwningPawn && TargetActor)
{
// 1. 计算从 AI 位置指向目标位置的旋转角度
const FRotator LookAtRot = UKismetMathLibrary::FindLookAtRotation(OwningPawn->GetActorLocation(),TargetActor->GetActorLocation());
// 2. 在当前旋转和目标旋转之间做时间平滑插值
const FRotator TargetRot = FMath::RInterpTo(OwningPawn->GetActorRotation(),LookAtRot,DeltaSeconds,RotationInterpSpeed);
// 3. 应用新旋转
OwningPawn->SetActorRotation(TargetRot);
}
}
添加等待任务测试

5.11 Envoroment Query System
环境查询系统(EQS)

EQS 由三个核心部分构成:
| 组件 | 作用 | 类比 |
|---|---|---|
| 1. 查询生成器(Generator) | 生成候选位置/Actor 列表 | “列出所有可能的藏身处” |
| 2. 测试(Tests) | 对每个候选打分或过滤 | “这个点是否在掩体后?视野好不好?” |
| 3. 查询(EnvQuery) | 整合 Generator + Tests,输出结果 | “综合评分,选出 Top 3 最佳点” |
创建一个EQS_Test

在 EQS 中,Context 决定了“以谁/以什么位置为参考点”来执行查询。

| Context 类型 | 作用 | 示例 |
|---|---|---|
Context_Self |
使用 执行查询的 AI 自身 作为参考 | “以我自己为中心找掩体” |
Context_Target |
使用 黑板中的目标 Actor 作为参考 | “以玩家位置为中心找包围点” |
Context_Origin |
使用 世界原点 (0,0,0) | 很少用 |
| 自定义 C++ Context | 任意位置或 Actor | “队伍中心点”、“最近队友位置” |
创建一个EQS测试组件,为了可视化

选择要可视化的eqs

可以去EQS_Test调整这两个属性看变化

添加Test,过滤失败的点

可以去测试不同的生成器看效果
5.12 Custom EQS Context
出现了一个问题,敌人不再面向玩家,敌人会看向EQS_TestPawn

改为OtherTeamAgent->GetGenericTeamId() < GetGenericTeamId(),玩家组别为0,敌人组别为0
ETeamAttitude::Type AWarriorAIController::GetTeamAttitudeTowards(const AActor& Other) const
{
const APawn* PawnToCheck = Cast<const APawn>(&Other);
const IGenericTeamAgentInterface* OtherTeamAgent = Cast<const IGenericTeamAgentInterface>(PawnToCheck->GetController());
if (OtherTeamAgent && OtherTeamAgent->GetGenericTeamId() < GetGenericTeamId())
{
return ETeamAttitude::Hostile;
}
return ETeamAttitude::Friendly;
}
修改AWarriorAIController::OnEnemyPerceptionUpdated,原先是每次看到都会注册一次TargetActor,现在是先看黑板里是否存储有TargetActor
void AWarriorAIController::OnEnemyPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus)
{
if (UBlackboardComponent* BlackboardComponent = GetBlackboardComponent())
{
if (!BlackboardComponent->GetValueAsObject(FName("TargetActor")))
{
if (Stimulus.WasSuccessfullySensed() && Actor)
{
BlackboardComponent->SetValueAsObject(FName("TargetActor"),Actor);
}
}
}
}
创建EQS_FindStrafingLocation,专门来找侧移位置

寻找中心应该以目标玩家为中心,创建一个EQSContext_TargetActor

EnvQueryContext_BlueprintBase 是 Unreal Engine 中用于环境查询系统的上下文基类,它允许你通过蓝图(Blueprint)自定义 EQS 查询中“谁”或“什么位置”作为参考点或目标源
Provide Actors Set |
TArray<AActor*> |
返回多个 Actor | 所有敌人、所有可交互物 |
Provide Location Set |
TArray<FVector> |
返回多个位置(坐标) | 多个掩体点、巡逻点 |
Provide Single Actor |
AActor* |
返回单个 Actor | 玩家、主目标 |
Provide Single Location |
FVector |
返回单个位置 | AI 自身位置、玩家脚下点 |
可以通过获取黑板来得到TargetActor


但是在场景中看不到选中的位置,因为EQSContext_TargetActor获取TargetActor只在运行中有效
改为Get Actor Of Class,这样在编辑器才可以看到,运行时要改为连接下面

可以加一些过滤条件

在BT_Guardian行为树中添加Run EQS Query任务节点,改Query Template为查找侧移点,模式改为前25%中随意选择,黑板中创建一个Key保存位置



然后再添加一个移动到任务节点

按单引号'键,选择4EQS类别,可以看到选中的点
记得测试时要改为玩家位置!!!

出现个问题:当玩家靠近墙壁时,会可能选取靠近墙壁的点

加一个过滤条件,需要离玩家至少480的节点才会被选取


5.13 Toggle Strafing State
创建一个BTTask_EnemyBase作为所有敌人任务的基类

添加函数OnEnemyExecuteTask,需要子类实现,执行任务后需手动操作任务成功!!!否则会卡住后续行为


无法获取敌方角色,要返回任务失败

创建基于BTTask_EnemyBase的任务BTTask_ToggleStrafingState

若ShouldEnable为True,关闭CharacterMovement的OrientRotationToMovement,在黑板设置了DefaultMaxWalkSpeed存储最大行走速度默认值,根据ShouldChangeMaxWalkSpeed设置最大行走速度

关闭
CharacterMovementComponent的bOrientRotationToMovement = false,原因如下:
🔍 核心原因:防止角色自动朝向移动方向旋转
✅ 默认行为(
bOrientRotationToMovement = true):
角色每帧都会 自动将自身朝向(Yaw)对齐到当前速度方向(Velocity)。
适用于普通跑动、追击等“面朝前进方向”的场景。
❌ 但侧移时的问题:
- 侧移的本质是:角色身体朝向一个方向(如面对敌人),但横向移动(左/右平移)。
- 如果此时
bOrientRotationToMovement = true:
- 角色会试图 把身体转向侧移的方向(比如向右平移 → 身体向右转 90°)
- 结果:角色背对或侧对目标,无法保持瞄准/注视敌人
在c++创建侧移状态
WARRIOR_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Enemy_Status_Strafing);
设置完最大行走速度后,还要添加侧移标签

ShouldEnable为Falses,重新开启OrientRotationToMovement,重新设置完最大行走速度后,移除侧移标签
行为树添加任务
获取角色的最大行走速度值设置到黑板上
使用showdebug abilitysystem看敌人是否带上标签

5.14 Calculate Direction
在WarriorCharacterAnimInstance添加属性LocomotionDirection记录侧移方向
UPROPERTY(VisibleDefaultsOnly, BlueprintReadOnly, Category = "AnimData|LocomotionData")
float LocomotionDirection;
用KismetAnimationLibrary计算夹角并保存在LocomotionDirection
#include "KismetAnimationLibrary.h"
void UWarriorCharacterAnimInstance::NativeThreadSafeUpdateAnimation(float DeltaSeconds)
{
if (!OwningCharacter || !OwningMovementComponent)
{
return;
}
GroundSpeed = OwningCharacter->GetVelocity().Size2D();
bHasAcceleration = OwningMovementComponent->GetCurrentAcceleration().SizeSquared2D() > 0.f;
LocomotionDirection = UKismetAnimationLibrary::CalculateDirection(OwningCharacter->GetVelocity(),OwningCharacter->GetActorRotation());
}
使用KismetAnimationLibrary要添加模块

在UWarriorBaseAnimInstance添加辅助函数(纯蓝图)
protected:
UFUNCTION(BlueprintPure, meta = (BlueprintThreadSafe))
bool DoesOwnerHaveTag(FGameplayTag TagToCheck) const;
#include "AnimInstances/WarriorBaseAnimInstance.h"
#include "WFunctionLibrary.h"
bool UWarriorBaseAnimInstance::DoesOwnerHaveTag(FGameplayTag TagToCheck) const
{
if (APawn* OwningPawn = TryGetPawnOwner())
{
return UWFunctionLibrary::NativeDoesActorHaveTag(OwningPawn, TagToCheck);
}
return false;
}
调整ABP_Enemy_Base使其可以切换为侧移动画,根据所带标签Enemy.Status.Strafing判断
5.15 Strafing Blend Space
创建一个BlendSpace——BS_Guardian_Strafing

修改垂直轴数值,LocomotionDirection就是前面计算的侧移方向

分配动画,上面从左到右是朝不同方向行走的动画,下面是静止的动画
5.16 Compute Success Chance
需要做的步骤
自定义一个装饰节点BTDecorator_ComputeSuccessChance计算攻击概率

重写Perform Condition Check AI, 使用Random Bool with Weight,根据权重概率返回bool

添加装饰器并测试

Cooldown Decorator(冷却装饰器) 是一种内置的 Decorator(装饰器),它的核心作用是:
限制某个行为节点在一段时间内只能执行一次,防止其被频繁重复触发。
5.17 Dot Product Test
现在有个问题,敌人会强行穿越玩家角色到达侧移位置,原因:玩家背后的点被选择成功了

解决办法:比较敌人的前向向量和敌人到生成点向量,根据点积值,假如大于0就不选,说明敌人前面的点不选择

在EQS_FindStrafingLocation添加Dot Test

设置Test Mode为Dot 2D(xy平面),过滤条件为最大值,0.45是为了有冗余,LineA模式是转向(敌人朝前方向),Line模式是两点


可以不需要第一个距离检测

5.18 Enemy Melee Ability
创建一个所有敌人攻击基类GA_Enemy_MeleeAttack_Base,修改属性值

添加播放蒙太奇

以GA_Enemy_MeleeAttack_Base为父类创建GA_Guardian_Melee_1和GA_Guardian_Melee_2,创建蒙太奇动画并分配
DA_Guardian添加CombatAbility

5.19 Active Ability By Tag
在WarriorAbilitySystemComponent.h添加TryActivateAbilityByTag函数
UFUNCTION(BlueprintCallable, Category = "Warrior|Ability")
bool TryActivateAbilityByTag(FGameplayTag AbilityTagToActivate);
找到能力并激活
bool UWarriorAbilitySystemComponent::TryActivateAbilityByTag(FGameplayTag AbilityTagToActivate)
{
check(AbilityTagToActivate.IsValid());
TArray<FGameplayAbilitySpec*> FoundAbilitySpecs;
GetActivatableGameplayAbilitySpecsByAllMatchingTags(AbilityTagToActivate.GetSingleTagContainer(), FoundAbilitySpecs);
if (!FoundAbilitySpecs.IsEmpty())
{
const int32 RandomAbilityIndex = FMath::RandRange(0, FoundAbilitySpecs.Num() - 1);
FGameplayAbilitySpec* SpecToActivate = FoundAbilitySpecs[RandomAbilityIndex];
check(SpecToActivate);
if (!SpecToActivate->IsActive()) {
return TryActivateAbility(SpecToActivate->Handle);
}
}
return false;
}
创建一个任务节点BTTask_ActivateAbilityByTag

创建一个测试敌方(必须是从BP_Gruntling_Guardian创建子类蓝图,而不是复制),控制器和行为树可以复制

要替换控制器的行为树和测试单位的控制器


行为树触发激活能力任务

5.20 Is Target Pawn Hostile
为敌人攻击动画添加武碰撞检测

在WFunctionLibrary.h添加IsTargetPawnHostile函数
UFUNCTION(BlueprintCallable, Category = "Warrior|FunctionLibrary")
static bool IsTargetPawnHostile(APawn* QueryPawn, APawn* TargetPawn );
bool UWFunctionLibrary::IsTargetPawnHostile(APawn* QueryPawn, APawn* TargetPawn)
{
check(QueryPawn && TargetPawn);
IGenericTeamAgentInterface* QueryTeamAgent = Cast<IGenericTeamAgentInterface>(QueryPawn->GetController());
IGenericTeamAgentInterface* TargetTeamAgent = Cast<IGenericTeamAgentInterface>(TargetPawn->GetController());
if (QueryTeamAgent && TargetTeamAgent)
{
return QueryTeamAgent->GetGenericTeamId() != TargetTeamAgent->GetGenericTeamId();
}
return false;
}
修改武器重叠逻辑,先判断是否是不同组号角色
void AWarriorWeaponBase::OnCollisionBoxBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
APawn* WeaponOwningPawn = GetInstigator<APawn>();
checkf(WeaponOwningPawn, TEXT("Forgot to assign an instiagtor as the owning pawn for the weapon: %s"), *GetName());
if (APawn* HitPawn = Cast<APawn>(OtherActor))
{
if (UWFunctionLibrary::IsTargetPawnHostile(WeaponOwningPawn,HitPawn))
{
OnWeaponHitTarget.ExecuteIfBound(OtherActor);
}
}
}
void AWarriorWeaponBase::OnCollisionBoxEndOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex)
{
APawn* WeaponOwningPawn = GetInstigator<APawn>();
checkf(WeaponOwningPawn, TEXT("Forgot to assign an instiagtor as the owning pawn for the weapon: %s"), *GetName());
if (APawn* HitPawn = Cast<APawn>(OtherActor))
{
if (UWFunctionLibrary::IsTargetPawnHostile(WeaponOwningPawn, HitPawn))
{
OnWeaponHitTarget.ExecuteIfBound(OtherActor);
}
}
}
EnemyCombatComponent需要重写OnHitTargetActor方法,写调试信息
public:
virtual void OnHitTargetActor(AActor* HitActor) override;
void UEnemyCombatComponent::OnHitTargetActor(AActor* HitActor)
{
Debug::Print(GetOwningPawn()->GetActorNameOrLabeL() + TEXT("is hitting ") + HitActor->GetActorNameOrLabeL());
}
5.21 Notify Melee Hit
void UEnemyCombatComponent::OnHitTargetActor(AActor* HitActor)
{
if (OverlappedActors.Contains(HitActor))
{
return;
}
OverlappedActors.AddUnique(HitActor);
bool bIsValidBlock = false;//检测是否格挡成功
const bool bIsPlayerBlocking = false;//检测玩家是否格挡
const bool bIsMyAttackUnblockable = false;//检测攻击能否格挡
if (bIsPlayerBlocking && !bIsMyAttackUnblockable)
{
//TODO::check if the block is valid
}
FGameplayEventData EventData;
EventData.Instigator = GetOwningPawn();
EventData.Target = HitActor;
if (bIsValidBlock)
{
//TODO::Handle successful block
}
else
{
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(
GetOwningPawn(),
WarriorGameplayTags::Shared_Event_MeleeHit,
EventData
);
}
}
在GA_Enemy_MeleeAttack_Base添加等待游戏事件和调试信息,在播放攻击动画并击中玩家后,会SendGameplayEventToActor触发Wait Gameplay Event


5.22 Enemy Damage Effect Spec Handler
在WarriorEnemyGameplayAbility添加MakeEnemyDamageEffectSpecHandle方法
UFUNCTION(BlueprintPure, Category = "Warrior|Ability")
FGameplayEffectSpecHandle MakeEnemyDamageEffectSpecHandle(TSubclassOf<UGameplayEffect> EffectClass,const FScalableFloat& InDamageScalableFloat);
FGameplayEffectSpecHandle UWarriorEnemyGameplayAbility::MakeEnemyDamageEffectSpecHandle(TSubclassOf<UGameplayEffect> EffectClass, const FScalableFloat& InDamageScalableFloat)
{
check(EffectClass);
FGameplayEffectContextHandle ContextHandle = GetWarriorAbilitySystemComponentFromActorInfo()->MakeEffectContext();
ContextHandle.SetAbility(this);
ContextHandle.AddSourceObject(GetAvatarActorFromActorInfo());
ContextHandle.AddInstigator(GetAvatarActorFromActorInfo(),GetAvatarActorFromActorInfo());
FGameplayEffectSpecHandle EffectSpecHandle = GetWarriorAbilitySystemComponentFromActorInfo()->MakeOutgoingSpec(
EffectClass,
GetAbilityLevel(),
ContextHandle
);
EffectSpecHandle.Data->SetSetByCallerMagnitude(
WarriorGameplayTags::Shared_SetByCaller_BaseDamage,
InDamageScalableFloat.GetValueAtLevel(GetAbilityLevel())
);
return EffectSpecHandle;
}
创建单独函数处理伤害,输入是In Payload(GameplayEventData),通过MakeEnemyDamageEffectSpecHandle获取EffectSpecHandle,然后Apply到目标Actor上,InDamageScalableFloat变为全局变量,需要GameplayAbility传入伤害曲线


5.23 Apply Enemy Damage
给GA_Guardian_Melee_1,GA_Guardian_Melee_2分配伤害曲线,可以复用GT_GuardianStats

在GT_GuardianStats添加GuardianBaseDamage

还要指定用哪个值和等级

后面是对伤害结果调试验证
5.24 Motion Warping
现在有个问题:在敌人攻击时绕到背后,敌人不会转向

使用Motion Warping技术
🎯 什么是 Motion Warping?
Motion Warping 是一种在动画播放过程中,实时修改根骨骼(Root Motion)或指定骨骼的位置/旋转,使其精确对齐到游戏世界中某个目标点的技术。
它的核心目标是:
让动画“刚好打中”、“刚好踩到”、“刚好看向”目标,而无需为每个距离/角度制作多套动画。
启用插件

在WarriorBaseCharacter添加运动扭曲组件
class UMotionWarpingComponent;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "MotionWarping")
UMotionWarpingComponent* MotionWarpingComponent;
添加引用模块

在构造函数中创建组件
AWarriorBaseCharacter::AWarriorBaseCharacter()
{
// Set this character to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
PrimaryActorTick.bStartWithTickEnabled = false;
GetMesh()->bReceivesDecals = false;
WarriorAbilitySystemComponent = CreateDefaultSubobject<UWarriorAbilitySystemComponent>(TEXT("WarriorAbilitySystemComponent"));
WarriorAttributeSet = CreateDefaultSubobject<UWarriorAttributeSet>(TEXT("WarriorAttributeSet"));
MotionWarpingComponent = CreateDefaultSubobject<UMotionWarpingComponent>(TEXT("MotionWarpingComponent"));
}

在敌人攻击蒙太奇动画添加动作扭曲通知

修改动作扭曲通知参数

在攻击动画开始进行动作扭曲,到出手

5.25 Update Motion Warp Target
因为目标Actor可能还会移动,所以只在开始进行动作扭曲还不够,但又不需要每帧都调整,所以用行为树合适,创建服务节点BTService_UpdateMotionWarpAttackTarget

将转化的EnemyCharacter存储为变量,然后调用动作扭曲组件调用Add Or Update Warp Target By Location改变位置,目标位置从黑板中获取


5.26 Constuect Native BT Task
用C++手动创建自定义任务节点,创建任务BTTask_RotateToFaceTarget

//BTTask_RotateToFaceTarget.h
struct FRotateToFaceTargetTaskMemory
{
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 HasReachedAnglePrecision(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;
};
构造函数
UBTTask_RotateToFaceTarget::UBTTask_RotateToFaceTarget()
{
NodeName = TEXT("Native Rotate to Face Target Actor");
AnglePrecision = 10.f;
RotationInterpSpeed = 5.f;
bNotifyTick = true;
bNotifyTaskFinished = true;
bCreateNodeInstance = false;
INIT_TASK_NODE_NOTIFY_FLAGS();
InTargetToFaceKey.AddObjectFilter(this,GET_MEMBER_NAME_CHECKED(ThisClass,InTargetToFaceKey),AActor::StaticClass());
}
bNotifyTick——启用 Tick 回调,引擎会在行为树执行该任务期间,每帧调用TickTask()函数
bNotifyTaskFinished——允许任务在 Tick 或异步操作中主动结束自己。
bCreateNodeInstance——控制是否为每个 AI 实例创建该节点的独立 C++ 实例。每个 AI 实例都拥有自己独立的状态存储(NodeMemory),但它们共享同一个 C++ 任务节点实例(UBTTask_RotateToFaceTarget对象),由这个“单例”C++ 对象来处理所有 AI 的逻辑。
INIT_TASK_NODE_NOTIFY_FLAGS();
这是一个宏,定义在BehaviorTree/Tasks/BTTaskNode.h中。自动同步bNotifyTick、bNotifyTaskFinished等标志到节点的内部 flag 位域(NodeFlags)中,以便行为树运行时系统能快速判断是否需要调用 Tick、是否支持 latent 结束等。
InTargetToFaceKey.AddObjectFilter是用来限制黑板键选择器(Blackboard Key Selector)只能选择特定类型的黑板键。
InitializeFromAsset函数
在行为树资产(UBehaviorTree)被加载或实例化时,让 C++ 节点有机会根据该行为树的上下文(尤其是其关联的 Blackboard)来初始化自身配置
这里InTargetToFaceKey.ResolveSelectedKey(*BBAsset)把现在所选的键名,在这个具体的 Blackboard 资产中对应的内部 Key ID
void UBTTask_RotateToFaceTarget::InitializeFromAsset(UBehaviorTree& Asset)
{
Super::InitializeFromAsset(Asset);
if (UBlackboardData* BBAsset = GetBlackboardAsset())
{
InTargetToFaceKey.ResolveSelectedKey(*BBAsset);
}
}
结构体FRotateToFaceTargetTaskMemory
为了保存单个 AI 在执行 “旋转朝向目标” 任务期间所需的临时状态数据
struct FRotateToFaceTargetTaskMemory
{
TWeakObjectPtr<APawn> OwningPawn;
TWeakObjectPtr<AActor> TargetActor;
bool IsValid() const
{
return OwningPawn.IsValid() && TargetActor.IsValid();
}
void Reset()
{
OwningPawn.Reset();
TargetActor.Reset();
}
};
UBTTask_RotateToFaceTarget完整代码
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "BTTask_RotateToFaceTarget.generated.h"
struct FRotateToFaceTargetTaskMemory
{
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
UPROPERTY(EditAnywhere, Category = "Face Target")
float AnglePrecision;
UPROPERTY(EditAnywhere, Category = "Face Target")
float RotationInterpSpeed;
UPROPERTY(EditAnywhere, Category = "Face Target")
FBlackboardKeySelector InTargetToFaceKey;
};
#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;
bNotifyTaskFinished = true;
bCreateNodeInstance = false;
INIT_TASK_NODE_NOTIFY_FLAGS();
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));
}
5.27 Rotate Enemy In Task
重写ExecuteTask和TickTask函数,并添加辅助函数HasReachedAnglePrecision
| 函数 | 作用 | 调用时机 |
|---|---|---|
ExecuteTask |
任务启动入口:初始化任务、启动异步操作、决定是否需要持续执行 | 任务第一次被激活时调用一次 |
TickTask |
任务持续更新:每帧执行的逻辑(如旋转、移动插值、进度检查) | 仅当 ExecuteTask 返回 InProgress 且 bNotifyTick = true 时,每帧调用 |
- 引擎调用
ExecuteTask一次 - 你在这里初始化数据(读黑板、存到
Memory) - 返回
InProgress→ 启动TickTask循环
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
bool HasReachedAnglePrecision(APawn* QueryPawn, AActor* TargetActor) const;
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);
Memory->OwningPawn = OwningPawn;
Memory->TargetActor = TargetActor;
if (!Memory->IsValid())
{
return EBTNodeResult::Failed;
}
if (HasReachedAnglePrecision(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 (HasReachedAnglePrecision(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::HasReachedAnglePrecision(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;
}
5.28 Melee Attack Branch
添加一个新分支,是否进入攻击范围(条件为与目标距离小于等于150)
添加等待任务时为了完成攻击之前防止跳转到其它条件分支
Cooldown可以使其在攻击后进入侧移

BTTask_RotateToFaceTarget参数设置
5.29 Does Actor Have Tag Decorator
敌人在受到攻击后立马进入侧移状态,受伤动画都没播放完

创建UnderAttack标签
WARRIOR_API UE_DECLARE_GAMEPLAY_TAG_EXTERN(Enemy_Status_UnderAttack);
UE_DEFINE_GAMEPLAY_TAG(Enemy_Status_UnderAttack, "Enemy.Status.UnderAttack");
创建新分支,并加上装饰节点Check Gameplay Tags On Actor 

但Check Gameplay Tags On Actor 没有中止条件,无法避免继续执行右侧2分支,只能自定义节点

就是从黑板获取到SelfActor,然后检查是否有Tag,还加了个反向条件,虽然节点自带了一个InverseCondition,但发现没有作用


检测标签的节点Observer aborts改为Lower Priority可以中止右侧分支
还加上了一个范围检查节点

5.30 Duration Gameplay Effect
添加一个新的GameplayEffect——GE_Enemy_UnderAttack


视频中的Target Tags改名了,参考下面文章
UE5 GAS GameplayEffect Component UE5.5 对照:(2 封私信 / 44 条消息) UE5 GAS GameEffect Component UE5.5 对照 - 知乎

接下来是Duration的参数

在GA_Enemy_HitReact_Base添加GameplayEffect

进行测试,敌人在受击后不会再侧移

这样设置敌人再受到攻击后如果在范围内会继续攻击

5.31 Should Abort All Logic
现在有个问题就是当玩家生命值归0,敌人还在持续攻击,要有个分支可以阻止i右侧分支执行
先创建装饰节点BTDecorator_ShouldAbortAllLogic

三个条件判定
| 条件 | 意义 |
|---|---|
Target Dead |
目标没了,别打了 |
AI Dead |
我自己挂了,别动了 |
Distance ≈ 0 |
太近了,容易出问题,暂停攻击 |


5.32 Guardian Attack Sound FX
给两个攻击动画添加音效,干就完了

给攻击音效设置并发

| 用途 | 说明 |
|---|---|
| 防止声音堆积 | 避免同一音效反复触发导致音频“爆炸” |
| 控制并发数量 | 限制最多几个实例可以同时播放 |
| 管理优先级 | 新的播放可以中断旧的,或反之 |
| 优化性能 | 减少 CPU 和内存开销(尤其是大量重复音效) |
| 提升听觉体验 | 让声音更真实、不冗余 |
击中目标触发音效

添加一个标签



第五章完结
更多推荐

























所有评论(0)