zoukankan      html  css  js  c++  java
  • 游戏AI(三)—行为树优化之基于事件的行为树

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

    问题

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

    解决办法

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

    监察函数

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

    行为调度器

    调度器负责管理基于事件的行为树的核心代码,负责对所有需要更新的行为进行集中式管理,不允许复合行为自主管理和运行自己的子节点。。。这里我们将调度器整合进了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连接

  • 相关阅读:
    JS经典面试题
    javascript数组(1) ——sort的工作原理及其他数组排序方法
    怎么去掉javascript 的Array的重复项
    Intellij IDEA运行Error ——Command line is too long
    angular-waring:global Angular与local Angular版本不一致问题
    idea(集成python)下载python插件失败
    PLSQL登录oracle显示无监听或协议适配器错误
    maven不能加载ojdbc6.jar的解决方法
    eclipse安装maven插件
    windows下gitbash安装教程
  • 原文地址:https://www.cnblogs.com/moonmagician/p/8099548.html
Copyright © 2011-2022 走看看