Understanding the Relationship Between CeresNode, CeresPort, and CeresGraph in Unity Workflows
The relationship between CeresNode, CeresPort, and CeresGraph follows a container-composite pattern where CeresGraph orchestrates CeresNode instances and manages their CeresPort connections to enable data-flow execution.
The akikurisu/ceres repository provides a node-based graph execution framework for Unity. Understanding the relationship between CeresNode, CeresPort, and CeresGraph is essential for building robust visual workflows, as these three classes form the core architecture that handles initialization, data flow, and runtime execution.
The Three Pillars of Ceres Graph Architecture
CeresGraph – The Workflow Container
Located in Runtime/Core/Models/Graph/CeresGraph.cs, the graph class maintains two critical collections: a List<CeresNode> nodes containing all executable units, and a HashSet<CeresPort> _internalPorts tracking every port instance across the workflow. During initialization, the graph invokes node.InitializeVariables() and node.InitializePorts() on each node, then registers every port via graph.LinkPort() to establish the connection topology.
CeresNode – The Executable Unit
Defined in Runtime/Core/Models/Graph/Nodes/CeresNode.cs, this abstract base class represents individual processing steps. Each node maintains a Dictionary<string, CeresPort> Ports for named data endpoints and a Dictionary<string, IList> PortLists for variable-length port collections. Nodes expose a unique Guid identifier and serialized CeresNodeData. During runtime, the node's ports are created, linked, and used to pass data between nodes.
CeresPort – The Data Conduit
Implemented in Runtime/Core/Models/Graph/Ports/CeresPort.cs, ports are the typed channels that transport values between nodes. The abstract CeresPort base defines GetValueType() for runtime type inspection and Link(CeresPort targetPort) for connection establishment. The generic subclass CeresPort<TValue> stores a defaultValue field and manages the Getter → AdaptedGetter chain for value resolution. When linked, ports register with the parent graph, enabling dependency path calculation and execution order determination.
How CeresNode, CeresPort, and CeresGraph Interact at Runtime
The relationship between these components follows a strict lifecycle during graph compilation. First, CeresGraph.Compile() iterates through the nodes list, triggering InitializeVariables() to set up shared blackboard bindings. Next, InitializePorts() populates the Ports and PortLists dictionaries with concrete CeresPort instances. Finally, the graph calls LinkPort() for every port, adding them to _internalPorts and resolving forward connections to build the dependency-execution order used by the runtime compiler.
Practical Implementation: Creating and Linking Nodes
Instantiating a Graph and Adding Nodes
using Ceres.Graph;
using UnityEngine;
// Create the container
var graph = new CeresGraph();
// Instantiate a concrete node (e.g., a debug print node)
var printNode = new PrintNode(); // derives from CeresNode
graph.nodes.Add(printNode);
// Compile to initialize ports and variables
graph.Compile(new CeresGraphCompiler());
Accessing and Configuring Ports
// Retrieve the port by name from the node's Ports dictionary
CeresPort<string> messagePort = (CeresPort<string>)printNode.Ports["message"];
// Set a default value for unconnected execution
messagePort.defaultValue = "Hello, Ceres Graph!";
Linking Nodes for Data Flow
// Create producer and consumer nodes
var producer = new IntProducerNode(); // exposes "output" port
var consumer = new IntConsumerNode(); // expects "input" port
graph.nodes.AddRange(new CeresNode[] { producer, consumer });
graph.Compile(new CeresGraphCompiler());
// Retrieve ports and establish connection
var outPort = (CeresPort<int>)producer.Ports["output"];
var inPort = (CeresPort<int>)consumer.Ports["input"];
// Link creates the data conduit; graph tracks this in _internalPorts
outPort.Link(inPort);
Referencing Nodes by GUID
// Store a reference using the node's unique identifier
NodeReference nodeRef = new NodeReference(producer.Guid);
// Later, resolve the reference against the graph instance
CeresNode resolvedNode = nodeRef.Get(graph);
Debug.Assert(resolvedNode == producer);
Summary
- CeresGraph serves as the workflow container, managing the
nodeslist and_internalPortscollection while orchestrating initialization viaCompile(). - CeresNode represents individual executable units, exposing
PortsandPortListsdictionaries that define data endpoints for inter-node communication. - CeresPort acts as the typed data conduit, with
CeresPort<TValue>storingdefaultValueand supportingLink()operations that the graph tracks to calculate execution order. - The initialization sequence—
InitializeVariables(),InitializePorts(), thenLinkPort()—establishes the runtime topology that drives the Ceres execution engine.
Frequently Asked Questions
What is the difference between CeresPort and CeresPort?
CeresPort is the abstract base class that defines the common API for all ports, including GetValueType() and Link(). CeresPort<TValue> is the generic concrete implementation that stores a strongly-typed defaultValue field and manages type-specific value resolution through the Getter chain. You interact with the generic version when declaring typed data endpoints in custom nodes.
How does CeresGraph maintain connections between nodes?
The graph maintains a HashSet<CeresPort> _internalPorts that tracks every port instance across all nodes. When Compile() is called, the graph invokes LinkPort() for each port, registering them in this internal set. This registration enables the graph to resolve forward links, calculate dependency paths, and determine the correct execution order for the runtime compiler.
Can I access a CeresNode after compilation without a direct reference?
Yes, every CeresNode exposes a unique Guid identifier that persists across serialization. You can store this identifier in a NodeReference struct and later call NodeReference.Get(graph) to resolve the actual node instance from the graph's nodes list. This is essential for runtime debugging and serialized graph reconstruction.
When should I use PortLists versus individual Ports in CeresNode?
Use the Ports dictionary (Dictionary<string, CeresPort>) for single, named data endpoints like "input" or "output". Use PortLists (Dictionary<string, IList>) when your node needs to accept a variable number of connections, such as an array of inputs or multiple execution flows. The graph initialization process handles both collections during InitializePorts().
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →