从上古卷轴中形形色色的人物,到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. Asp.Net Core API网关Ocelot

    首先,让我们简单了解下什么是API网关? API网关是一个服务器,是系统的唯一入口.从面向对象设计的角度看,它与外观模式类似.API网关封装了系统内部架构,为每个客户端提供一个定制的API.它可能还具 ...

  2. EF错误

    The model backing the 'XXXXDBContext' context has changed since the database was created. Either man ...

  3. Failed to call ScheduleShutdown in logind, proceeding with immediate shutdown: Access denied

    装了fedora23后定时关机:sudo shutdown -h +60 弹出如下信息后立刻就关机了: Failed to call ScheduleShutdown in logind, proce ...

  4. 老男孩Python全栈开发(92天全)视频教程 自学笔记04

    day4课程目录: 逻辑运算符 while循环 day4课程内容梳理: 逻辑运算符 算数运算符:+,-,*,/,%,** 比较运算符:< ,>, ==,<=,>=,!=, 逻辑 ...

  5. 负载均衡手段之DNS轮询

    大多数域名注册商都支持对统一主机添加多条A记录,这就是DNS轮询,DNS服务器将解析请求按照A记录的顺序,随机分配到不同的IP上,这样就完成了简单的负载均衡.下图的例子是:有3台联通服务器.3台电信服 ...

  6. web-php绕过

    0x01.web-PHP的悖论1 题目: 链接:http://game.sycsec.com:2009/10111.php 解题思路: 1.首先,web对于选择二进制方向的我这个菜鸡绝对是十分懵逼的, ...

  7. _2Python_注释命名等基本操作

    注释:注解,解释 主要用于在代码中给代码标识出相关的文字提示(提高代码的可读性)或调试程序,方便别人阅读和自己日后修改. Python中注释分为两类.单行注释:单行注释以#号开头,在一行内,#号后面的 ...

  8. [读书笔记]javascript语言精粹'

    人比较笨,以前只做项目,案例,然而一些javascript的很多理论不知道该怎么描述,所以最近开启一波读书之旅: 标识符 1.定义 标识符以字母开头,可能后面跟上一个或多个字母.数字或者下划线. 2. ...

  9. 开源API测试工具 Hitchhiker v0.5更新 - 完善细节

    Hitchhiker 是一款开源的支持多人协作的 Restful Api 测试工具,支持Schedule, 数据对比,压力测试,支持上传脚本定制请求,可以轻松部署到本地,和你的team成员一起管理Ap ...

  10. MySQL错误:2003-Can't connect to MySQL server on 'localhost'(10061 "unknown error")

    今天数据库出了一点错误之后决定重装一下,结果卡在了一个问题上,连装了5遍,加上网上各种配置教程都没能结局,错误如下图所示: 最后忽然想到会不会是因为每一次卸载的时候没有彻底卸载干净,然后就彻彻底底卸载 ...