|
| 1 | +# Ceres Custom Nodes |
| 2 | + |
| 3 | +Use this reference when writing custom Flow nodes, generic nodes, dynamic port nodes, or port behavior. |
| 4 | + |
| 5 | +## Choose A Base Class |
| 6 | + |
| 7 | +- Use `FlowNode` for simple synchronous nodes. Override `LocalExecute(ExecutionContext)`. Flow continues through the default `exec` port. |
| 8 | +- Use `ForwardNode` when the node is in a forward chain but needs async work or custom continuation. |
| 9 | +- Use `ExecutableNode` when the node needs full execution control or should run only in dependency path. |
| 10 | + |
| 11 | +Minimal synchronous node: |
| 12 | + |
| 13 | +```csharp |
| 14 | +using System; |
| 15 | +using Ceres.Annotations; |
| 16 | +using Ceres.Graph; |
| 17 | +using Ceres.Graph.Flow; |
| 18 | + |
| 19 | +[Serializable] |
| 20 | +[CeresGroup("Gameplay")] |
| 21 | +[CeresLabel("Add Score")] |
| 22 | +public class FlowNode_AddScore : FlowNode |
| 23 | +{ |
| 24 | + [InputPort, CeresLabel("Amount")] |
| 25 | + public CeresPort<int> amount = new(1); |
| 26 | + |
| 27 | + protected override void LocalExecute(ExecutionContext executionContext) |
| 28 | + { |
| 29 | + // Apply score. |
| 30 | + } |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +Async/custom continuation node: |
| 35 | + |
| 36 | +```csharp |
| 37 | +using System; |
| 38 | +using Ceres.Annotations; |
| 39 | +using Ceres.Graph; |
| 40 | +using Ceres.Graph.Flow; |
| 41 | +using Cysharp.Threading.Tasks; |
| 42 | + |
| 43 | +[Serializable] |
| 44 | +[CeresGroup("Utilities")] |
| 45 | +[CeresLabel("Delay")] |
| 46 | +public class FlowNode_Delay : ForwardNode |
| 47 | +{ |
| 48 | + [InputPort] |
| 49 | + public CeresPort<float> seconds = new(1f); |
| 50 | + |
| 51 | + [OutputPort(false), CeresLabel("")] |
| 52 | + public NodePort exec; |
| 53 | + |
| 54 | + protected override async UniTask Execute(ExecutionContext executionContext) |
| 55 | + { |
| 56 | + await UniTask.Delay(TimeSpan.FromSeconds(seconds.Value), |
| 57 | + cancellationToken: executionContext.Context.GetCancellationTokenOnDestroy()); |
| 58 | + executionContext.SetNext(exec.GetT<ExecutableNode>()); |
| 59 | + } |
| 60 | +} |
| 61 | +``` |
| 62 | + |
| 63 | +## Ports |
| 64 | + |
| 65 | +- Use `[InputPort] public CeresPort<T> name` for data inputs. |
| 66 | +- Use `[OutputPort] public CeresPort<T> name` for data outputs. |
| 67 | +- Use `[OutputPort(false)] public NodePort exec` for execution outputs. |
| 68 | +- Use `NodePort.GetT<ExecutableNode>()` to resolve the connected next node at runtime. |
| 69 | +- Give default values by constructing ports, such as `new CeresPort<float>(1f)`. |
| 70 | +- Use `[HideInGraphEditor]` for internal fields or connection-only ports. |
| 71 | +- Use `[CeresLabel]` to make port and node labels readable. |
| 72 | +- Use `[NodeInfo]` for tooltips. |
| 73 | + |
| 74 | +## Execution Path Metadata |
| 75 | + |
| 76 | +Use dependency path only for nodes that produce values without relying on forward order. |
| 77 | + |
| 78 | +```csharp |
| 79 | +[CeresMetadata("style = ConstNode", "path = Dependency")] |
| 80 | +public class FlowNode_ReadValue : ExecutableNode |
| 81 | +{ |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +Forward path is the default and should be used for side effects, control flow, async work, and Unity object mutation. |
| 86 | + |
| 87 | +## Port Arrays |
| 88 | + |
| 89 | +Use `IReadOnlyPortArrayNode` for fixed dynamic arrays and `IPortArrayNode` when the editor can resize them. |
| 90 | +Only one port array is supported per node type. |
| 91 | + |
| 92 | +```csharp |
| 93 | +using System; |
| 94 | +using Ceres.Annotations; |
| 95 | +using Ceres.Graph; |
| 96 | +using Ceres.Graph.Flow; |
| 97 | +using Cysharp.Threading.Tasks; |
| 98 | +using UnityEngine; |
| 99 | + |
| 100 | +[Serializable] |
| 101 | +[CeresGroup("Flow Control")] |
| 102 | +[CeresLabel("Sequence")] |
| 103 | +[CeresMetadata("style = ForwardNode")] |
| 104 | +public class FlowNode_SequenceLike : ForwardNode, ISerializationCallbackReceiver, IPortArrayNode |
| 105 | +{ |
| 106 | + [OutputPort(false), CeresLabel("Then"), CeresMetadata("DefaultLength = 2")] |
| 107 | + public NodePort[] outputs; |
| 108 | + |
| 109 | + [HideInGraphEditor] |
| 110 | + public int outputCount; |
| 111 | + |
| 112 | + protected override async UniTask Execute(ExecutionContext executionContext) |
| 113 | + { |
| 114 | + foreach (var output in outputs) |
| 115 | + { |
| 116 | + var next = output.GetT<ExecutableNode>(); |
| 117 | + if (next != null) |
| 118 | + await executionContext.Forward(next); |
| 119 | + } |
| 120 | + } |
| 121 | + |
| 122 | + public void OnBeforeSerialize() { } |
| 123 | + |
| 124 | + public void OnAfterDeserialize() |
| 125 | + { |
| 126 | + outputs = new NodePort[outputCount]; |
| 127 | + for (var i = 0; i < outputCount; i++) |
| 128 | + outputs[i] = new NodePort(); |
| 129 | + } |
| 130 | + |
| 131 | + public int GetPortArrayLength() => outputCount; |
| 132 | + public string GetPortArrayFieldName() => nameof(outputs); |
| 133 | + public void SetPortArrayLength(int newLength) => outputCount = newLength; |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +## Generic Nodes |
| 138 | + |
| 139 | +Generic nodes need two classes: |
| 140 | + |
| 141 | +- Runtime node: `FlowNode_NameT<T...>` |
| 142 | +- Template class: `FlowNode_NameT_Template : GenericNodeTemplate` |
| 143 | + |
| 144 | +The template name must match `{node type name without arity}_Template`. |
| 145 | + |
| 146 | +```csharp |
| 147 | +using System; |
| 148 | +using System.Linq; |
| 149 | +using Ceres.Annotations; |
| 150 | +using Ceres.Graph; |
| 151 | +using Ceres.Graph.Flow; |
| 152 | +using Ceres.Utilities; |
| 153 | +using Cysharp.Threading.Tasks; |
| 154 | + |
| 155 | +[Serializable] |
| 156 | +[CeresGroup("Utilities")] |
| 157 | +[CeresLabel("Cast to {0}")] |
| 158 | +public class FlowNode_CastLikeT<TFrom, TTo> : ForwardNode where TTo : TFrom |
| 159 | +{ |
| 160 | + [InputPort, HideInGraphEditor] |
| 161 | + public CeresPort<TFrom> source; |
| 162 | + |
| 163 | + [OutputPort] |
| 164 | + public CeresPort<TTo> result; |
| 165 | + |
| 166 | + [OutputPort(false), CeresLabel("")] |
| 167 | + public NodePort exec; |
| 168 | + |
| 169 | + protected override UniTask Execute(ExecutionContext executionContext) |
| 170 | + { |
| 171 | + result.Value = (TTo)source.Value; |
| 172 | + executionContext.SetNext(exec.GetT<ExecutableNode>()); |
| 173 | + return UniTask.CompletedTask; |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +public class FlowNode_CastLikeT_Template : GenericNodeTemplate |
| 178 | +{ |
| 179 | + public override bool RequirePort() => true; |
| 180 | + |
| 181 | + public override Type[] GetGenericArguments(Type portValueType, Type selectArgumentType) |
| 182 | + { |
| 183 | + return new[] { portValueType, selectArgumentType }; |
| 184 | + } |
| 185 | + |
| 186 | + public override Type[] GetAvailableArguments(Type portValueType) |
| 187 | + { |
| 188 | + return CeresPort.GetAssignedPortValueTypes() |
| 189 | + .Where(type => type.IsAssignableTo(portValueType) && type != portValueType) |
| 190 | + .ToArray(); |
| 191 | + } |
| 192 | +} |
| 193 | +``` |
| 194 | + |
| 195 | +Template guidance: |
| 196 | + |
| 197 | +- Return `true` from `RequirePort()` when type arguments depend on a dragged port. |
| 198 | +- Return selectable types from `GetAvailableArguments`. |
| 199 | +- Return generic arguments in the same order as the node type parameters. |
| 200 | +- Cache expensive type lists in static fields. |
| 201 | + |
| 202 | +## Metadata And Search |
| 203 | + |
| 204 | +- `[CeresGroup("Group/Subgroup")]` controls search grouping. |
| 205 | +- `[CeresLabel("Display")]` controls node, function, and port display text. |
| 206 | +- `[CeresMetadata("style = ConstNode")]` controls editor style classes. |
| 207 | +- `[RequirePort(typeof(SomeType))]` hides a node unless it is created from a compatible dragged port. |
| 208 | +- `[HideInGraphEditor]` hides serialized fields from property UI. |
| 209 | + |
| 210 | +## Useful Package Files |
| 211 | + |
| 212 | +- `Documentation~/docs/flow_custom_node.md` |
| 213 | +- `Documentation~/docs/flow_generic_node.md` |
| 214 | +- `Documentation~/docs/flow_advanced.md` |
| 215 | +- `Runtime/Core/Models/Graph/Nodes/CeresNode.cs` |
| 216 | +- `Runtime/Core/Models/Graph/Nodes/GenericNodeTemplate.cs` |
| 217 | +- `Runtime/Core/Models/Graph/Nodes/PortArrayNodeReflection.cs` |
| 218 | +- `Runtime/Core/Models/Graph/Ports/CeresPort.cs` |
| 219 | +- `Runtime/Flow/Models/Nodes/Core/ExecutableNode.cs` |
0 commit comments