【UE4C++-ActionRougelike-11】BT-Task和EQS
1、远程攻击
1.1 创建行为树任务节点
以BTTaskNode为父类创建行为树任务节点MyBTTask_RangedAttack。重写重写UBTTaskNode 的 ExecuteTask 函数,用于在行为树中执行任务逻辑。
UCLASS()
class ACTIONROGUELIKE_API UMyBTTask_RangedAttack : public UBTTaskNode
{
GENERATED_BODY()
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
UPROPERTY(EditAnywhere, Category = "AI")
TSubclassOf<AActor> ProjectileClass;
};
在MyBTTask_RangedAttack.cpp中实现ExecuteTask方法。
EBTNodeResult::Type UMyBTTask_RangedAttack::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
//获取当前任务节点所属的 AI 控制器
AAIController* MyAIController = OwnerComp.GetAIOwner();
if (ensure(MyAIController))
{
//获取 AI 控制器的控制角色
ACharacter* MyPawn = Cast<ACharacter>(MyAIController->GetPawn());
//如果控制角色为空,则返回 EBTNodeResult::Failed 表示任务失败。
if (MyPawn == nullptr)
{
return EBTNodeResult::Failed;
}
//获取枪口位置(MuzzleLocation)
FVector MuzzleLocation = MyPawn->GetMesh()->GetSocketLocation("Muzzle_01");
//获取黑板中保存的目标角色(TargetActor)
AActor* TargetActor = Cast<AActor>(OwnerComp.GetBlackboardComponent()->GetValueAsObject("TargetActor"));
//如果目标角色为空,则返回 EBTNodeResult::Failed 表示任务失败
if (TargetActor == nullptr)
{
return EBTNodeResult::Failed;
}
//计算从枪口位置到目标角色位置的方向向量(Direction),并将其转换为旋转
FVector Direction = TargetActor->GetActorLocation() - MuzzleLocation;
FRotator MuzzleRotation = Direction.Rotation();
FActorSpawnParameters Params;
Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
Params.Instigator = MyPawn;
AActor* NewProj = GetWorld()->SpawnActor<AActor>(ProjectileClass, MuzzleLocation, MuzzleRotation, Params);
//如果新生成的投射物实例不为空,则返回 EBTNodeResult::Succeeded 表示任务成功,否则返回 EBTNodeResult::Failed 表示任务失败
return NewProj ? EBTNodeResult::Succeeded : EBTNodeResult::Failed;
}
return EBTNodeResult::Failed;
}
1.2 修改行为树
Default Focus
(默认焦点)节点来指定AI角色在开始执行行为树时应该关注的默认目标。
Cooldown
(冷却)指的是在某个行为节点执行完毕后,需要等待一定的时间间隔后才能再次执行该节点的行为。
Within Attack Range?黑板节点的Observer aborts设置为Lower Priority,Key Query为Is Set。表示当WithinAttackRange设置为真时执行下面的节点;为假或者未设置时中止该节点和全部子树节点。
1.3 演示效果
2、EQS
2.1 创建EQS查询
新建一个**环境查询(Environment Query)**文件,命名为Query_FindNearByLocation。在Root下添加生成器类型,这里选择Point Donut。右键单击生成器,并选择要添加的测试:Distance测试和Trace测试。
Donut节点设置
设置节点**内半径(Inner Radius)**和外半径(Outer Radius),表示生成点和情景之间的最小距离和最大距离。
设置圆弧方向(Arc Direction),选择 两点(Two Points)用于确定圆弧方向。
设置圆弧角(Arc Angle),定义圆弧的角度
启用使用螺旋模式(Use Spiral Pattern),圆环将以螺旋模式旋转
设置中心(Center),作为搜索中心的情境
Distance节点设置
- 设置**过滤器类型(Filter Type)为
Mininum
,Float Value Min
设置为合适值。表示任何超出最小浮点值(Float Value Min)**属性所指定的范围的数值都将被剔除。 - 设置计分因子(Scoring Factor),表示计分公式最后要乘以规格化分数的权重(因子),设置为-1.0,则离AI越近的点得分越高,为了让AI每次选择离它最近的点,从而每次尽可能的少走。
Trace节点设置
- 设置**从情境追踪(Trace from Context)**,选择自定义情景QueryContext_TargetActor,该情景我们会在2.2介绍。
- 禁用布尔匹配(Bool Match),为了授予计分因子而需要匹配的值(true或false)。执行测试时,如果不匹配此值,将不会改变分数。例如当 Trace 测试节点执行射线检测,Bool Match 参数设置为 False,这意味着如果射线没有命中对象,将会视为测试成功,返回测试成功时的值(ValueOnSuccess),否则返回测试失败时的值(ValueOnFail)。
2.2 创建情景
以EnvQueryContext_BlueprintBase为父类创建自定义情景类蓝图QueryContext_TargetActor,蓝图中对ProvideSingleActor函数进行覆盖,该函数功能返回单个Actor。可以通过任何想要的方法获取该Actor。此处函数功能为返回AIController中的TargetActor。
2.3 将EQS用于行为树
在行为树中添加Tasks节点:Run EQSQuery,并且分配要执行的 **查询模板(Query Template)**(Query_FindNearByLocation)和它应该返回的 **黑板键(MoveToLocation),运行模式(Run Mode)设置为从前25%中随机选择项目(Single Random Item from Best 5%)**。表示从得分在总分75%
至100%
的项目中随机选择。删除先前的黑板节点OutSide Attack Range?,使AI会绕着玩家进行更灵活走动(攻击范围内完成连续攻击后也会移动)
2.3 演示效果
2.4 总结
EQS内容详见官方文档,对本课程中所用的EQS模块进行总结:
**环境查询(Environment Query)**:执行各种场景查询操作。这些节点可以在 EQS 蓝图中使用,用于定义查询规则和生成查询结果
- 场景查询使用流程: 从行为树调用场景查询,具体的场景查询将使用它的 生成器(Generator),引用 情境(Contexts),调用各种 测试(Test),共同为行为树提供权重最高的查询结果。
生成器(Generator):生成器 用于产生将要测试和加权的位置或Actor(统称为 项目(Item)),权重最高的位置或Actor会返回到行为树。具体内容见官方文档。
- Donut节点:按照用户指定的 环数 产生从 中心 情境辐射出的基于环形区域的追踪
测试(Test):执行 测试 来确定在给定的情景中下从生成器产生的哪个 项目(Item) 是”最佳”选择。具体内容见官方文档
- Distance节点:将返回项目(Item)和选择的 距离(Distance To) 属性之间的直线距离。如果”距离(Distance To)”是多个位置,它会取所有距离的平均值。
- 测试模式(Test Mode):用于测试距离的方法
- Distance 3D(在3D空间中)
- Distance 2D(在XY平面的2D上)
- Distance Z(沿Z轴)
- Distance Z(Absolute)(Z(绝对)轴)
- 距离(Distance To):用于测量距离的情境
- 测试模式(Test Mode):用于测试距离的方法
情境(Contexts):情境 为使用的所有测试或生成器提供参考框架,具体参考官方文档。
3、AI感知组件
3.1 添加AI感知组件
在MyAICharacter中添加AI感知组件UPawnSensingComponent,可以将先前写在AIController中BeginPlay里的设置黑板键TargetActor的逻辑注释掉了,这里会用UPawnSensingComponent来实现将看到的Actor赋值给黑板键TargetActor。
class ACTIONROGUELIKE_API AMyAICharacter : public ACharacter
{
GENERATED_BODY()
public:
// Sets default values for this character's properties
AMyAICharacter();
protected:
UPROPERTY(VisibleAnywhere, Category = "Components")
UPawnSensingComponent* PawnSensingComp;
UFUNCTION()
void OnPawnSeen(APawn* Pawn);
virtual void PostInitializeComponents() override;
};
在MyAICharacter.cpp中实现功能
AMyAICharacter::AMyAICharacter()
{
PawnSensingComp = CreateDefaultSubobject<UPawnSensingComponent>("PawnSensingComp");
}
//用于处理当角色看到其他角色时的逻辑
void AMyAICharacter::OnPawnSeen(APawn* Pawn)
{
AAIController* AIC = Cast<AAIController>(GetController());
//当AI看到角色对象时,设置"TargetActor" 的黑板键的值为检测到的角色对象 Pawn。
if (AIC)
{
UBlackboardComponent* BBComp = AIC->GetBlackboardComponent();
BBComp->SetValueAsObject("TargetActor", Pawn);
DrawDebugString(GetWorld(), GetActorLocation(), "PLAYER SPOTTED", nullptr, FColor::White, 4.0f, true);
}
}
//组件初始化完成后对OnSeePawn事件进行函数绑定
void AMyAICharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
//将 OnPawnSeen 函数与 UPawnSensingComponent 的 OnSeePawn 事件绑定,从而实现了当角色看到其他角色时触发 OnPawnSeen 函数的逻辑
PawnSensingComp->OnSeePawn.AddDynamic(this, &AMyAICharacter::OnPawnSeen);
}
3.2 设置AI感知组件属性
在MyAICharacter蓝图中可以设置AI感知组件的属性。
3.3 演示效果
4、优化AI动画
4.1 转身动画优化
AI转身动画会出现割裂感,不像玩具一样能够流畅的转身。取消勾选AIMyCharacter蓝图中Pawn->Use Controller Rotation Yaw,勾选Character Movement->Use Controller Desired Rotation。
修改前:
修改后:
4.2 动画混合空间
Target Weight Interpolation Speed Per Sec(目标权重插值速度每秒),该属性可以用于控制动画在不同状态之间的过渡速度,从而影响角色动画的流畅度和自然性。
4.3 断言Assert
使用好Assert能够后期更好的维护项目和发现错误,用AIController中的行为树举例。
void AMyAIController::BeginPlay()
{
Super::BeginPlay();
//运行行为树
if (ensureMsgf(BehaviorTree, TEXT("Behavior Tree is nullptr, Please assign Behavior Tree in your AIController")))
{
RunBehaviorTree(BehaviorTree);
}
}
如果运行后BehaviorTree是空指针,则输出日志会根据你编写的提示告诉你该怎么做。