行为树(Behavior Tree,BT)是一种控制结构,通过组合不同的行为节点,能够定义复杂的行为逻辑,使自主体能够根据当前的环境状态和目标做出决策并执行相应的动作。常用于游戏中AI逻辑的实现。
一.基本概念
1.节点: 行为树由多个节点构成每个节点代表一个行为或决策步骤。节点根据其在树中的位置和功能可以分为不同类型的节点,如根节点、内部节点(控制流节点)、叶节点(执行节点)等。
2. 状态: 每个节点在执行时都会返回三种状态之一,即成功(Success)、失败(Failure)和运行(Running)。成功和失败状态会通知其父节点其操作结果,而运行状态表示节点仍在执行中。
3. 根节点: 作为行为树的入口,根节点是AI的起点。每个行为树只有一个根节点,且根节点通常只有一个子节点。
二.节点类型
1.任务节点(TaskNode): 任务节点也叫执行节点(ActionNode) 它是叶节点,是行为树中最底层的节点,负责执行具体的行为或任务。它们不会拥有子节点,而是直接返回成功、失败或运行状态。。
2. 复合节点(CompositeNode): 内部节点,也称为组合节点。它们可以有一个或多个子节点,并控制子节点的执行顺序和方式。常见的控制流节点包括选择器(Selector)、序列器(Sequence)等。
3. 装饰节点(DecoratorNode): 一种特殊的控制流节点,它们通常只有一个子节点,并对子节点的执行结果进行修饰或改变。例如,反转装饰器可以反转子节点的成功或失败状态。
三.工作原理
行为树通过从上至下、从左到右的顺序遍历节点来执行AI的行为。当行为树运行到终结状态时,会回到根节点重新开始运行。在遍历过程中,每个节点都会根据其类型和功能执行相应的行为或决策,并返回状态给其父节点。父节点根据子节点的返回状态来决定下一步的执行计划。
四.黑板
黑板是行为树架构中的一个共享数据区域,它充当了不同节点之间通信和数据交换的桥梁。黑板上的数据可以包括目标位置、敌人信息、玩家状态等,这些数据对于行为树做出决策至关重要。
#pragma once
#include<string>
#include<unordered_map>
#include<iostream>
#include<any>
class Blackboard
{
private:
std::unordered_map<std::string, std::any> data;
public:
template<typename T>
void setValue(const std::string& key,const T&value)
{
data[key] = value;
}
template<typename T>
T getValue(const std::string& key) const
{
return std::any_cast<T>(data.at(key));
}
bool hasValue(const std::string& key)
{
return data.find(key) != data.end();
}
};
五.节点示例
- 节点基类
#pragma once
#include<vector>
#include<memory>
#include<iostream>
#include"Blackboard.h"
//节点状态
enum class NodeStatus
{
Success,
Failure,
Running
};
//行为树节点基类
class BehaviorNode
{
public:
virtual NodeStatus update(Blackboard& board) = 0;
virtual ~BehaviorNode() = default;
virtual void abort(){}//中断执行
};
- 复合节点
#pragma once
#include "BehaviorNode.h"
class CompositeNode :public BehaviorNode
{
protected:
std::vector<std::shared_ptr<BehaviorNode>> children;
size_t currentIndex = 0;
public:
void addNode(std::shared_ptr<BehaviorNode> nodePtr)
{
children.push_back(nodePtr);
}
};
- 选择节点
按顺序执行子节点,直到找到第一个成功的子节点,否则返回失败
#pragma once
#include"CompositeNode.h"
class SelectorNode :public CompositeNode
{
public:
NodeStatus update(Blackboard& board) override
{
for (const auto& child : children)
{
auto childStatus = child->update(board);
if (childStatus != NodeStatus::Failure) return childStatus;
}
return NodeStatus::Failure;
}
};
- 顺序节点
从左到右依次执行子节点,全部成功则返回成功,任意子节点失败则立即终止
#pragma once
#include "CompositeNode.h"
//顺序节点
class SequenceNode : public CompositeNode
{
private:
size_t currentIndex = 0;
protected:
NodeStatus update(Blackboard& board) override
{
while (currentIndex < children.size())//顺序遍历子节点
{
auto childStatus = children[currentIndex]->update(board);//调用子节点的tick函数
if (childStatus == NodeStatus::Running) return NodeStatus::Running;//正在执行返回执行状态
if (childStatus == NodeStatus::Failure)//执行失败返回失败状态并重置索引
{
currentIndex = 0;
return NodeStatus::Failure;
}
++currentIndex;
}
//所有子节点执行完成 重置索引并返回成功状态
currentIndex = 0;
return NodeStatus::Success;
}
};
- 并行节点
并行节点允许其下的所有子节点一起执行,并根据设定的成功标准(如一个子节点成功则成功,或所有子节点成功则成功)来判断整个并行节点的执行结果。这种并行执行的方式使得行为树能够同时处理多个任务或行为,提高了系统的并发性和响应速度。
#pragma once
#include"CompositeNode.h"
//并行节点
class ParallelNode :public CompositeNode
{
public:
//并行策略枚举
enum class ParallelPolicy
{
All,
ONE,
};
ParallelNode(ParallelPolicy _successPolicy, ParallelPolicy _failurePolicy)
:successPolicy(_successPolicy)
,failurePolicy(_failurePolicy){};
NodeStatus update(Blackboard& board) override
{
int successCount = 0;//成功数量
int failureCount = 0;//失败数量
for (auto& child : children)
{
NodeStatus childStatus = child->update(board);
if (childStatus == NodeStatus::Success) successCount++;
if (childStatus == NodeStatus::Failure) failureCount++;
}
//成功执行策略
if (successPolicy == ParallelPolicy::All && successCount == children.size() ||
successPolicy == ParallelPolicy::ONE && successCount > 0)
{
return NodeStatus::Success;
}
//失败执行策略
if (failurePolicy == ParallelPolicy::All && failureCount == children.size() ||
failurePolicy == ParallelPolicy::ONE && failureCount > 0)
{
return NodeStatus::Failure;
}
return NodeStatus::Running;
}
private:
ParallelPolicy successPolicy,failurePolicy;
};
- 条件节点
封装逻辑判断函数,根据执行结果返回成功或失败
#pragma once
#include<functional>
#include"BehaviorNode.h"
//条件节点
class ConditionNode :public BehaviorNode
{
public:
//定义条件函数别名
using CheckFunc = std::function<bool(Blackboard&)>;
;//构造函数关闭隐式转换
explicit ConditionNode(CheckFunc _func) :func(_func) {};
NodeStatus update(Blackboard& board)override
{
return func(board) ? NodeStatus::Success : NodeStatus::Failure;
}
private:
CheckFunc func;
};
- 装饰节点
#pragma once
#include "BehaviorNode.h"
//装饰节点
class DecoratorNode :public BehaviorNode
{
protected:
std::shared_ptr<BehaviorNode> child;
public:
DecoratorNode(std::shared_ptr<BehaviorNode> node) :child(node) {};
};
- 取反器
将执行结果取反
#pragma once
#include "DecoratorNode.h"
//取反器
class InverterNode :public DecoratorNode
{
public:
NodeStatus update(Blackboard& board) override
{
auto childStatus = child->update(board);
if (childStatus == NodeStatus::Success) return NodeStatus::Failure;
if (childStatus == NodeStatus::Failure)return NodeStatus::Success;
return childStatus;
}
};
- 重复器
重复执行节点n次
#pragma once
#include"DecoratorNode.h"
#include<iostream>
//重复执行节点
class RepeatNode :public DecoratorNode
{
private:
size_t repeatCount;//0代表无限循环
public:
RepeatNode(std::shared_ptr<BehaviorNode> node,size_t times = 0)
:DecoratorNode(node),repeatCount(times){}
NodeStatus update(Blackboard& board) override
{
for (int i = 0; !repeatCount || i < repeatCount; ++i)
{
std::cout << "Excute times:" << i + 1 << std::endl;
NodeStatus childStatus = child->update(board);
if (childStatus == NodeStatus::Failure) return NodeStatus::Failure;
}
return NodeStatus::Success;
}
};
- 任务节点
封装AI具体的执行逻辑
#pragma once
#include<functional>
#include"BehaviorNode.h"
//任务节点
class TaskNode :public BehaviorNode
{
public:
using TaskFunc = std::function<NodeStatus(Blackboard&)>;
TaskNode(TaskFunc _task) :task(_task) {};
NodeStatus update(Blackboard& board) override
{
return task(board);
}
private:
TaskFunc task;
};
以上就是行为树中一些基本节点,现在我们用以上部分节点构建一棵行为树,来执行一些简单的AI逻辑。
#pragma once
#include<memory>
#include"Blackboard.h"
#include"BehaviorNode.h"
#include"SelectorNode.h"
#include"SequenceNode.h"
#include"ParallelNode.h"
#include"ConditionNode.h"
#include"TaskNode.h"
#include"RepeatNode.h"
class BehaviorTree
{
private:
Blackboard& board;
public:
std::shared_ptr<SelectorNode> root;
BehaviorTree(Blackboard& _board, std::shared_ptr<SelectorNode> _root) :board(_board), root(_root) {};
void initBehavior()
{
//攻击行为
auto attack = std::make_shared<SequenceNode>();
attack->addNode(std::make_shared<ConditionNode>([](Blackboard& board) {
float distance = board.getValue<float>("TargetPosition") - board.getValue<float>("CurrentPosition");
float attackRange = board.getValue<float>("AttackDistance");
std::cout << attackRange << "," << distance << std::endl;
return distance <= attackRange;//检查攻击距离
}));
//补充子弹行为
auto checkBullets = std::make_shared<SelectorNode>();
//检查子弹是否充足
checkBullets->addNode(std::make_shared<ConditionNode>([](Blackboard& board) {
return board.getValue<int>("Bullets") > 0;
}));
//前往补充子弹
checkBullets->addNode(std::make_shared<TaskNode>([](Blackboard& board) {
std::cout << "Go supply bullets.\n";
board.setValue("CurrentPosition", -3.0f);//假设补给点在-10位置 移动到补给点
board.setValue("Bullets", 3);
return NodeStatus::Failure;//返回Failure 重新退到根节点重新执行检测逻辑
}));
attack->addNode(checkBullets);
//检测目标是否存活
attack->addNode(std::make_shared<ConditionNode>([](Blackboard& board) {
bool isAlive = board.getValue<int>("Health") > 0;
if (isAlive)
std::cout << "Target is alive start attack!\n";
else
std::cout << "Target is Dead! continue patrolling.\n";
return isAlive;
}));
//攻击目标
attack->addNode(std::make_shared<TaskNode>([](Blackboard& board) {
int bullets = board.getValue<int>("Bullets");
int hp = board.getValue<int>("Health");
board.setValue("Bullets", bullets - 1);
board.setValue("Health", hp - 1);
std::cout << "Attack Enemy! Current bulltes:"<< bullets-1 <<"Current HP:"<< hp-1 << std::endl;
return NodeStatus::Success;
}));
//创建巡逻分支
auto patrol = std::make_shared<TaskNode>([](Blackboard& board) {
float currentPosition = board.getValue<float>("CurrentPosition");
board.setValue("CurrentPosition", currentPosition + 1.0f);
std::cout << "Patrolling currentPosition:"<< currentPosition <<std::endl;
return NodeStatus::Running;
});
root->addNode(attack);
root->addNode(patrol);
}
void update()
{
root->update(board);
}
};
以上代码按如下逻辑执行:
- 创建选择节点作为根节点
- 创建顺序节点作为攻击行为分支
- 创建条件节点判断攻击距离
- 创建选择节点作为补充创建子弹行为分支
- 创建条件节点判断子弹是否充足
- 创建任务节点前往补充子弹
- 创建条件节点判断目标是否存活
- 创建任务节点攻击目标
- 创建任务节点作为巡逻行为分支(单节点)
- 创建顺序节点作为攻击行为分支
执行流程图如下:
最后在main函数中调用行为树:
#include<thread>
#include<memory>
#include"Blackboard.h"
#include"BehaviorTree.h"
int main()
{
Blackboard board;
board.setValue("TargetPosition", 13.f);
board.setValue("AttackDistance", 10.f);
board.setValue("CurrentPosition", 0.f);
board.setValue("Bullets", 2);
board.setValue("Health", 3);
//构建行为树
BehaviorTree bt(board, std::make_shared<SelectorNode>());
//初始化行为树
bt.initBehavior();
//模拟游戏帧循环逻辑一秒钟调用一次
while (true)
{
bt.update();
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
return 0;
}
执行结果:
先巡逻,发现目标后检查目标存活并发起攻击,减少目标血量,当子弹耗尽则前往补充子弹,再次巡逻检测目标距离,进入攻击范围再次攻击,目标死亡,继续巡逻。