├── .gitignore ├── READMEContent └── GraphEditor.png ├── Scripts ├── IGraphInputListener.cs ├── IGraphInputHandler.cs ├── EditorGraphDrawUtils.cs ├── TypeUtilities.cs ├── Node.cs ├── FunctionLibrary.cs ├── EditorLink.cs ├── EditorPin.cs ├── EditorGraph.cs ├── EditorNode.cs └── GraphEditor.cs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.meta -------------------------------------------------------------------------------- /READMEContent/GraphEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oshannonlepper/EditorGraph/HEAD/READMEContent/GraphEditor.png -------------------------------------------------------------------------------- /Scripts/IGraphInputListener.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | public interface IGraphInputListener 6 | { 7 | 8 | void OnMouseDown(int button, Vector2 mousePos); 9 | void OnMouseUp(int button, Vector2 mousePos); 10 | void OnMouseMove(Vector2 mousePos); 11 | 12 | } -------------------------------------------------------------------------------- /Scripts/IGraphInputHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | public interface IGraphInputHandler 6 | { 7 | 8 | void OnGraphLoaded(EditorGraph graph); 9 | 10 | void MoveNode(EditorNode node, Vector2 newPosition); 11 | bool LinkPins(EditorPinIdentifier pinA, EditorPinIdentifier pinB); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /Scripts/EditorGraphDrawUtils.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | public class EditorGraphDrawUtils 5 | { 6 | 7 | public static void DrawRect(Vector2 TopLeft, Vector2 BottomRight, Color Fill) 8 | { 9 | EditorGUI.DrawRect(new Rect(TopLeft, BottomRight - TopLeft), Fill); 10 | } 11 | 12 | public static void Line(Vector2 From, Vector2 To, Color InColour, float Thickness = 5.0f) 13 | { 14 | Handles.BeginGUI(); 15 | Vector3 FromTangent = new Vector3(0.5f * (From.x + To.x), From.y); 16 | Vector3 ToTangent = new Vector3(0.5f * (From.x + To.x), To.y); 17 | Handles.DrawBezier(From, To, FromTangent, ToTangent, InColour, null, Thickness); 18 | Handles.EndGUI(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Scripts/TypeUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | public class TypeUtilities 5 | { 6 | private static System.Reflection.Assembly[] _assemblyList; 7 | 8 | public static void GetAllSubclasses(System.Type _class, List _subclassList) 9 | { 10 | if (_assemblyList == null || _assemblyList.Length == 0) 11 | { 12 | _assemblyList = System.AppDomain.CurrentDomain.GetAssemblies(); 13 | } 14 | 15 | _subclassList.Clear(); 16 | 17 | foreach (var assembly in _assemblyList) 18 | { 19 | System.Type[] types = assembly.GetTypes(); 20 | foreach (var currentType in types) 21 | { 22 | if (currentType.IsSubclassOf(_class)) 23 | { 24 | _subclassList.Add(currentType); 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Scripts/Node.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using System.Reflection; 5 | 6 | // TODO: 7 | // Create editor variant of node, renders GL to the editor window 8 | // can be dragged around in the editor window 9 | // exports its data to Node class 10 | 11 | [System.Serializable] 12 | public class Pin 13 | { 14 | [SerializeField] public string Name; 15 | [SerializeField] public string Type; 16 | [SerializeField] public Pin Next; 17 | } 18 | 19 | [System.Serializable] 20 | public class Node 21 | { 22 | 23 | [SerializeField] public string ClassName { get; set; } 24 | [SerializeField] public string FunctionName { get; set; } 25 | [SerializeField] private Node NextFlowNode; 26 | [SerializeField] private List OutputPins; 27 | 28 | public void Evaluate() 29 | { 30 | 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Scripts/FunctionLibrary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | /** Derive from this when writing functions you wish to include in the GraphEditor */ 6 | public class FunctionLibrary 7 | { 8 | } 9 | 10 | public class MathFunctionLibrary : FunctionLibrary 11 | { 12 | 13 | public static float Add(float A, float B) 14 | { 15 | return A + B; 16 | } 17 | 18 | public static float Subtract(float A, float B) 19 | { 20 | return A - B; 21 | } 22 | 23 | public static float Multiply(float A, float B) 24 | { 25 | return A * B; 26 | } 27 | 28 | public static float Divide(float A, float B) 29 | { 30 | return A / B; 31 | } 32 | 33 | public static float Sin(float Input) 34 | { 35 | return Mathf.Sin(Input); 36 | } 37 | 38 | public static float Cos(float Input) 39 | { 40 | return Mathf.Cos(Input); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EditorGraph # 2 | 3 | ![EditorGraph Screenshot](https://raw.githubusercontent.com/oshannonlepper/EditorGraph/master/READMEContent/GraphEditor.png) 4 | 5 | Experimenting with extending Unity's editor functionality and C# reflection to create a graph editor. 6 | 7 | ## Setup ## 8 | 9 | * Place the folder in your project's Assets directory. 10 | 11 | * _Optional_: Create a folder titled 'Graphs' in the Assets directory as well. 12 | 13 | ## How to Use ## 14 | 15 | ### Creating a Graph ### 16 | 17 | * Right-click the graph editor and select 'New Graph'. 18 | 19 | * The editor will attempt to create a graph asset in your 'Assets/Graphs' directory, or just 'Assets' if you don't have that. 20 | 21 | ### Saving a Graph ### 22 | 23 | * Graphs are autosaved after each change. 24 | 25 | ### Loading a Graph ### 26 | 27 | * Right-click the graph editor and select 'Load Graph'. 28 | 29 | ### Creating a Node ### 30 | 31 | * To create your own nodes, simply define functions in a class that derives off of FunctionLibrary. 32 | 33 | * All function libraries are listed in the context menu, selecting functions from there automatically creates the nodes on your graph. 34 | 35 | * I will be adding support for non-function nodes soon. Because it would be nice to plug values in and actually retrieve results from this. 36 | -------------------------------------------------------------------------------- /Scripts/EditorLink.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | [System.Serializable] 6 | public class EditorLink 7 | { 8 | 9 | [SerializeField] private int FromNodeID; 10 | [SerializeField] private int FromPinID; 11 | [SerializeField] private int ToNodeID; 12 | [SerializeField] private int ToPinID; 13 | 14 | public EditorLink(EditorPinIdentifier LHSPin, EditorPinIdentifier RHSPin) 15 | { 16 | FromNodeID = LHSPin.NodeID; 17 | FromPinID = LHSPin.PinID; 18 | ToNodeID = RHSPin.NodeID; 19 | ToPinID = RHSPin.PinID; 20 | } 21 | 22 | public int NodeID_From 23 | { 24 | get 25 | { 26 | return FromNodeID; 27 | } 28 | } 29 | 30 | public int NodeID_To 31 | { 32 | get 33 | { 34 | return ToNodeID; 35 | } 36 | } 37 | 38 | public int PinID_From 39 | { 40 | get 41 | { 42 | return FromPinID; 43 | } 44 | } 45 | 46 | public int PinID_To 47 | { 48 | get 49 | { 50 | return ToPinID; 51 | } 52 | } 53 | 54 | public override string ToString() 55 | { 56 | return NodeID_From + "." + PinID_From + " to " + NodeID_To + "." + PinID_To; 57 | } 58 | 59 | public void RenderLink(EditorGraph Graph) 60 | { 61 | EditorNode FromNode = Graph.GetNodeFromID(NodeID_From); 62 | EditorNode ToNode = Graph.GetNodeFromID(NodeID_To); 63 | 64 | if (FromNode == null || ToNode == null) 65 | { 66 | return; 67 | } 68 | 69 | Rect FromRect = FromNode.GetPinRect(PinID_From); 70 | Rect ToRect = ToNode.GetPinRect(PinID_To); 71 | 72 | EditorGraphDrawUtils.Line(FromRect.center, ToRect.center, Color.black); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /Scripts/EditorPin.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | public enum EPinLinkType 6 | { 7 | None, 8 | Input, 9 | Output, 10 | } 11 | 12 | [System.Serializable] 13 | public class EditorPinTypeInfo 14 | { 15 | [SerializeField] private string Type; 16 | 17 | public EditorPinTypeInfo(string _Type) 18 | { 19 | Type = _Type; 20 | } 21 | 22 | public string TypeString 23 | { 24 | get 25 | { 26 | return Type; 27 | } 28 | } 29 | } 30 | 31 | [System.Serializable] 32 | public class EditorPin 33 | { 34 | 35 | [SerializeField] private EditorPinTypeInfo TypeInfo = null; 36 | [SerializeField] private string Name; 37 | [SerializeField] private int OwnerID; 38 | [SerializeField] private int ID; 39 | [SerializeField] private EPinLinkType PinLinkType; 40 | 41 | public EditorPin() 42 | { 43 | Name = ""; 44 | OwnerID = -1; 45 | ID = -1; 46 | PinLinkType = EPinLinkType.None; 47 | } 48 | 49 | public EditorPin(string _Type, string _Name, int _OwnerID, int _ID, EPinLinkType _PinLinkType) 50 | { 51 | TypeInfo = new EditorPinTypeInfo(_Type); 52 | Name = _Name; 53 | OwnerID = _OwnerID; 54 | ID = _ID; 55 | PinLinkType = _PinLinkType; 56 | } 57 | 58 | public override string ToString() 59 | { 60 | if (TypeInfo == null) 61 | { 62 | return Name; 63 | } 64 | return "(" + TypeInfo.TypeString + ") " + Name; 65 | } 66 | 67 | public string GetPinName() 68 | { 69 | return Name; 70 | } 71 | 72 | public int GetOwnerID() 73 | { 74 | return OwnerID; 75 | } 76 | 77 | public int GetID() 78 | { 79 | return ID; 80 | } 81 | 82 | public EPinLinkType GetPinLinkType() 83 | { 84 | return PinLinkType; 85 | } 86 | 87 | public System.Type GetPinType() 88 | { 89 | return System.Type.GetType(TypeInfo.TypeString); 90 | } 91 | 92 | public bool CanLinkTo(EditorPin Other) 93 | { 94 | if (OwnerID != Other.OwnerID) 95 | { 96 | if (PinLinkType == EPinLinkType.Input && Other.PinLinkType == EPinLinkType.Output) 97 | { 98 | if (TypeInfo == null && Other.TypeInfo == null) 99 | { 100 | return true; 101 | } 102 | else 103 | { 104 | if (TypeInfo.TypeString.Equals(Other.TypeInfo.TypeString)) 105 | { 106 | return true; 107 | } 108 | else 109 | { 110 | Debug.LogWarning("My type = " + TypeInfo.TypeString + ", their type = " + TypeInfo.TypeString); 111 | } 112 | } 113 | } 114 | else 115 | { 116 | Debug.LogWarning("My Link Type = " + PinLinkType + ", other Link Type = " + Other.PinLinkType); 117 | Debug.LogWarning("Mine should be input, theirs output."); 118 | } 119 | } 120 | else 121 | { 122 | Debug.LogWarning(OwnerID + " == " + Other.OwnerID); 123 | } 124 | 125 | return false; 126 | } 127 | 128 | public void RenderPin(EditorGraph Graph, EditorNode Node) 129 | { 130 | Rect PinRect = Node.GetPinRect(ID); 131 | GUI.Button(PinRect, " "); 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /Scripts/EditorGraph.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Reflection; 3 | using UnityEngine; 4 | 5 | [System.Serializable] 6 | public class EditorGraph : ScriptableObject { 7 | 8 | public delegate void EditorGraphEvent(); 9 | public event EditorGraphEvent OnGraphChanged; 10 | 11 | [SerializeField] private List Nodes; 12 | [SerializeField] private List Links; 13 | [SerializeField] public Vector2 EditorViewportOffset = new Vector2(); 14 | [SerializeField] private int UIDCounter = -1; 15 | 16 | private Dictionary NodeMap; 17 | private Dictionary PinMap; 18 | 19 | private EditorPinIdentifier SelectedElement; 20 | 21 | public EditorPinIdentifier GetSelectedElementID() 22 | { 23 | return SelectedElement; 24 | } 25 | 26 | public EditorNode GetSelectedNode() 27 | { 28 | return GetNodeFromID(SelectedElement.NodeID); 29 | } 30 | 31 | public void SelectNode(int NodeID) 32 | { 33 | SelectedElement.NodeID = NodeID; 34 | SelectedElement.PinID = -1; 35 | } 36 | 37 | public void SelectPin(EditorPinIdentifier PinIdentifier) 38 | { 39 | SelectedElement = PinIdentifier; 40 | } 41 | 42 | public void Deselect() 43 | { 44 | SelectedElement.NodeID = -1; 45 | SelectedElement.PinID = -1; 46 | } 47 | 48 | public int AddNode(EditorNode _Node) 49 | { 50 | if (Nodes == null) 51 | { 52 | Nodes = new List(); 53 | } 54 | if (NodeMap == null) 55 | { 56 | NodeMap = new Dictionary(); 57 | } 58 | 59 | NodeMap[_Node.ID] = _Node; 60 | Nodes.Add(_Node); 61 | _Node.OnNodeChanged += NotifyGraphChange; 62 | NotifyGraphChange(); 63 | return _Node.ID; 64 | } 65 | 66 | public bool RemoveNode(EditorNode _Node) 67 | { 68 | if (Nodes == null) 69 | { 70 | return false; 71 | } 72 | 73 | int NodeID = _Node.ID; 74 | bool bSuccess = Nodes.Remove(_Node); 75 | 76 | if (bSuccess) 77 | { 78 | // remove all associated links 79 | for (int Index = Links.Count - 1; Index >= 0; --Index) 80 | { 81 | EditorLink Link = Links[Index]; 82 | if (Link.NodeID_From == NodeID || Link.NodeID_To == NodeID) 83 | { 84 | Links.RemoveAt(Index); 85 | } 86 | } 87 | 88 | _Node.OnNodeChanged -= NotifyGraphChange; 89 | NotifyGraphChange(); 90 | return true; 91 | } 92 | else 93 | { 94 | return false; 95 | } 96 | } 97 | 98 | public EditorNode GetNodeFromID(int ID) 99 | { 100 | if (NodeMap == null) 101 | { 102 | NodeMap = new Dictionary(); 103 | } 104 | else if (NodeMap.ContainsKey(ID)) 105 | { 106 | return NodeMap[ID]; 107 | } 108 | 109 | foreach (EditorNode _Node in Nodes) 110 | { 111 | if (_Node.ID == ID) 112 | { 113 | NodeMap[ID] = _Node; 114 | return _Node; 115 | } 116 | } 117 | 118 | Debug.LogError("Trying to get Node with invalid ID " + ID + "."); 119 | return null; 120 | } 121 | 122 | public EditorPin GetPinFromID(EditorPinIdentifier PinIdentifier) 123 | { 124 | if (PinMap == null) 125 | { 126 | PinMap = new Dictionary(); 127 | } 128 | else if (PinMap.ContainsKey(PinIdentifier)) 129 | { 130 | return PinMap[PinIdentifier]; 131 | } 132 | 133 | EditorNode _Node = GetNodeFromID(PinIdentifier.NodeID); 134 | if (_Node != null) 135 | { 136 | EditorPin Pin = _Node.GetPin(PinIdentifier.PinID); 137 | PinMap[PinIdentifier] = Pin; 138 | return Pin; 139 | } 140 | 141 | return null; 142 | } 143 | 144 | public EditorNode CreateFromFunction(System.Type ClassType, string Methodname, bool bHasOutput = false, bool bHasInput = false) 145 | { 146 | return EditorNode.CreateFromFunction(this, ClassType, Methodname, bHasInput, bHasOutput); 147 | } 148 | 149 | public void LinkPins(EditorPinIdentifier LHSPin, EditorPinIdentifier RHSPin) 150 | { 151 | if (Links == null) 152 | { 153 | Links = new List(); 154 | } 155 | 156 | EditorLink NewLink = new EditorLink(LHSPin, RHSPin); 157 | Links.Add(NewLink); 158 | NotifyGraphChange(); 159 | } 160 | 161 | public List GetNodeList() 162 | { 163 | if (Nodes == null) 164 | { 165 | Nodes = new List(); 166 | } 167 | return Nodes; 168 | } 169 | 170 | public List GetLinkList() 171 | { 172 | if (Links == null) 173 | { 174 | Links = new List(); 175 | } 176 | return Links; 177 | } 178 | 179 | public void NotifyGraphChange() 180 | { 181 | if (OnGraphChanged != null) 182 | { 183 | OnGraphChanged(); 184 | } 185 | } 186 | 187 | public void RenderGraph() 188 | { 189 | foreach (EditorNode Node in Nodes) 190 | { 191 | Node.RenderNode(this, IsNodeSelected() && Node == GetNodeFromID(SelectedElement.NodeID)); 192 | } 193 | 194 | foreach (EditorLink Link in Links) 195 | { 196 | Link.RenderLink(this); 197 | } 198 | } 199 | 200 | public bool IsPinSelected() 201 | { 202 | return SelectedElement.PinID != -1; 203 | } 204 | 205 | public bool IsNodeSelected() 206 | { 207 | return SelectedElement.NodeID != -1 && SelectedElement.PinID == -1; 208 | } 209 | 210 | public int GenerateUniqueNodeID() 211 | { 212 | return ++UIDCounter; 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /Scripts/EditorNode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using System.Reflection; 6 | 7 | public struct EditorNodeRenderData 8 | { 9 | public Rect NodeRect; 10 | public float PinVerticalOffset; 11 | public float InputPinHorizontalOffset; 12 | public float OutputPinHorizontalOffset; 13 | public float PinVerticalSpacing; 14 | public float PinSize; 15 | } 16 | 17 | public struct EditorPinIdentifier 18 | { 19 | public EditorPinIdentifier(int _NodeID = -1, int _PinID = -1) 20 | { 21 | NodeID = _NodeID; 22 | PinID = _PinID; 23 | } 24 | 25 | public int NodeID; 26 | public int PinID; 27 | 28 | public override string ToString() 29 | { 30 | return NodeID + "." + PinID; 31 | } 32 | 33 | } 34 | 35 | [System.Serializable] 36 | public class EditorNode 37 | { 38 | 39 | public delegate void EditorNodeEvent(); 40 | public event EditorNodeEvent OnNodeChanged; 41 | 42 | [SerializeField] private List Pins; 43 | [SerializeField] private string _name; 44 | [SerializeField] private Vector2 _position; 45 | [SerializeField] private int _ID; 46 | private EditorNodeRenderData _renderData; 47 | 48 | public string Name 49 | { 50 | get 51 | { 52 | return _name; 53 | } 54 | set 55 | { 56 | _name = value; 57 | } 58 | } 59 | 60 | public Vector2 Position 61 | { 62 | get 63 | { 64 | return _position; 65 | } 66 | set 67 | { 68 | _position = value; 69 | } 70 | } 71 | 72 | public int ID 73 | { 74 | get 75 | { 76 | return _ID; 77 | } 78 | } 79 | 80 | public int PinCount 81 | { 82 | get 83 | { 84 | return Pins.Count; 85 | } 86 | } 87 | 88 | public EditorNode() 89 | { 90 | Debug.LogError("EditorNode being constructed without its graph owner as a parameter, unable to generate a unique identifier. (ID = "+_ID+")"); 91 | Init(); 92 | } 93 | 94 | public EditorNode(EditorGraph Owner) 95 | { 96 | _ID = Owner.GenerateUniqueNodeID(); 97 | Init(); 98 | } 99 | 100 | private void Init() 101 | { 102 | Pins = new List(); 103 | UpdateNodeRect(); 104 | } 105 | 106 | public EditorPin GetPin(int Index) 107 | { 108 | if (Index < 0 || Index >= Pins.Count) 109 | { 110 | Debug.LogError("Attempted to get pin of invalid index " + Index + ", (max = " + Pins.Count + ")"); 111 | } 112 | return Pins[Index]; 113 | } 114 | 115 | public Rect GetNodeRect() 116 | { 117 | UpdateNodeRect(); 118 | return _renderData.NodeRect; 119 | } 120 | 121 | public Rect GetPinRect(int ID) 122 | { 123 | EditorPin Pin = Pins[ID]; 124 | EPinLinkType LinkType = Pin.GetPinLinkType(); 125 | 126 | int TypeIndex = 1; 127 | for (int Index = 0; Index < ID; ++Index) 128 | { 129 | if (Pins[Index].GetPinLinkType() == LinkType) 130 | { 131 | ++TypeIndex; 132 | } 133 | } 134 | 135 | Rect ReturnRect = new Rect(); 136 | Vector2 RectPosition = new Vector2(); 137 | ReturnRect.width = _renderData.PinSize; 138 | ReturnRect.height = _renderData.PinSize; 139 | RectPosition.y = _renderData.NodeRect.position.y + _renderData.PinVerticalOffset + TypeIndex * (ReturnRect.height + _renderData.PinVerticalSpacing); 140 | if (LinkType == EPinLinkType.Input) 141 | { 142 | RectPosition.x = _renderData.NodeRect.position.x + _renderData.InputPinHorizontalOffset; 143 | } 144 | else 145 | { 146 | RectPosition.x = _renderData.NodeRect.position.x + _renderData.OutputPinHorizontalOffset - _renderData.PinSize; 147 | } 148 | ReturnRect.position = RectPosition; 149 | 150 | return ReturnRect; 151 | } 152 | 153 | public Rect GetPinTextRect(int ID) 154 | { 155 | float EstimatedCharacterWidth = 8.0f; 156 | float EstimatedCharacterHeight = 16.0f; 157 | 158 | Rect PinRect = GetPinRect(ID); 159 | PinRect.width = (Pins[ID].GetPinName().Length+1) * EstimatedCharacterWidth; 160 | PinRect.height = EstimatedCharacterHeight; 161 | Vector2 RectPos = PinRect.position; 162 | if (Pins[ID].GetPinLinkType() == EPinLinkType.Input) 163 | { 164 | RectPos.x += _renderData.PinSize; 165 | } 166 | else 167 | { 168 | RectPos.x -= PinRect.width - _renderData.PinSize; 169 | } 170 | PinRect.position = RectPos; 171 | return PinRect; 172 | } 173 | 174 | public string GetPinName(int ID) 175 | { 176 | return Pins[ID].GetPinName(); 177 | } 178 | 179 | public EditorPinIdentifier GetPinIdentifier(int ID) 180 | { 181 | EditorPinIdentifier Identifier = new EditorPinIdentifier(); 182 | Identifier.NodeID = _ID; 183 | Identifier.PinID = ID; 184 | return Identifier; 185 | } 186 | 187 | public void SetNodePosition(Vector2 InPos) 188 | { 189 | Position = InPos; 190 | UpdateNodeRect(); 191 | } 192 | 193 | public Vector2 GetNodePosition() 194 | { 195 | return Position; 196 | } 197 | 198 | public void UpdateNodeRect() 199 | { 200 | _renderData.PinSize = 10.0f; 201 | _renderData.NodeRect.position = Position; 202 | _renderData.NodeRect.width = 60.0f + GetLongestPinNameWidth(EPinLinkType.Input) + GetLongestPinNameWidth(EPinLinkType.Output); 203 | _renderData.NodeRect.height = 16.0f + (_renderData.PinVerticalOffset + ((_renderData.PinSize + _renderData.PinVerticalSpacing) * Mathf.Max(GetNumPins(EPinLinkType.Input), GetNumPins(EPinLinkType.Output)))); 204 | _renderData.InputPinHorizontalOffset = _renderData.PinSize; 205 | _renderData.OutputPinHorizontalOffset = _renderData.NodeRect.width - _renderData.PinSize; 206 | _renderData.PinVerticalOffset = 16.0f; 207 | _renderData.PinVerticalSpacing = 10.0f; 208 | } 209 | 210 | public void RenderNode(EditorGraph Graph, bool bIsSelected) 211 | { 212 | const float selectionBorder = 5.0f; 213 | if (bIsSelected) 214 | { 215 | EditorGraphDrawUtils.DrawRect(_renderData.NodeRect.min - Vector2.one * selectionBorder, _renderData.NodeRect.max + Vector2.one * selectionBorder, Color.yellow); 216 | } 217 | GUI.Box(_renderData.NodeRect, " "); 218 | 219 | for (int PinIndex = 0; PinIndex < PinCount; ++PinIndex) 220 | { 221 | Pins[PinIndex].RenderPin(Graph, this); 222 | } 223 | 224 | RenderNodeText(); 225 | } 226 | 227 | private void RenderNodeText() 228 | { 229 | Rect NodeRect = _renderData.NodeRect; 230 | NodeRect.height = 16.0f; 231 | EditorGUI.LabelField(NodeRect, Name); 232 | 233 | for (int PinIndex = 0; PinIndex < PinCount; ++PinIndex) 234 | { 235 | EditorGUI.LabelField(GetPinTextRect(PinIndex), GetPinName(PinIndex)); 236 | } 237 | } 238 | 239 | private float GetLongestPinNameWidth(EPinLinkType LinkType) 240 | { 241 | float Width = 0.0f; 242 | foreach (EditorPin Pin in Pins) 243 | { 244 | if (Pin.GetPinLinkType() == LinkType) 245 | { 246 | float CurrentPinWidth = EstimateStringWidth(Pin.GetPinName()); 247 | if (CurrentPinWidth > Width) 248 | { 249 | Width = CurrentPinWidth; 250 | } 251 | } 252 | } 253 | return Width; 254 | } 255 | 256 | private float EstimateStringWidth(string _String) 257 | { 258 | return _String.Length * 8.0f; 259 | } 260 | 261 | private int AddPin(EPinLinkType _LinkType, System.Type _Type, string _Name) 262 | { 263 | int PinID = Pins.Count; 264 | Debug.Log("Adding pin to node with ID " + ID + ", pinID = " + PinID); 265 | EditorPin NewPin = new EditorPin((_Type == null) ? "null" : _Type.ToString(), _Name, ID, PinID, _LinkType); 266 | Pins.Add(NewPin); 267 | UpdateNodeRect(); 268 | return PinID; 269 | } 270 | 271 | private void ClearPins() 272 | { 273 | Pins.Clear(); 274 | } 275 | 276 | private bool RemovePin(EditorPin _Pin) 277 | { 278 | return Pins.Remove(_Pin); 279 | } 280 | 281 | private int GetNumPins(EPinLinkType PinLinkType) 282 | { 283 | int OutNumPins = 0; 284 | for (int PinIndex = 0; PinIndex < PinCount; ++PinIndex) 285 | { 286 | EditorPin Pin = Pins[PinIndex]; 287 | if (Pin.GetPinLinkType() == PinLinkType) 288 | { 289 | ++OutNumPins; 290 | } 291 | } 292 | return OutNumPins; 293 | } 294 | 295 | private void NotifyGraphChange() 296 | { 297 | if (OnNodeChanged != null) 298 | { 299 | OnNodeChanged(); 300 | } 301 | } 302 | 303 | public static EditorNode CreateFromFunction(EditorGraph Owner, System.Type ClassType, string Methodname, bool bHasOutput = true, bool bHasInput = true) 304 | { 305 | EditorNode _Node = new EditorNode(Owner); 306 | 307 | if (ClassType != null) 308 | { 309 | MethodInfo methodInfo = ClassType.GetMethod(Methodname); 310 | 311 | if (methodInfo != null) 312 | { 313 | _Node.Name = SanitizeName(Methodname); 314 | //Debug.Log("Method name: " + _Node.Name); 315 | //Debug.Log("Return type: " + methodInfo.ReturnParameter.ParameterType.ToString()); 316 | //Debug.Log("Return name: " + methodInfo.ReturnParameter.Name); 317 | 318 | if (bHasOutput) 319 | { 320 | _Node.AddPin(EPinLinkType.Output, null, ""); 321 | } 322 | if (bHasInput) 323 | { 324 | _Node.AddPin(EPinLinkType.Input, null, ""); 325 | } 326 | 327 | _Node.AddPin(EPinLinkType.Output, methodInfo.ReturnParameter.ParameterType, "Output"); 328 | 329 | ParameterInfo[] Parameters = methodInfo.GetParameters(); 330 | foreach (ParameterInfo Parameter in Parameters) 331 | { 332 | //Debug.Log("Param type: " + Parameter.ParameterType.ToString()); 333 | //Debug.Log("Param name: " + Parameter.Name); 334 | 335 | _Node.AddPin(EPinLinkType.Input, Parameter.ParameterType, Parameter.Name); 336 | } 337 | } 338 | else 339 | { 340 | Debug.LogError("Function '" + ClassType.ToString() + "." + Methodname + "' not found."); 341 | } 342 | } 343 | else 344 | { 345 | Debug.LogError("Tried to create node from function from an unknown class type."); 346 | } 347 | 348 | _Node.NotifyGraphChange(); 349 | return _Node; 350 | } 351 | 352 | private static string SanitizeName(string Name) 353 | { 354 | string Result = "" + Name[0]; 355 | 356 | bool bWasCapital = Name[0] >= 'A' && Name[0] <= 'Z'; 357 | for (int i = 1; i < Name.Length; ++i) 358 | { 359 | if (Name[i] >= 'A' && Name[i] <= 'Z') 360 | { 361 | if (!bWasCapital) 362 | { 363 | Result += " " + Name[i]; 364 | } 365 | else 366 | { 367 | Result += Name[i]; 368 | } 369 | bWasCapital = true; 370 | } 371 | else 372 | { 373 | Result += Name[i]; 374 | bWasCapital = false; 375 | } 376 | } 377 | 378 | return Result; 379 | } 380 | 381 | } 382 | -------------------------------------------------------------------------------- /Scripts/GraphEditor.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEditor; 5 | 6 | public class GraphEditorWindow : EditorWindow, IGraphInputListener, IGraphInputHandler { 7 | 8 | private Dictionary> RegisteredFunctionDictionary; 9 | private Dictionary ShowFunctions; 10 | private List NodeList = new List(); 11 | private List LinkList = new List(); 12 | private EditorGraph GraphToEdit; 13 | private int controlId = -1; 14 | 15 | private Vector2 oldDragPosition = new Vector2(); 16 | 17 | [MenuItem("Window/Graph Editor")] 18 | public static void ShowGraphEditor() 19 | { 20 | GraphEditorWindow window = EditorWindow.GetWindow(typeof(GraphEditorWindow)) as GraphEditorWindow; 21 | window.Reset(); 22 | } 23 | 24 | public void Reset() 25 | { 26 | if (GraphToEdit != null) 27 | { 28 | GraphToEdit.Deselect(); 29 | } 30 | UpdateFunctionMap(); 31 | } 32 | 33 | private bool HitTestPointToRect(Vector2 InPoint, Rect InRect) 34 | { 35 | if (InPoint.x >= InRect.min.x && InPoint.x <= InRect.max.x && 36 | InPoint.y >= InRect.min.y && InPoint.y <= InRect.max.y) 37 | { 38 | return true; 39 | } 40 | return false; 41 | } 42 | 43 | private void OnGUI() 44 | { 45 | DrawGrid(20, 0.2f, Color.gray); 46 | DrawGrid(100, 0.4f, Color.gray); 47 | 48 | ProcessEvents(Event.current); 49 | 50 | if (GraphToEdit == null) 51 | { 52 | Reset(); 53 | return; 54 | } 55 | 56 | if (GraphToEdit != null) 57 | { 58 | if (RegisteredFunctionDictionary == null) 59 | { 60 | Reset(); 61 | } 62 | } 63 | 64 | RenderGraph(); 65 | // draw editors for selected node 66 | 67 | if (GraphToEdit != null && GraphToEdit.IsPinSelected()) 68 | { 69 | EditorNode OwnerNode = GraphToEdit.GetSelectedNode(); 70 | Vector2 PinPos = OwnerNode.GetPinRect(GraphToEdit.GetSelectedElementID().PinID).center; 71 | EditorGraphDrawUtils.Line(PinPos, Event.current.mousePosition, Color.magenta); 72 | } 73 | } 74 | 75 | private void OnClick_NewGraph() 76 | { 77 | EditorGraph NewGraph = ScriptableObject.CreateInstance(); 78 | string AssetDirectory = "Assets/"; 79 | if (AssetDatabase.IsValidFolder("Assets/Graphs")) 80 | { 81 | AssetDirectory = "Assets/Graphs/"; 82 | } 83 | string AssetName = "NewGraph"; 84 | int ID = 1; 85 | while (AssetDatabase.FindAssets(AssetName).Length > 0) 86 | { 87 | AssetName = "NewGraph_" + ID; 88 | ++ID; 89 | } 90 | AssetDatabase.CreateAsset(NewGraph, AssetDirectory + AssetName + ".asset"); 91 | AssetDatabase.SaveAssets(); 92 | NewGraph.Deselect(); 93 | OnGraphLoaded(NewGraph); 94 | } 95 | 96 | private void OnClick_LoadGraph() 97 | { 98 | controlId = GUIUtility.GetControlID(FocusType.Passive); 99 | EditorGUIUtility.ShowObjectPicker(null, false, "", controlId); 100 | } 101 | 102 | private void AddFunctionListToContextMenu(GenericMenu menu, Vector2 mousePos) 103 | { 104 | if (GraphToEdit == null) 105 | { 106 | menu.AddDisabledItem(new GUIContent("No graph available")); 107 | return; 108 | } 109 | 110 | foreach (System.Type LibraryType in RegisteredFunctionDictionary.Keys) 111 | { 112 | string libraryName = LibraryType.ToString(); 113 | foreach (string MethodName in RegisteredFunctionDictionary[LibraryType]) 114 | { 115 | menu.AddItem(new GUIContent(libraryName + "/" + MethodName), false, () => OnClick_AddNode(LibraryType, MethodName, mousePos)); 116 | } 117 | } 118 | } 119 | 120 | private void OnClick_AddNode(System.Type LibraryType, string MethodName, Vector2 mousePos) 121 | { 122 | int NodeID = GraphToEdit.AddNode(EditorNode.CreateFromFunction(GraphToEdit, LibraryType, MethodName, false, false)); 123 | EditorNode Node = GraphToEdit.GetNodeFromID(NodeID); 124 | Node.SetNodePosition(mousePos); 125 | Repaint(); 126 | } 127 | 128 | private void ProcessEvents(Event e) 129 | { 130 | switch (e.type) 131 | { 132 | case EventType.MouseDown: 133 | { 134 | OnMouseDown(e.button, e.mousePosition); 135 | break; 136 | } 137 | case EventType.MouseUp: 138 | { 139 | OnMouseUp(e.button, e.mousePosition); 140 | break; 141 | } 142 | case EventType.MouseDrag: 143 | { 144 | OnMouseMove(e.mousePosition); 145 | break; 146 | } 147 | case EventType.KeyUp: 148 | { 149 | if (e.keyCode == KeyCode.Delete) 150 | { 151 | if (GraphToEdit.IsNodeSelected()) 152 | { 153 | GraphToEdit.RemoveNode(GraphToEdit.GetSelectedNode()); 154 | GraphToEdit.Deselect(); 155 | Repaint(); 156 | } 157 | } 158 | break; 159 | } 160 | } 161 | 162 | if (e.commandName.Equals("ObjectSelectorClosed")) 163 | { 164 | OnGraphLoaded(EditorGUIUtility.GetObjectPickerObject() as EditorGraph); 165 | controlId = -1; 166 | } 167 | } 168 | 169 | private void ProcessContextMenu(Vector2 mousePosition) 170 | { 171 | GenericMenu genericMenu = new GenericMenu(); 172 | genericMenu.AddItem(new GUIContent("New Graph"), false, () => OnClick_NewGraph()); 173 | genericMenu.AddItem(new GUIContent("Load Graph"), false, () => OnClick_LoadGraph()); 174 | genericMenu.AddSeparator(""); 175 | AddFunctionListToContextMenu(genericMenu, mousePosition); 176 | genericMenu.ShowAsContext(); 177 | } 178 | 179 | private void UpdateFunctionMap() 180 | { 181 | UpdateGraphCache(); 182 | 183 | RegisteredFunctionDictionary = new Dictionary>(); 184 | ShowFunctions = new Dictionary(); 185 | 186 | List SubclassList = new List(); 187 | TypeUtilities.GetAllSubclasses(typeof(FunctionLibrary), SubclassList); 188 | 189 | foreach (System.Type Subclass in SubclassList) 190 | { 191 | RegisteredFunctionDictionary[Subclass] = new List(); 192 | System.Reflection.MethodInfo[] methodInfos = Subclass.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); 193 | foreach (System.Reflection.MethodInfo methodInfo in methodInfos) 194 | { 195 | RegisteredFunctionDictionary[Subclass].Add(methodInfo.Name); 196 | } 197 | } 198 | } 199 | 200 | public void SetGraph(EditorGraph _Graph) 201 | { 202 | EditorGraph OldGraph = GraphToEdit; 203 | GraphToEdit = _Graph; 204 | if (GraphToEdit != null) 205 | { 206 | if (OldGraph != null && OldGraph != GraphToEdit) 207 | { 208 | OldGraph.OnGraphChanged -= OnGraphChanged; 209 | } 210 | 211 | UpdateGraphCache(); 212 | 213 | GraphToEdit.OnGraphChanged += OnGraphChanged; 214 | } 215 | } 216 | 217 | public void OnGraphChanged() 218 | { 219 | UpdateGraphCache(); 220 | 221 | SaveGraph(); 222 | } 223 | 224 | private void SaveGraph() 225 | { 226 | if (GraphToEdit != null) 227 | { 228 | EditorUtility.SetDirty(GraphToEdit); 229 | AssetDatabase.SaveAssets(); 230 | } 231 | } 232 | 233 | public void RenderGraph() 234 | { 235 | if (GraphToEdit != null) 236 | { 237 | GraphToEdit.RenderGraph(); 238 | } 239 | } 240 | 241 | public void OnGraphLoaded(EditorGraph graph) 242 | { 243 | SetGraph(graph); 244 | UpdateFunctionMap(); 245 | } 246 | 247 | private void UpdateGraphCache() 248 | { 249 | if (GraphToEdit != null) 250 | { 251 | NodeList = GraphToEdit.GetNodeList(); 252 | 253 | foreach (EditorNode Node in NodeList) 254 | { 255 | Node.UpdateNodeRect(); 256 | } 257 | 258 | LinkList = GraphToEdit.GetLinkList(); 259 | Repaint(); 260 | } 261 | } 262 | 263 | public void OnMouseDown(int button, Vector2 mousePos) 264 | { 265 | if (GraphToEdit == null) 266 | { 267 | return; 268 | } 269 | 270 | if (button == 0) 271 | { 272 | oldDragPosition = mousePos; 273 | 274 | foreach (EditorNode _Node in NodeList) 275 | { 276 | int NumPins = _Node.PinCount; 277 | for (int PinIndex = 0; PinIndex < NumPins; ++PinIndex) 278 | { 279 | Rect PinRect = _Node.GetPinRect(PinIndex); 280 | if (HitTestPointToRect(mousePos, PinRect)) 281 | { 282 | GraphToEdit.SelectPin(_Node.GetPinIdentifier(PinIndex)); 283 | return; 284 | } 285 | } 286 | 287 | Rect NodeRect = _Node.GetNodeRect(); 288 | if (HitTestPointToRect(mousePos, NodeRect)) 289 | { 290 | GraphToEdit.Deselect(); 291 | GraphToEdit.SelectNode(_Node.ID); 292 | Repaint(); 293 | return; 294 | } 295 | } 296 | 297 | GraphToEdit.Deselect(); 298 | } 299 | else if (button == 1) 300 | { 301 | ProcessContextMenu(mousePos); 302 | } 303 | Repaint(); 304 | } 305 | 306 | public void OnMouseUp(int button, Vector2 mousePos) 307 | { 308 | if (button == 0) 309 | { 310 | if (GraphToEdit == null) 311 | { 312 | return; 313 | } 314 | 315 | bool bMouseOverNode = false; 316 | foreach (EditorNode _Node in NodeList) 317 | { 318 | Rect NodeRect = _Node.GetNodeRect(); 319 | if (HitTestPointToRect(mousePos, NodeRect)) 320 | { 321 | bMouseOverNode = true; 322 | int NumPins = _Node.PinCount; 323 | for (int PinIndex = 0; PinIndex < NumPins; ++PinIndex) 324 | { 325 | Rect PinRect = _Node.GetPinRect(PinIndex); 326 | if (HitTestPointToRect(mousePos, PinRect)) 327 | { 328 | if (GraphToEdit.IsPinSelected()) 329 | { 330 | EditorPinIdentifier SelectedPinIdentifier = GraphToEdit.GetSelectedElementID(); 331 | LinkPins(SelectedPinIdentifier, _Node.GetPinIdentifier(PinIndex)); 332 | UpdateGraphCache(); 333 | break; 334 | } 335 | } 336 | } 337 | } 338 | } 339 | 340 | if (!bMouseOverNode || GraphToEdit.IsPinSelected()) 341 | { 342 | GraphToEdit.Deselect(); 343 | } 344 | } 345 | else if (button == 1) 346 | { 347 | ProcessContextMenu(mousePos); 348 | } 349 | 350 | Repaint(); 351 | } 352 | 353 | public void OnMouseMove(Vector2 mousePosition) 354 | { 355 | Vector2 mouseDelta = mousePosition - oldDragPosition; 356 | if (GraphToEdit == null) 357 | { 358 | return; 359 | } 360 | 361 | if (GraphToEdit.IsNodeSelected()) 362 | { 363 | MoveNode(GraphToEdit.GetSelectedNode(), mouseDelta); 364 | } 365 | oldDragPosition = mousePosition; 366 | 367 | Repaint(); 368 | } 369 | 370 | public bool LinkPins(EditorPinIdentifier pinA, EditorPinIdentifier pinB) 371 | { 372 | EditorPin pinAData = GraphToEdit.GetPinFromID(pinA); 373 | EditorPin pinBData = GraphToEdit.GetPinFromID(pinB); 374 | 375 | if (!pinBData.CanLinkTo(pinAData)) 376 | { 377 | Debug.LogWarning("Failed to link pin "+pinA+" to "+pinB+"."); 378 | return false; 379 | } 380 | 381 | GraphToEdit.LinkPins(pinA, pinB); 382 | return true; 383 | } 384 | 385 | public void MoveNode(EditorNode node, Vector2 delta) 386 | { 387 | Vector2 position = node.GetNodePosition(); 388 | node.SetNodePosition(position + delta); 389 | Repaint(); 390 | } 391 | 392 | private Vector2 drag = new Vector2(); 393 | 394 | private void DrawGrid(float cellSize, float opacity, Color colour) 395 | { 396 | int widthDivs = Mathf.CeilToInt(position.width / cellSize); 397 | int heightDivs = Mathf.CeilToInt(position.height / cellSize); 398 | 399 | Handles.BeginGUI(); 400 | Handles.color = new Color(colour.r, colour.g, colour.b, opacity); 401 | 402 | Vector3 newOffset = new Vector3(); 403 | if (GraphToEdit != null) 404 | { 405 | GraphToEdit.EditorViewportOffset += drag * 0.5f; 406 | newOffset = new Vector3(GraphToEdit.EditorViewportOffset.x % cellSize, GraphToEdit.EditorViewportOffset.y % cellSize, 0); 407 | } 408 | 409 | for (int i = 0; i < widthDivs; ++i) 410 | { 411 | Handles.DrawLine(new Vector3(cellSize * i, -cellSize, 0) + newOffset, new Vector3(cellSize * i, position.height, 0) + newOffset); 412 | } 413 | 414 | for (int j = 0; j < heightDivs; ++j) 415 | { 416 | Handles.DrawLine(new Vector3(-cellSize, cellSize * j, 0) + newOffset, new Vector3(position.width, cellSize * j, 0) + newOffset); 417 | } 418 | 419 | Handles.color = Color.white; 420 | Handles.EndGUI(); 421 | } 422 | } 423 | --------------------------------------------------------------------------------