Skip to content

quabug/GraphExt

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

⚠️ This is not a comprehensive Visual Script solution.

openupm

GraphExt

A library to help you customize your own Unity3D Graph solution.

Why not use UIElement.Graph directly?

  1. Editor-only, you have to implement your own runtime graph if needed.
  2. Lack of serializer and deserializer.
  3. Not easy to start, have to set up bunch of stuff before actually write your own code.

Features

Architecture

image

Tutorial (Binary Expression Tree)

Step-by-step tutorial to build a following binary expression tree:

image

1. Define runtime nodes:

  1. Define the interface of expression node:
public interface IExpressionNode : INode<GraphRuntime<IExpressionNode>>
{
    float GetValue([NotNull] GraphRuntime<IExpressionNode> graph);
}
  1. 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) {}
}
  1. 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) {}
}

2. Define graph and node ScriptableObject to store graph data.

// 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> {}

image

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)
    {
    }
}

4. Create a new graph window config file and set to use expression extensions:

image image

5. Open expression graph window and choose a "Expression Graph" to modifying:

image

image

6. (Optional) Make it nicer:

  • compact node look of AddNode:image
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:

    1. Use ITreeNode<> interface instead of INode<>
    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);
    ...
    1. Define tree component:
    // ExpressionTreeNodeComponent.cs
    public class ExpressionTreeNodeComponent : TreeNodeComponent<IExpressionNode, ExpressionTreeNodeComponent> {}
    1. 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> {}
    1. Create window config:
    image
    1. Open a prefab and start to modify:

    image

Backend

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

Memory Backend (with JSON serializer)

Expression Tree with Memory Backend

Scriptable Object Backend

Expression Tree with Scriptable Object Backend

Prefab Backend

Expression Tree with Prefab Backend

HowTo

Customize graph menu

  1. Implement a class of IMenuEntry
  2. Add it into Menu Entries of GraphConfig.WindowExtension

image

Customize view of node property

  1. 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;
        }
    }
  1. Add it into the top of Factories of Node Property View Factory

image

  1. Create a node and will change its property view.

image

About

A library to help you customize your own Unity3D Graph solution.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages