六一的部落格


关关难过关关过,前路漫漫亦灿灿。




说明

AI Service / AIDecorator


BTService

添加到行为树节点的特殊类. 自带Tick函数, 在其中实现游戏逻辑.

在给定条件, 可以修改黑板中的变量值.

本节将通过服务重写定位敌人逻辑, 当敌人出现在NPC的捕捉范围内, 计算得到敌人附近一点, 使NPC跑去该位置.

可以添加到行为树的任意节点


添加黑板变量

BB_STUCharacter

添加类型为Object的变量, 命名为EnemyActor; 存放选中敌人



创建行为树服务类

-
AI/Services
基类 BTService
Public
名称 STUFindEnemyService

每秒搜索距离NPC最近的敌人


修改头文件搜索路径

ShootThemUp: ShootThemUp.Build.cs

 1PublicIncludePaths.AddRange(new string[]
 2{
 3    "ShootThemUp/Public/Player",
 4    "ShootThemUp/Public/Components",
 5    "ShootThemUp/Public/Dev",
 6    "ShootThemUp/Public/Weapon",
 7    "ShootThemUp/Public/UI",
 8    "ShootThemUp/Public/Animations",
 9    "ShootThemUp/Public/Pickups",
10    "ShootThemUp/Public/Weapon/Components",
11    "ShootThemUp/Public/AI",
12    "ShootThemUp/Public/AI/Tasks",
13    "ShootThemUp/Public/AI/Services"            
14});

添加构造函数

public

ShootThemUp: AI/Services/STUFindEnemyService.h

1USTUFindEnemyService();

设置服务名称

ShootThemUp: AI/Services/STUFindEnemyService.cpp

1USTUFindEnemyService::USTUFindEnemyService()
2{
3    NodeName = "Find Enemy";
4}

添加属性: 存放黑板变量信息

protected

ShootThemUp: AI/Services/STUFindEnemyService.h

1UPROPERTY(EditAnywhere, BlueprintReadWrite)
2FBlackboardKeySelector EnemyActorKey;

添加TickNode函数

可以设置调用频率; 在此定位敌人

protected

ShootThemUp: AI/Services/STUFindEnemyService.h

1virtual void TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds) override;

ShootThemUp: AI/Services/STUFindEnemyService.cpp

 1#include "BehaviorTree/BlackboardComponent.h"
 2#include "AIController.h"
 3#include "STUUtils.h"
 4#include "Components/STUAIPerceptionComponent.h"
 5
 6void USTUFindEnemyService::TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds)
 7{
 8    const auto Blackboard = OwnerComp.GetBlackboardComponent();
 9    if (Blackboard)
10    {
11        const auto Controller = OwnerComp.GetAIOwner();
12        const auto PerceptionComponent = STUUtils::GetSTUPlayerComponent<USTUAIPerceptionComponent>(Controller);
13        if (PerceptionComponent)
14        {
15            Blackboard->SetValueAsObject(EnemyActorKey.SelectedKeyName, PerceptionComponent->GetClosestEnemy());
16        }
17    }
18    Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
19}

SetValueAsObject函数

第一个参数: 变量名

第二个参数: 给出指向敌人的UObject指针


在AIController中使用服务


添加属性: 存放变量名

无法使用选择器, Controller不涉及黑板变量逻辑

ShootThemUp: AI/STUAIController.h

1UPROPERTY(EditAnywhere, BlueprintReadWrite)
2FName FocusOnKeyName = "EnemyActor";

添加接口: 返回敌人对象

private

ShootThemUp: AI/STUAIController.h

1AActor *GetFocusOnActor() const;

ShootThemUp: AI/STUAIController.cpp

1#include "BehaviorTree/BlackboardComponent.h"
2
3AActor *ASTUAIController::GetFocusOnActor() const
4{
5    if (!GetBlackboardComponent()) return nullptr;
6    return Cast<AActor>(GetBlackboardComponent()->GetValueAsObject(FocusOnKeyName));
7}

在Tick函数中使用服务

不直接使用感知组件接口

ShootThemUp: AI/STUAIController.cpp

1// Tick
2const auto AimActor = GetFocusOnActor();

NPC行为树

BT_STUCharacter

NPC有两个职责:

  • 在地图上巡逻

    将序列命名为Random roam
  • 攻击

    添加节点: Sequence, 命名为Attack

添加节点: Selector

从左到右运行子树. 若排行较前的子树返回Fail, 则执行下一子树



为Selector添加服务: 一直查找敌人

右键 Selector , Add Service > STUFindEnemyService



设置STUFindEnemyService

- 说明
EnemyActorKey EnemyActor
Service > Interval Tick调用频率上限F
Service > RandomDeviation Tick调用频率活动范围FR

Tick实际调用频率在F-FR ~ F+FR之间



为Attack添加任务: MoveTo

Blackboard Key无法选择EnemyActor


设置黑板变量EnemyActor

BB_STUCharacter > Enemy Actor > Blackboard Details > Key Type > Base Class , 设置为Actor : UObject对象在世界中不具有变化属性



设置MoveTo目的地为EnemyActor; 勾选 Observe Blackboard Value

变量值发生改变时, 新目的地立即生效



查看

UE4.27: 当Find Enemy不为空, 且无法找到AimLocation时, 才会执行Attack序列

UE5.1: 定时执行Find Enemy, 从左到右执行Attack和Rand roam; Enemy Actor不为空, 靠近选中敌人, 为空, 去到随机生成的位置


使用Decorator

Decorator包含逻辑判断, 类似switch, 可以中断整棵子树

根据EnemyActor的设置情况, 决定执行哪个序列


为Attack添加Decorator: Blackboard

Blackboard会检查黑板变量的值


  • 选中 Blackboard Based Condition

    Details > Description > Node Name 命名为 Has Enemy

    Details > Blackboard > Blackboard Key , 选中EnemyActor

    Details > Blackboard > Key Query , 选择IsSet分支

    Details > Flow Control > Observer aborts , 选择Self


  • 若EnemyActor有值, 执行Attack, 否则中止序列


为Random roam添加Decorator

  • 命名为No Enemy

    Details > Blackboard > Blackboard Key , 选中EnemyActor

    Details > Blackboard > Key Query , 选择IsNotSet分支

    Details > Flow Control > Observer aborts , 选择Self


  • 若EnemyActor没有值, 执行Random roam, 否则中止序列


使NPC移动到敌人附近

  • 添加标志位: 以自身为圆心寻找随机点 巡逻 ; 以敌人为圆心寻找随机点 攻击

    protected

    ShootThemUp: AI/Tasks/STUNextLocationTask.h
    1UPROPERTY(EditAnywhere, BlueprintReadWrite)
    2bool SelfCenter = true;
  • 添加属性: 如果以敌人为圆心寻找随机点, 保存敌人信息

    protected

    ShootThemUp: AI/Tasks/STUNextLocationTask.h
    1UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "!SelfCenter"))
    2FBlackboardKeySelector CenterActorKey;
  • 设置随机位置中心点

    ShootThemUp: AI/Tasks/STUNextLocationTask.cpp
     1// ExecuteTask
     2
     3auto Location = Pawn->GetActorLocation();
     4if (!SelfCenter)
     5{
     6    auto CenterActor = Cast<AActor>(Blackboard->GetValueAsObject(CenterActorKey.SelectedKeyName));
     7    if (!CenterActor) return EBTNodeResult::Failed;
     8    Location = CenterActor->GetActorLocation();
     9}
    10
    11const auto Found = NavSys->GetRandomReachablePointInRadius(Location, Radius, NavLocation);

设置Attack序列

  • 添加任务: STUNextLocationTask

    进行如下设置


  • 设置MoveTo目的地


自定义行为树服务



说明

AI Service / AIDecorator


BTService

添加到行为树节点的特殊类. 自带Tick函数, 在其中实现游戏逻辑.

在给定条件, 可以修改黑板中的变量值.

本节将通过服务重写定位敌人逻辑, 当敌人出现在NPC的捕捉范围内, 计算得到敌人附近一点, 使NPC跑去该位置.

可以添加到行为树的任意节点


添加黑板变量

BB_STUCharacter

添加类型为Object的变量, 命名为EnemyActor; 存放选中敌人



创建行为树服务类

-
AI/Services
基类 BTService
Public
名称 STUFindEnemyService

每秒搜索距离NPC最近的敌人


修改头文件搜索路径

ShootThemUp: ShootThemUp.Build.cs

 1PublicIncludePaths.AddRange(new string[]
 2{
 3    "ShootThemUp/Public/Player",
 4    "ShootThemUp/Public/Components",
 5    "ShootThemUp/Public/Dev",
 6    "ShootThemUp/Public/Weapon",
 7    "ShootThemUp/Public/UI",
 8    "ShootThemUp/Public/Animations",
 9    "ShootThemUp/Public/Pickups",
10    "ShootThemUp/Public/Weapon/Components",
11    "ShootThemUp/Public/AI",
12    "ShootThemUp/Public/AI/Tasks",
13    "ShootThemUp/Public/AI/Services"            
14});

添加构造函数

public

ShootThemUp: AI/Services/STUFindEnemyService.h

1USTUFindEnemyService();

设置服务名称

ShootThemUp: AI/Services/STUFindEnemyService.cpp

1USTUFindEnemyService::USTUFindEnemyService()
2{
3    NodeName = "Find Enemy";
4}

添加属性: 存放黑板变量信息

protected

ShootThemUp: AI/Services/STUFindEnemyService.h

1UPROPERTY(EditAnywhere, BlueprintReadWrite)
2FBlackboardKeySelector EnemyActorKey;

添加TickNode函数

可以设置调用频率; 在此定位敌人

protected

ShootThemUp: AI/Services/STUFindEnemyService.h

1virtual void TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds) override;

ShootThemUp: AI/Services/STUFindEnemyService.cpp

 1#include "BehaviorTree/BlackboardComponent.h"
 2#include "AIController.h"
 3#include "STUUtils.h"
 4#include "Components/STUAIPerceptionComponent.h"
 5
 6void USTUFindEnemyService::TickNode(UBehaviorTreeComponent &OwnerComp, uint8 *NodeMemory, float DeltaSeconds)
 7{
 8    const auto Blackboard = OwnerComp.GetBlackboardComponent();
 9    if (Blackboard)
10    {
11        const auto Controller = OwnerComp.GetAIOwner();
12        const auto PerceptionComponent = STUUtils::GetSTUPlayerComponent<USTUAIPerceptionComponent>(Controller);
13        if (PerceptionComponent)
14        {
15            Blackboard->SetValueAsObject(EnemyActorKey.SelectedKeyName, PerceptionComponent->GetClosestEnemy());
16        }
17    }
18    Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);
19}

SetValueAsObject函数

第一个参数: 变量名

第二个参数: 给出指向敌人的UObject指针


在AIController中使用服务


添加属性: 存放变量名

无法使用选择器, Controller不涉及黑板变量逻辑

ShootThemUp: AI/STUAIController.h

1UPROPERTY(EditAnywhere, BlueprintReadWrite)
2FName FocusOnKeyName = "EnemyActor";

添加接口: 返回敌人对象

private

ShootThemUp: AI/STUAIController.h

1AActor *GetFocusOnActor() const;

ShootThemUp: AI/STUAIController.cpp

1#include "BehaviorTree/BlackboardComponent.h"
2
3AActor *ASTUAIController::GetFocusOnActor() const
4{
5    if (!GetBlackboardComponent()) return nullptr;
6    return Cast<AActor>(GetBlackboardComponent()->GetValueAsObject(FocusOnKeyName));
7}

在Tick函数中使用服务

不直接使用感知组件接口

ShootThemUp: AI/STUAIController.cpp

1// Tick
2const auto AimActor = GetFocusOnActor();

NPC行为树

BT_STUCharacter

NPC有两个职责:

  • 在地图上巡逻

    将序列命名为Random roam
  • 攻击

    添加节点: Sequence, 命名为Attack

添加节点: Selector

从左到右运行子树. 若排行较前的子树返回Fail, 则执行下一子树



为Selector添加服务: 一直查找敌人

右键 Selector , Add Service > STUFindEnemyService



设置STUFindEnemyService

- 说明
EnemyActorKey EnemyActor
Service > Interval Tick调用频率上限F
Service > RandomDeviation Tick调用频率活动范围FR

Tick实际调用频率在F-FR ~ F+FR之间



为Attack添加任务: MoveTo

Blackboard Key无法选择EnemyActor


设置黑板变量EnemyActor

BB_STUCharacter > Enemy Actor > Blackboard Details > Key Type > Base Class , 设置为Actor : UObject对象在世界中不具有变化属性



设置MoveTo目的地为EnemyActor; 勾选 Observe Blackboard Value

变量值发生改变时, 新目的地立即生效



查看

UE4.27: 当Find Enemy不为空, 且无法找到AimLocation时, 才会执行Attack序列

UE5.1: 定时执行Find Enemy, 从左到右执行Attack和Rand roam; Enemy Actor不为空, 靠近选中敌人, 为空, 去到随机生成的位置


使用Decorator

Decorator包含逻辑判断, 类似switch, 可以中断整棵子树

根据EnemyActor的设置情况, 决定执行哪个序列


为Attack添加Decorator: Blackboard

Blackboard会检查黑板变量的值


  • 选中 Blackboard Based Condition

    Details > Description > Node Name 命名为 Has Enemy

    Details > Blackboard > Blackboard Key , 选中EnemyActor

    Details > Blackboard > Key Query , 选择IsSet分支

    Details > Flow Control > Observer aborts , 选择Self


  • 若EnemyActor有值, 执行Attack, 否则中止序列


为Random roam添加Decorator

  • 命名为No Enemy

    Details > Blackboard > Blackboard Key , 选中EnemyActor

    Details > Blackboard > Key Query , 选择IsNotSet分支

    Details > Flow Control > Observer aborts , 选择Self


  • 若EnemyActor没有值, 执行Random roam, 否则中止序列


使NPC移动到敌人附近

  • 添加标志位: 以自身为圆心寻找随机点 巡逻 ; 以敌人为圆心寻找随机点 攻击

    protected

    ShootThemUp: AI/Tasks/STUNextLocationTask.h
    1UPROPERTY(EditAnywhere, BlueprintReadWrite)
    2bool SelfCenter = true;
  • 添加属性: 如果以敌人为圆心寻找随机点, 保存敌人信息

    protected

    ShootThemUp: AI/Tasks/STUNextLocationTask.h
    1UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (EditCondition = "!SelfCenter"))
    2FBlackboardKeySelector CenterActorKey;
  • 设置随机位置中心点

    ShootThemUp: AI/Tasks/STUNextLocationTask.cpp
     1// ExecuteTask
     2
     3auto Location = Pawn->GetActorLocation();
     4if (!SelfCenter)
     5{
     6    auto CenterActor = Cast<AActor>(Blackboard->GetValueAsObject(CenterActorKey.SelectedKeyName));
     7    if (!CenterActor) return EBTNodeResult::Failed;
     8    Location = CenterActor->GetActorLocation();
     9}
    10
    11const auto Found = NavSys->GetRandomReachablePointInRadius(Location, Radius, NavLocation);

设置Attack序列

  • 添加任务: STUNextLocationTask

    进行如下设置


  • 设置MoveTo目的地