测试用例 Logstash 网页后台模板 linux查看mysql进程 sublime分屏快捷键 python与机器学习 erp项目描述 kb转mb 增删改查sql语句 多线程实现方式 mysql合并结果集 python中的join函数 java搭建 python网站开发实例 javascript案例 俄罗斯方块java代码 彻底删除mysql oem修改器 collect 魔兽改图工具 摩斯密码在线翻译 pr放大画面 软件龙头股 mtu设置多少最好 灰色按钮激活精灵 igfxpers 汇通启富下载 图片文字提取软件 lol改皮肤软件 php单例模式 唯品会客服在哪 this关键字 金融大师 java数组转字符串 cdr复制属性快捷键 CST软件 hdcp功能 imm32 服务器软件 nwiz
当前位置: 首页 > 学习教程  > 编程语言

学习行为树的心得,以及如何使用BehaviorTree.js

2020/8/31 14:47:41 文章标签:

首先给大家推荐两篇个人认为最好的,讲行为树的文章。行为树原理

还有Unreal引擎的这篇,应该算是进阶版行为树进阶

然后自己在github上面找了一个相对比较受欢迎的行为树框架BehaviorTree.js

README.md 中有两种方法

  1. 方法一:一个个建立节点 BehaviorTree.register()
  2. BehaviorTreeImporter 一次性导入json格式的行为树

第一种方法

有三种状态,actionA, actionB, actionC ,并且给与它们一个共同的selector节点,此树从左向右执行,其中一种状态返回结果为SUCCESS的话就直接返回整个树的结果 SUCCESS,如果状态返回的是FAILURE,那么继续执行右边的节点, 如果所有叶子节点都是FAILURE,那么整个树返回 FAILURE

行为树

用一个对象来表示下:

let board = {
  actionA: () => console.log("actionA"),
  actionB: () => console.log("actionB"),
  actionC: () => console.log("actionC"),
};

这里的board 其实就是以后要传到行为树中的blackboard
blackboard 是行为树中很重要的概念。我一开始也不太理解,后来想到其实就是黑板的原意。

我们把这个对象的一切行为都写在黑板上向大家展示。然后再输入到行为树的框架中,通过逻辑运算来控制,告知大家黑板上的当前行为在当前状态下如何运行。 我们先注册下每个行为:


const {
  BehaviorTree,
  BehaviorTreeImporter,
  Sequence,
  Selector,
  Random,
  Task,
  SUCCESS,
  FAILURE,
  RUNNING,
} = require("behaviortree"); //npm下载以后先按需require一下

BehaviorTree.register(
  "actionA", // 此行为,或者叫任务的名字
  new Task({ //行为树会根据条件执行的构造函数
    run: (blackboard) => {
      blackboard.actionA();
      return SUCCESS;
    },
  })
);

BehaviorTree.register(
  "actionB",
  new Task({
    run: (blackboard) => {
      blackboard.actionB();
      return SUCCESS;
    },
  })
);

BehaviorTree.register(
  "actionC",
  new Task({
    run: (blackboard) => {
      blackboard.actionB();
      return SUCCESS;
    },
  })
);

我们不满足于简单执行,所以我希望给其中的某一个行为加上一个修饰节点,而我加上的是条件节点。 这个条件节点是我自己写的,目的是,如果为true,就执行此节点以及此节点为根部的所有叶子节点,如果为false, 则直接返回 FAILURE,而不执行这一支的节点。

const { FAILURE, Decorator } = require("behaviortree");

module.exports = class ConditionDecorator extends Decorator {
  constructor(props) {
    super(props);
    this.nodeType = "ConditionDecorator ";
  }
  setConfig({ isMatch}) {
    this.config = {
      isMatch
    };
  }

  decorate(run) { 
    if (this.config.isMatch) return run();
    return FAILURE;
  }
};

行为树基本上可以取代状态机,并且比状态机有更好的扩展性,以及易读性。

但是Unreal提出一个问题, 行为树真正执行任务的节点都在叶子部位,也就是最底层。而中间的根茎部位都是进行逻辑判断。

如果我在某一时刻,已经知道某个支路是不需要执行的,希望立马跳出,但是不可以,因为一直要执行到最底层的叶子节点才能判断。无论从效率上还是从结构的易读性上都不好。

需要执行到最底层

所以我们需要给这个支路加上一个修饰节点,立马返回结果给上层节点,从而跳过此支路的执行。

const ConditionDecorator = require("./conditionDecorator");
let conditionTask = new ConditionDecorator({
  config: { isMatch: true},
  node: "actionA", 
});//此条件节点叠加在actionA之上

BehaviorTree.register("condition", conditionTask);

再向上叠加一个selector 节点

const mySelector = new Selector({
    nodes: ["condition", "actionB", "actionC"],
 });

BehaviorTree.register("the root", mySelector);

那这个行为树的结构就变成

条件节点
把这颗树画好以后,我们就去生成行为树的实例

 let bTree = new BehaviorTree({
   tree: mySelector, //根节点,已经连接上整个树
   blackboard: board, 
  });

目前conditionTaskconfig.isMatchtrue, 所以照常执行,如果我们在运行中让config.isMarch = false那么actionA将不会执行,直接返回FAILURE, 因为顶层是 selector节点,所以接下来会继续执行actionB

如果我们想执行这棵树,可以这么写

setInterval(() => {
  bTree.step();  
}, 1000);

用这种方法并不好,因为会消耗太多的不必要的计算资源。

Unreal 建议我们只有在事件驱动的时候才执行行为树,也就是说有必要才执行,没有必要不应该执行。

如果我们要在运行中不执行actionA, 那么就要更改conditionTaskconfig

setTimeout(() => { //这里用延时代替事件驱动
   conditionTask.setConfig({ isMatch: false });
 }, 3000);

这时候你会发现actionA 将不会被执行,直接执行actionB, 这就是我们需要的效果。

这种一个一个task注册的方法既在这里插入代码片有优点也有缺点。

  • 优点:在行为树的运行过程中,你可以修改动态的修改修饰节点的配置,从而达到控制行为树的目的。
  • 缺点:就是写起来太麻烦,而且你无法从代码很直观的看到整个行为树长什么样。

第二种方法

JSON

我们希望用一种更加直观的方法,直接给出一整颗行为树,增加易读性,同时减少代码量。

我们用json格式来表示

let json = {
  type: "selector",
  name: "the root",
  nodes: [
    {
      type: "condition",
      name: "condition",
      node: {
        type: "actionA",
        name: "actionA",
      },
      isMatch: true, 
    },
    {
      type: "actionB",
      name: "actionB",
    },
    {
      type: "actionC",
      name: "actionC",
    },
  ],
};

当然,我们还是要通过 BehaviorTree.register()注册task,不过只需要注册这些反应业务状况的叶子节点就好。不需要注册逻辑节点,诸如 selector, sequendecoratorce 等,也不需要注册内置的修饰节点decorator

BehaviorTree.register(
  "actionA",
  new Task({
    run: (blackboard) => {
      blackboard.actionA();
      return SUCCESS;
    },
  })
);

BehaviorTree.register(
  "actionB",
  new Task({
    run: (blackboard) => {
      blackboard.actionB();
      return SUCCESS;
    },
  })
);

BehaviorTree.register(
  "actionC",
  new Task({
    run: (blackboard) => {
      blackboard.actionB();
      return SUCCESS;
    },
  })
);

当然,如果是自定义的修饰节点,就需要定义一下。

const ConditionDecorator = require("./conditionDecorator");
const behaviorTreeImporter = new BehaviorTreeImporter();
behaviorTreeImporter.defineType('condition', ConditionDecorator);

这里的condition, 对应于json数据结构中的 node type
在json中,节点之间的关系,也就是逻辑节点和修饰节点的配置已经在数据结构中反应出来了,所以不需要重复注册。

接下来再把json解析,以及生成行为树的实例对象。

let bTree = new BehaviorTree({
  tree: behaviorTreeImporter.parse(json),
  blackboard: board,
});

然后运行即可, 运行方法和第一种方法一样。

YAML

我们还可以做一些小小的改进。用YAML文件来替代 json。 这样可以进一步简化。把它作为一种独立的文件单独保存,当作游戏资源加载,提升性能和可配置性。

btree.yml

type: selector
name: the root
nodes:
  - type: condition
    name: condition
    node: 
      type: actionA
      name: actionA
    isMatch: true
  - type: actionB
    name: actionB 
  - type: actionC
    name: actionC 

然后我们通过npm,下载一个ymaljs 包,用异步方法来加载yaml文件,并且转换成json 格式。 然后生成实例再运行行为树。

async function loadBTreeFile(file, blackboard) {
   const json = await YAML.load(file);
   return new BehaviorTree({
     tree: behaviorTreeImporter.parse(json),
     blackboard,
   });
 }

 loadBTreeFile("./btree.yml", board).then((bTree) => exec(bTree));

 let exec = (bTree) => {
   setInterval(() => {
     bTree.step()}, 1000);
 };

这样是不是更酷一点呢?

来说说这种方法的优缺点:

  • 优点:直接加载一整颗行为树,易读性很好
  • 缺点:初始加载的配置是什么样就是什么样。在runtime中,无法修改行为树的配置,比如给修饰节点传递参数。
setTimeout(()=>{
  let conditionTask = new ConditionDecorator({
    config: { isMatch: false }, //把true改成false
    node: "actionA",
  });
  BehaviorTree.register("condition", conditionTask);
})

这里修饰节点的实例,不是当前运行中行为树里修饰节点的实例。因为 在创造behaviorTreeImporter 实例时, 会自行生成逻辑节点和修饰节点的实例。

我思考调试半天,只能用一种黑客方法,黑进行为树来修改。

 let tree; 
 let exec = (bTree) => {
   setInterval(() => {
     tree = bTree.step()}, 1000);
 };

 setTimeout(() => {
    tree.blueprint.nodes[0].config.isMatch = false;
 }, 3000);

结构非常不清晰,这种方法显然不好。

github 上和作者沟通后,作者提供了一个不一样的修饰节点,解决了这个问题。

修饰节点:CanJumpDecorator

这个修饰节点之所以可行,是因为我们不必在运行时动态的改变修饰节点的配置项,而是通过直接传入参数 blackboard,并且指定配置项 checkblackboard的某个属性。通过在runtime改变blackboard的目标属性, 来动态的改变修饰节点的配置。

我承认,作者比我更聪明,能想到用这种方法。我之前也隐约想到可以通过改变blackboard的属性来改变行为树的配置,但是我却写不出代码。

很明显,这种方法更好。为你可以在blackboard 的初始化中显示自己的配置项。使得其和行为树一体,一目了然的看出来你想要做什么。如果runtime中,去改修饰节点的内部配置的话,你就无法在初始状态下,看清楚整个行为树的逻辑是什么。

所以,我推荐各位用第二种方法,直接通过生成behaviorTreeImporter 实例来加载整个行为树,然后通过额外再blackboard 中设置修饰节点的配置项,以达到在runtime中,动态更新行为树逻辑的目的。

至此之前我们提到的缺点 都已经被完美解决了。

希望各位都能创作出酷炫好玩的游戏哦!


本文链接: http://www.dtmao.cc/news_show_150290.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?