Definition Creation

This is a practitioner’s guide to using the Execution Framework. Before continuing, it is recommended you first review the Execution Framework Overview along with basic topics such as Graphs Concepts, Pass Concepts, and Execution Concepts.

Definitions in the Execution Framework define the work each node represents. Definitions come in two forms: opaque definitions (implemented by NodeDef) and definitions described by a graph (i.e. NodeGraphDef). Each is critical to EF’s operation. This article covers how to create both.

Customizing NodeDef

NodeDef encapsulates opaque user code the Execution Framework cannot examine/optimize.

Probably the best example of how we can customize NodeDef is by looking at how NodeDefLambda is implemented. The implementation is simple. At creation, the object is given a function pointer, which it stores. When INodeDef::execute() is called, the stored function is invoked.

Listing 14 Implementation of NodeDefLambda
class NodeDefLambda : public NodeDef
{
public:
    //! Templated constructor for wrapper class
    //!
    //! The given definition name must not be @c nullptr.
    //!
    //! The given invokable object myst not be @c nullptr.
    //!
    //! The returned object will not be @c nullptr.
    //!
    //! @tparam Fn Invokable type (e.g. function, functor, lambda, etc) with the signature `Status(ExecutionTask&)`.
    //!
    //! @param definitionName Definition name is considered as a token that transformation passes can register against.
    //!
    //! @param fn Execute function body. Signature should be `Status(ExecutionTask&)`.
    //!
    //! @param schedInfo Fixed at runtime scheduling constraint.
    template <typename Fn>
    static omni::core::ObjectPtr<NodeDefLambda> create(const carb::cpp::string_view& definitionName,
                                                       Fn&& fn,
                                                       SchedulingInfo schedInfo) noexcept
    {
        OMNI_GRAPH_EXEC_ASSERT(definitionName.data());
        return omni::core::steal(new NodeDefLambda(definitionName, std::forward<Fn>(fn), schedInfo));
    }

protected:
    //! Templated and protected constructor for wrapper class.
    //!
    //! Use the `create` factory method to construct objects of this class.
    template <typename Fn>
    NodeDefLambda(const carb::cpp::string_view& definitionName, Fn&& fn, SchedulingInfo schedInfo) noexcept
        : NodeDef(definitionName), m_fn(std::move(fn)), m_schedulingInfo(schedInfo)
    {
    }

    //! @copydoc omni::graph::exec::unstable::IDef::execute_abi
    Status execute_abi(ExecutionTask* info) noexcept override
    {
        OMNI_GRAPH_EXEC_ASSERT(info);
        return m_fn(*info);
    }

    //! @copydoc omni::graph::exec::unstable::IDef::getSchedulingInfo_abi
    SchedulingInfo getSchedulingInfo_abi(const ExecutionTask* info) noexcept override
    {
        return m_schedulingInfo;
    }

private:
    std::function<Status(ExecutionTask&)> m_fn; //!< Execute function body
    SchedulingInfo m_schedulingInfo; //!< Scheduling constraint
};

Customizing NodeGraphDef (static symbols)

When all nodes in the graph are known at compilation time, we can define the nodes as part of the definition. Below is an example of constructing a behavior tree. Notice the nodes in the behavior tree are members of the definition and therefore owned by the definition.

Listing 15 Definition of a behavior tree.
//
//                                                 ┌─────────────────┐
//                                                 │                 │
//                                                 │    SEQUENCE     │
//                                                 │                 │
//                                                 └────────┬────────┘
//                                                          │
//                                   ┌──────────────────────┴──────────────┬─────────────────────────┐
//                                   │                                     │                         │
//                          ┌────────▼────────┐                   ┌────────▼─────────┐      ┌────────▼────────┐
//                          │                 │                   │ ┌──────────────┐ │      │                 │
//                          │    SELECTOR     │                   │ │BtRunAndWinDef│ │      │    CELEBRATE    │
//                          │                 │                   │ └──────────────┘ │      │                 │
//                          └────────┬────────┘                   └──────────────────┘      └─────────────────┘
//                                   │
//            ┌──────────────────────┴───────────────────────┐
//            │                                              │
//   ┌────────▼────────┐                            ┌────────▼────────┐
//   │                 │                            │                 │
//   │ READY FOR RACE  │                            │  TRAIN TO RUN   │
//   │                 │                            │                 │
//   └─────────────────┘                            └─────────────────┘
//! Nested behavior tree leveraging composability of EF to add training behavior to BtRunAndWinDef definition.
//! We added a @p CELEBRATE node which together with the behavior @p SEQUENCE will require proper state propagation
//! from nested @p BtRunAndWinDef definition.
class BtTrainRunAndWinDef : public NodeGraphDef
{
public:
    //! Factory method
    static omni::core::ObjectPtr<BtTrainRunAndWinDef> create(IGraphBuilder* builder)
    {
        auto def = omni::core::steal(new BtTrainRunAndWinDef(builder->getGraph(), "tests.def.BtTrainRunAndWinDef"));
        def->build(builder);
        return def;
    }

    // The definition owns its nodes
    using NodePtr = omni::core::ObjectPtr<Node>;
    NodePtr sequenceNode;
    NodePtr selectorNode;
    NodePtr readyNode;
    NodePtr trainNode;
    NodePtr runAndWinNode;
    NodePtr celebrateNode;

protected:
    //! Constructor
    BtTrainRunAndWinDef(IGraph* graph, const carb::cpp::string_view& definitionName) noexcept;

private:
    //! Connect the topology of already allocated nodes and populate definition of @p runAndWinNode node
    void build(IGraphBuilder* parentBuilder) noexcept
    {
        // Create the graph seen above using the builder. Only builder objects can modify the topology.
        auto builder{ GraphBuilder::create(parentBuilder, this) };
        builder->connect(getRoot(), sequenceNode);
        builder->connect(sequenceNode, selectorNode);
        builder->connect(sequenceNode, runAndWinNode);
        builder->connect(sequenceNode, celebrateNode);
        builder->connect(selectorNode, readyNode);
        builder->connect(selectorNode, trainNode);

        builder->setNodeGraphDef(runAndWinNode, BtRunAndWinDef::create(builder.get()));
    }
};

Customizing NodeGraphDef

When we do not know the nodes at compile time, we are still responsible for maintaining the nodes’ lifetime. We also are encouraged to reuse of nodes between topology changes.

In the example below, we create a definition that builds a graph where each node represents a runner. The number of runners is not known at compile time and is specified at runtime as an argument to the build() method. During build(), each node is stored in a std::vector and a definition is attached to the node to define each runner’s behavior.

Listing 16 Definition of a runner graph using behavior tree.
//                   ┌────────────┐
//                   │            │
//             ┌────►│  Runner_1  │
//             │     │            │
//             │     └────────────┘
//             │     ┌────────────┐
//         ├───┘     │            │
//         ├────────►│    ...     │
//         ├───┐     │            │
//             │     └────────────┘
//             │     ┌────────────┐
//             │     │            │
//             └────►│  Runner_N  │
//                   │            │
//                   └────────────┘
//! Definition for instantiating a given number of runners. Each runner shares the same @p NodeGraphDef provided as a
//! template parameter RunnerDef. Definition can be repopulated with reuse on nodes and definition.
template <typename RunnerDef>
class BtRunnersDef : public NodeGraphDef
{
    using ThisClass = BtRunnersDef<RunnerDef>;

public:
    //! Factory method
    static omni::core::ObjectPtr<ThisClass> create(IGraph* graph)
    {
        return omni::core::steal(new ThisClass(graph, "tests.def.BtRunnersDef"));
    }

    //! Construct the graph topology by reusing as much as possible already allocated runners.
    //! All runners will share the same behavior tree instance.
    void build(IGraphBuilder* builder, uint32_t runnersCount)
    {
        if (runnersCount < m_all.size())
        {
            m_all.resize(runnersCount);
        }
        else if (runnersCount > m_all.size())
        {
            m_all.reserve(runnersCount);

            NodeGraphDefPtr def;
            if (m_all.empty())
            {
                def = RunnerDef::create(builder);
            }
            else
            {
                def = omni::core::borrow(m_all.front()->getNodeGraphDef());
            }

            for (uint64_t i = m_all.size(); i < runnersCount; i++)
            {
                std::string newNodeName = carb::fmt::format("Runner_{}", i);
                auto newNode = Node::create(getTopology(), def, newNodeName);
                m_all.emplace_back(newNode);
            }
        }

        INode* rootNode = getRoot();
        for (uint64_t i = 0; i < m_all.size(); i++)
        {
            builder->connect(rootNode, m_all[i].get());
        }
    }

    //! Acquire runner state in given execution context at given index. If doesn't exist, default one will be allocated.
    BtActorState* getRunnerState(IExecutionContext* context, uint32_t index);

protected:
    //! Initialize each runner state when topology changes. Make goals for each runner different.
    void initializeState_abi(ExecutionTask* rootTask) noexcept override;

    //! Constructor
    BtRunnersDef(IGraph* graph, const carb::cpp::string_view& definitionName) noexcept
        : NodeGraphDef(graph, BtRunnersExecutor::create, definitionName)
    {
    }

private:
    using NodePtr = omni::core::ObjectPtr<Node>;
    std::vector<NodePtr> m_all; //!< Holds all runners used in the current topology.
};

Next Steps

Readers are encouraged to examine kit/source/extensions/omni.graph.exec/tests.cpp/graphs/TestBehaviorTree.cpp to see the full implementation of behavior trees using EF.

Now that you saw how to create definitions, make sure to consult the Pass Creation guide. If you haven’t yet created a module for extending EF, consult the Plugin Creation guide.