【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(三):基于BT行为树实现复杂敌人BOSS-AI
前言
- (题外话)nav2系列教材,yolov11部署,系统迁移教程我会放到年后一起更新,最近年末手头事情多,还请大家多多谅解。
- 回顾我们整个学习历程,我们已经学习了很多C++的代码特性,也学习了很多ROS2的跨进程通讯方式,也学会了很多路径规划的种种算法。那么如何将这些学习到的东西整合在一起,合并在工程中,使我们的机器人可以自主进行多任务执行和状态切换呢?本系列教程我们就来看看工程中最常用的几个AI控制结构:
- 第一期(状态模式):【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(一):从电梯出发的状态模式State Pattern
- 第二期(有限状态机FSM):【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(二):从FSM开始的2D游戏角色操控底层源码编写
- 行为树
BTtree
- 决策树
- 回顾上一节的总结,
FSM
通常通过状态和转换来表示系统的行为,适合简单的,线性的行为,但是当有大量状态和转换出现的时候,FSM
就不是很适合了。 - 那么这一节,我们就来看一看行为树的编写,并使用BT行为树实现复杂敌人BOSS-AI,取复现空洞骑士大黄蜂boss的攻击模式
0 前置知识std::function
- 在正式开始之前我们回顾一下一些C++冷知识
std::function
是 C++11 引入的一个标准库模板类,定义在头文件<functional>
中。它封装了一个可调用对象(如函数指针、lambda 表达式、函数对象等),并提供了一种通用的方式来存储和调用这些对象。使用std::function
可以让你将各种可调用对象作为参数传递、返回值、或存储在容器中,从而极大增强了代码的灵活性和可重用性。
0-1 std::function
的定义和基本用法
std::function
是一个模板类,其模板参数是函数的签名。- 例如,如果你想存储一个返回
int
的函数,它没有参数,那么你可以这样定义std::function
#include <iostream>
#include <functional> // 必须包含这个头文件std::function<int()> myFunction;
std::function
可以存储以下类型的可调用对象:- 普通函数
- Lambda 表达式
- 函数指针
- 函数对象(仿函数)
0-2 常见的 std::function
使用场景
- 存储普通函数
#include <iostream>
#include <functional>int add() {return 10 + 20;
}int main() {std::function<int()> f = add; // 存储普通函数std::cout << f() << std::endl; // 输出 30return 0;
}
- 存储 Lambda 表达式
#include <iostream>
#include <functional>int main() {std::function<int()> f = []() { return 10 + 20; }; // 存储 lambda 表达式std::cout << f() << std::endl; // 输出 30return 0;
}
- 存储函数指针
#include <iostream>
#include <functional>int multiply(int a, int b) {return a * b;
}int main() {std::function<int(int, int)> f = multiply; // 存储函数指针std::cout << f(5, 4) << std::endl; // 输出 20return 0;
}
- 存储函数对象(仿函数)
#include <iostream>
#include <functional>struct Adder {int operator()(int a, int b) {return a + b;}
};int main() {std::function<int(int, int)> f = Adder(); // 存储仿函数std::cout << f(10, 20) << std::endl; // 输出 30return 0;
}
0-3 std::function
的灵活性
std::function
提供了非常高的灵活性,它使得你可以把函数作为参数传递给其他函数,或将函数作为返回值返回。这使得在许多编程场景中,尤其是回调、事件处理、以及行为树中的动作和条件节点时,std::function
显得尤为重要。- 使用
std::function
作为参数传递给其他函数
#include <iostream>
#include <functional>void process(std::function<int(int, int)> operation) {std::cout << "Result: " << operation(10, 20) << std::endl;
}int main() {std::function<int(int, int)> add = [](int a, int b) { return a + b; };process(add); // 将 lambda 表达式作为参数传递给函数return 0;
}
1 行为树Behavior Tree概念解析
行为树(Behavior Tree,简称BT)
是一种用于建模和控制行为的结构化方式,广泛应用于人工智能(AI)领域,尤其是在游戏开发和机器人控制中。它能够让AI的决策过程更加灵活、易于理解和维护。行为树提供了一种层次化
、模块化
的方式来描述AI的行为,避免了传统状态机中可能出现的复杂和混乱的状态转移。
1-1 行为树的基本概念
- 行为树的核心思想是将AI的决策过程分解成一系列的节点,每个节点代表一个具体的行为或决策过程。这些节点通过不同的控制结构(如顺序执行、选择等)组织在一起,形成树状结构。行为树通常由以下几种类型的节点组成:
动作节点(Action Node)
这是树中的基本节点,代表AI执行的具体操作。例如,玩家移动、攻击敌人等。动作节点通常会返回“成功”
或“失败”
来表示操作的结果。条件节点(Condition Node)
条件节点用于检查某些条件是否满足
,例如玩家是否在视野范围内、AI的生命值是否低于某个阈值等。条件节点的作用是帮助决策是否执行某个动作
。复合节点(Composite Node)
复合节点通过控制其子节点的执行顺序
来管理AI行为的流程。复合节点有两种常见类型:顺序节点(Sequence Node)
:按顺序执行其子节点,直到其中某个子节点失败为止
。如果所有子节点都成功执行,则顺序节点本身返回成功。选择节点(Selector Node)
:按顺序执行其子节点,直到某个子节点成功为止。如果所有子节点都失败,则选择节点本身返回失败。
装饰节点(Decorator Node)
装饰节点通常用来修改或影响子节点的执行
。例如,可以用装饰节点来设置执行条件、重复执行某个动作、限制执行次数等。装饰节点不会直接影响行为树的控制流程,但可以改变子节点的行为或执行方式。
1-2 行为树的结构
- 行为树的树状结构通常从一个
根节点
开始,根节点下连接多个子节点。常见的节点类型组合如下:根节点(Root Node)
:根节点是行为树的起点,通常只有一个。顺序节点(Sequence)
:顺序节点按顺序依次执行其子节点,直到一个节点失败或者所有子节点都成功执行完毕。选择节点(Selector)
:选择节点逐一执行其子节点,直到找到一个成功的节点为止。如果没有子节点成功,则选择节点返回失败。
1-3 行为树的执行
- 行为树的执行通常是从根节点开始的,并且执行流程会根据控制结构(如顺序、选择等)依次向下。每个节点的执行会返回一个状态,常见的状态有:
成功(Success)
:节点执行完成并且达成目标。失败(Failure)
:节点执行未能达成目标。运行中(Running)
:节点正在执行,但尚未完成。
- 执行流程中,如果某个节点返回“失败”,则会根据树的结构影响父节点的执行。如果某个节点返回“运行中”,则父节点会继续等待该节点的结果,直到它完成执行。
1-4 行为树的优点
- 可扩展性和模块化
行为树通过分解复杂的决策过程为简单的行为模块,易于扩展和维护。每个节点代表一个独立的行为,开发者可以轻松地添加或修改节点来调整AI行为。 - 易于理解和调试
行为树的层次结构和执行流程清晰明了,使得AI行为更加直观易懂。相对于复杂的状态机或规则系统,行为树在调试和优化时更具优势。 - 灵活性
行为树能够灵活地组合不同类型的节点,形成复杂的行为模式。通过复合节点和装饰节点的组合,开发者可以实现非常复杂的行为逻辑。 - 避免状态爆炸问题
传统的状态机可能会因状态数量的增加而导致状态爆炸(状态之间的转移关系过于复杂),而行为树通过树状结构避免了这一问题,简化了状态之间的关系。
1-5 行为树的应用
-
游戏AI
行为树在游戏开发中有着广泛的应用,尤其是在控制NPC(非玩家角色)的行为时。通过行为树,可以让NPC在复杂的游戏环境中做出灵活且多样的反应。例如,敌人AI可以根据不同的游戏情境(如玩家的距离、视野等)选择攻击、追击或逃跑等行为。 -
机器人控制
在机器人技术中,行为树被用来描述机器人执行任务的决策过程。例如,机器人在执行复杂任务(如清扫、抓取物体、避障等)时,行为树能够帮助机器人做出合理的决策。- 回顾
ROS2
的nav2
框架,就是采用可如下行为树框架。
- 回顾
-
自动化系统
行为树也可以应用于自动化系统中,用来管理和优化工作流程或任务的执行。例如,自动化测试工具、智能家居控制等领域也能通过行为树来管理任务的执行和调度。
2 节点类型编程实现
2-1 思路分析
- 我们在 C++ 中实现行为树(Behavior Tree)通常包括以下几个关键步骤:
- 定义节点类型(Action, Condition, Composite, Decorator)
每个节点都有不同的功能,通常是基于类的结构设计。 - 实现节点的执行逻辑
每个节点通常会有一个Tick()
方法,用来执行节点的操作,并根据执行结果返回Success
、Failure
或Running
等状态。 - 组合节点
行为树的复合节点(如Sequence
和Selector
)用于控制节点的执行顺序和条件。 - 树的根节点
行为树的根节点是行为树的起始点,通常会有一个Run()
方法来启动树的执行。
2-2 定义节点状态类型
- 根据上面我们学习的,每个节点的执行会返回一个状态,常见的状态有:
成功(Success)
:节点执行完成并且达成目标。失败(Failure)
:节点执行未能达成目标。运行中(Running)
:节点正在执行,但尚未完成。
// 定义节点状态类型
enum class NodeStatus {Success,Failure,Running
};
2-3 抽象的节点类
- 正如上面所说的,每个节点通常会有一个
Tick()
方法,用来执行节点的操作,并根据执行结果返回Success
、Failure
或Running
等状态。 - 因此我们为了利于编成和扩展,我们定义一个抽象接口
// 抽象的节点类
class BehaviorTreeNode {
public:virtual ~BehaviorTreeNode() = default;virtual NodeStatus Tick() = 0; // 每个节点都需要实现Tick方法
};
2-4 动作节点
- 动作节点是树中的基本节点,代表AI执行的具体操作。
- 动作节点通常会返回
“成功”
或“失败”
来表示操作的结果。
// -------------------- 动作节点 --------------------
class ActionNode : public BehaviorTreeNode {
private:std::function<NodeStatus()> action;
public:ActionNode(std::function<NodeStatus()> actionFunc) : action(actionFunc) {}NodeStatus Tick() override {return action();}
};
2-5 条件节点
- 条件节点用于
检查某些条件是否满足
// -------------------- 条件节点 --------------------
class ConditionNode : public BehaviorTreeNode {
private:std::function<bool()> condition;
public:ConditionNode(std::function<bool()> conditionFunc) : condition(conditionFunc) {}NodeStatus Tick() override {if (condition()) {return NodeStatus::Success;}return NodeStatus::Failure;}
};
// -------------------- 选择节点 --------------------
class SelectorNode : public CompositeNode {
public:NodeStatus Tick() override {for (auto& child : children) {NodeStatus status = child->Tick();if (status == NodeStatus::Success) {return NodeStatus::Success;}if (status == NodeStatus::Running) {return NodeStatus::Running;}}return NodeStatus::Failure;}
};
2-6 复合节点(一):复合节点基类
- 复合节点控制其子节点的执行顺序。下面是两种常见类型:顺序节点和选择节点。
- 由于两种类型都涉及到子节点的存储和添加,因此我们添加基类
// -------------------- 复合节点 --------------------
class CompositeNode : public BehaviorTreeNode {
protected:std::vector<std::shared_ptr<BehaviorTreeNode>> children;
public:void AddChild(std::shared_ptr<BehaviorTreeNode> child) {children.push_back(child);}
};
2-7 复合节点(二):顺序节点
顺序节点(Sequence Node)
:按顺序执行其子节点,直到其中某个子节点失败为止
。如果所有子节点都成功执行,则顺序节点本身返回成功。
// -------------------- 顺序节点(Sequence) --------------------
class SequenceNode : public CompositeNode {
public:NodeStatus Tick() override {for (auto& child : children) {NodeStatus status = child->Tick();if (status == NodeStatus::Failure) {return NodeStatus::Failure;}if (status == NodeStatus::Running) {return NodeStatus::Running;}}return NodeStatus::Success;}
};
2-8 复合节点(二):选择节点
选择节点(Selector Node)
:按顺序执行其子节点,直到某个子节点成功为止。如果所有子节点都失败,则选择节点本身返回失败。
// -------------------- 选择节点(Selector) --------------------
class SelectorNode : public CompositeNode {
public:NodeStatus Tick() override {for (auto& child : children) {NodeStatus status = child->Tick();if (status == NodeStatus::Success) {return NodeStatus::Success;}if (status == NodeStatus::Running) {return NodeStatus::Running;}}return NodeStatus::Failure;}
};
2-9 装饰节点
- 装饰节点通常用来
修改或影响子节点的执行
。 - 装饰节点本身不执行操作,只是“装饰”或“修饰”其他节点。
- 如上图,装饰节点是“重复”装饰器,它会使得条件节点(检查玩家是否在视野内)重复执行。装饰节点的作用是让条件节点持续检查,直到条件成立为止。
// -------------------- 装饰节点 --------------------
class DecoratorNode : public BehaviorTreeNode {
protected:std::shared_ptr<BehaviorTreeNode> child;
public:void SetChild(std::shared_ptr<BehaviorTreeNode> node) {child = node;}
};// -------------------- 重复装饰节点(Repeat Until Success) --------------------
class RepeatUntilSuccessNode : public DecoratorNode {
public:NodeStatus Tick() override {NodeStatus status = child->Tick();if (status == NodeStatus::Success) {return NodeStatus::Success;}return NodeStatus::Running; // Keep running until success}
};
3 构建行为树
3-1 封装
- 我们把上述所有节点封装到一个头文件内
BTNodeType.hpp
#ifndef __BT_NODE_TYPE_HPP_
#define __BT_NODE_TYPE_HPP_
#include <vector>
#include <functional>
#include <memory>// 定义节点状态类型
enum class NodeStatus {Success,Failure,Running
};// 抽象的节点类
class BehaviorTreeNode {
public:virtual ~BehaviorTreeNode() = default;virtual NodeStatus Tick() = 0; // 每个节点都需要实现Tick方法
};// -------------------- 动作节点 --------------------
class ActionNode : public BehaviorTreeNode {
private:std::function<NodeStatus()> action;
public:ActionNode(std::function<NodeStatus()> actionFunc) : action(actionFunc) {}NodeStatus Tick() override {return action();}
};// -------------------- 条件节点 --------------------
class ConditionNode : public BehaviorTreeNode {
private:std::function<bool()> condition;
public:ConditionNode(std::function<bool()> conditionFunc) : condition(conditionFunc) {}NodeStatus Tick() override {if (condition()) {return NodeStatus::Success;}return NodeStatus::Failure;}
};// -------------------- 复合节点 --------------------
class CompositeNode : public BehaviorTreeNode {
protected:std::vector<std::shared_ptr<BehaviorTreeNode>> children;
public:void AddChild(std::shared_ptr<BehaviorTreeNode> child) {children.push_back(child);}
};// -------------------- 顺序节点(Sequence) --------------------
class SequenceNode : public CompositeNode {
public:NodeStatus Tick() override {for (auto& child : children) {NodeStatus status = child->Tick();if (status == NodeStatus::Failure) {return NodeStatus::Failure;}if (status == NodeStatus::Running) {return NodeStatus::Running;}}return NodeStatus::Success;}
};// -------------------- 装饰节点 --------------------
class DecoratorNode : public BehaviorTreeNode {
protected:std::shared_ptr<BehaviorTreeNode> child;
public:void SetChild(std::shared_ptr<BehaviorTreeNode> node) {child = node;}
};// -------------------- 重复装饰节点(Repeat Until Success) --------------------
class RepeatUntilSuccessNode : public DecoratorNode {
public:NodeStatus Tick() override {NodeStatus status = child->Tick();if (status == NodeStatus::Success) {return NodeStatus::Success;}return NodeStatus::Running; // Keep running until success}
};
#endif
3-2 构造行为树
- 我们来写一个简单的 行为树(Behavior Tree) 实现,用于模拟一个游戏中 AI 行为的决策过程。
#include <iostream>
#include "../include/BTNodeType.hpp"
int main() {// 动作节点:玩家移动auto moveAction = []() -> NodeStatus {std::cout << "Moving...\n";return NodeStatus::Success; // 假设移动成功};// 条件节点:检查玩家是否在视野内auto playerInSight = []() -> bool {std::cout << "Checking if player is in sight...\n";return true; // 假设玩家在视野内};// 创建动作节点和条件节点auto moveNode = std::make_shared<ActionNode>(moveAction);auto sightConditionNode = std::make_shared<ConditionNode>(playerInSight);// 创建顺序节点auto sequenceNode = std::make_shared<SequenceNode>();sequenceNode->AddChild(sightConditionNode);sequenceNode->AddChild(moveNode);// 运行行为树std::cout << "Running Behavior Tree...\n";NodeStatus status = sequenceNode->Tick();if (status == NodeStatus::Success) {std::cout << "Behavior succeeded!\n";} else if (status == NodeStatus::Failure) {std::cout << "Behavior failed.\n";} else {std::cout << "Behavior is running.\n";}return 0;
}
-
我们编译执行后有
-
-
检查条件:
sightConditionNode
(条件节点)检查玩家是否在视野内,返回true
(假设玩家在视野内)。
- 执行动作:条件成立后,
moveNode
(动作节点)执行玩家移动,返回NodeStatus::Success
。 - 行为树成功:所有节点都成功执行,
sequenceNode
返回Success
,最终输出"Behavior succeeded!"
。
3-3 封装行为树
- 行为树的树状结构通常从一个
根节点
开始,根节点下连接多个子节点。 - 我们封装为
BT.hpp
// -------------------- 行为树(BehaviorTree) --------------------
class BehaviorTree {
private:std::shared_ptr<BehaviorTreeNode> root; // 树的根节点
public:// 设置根节点void SetRoot(std::shared_ptr<BehaviorTreeNode> rootNode) {root = rootNode;}// 启动行为树的执行NodeStatus Tick() {if (root) {return root->Tick();}return NodeStatus::Failure;}
};
- 如此我们可以修正main函数
#include <iostream>
#include "../include/BT.hpp"
int main() {// 动作节点:玩家移动auto moveAction = []() -> NodeStatus {std::cout << "Moving...\n";return NodeStatus::Success; // 假设移动成功};// 条件节点:检查玩家是否在视野内auto playerInSight = []() -> bool {std::cout << "Checking if player is in sight...\n";return true; // 假设玩家在视野内};// 创建动作节点和条件节点auto moveNode = std::make_shared<ActionNode>(moveAction);auto sightConditionNode = std::make_shared<ConditionNode>(playerInSight);// 创建顺序节点auto sequenceNode = std::make_shared<SequenceNode>();sequenceNode->AddChild(sightConditionNode);sequenceNode->AddChild(moveNode);// 创建行为树并设置根节点BehaviorTree behaviorTree;behaviorTree.SetRoot(sequenceNode);// 运行行为树std::cout << "Running Behavior Tree...\n";NodeStatus status = behaviorTree.Tick();if (status == NodeStatus::Success) {std::cout << "Behavior succeeded!\n";} else if (status == NodeStatus::Failure) {std::cout << "Behavior failed.\n";} else {std::cout << "Behavior is running.\n";}return 0;
}
4 利用行为树复现空洞骑士大黄蜂的底层AI逻辑
4-1 问题分析
- 大黄蜂的战斗模式通常包含多个阶段和攻击方式,例如:
- 快速移动:大黄蜂会迅速在场景中移动。
- 刺客突刺:大黄蜂会向玩家冲刺并进行攻击。
- 远程攻击:大黄蜂可能使用一些远程攻击。
- 战斗模式切换:在不同的生命值阶段,大黄蜂的攻击模式可能会变化。
- 逃避行为:当大黄蜂受到威胁时,它可能会通过跳跃或飞行等行为来进行规避。
- 我们可以通过下属节点图来大致概括
4-2 文件架构
- 我们有如下文件架构
├── CMakeLists.txt
├── include
│ ├── AggressiveAttackNode.hpp
│ ├── AssassinDashNode.hpp
│ ├── BT.hpp
│ ├── BTNodeType.hpp
│ ├── EvadeDecorator.hpp
│ ├── HornetAI.hpp
│ ├── QuickMove.hpp
│ └── RangedAttackNode.hpp
├── merge.sh
└── src└── main.cpp
4-3 具体节点书写
- 我们需要分别完成
- 快速移动:大黄蜂会迅速在场景中移动。
- 刺客突刺:大黄蜂会向玩家冲刺并进行攻击。
- 远程攻击:大黄蜂可能使用一些远程攻击。
- 战斗模式切换:在不同的生命值阶段,大黄蜂的攻击模式可能会变化。
- 逃避行为:当大黄蜂受到威胁时,它可能会通过跳跃或飞行等行为来进行规避。
- 快速移动
QuickMoveNode.hpp
#include "../include/BTNodeType.hpp"
// -------------------- 快速移动节点 --------------------
class QuickMoveNode : public ActionNode {
public:QuickMoveNode() : ActionNode([this]() -> NodeStatus {return QuickMove();}) {}NodeStatus QuickMove() {// 模拟大黄蜂快速移动std::cout << "Boss is quickly moving towards the player!" << std::endl;return NodeStatus::Success;}
};
- 刺客突刺
AssassinDashNode.hpp
#include "../include/BTNodeType.hpp"class AssassinDashNode : public ActionNode {
public:AssassinDashNode() : ActionNode([this]() -> NodeStatus {return AssassinDash();}) {}NodeStatus AssassinDash() {std::cout << "Boss performs an assassin dash!" << std::endl;return NodeStatus::Success;}
};
- 远程攻击节点
RangedAttackNode.hpp
#include "../include/BTNodeType.hpp"class RangedAttackNode : public ActionNode {
public:RangedAttackNode() : ActionNode([this]() -> NodeStatus {return RangedAttack();}) {}NodeStatus RangedAttack() {std::cout << "Boss performs a ranged attack!" << std::endl;return NodeStatus::Success;}
};
- 攻击模式的变化
AggressiveAttackNode.hpp
#include "../include/BTNodeType.hpp"// 攻击节点示例
class AggressiveAttackNode : public ActionNode {
public:AggressiveAttackNode() : ActionNode([this]() -> NodeStatus {return AggressiveAttack();}) {}NodeStatus AggressiveAttack() {// 模拟大黄蜂的激进攻击std::cout << "Boss performs an aggressive attack!" << std::endl;return NodeStatus::Success;}
};
- 回避行为
EvadeDecorator.hpp
#include "../include/BTNodeType.hpp"
class EvadeDecorator : public DecoratorNode {
private:int bossHealth;
public:EvadeDecorator(int health) : bossHealth(health) {}NodeStatus Tick() override {if (bossHealth < 50) {return Evade();}return child->Tick(); // 如果不需要回避,执行正常行为}private:NodeStatus Evade() {std::cout << "Boss is evading due to low health!" << std::endl;return NodeStatus::Success;}
};
4-4 行为树构建
- 我们
#include "./AggressiveAttackNode.hpp"
#include "./RangedAttackNode.hpp"
#include "./AssassinDashNode.hpp"
#include "./EvadeDecorator.hpp"
#include "./QuickMove.hpp"
#include "./BT.hpp"// -------------------- HornetAI --------------------
class HornetAI {
private:std::shared_ptr<BehaviorTree> behaviorTree;int bossHealth;int playerDistance; // 玩家与大黄蜂的距离public:HornetAI(int health, int distance) : bossHealth(health), playerDistance(distance) {behaviorTree = std::make_shared<BehaviorTree>();// 创建选择节点(Selector)auto selectorNode = std::make_shared<SelectorNode>();// 创建子节点:快速移动、远程攻击、刺客突刺、激进攻击、逃避if (playerDistance <= 3){// 玩家距离近,执行近战攻击auto assassinDashNode = std::make_shared<AssassinDashNode>();selectorNode->AddChild(assassinDashNode); // 刺客突刺}else if (playerDistance <= 5) {// 玩家距离适中,执行快速移动auto quickMoveNode = std::make_shared<QuickMoveNode>();selectorNode->AddChild(quickMoveNode); // 快速移动} else {// 玩家距离远,执行远程攻击auto rangedAttackNode = std::make_shared<RangedAttackNode>();selectorNode->AddChild(rangedAttackNode); // 远程攻击}auto aggressiveAttackNode = std::make_shared<AggressiveAttackNode>();auto evadeDecorator = std::make_shared<EvadeDecorator>(bossHealth);selectorNode->AddChild(aggressiveAttackNode); // 激进攻击selectorNode->AddChild(evadeDecorator); // 逃避行为// 设置行为树根节点behaviorTree->SetRoot(selectorNode);// 执行行为树behaviorTree->Tick();}void RunBehaviorTree() {std::cout << "Running Behavior Tree...\n";NodeStatus status = behaviorTree->Tick();if (status == NodeStatus::Success) {std::cout << "Behavior succeeded!\n";} else if (status == NodeStatus::Failure) {std::cout << "Behavior failed.\n";} else {std::cout << "Behavior is running.\n";}}
};
HornetAI
类通过行为树(BehaviorTree
)来决定大黄蜂的行为。- 行为树的根节点是一个选择节点(
SelectorNode
),它根据玩家与大黄蜂的距离来决定执行不同的行为:- 刺客突刺(当玩家很近时)。
- 快速移动(当玩家适中距离时)。
- 远程攻击(当玩家距离较远时)。
- 还包括激进攻击和回避行为,当大黄蜂血量低时会回避。
4-5 代码测试
- 我们在主函数中测试
#include "../include/HornetAI.hpp"// 主函数
int main() {// 测试Boss血量为60,玩家距离为4(应该执行快速移动)HornetAI hornetAI1(60, 4);hornetAI1.RunBehaviorTree();std::cout << "\n";// 测试Boss血量为40,玩家距离为2(应该执行刺客突刺)HornetAI hornetAI2(40, 2);hornetAI2.RunBehaviorTree();std::cout << "\n";// 测试Boss血量为30,玩家距离为6(应该执行远程攻击)HornetAI hornetAI3(30, 6);hornetAI3.RunBehaviorTree();return 0;
}
- 可以看到我们的代码正常执行了!!!
5 FSM有限状态机和BT行为树对比
5-1 概念回顾
- 有限状态机 (FSM):
- 状态:AI 系统的不同情况或行为。
- 转移:从一个状态到另一个状态的变化,通常由事件或条件触发。
- 事件:触发状态转移的条件,可以是时间、玩家动作等。
- 图示:FSM 通过一组状态和状态之间的转换来表示决策过程。
- 行为树 (BT):
- 节点:行为树由多个节点构成,节点有不同类型:动作节点(执行具体的行为)、条件节点(检查条件是否满足)、复合节点(组合子节点)。
- 树状结构:行为树通常以树的形式组织,根节点通过选择或顺序节点组织决策。
- Tick:行为树通常是持续进行的,每次Tick都评估树的节点并做出决定。
5-2 结构对比
特性 | 有限状态机(FSM) | 行为树(BT) |
---|---|---|
结构 | 由状态和转移组成,状态间有明确的转换规则 | 树状结构,由多个节点构成(选择节点、顺序节点、动作节点等) |
决策方式 | 基于当前状态和输入事件,简单的状态跳转 | 基于树形结构的条件判断,依赖多个节点的组合 |
层次性 | 一般没有层级,所有状态在同一级 | 节点具有层级,能更好地组织和管理复杂行为 |
易于理解 | 适合简单任务,易于理解和实现 | 更适合复杂行为,结构更清晰,扩展性好 |
5-3 控制流程对比
- FSM(有限状态机):
- 每次只有一个活动状态,状态之间的切换是根据输入事件或条件触发的。
- 状态机通常是"单一行为",每次执行时都只能处于一个状态。例如,在战斗中,NPC 可能会有攻击、巡逻、躲避等状态,而每个状态只有一个行为。
- BT(行为树):
- 行为树是一个并行决策的结构,能在多个子树之间进行选择或顺序执行。行为树的节点可以同时存在不同的活动状态(例如,激进攻击和回避)。
- 每个节点都可以独立评估是否执行。不同的节点(例如动作节点、条件节点等)可以组合出复杂的行为决策。
5-4 扩展性和灵活性
- FSM(有限状态机):
- 对于简单的、线性的行为系统非常适用,但对于复杂和多变的任务会变得难以维护。
- 需要显式地在状态之间编写转移规则,复杂的状态机会有很多冗余的转移,使得系统变得难以管理和调试。
- 扩展性差,因为新状态的加入可能需要改动现有状态之间的转移规则。
- BT(行为树):
- 更适合复杂的决策逻辑和动态行为。通过组合不同的节点,可以灵活地表达多种行为。
- 新的行为或状态可以很容易地通过添加或修改节点来扩展,而不必大幅修改现有结构。
- 行为树的分层结构使得它比状态机更易于维护和扩展,尤其是当AI需要处理复杂的决策时。
6 总结
- 本文我们介绍了如何实现BT行为树的各种类型的节点以及介绍了行为树的工作原理,并且利用行为树复现空洞骑士大黄蜂的底层AI逻辑。
- 如有错误,欢迎指出!!!!
- 感谢大家的支持!!!!祝大家圣诞节快乐!!
相关文章:
【C++决策和状态管理】从状态模式,有限状态机,行为树到决策树(三):基于BT行为树实现复杂敌人BOSS-AI
前言 (题外话)nav2系列教材,yolov11部署,系统迁移教程我会放到年后一起更新,最近年末手头事情多,还请大家多多谅解。回顾我们整个学习历程,我们已经学习了很多C的代码特性,也学习了很多ROS2的跨…...
SpringCloudAlibaba技术栈-Higress
1、什么是Higress? 云原生网关,干啥的?用通俗易懂的话来说,微服务架构下Higress 就像是一个智能的“交通警察”,它站在你的网络世界里,负责指挥和调度所有进出的“车辆”(也就是数据流量)。它的…...
《信息传播:人工智能助力驱散虚假信息阴霾》
在信息爆炸的时代,虚假信息和谣言如同脱缰野马,肆意传播,对社会秩序和公众生活造成了严重影响。人工智能作为一种强大的技术工具,正逐渐成为信息传播的有力助手,为防止虚假信息和谣言扩散提供了新的途径。 虚假信息和…...
玩客云v1.0 刷机时无法识别USB
v1.0刷机时公对公插头掉了,刷机失败,再次刷机,一直提示无法识别的USB设备,此时LED一直不亮,就像是刷成砖了一样,查了好多文章最后发现正面还有一个地方需要短接。 背面的短接点 【免费】玩客云刷机包s805-…...
STM32F103RCT6学习之五:ADC
1.ADC基础 ADC(Analog-Digital Converter)模拟-数字转换器ADC可以将引脚上连续变化的模拟电压转换为内存中存储的数字变量,建立模拟电路到数字电路的桥梁12位逐次逼近型ADC,1us转换时间 输入电压范围:0~3.3Vÿ…...
通过Cephadm工具搭建Ceph分布式存储以及通过文件系统形式进行挂载的步骤
1、什么是Ceph Ceph是一种开源、分布式存储系统,旨在提供卓越的性能、可靠性和可伸缩性。它是为了解决大规模数据存储问题而设计的,使得用户可以在无需特定硬件支持的前提下,通过普通的硬件设备来部署和管理存储解决方案。Ceph的灵活性和设计…...
flink+kafka实现流数据处理学习
在应用系统的建设过程中,通常都会遇到需要实时处理数据的场景,处理实时数据的框架有很多,本文将以一个示例来介绍flinkkafka在流数据处理中的应用。 1、概念介绍 flink:是一个分布式、高可用、高可靠的大数据处理引擎,…...
SpringBoot使用外置的Servlet容器(详细步骤)
嵌入式Servlet容器:应用打成可执行的jar 优点:简单、便携; 缺点:默认不支持JSP、优化定制比较复杂.; 外置的Servlet容器:外面安装Tomcat---应用war包的方式打包; 操作步骤: 方式一&…...
C# 中的委托与事件:实现灵活的回调机制
C#中的委托(Delegate)和事件(Event)。委托和事件是C#中非常重要的特性,它们允许你实现回调机制和发布-订阅模式,从而提高代码的灵活性和解耦程度。通过使用委托和事件,你可以编写更加模块化和可…...
大模型辅助测试的正确打开方式?
测试的基本目的之一,是对被测对象进行质量评估。换言之,是要提供关于被测对象质量的“确定性”。因此,我们很忌讳在测试设计中引入“不确定性”,比如采用不可靠的测试工具、自动化测试代码逻辑复杂易错、测试选择假设过于主观等等…...
设计模式之代理模式
代理模式代码示例:https://blog.csdn.net/weixin_39865508/article/details/141924680 代理模式的左右,一定程度上不暴露被代课对象的内容,更安全,也减少系统的耦合性 静态代理 代理对象和被代理对象都继承同一个接口 在代理对象…...
win11中win加方向键失效的原因
1、可能是你把win键锁了: 解决办法:先按Fn键,再按win键 2、可能是可能是 贴靠窗口设置 中将贴靠窗口关闭了,只需要将其打开就好了...
Html——11. 页面跳转
<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>页面跳转</title><meta http-equiv"refresh" content"5; urlhttps://www.51zxw.net"/><!--<meta http-equiv"refresh" co…...
要查询 `user` 表中 `we_chat_open_id` 列不为空的用户数量
要查询 user 表中 we_chat_open_id 列不为空的用户数量,你可以使用以下 SQL 查询语句: SELECT COUNT(*) FROM user WHERE we_chat_open_id IS NOT NULL AND we_chat_open_id ! ;解释: SELECT COUNT(*): 表示要计算符合条件的行数。FROM us…...
docker-compos mysql5.7主从配置
docker-compos mysql5.7主从配置 docker-compose目录结构 配置文件 master/my.cnf [client] port 3306 socket /var/run/mysqld/mysqld.sock[mysqld_safe] pid-file /var/run/mysqld/mysqld.pid socket /var/run/mysqld/mysqld.sock nice 0…...
关于无线AP信道调整的优化(锐捷)
目录 一、信道优化的基本原则二、2.4G频段信道优化三、5G频段信道优化四、信道优化代码具体示例五、其他优化措施 一、信道优化的基本原则 信道优化旨在减少信道间的干扰,提高网络覆盖范围和信号质量。基本原则包括: 1. 选择合适的信道:根据…...
Flask入门一(介绍、Flask安装、Flask运行方式及使用、虚拟环境、调试模式、配置文件、路由系统)
文章目录 一、Flask介绍二、Flask创建和运行 1.安装2.快速使用3.Flask小知识4.flask的运行方式 三、Werkzeug介绍四、Jinja2介绍五、Click CLI 介绍六、Flask安装 介绍watchdog使用python–dotenv使用(操作环境变量) 七、虚拟环境 介绍Mac/linux创建虚拟…...
解决Docker国内网络问题
6月后以来,大量Docker镜像网站停服,Docker无法下载安装 本仓库致力于解决国内网络原因无法使用Docker的问题。 特点: 使用Github Action将官网的安装脚本/安装包定时下载到本项目Release,供国内使用官方安装包,安全可…...
Anaconda 安装与虚拟环境创建完整指南
Anaconda 安装与虚拟环境创建完整指南 Anaconda 是目前最流行的 Python 和数据科学工具集之一,它不仅可以轻松管理 Python 包,还能提供强大的虚拟环境功能,避免项目之间的依赖冲突。如果你是机器学习、数据科学或计算机视觉的开发者…...
【Java数据结构】LinkedList与链表
认识LinkedList LinkedList就是一个链表,它也是实现List接口的一个类。LinkedList就是通过next引用将所有的结点链接起来,所以不需要数组。LinkedList也是以泛型的方法实现的,所以使用这个类都需要实例化对象。 链表分为很多种,比…...
Linux 搭建 nginx+keepalived 高可用 | Nginx反向代理
注意:本文为 “Linux 搭建 nginxkeepalived (主备双主模式) 高可用 | Nginx反向代理” 相关文章合辑。 KeepalivedNginx实现高可用(HA) xyang0917 于 2016-09-17 00:24:15 发布 keepalived 的 HA 分为抢占模式和非抢占模式,抢占…...
17_HTML5 Web 存储 --[HTML5 API 学习之旅]
HTML5 Web 存储(Web Storage)是 HTML5 引入的一种在用户浏览器中存储数据的机制。它提供了比传统的 cookies 更加方便和强大的功能,包括更大的存储空间、更好的性能以及更简单的 API。Web 存储主要分为两种类型:localStorage 和 s…...
uni-app(优医咨询)项目实战 - 第7天
学习目标: 能够基于 WebSocket 完成问诊全流程 能够使用 uniCloud 云存储上传文件 能够完成查看电子处方的功能 能够完成医生评价的功能 一、问诊室 以对话聊天的方式向医生介绍病情并获取诊断方案,聊天的内容支持文字和图片两种形式。 首先新建一…...
今日总结 2024-12-28
今天全身心投入到鸿蒙系统下 TCPSocket 的学习中。从最基础的 TCP 协议三次握手、四次挥手原理重新梳理,深刻理解其可靠连接建立与断开机制,这是后续运用 TCPSocket 无误通信的根基。在深入鸿蒙体系时,仔细研读了其为 TCPSocket 封装的 API&a…...
ip归属地怎么判定?如何查看自己ip属地
在当今数字化时代,IP地址作为互联网通信的基础,扮演着至关重要的角色。而IP归属地的判定与查看,不仅关乎网络安全、隐私保护,还直接影响到社交平台的信任机制与信息传播的真实性。本文将深入探讨IP归属地的判定原理以及如何查看自…...
4.采用锁操作并支持等待功能的线程安全队列
分析 书接上文 修改push()似乎并不困难:在函数末尾加上对data_cond.notify_one()的调用即可,与代码清单1(第一篇文章)一样。事情其实没那么简单,我们之所以采用精细粒度的锁,目的是尽可能提高并发操作的数量。如果在notify_one()调用期间&a…...
螺杆支撑座在运用中会出现哪些问题?
螺杆支撑座是一种用于支撑滚珠螺杆的零件,通常用于机床、数控机床、自动化生产线等高精度机械设备中。在运用中可能会出现多种问题,这些问题源于多个方面,以下是对可能出现的问题简单了解下: 1、安装不当:安装过程中没…...
OSI 七层模型 | TCP/IP 四层模型
注:本文为 “OSI 七层模型 | TCP/IP 四层模型” 相关文章合辑。 未整理去重。 OSI 参考模型(七层模型) BeretSEC 于 2020-04-02 15:54:37 发布 OSI 的概念 七层模型,亦称 OSI(Open System Interconnection…...
秒鲨后端之MyBatis【2】默认的类型别名、MyBatis的增删改查、idea中设置文件的配置模板、MyBatis获取参数值的两种方式、特殊SQL的执行
别忘了请点个赞收藏关注支持一下博主喵!!!! ! ! 下篇更新: 秒鲨后端之MyBatis【3】自定义映射resultMap、动态SQL、MyBatis的缓存、MyBatis的逆向工程、分页插件。 默认的类型别名 MyBatis的增删改查 添加 <!--int insertUs…...
快云服务器小助手getdetail存在任意文件文件读取漏洞
免责声明: 本文旨在提供有关特定漏洞的深入信息,帮助用户充分了解潜在的安全风险。发布此信息的目的在于提升网络安全意识和推动技术进步,未经授权访问系统、网络或应用程序,可能会导致法律责任或严重后果。因此,作者不对读者基于本文内容所采取的任何行为承担责任。读者在…...
尚硅谷Vue3入门到实战 —— 02 编写 App 组件
根目录下的 index.html 是项目的入口文件,默认的具体内容如下: src 文件夹分析 <!DOCTYPE html> <html lang""><head><meta charset"UTF-8"><link rel"icon" href"/favicon.ico"&…...
Java - 日志体系_Simple Logging Facade for Java (SLF4J)日志门面_SLF4J实现原理分析
文章目录 官网SLF4J 简单使用案例分析SLF4J 获取 Logger 的原理获取 ILoggerFactory 的过程获取 Logger 的过程SLF4J 与底层日志框架的集成 小结 官网 https://slf4j.org/ Simple Logging Facade for Java (SLF4J) 用作各种日志记录框架(e.g…...
Flutter 调试环境下浏览器网络请求跨域问题解决方案
本篇文章主要讲解,Flutter调试环境情况下,浏览器调试报错跨域问题的解决方法,通过本篇文章你可以快速掌握Flutter调试环境情况下的跨域问题。 日期:2024年12月28日 作者:任聪聪 报错现象: 报文信息…...
python编译为可执行文件
1.用py2exe生成可执行文件 目前,在py2exe 0.9.2版本已经支持python3.x,它可以将python程序打包为windows下独立的可执行文件。 要使用py2exe,首先要编写一个编译程序(例如编写一个名为setup.py的程序),然后在python中运行…...
【机器学习(九)】分类和回归任务-多层感知机(Multilayer Perceptron,MLP)算法-Sentosa_DSML社区版 (1)111
文章目录 一、算法概念111二、算法原理(一)感知机(二)多层感知机1、隐藏层2、激活函数sigma函数tanh函数ReLU函数 3、反向传播算法 三、算法优缺点(一)优点(二)缺点 四、MLP分类任务…...
spring cloud微服务-OpenFeign的使用
OpenFeign的使用 openFeign的作用是服务间的远程调用 ,比如通过OpenFeign可以实现调用远程服务。 已经有了LoadBalancer为什么还要用openFeign? 在微服务架构中,LoadBalancer和OpenFeign虽然都提供了服务间调用的能力,但它们的设计目的和…...
欢迪迈手机商城设计与实现基于(代码+数据库+LW)
摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本欢迪迈手机商城就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息…...
GCP Cloud Architect exam - PASS
备考指南 推荐视频课程 https://www.udemy.com/course/google-cloud-architect-certifications/?couponCodeKEEPLEARNING 推荐题库 https://www.udemy.com/course/gcp-professional-cloud-architect-exam-practice-tests-2024/?couponCodeKEEPLEARNING 错题集 http…...
通过 `@Configuration` 和 `WebMvcConfigurer` 配置 Spring MVC 中的静态资源映射
通过 Configuration 和 WebMvcConfigurer 配置 Spring MVC 中的静态资源映射 一、前言1. 了解静态资源的默认配置2. 使用 Configuration 和 WebMvcConfigurer 自定义资源映射示例:将 /upload/ 和 /img/ 映射到不同的文件系统目录解释:为什么使用 classpa…...
敏捷测试文化的转变
敏捷文化是敏捷测试转型的基础,只有具备敏捷文化的氛围,对组织架构、流程和相关测试实践的调整才能起作用。在前面的敏捷测试定义中,敏捷测试是遵从敏捷软件开发原则的一种测试实践,这意味着敏捷的价值观。 此外,从传…...
深度学习:从原理到搭建基础模型
引言: 深度学习为什么火? 深度学习在处理复杂的感知和模式识别任务方面展现出了前所未有的能力。以图像识别为例,深度学习模型(如卷积神经网络 CNN)能够识别图像中的各种物体、场景和特征,准确率远超传统的计算机视觉方法。 当然这之中也还因为 大数据时代的推动(随着…...
MySQL和HBase的对比
Mysql :关系型数据库,主要面向 OLTP ,支持事务,支持二级索引,支持 sql ,支持主从、 Group Replication 架构模型(此处以 Innodb 为例,不涉及别的存储引擎)。 HBase &am…...
Gateway Timeout504 网关超时的完美解决方法
引言 在Web开发中,遇到HTTP状态码504(Gateway Timeout)是相当常见的。这个状态码表示前端服务器(如负载均衡器或代理服务器)作为网关工作时,在尝试访问后端服务器处理请求时未能及时得到响应。本文将探讨导…...
【探花交友】day03—MongoDB基础
目录 课程介绍 1、通用设置 1.1 需求分析 1.2 查询通用设置 1.2 陌生人问题 1.3 通知设置 1.4 黑名单管理 2、MongoDB简介 1.1、MongoDB简介 1.2、MongoDB的特点 1.3 数据类型 3、MongoDB入门 2.1、数据库以及表的操作 2.2、新增数据 2.3、更新数据 2.4、删除数…...
总结-常见缓存替换算法
缓存替换算法 1. 总结 1. 总结 常见的缓存替换算法除了FIFO、LRU和LFU还有下面几种: 算法优点缺点适用场景FIFO简单实现可能移除重要数据嵌入式系统,简单场景LRU局部性原理良好维护成本高,占用更多存储空间内存管理,浏览器缓存L…...
宏集eX710物联网工控屏在石油开采机械中的应用与优势
案例概况 客户:天津某石油机械公司 应用产品:宏集eX710物联网工控屏 应用场景:钻井平台设备控制系统 一、应用背景 石油开采和生产过程复杂,涵盖钻井平台、采油设备、压缩机、分离器、管道输送系统等多种机械设备。这些设备通…...
【社区投稿】自动特征auto trait的扩散规则
自动特征auto trait的扩散规则 公式化地概括,auto trait marker trait derived trait。其中,等号右侧的marker与derived是在Rustonomicon书中的引入的概念,鲜见于Rust References。所以,若略感生僻,不奇怪。 marker …...
【MySQL】第一弹----库的操作及数据类型
笔上得来终觉浅,绝知此事要躬行 🔥 个人主页:星云爱编程 🔥 所属专栏:MySQL 🌷追光的人,终会万丈光芒 🎉欢迎大家点赞👍评论📝收藏⭐文章 一、SQL 语句分类 DDL:数据定…...
【服务器主板】定制化:基于Intel至强平台的全新解决方案
随着数据处理需求不断增长,服务器硬件的发展也在持续推进。在这一背景下,为用户定制了一款全新的基于Intel至强平台的服务器主板,旨在提供强大的计算能力、优异的内存支持以及高速存储扩展能力。适用于需要高性能计算、大规模数据处理和高可用…...
Flutter路由工具类RouteUtils,可二次开发,拿来即用
一、RouteUtils路由核心类 /*** 路由封装*/ class RouteUtils {RouteUtils._();static final navigatorKey GlobalKey<NavigatorState>();// App 根节点Contextstatic BuildContext get context > navigatorKey.currentContext!;static NavigatorState get navigato…...