游戏AI—行为树优化之基于事件的行为树

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏AI—行为树优化之基于事件的行为树相关的知识,希望对你有一定的参考价值。

上一篇我们讲到了关于行为树的内存优化,这一篇我们将讲述行为树的另一种优化方法——基于事件的行为树。

问题

在之前的行为树中,我们每帧都要从根节点开始遍历行为树,而目的仅仅是为了得到最近激活的节点,既然如此,为什么我们不单独维护一个保存这些行为的列表,以方便快速访问呢。我们可以把这个列表叫做调度器,用来保存已经激活的行为,并在必要时更新他们。

解决办法

我们不再每帧都从根节点去遍历行为树,而是维护一个调度器负责保存已激活的节点,当正在执行的行为终止时,由其父节点决定接下来的行为。

监察函数

为了实现基于事件的驱动,我们必须要有一个监察函数,当行为终止时,我们通过执行监察函数通知父节点并让父节点做出相应处理,这里我们通过C++标准库中的std::funcion实现监察函数
using BehaviorObserver = std::function

行为调度器

调度器负责管理基于事件的行为树的核心代码,负责对所有需要更新的行为进行集中式管理,不允许复合行为自主管理和运行自己的子节点。。。这里我们将调度器整合进了BehvaiorTree类。当然也可以弄个单独的类进行管理。

class BehaviorTree
{
public:
        BehaviorTree(Behavior* InRoot) :Root(InRoot) {}
        void Tick();
        bool Step();
        void Start(Behavior* Bh,BehaviorObserver* Observe);
        void Stop(Behavior* Bh,EStatus Result);
private:
        //已激活行为列表
        std::deque<Behavior*> Behaviors;
        Behavior* Root;
};

void BehaviorTree::Tick()
{
    //将更新结束标记插入任务列表
    Behaviors.push_back(nullptr);
    while (Step())
    {
    }
}

bool BehaviorTree :: Step()
{
    Behavior* Current = Behaviors.front();
    Behaviors.pop_front();
    //如果遇到更新结束标记则停止
    if (Current == nullptr)
        return false;
    //执行行为更新
    Current->Tick();
    //如果该任务被终止则执行监察函数
    if (Current->IsTerminate() && Current->Observer)
    {
        Current->Observer(Current->GetStatus());
    }
    //否则将其插入队列等待下次tick处理
    else
    {
        Behaviors.push_back(Current);
    }
}

void BehaviorTree::Start(Behavior* Bh, BehaviorObserver* Observe)
{
    if (Observe)
    {
        Bh->Observer = *Observe;
    }
    Behaviors.push_front(Bh);
}
void BehaviorTree::Stop(Behavior* Bh, EStatus Result)
{
    assert(Result != EStatus::Running);
    Bh->SetStatus(Result);
    if (Bh->Observer)
    {
        Bh->Observer(Result);
    }
}

我们通过一个双端队列保存已激活行为,在更新时从首端去走哦偶行为,再将需要更新的行为压入队列尾端。当发现任务终止时,执行其监察函数。
而Start()函数负责将行为压入队列首端,Stop()节点则负责设置行为执行状态并显示调用监察函数。

事件驱动的复合节点

大部分动作和条件代码并不受事件驱动方式的影响。而复合节点则是受事件驱动影响最明显的节点。复合节点不再自己更新和管理子节点,而是通过向调度器提出请求以更新子节点。这里我们以Sequence节点为例。
/顺序器:依次执行所有节点直到其中一个失败或者全部成功位置
class Sequence :public Composite
{
public:
virtual std::string Name() override { return "Sequence"; }
static Behavior* Create() { return new Sequence(); }
void OnChildComplete(EStatus Status);
protected:
virtual void OnInitialize() override;
protected:
Behaviors::iterator CurrChild;
BehaviorTree* m_pBehaviorTree;
};

void Sequence::OnInitialize()
{
    CurrChild = Children.begin();
    BehaviorObserver observer = std::bind(&Sequence::OnChildComplete, this, std::placeholders::_1);
    Tree->Start(*CurrChild, &observer);
}


void Sequence::OnChildComplete(EStatus Status)
{
    Behavior* child = *CurrChild;
    //当当前子节点执行失败时,顺序器失败
    if (child->IsFailuer())
    {
        m_pBehaviorTree->Stop(this, EStatus::Failure);
        return;
    }
    
    assert(child->GetStatus() == EStatus::Success);
    //当前子节点执行成功时,判断是否执行到数组尾部
    if (++CurrChild == Children.end())
    {
        Tree->Stop(this, EStatus::Success);
    }
    //调度下一个子节点
    else
    {
        BehaviorObserver observer = std::bind(&Sequence::OnChildComplete, this, std::placeholders::_1);
        Tree->Start(*CurrChild, &observer);
    }
}

因为现在各节点由调度器统一管理,所以Update函数不再需要。我们在OnIntialize()函数中设置需要更新的首个节点,并将OnChildComplete作为其监察函数。在OnchildComplete函数中实现后续子节点的更新。

总结

通过基于事件的方式,我们可以在行为树执行时节省大量的函数调用,对其性能无疑是一次巨大的提升。
github连接

以上是关于游戏AI—行为树优化之基于事件的行为树的主要内容,如果未能解决你的问题,请参考以下文章

UE4蓝图AI角色制作之测试行为树EQS查询饥饿策略

游戏设计模式 之 AI_行为树

游戏AI-行为树

FSM(状态机)HFSM(分层状态机)BT(行为树)的区别

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

游戏AI自定义--行为树+xlua+json