How to Create and Use Generic Nodes in Ceres Flow Visual Scripting
To create generic nodes in Ceres Flow, define a generic class inheriting from ForwardNode or FlowNode with type parameters and attributes, then implement a GenericNodeTemplate subclass in the Editor folder to resolve type arguments and populate the type selector.
Ceres Flow, the visual scripting framework in the akikurisu/ceres repository, provides a type-safe generic node system that lets you write node logic once and reuse it across multiple data types. Generic nodes eliminate code duplication by using C# type parameters while the editor automatically generates concrete instances at design-time based on your type selections.
Defining the Generic Node Class
Generic node classes inherit from ForwardNode (for synchronous execution) or FlowNode and declare one or more type parameters. You define ports using CeresPort<T> and attributes that control the node's appearance in the graph editor.
The following example from Runtime/Flow/Models/Nodes/Utilities/FlowNode_CastT.cs demonstrates a two-parameter generic node that casts between compatible types:
using System;
using Ceres.Annotations;
using Cysharp.Threading.Tasks;
using Ceres.Graph.Flow;
namespace Ceres.Graph.Flow.Utilities
{
[Serializable]
[CeresGroup("Utilities")]
[CeresLabel("Cast to {0}")]
[CeresMetadata("style = ConstNode")]
public class FlowNode_CastT<TFrom, TTo> : ForwardNode where TTo : TFrom
{
[OutputPort(false), CeresLabel("")] public NodePort exec;
[InputPort, HideInGraphEditor, CeresLabel("Source")] public CeresPort<TFrom> sourceValue;
[OutputPort(false), CeresLabel("Cast Failed")] public NodePort castFailed;
[OutputPort, CeresLabel("Result")] public CeresPort<TTo> resultValue;
protected sealed override UniTask Execute(ExecutionContext ctx)
{
try {
resultValue.Value = (TTo)sourceValue.Value;
ctx.SetNext(exec.GetT<ExecutableNode>());
} catch (InvalidCastException) {
ctx.SetNext(castFailed.GetT<ExecutableNode>());
}
return UniTask.CompletedTask;
}
}
}
Key implementation details:
- Type constraints: The
where TTo : TFromclause ensures compile-time safety for the cast operation. - Hidden input ports: The
[HideInGraphEditor]attribute onsourceValueforces type inference from a connected port rather than manual entry. - Dynamic labeling: The
[CeresLabel("Cast to {0}")]attribute includes a{0}placeholder that the editor replaces with the concrete type name. - Port definitions: Use
[InputPort]and[OutputPort]attributes withCeresPort<T>for typed data flow, andNodePortfor execution flow.
Implementing the Generic Node Template
The generic node template tells the Flow editor how to resolve type arguments and which types to display in the selection dropdown. Templates inherit from GenericNodeTemplate and reside in the Editor/Flow/Templates/ directory.
Here is the template for the Cast node from Editor/Flow/Templates/FlowNode_CastT_Template.cs:
using System;
using System.Linq;
using Ceres.Annotations;
using Ceres.Graph;
using Ceres.Utilities;
namespace Ceres.Editor.Graph.Flow
{
internal class FlowNode_CastT_Template : GenericNodeTemplate
{
public override bool RequirePort() => true;
public override Type[] GetGenericArguments(Type portValueType, Type selectArgumentType) =>
new[] { portValueType, selectArgumentType };
public override Type[] GetAvailableArguments(Type portValueType) =>
CeresPort.GetAssignedPortValueTypes()
.Concat(ExecutableFunctionRegistry.Get().GetManagedTypes())
.Distinct()
.Where(t => t.IsAssignableTo(portValueType) && t != portValueType)
.ToArray();
protected override string GetGenericNodeBaseName(string label, Type[] argumentTypes) =>
string.Format(label, CeresLabel.GetTypeName(argumentTypes[1]));
}
}
Critical template methods:
RequirePort(): Returnstruewhen the template needs a connected port to infer the first type parameter (TFrom). Returnfalsefor nodes likeGetComponentwhere the user selects the type manually.GetGenericArguments: Constructs the type array that instantiates the generic class. For the Cast node, it returns[portValueType, selectArgumentType].GetAvailableArguments: Returns the list of types displayed in the editor's dropdown. The example filters for types assignable toTFrombut excluding the source type itself.GetGenericNodeBaseName: Formats the final node label by substituting placeholders in the[CeresLabel]with actual type names.
Adding Generic Nodes to a Flow Graph
Once you have created the node class and template, you can use them in the visual editor:
- Open the Flow editor through Window → Ceres Flow.
- Locate your node in the palette (grouped by the
[CeresGroup]attribute). - Drag the node onto the canvas.
- Connect a port to the hidden input (if
RequirePort()returnstrue) to establishTFrom. - Click the Select Type dropdown in the node inspector to choose the concrete type for
TTo. - The node's label updates automatically (e.g., "Cast to Vector2") and the concrete node is compiled for execution.
Practical Examples of Generic Nodes
Beyond the Cast node, Ceres Flow includes several built-in generic nodes demonstrating different parameter patterns.
GetComponent Node
The FlowNode_GetComponentT<T> node retrieves a component from a GameObject using a single type parameter. Located in Runtime/Flow/Models/Nodes/GameObject/FlowNode_GetComponentT.cs, it uses the constraint where T : Component. Its template (FlowNode_GetComponentT_Template.cs) sets RequirePort() to false and returns all non-abstract Component types from GetAvailableArguments, allowing users to select any component type without needing a source port connection.
FindObjectOfType Node
The FlowNode_FindObjectOfTypeT<T> node (in Runtime/Flow/Models/Nodes/Utilities/FlowNode_FindObjectOfTypeT.cs) searches the scene for a specific Unity Object type. With the constraint where T : Object, it uses a template that provides every concrete Unity Object type in the dropdown. The node includes a "Not Found" output port for handling missing objects safely.
Best Practices for Generic Node Development
- Apply type constraints: Use
whereclauses to restrictTto valid types (e.g.,ComponentorTFrom), which narrows the editor dropdown and prevents runtime errors. - Hide inferred ports: Apply
[HideInGraphEditor]to ports where the type should be determined by a connection, preventing manual type mismatches. - Cache type lookups: In
GetAvailableArguments(), cache expensive reflection calls (likeCeresPort.GetAssignedPortValueTypes()) in static fields to improve editor performance. - Use descriptive labels: Include
{0},{1}placeholders in[CeresLabel]attributes so instantiated nodes display their concrete types directly on the canvas. - Handle null cases: Always provide output ports for failure states (like "Cast Failed" or "Not Found") to prevent exceptions when objects are missing.
Summary
- Generic nodes consist of a runtime class (inheriting
ForwardNodeorFlowNode) and an editor template (inheritingGenericNodeTemplate). - Place node classes in
Runtime/Flow/Models/Nodes/and templates inEditor/Flow/Templates/. - Implement
RequirePort(),GetGenericArguments(),GetAvailableArguments(), andGetGenericNodeBaseName()in your template to control type resolution. - Use
[HideInGraphEditor]on ports that should infer types from connections, and[CeresLabel]with placeholders for dynamic naming. - Reference the Cast, GetComponent, and FindObjectOfType implementations as canonical patterns for multi-parameter and single-parameter generic nodes.
Frequently Asked Questions
What is the purpose of a generic node template in Ceres Flow?
The generic node template bridges the visual editor and the runtime generic class. It tells the editor which types to display in selection dropdowns, how to resolve type parameters from port connections or user input, and how to format the node's display name. Without a template, the editor cannot instantiate concrete versions of generic nodes.
How does Ceres Flow infer generic type arguments from ports?
When RequirePort() returns true in the template, the editor reads the type of the connected port and passes it as the portValueType argument to GetGenericArguments(). The method then constructs the type array (e.g., [TFrom, TTo]) used to instantiate the generic class. The [HideInGraphEditor] attribute on the corresponding CeresPort<T> field prevents manual type entry, ensuring the connection drives the type inference.
Can I create generic nodes with multiple type parameters?
Yes, Ceres Flow supports multiple type parameters as demonstrated by FlowNode_CastT<TFrom, TTo>. In the template's GetGenericArguments method, return a Type[] array where the order matches the generic class declaration. Use argumentTypes[0], argumentTypes[1], etc., in GetGenericNodeBaseName to reference specific type names for labeling.
Where should generic node files be located in the project structure?
Place the runtime node class (the generic logic) in a subdirectory of Runtime/Flow/Models/Nodes/ following the existing organization (e.g., Utilities/ or GameObject/). Place the editor template in Editor/Flow/Templates/. This separation ensures that editor-only code does not compile into builds while maintaining the correct assembly references between runtime and editor components.
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 →