UEC++实现人物按照A星路线移动

遇到的问题:

  1. 如何使用UEC++实现UE蓝图中的方法
  2. 世界中存在一个不是自己放置的类
  3. 在一个C++类中调用另一个C++类中的变量或者函数
  4. Get All Actors of Class的替代
  5. 蓝图Actor的名字和在世界中获取到的Actor中的名字不相同

详见博客置顶文章-UE遇到的各种问题

实现A星算法规划路径

代码

AStar_test2Character.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52


// 定义的变量
public:

// 自定义定时器句柄
FTimerHandle TimerHandle;

UFUNCTION()
void ShowWay();

// 自定义函数 - 进入FuncAStarMoveCaculate
UFUNCTION(BlueprintCallable)
void AStarMoveCaculate();

// A星算法移动代价计算函数
void FuncAStarMoveCaculate();

// 曼哈顿距离计算函数
int32 DistanceToDistination(Aboard* board);

// 用于存储样条点的坐标
TArray<FVector> AllPositions;

// 开始移动的节点
Aboard* MoveStartBoard = nullptr;

// 终点的坐标
AMyDestination* Destination = nullptr;

// 存储已经扫描过的节点
TArray<Aboard*> BoardHasScanned;

// 计算已经走过的步数
int32 moveStep = 0;

// 盒体检测参数
UPROPERTY(EditAnywhere,Category="BoxTrace")
FVector HalfSize = FVector(50.0f, 50.0f, 10.0f);

// 盒体检测参数
UPROPERTY(EditAnywhere,Category="BoxTrace")
FRotator Orientation = FRotator(0.0f, 45.0f, 0.0f);

// 判断方向
int32 isChange = -1;
//上一个坐标
FVector LastPosition;

// 加入指定位置的点于数组中
void AddSpecialPositionToArray();

AStar_test2Character.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198



void AAStar_test2Character::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
Super::SetupPlayerInputComponent(PlayerInputComponent);
// 绑定输入:键盘1 形成A星算法路径;键盘2 根据A星算法路径形成样条点;键盘3 AI开始按照A星算法规划路径移动(实际上是按照样条点移动)
PlayerInputComponent->BindAction(TEXT("showWay"), IE_Pressed, this, &AAStar_test2Character::ShowWay);
PlayerInputComponent->BindAction(TEXT("createSpine"), IE_Pressed, this, &AAStar_test2Character::SpineSet);
PlayerInputComponent->BindAction(TEXT("AiMove"), IE_Pressed, this, &AAStar_test2Character::AIMove);

}

void AAStar_test2Character::ShowWay()
{
// 获取World
UWorld* World = GetWorld();
if (!World) return;

// Step 1: 获取指定类的所有实例
TArray<AActor*> FoundActors;

// 获取StartActor 起始点 获取DestinationActor 终点
UGameplayStatics::GetAllActorsOfClass(World, AMyStart::StaticClass(), FoundActors);
AActor* StartActor = FoundActors.Num() > 0 ? FoundActors[0] : nullptr;
UGameplayStatics::GetAllActorsOfClass(World, AMyDestination::StaticClass(), FoundActors);
AActor* DestinationActor = FoundActors.Num() > 0 ? FoundActors[0] : nullptr;
if (!StartActor || !DestinationActor) return;

// 将终点的坐标放入变量 Destination
Destination = Cast<AMyDestination>(DestinationActor);

// 获取起始点的坐标位置 并添加到AllPositions(样条点数组)中
FVector StartActorLocation = StartActor->GetActorLocation();
AllPositions.Add(StartActorLocation);

// 初始化LastPosition,用于之后判断是否需要添加新的样条点
LastPosition = StartActorLocation;

// 自定义线条追踪穿过的Z轴
float StartZ = 100.0f;
float EndZ = -100.0f;
FVector StartTraceLocation = FVector(StartActorLocation.X, StartActorLocation.Y, StartZ);
FVector EndTraceLocation = FVector(StartActorLocation.X, StartActorLocation.Y, EndZ);

// Step 2: 进行射线追踪
FHitResult HitResult;

// 定义 trace 的行为参数
FCollisionQueryParams TraceParams(FName(TEXT("")), false, this);

// 定义 trace 的对象参数,即需要检测的对象类型
FCollisionObjectQueryParams ObjectQueryParams;
// 设置查询世界中动态的物体
ObjectQueryParams.AddObjectTypesToQuery(ECC_WorldDynamic);

// 进行 trace 射线检测
bool bHit = GetWorld()->LineTraceSingleByObjectType(
HitResult,
StartTraceLocation,
EndTraceLocation,
ObjectQueryParams,
TraceParams
);

// Step 3: 处理命中结果
if (!bHit) UE_LOG(LogTemp, Error, TEXT("射线没有命中!"));
AActor* HitActor = HitResult.GetActor();
UE_LOG(LogTemp, Warning, TEXT("命中的Actor是: %s"), *HitActor->GetClass()->GetName());

// Step 4: 类型转换为 BP_Board
Aboard* Board = Cast<Aboard>(HitActor);
if (!Board) UE_LOG(LogTemp, Warning, TEXT("命中了Actor,但不是BP_Board!"));

// 更新 MoveStartBoard 添加到 BoardHasScanned 中
MoveStartBoard = Board;
BoardHasScanned.Add(MoveStartBoard);

// 计算移动代价
AStarMoveCaculate();
}

void AAStar_test2Character::AStarMoveCaculate()
{
GetWorld()->GetTimerManager().SetTimer(TimerHandle,this,&AAStar_test2Character::FuncAStarMoveCaculate,0.01f,false);
}

void AAStar_test2Character::FuncAStarMoveCaculate()
{

// 初始化 当前位置至终点 的最小移动代价
int32 minMoveCost = 9999;

// Step 1: 初始化盒体追踪检测参数,获取前后左右四个方向的位置进行判断
TArray<FHitResult> OutHits;

// 设置追踪中心 的起点和终点的Z值
float StartZ = 100.0f;
float EndZ = -100.0f;
FVector Location = MoveStartBoard->GetActorLocation();
FVector StartTraceLocation = FVector(Location.X, Location.Y, StartZ);
FVector EndTraceLocation = FVector(Location.X, Location.Y, EndZ);

// 忽略的 Actor 数组和对象类型
TArray<AActor*> ActorsToIgnore;

// 指定要检测的对象类型数组
TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes;
ObjectTypes.Add(UEngineTypes::ConvertToObjectType(ECC_WorldDynamic));

// Step 2: 执行盒体追踪
bool bHit = UKismetSystemLibrary::BoxTraceMultiForObjects(
this, //当前对象作为追踪发起者
StartTraceLocation,
EndTraceLocation,
HalfSize,
Orientation,
ObjectTypes,
false, // 是否检测复杂碰撞 false表示简单碰撞
ActorsToIgnore,
EDrawDebugTrace::None, // 是否绘制调试信息
OutHits,
true // 是否返回命中结果
);

// Step 3: 循环遍历检测结果,得到下一个前进的点
if(OutHits.Num() > 0){
// Step 3.1: 计算得到检测结果中距离最近的点
for (const FHitResult& HitResult : OutHits)
{
// 获取该方向的命中结果
AActor* HitActor = HitResult.GetActor();
if (!HitActor) continue;

// 类型转换为 BP_Board
Aboard* Board = Cast<Aboard>(HitActor);
if (!Board || BoardHasScanned.Contains(Board)) continue;

// 若为BP_Board且不包含在 BoardHasScanned 中,计算移动代价 和最小的移动方向进行比较
BoardHasScanned.Add(Board);
int32 moveCost = DistanceToDistination(Board) + moveStep;

if (minMoveCost > moveCost)
{
MoveStartBoard = Board;
minMoveCost = moveCost;
}

}

// Step 3.2: 判断检测结果中距离最近的点 是否在可以移动的范围内
if(MoveStartBoard != nullptr)
{
// 判断该位置是否可以加入样条点数组中,函数在下一个专题代码中展示
AddSpecialPositionToArray();

// 修改moveStartBoard的颜色,来示该位置已经在A星算法规划的路线中
MoveStartBoard->SetColor(MoveStartBoard);

// 更新移动步数
moveStep++;

// 判断是否到达终点
if (!(DistanceToDistination(MoveStartBoard) < 2))
{
AStarMoveCaculate();
}
else
{
// 将终点加入AllPositions
AllPositions.Add(Destination->GetActorLocation());
}

}

}

}

// 获取 目标点距离终点的 曼哈顿距离
int32 AAStar_test2Character::DistanceToDistination(Aboard* board)
{
FVector BoardLocation = board->GetActorLocation();
FVector DistinationLocation = Destination->GetActorLocation();

// 表示每个网格单元的大小,用于将实际距离转换为 格子 数量
float GridSize = 100.0f;
float Dt = FMath::Abs(BoardLocation.X - DistinationLocation.X) + FMath::Abs(BoardLocation.Y - DistinationLocation.Y);
Dt = FMath::GridSnap(Dt,GridSize);
Dt /= GridSize;

int32 Distance = FMath::CeilToInt(Dt);

return Distance;

}

}

A星算法路径存入样条点数组中

难点

如何获取样条线组件

优化前:在AStar_test2Character.h创建一个ASpine类的变量SpineRef,然后在AStar_test2Character.cpp中创建了一个ASpine类的对象,对该对象执行函数操作;此时Spine.cpp类中的this指的是系统创建的对象。

优化后:直接在AStar_test2Character.cpp中使用GetAllActorsOfClass方法获取到真正的ASpine的引用,然后直接对ASpine本体执行函数操作;此时Spine.cpp类中的this指的是Spine.cpp本身。

优化前

AStar_test2Character类和Spine

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// AStar_test2Character.h
ASpine* SpineRef;

// AStar_test2Character.cpp

if (!SpineRef)
{
// 创建了一个Spine的动态对象,此步以后,由 GetAllActorsOfClass 找到的ASpine会有两个,一个是系统创建的,一个是自己创建的
SpineRef = GetWorld()->SpawnActor<ASpine>();

}
if (SpineRef)
{
UE_LOG(LogTemp, Warning, TEXT("进入执行SpineSet222"));
// 得到的结果是Spine0,即系统创建的ASpine类的动态对象
UE_LOG(LogTemp, Warning, TEXT("SpineRef是 %s"), *SpineRef->GetName());
SpineRef->setSpine();
}

===================================================
// Spine.cpp

// 查找名为 "BP_Spine" 的 Actor
ASpine* SpineActor = nullptr;
for (TActorIterator<ASpine> It(GetWorld()); It; ++It)
{
SpineActor = *It;
if (SpineActor && SpineActor->GetName().Contains("BP_Spine"))
{
UE_LOG(LogTemp, Warning, TEXT("Found BP_Spine: %s"), *SpineActor->GetName());
}
}
if (!SpineActor)
{
UE_LOG(LogTemp, Warning, TEXT("未找到名为 BP_Spine 的 Actor"));
return;
}

// 获取样条组件
USplineComponent* SplineComp = SpineActor -> FindComponentByClass<USplineComponent>();
if (!SplineComp) return;

优化后(减少了很多不必要的代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// AStar_test2Character.cpp

UWorld* World = GetWorld();
TArray<AActor*> FoundActors;
UGameplayStatics::GetAllActorsOfClass(World, ASpine::StaticClass(), FoundActors);
ASpine* SpineRefT = Cast<ASpine>(FoundActors[0]);
SpineRefT->setSpine();

===================================================
// Spine.cpp

// 获取样条组件
USplineComponent* SplineComp = this -> FindComponentByClass<USplineComponent>();
if (!SplineComp) return;

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// AStar_test2Character.cpp

// 判断是否可以加入样条点数组中,方向改变才加入
void AAStar_test2Character::AddSpecialPositionToArray()
{
// 判断当前点相对于上一个点要走的方向 只有横竖两个方向,故只判断X轴是否相等
FVector moveStartLocation = MoveStartBoard->GetActorLocation();
bool isXSame = moveStartLocation.X - LastPosition.X == 0.0f;

// isChange: -1 表示是加入的第一个点;0 表示上一个点的方向是Y轴的;1 表示上一个点的方向是X轴
if (isChange == -1)
{
if (isXSame)
{
isChange = 0;
}
else
{
isChange = 1;
}
LastPosition = moveStartLocation;
}
else // 不是第一个加入的点,判断方向有没有变化
{
if (isXSame)
{
if (isChange == 0) // 方向没有改变
{
LastPosition = moveStartLocation;
}
else
{
AllPositions.Add(LastPosition);
LastPosition = moveStartLocation;
isChange = 0;
}
}
else
{
if (isChange == 1) // 方向没有改变
{
LastPosition = moveStartLocation;
}
else
{
AllPositions.Add(LastPosition);
LastPosition = moveStartLocation;
isChange = 1;
}
}
}

}

人物实现AI行为树——按照样条线行走

难点

自定义实现AI行为树中的Decorator和Task节点

1. 创建C++类

Decorator应继承BTDecorator,Task应继承BTTaskNode;如果希望使用黑板键,可以继承自相应的有_BlackboardBase后缀的类,这个类会提供一个黑板键成员变量,当然也可以不继承这个_BlackboardBase,自己写

45cc20eedf263cb9d4100dca430d8478

2. 关键函数

只举例了代码中用到的部分函数,其他函数请自己参考BTDecorator.hBTTaskNode.h中的函数以及注释

Decorator的判断函数为
1
virtual bool CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const override;

重写这个函数,返回值是能否执行子节点

Task的开始执行函数
1
2
3
4
5
/** starts this task, should return Succeeded, Failed or InProgress
* (use FinishLatentTask() when returning InProgress)
* this function should be considered as const (don't modify state of object) if node is not instanced! */

AIMODULE_API virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory);

可用的返回值由三种

1
2
3
EBTNodeResult::Failed
EBTNodeResult::Succeeded
EBTNodeResult::InProgress

前两种自然就是立刻返回当前节点执行成功与否,最后一种是表示节点虽然已经开始执行,但暂时还没执行结束,当希望结束执行InProgress的节点(以成功执行举例),就需要调用

1
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);

该函数一般在TickTask中调用,也就是每个Tick都去检查是否执行完成。

TickTask函数原型
1
2
3
4
5
/** ticks this task 
* this function should be considered as const (don't modify state of object) if node is not instanced!
* bNotifyTick must be set to true for this function to be called
* Calling INIT_TASK_NODE_NOTIFY_FLAGS in the constructor of the task will set this flag automatically */
AIMODULE_API virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds);

根据上面的注释,我们可以知道,只有当设置bNotifyTick = true时TickTask才会执行;同理还有bNotifyActivation OnNodeActivationbNotifyDeactivationOnNodeDeactivation

如何在AIController中获取黑板,并修改其中的变量值

1. 定义变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取控制角色,初始化AI逻辑和状态
void OnPossess(APawn* possesPawn);

// AI移动的函数
UFUNCTION(BlueprintCallable, Category = "AI")
void goAIMove();

// 行为树
UPROPERTY(EditAnywhere, Category = "AI")
class UBehaviorTree* BehaviorTree;

// 黑板组件
UPROPERTY()
class UBlackboardComponent* BlackboardComp;

// 存储黑板资产的引用
UPROPERTY(EditDefaultsOnly, Category = "AI")
UBlackboardData* BlackboardAsset;

UBlackboardComponent* BlackboardCompUBlackboardData* BlackboardAsset的区别

  • **UBlackboardComponent**:这是一个实际的游戏性组件(Actor Component),你可以将其添加到你的AI控制器或者需要访问黑板数据的角色上。它负责运行时存储所有黑板键值对(Key-Value Pairs),这些键值对代表了影响AI决策的数据点。例如,目标位置、最近看到的敌人等。简单来说,UBlackboardComponent 就是AI用来在游戏过程中实时记录和查询各种状态的地方。
  • **UBlackboardData**:这是一个资产(Asset),定义了黑板的结构或模式。它描述了可以在UBlackboardComponent中使用的不同键(Keys)以及每个键的类型(比如Object, Vector, Bool等)。这个资产主要用于设计阶段,在编辑器中设置,决定了哪些键可用及其默认值。因此,UBlackboardData更像是一个模板或蓝图,指定了你希望在黑板上跟踪的所有信息的种类。

image-20250629142652730

2. 在OnPossess函数中初始化行为树、黑板组件

当一个 AIController 开始控制一个新的 Pawn(通常是NPC或玩家角色的基类)时,就会调用 OnPossess 函数。这个函数的主要作用是初始化与新控制的角色相关的任何AI逻辑或状态。具体来说,它允许AI控制器执行一些设置操作,比如为该角色创建行为树、设置黑板数据、初始化感知系统等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void AMyAIController::OnPossess(APawn* possesPawn)
{
Super::OnPossess(possesPawn);
// 检查黑板资产是否已设置
if (BlackboardAsset)
{
// 初始化 UBlackboardComponent 使用指定的 UBlackboardData 资产
if (UseBlackboard(BlackboardAsset, BlackboardComp))
{
UE_LOG(LogTemp, Warning, TEXT("黑板初始化成功"));

// 启动行为树(如果有)
if (BehaviorTree)
{
RunBehaviorTree(BehaviorTree);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("黑板初始化失败!请检查BlackboardAsset是否正确设置"));
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("请设置BlackboardAsset!"));
}

}

UseBlackboard(BlackboardAsset, BlackboardComp)函数调用的作用

具体来说,该函数的作用如下:

  • 参数1 (BlackboardAsset): 这是一个指向 UBlackboardData 类型对象的指针,定义了黑板中可用键(Keys)的结构和类型。它描述了哪些信息将被存储在黑板上,例如目标位置、最近发现的敌人等。
  • 参数2 (BlackboardComp): 这是一个指向 UBlackboardComponent 类型对象的引用,表示实际运行时用来存储和访问黑板数据的组件。通过这个组件,AI 系统可以在运行时读取和写入黑板上的数据。

当调用 UseBlackboard() 函数时,它会执行以下操作:

  1. 初始化黑板组件:基于提供的 BlackboardAsset,初始化关联的 BlackboardComp,使其准备好根据定义的模式存储和管理数据。
  2. 配置黑板键:根据 BlackboardAsset 中定义的键值对,配置 BlackboardComp 以确保所有必要的键都已准备就绪,并可以被行为树中的任务和服务所使用。
  3. 返回值:如果初始化成功,则返回 true,否则返回 false。这允许你在初始化失败的情况下进行错误处理或日志记录。

3. 在AIController类的函数中修改黑板中的变量值

1
2
3
4
5
6
7
8
9
10
11
// 设置黑板中的值
if (BlackboardComp)
{
UE_LOG(LogTemp, Warning, TEXT("获取到黑板中的值"));
// 修改黑板中的值,修改后,触发装饰器Decorator判断函数,执行Task中的执行函数
BlackboardComp->SetValueAsBool(FName("AI_Move"), true);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("无法获取黑板组件"));
}

修改黑板中的变量值后,会触发Decorator中的判断函数CalculateRawConditionValue,若返回True,则执行Task中的ExecuteTask函数,实现AI行为树的全部过程。

蓝图中AI行为树的配置如下

image-20250629143948740

代码

继承BTDecoratorUBTD_Judge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
UBTD_Judge::UBTD_Judge(FObjectInitializer const& ObjectInitializer)
{
// 设置此装饰器在每次 tick 时检查条件
this->bNotifyBecomeRelevant = true;
}

bool UBTD_Judge::CalculateRawConditionValue(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) const
{
// 获取与行为树组件关联的黑板组件
UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent();
if (BlackboardComp == nullptr)
{
UE_LOG(LogTemp, Warning, TEXT("no BlackboardComp"));
return false;
}
// UE_LOG(LogTemp, Warning, TEXT("进入筛选器"));

// 假设我们要检查名为 "AI_Move" 的布尔键
bool bAI_Move = BlackboardComp->GetValueAsBool("AI_Move");
UE_LOG(LogTemp, Warning, TEXT("筛选器获得的AI_Move的值:%s"), bAI_Move ? TEXT("true") : TEXT("false"));

// 根据黑板中的值返回条件结果
return bAI_Move;
}

继承BTTaskNodeUUBTT_MoveBySpine

Task的代码有问题,以后有时间会修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
UUBTT_MoveBySpine::UUBTT_MoveBySpine()
{
UE_LOG(LogTemp,Warning,TEXT("进入AI行为树中的Task"))
MoveKeySelector.AddBoolFilter(this,TEXT("AI_Move"));
}



void UUBTT_MoveBySpine::InitializeFromAsset(UBehaviorTree& Asset)
{
Super::InitializeFromAsset(Asset);
UE_LOG(LogTemp,Warning,TEXT("进入AI行为树中的Task的InitializeFromAsset"))

// 获取行为树使用的黑板
UBlackboardData* BBAsset = GetBlackboardAsset();
if (ensure(BBAsset))
{
// 根据黑板中的条目做初始化
MoveKeySelector.ResolveSelectedKey(*BBAsset);
}
}



EBTNodeResult::Type UUBTT_MoveBySpine::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
UE_LOG(LogTemp, Warning, TEXT("进入AI行为树的方法执行中"));


UWorld* World = GetWorld();
if (!World) return EBTNodeResult::Failed;



// 查找名为 "BP_Spine" 的 Actor
ASpine* SpineActor = nullptr;
for (TActorIterator<ASpine> It(GetWorld()); It; ++It)
{
SpineActor = *It;
if (SpineActor && SpineActor->GetName().Contains("BP_Spine"))
{

UE_LOG(LogTemp, Warning, TEXT("Found BP_Spine: %s"), *SpineActor->GetName());
}
}
if (!SpineActor)
{
UE_LOG(LogTemp, Warning, TEXT("未找到名为 BP_Spine 的 Actor"));
return EBTNodeResult::Failed;
}

// 获取样条组件
USplineComponent* SplineComp = SpineActor -> FindComponentByClass<USplineComponent>();
if (!SplineComp) return EBTNodeResult::Failed;

int32 NumberOfSplinePoints = SplineComp->GetNumberOfSplinePoints();
UE_LOG(LogTemp, Warning, TEXT("NumberOfSplinePoints的值为:%d"),NumberOfSplinePoints);
UE_LOG(LogTemp, Warning, TEXT("index值为:%d"),index);

// 获取 AI 控制器
AMyAIController* myAIController = Cast<AMyAIController>(OwnerComp.GetAIOwner());
if (!myAIController) return EBTNodeResult::Failed;

// 获取控制的角色
APawn* ControlledPawn = myAIController->GetPawn();
if (!ControlledPawn) return EBTNodeResult::Failed;


if (index >= NumberOfSplinePoints)
{
index = 1;
}


FVector Point = SplineComp->GetLocationAtSplinePoint(index, ESplineCoordinateSpace::World);
UE_LOG(LogTemp, Warning, TEXT("Points的值为:%s"),*Point.ToString());
UGameplayStatics::GetPlayerController(myAIController->GetWorld(), 0); // 确保世界上下文正确
UAIBlueprintHelperLibrary::SimpleMoveToLocation(myAIController, Point);

index ++;

UE_LOG(LogTemp, Warning, TEXT(""));


return EBTNodeResult::Succeeded;
}

继承AIControllerAMyAIController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// .h
// 获取控制角色,初始化AI逻辑和状态
void OnPossess(APawn* possesPawn);

// AI移动的函数
UFUNCTION(BlueprintCallable, Category = "AI")
void goAIMove();

// 行为树
UPROPERTY(EditAnywhere, Category = "AI")
class UBehaviorTree* BehaviorTree;

// 黑板组件
UPROPERTY()
class UBlackboardComponent* BlackboardComp;

// 存储黑板资产的引用
UPROPERTY(EditDefaultsOnly, Category = "AI")
UBlackboardData* BlackboardAsset;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
.cpp

AMyAIController::AMyAIController()
{
BehaviorTree = nullptr;
BlackboardComp = nullptr;
}

void AMyAIController::BeginPlay()
{
Super::BeginPlay();

}

void AMyAIController::OnPossess(APawn* possesPawn)
{
Super::OnPossess(possesPawn);
// 检查黑板资产是否已设置
if (BlackboardAsset)
{
// 初始化黑板 初始化 UBlackboardComponent 使用指定的 UBlackboardData 资产
if (UseBlackboard(BlackboardAsset, BlackboardComp))
{
UE_LOG(LogTemp, Warning, TEXT("黑板初始化成功"));

// 启动行为树(如果有)
if (BehaviorTree)
{
RunBehaviorTree(BehaviorTree);
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("黑板初始化失败!请检查BlackboardAsset是否正确设置"));
}
}
else
{
UE_LOG(LogTemp, Error, TEXT("请设置BlackboardAsset!"));
}

}



void AMyAIController::goAIMove()
{
UE_LOG(LogTemp, Warning, TEXT("AI开始移动"));

// 确保OnPossess已被调用
if (!BlackboardComp && !BlackboardAsset)
UE_LOG(LogTemp, Error, TEXT("BlackboardComp和BlackboardAsset均无法获取!"));
if (!BlackboardComp && BlackboardAsset)
{
UE_LOG(LogTemp, Warning, TEXT("黑板组件为空,尝试重新初始化"));
if (UseBlackboard(BlackboardAsset, BlackboardComp))
{
UE_LOG(LogTemp, Warning, TEXT("黑板重新初始化成功"));
}
else
{
UE_LOG(LogTemp, Error, TEXT("黑板重新初始化失败!"));
return;
}
}

UE_LOG(LogTemp, Warning, TEXT("尝试修改黑板值"));
// 设置黑板中的值
if (BlackboardComp)
{
UE_LOG(LogTemp, Warning, TEXT("获取到黑板中的值"));
// 修改黑板中的值,修改后,触发装饰器Decorator判断函数,执行Task函数
BlackboardComp->SetValueAsBool(FName("AI_Move"), true);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("无法获取黑板组件"));
}

}

参考文章:

UE4 C++自定义AI行为树的Decorator和Task节点

【UEC++】AI行为树及其相关蓝图部分在C++中的实现

UEC++学习(十一)AI行为树

UE5&C++ 在 AIController中绑定行为树,和黑板初始化