API Documentation for:
Show:

File:BehaviorTree.js

import { createUUID } from '../b3.functions';
import { COMPOSITE, DECORATOR } from '../constants';
import * as Decorators from '../decorators';
import * as Composites from '../composites';
import * as Actions from '../actions';
import Tick from './Tick';

/**
 * The BehaviorTree class, as the name implies, represents the Behavior Tree
 * structure.
 *
 * There are two ways to construct a Behavior Tree: by manually setting the
 * root node, or by loading it from a data structure (which can be loaded
 * from a JSON). Both methods are shown in the examples below and better
 * explained in the user guide.
 *
 * The tick method must be called periodically, in order to send the tick
 * signal to all nodes in the tree, starting from the root. The method
 * `BehaviorTree.tick` receives a target object and a blackboard as
 * parameters. The target object can be anything: a game agent, a system, a
 * DOM object, etc. This target is not used by any piece of Behavior3JS,
 * i.e., the target object will only be used by custom nodes.
 *
 * The blackboard is obligatory and must be an instance of `Blackboard`. This
 * requirement is necessary due to the fact that neither `BehaviorTree` or
 * any node will store the execution variables in its own object (e.g., the
 * BT does not store the target, information about opened nodes or number of
 * times the tree was called). But because of this, you only need a single
 * tree instance to control multiple (maybe hundreds) objects.
 *
 * Manual construction of a Behavior Tree
 * --------------------------------------
 *
 *     var tree = new b3.BehaviorTree();
 *
 *     tree.root = new b3.Sequence({children:[
 *       new b3.Priority({children:[
 *         new MyCustomNode(),
 *         new MyCustomNode()
 *       ]}),
 *       ...
 *     ]});
 *
 *
 * Loading a Behavior Tree from data structure
 * -------------------------------------------
 *
 *     var tree = new b3.BehaviorTree();
 *
 *     tree.load({
 *       'title'       : 'Behavior Tree title'
 *       'description' : 'My description'
 *       'root'        : 'node-id-1'
 *       'nodes'       : {
 *         'node-id-1' : {
 *           'name'        : 'Priority', // this is the node type
 *           'title'       : 'Root Node',
 *           'description' : 'Description',
 *           'children'    : ['node-id-2', 'node-id-3'],
 *         },
 *         ...
 *       }
 *     })
 *
 *
 * @module b3
 * @class BehaviorTree
 **/

export default class BehaviorTree {

  /**
   * Initialization method.
   * @method initialize
   * @constructor
   **/
  constructor() {
    /**
     * The tree id, must be unique. By default, created with `createUUID`.
     * @property {String} id
     * @readOnly
     **/
    this.id = createUUID();

    /**
     * The tree title.
     * @property {String} title
     * @readonly
     **/
    this.title = 'The behavior tree';

    /**
     * Description of the tree.
     * @property {String} description
     * @readonly
     **/
    this.description = 'Default description';

    /**
     * A dictionary with (key-value) properties. Useful to define custom
     * variables in the visual editor.
     *
     * @property {Object} properties
     * @readonly
     **/
    this.properties = {};

    /**
     * The reference to the root node. Must be an instance of `BaseNode`.
     * @property {BaseNode} root
     **/
    this.root = null;

    /**
     * The reference to the debug instance.
     * @property {Object} debug
     **/
    this.debug = null;
  }

  /**
   * This method loads a Behavior Tree from a data structure, populating this
   * object with the provided data. Notice that, the data structure must
   * follow the format specified by Behavior3JS. Consult the guide to know
   * more about this format.
   *
   * You probably want to use custom nodes in your BTs, thus, you need to
   * provide the `names` object, in which this method can find the nodes by
   * `names[NODE_NAME]`. This variable can be a namespace or a dictionary,
   * as long as this method can find the node by its name, for example:
   *
   *     //json
   *     ...
   *     'node1': {
   *       'name': MyCustomNode,
   *       'title': ...
   *     }
   *     ...
   *
   *     //code
   *     var bt = new b3.BehaviorTree();
   *     bt.load(data, {'MyCustomNode':MyCustomNode})
   *
   *
   * @method load
   * @param {Object} data The data structure representing a Behavior Tree.
   * @param {Object} [names] A namespace or dict containing custom nodes.
   **/
  load(data, names) {
    names = names || {};

    this.title = data.title || this.title;
    this.description = data.description || this.description;
    this.properties = data.properties || this.properties;

    var nodes = {};
    var id, spec, node;
    // Create the node list (without connection between them)
    for (id in data.nodes) {
      spec = data.nodes[id];
      var Cls;

      if (spec.name in names) {
        // Look for the name in custom nodes
        Cls = names[spec.name];
      } else if (spec.name in Decorators) {
        // Look for the name in default nodes
        Cls = Decorators[spec.name];
      } else if (spec.name in Composites) {
        Cls = Composites[spec.name];
      } else if (spec.name in Actions) {
        Cls = Actions[spec.name];
      } else {
        // Invalid node name
        throw new EvalError('BehaviorTree.load: Invalid node name + "' +
          spec.name + '".');
      }

      node = new Cls(spec.properties);
      node.id = spec.id || node.id;
      node.title = spec.title || node.title;
      node.description = spec.description || node.description;
      node.properties = spec.properties || node.properties;

      nodes[id] = node;
    }

    // Connect the nodes
    for (id in data.nodes) {
      spec = data.nodes[id];
      node = nodes[id];

      if (node.category === COMPOSITE && spec.children) {
        for (var i = 0; i < spec.children.length; i++) {
          var cid = spec.children[i];
          node.children.push(nodes[cid]);
        }
      } else if (node.category === DECORATOR && spec.child) {
        node.child = nodes[spec.child];
      }
    }

    this.root = nodes[data.root];
  }

  /**
   * This method dump the current BT into a data structure.
   *
   * Note: This method does not record the current node parameters. Thus,
   * it may not be compatible with load for now.
   *
   * @method dump
   * @return {Object} A data object representing this tree.
   **/
  dump() {
    var data = {};
    var customNames = [];

    data.title = this.title;
    data.description = this.description;
    data.root = (this.root) ? this.root.id : null;
    data.properties = this.properties;
    data.nodes = {};
    data.custom_nodes = [];

    if (!this.root) return data;

    var stack = [this.root];
    while (stack.length > 0) {
      var node = stack.pop();

      var spec = {};
      spec.id = node.id;
      spec.name = node.name;
      spec.title = node.title;
      spec.description = node.description;
      spec.properties = node.properties;
      spec.parameters = node.parameters;

      // verify custom node
      var proto = (node.constructor && node.constructor.prototype);
      var nodeName = (proto && proto.name) || node.name;
      if (!Decorators[nodeName] && !Composites[nodeName] && !Actions[nodeName] && customNames.indexOf(nodeName) < 0) {
        var subdata = {};
        subdata.name = nodeName;
        subdata.title = (proto && proto.title) || node.title;
        subdata.category = node.category;

        customNames.push(nodeName);
        data.custom_nodes.push(subdata);
      }

      // store children/child
      if (node.category === COMPOSITE && node.children) {
        var children = [];
        for (var i = node.children.length - 1; i >= 0; i--) {
          children.push(node.children[i].id);
          stack.push(node.children[i]);
        }
        spec.children = children;
      } else if (node.category === DECORATOR && node.child) {
        stack.push(node.child);
        spec.child = node.child.id;
      }

      data.nodes[node.id] = spec;
    }

    return data;
  }

  /**
   * Propagates the tick signal through the tree, starting from the root.
   *
   * This method receives a target object of any type (Object, Array,
   * DOMElement, whatever) and a `Blackboard` instance. The target object has
   * no use at all for all Behavior3JS components, but surely is important
   * for custom nodes. The blackboard instance is used by the tree and nodes
   * to store execution variables (e.g., last node running) and is obligatory
   * to be a `Blackboard` instance (or an object with the same interface).
   *
   * Internally, this method creates a Tick object, which will store the
   * target and the blackboard objects.
   *
   * Note: BehaviorTree stores a list of open nodes from last tick, if these
   * nodes weren't called after the current tick, this method will close them
   * automatically.
   *
   * @method tick
   * @param {Object} target A target object.
   * @param {Blackboard} blackboard An instance of blackboard object.
   * @return {Constant} The tick signal state.
   **/
  tick(target, blackboard) {
    if (!blackboard) {
      throw 'The blackboard parameter is obligatory and must be an ' +
      'instance of b3.Blackboard';
    }

    /* CREATE A TICK OBJECT */
    var tick = new Tick();
    tick.debug = this.debug;
    tick.target = target;
    tick.blackboard = blackboard;
    tick.tree = this;

    /* TICK NODE */
    var state = this.root._execute(tick);

    /* CLOSE NODES FROM LAST TICK, IF NEEDED */
    var lastOpenNodes = blackboard.get('openNodes', this.id);
    var currOpenNodes = tick._openNodes.slice(0);

    // does not close if it is still open in this tick
    var start = 0;
    var i;
    for (i = 0; i < Math.min(lastOpenNodes.length, currOpenNodes.length); i++) {
      start = i + 1;
      if (lastOpenNodes[i] !== currOpenNodes[i]) {
        break;
      }
    }

    // close the nodes
    for (i = lastOpenNodes.length - 1; i >= start; i--) {
      lastOpenNodes[i]._close(tick);
    }

    /* POPULATE BLACKBOARD */
    blackboard.set('openNodes', currOpenNodes, this.id);
    blackboard.set('nodeCount', tick._nodeCount, this.id);

    return state;
  }

  /**
   * Close all open nodes to ensure close function in nodes be called.
   * If stop running tree tick via external, call this function.
   *
   * @method close
   * @param {Object} target A target object.
   * @param {Blackboard} blackboard An instance of blackboard object.
   */
  close(target, blackboard) {
    if (!blackboard) {
      throw 'The blackboard parameter is obligatory and must be an ' +
      'instance of b3.Blackboard';
    }

    /* CREATE A TICK OBJECT */
    var tick = new Tick();
    tick.debug = this.debug;
    tick.target = target;
    tick.blackboard = blackboard;
    tick.tree = this;

    /* CLOSE ALL OPEN NODES */
    var lastOpenNodes = blackboard.get('openNodes', this.id);
    for (var i = 0; i < lastOpenNodes.length; i++) {
      lastOpenNodes[i]._close(tick);
    }

    /* CLEAR BLACKBOARD */
    blackboard.clear(this.id);
  }
};