// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.UIManager.ScriptableObjects; using UnityEngine; using UnityEngine.Events; namespace Doozy.Runtime.Nody { /// Base class for all nodes in Nody system [Serializable] public abstract class FlowNode : ScriptableObject { /// True is Multiplayer Mode is enabled public bool multiplayerMode => UIManagerInputSettings.instance.multiplayerMode & flowGraph.controller.hasMultiplayerInfo; /// Default player index value (used for global user) public int playerIndex => flowGraph.controller.playerIndex; [SerializeField] private string FlowGraphId; /// Flow Graph Id this node belongs to public string flowGraphId { get => FlowGraphId; internal set => FlowGraphId = value; } /// Reference to the flow graph this node belongs to public FlowGraph flowGraph { get; internal set; } [SerializeField] private string NodeId; /// Node Id public string nodeId { get => NodeId; internal set => NodeId = value; } [SerializeField] private NodeType NodeType; /// Type of node public NodeType nodeType => NodeType; [SerializeField] private string NodeName; /// Name of this node public string nodeName { get => NodeName; internal set => NodeName = value; } [SerializeField] private string NodeDescription; /// Description for this node public string nodeDescription { get => NodeDescription; internal set => NodeDescription = value; } [SerializeField] private List InputPorts; /// All the input ports this node has public List inputPorts { get => InputPorts; internal set => InputPorts = value; } /// Get the first input port. If one does not exit, this returns null public FlowPort firstInputPort => inputPorts.FirstOrDefault(); /// Get the last input port. If one does not exit, this returns null public FlowPort lastInputPort => inputPorts.Last(); /// List of all the input port ids connected to this node public List inputConnections { get { var list = new List(); foreach (FlowPort port in inputPorts) list.AddRange(port.connections); return list; } } [SerializeField] private List OutputPorts; /// All the output ports this node has public List outputPorts { get => OutputPorts; internal set => OutputPorts = value; } /// Get the first output port. If one does not exit, this returns null public FlowPort firstOutputPort => outputPorts.FirstOrDefault(); /// Get the last output port. If one does not exit, this returns null public FlowPort lastOutputPort => outputPorts.Last(); /// List of all the output port ids connected to this node public List outputConnections { get { var list = new List(); foreach (FlowPort port in outputPorts) list.AddRange(port.connections); return list; } } /// All the ports this node had (input and output) public List ports { get { var list = new List(); list.AddRange(inputPorts); list.AddRange(outputPorts); return list; } } /// List of all the port ids connected to this node (input and output) public List connections { get { var list = new List(); list.AddRange(inputConnections); list.AddRange(outputConnections); return list; } } /// /// Minimum number of input ports this node can have. /// Value checked when deleting ports to prevent deletion of important ports /// public virtual int minNumberOfInputPorts => 0; /// /// Minimum number of output ports this node can have. /// Value checked when deleting ports to prevent deletion of important ports /// public virtual int minNumberOfOutputPorts => 0; [SerializeField] private bool RunUpdate; /// Run Update when the node is active (default: false) public bool runUpdate { get => RunUpdate; set => RunUpdate = value; } [SerializeField] private bool RunFixedUpdate; /// Run FixedUpdate when the node is active (default: false) public bool runFixedUpdate { get => RunFixedUpdate; set => RunFixedUpdate = value; } [SerializeField] private bool RunLateUpdate; /// Run LateUpdate when the node is active (default: false) public bool runLateUpdate { get => RunLateUpdate; set => RunLateUpdate = value; } [SerializeField] protected NodeState NodeState = NodeState.Idle; /// Current node state public NodeState nodeState { get => NodeState; set { NodeState = value; onStateChanged?.Invoke(value); } } /// Triggered every time the node changes its state public UnityAction onStateChanged { get; set; } [SerializeField] private bool CanBeDeleted; /// /// [Editor] True if this node can be deleted (default: true) /// Used to prevent special nodes from being deleted in the editor /// public bool canBeDeleted { get => CanBeDeleted; internal set => CanBeDeleted = value; } /// Show Passthrough On/Off switch in the editor (default: false) public virtual bool showPassthroughInEditor => false; [SerializeField] private bool Passthrough; /// Allow the graph to bypass this node when going back (default: true) public bool passthrough { get => Passthrough; set => Passthrough = value; } /// Show ClearGraphHistory On/Off switch in the editor (default: false) public virtual bool showClearGraphHistoryInEditor => false; [SerializeField] private bool ClearGraphHistory; /// OnEnter, clear graph history and remove the possibility of being able to go back to previously active nodes (default: false) public bool clearGraphHistory { get => ClearGraphHistory; set => ClearGraphHistory = value; } [SerializeField] private Vector2 Position = Vector2.zero; /// [Editor] Position of the node in the graph public Vector2 position { get => Position; internal set => Position = value; } /// [Editor] Ping this node public UnityAction ping { get; set; } /// [Editor] Refresh this node's editor public UnityAction refreshNodeEditor { get; set; } /// [Editor] Refresh this node's view public UnityAction refreshNodeView { get; set; } /// [Editor] Called when OnEnter is triggered public UnityAction onEnter { get; set; } /// [Editor] Called when OnExit is triggered public UnityAction onExit { get; set; } /// [Editor] Called when Start is triggered public UnityAction onStart { get; set; } /// [Editor] Called when Stop is triggered public UnityAction onStop { get; set; } /// [Editor] Called when the node is paused public UnityAction onPaused { get; set; } /// [Editor] Called when the node is unpaused public UnityAction onUnPaused { get; set; } /// Check if this node is idle public bool isIdle => nodeState == NodeState.Idle; /// Check if this node is running. This means that this is probably a global node. public bool isRunning => nodeState == NodeState.Running; /// Check if this node is active public bool isActive => nodeState == NodeState.Active; /// Check if this node is paused public bool isPaused => nodeState == NodeState.Paused; protected FlowNode(NodeType type) { FlowGraphId = string.Empty; NodeId = Guid.NewGuid().ToString(); NodeType = type; NodeName = ObjectNames.NicifyVariableName(GetType().Name.Replace("Node", "")); NodeDescription = string.Empty; InputPorts = new List(); OutputPorts = new List(); RunUpdate = false; RunFixedUpdate = false; RunLateUpdate = false; CanBeDeleted = true; Passthrough = true; ClearGraphHistory = false; } /// Called when the parent graph started and this is global node public virtual void Start() { nodeState = NodeState.Running; onStart?.Invoke(); } /// Called when the parent graph stopped and this is a global node public virtual void Stop() { nodeState = NodeState.Idle; onStop?.Invoke(); } /// /// Called on the frame when this node becomes active, /// just before any of the node's Update methods are called for the first time /// /// Source Node /// Source Port public virtual void OnEnter(FlowNode previousNode = null, FlowPort previousPort = null) { nodeState = NodeState.Active; onEnter?.Invoke(); if (clearGraphHistory) flowGraph.ClearHistory(); } /// Called just before this node becomes idle public virtual void OnExit() { nodeState = NodeState.Idle; onExit?.Invoke(); } /// Called when the node is paused public virtual void OnPaused() { if (!isActive) return; nodeState = NodeState.Paused; onPaused?.Invoke(); } /// Called when the node is unpaused public virtual void OnUnPaused() { if (!isPaused) return; nodeState = NodeState.Active; onUnPaused?.Invoke(); } /// Called every frame, if the node is enabled and active (Update Method) public virtual void Update() {} /// Called every frame, if the node is enabled and active (FixedUpdate Method) public virtual void FixedUpdate() {} /// Called every frame, if the node is enabled and active (LateUpdate Method) public virtual void LateUpdate() {} /// Clone this node public virtual FlowNode Clone() => Instantiate(this); /// Go to the next node, by accessing the first connection of the given output port /// Output port to activate protected virtual void GoToNextNode(FlowPort outputPort) { if (flowGraph == null) return; //graph null check FlowPort fromPort = outputPort; //get from port -> this node's target output port FlowPort toPort = flowGraph.GetPortById(fromPort.firstConnection); //get to port -> other node's target input port if (toPort == null) return; //next port null check if (toPort.node == null) return; //next node null check flowGraph.SetActiveNode(toPort.node, fromPort); //activate next node } public virtual void ResetNode() { nodeState = NodeState.Idle; } /// Add a new port to this node /// Port direction (Input/Output) - the port's connections direction /// Port capacity (Single/Multi) - the number of connections this port can have public virtual FlowPort AddPort(PortDirection direction, PortCapacity capacity) { FlowPort port = new FlowPort().SetNodeId(nodeId).SetDirection(direction).SetCapacity(capacity); switch (direction) { case PortDirection.Input: inputPorts.Add(port); break; case PortDirection.Output: outputPorts.Add(port); break; default: throw new ArgumentOutOfRangeException(nameof(direction), direction, null); } return port; } /// Add a new input port to this node /// Port capacity (Single/Multi) - the number of connections this port can have public virtual FlowPort AddInputPort(PortCapacity capacity = PortCapacity.Multi) => AddPort(PortDirection.Input, capacity); /// Add a new output port to this node /// Port capacity (Single/Multi) - the number of connections this port can have public virtual FlowPort AddOutputPort(PortCapacity capacity = PortCapacity.Single) => AddPort(PortDirection.Output, capacity); } [SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public static class FlowNodeExtensions { /// /// Get the input port with the given port id. /// If one is not found, this method returns null /// /// Target node /// Port id to search for public static FlowPort GetInputPortFromId(this T target, string portId) where T : FlowNode => target.inputPorts.FirstOrDefault(port => port.portId.Equals(portId)); /// /// Get the output port with the given port id. /// If one is not found, this method returns null /// /// Target node /// Port id to search for public static FlowPort GetOutputPortFromId(this T target, string portId) where T : FlowNode => target.outputPorts.FirstOrDefault(port => port.portId.Equals(portId)); /// /// Get the port with the given port id. It can be either an input or an output port. /// If one is not found, this method returns null /// /// Target node /// Port id to search for public static FlowPort GetPortFromId(this T target, string portId) where T : FlowNode => target.ports.FirstOrDefault(port => port.portId.Equals(portId)); /// True if this node has at least one connected port /// Target node public static bool IsConnected(this T target) where T : FlowNode => target.inputPorts.Any(p => p.isConnected) || target.outputPorts.Any(p => p.isConnected); /// True if this node is connected to a node that has a port with the given port id /// Target node /// Port id to search for public static bool IsConnectedToPort(this T target, string portId) where T : FlowNode => target.inputPorts.Any(port => port.IsConnectedToPort(portId)) || target.outputPorts.Any(port => port.IsConnectedToPort(portId)); /// True if the given port can be deleted from this node /// Target node /// Port id to search for public static bool CanDeletePort(this T target, string portId) where T : FlowNode { FlowPort port = target.GetPortFromId(portId); if (port == null) return false; //port node found -> false if (!port.canBeDeleted) return false; //port marked as do not delete -> false switch (port.direction) { case PortDirection.Input: if (target.inputPorts.Count <= target.minNumberOfInputPorts) return false; //deleting the port would invalidate the min number of ports -> false break; case PortDirection.Output: if (target.outputPorts.Count <= target.minNumberOfOutputPorts) return false; //deleting the port would invalidate the min number of ports -> false break; default: throw new ArgumentOutOfRangeException(); } return true; } /// True if the port was deleted from this node /// Target node /// Port id to search for public static bool DeletePort(this T target, string portId) where T : FlowNode { if (!target.CanDeletePort(portId)) return false; FlowPort port = target.GetPortFromId(portId); switch (port.direction) { case PortDirection.Input: target.inputPorts.Remove(port); break; case PortDirection.Output: target.outputPorts.Remove(port); break; default: throw new ArgumentOutOfRangeException(); } return true; } /// Set the flow graph for this node (also updates the node's flowGraphId) /// Target node /// Target flow graph public static T SetFlowGraph(this T target, FlowGraph flowGraph) where T : FlowNode { target.flowGraph = flowGraph; target.flowGraphId = flowGraph != null ? flowGraph.id : string.Empty; target.ports.ForEach(port => port.node = target); return target; } /// Set a new name for this node /// Target node /// New node name public static T SetNodeName(this T target, string nodeName) where T : FlowNode { target.nodeName = nodeName; return target; } /// Set a new description for this node /// Target node /// New node description public static T SetNodeDescription(this T target, string nodeDescription) where T : FlowNode { target.nodeDescription = nodeDescription; return target; } /// [Editor] Set the position of the node in the graph /// Target node /// Position in graph public static T SetPosition(this T target, Vector2 position) where T : FlowNode { target.position = position; return target; } /// [Editor] Ping node /// Target node /// Flow direction (back flow is when returning to the previous node) ( public static T Ping(this T target, FlowDirection flowDirection) where T : FlowNode { target.ping?.Invoke(flowDirection); return target; } /// [Editor] Refresh node's editor /// Target node public static T RefreshNodeEditor(this T target) where T : FlowNode { target.refreshNodeEditor?.Invoke(); return target; } /// [Editor] Refresh node's view /// Target node public static T RefreshNodeView(this T target) where T : FlowNode { target.refreshNodeView?.Invoke(); return target; } } }