精华内容
下载资源
问答
  •  基于ECS模式,我们将行为树的层级结构拆分成下图  其中:   (1)NodeComponent只用来用来存储NodeData数据,不包含具体逻辑。  (2)NodeData,即多分支节点数据、单分支节点数据、叶子节点数据三种。多...

    一、设计思路

            基于ECS模式,我们将行为树的层级结构拆分成下图

            其中:       

            (1)NodeComponent只用来用来存储NodeData数据,不包含具体逻辑。

            (2)NodeData,即多分支节点数据、单分支节点数据、叶子节点数据三种。多分支节点数据对应的是组合节点,单分支节点数据对应的是装饰节点,叶子节点数据对应的是行为节点和条件节点。

           

            (3)TaskData,具体执行的任务的数据。可能是根据布尔值,可能是根据条件,可能是根据数量等数据进行具体的执行

           

            (4)BehaviorEntity,主要作为索引

            (5)BehaviorSystem,对应的轮询、节点处理、数据反序列化逻辑都在这里存放

            (6)NodeHandler,具体节点处理的逻辑

            

            (7)NodeDataSerializor,Node数据序列化/反序列化器

            

            (8)TaskDataSerilizor,Task数据序列化/反序列化器

            

     

    二、运行示例

    Git库:https://gitee.com/MK_Aliens/StarLightFramework-ECS.git

    展开全文
  • 游戏AI的实现通常分为两种,有限状态机(FSM)以及行为树,我们这里首先将有限状态机应用到游戏《古月》的怪物AI设计中,在之后的版本中再用行为树替换有限状态机。 2018.1.11更新 目前正在写行为树的实现,预计本周...

    写在前面 - (更新)

    游戏AI的实现通常分为两种,有限状态机(FSM)以及行为树,我们这里首先将有限状态机应用到游戏《古月》的怪物AI设计中,在之后的版本中再用行为树替换有限状态机。
    2018.1.11更新 目前正在写行为树的实现,预计本周内完成行为树的设计,会更新在博客中,博主目前找到了武汉胡莱游戏的实习工作,预计会实习到6月份,希望能够有所提升。

    有限状态机(FSM)

    我们知道,有限状态机维护了一张图,图中的结点代表不同的状态,状态之间通过某种条件触发转换,如果不满足条件则维持原状态。这里以一个简单的例子来说明。
    下图为游戏中使用的怪物AI有限状态机。
    怪物AI状态机

    从图中可以看出,有限状态机表示的AI不可能同时处于图示状态中的多个,而只可能处于其中一种状态,状态的转换取决于转换条件和当前所处状态。

    我们不考虑怪物的出生和死亡状态,只考虑图中列出的这几个状态,可以看出,它们的转换关系是非常显而易见的,控制状态转换的核心条件是怪物与玩家的距离,这个距离驱动了整个状态机的状态切换。

    状态模式与有限状态机

    简单的有限状态机可以用枚举+switch的方式实现,但这样的实现不可避免地会出现很多问题。首先,若状态改变需要额外的参数控制(比如巡逻状态的巡逻方向),这种处理就不得不在怪物类中增加成员,其次,这种做法也不符合面向对象思想。
    状态模式的处理就是把每一种状态都当做对象进行处理,也就是说,我们为每种状态定义一个类,这个类的实例代表了当前目标所处的状态。为了实现这个目标,我们要做下面几步:
    1. 定义一个状态基类(或状态接口)。
    2. 为每种状态定义新类别继承自基类(或实现状态接口)。
    3. 实现新的状态类中的接口。

    我们定义的状态基类包含以下接口:

    class AIState{
    publc:
        virtual void enter();
        virtual AIState* handle();
        virtual void exit();
        virtual ~AIState(){};
    }

    虚函数在这里是必要的,我们后续的状态转换完全依赖了虚函数的动态多态特性。同样的原因,析构函数必须是虚函数,这样才能够保证析构的正确性。

    我们简化问题的处理,只考虑游戏世界只存在单个玩家的情况。驱动状态转换的距离就是玩家与AI的距离。在状态中,处理转换过程的函数目前只需要一个参数,就是AI本身,我们以Enemy代表这个AI,handle函数因此需要改写成:

    virtual AIState* handle(Enemy& enemy);

    由上图的状态转换我们可以写出巡逻状态类声明(cocos2dx):

    class PatrolState :public AIState {
    
    public:
        AIState* handle(Monomer& enemy, Monomer* attacker) override;
        void exit() override;
        void update(float deltaTime) override;
        void patrol();
    };

    其中Monomer是Enemy基类。

    我们主要关注handle函数的实现。

    AIState* PatrolState::handle(Monomer& enemy, Monomer* at) {
    
        auto &en = static_cast<Enemy&> (enemy);
    
        auto player = Player::sharePlayer();
    
        auto point = MapPoint(en.getPosition());
        auto playerPoint = MapPoint(player->getPosition());
        auto distance = point.getDistance(playerPoint);
        if (distance>VISUAL_RANGE||player->getStateType() == FStateDeath)
        {
            en.patrol();
            return nullptr;
        }
        auto attacker = en.getAttackMonomerMajor();
        if (distance <= VISUAL_RANGE)
        {
            if (attacker == nullptr)
            {
                en.setAttackMonomerMajor(player);
                attacker = player;
                player->addAgainstMe(&en, 0);
            }
            auto newState = new FollowTracesState();
            enemy.followTheTracks();
            return newState;
        }
        if (attacker)
        {
            attacker->removeAgainstMe(&en);
            en.removeAttackMonomerMajor(attacker);
        }
        en.patrol();
        return nullptr;
    }

    handle类根据视野范围进行状态切换,如果满足条件就返回新的状态,如果不满足,则返回空指针,表示维持在当前状态。

    其它类的设计与patrol类的设计相似,在各自的状态类中,我们执行该状态对应的函数,或转换到其余状态。

    在怪物类 Enemy中,我们设计一个update函数,这个函数会逐帧运行(时间驱动)。

    void Enemy::update(float delta)
    {
    
        auto newState = _mCurrState->handle(*this, m_attackMonomerMajor);
        if (newState) {
            delete _mCurrState;
            _mCurrState = newState;
        }
    }

    _mCurrState是一个指向当前状态的指针(数据成员),在状态发生改变时,newState不为空,我们这时删除原状态指针,更新为新状态。

    后记

    状态机设计完成之后,下一步就是在当前状态机基础上设计更智能的AI,此外,更进一步的想法是使用行为树来代替有限状态机。

    更新 –> 行为树的实现

    好久没更新了。。事情多算是在为自己找借口吧,行为树版本的AI已经写好有一段时间了,这里做一个简单的总结。

    关于行为树

    本文中使用的行为树来自一份开源代码(行为树的开源实现),在阅读之前最好能够去看一下原作者关于行为树的简单介绍,本文会简单介绍一下使用到的几种节点。

    行为树: 行为树,英文是Behavior Tree,简称BT,是由行为节点组成的树状结构。
    节点:行为树上节点主要包括两种,一种行为节点,一种控制节点。行为节点是行为树的叶节点,非叶节点就是控制节点。行为节点真正执行了AI的行为,而控制节点则控制行为树具体自上而下的分支,行为树每次执行都会有一条自上而下的节点连起来的路径表示AI正处于的状态。

    行为树主要的控制节点:
    带优先级的选择结点:在本次应用中使用最多的控制节点。这种选择节点每次都从第一个节点开始判断前提是否满足,因此带优先级的选择节点要求各节点的前提的约束范围必须从小到大(否则后续的节点永远不可能得到执行)。

    不带优先级的选择结点:
    与带优先级选择节点不同,不带优先级的选择节点首先判断上次执行节点的前提(请仔细阅读上述博客的内容),若上次节点的前提判断为真,则继续运行该节点,否则,则依次判断所有节点的前提,选择新的节点运行。
    因为依次判断的原因,不带优先级的选择节点只要求判断条件互斥。

    顺序控制节点:按序执行的控制节点

    顺序节点是否能够执行,取决于顺序节点本身的前提以及顺序节点中当前节点的前提是否满足(evaluate方法),若是第一次执行,则取决于第一个节点前提是否满足。

    顺序节点的更新操作:顺序节点首先更新当前运行节点的运行状态,若返回完成,则执行下一节点,若为最后一个节点,则返回结束,否则仍返回运行中。

    顺序节点的清理操作:顺序节点会将所有值置为初始状态。

    关于并行节点和循环节点这里不再介绍。

    了解了行为树之后,我们可以总结出一个很重要的点,行为树的最终行为是由当前路径上所有前提条件的相与共同决定,也就是说当前路径上的前提条件必须全部满足(行为树也正是这样进行设计和运行的)。

    行为树节点的基本结构:

    class BevNode {
        BevNode* childNodeList[MAX_CHILD_NODE_CNT];
        unsigned int childNodeCount;
        BevNode* parentNode;
        BevNodePrecondition* nodePrecondition;
    
        virtual bool _DoEvaluate(const BevNodeInputParam& input)
        {
            return true;
        }
    
        virtual void _DoTransition(const BevNodeInputParam& input)
        {
    
        }
    
        virtual BevRunningStatus _DoTick(const BevNodeInputParam& input, BevNodeOutputParam& output)
        {
            return K_BRS_Finish;
        }
    };

    先看数据成员,分别代表节点的子节点、父节点、子节点数量以及前提。
    函数方面,分别代表了三种操作:判断(evaluate),更新/执行(tick),清理(transition).

    在具体控制节点实现中,判断的实现已经在上述几种控制条件中写出,更新操作基本就是自上而下进行,清理操作用于上一状态退出时的原节点的清理处理。

    针对本游戏内的AI设计,我们首先给出设计好的行为树:
    行为树

    此处红色节点代表行为节点,绿色节点代表带优先级的选择节点。我们首先按照优先级(取值范围由小到大)排列好根节点的子节点。可以看到,死亡状态节点是优先级最高的。与状态机类似,我们同样设计了逃跑节点以及跟随节点。按照设计好的行为树,我们每一帧去更新行为树的输入,并确定新的行为树状态。可以看到,与状态机相比,行为树的层次更加清晰,代码耦合度更低,同样也更容易扩展,行为树不需要考虑上次的状态,所有行为都由前提决定(所有前提都满足构成一条路径)。因此是状态机的很好替代。

    展开全文
  • 经历了这么久,终于有人给我这篇文章金木研:ue4文章...本系列主要从AI的基础概念、UE4里的AI示例及群集AI三个方向去介绍UE4内的AI,本文的主题是“行为树、状态机与状态设计模式”。所谓状态设计模式,就是基于状...

    经历了这么久,终于有人给我这篇文章

    金木研:ue4文章意见征集zhuanlan.zhihu.com

    评论了,

    8bb61d412d57c2403438542d3f359089.png

    不过AI其实是一个很庞大的系统,我就试着分析一下吧(希望以后留言的时候可以写的具体一点,因为写一个大方向确实需要太多的篇幅了哈哈哈)。

    本系列主要从AI的基础概念、UE4里的AI示例及群集AI三个方向去介绍UE4内的AI,本文的主题是“行为树、状态机与状态设计模式”。

    所谓状态设计模式,就是基于状态分析问题。比如动作系统中,走、跳、蹲这种不同的状态下,我们希望它可以调用不同的动画,运用状态设计模式去解决它就比较不错。于是很自然地,人们提出了状态机这个概念。

    0d8d03ffe49249095e46bb6b9c1288b3.png
    图来自《游戏AI程序设计实战》-王磊【书的内容比较基础,建议入门者购买】

    也就是运用上图这种方式,将问题划分成多个状态,根据不同状态让主体去执行相应代码。

    随着状态的增多,状态之间的连线会愈加复杂

    be22f194a3fb5ba09e9e75bb4e7689e7.png
    一个典型的动作状态机

    于是针对AI设计,人们提出了分层有限状态机

    1322f5cb7663a7bf55c1d4c97c10815b.png
    图来自《游戏AI程序设计实战》-王磊

    这种方式可以简化多种子状态的关系,降低管理成本。通过对分层有限状态机的整合优化,人们设计出了行为树(有些文章说行为树和分层有限状态机其实是同一种东西,我研究不深就不发表看法了,而且这也不是本文的重点,但它们的确很类似)。

    行为树就是将状态判断条件、行为等拆解,然后通过树的形式组合。让AI设计更加方便阅读和改动。

    在阅读下篇文章之前,希望读者对UE4的行为树有一定了解,比如各个节点的作用与区别以及黑板、行为树组件的基础作用。官方文档、官方视频都是很好的方式,b站也有很多up主做过有关视频,我就不详细介绍了。

    金木研:UE4内的AI(二)基于状态设计模式的AI逻辑架构zhuanlan.zhihu.com
    金木研:UE4内的AI(三)群集AI(战斗AI)zhuanlan.zhihu.com
    展开全文
  • 本篇介绍有限状态机和行为树。有限状态机用于有限的状态下的AI,由于同时只能处于一个状态,多个状态需要多个有限状态机,一般用于简单的AI行为。行为树是基于固定行为,通过遍历树来决定采用哪种行为。行为的设计和...

    本篇介绍有限状态机和行为树。有限状态机用于有限的状态下的AI,由于同时只能处于一个状态,多个状态需要多个有限状态机,一般用于简单的AI行为。行为树是基于固定行为,通过遍历树来决定采用哪种行为。行为的设计和执行采用解释器模式,由策划设计数据,程序解析执行,行为组合的灵活性高,比较适合剧情NPC。但当树比较深、分支比较多时,遍历的效率就需要考虑优化。一般我们认为有限状态机执行的性能优于行为树,但不能胜任复杂、灵活的AI设计。而行为树则比较适合复杂、灵活的AI设计。

    先介绍下有限状态机。考虑在一个类似《刀塔传奇》的横版动作卡牌游戏的战斗里,每个英雄有出场、站立、走位、受击、吟唱、施法等状态。英雄每时每刻只能处在这些状态中其中一个状态,每个状态都有自己的逻辑,状态的改变都由事件驱动。像这样简单的AI,可以使用有限状态机来实现。

    有限状态机包括几个要素:

    1.状态,状态机同时只能处于一个状态,在指定状态下有相应的逻辑,例如行走状态,播放行走动画,每帧修改英雄的x、y值

    2.事件,事件是状态转变的触发器,包括内部事件和外部事件。例如最近的敌人达到攻击距离,触发从行走状态转变为站立状态。CD时间到达,触发从站立状态转变为施法状态

    3.状态转变,状态间可以相互转变,转变过程有对应的逻辑。例如从行走状态转变为站立状态,播放站立动作。

    现在来介绍下行为树。在RPG游戏中,地图上存在一些剧情NPC,不同的剧情下,NPC的行为会不一样。这些NPC的行为可以通过行为树进行管理。行为树是在固有行为集下,进行行为抉择的AI算法。行为树包括数据解析、逻辑控制、行为执行三部分。

    行为树数据由节点组成,每个节点有对应的行为类型、参数、返回值。节点有一个子节点数组,通过这种方式将节点组织成树状。

    export class BehaviorNode {

    private type: number = 0;

    private params: any = null;

    private retVal: any = null;

    private subBehaviors: Array = [];

    }

    逻辑控制节点都有子节点,逻辑控制指的是跟编程类似的if条件判断、while循环、串行执行、并行执行等。if行为如果返回true,执行子节点行为,子行为结束则整体行为结束。while行为如果返回true,执行子节点行为,如果子节点结束,重置子节点重新执行。串行行为,子节点一条一条的依次执行,子节点结束则整体结束。并行行为,子节点同时执行,子节点结束则整体结束。

    88ebd92f2d6bc836755dca4c994a1b1c.png

    行为树的叶节点是实际行为执行的节点,在开发一款RPG游戏时,需要根据剧情需要,提炼出角色的细粒行为,例如行走、对话、播放表情、切换动画、触发战斗等。一般地,RPG都会开发一个对应的剧情编辑器,对地图上的NPC进行行为设定,导出对应行为的参数。游戏加载这些数据,解析生成行为树,NPC每帧执行行为树,叶节点行为有对应的执行方法,方法的参数为行为节点的参数。

    private _parseWalkData(): BehaviorNode {

    // TODO 二进制数据解析为json

    }

    public execBehavior(b: BehaviorNode): void {

    if (!b) {

    return;

    }

    switch(b.type) {

    case BehaviorType.WALK:

    this.execWalk(b);

    break;

    }

    }

    private _execWalk(b: BehaviorNode): void {

    let actorId = b.params.id;

    let destGridX = b.params.destGridX;

    let destGridY = b.params.destGridY;

    let actor = map.getActor(actorId);

    let curGridX = actor.gridX;

    let curGridY = actor.gridY;

    let loadGrids = AStar.findLoad(curGridX, curGridY, destGridX, destGridY);

    actor.setLoad(loadGrids);

    }

    一般地,游戏地图中的物件都可以挂载行为树,地图本身、角色、地图物品等,将一个剧情的复杂行为,分拆到每一个地图物件上,通过剧情任务作为条件区分触发,简化行为的组织。程序员只负责将策划的设定提取出细粒行为,编写对应的数据解析和执行方法,由策划使用编辑器编辑数据,由数据驱动剧情的推进。

    有限状态机和行为树先说到这里,下一篇我们将介绍状态同步。

    展开全文
  • 表驱动的模式,使用map 或者 配置文件的形式来做强关联 策略模式,抽取公共方法,让具体实现类继承该接口,通过map,或者反射的形式进行分支选择 责任链模式,实现责任链逻辑 有限状态机,在java网站领域并...
  • LuaBT是一种可作为服务端AI实现的行为树方案,是NodeCanvas行为树的Lua实现,支持Unity编辑,运行时预览和前调试。 特性 NodeCanvas行为树的Lua实现(不支持状态机) 支持Unity编辑行为树,导出JSON文件 支持运行时...
  • 模式的定义与特点 解释器(Interpreter)模式的定义:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。...解释器模式是一种类行为模式,其主要优点如下。
  • UE4学习笔记--AI行为树架构

    千次阅读 2019-02-18 15:57:12
    虚幻4AI行为控制采用事件驱动模式,需由行为树和黑板配合使用的,行为树执行AI逻辑,黑板通过黑板变量来存储AI数据, 黑板变量的改变事件驱动AI行为树的逻辑执行。 一、控制器-行为树关联: 每个AI都有自己的控制...
  • 行为树设计类似于组合设计模式行为树的结点这里粗略的分为枝结点和叶结点。 枝结点用来组织一个或以上的子节点,而叶结点用来进行判断或执行相应的逻辑。 首先先实现枝节点中的以下类型: 根结点(Root):仅用来...
  • 在语法中的每个表达式节点类都是类似的,所以实现其文法较为容易。 缺点 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦; 会...
  • 一、模式的定义与特点 解释器(Interpreter)模式:给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。 也就是说,用编译语言的...解释器模式是一种类行为模式,其优点如下:
  • 解释器模式是一种类行为模式。 优点: 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。 容易实现。在语法中的每个表达式节点类都是相似的,所以...
  • 功能 给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子 解决 主要解决:对于一些固定文法构建...如何解决:构建语法,定义终结符与非终结符 关键代码:构建环境类,包...
  • 黑板(Blackboard)是一种数据集中式的设计模式,一般用于多模块间的数据共享,我在做行为树的过程中,发现黑板非常适合作为行为树的辅助模块来使用,这次就来谈谈如何在行为树中使用黑板。 行为树的决策一般要依赖...
  • 迭代器模式是一种行为设计模式,让你能在不暴露集合底层表现形式(列表、栈和等)的情况下遍历集合中所有的元素。 问题 集合是编程中最常使用的数据类型之一。尽管如此,集合只是一组对象的容器而已。 ...
  • 解释器模式 解释器(Interpreter)模式的定义:给...例如,汉语中的句子有很多,“我是中国人”是其中的一个句子,可以用一棵语法来直观地描述语言中的句子。 优点 扩展性好。由于在解释器模式中使用类来表示语言的文
  • 解释器Interpreter是一种类行为型设计模式,目的是给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。当有一个语言需要解释执行,并且可将该语言中的句子表示为一...
  • 当有一个语言需要解释执行,并且你可将语言中的句子表示为一个抽象的语法时,可使用解释器模式。而当存在以下情况时该模式效果最好: 该文法简单对于复杂的文法,文法的类层次变得庞大而无法管理。此时语法分析程序...
  • 概述 很多情况下,在一个...职责链可以是一条直线、一个环或者一个形结构,最常见的职责链是直线型,即沿着一条单向的链来传递请求。链上的每一个对象都是请求处理者,职责链模式可以将请求的处理者组织成一条...
  • 职责链模式 很多情况下,在一个软件系统中可以处理某个请求的对象不止一个,例如SCM系统中的采购单审批,主任、副董事长、董事长和董事会都可以处理采购单,他们可以构成一条处理采购单的链式结构,采购单沿着这...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 918
精华内容 367
关键字:

行为树模式