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.
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.
//
// ┌─────────────────┐
// │ │
// │ 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.
// ┌────────────┐
// │ │
// ┌────►│ 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.