Behavior Tree Framework

For Cornell University Game Library

Read-Only Tree Structures

The behavior tree in this project is designed as a read-only structure, enabling multiple instances to operate on a single shared tree object. This approach eliminates the need to duplicate the tree for each entity, significantly reducing memory usage and improving runtime efficiency. At compile time, the tree's logic is pre-validated and optimized, ensuring predictable behavior and avoiding costly runtime errors. This design streamlines execution and allows for scalable AI systems, where many entities can simultaneously exhibit complex, coordinated behaviors with minimal overhead.

Instance initialization with reference to Tree

Multiple instances share a single read-only behavior tree, passed as a parameter during initialization. Each instance manages its own NodeDataVector for unique execution data like random seeds, ensuring independent behaviors. This design reduces memory overhead, enhances runtime efficiency, and simplifies instance creation without duplicating the tree's logic.

/**
       * @param instanceKey The string key uniquely identifying this TreeInstance.
       * @param tree        The behvaior tree defining this instance's logic.
       * @param actionMap   The user-defined map of leaf node behaviors for this TreeInstance.
       *
       * @return true if this TreeInstance was successfully initialized.
       */
      bool init(std::string instanceKey, std::shared_ptr<BehaviorTree> tree, const Node::ActionMap actionMap)
      {
        if (instanceKey.empty())
          return false;
        std::random_device rd;
        this->instanceKey = instanceKey;
        this->tree = tree;
        std::shared_ptr<Node::NodeDataVector> dataVector = std::make_shared<Node::NodeDataVector>();
        unsigned int randSeed = rd();
        this->seed = randSeed;
        CULog("Instance created with seed %u", seed);
        nodeDataVector = dataVector;
        this->createNodeDataVector(this->tree->rootNode, nodeDataVector);
        (*nodeDataVector)[0].seed = randSeed;
        assignLeafExecutes(actionMap);

        return true;
      }
/**
       * Executes a user-defined leaf node. This node executes the user-defined function stored in
       * the node data vector passed in from executing instance.
       *
       * @param data  The node data vector from the tree instance running this node.
       *
       * @throw exception if data does not store an execute function.
       * @return the result of the user-defined execute function.
       */
      std::function<State(std::shared_ptr<NodeDataVector>)> executeUserLeaf = [this](std::shared_ptr<NodeDataVector> data)
      {
        ExecuteType execute = getNodeData(data, treeIndex).userDefinedExecute;
        if (!execute)
        {
          throw std::exception("No matching user-defined execute in this tree instance.");
        }
        State result = execute(data);
        return result;
      };

      /**
       * Executes a root node. Executing this node will execute the tree rooted at this node.
       *
       * A root node has a single child and returns the same state as its child. When a root
       * node completes and returns success or failure it resets the tree.
       *
       * @param data  The node data vector from the tree instance running this node.
       *
       * @return the state its child returns.
       */
      ExecuteType executeRoot = [this](std::shared_ptr<NodeDataVector> data)
      {
        CULog("execute root");
        State state = getChild(0)->execute(data);
        (*data)[treeIndex].state = state;
        if (state == State::Success || state == State::Failure)
        {
          reset(data);
        }
        return (*data)[treeIndex].state;
      }

User-Custom Behaviors

We dynamically retrieve and run the user-defined function stored in the instances' NodeDataVector, ensuring flexibility and adaptability in node behavior. By allowing custom logic at the leaf level, this design provides a powerful way to tailor AI behaviors while maintaining the efficiency and structure of the shared behavior tree. It ensures extensibility without compromising the tree's core logic.

This project is part of the latest version of Cornell University Game Library (CUGL). The complete version of the code can be found via GDIAC with a Cornell Identity.