View Categories

World Builder – Guide to Implementing Custom Nodes

7 min read

Introduction #

Custom nodes allow you to extend the functionality of our node editor system with your own specialized logic. This guide walks you through the process of creating and implementing custom nodes that integrate seamlessly with the existing system.

Prerequisites #

  • Basic understanding of C# and Unity
  • Access to the node system codebase
  • Visual Studio or another C# IDE

The Anatomy of a Custom Node #

Every custom node in our system follows a consistent structure:

namespace MyNodes // Your namespace here
{
    public class MyCustomNode : NodeBase
    {
        // 1. Unique Identifier (required)
        public static string NODE_GUID => "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";

        // 2. Display Name (required)
        public static string NODE_CLASS_NAME => "My Custom Node";

        // 3. GUID and Name Implementation
        public override Guid NodeClassGuid => Guid.Parse(NODE_GUID);
        public override string NodeClassName => NODE_CLASS_NAME;

        // 4. Input/Output Type Definitions
        public static List<NodeFactory.VarType> InputTypes => m_InputTypes;
        public static List<NodeFactory.VarType> OutputTypes => m_OutputTypes;

        private static List<NodeFactory.VarType> m_InputTypes = 
            new List<NodeFactory.VarType> { /* Input types go here */ };

        private static List<NodeFactory.VarType> m_OutputTypes = 
            new List<NodeFactory.VarType> { /* Output types go here */ };

        // 5. Input/Output References
        private InputBase m_SomeInput;
        private FloatOutput m_SomeOutput;

        // 6. Constructors (both required)
        public MyCustomNode(NodeTree nodeTree, Guid nodeInstanceGuid) :
            base(nodeTree, nodeInstanceGuid, null, m_InputTypes, m_OutputTypes)
        {
            // Constructor initialization
        }

        public MyCustomNode(NodeTree nodeTree, NodeFile nodeFile, NodeFileEntry nodeFileEntry) :
            base(nodeTree, nodeFileEntry.Identity, nodeFileEntry, m_InputTypes, m_OutputTypes)
        {
            // Load saved data if needed
        }

        // 7. I/O Setup Method
        protected override void MakeIO(NodeFileEntry nodeFileEntry)
        {
            // Configure inputs and outputs
        }

        // 8. Node Functionality Methods
        // Your custom logic here
    }
}

Step-by-Step Implementation Guide #

Step 1: Create a New Class File #

Create a new C# script file with your node name (e.g., MyCustomNode.cs).

Step 2: Define the Class Structure #

Your class should inherit from NodeBase and implement INodeBase if needed:

using System;
using System.Collections.Generic;
using WorldBuilder.Nodes;
using WorldBuilder.Nodes.IO;
using Worldbuilder.Nodes.FileIO;

namespace MyNodes
{
    public class MyCustomNode : NodeBase
    {
        // Implementation will go here
    }
}

Step 3: Generate a Unique GUID #

Every node needs a unique identifier. Generate a GUID using one of these methods:

  • Option 1: Use Visual Studio’s Tools > Create GUID
  • Option 2: Use an online GUID generator
  • Option 3: Run this C# code: Console.WriteLine(Guid.NewGuid().ToString());

Then add it to your node class:

public static string NODE_GUID => "b4fc3090-faf0-4417-88b9-92c04a986117"; // Your unique GUID
public static string NODE_CLASS_NAME => "My Custom Node";

public override Guid NodeClassGuid => Guid.Parse(NODE_GUID);
public override string NodeClassName => NODE_CLASS_NAME;

IMPORTANT: Never reuse GUIDs between different node types. Each node class must have its own unique GUID.

Step 4: Define Input and Output Types #

Specify what kinds of data your node will accept and produce:

public static List<NodeFactory.VarType> InputTypes => m_InputTypes;
public static List<NodeFactory.VarType> OutputTypes => m_OutputTypes;

private static List<NodeFactory.VarType> m_InputTypes =
    new List<NodeFactory.VarType> { 
        NodeFactory.VarType.Flow,     // Execution flow input
        NodeFactory.VarType.Float     // Float value input
    };

private static List<NodeFactory.VarType> m_OutputTypes =
    new List<NodeFactory.VarType> { 
        NodeFactory.VarType.Flow,     // Execution flow output
        NodeFactory.VarType.String    // String value output
    };

Common variable types:

  • NodeFactory.VarType.Flow – Execution flow
  • NodeFactory.VarType.Float – Floating point number
  • NodeFactory.VarType.Int – Integer
  • NodeFactory.VarType.String – Text string
  • NodeFactory.VarType.Bool – Boolean (true/false)
  • NodeFactory.VarType.GameObject – Unity GameObject reference

Step 5: Create Input/Output References #

Create member variables to store references to your inputs and outputs:

private InputBase m_FlowInput;
private InputBase m_FloatInput;
private FloatOutput m_ResultOutput;

Step 6: Implement Both Constructors #

You need two constructors for your node:

// Constructor for new node instances
public MyCustomNode(NodeTree nodeTree, Guid nodeInstanceGuid) :
    base(nodeTree, nodeInstanceGuid, null, m_InputTypes, m_OutputTypes)
{
    // Any initialization code
}

// Constructor for loading existing nodes
public MyCustomNode(NodeTree nodeTree, NodeFile nodeFile, NodeFileEntry nodeFileEntry) :
    base(nodeTree, nodeFileEntry.Identity, nodeFileEntry, m_InputTypes, m_OutputTypes)
{
    // Load any saved data from nodeFile if needed
}

Step 7: Configure Inputs and Outputs #

Override the MakeIO method to set up your inputs and outputs:

protected override void MakeIO(NodeFileEntry nodeFileEntry)
{
    // Set up flow input (execution)
    m_FlowInput = Inputs[0];
    m_FlowInput.DisplayName = "Execute";
    m_FlowInput.RunAction = () => YourActionMethod();

    // Set up value input
    m_FloatInput = Inputs[1];
    m_FloatInput.DisplayName = "Value";

    // Set up outputs
    OutputBase flowOutput = Outputs[0];
    flowOutput.DisplayName = "Next";

    m_ResultOutput = Outputs[1] as FloatOutput;
    m_ResultOutput.DisplayName = "Result";
    m_ResultOutput.SetGetValueFunc(() => CalculateResult());
}

Step 8: Implement Node Functionality #

Add methods that implement your node’s actual behavior:

private void YourActionMethod()
{
    // Do something when the node is executed
    float value = m_FloatInput.GetFloat();

    // Your logic here

    // Continue execution flow
    Run(); // This triggers the next node in the flow
}

private float CalculateResult()
{
    // Calculate and return a value
    return m_FloatInput.GetFloat() * 2.0f;
}

Example Node Implementation #

Let’s create a simple node that multiplies a float value by 2:

using System;
using System.Collections.Generic;
using WorldBuilder.Nodes;
using WorldBuilder.Nodes.IO;
using Worldbuilder.Nodes.FileIO;

namespace MyNodes
{
    public class MyDoubleValueNode : NodeBase
    {
        public static string NODE_GUID => "2a7b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d";
        public static string NODE_CLASS_NAME => "Double Value";

        public override Guid NodeClassGuid => Guid.Parse(NODE_GUID);
        public override string NodeClassName => NODE_CLASS_NAME;

        public static List<NodeFactory.VarType> InputTypes => m_InputTypes;
        public static List<NodeFactory.VarType> OutputTypes => m_OutputTypes;

        private static List<NodeFactory.VarType> m_InputTypes =
            new List<NodeFactory.VarType> { NodeFactory.VarType.Float };

        private static List<NodeFactory.VarType> m_OutputTypes =
            new List<NodeFactory.VarType> { NodeFactory.VarType.Float };

        private InputBase m_ValueInput;
        private FloatOutput m_ResultOutput;

        public MyDoubleValueNode(NodeTree nodeTree, Guid nodeInstanceGuid) :
            base(nodeTree, nodeInstanceGuid, null, m_InputTypes, m_OutputTypes)
        {
        }

        public MyDoubleValueNode(NodeTree nodeTree, NodeFile nodeFile, NodeFileEntry nodeFileEntry) :
            base(nodeTree, nodeFileEntry.Identity, nodeFileEntry, m_InputTypes, m_OutputTypes)
        {
        }

        protected override void MakeIO(NodeFileEntry nodeFileEntry)
        {
            m_ValueInput = Inputs[0];
            m_ValueInput.DisplayName = "Value";

            m_ResultOutput = Outputs[0] as FloatOutput;
            m_ResultOutput.DisplayName = "Doubled Value";
            m_ResultOutput.SetGetValueFunc(CalculateDouble);
        }

        private float CalculateDouble()
        {
            return m_ValueInput.GetFloat() * 2.0f;
        }
    }
}

Common Node Types and Their Implementation #

Value Processing Node #

Like our example above, focuses on transforming values.

Flow Control Node #

Controls execution flow with multiple output paths:

// In MakeIO method
m_ConditionInput = Inputs[1]; // Boolean input
Outputs[0].DisplayName = "True"; // First flow output
Outputs[1].DisplayName = "False"; // Second flow output

// In RunAction method
if (m_ConditionInput.GetBool())
    RunAt(0); // Run the "True" path
else
    RunAt(1); // Run the "False" path

Event Handling Node #

Responds to system events:

// Subscribe to events in the constructor
EventManager.Instance.OnSomeEvent += HandleEvent;

// Handle the event
private void HandleEvent(EventArgs args)
{
    // Do something and then continue flow
    Run();
}

// Clean up in Dispose method
public override void Dispose()
{
    EventManager.Instance.OnSomeEvent -= HandleEvent;
    base.Dispose();
}

Advanced Topics #

Saving Custom Data #

If your node needs to save additional data:

// Override GetExtraData
public override string GetExtraData()
{
    return JsonConvert.SerializeObject(myCustomData);
}

// Override SetExtraData
public override void SetExtraData(string extraData)
{
    myCustomData = JsonConvert.DeserializeObject<MyCustomDataType>(extraData);
}

Custom Node Editor UI #

For nodes that need custom interface elements:

public override void InsertVisual(NodeTreeVisualizer visualizer, VisualElement root, Vector2 position)
{
    base.InsertVisual(visualizer, root, position);

    // Add your custom UI elements
    IUIElementFactory elementFactory = NodeTree.ReferenceProvider.ElementFactory;
    elementFactory.CreateButton(m_NodeBox, "My Button", OnButtonClick);
}

private void OnButtonClick()
{
    // Handle button click
}

Troubleshooting #

Node Doesn’t Appear in Editor #

  • Ensure your namespace is correct
  • Verify that the GUID is unique and properly formatted
  • Check that your node class is public

Node Inputs/Outputs Don’t Work #

  • Make sure the index in Inputs[x] matches the order in m_InputTypes
  • Verify that you’re using the correct getter method (GetFloat(), GetString(), etc.)
  • Check that you’ve called Run() to continue execution flow

Runtime Errors #

  • Verify all inputs have proper null checks
  • Ensure any saved data is properly deserialized
  • Check for any event subscriptions that might not be unsubscribed

Conclusion #

Following this guide, you should now be able to create custom nodes that extend the functionality of the node system. Remember to always generate unique GUIDs for each node type, and to properly set up inputs and outputs.

Feel free to explore the example nodes provided in the documentation to see more complex implementations and patterns.