资讯专栏INFORMATION COLUMN

游戏AI—行为树研究及实现

Harriet666 / 3270人阅读

摘要:另外,当并行器满足条件提前退出时,所有正在执行的子行为也应该立即被终止,我们在函数中调用每个子节点的终止方法监视器监视器是并行器的应用之一,通过在行为运行过程中不断检查是否满足某条件,如果不满足则立刻退出。将条件放在并行器的尾部即可。

从上古卷轴中形形色色的人物,到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;
    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 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地址

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/19686.html

相关文章

  • 游戏人工智能 读书笔记 (四) AI算法简介——Ad-Hoc 行为编程

    摘要:原文链接本文内容包含以下章节本书英文版这个章节主要讨论了在游戏中经常用到的一些基础的人工智能算法。行为树是把的图转变成为一颗树结构。根据当前游戏的环境状态得到某一个行为的效用值。 作者:苏博览商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。原文链接:https://wetest.qq.com/lab/view/427.html 本文内容包含以下章节: Chapter 2 ...

    xinhaip 评论0 收藏0
  • 游戏人工智能 读书笔记 (三) 游戏和人工智能的相互影响

    摘要:从游戏界的角度来说人工智能技术的发展可以为游戏带来什么改变和收益。使用人工智能技术可以给游戏带来更多更好的内容,也可以减轻游戏开发的成本。 作者:苏博览,腾讯互动娱乐高级研究员商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。原文链接:https://wetest.qq.com/lab/view/412.html 本文内容包含以下章节: Chapter 1.3 Why Ga...

    CrazyCodes 评论0 收藏0
  • 游戏AI(三)—行为优化之基于事件的行为

    摘要:上一篇我们讲到了关于行为树的内存优化,这一篇我们将讲述行为树的另一种优化方法基于事件的行为树。而函数负责将行为压入队列首端,节点则负责设置行为执行状态并显示调用监察函数。 上一篇我们讲到了关于行为树的内存优化,这一篇我们将讲述行为树的另一种优化方法——基于事件的行为树。 问题 在之前的行为树中,我们每帧都要从根节点开始遍历行为树,而目的仅仅是为了得到最近激活的节点,既然如此,为什么我们...

    vvpvvp 评论0 收藏0
  • 我是如何学习游戏引擎的?

    摘要:下面列举了游戏开发中常见的岗位以及两条常见的协作开发的流水线其实学习游戏引擎,前期对于任何岗位来说路线都是相似的,基本上就是一个熟悉基本操作理解基本概念拓展专业知识的过程。当然这不是绝对的,任何引擎的开始阶段和大成阶段都是相似的。 这是【游戏开发那些事】第51篇原创 前言:游戏引擎,表面...

    未东兴 评论0 收藏0

发表评论

0条评论

Harriet666

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<