
Creating a custom node in C++
Back in Chapter 1, Introduction and Setup, we took a first look at compiling and running the GameDLL, packaged here as MiniMonoGameSample.sln
for Visual Studio. Let's load that up again, making sure that any CryENGINE instances such as the Launcher or Sandbox are closed, as we're going to overwrite the CryGame.dll
file that's used at runtime.
Organizing nodes
The standard practice for CryENGINE games is to have a filter in the GameDLL project, CryGame, called Nodes. If this doesn't exist, go ahead and create it now.

Creating a new node file
Nodes are never referenced in other areas of the project, so it's fine to simply implement a node as a single .cpp
file without a header.
In our case, let's just add a new file, TutorialNode.cpp
, and create the basic structure:
#include "stdafx.h" #include "Nodes/G2FlowBaseNode.h" class CTutorialNode : public CFlowBaseNode<eNCT_Instanced> { }; REGISTER_FLOW_NODE("Tutorial:Multiplier", CTutorialNode);
Firstly, we included stdafx.h
; this provides common functionality and some standardized "includes" for your file. This is also required to compile files.
After that, we included a second file, Nodes/G2FlowBaseNode.h
. While it's not strictly a CryENGINE component, this file is widely used in CryENGINE games to encapsulate node functionality into an easily accessible base class.
We then create our actual class definition. We inherit from the aforementioned base node, and then specify that our node is an instanced node; generally speaking, you'll work with instanced nodes in CryENGINE.
In order to make node registration easier, CryENGINE exposes REGISTER_FLOW_NODE
pre-processor macro. This system will automatically handle registration of your node during startup.
The node functions overview
For the purposes of the node we are creating, we don't need to store any private information, so simply make all node information public using the C++ modifier as the first line inside your class:
public:
We then start off by implementing two functions, the constructor and the Clone
method. We don't need any logic in either of these, so the implementations are very simple; the constructor doesn't initialize anything, and Clone
simply returns a new instance of the current node:
CTutorialNode(SActivationInfo *pActInfo) { } virtual IFlowNodePtr Clone(SActivationInfo *pActInfo) { return new CTutorialNode(pActInfo); }
Here, we're also introduced to SActivationInfo
for the first time. This struct contains information about the node's present state, as well as the graph it's contained within, and we'll be using this elsewhere later.
Now, three more functions are required for our node to at least compile:
virtual void ProcessEvent(EFlowEvent evt, SActivationInfo *pActInfo) { } virtual void GetConfiguration(SFlowNodeConfig &config) { } virtual void GetMemoryUsage(ICrySizer *s) const { s->Add(*this); }
ProcessEvent
is where we'll be doing most of our node logic; this function is called when interesting things happen to our node, such as ports being triggered. GetConfiguration
controls how the node will be displayed, as well as what input and output ports it contains. GetMemoryUsage
doesn't need any extra implementation from us, so we can just add a reference to this node for memory usage tracking.
Now, it would be a good point to verify that your code compiles; if not, check whether you've declared all the function signatures correctly, and included the headers.
Implementing GetConfiguration
As mentioned earlier, GetConfiguration
is where we set up how our node can be used in the Flowgraph Editor. Firstly, let's set up enum
to describe our input ports; we're going to use two values, left and right, as well as an activation port to trigger the calculation. Declare this inside the class:
enum EInput { EIP_Activate, EIP_Left, EIP_Right };
Of course, we also need an output port for the calculation, so let's create enum
with a single value for that also. It's not required, but it's a good practice to be consistent, and most nodes will have more than one output:
enum EOutput { EOP_Result };
With those declared, we can start building up our node. Ports are defined as entries in a constant static array declared in GetConfiguration
, and are constructed using some helper functions, namely InputPortConfig<T>
for a specific type of value, as well as InputPortConfig_AnyType
for allowing all values, and InputPortConfig_Void
for ports that use no data.
With that in mind, we know that a void input will be required for our trigger input on top of two float templated ports. We'll also need a float output.
virtual void GetConfiguration(SFlowNodeConfig &config) { static const SInputPortConfig inputs[] = { InputPortConfig_Void("Activate", "Triggers the calculation"), InputPortConfig<float>("Left", 0, "The left side of the calculation"), InputPortConfig<float>("Right", 0, "The right side of the calculation"), {0} }; }
As you can see, we get to specify the name of the port, the description, as well as a default value for ports that use data. They should match the order of the enums that we declared earlier.
Now we repeat that process, except we use the output set of functions:
static const SOutputPortConfig outputs[] = { OutputPortConfig<float>("Result", "The result of the calculation"), {0} };
Following the process of creating our ports, we need to assign these arrays to our config
parameter, as well as provide a description and category:
config.pInputPorts = inputs; config.pOutputPorts = outputs; config.sDescription = _HELP("Multiplies two numbers"); config.SetCategory(EFLN_APPROVED);
If you compile the code now, the node should be fully visible in the Editor. But as you'll see, it does nothing yet; to fix that, we have to implement ProcessEvent
!
The SFlowNodeConfig
struct allows you to assign optional flags to the flownode, listed as shown:
EFLN_TARGET_ENTITY
: This is used to indicate that this node should support a target entity. To obtain the currently assigned target entity, have a look atSActivationInfo::pEntity
.EFLN_HIDE_UI
: This hides the node from the user in the flowgraph UI.EFLN_UNREMOVEABLE
: This disables the ability for the user to remove the node.
To append a flag within GetConfiguration
, in this case to support a target entity, simply add the flag to the nFlags
variable:
config.nFlags |= EFLN_TARGET_ENTITY;
Implementing ProcessEvent
ProcessEvent
is where we catch all the interesting events for our node, such as ports being triggered. In our case, we want to perform a calculation whenever our Activate
port is triggered, so we need to check for port activations. First though, we can save ourselves some processing by checking which event we'd like to handle.
virtual void ProcessEvent(EFlowEvent evt, SActivationInfo *pActInfo) { switch (evt) { case eFE_Activate: { } break; } }
Usually you'll be handling more than one event, so it's good to get into the habit of using a switch
statement here.
Inside that, let's take a look at the various flownode functions we use to check for activations, to retrieve data, and then trigger an output:
if (IsPortActive(pActInfo, EIP_Activate)) { float left = GetPortFloat(pActInfo, EIP_Left); float right = GetPortFloat(pActInfo, EIP_Right); float answer = left * right; ActivateOutput(pActInfo, EOP_Result, answer); }
To summarize, we use our activation information in all these functions to represent the current state. We can then retrieve values using the GetPort*
functions for the various port types, and then trigger an output with data.
It's time to load up the Editor and test; if all's gone well, you should be able to see your node in the Tutorial category. Congratulations, you've just written your first C++ code for CryENGINE!
