Calamari/BehaviorTree.js

How to pass parameters to behaviortree tasks

gsommavilla opened this issue · 14 comments

I am asking for an explanation about how to pass parameters to the
behaviortree tasks.

In the README.md file I see that, in a JSON example, the
"cooldown" decorator presents a key called "cooldown" set to 1. I have
tested this decorator and I have seen that his logic is to run the
child node and then wait a number of seconds equal to the "cooldown"
key (I tried with very different values).

I looked at the behaviortree source code and saw that the setConfig function
is overridden in CooldownDecorator.js, like this:

setConfig ({ cooldown = 5 }) {
  this.config = {
    cooldown
    }
  }

I think it is this function (which is invoked in the Decorator
constructor - from which CooldownDecorator.js inherits) that reads the
value of the "cooldown" key in the sample JSON and sets it in the
this.config object of CoolDownDecorator. Is it correct?

In this way I seem to have understood how to pass parameters to
decorators, but I do not understand how to apply this functionality to
tasks, since I see that the setConfig function is not present in
Task.js nor in Node.js (from which Task.js inherits).

An example of what I would like to do is the following:

  1. create a "say" task in the BT register that triggers a speech
    synthesizer saying the string "utterance" (which by default is
    "Hello!")

  2. create a BT defined by a JSON file that uses the "say" task and
    specify a parameter with "utterance" as key and "Have a nice day!"
    as value (different from the default).

Can you help me with this?

again, this is late, but i think the entire JSON defintion of the type is passed to the Decorator's constructor, so in your decorator class you would write it like:

import { Decorator, RUNNING, SUCCESS, FAILURE } from "behaviortree";

export class CooldownDecorator extends Decorator {
  constructor(props) {
    super(props);
    this.nodeType = "CooldownDecorator ";
    this.setConfig(props);
  }

  setConfig ({ cooldown = 5 }) {
    this.config = {
      cooldown
    }
  }

  decorate (run) {
    if (this.lock) {
      return FAILURE
    }
    this.lock = true
    setTimeout(() => {
      this.lock = false
    }, this.config.cooldown * 1000)
    return run()
  }
}

looking more at the source... you will also have to register your custom type, or overwrite CooldownDecorator class type like this as an example:

// import a custom cooldown decorator and behavior tree importer
import { CooldownDecorator} from './decorators/CooldownDecorator';
import { BehaviorTreeImporter} from 'behaviortree';

// (... import your other node types 
// and register them here)

// instantiate importer
const behaviorTreeImporter = new BehaviorTreeImporter();

// overwrite the `cooldown` decorator type with custom cooldown decorator class
behaviorTreeImporter.defineType ('cooldown', CooldownDecorator);

// parse json string into an object
const importedBehaviorTree = JSON.parse(`{
  "type": "selector",
  "name": "the root",
  "nodes": [
    {
      "type": "ifEnemyInSight",
      "name": "handling enemies",
      "node": { "type": "walk", "name": "go to enemy" }
    },
    {
      "type": "cooldown",
      "name": "jumping around",
      "cooldown": 1,
      "node": { "type": "jump", "name": "jump up" }
    },
    { "type": "idle", "name": "doing nothing" }
  ]
}`);

behaviorTreeImporter.parse(importedBehaviorTree);

Very well written. Thanks for chipping in.
I wonder why I didn't see the issue…

If there are other problems @gsommavilla, please still ask :) I try to be more responsive next time :-x

what if i want to change cooldown = 6, during running?
how can we change parameters of decorator in runtime?

What exactly do you mean with "during running"?
Usually you do a "think" step that decides the next action, so you call step on your root tree node, which then walks thought the tree for you.

If you take @sxtxixtxcxh’s example from above:

import { Decorator, RUNNING, SUCCESS, FAILURE } from "behaviortree";

export class CooldownDecorator extends Decorator {
  constructor(props) {
    super(props);
    this.nodeType = "CooldownDecorator ";
    this.setConfig(props);
  }

  setConfig ({ cooldown = 5 }) {
    this.config = {
      cooldown
    }
  }

  decorate (run) {
    if (this.lock) {
      return FAILURE
    }
    this.lock = true
    setTimeout(() => {
      this.lock = false
    }, this.config.cooldown * 1000)
    return run()
  }
}

you can call decorator.setConfig({ cooldown: 5 }) before one tree.step() call and before the next step you can do decorator.setConfig({ cooldown: 6 }) and the steps afterwards will use a 6 second cooldown from now.

If you want to get really fancy, not quite sure if that is a good idea or not, then you could even write decorators that have configs that figure their values out on the go. Here is an example:

import { Decorator, RUNNING, SUCCESS, FAILURE } from "behaviortree";

function randomCooldown() {
  return Math.random() * 3 + 3
}

export class CooldownDecorator extends Decorator {
  constructor(props) {
    super(props);
    this.nodeType = "CooldownDecorator ";
    this.setConfig(props);
  }

  setConfig ({ getCooldown = randomCooldown }) {
    this.config = {
      getCooldown
    }
  }

  decorate (run) {
    if (this.lock) {
      return FAILURE
    }
    this.lock = true
    setTimeout(() => {
      this.lock = false
    }, this.config.getCooldown() * 1000)
    return run()
  }
}

This would be default generate a cooldown decorator that has a random cooldown on every step, and you could even pass in a different getCooldown method that returns a specific cooldown. But I somehow think that it makes the behavior tree harder to reason about and/or debug. But it might have it’s use cases. 🤔

Does this help?

thanks a lot. it really helps. but i am thinking another fancy one. i am think of how to make it happen in BehaviorTreeImporter .
i am writing a condition decorator, inspired by Unreal Game Engine. to me, it is very helpful, i can take it as state machine. it can decide if i need to run root node or not instead of making judgement in leaf node only, this can simplify behavior tree.

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

module.exports = class ConditionDecorator extends Decorator {
  nodeType = "ConditionDecorator";
  setConfig({isMatch = true}) {
    this.config = {
      isMatch,
    };
  }

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

in the meantime, i want to import a yaml file as resource then convert into json file for BehaviorTreeImporter



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

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


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


This is yaml file

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

As you can see i can't change config isMatch value in runtime.
the only way it can work is that i hack into the bTree as below, after i study the bTree structure. it doesn't look graceful.

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

setTimeout(() => {
   tree.blueprint.nodes[0].config.isMatch = false; //hack into bTree to change condition decorator config in runtime.
}, 3000);

not sure if i describe it clearly.

Thanks @sunq0001 for the explanation. I now understand what you want to achieve. A train ride is a good place to think about this kind of stuff :-)

So the main problem that does not work, as far as I understand it, is that you want to add configuration within the blueprint used within the import that can configure your tree. That sounds like a reasonable thing to do.
But it does not work (right now) due to a limitation. The importer does not use additional properties.

But I think I have an idea, and will sketch up a solution.

I added a PR that should make the importer more useful and should make your use case possible. @sunq0001 , please look at the examples/importingExample.js file in that PR. Can this work for you? I tried modelling your use case there.
Still, have to finish adding tests.

Thanks a lot @Calamari
Your solution is better than mine because you are thinking with a train ride, while i am thinking hard when lying in bed :)
You cleverly use blackboard property to control the decorator's config, but i only think over by playing with decorator's internal config.
It totally works for me.
i believe that with this update, the behaviortree.js can work more efficiently as you can get away with unnecessary calculation. and the json tree is more readable. :)

Well, it looks like the vacation I am just going to is duely needed. The good thing is, there are no changes needed to make it work.
I just updated the example (changed 2 lines), so you can use that way right away. Everything is already in place 🤦

Glad if that helps :-)

If you like you can share the finished piece where you use it. I am like to see, what people are using it for.

i will once i finished my game. :)

I'm running into wanting to do the exact same thing with the Importer. I want to write BT's and customize the parameters of the task(s) in the json/yaml and use them when the task runs. Is it possible to get this merged in to use?

Actually I think it was merged looking at the code. My problem is I'm trying to property drive things in Tasks not just decorators. I see the setConfig stuffs in the Decorator class, is there a bad idea I'm not thinking of moving that up to the Node level so all things get it?

Maybe this is an antipattern with BT's, to me it feels more natural to add an enemies default run speed into the move task and override it there when I want to when I define certain enemies. But I could probably get convinced to bury that information into the bb somewhere.

Hey @gbarton.
I would consider a different approach: The moving speed of an enemy is depending on the entity itself (maybe also health, since feeling sick or having a limp might slow you down?) so I would put such variables into the enemy and pass the enemy into the blackboard. That way, you can have a generic movement behavior node that just uses the speed from the agent it is working on.

What do you think?

Yea I went with something similar for now. The modifiers is exactly what I was trying to do, if the enemy was shot, its run speed was diminished so the run task under 'damaged' branch had a lower base run speed than its normal run task in 'attacking' branch.

Thanks again for an awesome little library!