从上古卷轴中形形色色的人物,到NBA2K中挥洒汗水的球员,从使命召唤中诡计多端的敌人,到刺客信条中栩栩如生的人群。游戏AI几乎存在于游戏中的每个角落,默默构建出一个令人神往的庞大游戏世界。

那么这些复杂的AI又是怎么实现的呢?下面就让我们来了解并亲手实现一下游戏AI基础架构之一的行为树。

行为树简介

行为树是一种树状的数据结构,树上的每一个节点都是一个行为。每次调用会从根节点开始遍历,通过检查行为的执行状态来执行不同的节点。他的优点是耦合度低扩展性强,每个行为可以与其他行为完全独立。目前的行为树已经可以将几乎任意架构(如规划器,效用论等)应用于AI之上。

class BehaviorTree
{
public:
BehaviorTree(Behavior* InRoot) { Root = InRoot; }
void Tick()
{
Root->Tick();
}
bool HaveRoot() { return Root?true:false; }
void SetRoot(Behavior* InNode) { Root= InNode; }
void Release() { Root->Release(); }
private:
Behavior* Root;
};

上面提供了行为树的实现,行为树有一个根节点和一个Tick()方法,在游戏过程中每个一段时间会调用依次Tick方法,令行为树从根节点开始执行。

行为(behavior)

行为(behavior)是行为树最基础的概念,是几乎所有行为树节点的基类,是一个抽象接口,而如动作条件等节点则是它的具体实现。

下面是Behavior的实现,省略掉了一些简单的判断状态的方法完整源码可以参照文尾的github链接

class Behavior
{
public:
//释放对象所占资源
virtual void Release() = 0;
//包装函数,防止打破调用契约
EStatus Tick(); EStatus GetStatus() { return Status; }
virtual void AddChild(Behavior* Child){}; protected:
//创建对象请调用Create()释放对象请调用Release()
Behavior():Status(EStatus::Invalid){}
virtual ~Behavior() {}
virtual void OnInitialize() {};
virtual EStatus Update() = 0;
virtual void OnTerminate(EStatus Status) {}; protected:
EStatus Status;
};

Behavior接口是所有行为树节点的核心,且我规定所有节点的构造和析构方法都必须是protected,以防止在栈上创建对象,所有的节点对象通过Create()静态方法在堆上创建,通过Release()方法销毁,由于Behavior是个抽象接口,故没有提供Create()方法,本接口满足如下契约

  • 在Update方法被首次调用前,调用一次OnInitialize函数,负责初始化等操作
  • Update()方法在行为树每次更新时调用且仅调用一次。
  • 当行为不再处于运行状态时,调用一次OnTerminate(),并根据返回状态不同执行不同的逻辑

为了保证契约不被打破,我们将这三个方法包装在Tick()方法里。Tick()的实现如下

//update方法被首次调用前执行OnInitlize方法,每次行为树更新时调用一次update方法
//当刚刚更新的行为不再运行时调用OnTerminate方法
if (Status != EStatus::Running)
{
OnInitialize();
} Status = Update(); if (Status != EStatus::Running)
{
OnTerminate(Status);
} return Status;

其中返回值Estatus是一个枚举值,表示节点运行状态。

enum class EStatus:uint8_t
{
Invalid, //初始状态
Success, //成功
Failure, //失败
Running, //运行
Aborted, //终止
};

动作(Action)

动作是行为树的叶子节点,表示角色做的具体操作(如攻击,上弹,防御等),负责改变游戏世界的状态。动作节点可直接继承自Behavior节点,通过实现不同的Update()方法实现不同的逻辑,在OnInitialize()方法中获取数据和资源,在OnTerminate中释放资源。

//动作基类
class Action :public Behavior
{
public:
virtual void Release() { delete this; } protected:
Action() {}
virtual ~Action() {}
};

在这里我实现了一个动作基类,主要是为了一个公用的Release方法负责释放节点内存空间,所有动作节点均可继承自这个方法

条件

条件同样是行为树的叶子节点,用于查看游戏世界信息(如敌人是否在攻击范围内,周围是否有可攀爬物体等),通过返回状态表示条件的成功。

//条件基类
class Condition :public Behavior
{
public:
virtual void Release() { delete this; } protected:
Condition(bool InIsNegation):IsNegation(InIsNegation) {}
virtual ~Condition() {} protected:
//是否取反
bool IsNegation=false;
};

这里我实现了条件基类,一个IsNegation来标识条件是否取反(比如是否看见敌人可以变为是否没有看见敌人)

装饰器(Decorator)

装饰器(Decorator)是只有一个子节点的行为,顾名思义,装饰即是在子节点的原有逻辑上增添细节(如重复执行子节点,改变子节点返回状态等)

//装饰器
class Decorator :public Behavior
{
public:
virtual void AddChild(Behavior* InChild) { Child=InChild; }
protected:
Decorator() {}
virtual ~Decorator(){}
Behavior* Child;
};

实现了装饰器基类,下面我们来实现下具体的装饰器,也就是上面提到的重复执行多次子节点的装饰器

class Repeat :public Decorator
{
public:
static Behavior* Create(int InLimited) { return new Repeat(InLimited); }
virtual void Release() { Child->Release(); delete this; }
protected:
Repeat(int InLimited) :Limited(InLimited) {}
virtual ~Repeat(){}
virtual void OnInitialize() { Count = 0; }
virtual EStatus Update()override;
virtual Behavior* Create() { return nullptr; }
protected:
int Limited = 3;
int Count = 0;
};

正如上面提到的,Create函数负责创建节点,Release负责释放

其中Update()方法的实现如下

EStatus Repeat::Update()
{
while (true)
{
Child->Tick();
if (Child->IsRunning())return EStatus::Success;
if (Child->IsFailuer())return EStatus::Failure;
if (++Count == Limited)return EStatus::Success;
Child->Reset();
}
return EStatus::Invalid;
}

逻辑很简单,如果执行失败就立即返回,执行中就继续执行,执行成功就把计数器+1重复执行

复合行为

我们将行为树中具有多个子节点的行为称为复合节点,通过复合节点我们可以将简单节点组合为更有趣更复杂的行为逻辑。

下面实现了一个符合节点的基类,将一些公用的方法放在了里面(如添加清除子节点等)

//复合节点基类
class Composite:public Behavior
{
virtual void AddChild(Behavior* InChild) override{Childern.push_back(InChild);}
void RemoveChild(Behavior* InChild);
void ClearChild() { Childern.clear(); }
virtual void Release()
{
for (auto it : Childern)
{
it->Release();
} delete this;
} protected:
Composite() {}
virtual ~Composite() {}
using Behaviors = std::vector<Behavior*>;
Behaviors Childern;
};

顺序器(Sequence)

顺序器(Sequence)是复合节点的一种,它依次执行每个子行为,直到所有子行为执行成功或者有一个失败为止。

//顺序器:依次执行所有节点直到其中一个失败或者全部成功位置
class Sequence :public Composite
{
public:
virtual std::string Name() override { return "Sequence"; }
static Behavior* Create() { return new Sequence(); }
protected:
Sequence() {}
virtual ~Sequence(){}
virtual void OnInitialize() override { CurrChild = Childern.begin();}
virtual EStatus Update() override; protected:
Behaviors::iterator CurrChild;
};

其中Update()方法的实现如下

EStatus Sequence::Update()
{
while (true)
{
EStatus s = (*CurrChild)->Tick();
//如果执行成功了就继续执行,否则返回
if (s != EStatus::Success)
return s;
if (++CurrChild == Childern.end())
return EStatus::Success;
}
return EStatus::Invalid; //循环意外终止
}

选择器(Selector)

选择器(Selector)是另一种常用的复合行为,它会依次执行每个子行为直到其中一个成功执行或者全部失败为止

由于与顺序器仅仅是Update函数不同,下面仅贴出Update方法

EStatus Selector::Update()
{
while (true)
{
EStatus s = (*CurrChild)->Tick();
if (s != EStatus::Failure)
return s;
//如果执行失败了就继续执行,否则返回
if (++CurrChild == Childern.end())
return EStatus::Failure;
}
return EStatus::Invalid; //循环意外终止
}

并行器(Parallel)

顾名思义,并行器(Parallel)是一种让多个行为并行执行的节点。但仔细观察便会发现实际上只是他们的更新函数在同一帧被多次调用而已。

//并行器:多个行为并行执行
class Parallel :public Composite
{
public:
static Behavior* Create(EPolicy InSucess, EPolicy InFailure){return new Parallel(InSucess, InFailure); }
virtual std::string Name() override { return "Parallel"; } protected:
Parallel(EPolicy InSucess, EPolicy InFailure) :SucessPolicy(InSucess), FailurePolicy(InFailure) {}
virtual ~Parallel() {}
virtual EStatus Update() override;
virtual void OnTerminate(EStatus InStatus) override; protected:
EPolicy SucessPolicy;
EPolicy FailurePolicy;
};

这里的Epolicy是一个枚举类型,表示成功和失败的条件(是成功或失败一个还是全部成功或失败)

//Parallel节点成功与失败的要求,是全部成功/失败,还是一个成功/失败
enum class EPolicy :uint8_t
{
RequireOne,
RequireAll,
};

update函数实现如下


EStatus Parallel::Update()
{
int SuccessCount = 0, FailureCount = 0;
int ChildernSize = Childern.size();
for (auto it : Childern)
{
if (!it->IsTerminate())
it->Tick(); if (it->IsSuccess())
{
++SuccessCount;
if (SucessPolicy == EPolicy::RequireOne)
{
it->Reset();
return EStatus::Success;
} } if (it->IsFailuer())
{
++FailureCount;
if (FailurePolicy == EPolicy::RequireOne)
{
it->Reset();
return EStatus::Failure;
}
}
} if (FailurePolicy == EPolicy::RequireAll&&FailureCount == ChildernSize)
{
for (auto it : Childern)
{
it->Reset();
} return EStatus::Failure;
}
if (SucessPolicy == EPolicy::RequireAll&&SuccessCount == ChildernSize)
{
for (auto it : Childern)
{
it->Reset();
}
return EStatus::Success;
} return EStatus::Running;
}

在代码中,并行器每次更新都执行每一个尚未终结的子行为,并检查成功和失败条件,如果满足则立即返回。

另外,当并行器满足条件提前退出时,所有正在执行的子行为也应该立即被终止,我们在OnTerminate()函数中调用每个子节点的终止方法

void Parallel::OnTerminate(EStatus InStatus)
{
for (auto it : Childern)
{
if (it->IsRunning())
it->Abort();
}
}

监视器(Monitor)

监视器是并行器的应用之一,通过在行为运行过程中不断检查是否满足某条件,如果不满足则立刻退出。将条件放在并行器的尾部即可。

主动选择器

主动选择器是选择器的一种,与普通的选择器不同的是,主动选择器会不断的主动检查已经做出的决策,并不断的尝试高优先级行为的可行性,当高优先级行为可行时胡立即打断低优先级行为的执行(如正在巡逻的过程中发现敌人,即时中断巡逻,立即攻击敌人)。

其Update()方法和OnInitialize方法实现如下

//初始化时将CurrChild初始化为子节点的末尾
virtual void OnInitialize() override { CurrChild = Childern.end(); } EStatus ActiveSelector::Update()
{
//每次执行前先保存的当前节点
Behaviors::iterator Previous = CurrChild;
//调用父类OnInlitiallize函数让选择器每次重新选取节点
Selector::OnInitialize();
EStatus result = Selector::Update();
//如果优先级更高的节点成功执行或者原节点执行失败则终止当前节点的执行
if (Previous != Childern.end()&CurrChild != Previous)
{
(*Previous)->Abort();
} return result;
}

示例##

这里我创建了一名角色,该角色一开始处于巡逻状态,一旦发现敌人,先检查自己生命值是否过低,如果是就逃跑,否则就攻击敌人,攻击过程中如果生命值过低也会中断攻击,立即逃跑,如果敌人死亡则立即停止攻击,这里我们使用了构建器来创建了一棵行为树,关于构建器的实现后面会讲到,这里每个函数创建了对应函数名字的节点,

//构建行为树:角色一开始处于巡逻状态,一旦发现敌人,先检查自己生命值是否过低,如果是就逃跑,否则就攻击敌人,攻击过程中如果生命值过低也会中断攻击,立即逃跑,如果敌人死亡则立即停止攻击
BehaviorTreeBuilder* Builder = new BehaviorTreeBuilder();
BehaviorTree* Bt=Builder
->ActiveSelector()
->Sequence()
->Condition(EConditionMode::IsSeeEnemy,false)
->Back()
->ActiveSelector()
-> Sequence()
->Condition(EConditionMode::IsHealthLow,false)
->Back()
->Action(EActionMode::Runaway)
->Back()
->Back()
->Monitor(EPolicy::RequireAll,EPolicy::RequireOne)
->Condition(EConditionMode::IsEnemyDead,true)
->Back()
->Action(EActionMode::Attack)
->Back()
->Back()
->Back()
->Back()
->Action(EActionMode::Patrol)
->End(); delete Builder;

然后我通过一个循环模拟行为树的执行。同时在各条件节点内部通过随机数表示条件是否执行成功(具体见文末github源码)

	//模拟执行行为树
for (int i = 0; i < 10; ++i)
{
Bt->Tick();
std::cout << std::endl;
}

执行结果如下,由于随机数的存在每次执行结果都不一样

构建器的实现

上面创建行为树的时候用到了构建器,下面我就介绍一下自己的构建器实现

//行为树构建器,用来构建一棵行为树,通过前序遍历方式配合Back()和End()方法进行构建
class BehaviorTreeBuilder
{
public:
BehaviorTreeBuilder() { }
~BehaviorTreeBuilder() { }
BehaviorTreeBuilder* Sequence();
BehaviorTreeBuilder* Action(EActionMode ActionModes);
BehaviorTreeBuilder* Condition(EConditionMode ConditionMode,bool IsNegation);
BehaviorTreeBuilder* Selector();
BehaviorTreeBuilder* Repeat(int RepeatNum);
BehaviorTreeBuilder* ActiveSelector();
BehaviorTreeBuilder* Filter();
BehaviorTreeBuilder* Parallel(EPolicy InSucess, EPolicy InFailure);
BehaviorTreeBuilder* Monitor(EPolicy InSucess, EPolicy InFailure);
BehaviorTreeBuilder* Back();
BehaviorTree* End(); private:
void AddBehavior(Behavior* NewBehavior); private:
Behavior* TreeRoot=nullptr;
//用于存储节点的堆栈
std::stack<Behavior*> NodeStack;
};
BehaviorTreeBuilder* BehaviorTreeBuilder::Sequence()
{
Behavior* Sq=Sequence::Create();
AddBehavior(Sq);
return this;
} void BehaviorTreeBuilder::AddBehavior(Behavior* NewBehavior)
{
assert(NewBehavior);
//如果没有根节点设置新节点为根节点
if (!TreeRoot)
{
TreeRoot=NewBehavior;
}
//否则设置新节点为堆栈顶部节点的子节点
else
{
NodeStack.top()->AddChild(NewBehavior);
} //将新节点压入堆栈
NodeStack.push(NewBehavior);
} BehaviorTreeBuilder* BehaviorTreeBuilder::Back()
{
NodeStack.pop();
return this;
} BehaviorTree* BehaviorTreeBuilder::End()
{
while (!NodeStack.empty())
{
NodeStack.pop();
}
BehaviorTree* Tmp= new BehaviorTree(TreeRoot);
TreeRoot = nullptr;
return Tmp;
}

在上面的实现中,我在每个方法里创建对应节点,检测当前是否有根节点,如果没有则将其设为根节点,如果有则将其设为堆栈顶部节点的子节点,随后将其压入堆栈,每次调用back则退栈,每个创建节点的方法都返回this以方便调用下一个方法,最后通过End()表示行为树创建完成并返回构建好的行为树。

那么上面就是行为树的介绍和实现了,下一篇我们将对行为树进行优化,慢慢进入第二代行为树。

[github地址][1]

游戏AI-行为树理论及实现的更多相关文章

  1. 如何建立一个完整的游戏AI

    http://blog.friskit.me/2012/04/how-to-build-a-perfect-game-ai/ 人工智能(Artificial Intelligence)在游戏中使用已经 ...

  2. 游戏AI-行为树

    参考: 游戏AI—行为树研究及实现 GAD腾讯游戏开发者平台:游戏中的人工智能AI 腾讯开源项目behaviac 占坑,待编辑

  3. 使用行为树(Behavior Tree)实现游戏AI

    ——————————————————————— 谈到游戏AI,很明显智能体拥有的知识条目越多,便显得更智能,但维护庞大数量的知识条目是个噩梦:使用有限状态机(FSM),分层有限状态机(HFSM),决策 ...

  4. 游戏AI之决策结构—有限状态机/行为树(2)

    目录 有限状态机 行为树 控制节点 条件节点 行为节点 装饰节点 总结 额外/细节/优化 游戏AI的决策部分是比较重要的部分,游戏程序的老前辈们留下了两种经过考验的用于AI决策的结构: 有限状态机 行 ...

  5. 做游戏长知识------基于行为树与状态机的游戏AI(一)

    孙广东 2014.6.30 AI. 我们的第一印象可能是机器人,如今主要说在游戏中的应用. 现代的计算机游戏中已经大量融入了AI元素,平时我们进行游戏时产生的交互都是由AI来完毕的.比方在RPG游戏中 ...

  6. Unity教程之-基于行为树与状态机的游戏AI

    AI.我们的第一印象可能是机器人,现在主要说在游戏中的应用.关于AI的相关文章我们在前面也提到过,详细请戳这现代的计算机游戏中已经大量融入了AI元素,平时我们进行游戏时产生的交互都是由AI来完成的.比 ...

  7. 游戏AI之初步介绍(0)

    目录 游戏AI是什么? 游戏AI和理论AI 智能的假象 (更新)游戏AI和机器学习 介绍一些游戏AI 4X游戏AI <求生之路>系列 角色扮演/沙盒游戏中的NPC 游戏AI 需要学些什么? ...

  8. 趣说游戏AI开发:对状态机的褒扬和批判

    0x00 前言 因为临近年关工作繁忙,已经有一段时间没有更新博客了.到了元旦终于有时间来写点东西,既是积累也是分享.如题目所示,本文要来聊一聊在游戏开发中经常会涉及到的话题--游戏AI.设计游戏AI的 ...

  9. 对弈类游戏的人工智能(5)--2048游戏AI的解读

    前言: 闲得没事, 网上搜"游戏AI", 看到一篇<<2048游戏的最佳算法是?来看看AI版作者的回答>>的文章. 而这篇文章刚好和之前讲的对弈类游戏AI对 ...

随机推荐

  1. 网页静态化技术Freemarker的详细介绍

    网页静态化技术Freemarker 一.Freemarker的基本介绍 1.1为什么要使用网页静态化技术 网页静态化解决方案在实际开发中运用比较多,例如新闻网站,门户网站中的新闻频道或者是文章类的频道 ...

  2. Vue Elementui 如何让输入框每次自动聚焦

    在项目优化中碰到一个小问题,在每次提示框显示的时候让提示框中的输入框聚焦.如下图.一般情况下提示框是隐藏的.点击了编辑才会弹出. 那么原生属性autofocus 只在模板加载完成时起作用,也就是说只有 ...

  3. 面试题:判断连个字符串是否互为回环变位(Circular Rotaion)

    题干:   如果字符串 s 中的字符循环移动任意位置之后能够得到另一个字符串 t,那么 s 就被称为 t 的回环变位(circular rotation).   例如,ACTGACG 就是 TGACG ...

  4. 【S2-052】漏洞复现(CVE-2017-9805)

    一.漏洞描述 Struts2 的REST插件,如果带有XStream组件,那么在进行反序列化XML请求时,存在未对数据内容进行有效验证的安全隐患,可能发生远程命令执行. 二.受影响版本 Struts2 ...

  5. 【Java框架型项目从入门到装逼】第二节 - Spring框架 AOP的丧心病狂解说,你喜欢露娜的月下无限连吗?

    继续上一节的内容,多几个jar包: aop技术是面向切面编程思想,作为OOP的延续思想添加到企业开发中,用于弥补OOP开发过程中的缺陷而提出的编程思想.AOP底层也是面向对象:只不过面向的不是普通的O ...

  6. java对象拷贝和复制

    参考文章:http://blog.csdn.net/XIAXIA__/article/details/41652057 解决问题:深拷贝.浅拷贝 和普通的对象赋值有什么区别? 对象复制 例如:Pers ...

  7. spring boot 中 Mybatis plus 多数据源的配置方法

    最近在学习spring boot,发现在jar包依赖方面做很少的工作量就可以了,对于数据库操作,我用的比较多的是mybatis plus,在中央仓库已经有mybatis-plus的插件了,对于单数据源 ...

  8. Python 集合整理

  9. Java实现Html转PDF的方法

    写在前面 以下路径问题根据项目结构自己修改,以下是我使用spring boot打成jar包的写法. 一.需求背景 在前端编辑器中输入任意的文本,包括css样式变化,保存为html文本.通过Java后台 ...

  10. 详谈Javascript类与继承

    本文将从以下几方面介绍类与继承 类的声明与实例化 如何实现继承 继承的几种方式 类的声明与实例化 类的声明一般有两种方式 //类的声明 var Animal = function () { this. ...