⚠️ This is not a comprehensive Visual Script solution.
A library to help you customize your own Unity3D Graph solution.
- Editor-only, you have to implement your own runtime graph if needed.
- Lack of serializer and deserializer.
- Not easy to start, have to set up bunch of stuff before actually write your own code.
- Separate runtime and editor graph.
- Easy to scratch a new type of node from
INode
- Customize node by each properties via
INodeProperty
- Customize graph menu by extend
IMenuEntry
- Have
Memory
,ScriptableObject
andPrefab
back-end to store graph data by default.
Step-by-step tutorial to build a following binary expression tree:
- Define the interface of expression node:
public interface IExpressionNode : INode<GraphRuntime<IExpressionNode>>
{
float GetValue([NotNull] GraphRuntime<IExpressionNode> graph);
}
- Define
ValueNode
with a single-input-port and a value property.
[Serializable]
public class ValueNode : IExpressionNode
{
// define a single-float-input port
[NodePort] private static float _input;
// define a node property of `_value:SerializedProperty`
[NodeProperty(InputPort = nameof(_input))] public float Value;
public float GetValue(GraphRuntime<IExpressionNode> _)
{
return Value;
}
public bool IsPortCompatible(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) => true;
public void OnConnected(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) {}
public void OnDisconnected(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) {}
}
- Define
AddNode
with a single-input-port and a multi-output-port.GetValue
will sum the value of output-connected nodes together.
[Serializable]
public class AddNode : IExpressionNode
{
// define a single-float-input port
[NodePort(Direction = PortDirection.Input, HideLabel = true)] private static float _input;
// define a multi-float-output port with at most 2 connections
[NodePort(Direction = PortDirection.Output, Capacity = 2, HideLabel = true)] private static float _output;
public float GetValue(GraphRuntime<IExpressionNode> graph)
{
var thisNodeId = graph[this];
var inputPortId = new PortId(thisNodeId, nameof(_input));
var connectedNodeIds = graph.FindConnectedNodes(inputPortId);
var connectedNodes = connectedNodeIds.Select(nodeId => graph[nodeId]);
return connectedNodes.Sum(node => node.GetValue(graph));
}
public bool IsPortCompatible(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) => true;
public void OnConnected(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) {}
public void OnDisconnected(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) {}
}
// ExpressionNodeScriptableObject.cs
public class ExpressionNodeScriptableObject : NodeScriptableObject<IExpressionNode> {}
// ExpressionGraphScriptableObject.cs
[CreateAssetMenu(menuName = "Expression Graph", fileName = "Graph/New Expression Graph", order = 0)]
public class ExpressionGraphScriptableObject : GraphScriptableObject<IExpressionNode, ExpressionNodeScriptableObject> {}
3. Define graph menu entries and window extension to use ExpressionGraphScriptableObject
as backend:
public class ExpressionNodeBasicGraphInstaller : BasicGraphInstaller<IExpressionNode> {}
public class ExpressionNodeGraphWindowExtension : ScriptableObjectNodeCreationMenuEntry<IExpressionNode, ExpressionNodeScriptableObject> {}
public class ExpressionNodeInstaller : ScriptableObjectWindowExtension<IExpressionNode, ExpressionNodeScriptableObject> {}
public class ExpressionNodeMenuEntry : NodeMenuEntry<IExpressionNode>
{
public VisualNodeMenuEntry(GraphRuntime<IExpressionNode> graphRuntime, InitializeNodePosition initializeNodePosition) : base(graphRuntime, initializeNodePosition)
{
}
}

public class AddNode : IExpressionNode
{
// define a property to hold input and output port
[NodeProperty(HideValue = true, InputPort = nameof(_input), OutputPort = nameof(_output))] private static int add;
...
- make a base expression node:
public abstract class ExpressionNode : IExpressionNode
{
[NodePort(Direction = PortDirection.Input, Capacity = 1, Hide = true)] protected static float _input;
[NodePort(Direction = PortDirection.Output, Capacity = 2, Hide = true)] protected static float _output;
public bool IsPortCompatible(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) => true;
public void OnConnected(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) {}
public void OnDisconnected(GraphRuntime<IExpressionNode> graph, in PortId input, in PortId output) {}
public abstract float GetValue(GraphRuntime<IExpressionNode> graph);
protected IEnumerable<IExpressionNode> GetConnectedNodes(GraphRuntime<IExpressionNode> graph)
{
var thisNodeId = graph[this];
var inputPortId = new PortId(thisNodeId, nameof(_input));
var connectedNodeIds = graph.FindConnectedNodes(inputPortId);
var connectedNodes = connectedNodeIds.Select(nodeId => graph[nodeId]);
return connectedNodes;
}
}
[Serializable]
public class ValueNode : ExpressionNode
{
[NodeProperty(InputPort = nameof(_input))] public float Value;
public override float GetValue(GraphRuntime<IExpressionNode> _) => Value;
}
[Serializable]
public class AddNode : ExpressionNode
{
[NodeProperty(HideValue = true, InputPort = nameof(_input), OutputPort = nameof(_output))] private static int add;
public override float GetValue(GraphRuntime<IExpressionNode> graph) => GetConnectedNodes(graph).Sum(node => node.GetValue(graph));
}
-
Use prefab (and GameObject tree) as backend:
- Use
ITreeNode<>
interface instead ofINode<>
public interface IExpressionNode : ITreeNode<GraphRuntime<IExpressionNode>> { float GetValue([NotNull] GraphRuntime<IExpressionNode> graph); } public abstract class ExpressionNode : IExpressionNode { public string InputPortName => nameof(_input); public string OutputPortName => nameof(_output); ...
- Define tree component:
// ExpressionTreeNodeComponent.cs public class ExpressionTreeNodeComponent : TreeNodeComponent<IExpressionNode, ExpressionTreeNodeComponent> {}
- Define window extension and installers of prefab:
public class ExpressionPrefabGraphWindowExtension : PrefabGraphWindowExtension<IExpressionNode, ExpressionTreeNodeComponent> {} public class ExpressionPrefabInstaller : SerializableGraphBackendInstaller<IExpressionNode, ExpressionTreeNodeComponent> {} public class ExpressionPrefabIsPortCompatibleInstaller : PrefabIsPortCompatibleInstaller<IExpressionNode, ExpressionTreeNodeComponent> {}
- Create window config:
- Open a prefab and start to modify:
- Use
The way to store (serialize) graph data, different backend provide different features by default.
Feature | Memory | Scriptable Object | Prefab |
---|---|---|---|
Focus on Selection.objects | no | yes | yes |
Inspect chosen node | no | yes | yes |
Tree structure | no | no | optional |
Copy/Paste | no | no | only in hierarchy |
- Implement a class of
IMenuEntry
- Add it into Menu Entries of
GraphConfig.WindowExtension
- Implement a property factory of
INodePropertyFactory
public class FloatFieldViewFactory : INodePropertyViewFactory
{
public VisualElement Create(Node node, INodeProperty property, INodePropertyViewFactory factory)
{
return property is FieldInfoProperty<float> _ ? new Label("replace view") : null;
}
}
- Add it into the top of Factories of Node Property View Factory
- Create a node and will change its property view.