当前位置: 首页 > article >正文

【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 使用场景
  1. 存储普通函数
#include <iostream>
#include <functional>

int add() {
    return 10 + 20;
}

int main() {
    std::function<int()> f = add;  // 存储普通函数
    std::cout << f() << std::endl;  // 输出 30
    return 0;
}

  1. 存储 Lambda 表达式
#include <iostream>
#include <functional>

int main() {
    std::function<int()> f = []() { return 10 + 20; };  // 存储 lambda 表达式
    std::cout << f() << std::endl;  // 输出 30
    return 0;
}

  1. 存储函数指针
#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;  // 输出 20
    return 0;
}

  1. 存储函数对象(仿函数)
#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;  // 输出 30
    return 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的决策过程分解成一系列的节点,每个节点代表一个具体的行为或决策过程。这些节点通过不同的控制结构(如顺序执行、选择等)组织在一起,形成树状结构。行为树通常由以下几种类型的节点组成:
  1. 动作节点(Action Node)
    这是树中的基本节点,代表AI执行的具体操作。例如,玩家移动、攻击敌人等。动作节点通常会返回“成功”“失败”来表示操作的结果。
  2. 条件节点(Condition Node)
    条件节点用于检查某些条件是否满足,例如玩家是否在视野范围内、AI的生命值是否低于某个阈值等。条件节点的作用是帮助决策是否执行某个动作
  3. 复合节点(Composite Node)
    复合节点通过控制其子节点的执行顺序来管理AI行为的流程。复合节点有两种常见类型:
    • 顺序节点(Sequence Node):按顺序执行其子节点,直到其中某个子节点失败为止。如果所有子节点都成功执行,则顺序节点本身返回成功。
    • 选择节点(Selector Node):按顺序执行其子节点,直到某个子节点成功为止。如果所有子节点都失败,则选择节点本身返回失败。
  4. 装饰节点(Decorator Node)
    装饰节点通常用来修改或影响子节点的执行。例如,可以用装饰节点来设置执行条件、重复执行某个动作、限制执行次数等。装饰节点不会直接影响行为树的控制流程,但可以改变子节点的行为或执行方式。

1-2 行为树的结构
  • 行为树的树状结构通常从一个根节点开始,根节点下连接多个子节点。常见的节点类型组合如下:
    • 根节点(Root Node):根节点是行为树的起点,通常只有一个。
    • 顺序节点(Sequence):顺序节点按顺序依次执行其子节点,直到一个节点失败或者所有子节点都成功执行完毕。
    • 选择节点(Selector):选择节点逐一执行其子节点,直到找到一个成功的节点为止。如果没有子节点成功,则选择节点返回失败。

1-3 行为树的执行
  • 行为树的执行通常是从根节点开始的,并且执行流程会根据控制结构(如顺序、选择等)依次向下。每个节点的执行会返回一个状态,常见的状态有:
    • 成功(Success):节点执行完成并且达成目标。
    • 失败(Failure):节点执行未能达成目标。
    • 运行中(Running):节点正在执行,但尚未完成。
  • 执行流程中,如果某个节点返回“失败”,则会根据树的结构影响父节点的执行。如果某个节点返回“运行中”,则父节点会继续等待该节点的结果,直到它完成执行。

1-4 行为树的优点
  1. 可扩展性和模块化
    行为树通过分解复杂的决策过程为简单的行为模块,易于扩展和维护。每个节点代表一个独立的行为,开发者可以轻松地添加或修改节点来调整AI行为。
  2. 易于理解和调试
    行为树的层次结构和执行流程清晰明了,使得AI行为更加直观易懂。相对于复杂的状态机或规则系统,行为树在调试和优化时更具优势。
  3. 灵活性
    行为树能够灵活地组合不同类型的节点,形成复杂的行为模式。通过复合节点和装饰节点的组合,开发者可以实现非常复杂的行为逻辑。
  4. 避免状态爆炸问题
    传统的状态机可能会因状态数量的增加而导致状态爆炸(状态之间的转移关系过于复杂),而行为树通过树状结构避免了这一问题,简化了状态之间的关系。

1-5 行为树的应用
  • 游戏AI
    行为树在游戏开发中有着广泛的应用,尤其是在控制NPC(非玩家角色)的行为时。通过行为树,可以让NPC在复杂的游戏环境中做出灵活且多样的反应。例如,敌人AI可以根据不同的游戏情境(如玩家的距离、视野等)选择攻击、追击或逃跑等行为。

  • 机器人控制
    在机器人技术中,行为树被用来描述机器人执行任务的决策过程。例如,机器人在执行复杂任务(如清扫、抓取物体、避障等)时,行为树能够帮助机器人做出合理的决策。

    • 回顾ROS2nav2框架,就是采用可如下行为树框架。请添加图片描述
  • 自动化系统
    行为树也可以应用于自动化系统中,用来管理和优化工作流程或任务的执行。例如,自动化测试工具、智能家居控制等领域也能通过行为树来管理任务的执行和调度。


2 节点类型编程实现

2-1 思路分析
  • 我们在 C++ 中实现行为树(Behavior Tree)通常包括以下几个关键步骤:
  1. 定义节点类型(Action, Condition, Composite, Decorator)
    每个节点都有不同的功能,通常是基于类的结构设计。
  2. 实现节点的执行逻辑
    每个节点通常会有一个 Tick() 方法,用来执行节点的操作,并根据执行结果返回 SuccessFailureRunning 等状态。
  3. 组合节点
    行为树的复合节点(如 SequenceSelector)用于控制节点的执行顺序和条件。
  4. 树的根节点
    行为树的根节点是行为树的起始点,通常会有一个 Run() 方法来启动树的执行。

2-2 定义节点状态类型
  • 根据上面我们学习的,每个节点的执行会返回一个状态,常见的状态有:
    • 成功(Success):节点执行完成并且达成目标。
    • 失败(Failure):节点执行未能达成目标。
    • 运行中(Running):节点正在执行,但尚未完成。
// 定义节点状态类型
enum class NodeStatus {
    Success,
    Failure,
    Running
};

2-3 抽象的节点类
  • 正如上面所说的,每个节点通常会有一个 Tick() 方法,用来执行节点的操作,并根据执行结果返回 SuccessFailureRunning 等状态。
  • 因此我们为了利于编成和扩展,我们定义一个抽象接口
// 抽象的节点类
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):按顺序执行其子节点,直到某个子节点成功为止。如果所有子节点都失败,则选择节点本身返回失败。
成功
成功
失败
成功
成功
失败
选择节点
条件节点: 玩家在视野内
动作节点: 攻击
成功
条件节点: AI生命值低
动作节点: 逃跑
动作节点: 巡逻
// -------------------- 选择节点(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 行为的决策过程。
条件成立
条件不成立
执行成功
执行失败
根节点: SequenceNode
条件节点: 检查玩家是否在视野内
动作节点: 移动
成功
失败
#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 问题分析
  • 大黄蜂的战斗模式通常包含多个阶段和攻击方式,例如:
  1. 快速移动:大黄蜂会迅速在场景中移动。
  2. 刺客突刺:大黄蜂会向玩家冲刺并进行攻击。
  3. 远程攻击:大黄蜂可能使用一些远程攻击。
  4. 战斗模式切换:在不同的生命值阶段,大黄蜂的攻击模式可能会变化。
  5. 逃避行为:当大黄蜂受到威胁时,它可能会通过跳跃或飞行等行为来进行规避。
  • 我们可以通过下属节点图来大致概括
Boss Health < 50
Player Distance <= 3
Player Distance <= 5
Player Distance > 5
Aggressive Attack
SelectorNode
EvadeDecorator
AssassinDashNode
QuickMoveNode
RangedAttackNode
AggressiveAttackNode
Evade Action
Assassin Dash Action
Quick Move Action
Ranged Attack Action
Aggressive Attack Action

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 具体节点书写
  • 我们需要分别完成
    1. 快速移动:大黄蜂会迅速在场景中移动。
    2. 刺客突刺:大黄蜂会向玩家冲刺并进行攻击。
    3. 远程攻击:大黄蜂可能使用一些远程攻击。
    4. 战斗模式切换:在不同的生命值阶段,大黄蜂的攻击模式可能会变化。
    5. 逃避行为:当大黄蜂受到威胁时,它可能会通过跳跃或飞行等行为来进行规避。
  1. 快速移动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;
    }
};
  1. 刺客突刺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;
    }
};
  1. 远程攻击节点 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;
    }
};
  1. 攻击模式的变化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;
    }
};


  1. 回避行为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 行为树构建
Boss Health < 50
Player Distance <= 3
Player Distance <= 5
Player Distance > 5
Aggressive Attack
SelectorNode
EvadeDecorator
AssassinDashNode
QuickMoveNode
RangedAttackNode
AggressiveAttackNode
Evade Action
Assassin Dash Action
Quick Move Action
Ranged Attack Action
Aggressive Attack Action
  • 我们
#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逻辑。
  • 如有错误,欢迎指出!!!!
  • 感谢大家的支持!!!!祝大家圣诞节快乐!!请添加图片描述

http://www.kler.cn/a/452019.html

相关文章:

  • 模型的量化(Quantization)
  • iClient3D for Cesium 加载shp数据并拉伸为白模
  • TGRS | 可变形傅里叶卷积用于遥感道路分割
  • Linux服务器端自动挂载存储设备(U盘、移动硬盘)
  • kkfileview代理配置,Vue对接kkfileview实现图片word、excel、pdf预览
  • 畅捷通T+13管理员密码任意重置漏洞
  • MVC 参考手册
  • Flink中并行度和slot的关系——任务和任务槽
  • VUE前端实现防抖节流 Lodash
  • TCN-Transformer+LSTM多变量回归预测(Matlab)添加气泡图、散点密度图
  • “自动驾驶第一股” 图森未来退市转型:改名 CreateAI、发布图生视频大模型 “Ruyi”
  • 大模型-Dify使用笔记
  • QT安装5.15之后的版本和安装后添加其他漏装模块
  • mac中idea中英文版本切换
  • 金融数据可视化实现
  • mac启ssh服务用于快速文件传输
  • [创业之路-204]:《华为战略管理法-DSTE实战体系》- 5-平衡记分卡绩效管理
  • M系列芯片切换镜像源并安装 openJDK17
  • 【Mac】终端改色-让用户名和主机名有颜色
  • 一个C#开发的APP
  • MySQL最左匹配原则是什么
  • 【开发问题记录】eslint9 中 eslint 和 prettier冲突
  • 《Cocos Creator游戏实战》非固定摇杆实现原理
  • C#Directory类文件夹基本操作大全
  • 微信小程序的轮播图学习报告
  • ChatGPT之父:奥尔特曼