摘要:上一篇我们讲到了关于行为树的内存优化,这一篇我们将讲述行为树的另一种优化方法基于事件的行为树。而函数负责将行为压入队列首端,节点则负责设置行为执行状态并显示调用监察函数。
上一篇我们讲到了关于行为树的内存优化,这一篇我们将讲述行为树的另一种优化方法——基于事件的行为树。
问题在之前的行为树中,我们每帧都要从根节点开始遍历行为树,而目的仅仅是为了得到最近激活的节点,既然如此,为什么我们不多带带维护一个保存这些行为的列表,以方便快速访问呢。我们可以把这个列表叫做调度器,用来保存已经激活的行为,并在必要时更新他们。
解决办法我们不再每帧都从根节点去遍历行为树,而是维护一个调度器负责保存已激活的节点,当正在执行的行为终止时,由其父节点决定接下来的行为。
监察函数为了实现基于事件的驱动,我们必须要有一个监察函数,当行为终止时,我们通过执行监察函数通知父节点并让父节点做出相应处理,这里我们通过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::dequeBehaviors; 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连接
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/19684.html
摘要:原文链接本文内容包含以下章节本书英文版这个章节主要讨论了在游戏中经常用到的一些基础的人工智能算法。行为树是把的图转变成为一颗树结构。根据当前游戏的环境状态得到某一个行为的效用值。 作者:苏博览商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。原文链接:https://wetest.qq.com/lab/view/427.html 本文内容包含以下章节: Chapter 2 ...
摘要:另外,当并行器满足条件提前退出时,所有正在执行的子行为也应该立即被终止,我们在函数中调用每个子节点的终止方法监视器监视器是并行器的应用之一,通过在行为运行过程中不断检查是否满足某条件,如果不满足则立刻退出。将条件放在并行器的尾部即可。 从上古卷轴中形形色色的人物,到NBA2K中挥洒汗水的球员,从使命召唤中诡计多端的敌人,到刺客信条中栩栩如生的人群。游戏AI几乎存在于游戏中的每个角落,默...
摘要:从游戏界的角度来说人工智能技术的发展可以为游戏带来什么改变和收益。使用人工智能技术可以给游戏带来更多更好的内容,也可以减轻游戏开发的成本。 作者:苏博览,腾讯互动娱乐高级研究员商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。原文链接:https://wetest.qq.com/lab/view/412.html 本文内容包含以下章节: Chapter 1.3 Why Ga...
阅读 892·2023-04-25 18:51
阅读 1842·2021-09-09 11:39
阅读 3259·2019-08-30 15:53
阅读 2073·2019-08-30 13:03
阅读 1279·2019-08-29 16:17
阅读 545·2019-08-29 11:33
阅读 1837·2019-08-26 14:00
阅读 2096·2019-08-26 13:41