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)→ 在速度圆里找一个不进任何锥的最优速度 → 移动

   关键特点:

  1. 分布式计算:每个智能体只根据周围邻居的位置和速度做局部决策,不需要全局规划。
  2. 实时高效:计算量小,适合大量单位同时避障(如 RTS 游戏中的百人军队)。
  3. 无中心协调:不需要中央控制器,天然支持动态增减智能体。
  4. 避免死锁:在对称场景(如两人对冲)中也能自然解耦。
  • 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;
};

 UAIPerceptionComponentUAISenseConfig_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 是一个结构体,用于让用户在编辑器中选择一个黑板键

  • 但黑板可能有多种类型的键:VectorBoolObjectClass 等

  • 你希望 只允许选择类型为 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设置最大行走速度

关闭 CharacterMovementComponentbOrientRotationToMovement = 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());
}
  1. bNotifyTick——启用 Tick 回调,引擎会在行为树执行该任务期间,每帧调用 TickTask() 函数

  2. bNotifyTaskFinished——允许任务在 Tick 或异步操作中主动结束自己

  3. bCreateNodeInstance——控制是否为每个 AI 实例创建该节点的独立 C++ 实例每个 AI 实例都拥有自己独立的状态存储(NodeMemory),但它们共享同一个 C++ 任务节点实例(UBTTask_RotateToFaceTarget 对象),由这个“单例”C++ 对象来处理所有 AI 的逻辑。

  4. INIT_TASK_NODE_NOTIFY_FLAGS();
    这是一个宏,定义在 BehaviorTree/Tasks/BTTaskNode.h 中。自动同步 bNotifyTickbNotifyTaskFinished 等标志到节点的内部 flag 位域(NodeFlags)中,以便行为树运行时系统能快速判断是否需要调用 Tick、是否支持 latent 结束等。

  5. 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 和内存开销(尤其是大量重复音效)
提升听觉体验 让声音更真实、不冗余

击中目标触发音效

添加一个标签

第五章完结

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐