using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using System.Reflection; using Unity.Jobs; using System.Linq; namespace GraphProcessor { public delegate IEnumerable CustomPortBehaviorDelegate(List edges); public delegate IEnumerable CustomPortTypeBehaviorDelegate(string fieldName, string displayName, object value); [Serializable] public abstract class BaseNode { [SerializeField] internal string nodeCustomName = null; // The name of the node in case it was renamed by a user /// /// Name of the node, it will be displayed in the title section /// /// public virtual string name => GetType().Name; /// /// The accent color of the node /// public virtual Color color => Color.clear; /// /// Set a custom uss file for the node. We use a Resources.Load to get the stylesheet so be sure to put the correct resources path /// https://docs.unity3d.com/ScriptReference/Resources.Load.html /// public virtual string layoutStyle => string.Empty; /// /// If the node can be locked or not /// public virtual bool unlockable => true; /// /// Is the node is locked (if locked it can't be moved) /// public virtual bool isLocked => nodeLock; //id public string GUID; public int computeOrder = -1; /// Tell wether or not the node can be processed. Do not check anything from inputs because this step happens before inputs are sent to the node public virtual bool canProcess => true; /// Show the node controlContainer only when the mouse is over the node public virtual bool showControlsOnHover => false; /// True if the node can be deleted, false otherwise public virtual bool deletable => true; /// /// Container of input ports /// [NonSerialized] public readonly NodeInputPortContainer inputPorts; /// /// Container of output ports /// [NonSerialized] public readonly NodeOutputPortContainer outputPorts; //Node view datas public Rect position; /// /// Is the node expanded /// public bool expanded; /// /// Is debug visible /// public bool debug; /// /// Node locked state /// public bool nodeLock; public delegate void ProcessDelegate(); /// /// Triggered when the node is processes /// public event ProcessDelegate onProcessed; public event Action onMessageAdded; public event Action onMessageRemoved; /// /// Triggered after an edge was connected on the node /// public event Action onAfterEdgeConnected; /// /// Triggered after an edge was disconnected on the node /// public event Action onAfterEdgeDisconnected; /// /// Triggered after a single/list of port(s) is updated, the parameter is the field name /// public event Action onPortsUpdated; [NonSerialized] bool _needsInspector = false; /// /// Does the node needs to be visible in the inspector (when selected). /// public virtual bool needsInspector => _needsInspector; /// /// Can the node be renamed in the UI. By default a node can be renamed by double clicking it's name. /// public virtual bool isRenamable => false; /// /// Is the node created from a duplicate operation (either ctrl-D or copy/paste). /// public bool createdFromDuplication { get; internal set; } = false; /// /// True only when the node was created from a duplicate operation and is inside a group that was also duplicated at the same time. /// public bool createdWithinGroup { get; internal set; } = false; [NonSerialized] internal Dictionary nodeFields = new Dictionary(); [NonSerialized] internal Dictionary customPortTypeBehaviorMap = new Dictionary(); [NonSerialized] List messages = new List(); [NonSerialized] protected BaseGraph graph; internal class NodeFieldInformation { public string name; public string fieldName; public FieldInfo info; public bool input; public bool isMultiple; public string tooltip; public CustomPortBehaviorDelegate behavior; public bool vertical; public NodeFieldInformation(FieldInfo info, string name, bool input, bool isMultiple, string tooltip, bool vertical, CustomPortBehaviorDelegate behavior) { this.input = input; this.isMultiple = isMultiple; this.info = info; this.name = name; this.fieldName = info.Name; this.behavior = behavior; this.tooltip = tooltip; this.vertical = vertical; } } struct PortUpdate { public List fieldNames; public BaseNode node; public void Deconstruct(out List fieldNames, out BaseNode node) { fieldNames = this.fieldNames; node = this.node; } } // Used in port update algorithm Stack fieldsToUpdate = new Stack(); HashSet updatedFields = new HashSet(); /// /// Creates a node of type T at a certain position /// /// position in the graph in pixels /// type of the node /// the node instance public static T CreateFromType(Vector2 position) where T : BaseNode { return CreateFromType(typeof(T), position) as T; } /// /// Creates a node of type nodeType at a certain position /// /// position in the graph in pixels /// type of the node /// the node instance public static BaseNode CreateFromType(Type nodeType, Vector2 position) { if (!nodeType.IsSubclassOf(typeof(BaseNode))) return null; var node = Activator.CreateInstance(nodeType) as BaseNode; node.position = new Rect(position, new Vector2(100, 100)); ExceptionToLog.Call(() => node.OnNodeCreated()); return node; } #region Initialization // called by the BaseGraph when the node is added to the graph public void Initialize(BaseGraph graph) { this.graph = graph; ExceptionToLog.Call(() => Enable()); InitializePorts(); } void InitializeCustomPortTypeMethods() { MethodInfo[] methods = new MethodInfo[0]; Type baseType = GetType(); while (true) { methods = baseType.GetMethods(BindingFlags.NonPublic | BindingFlags.Instance); foreach (var method in methods) { var typeBehaviors = method.GetCustomAttributes().ToArray(); if (typeBehaviors.Length == 0) continue; CustomPortTypeBehaviorDelegate deleg = null; try { deleg = Delegate.CreateDelegate(typeof(CustomPortTypeBehaviorDelegate), this, method) as CustomPortTypeBehaviorDelegate; } catch (Exception e) { Debug.LogError(e); Debug.LogError( $"Cannot convert method {method} to a delegate of type {typeof(CustomPortTypeBehaviorDelegate)}"); } foreach (var typeBehavior in typeBehaviors) customPortTypeBehaviorMap[typeBehavior.type] = deleg; } // Try to also find private methods in the base class baseType = baseType.BaseType; if (baseType == null) break; } } /// /// Use this function to initialize anything related to ports generation in your node /// This will allow the node creation menu to correctly recognize ports that can be connected between nodes /// public virtual void InitializePorts() { InitializeCustomPortTypeMethods(); foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) { var nodeField = nodeFields[key.Name]; if (HasCustomBehavior(nodeField)) { UpdatePortsForField(nodeField.fieldName, sendPortUpdatedEvent: false); } else { // If we don't have a custom behavior on the node, we just have to create a simple port AddPort(nodeField.input, nodeField.fieldName, new PortData { acceptMultipleEdges = nodeField.isMultiple, displayName = nodeField.name, tooltip = nodeField.tooltip, vertical = nodeField.vertical }); } } } /// /// Override the field order inside the node. It allows to re-order all the ports and field in the UI. /// /// List of fields to sort /// Sorted list of fields public virtual IEnumerable OverrideFieldOrder(IEnumerable fields) { long GetFieldInheritanceLevel(FieldInfo f) { int level = 0; var t = f.DeclaringType; while (t != null) { t = t.BaseType; level++; } return level; } // Order by MetadataToken and inheritance level to sync the order with the port order (make sure FieldDrawers are next to the correct port) return fields.OrderByDescending( f => (long) (((GetFieldInheritanceLevel(f) << 32)) | (long) f.MetadataToken)); } protected BaseNode() { inputPorts = new NodeInputPortContainer(this); outputPorts = new NodeOutputPortContainer(this); InitializeInOutDatas(); } /// /// Update all ports of the node /// public bool UpdateAllPorts() { bool changed = false; foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) { var field = nodeFields[key.Name]; changed |= UpdatePortsForField(field.fieldName); } return changed; } /// /// Update all ports of the node without updating the connected ports. Only use this method when you need to update all the nodes ports in your graph. /// public bool UpdateAllPortsLocal() { bool changed = false; foreach (var key in OverrideFieldOrder(nodeFields.Values.Select(k => k.info))) { var field = nodeFields[key.Name]; changed |= UpdatePortsForFieldLocal(field.fieldName); } return changed; } /// /// Update the ports related to one C# property field (only for this node) /// /// public bool UpdatePortsForFieldLocal(string fieldName, bool sendPortUpdatedEvent = true) { bool changed = false; if (!nodeFields.ContainsKey(fieldName)) return false; var fieldInfo = nodeFields[fieldName]; if (!HasCustomBehavior(fieldInfo)) return false; List finalPorts = new List(); var portCollection = fieldInfo.input ? (NodePortContainer) inputPorts : outputPorts; // Gather all fields for this port (before to modify them) var nodePorts = portCollection.Where(p => p.fieldName == fieldName); // Gather all edges connected to these fields: var edges = nodePorts.SelectMany(n => n.GetEdges()).ToList(); if (fieldInfo.behavior != null) { foreach (var portData in fieldInfo.behavior(edges)) AddPortData(portData); } else { var customPortTypeBehavior = customPortTypeBehaviorMap[fieldInfo.info.FieldType]; foreach (var portData in customPortTypeBehavior(fieldName, fieldInfo.name, fieldInfo.info.GetValue(this))) AddPortData(portData); } void AddPortData(PortData portData) { var port = nodePorts.FirstOrDefault(n => n.portData.identifier == portData.identifier); // Guard using the port identifier so we don't duplicate identifiers if (port == null) { AddPort(fieldInfo.input, fieldName, portData); changed = true; } else { // in case the port type have changed for an incompatible type, we disconnect all the edges attached to this port if (!BaseGraph.TypesAreConnectable(port.portData.displayType, portData.displayType)) { foreach (var edge in port.GetEdges().ToList()) graph.Disconnect(edge.GUID); } // patch the port data if (port.portData != portData) { port.portData.CopyFrom(portData); changed = true; } } finalPorts.Add(portData.identifier); } // TODO // Remove only the ports that are no more in the list if (nodePorts != null) { var currentPortsCopy = nodePorts.ToList(); foreach (var currentPort in currentPortsCopy) { // If the current port does not appear in the list of final ports, we remove it if (!finalPorts.Any(id => id == currentPort.portData.identifier)) { RemovePort(fieldInfo.input, currentPort); changed = true; } } } // Make sure the port order is correct: portCollection.Sort((p1, p2) => { int p1Index = finalPorts.FindIndex(id => p1.portData.identifier == id); int p2Index = finalPorts.FindIndex(id => p2.portData.identifier == id); if (p1Index == -1 || p2Index == -1) return 0; return p1Index.CompareTo(p2Index); }); if (sendPortUpdatedEvent) onPortsUpdated?.Invoke(fieldName); return changed; } bool HasCustomBehavior(NodeFieldInformation info) { if (info.behavior != null) return true; if (customPortTypeBehaviorMap.ContainsKey(info.info.FieldType)) return true; return false; } /// /// Update the ports related to one C# property field and all connected nodes in the graph /// /// public bool UpdatePortsForField(string fieldName, bool sendPortUpdatedEvent = true) { bool changed = false; fieldsToUpdate.Clear(); updatedFields.Clear(); fieldsToUpdate.Push(new PortUpdate {fieldNames = new List() {fieldName}, node = this}); // Iterate through all the ports that needs to be updated, following graph connection when the // port is updated. This is required ton have type propagation multiple nodes that changes port types // are connected to each other (i.e. the relay node) while (fieldsToUpdate.Count != 0) { var (fields, node) = fieldsToUpdate.Pop(); // Avoid updating twice a port if (updatedFields.Any((t) => t.node == node && fields.SequenceEqual(t.fieldNames))) continue; updatedFields.Add(new PortUpdate {fieldNames = fields, node = node}); foreach (var field in fields) { if (node.UpdatePortsForFieldLocal(field, sendPortUpdatedEvent)) { foreach (var port in node.IsFieldInput(field) ? (NodePortContainer) node.inputPorts : node.outputPorts) { if (port.fieldName != field) continue; foreach (var edge in port.GetEdges()) { var edgeNode = (node.IsFieldInput(field)) ? edge.outputNode : edge.inputNode; var fieldsWithBehavior = edgeNode.nodeFields.Values.Where(f => HasCustomBehavior(f)) .Select(f => f.fieldName).ToList(); fieldsToUpdate.Push(new PortUpdate {fieldNames = fieldsWithBehavior, node = edgeNode}); } } changed = true; } } } return changed; } HashSet portUpdateHashSet = new HashSet(); internal void DisableInternal() { // port containers are initialized in the OnEnable inputPorts.Clear(); outputPorts.Clear(); ExceptionToLog.Call(() => Disable()); } internal void DestroyInternal() => ExceptionToLog.Call(() => Destroy()); /// /// Called only when the node is created, not when instantiated /// public virtual void OnNodeCreated() => GUID = Guid.NewGuid().ToString(); public virtual FieldInfo[] GetNodeFields() => GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); void InitializeInOutDatas() { var fields = GetNodeFields(); var methods = GetType().GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); foreach (var field in fields) { var inputAttribute = field.GetCustomAttribute(); var outputAttribute = field.GetCustomAttribute(); var tooltipAttribute = field.GetCustomAttribute(); var showInInspector = field.GetCustomAttribute(); var vertical = field.GetCustomAttribute(); bool isMultiple = false; bool input = false; string name = field.Name; string tooltip = null; if (showInInspector != null) _needsInspector = true; if (inputAttribute == null && outputAttribute == null) continue; //check if field is a collection type isMultiple = (inputAttribute != null) ? inputAttribute.allowMultiple : (outputAttribute.allowMultiple); input = inputAttribute != null; tooltip = tooltipAttribute?.tooltip; if (!String.IsNullOrEmpty(inputAttribute?.name)) name = inputAttribute.name; if (!String.IsNullOrEmpty(outputAttribute?.name)) name = outputAttribute.name; // By default we set the behavior to null, if the field have a custom behavior, it will be set in the loop just below nodeFields[field.Name] = new NodeFieldInformation(field, name, input, isMultiple, tooltip, vertical != null, null); } foreach (var method in methods) { var customPortBehaviorAttribute = method.GetCustomAttribute(); CustomPortBehaviorDelegate behavior = null; if (customPortBehaviorAttribute == null) continue; // Check if custom port behavior function is valid try { var referenceType = typeof(CustomPortBehaviorDelegate); behavior = (CustomPortBehaviorDelegate) Delegate.CreateDelegate(referenceType, this, method, true); } catch { Debug.LogError("The function " + method + " cannot be converted to the required delegate format: " + typeof(CustomPortBehaviorDelegate)); } if (nodeFields.ContainsKey(customPortBehaviorAttribute.fieldName)) nodeFields[customPortBehaviorAttribute.fieldName].behavior = behavior; else Debug.LogError("Invalid field name for custom port behavior: " + method + ", " + customPortBehaviorAttribute.fieldName); } } #endregion #region Events and Processing public void OnEdgeConnected(SerializableEdge edge) { bool input = edge.inputNode == this; NodePortContainer portCollection = (input) ? (NodePortContainer) inputPorts : outputPorts; portCollection.Add(edge); UpdateAllPorts(); onAfterEdgeConnected?.Invoke(edge); } protected virtual bool CanResetPort(NodePort port) => true; public void OnEdgeDisconnected(SerializableEdge edge) { if (edge == null) return; bool input = edge.inputNode == this; NodePortContainer portCollection = (input) ? (NodePortContainer) inputPorts : outputPorts; portCollection.Remove(edge); // Reset default values of input port: bool haveConnectedEdges = edge.inputNode.inputPorts.Where(p => p.fieldName == edge.inputFieldName) .Any(p => p.GetEdges().Count != 0); if (edge.inputNode == this && !haveConnectedEdges && CanResetPort(edge.inputPort)) edge.inputPort?.ResetToDefault(); UpdateAllPorts(); onAfterEdgeDisconnected?.Invoke(edge); } public void OnEnter() { } public void OnLeave() { } protected virtual void Enter() { } protected virtual void Leave() { } public void OnProcess() { inputPorts.PullDatas(); ExceptionToLog.Call(() => Process()); InvokeOnProcessed(); outputPorts.PushDatas(); } public void InvokeOnProcessed() => onProcessed?.Invoke(); /// /// Called when the node is enabled /// protected virtual void Enable() { } /// /// Called when the node is disabled /// protected virtual void Disable() { } /// /// Called when the node is removed /// protected virtual void Destroy() { } /// /// Override this method to implement custom processing /// protected virtual void Process() { } #endregion #region API and utils /// /// Add a port /// /// is input port /// C# field name /// Data of the port public void AddPort(bool input, string fieldName, PortData portData) { // Fixup port data info if needed: if (portData.displayType == null) portData.displayType = nodeFields[fieldName].info.FieldType; if (input) inputPorts.Add(new NodePort(this, fieldName, portData)); else outputPorts.Add(new NodePort(this, fieldName, portData)); } /// /// Remove a port /// /// is input port /// the port to delete public void RemovePort(bool input, NodePort port) { if (input) inputPorts.Remove(port); else outputPorts.Remove(port); } /// /// Remove port(s) from field name /// /// is input /// C# field name public void RemovePort(bool input, string fieldName) { if (input) inputPorts.RemoveAll(p => p.fieldName == fieldName); else outputPorts.RemoveAll(p => p.fieldName == fieldName); } /// /// Get all the nodes connected to the input ports of this node /// /// an enumerable of node public IEnumerable GetInputNodes() { foreach (var port in inputPorts) foreach (var edge in port.GetEdges()) yield return edge.outputNode; } /// /// Get all the nodes connected to the output ports of this node /// /// an enumerable of node public IEnumerable GetOutputNodes() { foreach (var port in outputPorts) foreach (var edge in port.GetEdges()) yield return edge.inputNode; } public List GetOutputNodesForList(string name=null) { List allNode = new List(); foreach (var port in outputPorts) { if (name != null && port.fieldName != name) { continue; } foreach (var edge in port.GetEdges()) { allNode.Add(edge.inputNode); } } return allNode; } /// /// Return a node matching the condition in the dependencies of the node /// /// Condition to choose the node /// Matched node or null public BaseNode FindInDependencies(Func condition) { Stack dependencies = new Stack(); dependencies.Push(this); int depth = 0; while (dependencies.Count > 0) { var node = dependencies.Pop(); // Guard for infinite loop (faster than a HashSet based solution) depth++; if (depth > 2000) break; if (condition(node)) return node; foreach (var dep in node.GetInputNodes()) dependencies.Push(dep); } return null; } /// /// Get the port from field name and identifier /// /// C# field name /// Unique port identifier /// public NodePort GetPort(string fieldName, string identifier) { return inputPorts.Concat(outputPorts).FirstOrDefault(p => { var bothNull = String.IsNullOrEmpty(identifier) && String.IsNullOrEmpty(p.portData.identifier); return p.fieldName == fieldName && (bothNull || identifier == p.portData.identifier); }); } /// /// Return all the ports of the node /// /// public IEnumerable GetAllPorts() { foreach (var port in inputPorts) yield return port; foreach (var port in outputPorts) yield return port; } /// /// Return all the connected edges of the node /// /// public IEnumerable GetAllEdges() { foreach (var port in GetAllPorts()) foreach (var edge in port.GetEdges()) yield return edge; } /// /// Is the port an input /// /// /// public bool IsFieldInput(string fieldName) => nodeFields[fieldName].input; /// /// Add a message on the node /// /// /// public void AddMessage(string message, NodeMessageType messageType) { if (messages.Contains(message)) return; onMessageAdded?.Invoke(message, messageType); messages.Add(message); } /// /// Remove a message on the node /// /// public void RemoveMessage(string message) { onMessageRemoved?.Invoke(message); messages.Remove(message); } /// /// Remove a message that contains /// /// public void RemoveMessageContains(string subMessage) { string toRemove = messages.Find(m => m.Contains(subMessage)); messages.Remove(toRemove); onMessageRemoved?.Invoke(toRemove); } /// /// Remove all messages on the node /// public void ClearMessages() { foreach (var message in messages) onMessageRemoved?.Invoke(message); messages.Clear(); } /// /// Set the custom name of the node. This is intended to be used by renamable nodes. /// This custom name will be serialized inside the node. /// /// New name of the node. public void SetCustomName(string customName) => nodeCustomName = customName; /// /// Get the name of the node. If the node have a custom name (set using the UI by double clicking on the node title) then it will return this name first, otherwise it returns the value of the name field. /// /// The name of the node as written in the title public string GetCustomName() => String.IsNullOrEmpty(nodeCustomName) ? name : nodeCustomName; #endregion } }