// 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.Linq; using Doozy.Runtime.Common.Extensions; using Doozy.Runtime.Common.Utils; using Doozy.Runtime.Nody.Nodes.System; using Doozy.Runtime.UIManager.ScriptableObjects; using UnityEngine; using UnityEngine.Events; // ReSharper disable MemberCanBeProtected.Global // ReSharper disable UnusedMember.Global // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable PartialTypeWithSinglePart // ReSharper disable ClassWithVirtualMembersNeverInherited.Global namespace Doozy.Runtime.Nody { /// Scriptable object class used a container for nodes [CreateAssetMenu(menuName = "Doozy/Flow Graph", fileName = "Flow Graph", order = -1000)] public partial class FlowGraph : ScriptableObject { /// Pointer to the UIManagerInputSettings instance public static UIManagerInputSettings inputSettings => UIManagerInputSettings.instance; /// True is Multiplayer Mode is enabled public static bool multiplayerMode => inputSettings.multiplayerMode; /// Default player index value (used for global user) public static int defaultPlayerIndex => inputSettings.defaultPlayerIndex; [SerializeField] public Vector3 EditorPosition = Vector3.zero; /// Used by the editor to keep track of the graph position inside the Nody window internal Vector3 editorPosition { get => EditorPosition; set => EditorPosition = value; } [SerializeField] public Vector3 EditorScale = Vector3.one; /// Used by the editor to keep track of the graph scale (zoom) inside the Nody window internal Vector3 editorScale { get => EditorScale; set => EditorScale = value; } [SerializeField] private string Id; /// Flow Graph Id public string id { get => Id; set => Id = value; } [SerializeField] private string GraphName; /// Name of the graph public string graphName { get => GraphName; set => GraphName = value; } [SerializeField] private string GraphDescription; /// Description for the graph public string graphDescription { get => GraphDescription; set => GraphDescription = value; } [SerializeField] private bool IsSubGraph; /// Flag used to mark the graph as a sub-graph public bool isSubGraph { get => IsSubGraph; set => IsSubGraph = value; } [SerializeField] protected GraphState GraphState = GraphState.Idle; /// Current graph state public GraphState graphState { get => GraphState; set { GraphState = value; OnStateChanged?.Invoke(value); } } /// Called every time the graph changes its state public GraphStateEvent OnStateChanged = new GraphStateEvent(); /// Called when the flow graph starts or restarts public UnityEvent OnStart = new UnityEvent(); /// Called when the flow graph is stopped public UnityEvent OnStop = new UnityEvent(); /// Called when the flow graph is paused public UnityEvent OnPause = new UnityEvent(); /// Called when the flow graph is resumed public UnityEvent OnResume = new UnityEvent(); /// Called when the flow graph is reset public UnityEvent OnBackFlow = new UnityEvent(); [SerializeField] private List Nodes; /// All the nodes in the graph public List nodes { get => Nodes; private set => Nodes = value; } /// All the global nodes in the graph public IEnumerable globalNodes => Nodes.Where(node => node.nodeType == NodeType.Global); [SerializeField] private FlowNode RootNode; /// /// The first node that becomes active. /// If this is a graph it will be a Start Node /// Id this is a sub-graph it will be an Enter Node public FlowNode rootNode { get => RootNode; set => RootNode = value; } [SerializeField] private FlowNode ActiveNode; /// The node that is currently active public FlowNode activeNode { get => ActiveNode; private set => ActiveNode = value; } /// The node that was previously active public FlowNode previousActiveNode { get; private set; } /// The port that lead to the previously active node public FlowPort previousActivePort { get; private set; } /// The sub-graph that is currently active (can be null) public FlowGraph activeSubGraph { get; private set; } /// The parent graph that contains this graph (if this is a sub-graph) (can be null) public FlowGraph parentGraph { get; private set; } /// All the input ports in the graph public List inputPorts { get { var list = new List(); foreach (FlowNode node in Nodes) list.AddRange(node.inputPorts); return list; } } /// All the output ports in the graph public List outputPorts { get { var list = new List(); foreach (FlowNode node in Nodes) list.AddRange(node.outputPorts); return list; } } /// All the ports in the graph (input and output) public List ports { get { var list = new List(); foreach (FlowNode node in Nodes) { list.AddRange(node.inputPorts); list.AddRange(node.outputPorts); } return list; } } /// Current controller for the graph public FlowController controller { get; internal set; } /// Construct a new FlowGraph public FlowGraph() { Id = Guid.NewGuid().ToString(); GraphName = ObjectNames.NicifyVariableName(nameof(FlowGraph)); Nodes = new List(); } /// Reset the graph editor settings used inside the Nody window internal void ResetEditorSettings() { EditorPosition = Vector3.zero; EditorScale = Vector3.one; } /// Reset the graph public void ResetGraph() { ClearHistory(); previousActiveNode = null; activeNode = null; nodes.ForEach(n => n.ResetNode()); CleanGraph(); graphState = GraphState.Idle; } private void CleanGraph() { nodes.ForEach(n => { foreach (FlowPort inputPort in n.inputPorts) foreach (string otherPortId in inputPort.connections.ToList() .Where(otherPortId => GetPortById(otherPortId) == null)) inputPort.connections.Remove(otherPortId); foreach (FlowPort outputPort in n.outputPorts) foreach (string otherPortId in outputPort.connections.ToList() .Where(otherPortId => GetPortById(otherPortId) == null)) outputPort.connections.Remove(otherPortId); }); } /// Activate the given node /// Target node (next active node) /// Port that activated this node (can be null) public void SetActiveNode(FlowNode node, FlowPort fromPort = null) { if (node == null) return; if (activeNode != null) activeNode.OnExit(); history.Push(new GraphHistory(previousActiveNode, previousActivePort, activeNode)); previousActiveNode = activeNode; previousActivePort = fromPort; activeNode = node; activeNode.OnEnter(); activeNode.Ping(FlowDirection.Forward); fromPort?.Ping(FlowDirection.Forward); } /// Go back to the previous node (if possible) by activating the node in the graph public void GoBack() => GoBack(defaultPlayerIndex); /// Go back to the previous node (if possible) by activating the node in the graph /// Player index public void GoBack(int playerIndex) { if (history.Count == 0) return; //cannot go back //node type check -> block going back to special nodes switch (previousActiveNode) { case StartNode _: case EnterNode _: return; } if ( multiplayerMode && //check if multiplayer mode is enabled playerIndex != defaultPlayerIndex && //check if default player index -> ignore player index controller.hasMultiplayerInfo && //check if the controller is bound to a player index playerIndex != controller.multiplayerInfo.playerIndex //check player index ) return; tempNodesSet.Clear(); tempPortsSet.Clear(); tempPortsSet.Add(previousActivePort); if (history.All(item => item.activeNode.passthrough)) return; //cannot go back as there is no non-passthrough node in history GraphHistory peek = history.Peek(); //multiple entries of the same node in history and passthrough check if (peek.activeNode == activeNode | peek.activeNode.passthrough) //we may have 1 or more nodes that are passthrough { while (history.Count > 0) //iterate through history { peek = history.Peek(); if (peek.activeNode == activeNode) //we found the node we are currently clear the pings for the visual need to dig deeper { tempNodesSet.Clear(); //don't ping nodes tempPortsSet.Clear(); //don't ping ports history.Pop(); continue; } if (peek.activeNode.passthrough) //check if the node is passthrough { // tempNodesSet.Add(peek.activeNode); //save node to be able to ping it (visuals matter) tempPortsSet.Add(peek.previousActivePort); //save port to be able to ping it (visuals matter) history.Pop(); //pop history and go to the next entry continue; } break; //found node that is NOT passthrough -> prepare to activate it } } if (history.Count == 0) return; //cannot go back if (activeNode != null) activeNode.OnExit(); //ping nodes tempNodesSet.Remove(null); //remove null foreach (FlowNode flowNode in tempNodesSet) flowNode.Ping(FlowDirection.Back); //ping ports tempPortsSet.Remove(null); //remove null foreach (FlowPort flowPort in tempPortsSet) flowPort.Ping(FlowDirection.Back); previousActiveNode = history.Peek().previousActiveNode; previousActivePort = history.Peek().previousActivePort; activeNode = history.Peek().activeNode; history.Pop(); activeNode.OnEnter(); tempNodesSet.Clear(); tempPortsSet.Clear(); OnBackFlow?.Invoke(); } /// Activate the first node with the given node name /// Node name to search for public void SetActiveNodeByNodeName(string nodeName) => SetActiveNode(GetNodeByName(nodeName)); /// Activate the node with the given node id /// Node id to search for public void SetActiveNodeByNodeId(string nodeId) => SetActiveNode(GetNodeById(nodeId)); /// /// Restart the graph. This will reset the graph and activate the first node. /// Even if the graph is paused, it will reset and start from the beginning. /// public void Restart() { ResetGraph(); UpdateNodes(); StartGlobalNodes(); SetActiveNode(RootNode); graphState = GraphState.Running; OnStart?.Invoke(); } /// /// Start the graph. This will reset the graph and activate the first node. /// If the graph is paused, it will resume from the last active node instead of the first node. /// public void Start() { if (graphState == GraphState.Paused) { Resume(); //graph is paused -> resume return; } ResetGraph(); UpdateNodes(); StartGlobalNodes(); SetActiveNode(RootNode); graphState = GraphState.Running; OnStart?.Invoke(); } /// /// Resume the graph. /// If the graph state is Idle (stopped), it will start the graph instead. /// public void Resume() { if (graphState == GraphState.Idle) { Start(); //graph is not paused -> start the graph return; } graphState = GraphState.Running; //set graph state to running StartGlobalNodes(); //start global nodes if (activeNode != null) //check if there is an active node activeNode.OnUnPaused(); //resume the active node OnResume?.Invoke(); //invoke graph resumed event } /// /// Pause the graph. /// If the graph state is not Running, it will do nothing. /// public void Pause() { if (graphState != GraphState.Running) //check if the graph is running return; //graph is not running -> do nothing graphState = GraphState.Paused; //set graph state to paused StopGlobalNodes(); //stop global nodes if (activeNode != null) //check if there is an active node activeNode.OnPaused(); //pause the active node OnPause?.Invoke(); //invoke graph paused event } /// Toggle the graph state between running and paused /// If true, the graph will be paused, otherwise it will be resumed public void SetPaused(bool paused) { if (paused) { Pause(); return; } Resume(); } /// Stop the graph public void Stop() { StopGlobalNodes(); //stop global nodes if (activeNode != null) //check if there is an active node activeNode.OnExit(); //deactivate the active node activeNode = null; //set active node to null graphState = GraphState.Idle; //set graph state to idle OnStop?.Invoke(); //invoke graph stopped event } /// Start all the global nodes inside the graph public void StartGlobalNodes() { // ReSharper disable once ForCanBeConvertedToForeach for (int i = 0; i < nodes.Count; i++) //iterate through nodes { FlowNode node = nodes[i]; //get node if (node.nodeType != NodeType.Global) continue; //check if the node is global node.Start(); //start the node } if (activeSubGraph != null) //check if there is an active sub graph activeSubGraph.StartGlobalNodes(); //start global nodes in the active sub graph } /// Stop all the global nodes inside the graph public virtual void StopGlobalNodes() { // ReSharper disable once ForCanBeConvertedToForeach for (int i = 0; i < nodes.Count; i++) //iterate through nodes { FlowNode node = nodes[i]; //get node if (node.nodeType != NodeType.Global) continue; //check if the node is global node.Stop(); //stop the node } if (activeSubGraph != null) //check if there is an active sub graph activeSubGraph.StopGlobalNodes(); //stop global nodes in the active sub graph } /// FixedUpdate is called every fixed framerate frame, if this flow has been loaded by a controller public void FixedUpdate() { if (graphState != GraphState.Running) //check if the graph is running return; //graph is not running -> return if (activeNode != null && activeNode.runFixedUpdate) //check if the active node is not null and if it should run fixed update activeNode.FixedUpdate(); //run fixed update if (activeSubGraph != null) //check if the active sub graph is not null activeSubGraph.FixedUpdate(); //run fixed update // ReSharper disable once ForCanBeConvertedToForeach for (int i = 0; i < nodes.Count; i++) //iterate through nodes { FlowNode node = nodes[i]; //get node if (node.nodeType != NodeType.Global) continue; //check if node is global if (!node.runFixedUpdate) continue; //check if node should run fixed update node.FixedUpdate(); //run fixed update } } /// LateUpdate is called every frame, after all Update functions have been called and if this flow has been loaded by a controller public void LateUpdate() { if (graphState != GraphState.Running) //check if the graph is running return; //graph is not running -> return if (activeNode != null && activeNode.runLateUpdate) //check if the active node is running activeNode.LateUpdate(); //run the active node if (activeSubGraph != null) //check if the active subgraph is running activeSubGraph.LateUpdate(); //run the active subgraph // ReSharper disable once ForCanBeConvertedToForeach for (int i = 0; i < nodes.Count; i++) //iterate through all nodes { FlowNode node = nodes[i]; //get the current node if (node.nodeType != NodeType.Global) continue; //check if the node is global if (node.runLateUpdate == false) continue; //check if the node is running if (node.nodeState == NodeState.Idle) continue; //check if the node is idle node.LateUpdate(); //run the node } } /// Update is called every frame, if this flow has been loaded by a controller public void Update() { if (graphState != GraphState.Running) //check if the graph is running return; //graph is not running -> return if (activeNode != null && activeNode.runUpdate) //check if the active node is running activeNode.Update(); //run the active node if (activeSubGraph != null) //check if the active subgraph is running activeSubGraph.Update(); //run the active subgraph // ReSharper disable once ForCanBeConvertedToForeach for (int i = 0; i < nodes.Count; i++) //iterate through all nodes { FlowNode node = nodes[i]; //get the current node if (node.nodeType != NodeType.Global) continue; //check if the node is global if (node.runUpdate == false) continue; //check if the node is running if (node.nodeState == NodeState.Idle) continue; //check if the node is idle node.Update(); //run the node } } /// Refresh all the references for the graph's nodes public void UpdateNodes() { Nodes.RemoveNulls(); // ReSharper disable once ForCanBeConvertedToForeach for (int i = 0; i < nodes.Count; i++) //iterate through all nodes nodes[i].SetFlowGraph(this); //set the flow graph for the node } /// Create a clone of this graph /// The cloned graph public FlowGraph Clone() { FlowGraph flowClone = Instantiate(this); flowClone.RootNode = RootNode.Clone().SetFlowGraph(flowClone); flowClone.nodes = nodes.ConvertAll(n => n.Clone()); flowClone.UpdateNodes(); return flowClone; } /// True if the graph contains the given node /// Node to search for public bool ContainsNode(FlowNode node) => node != null && nodes.Contains(node); /// True if the graph contains a node with the given id /// Node id to search for public bool ContainsNodeById(string nodeId) => nodes.Any(node => node.nodeId.Equals(nodeId)); /// True if the graph contains a node with the given node name /// Node name to search for public bool ContainsNodeByName(string nodeName) => nodes.Any(node => node.nodeName.Equals(nodeName)); /// /// Get the StartNode if this is not a sub graph. /// If not found, this method returns null /// public StartNode GetStartNode() => (StartNode)nodes.FirstOrDefault(n => n is StartNode); /// /// Get the EnterNode if this is a sub graph. /// If not found, this method returns null /// public EnterNode GetEnterNode() => (EnterNode)nodes.FirstOrDefault(n => n is EnterNode); /// /// Get the ExitNode if this is a sub graph. /// If not found, this method returns null /// public ExitNode GetExitNode() => (ExitNode)nodes.FirstOrDefault(n => n is ExitNode); /// /// Get the first node with the given node name. /// If one is not found, this method returns null /// /// Node name to search for public FlowNode GetNodeByName(string nodeName) => nodes.FirstOrDefault(node => node.nodeName.Equals(nodeName)); /// /// Get the node with the given node id. /// If one is not found, this method returns null /// /// Node id to search for public FlowNode GetNodeById(string nodeId) => nodes.FirstOrDefault(node => node.nodeId.Equals(nodeId)); /// Get all the nodes of the given type /// Type of node public List GetNodeByType() where T : FlowNode => (List)nodes.Where(node => node is T); /// /// Get the port with the given port id. /// If one is not found, this method returns null /// /// Port id to search for public FlowPort GetPortById(string portId) => nodes.Select(node => node.GetPortFromId(portId)).FirstOrDefault(port => port != null); #region Graph History and Cats private Stack history { get; set; } private HashSet tempNodesSet { get; set; } private HashSet tempPortsSet { get; set; } // |\__/,| (`\ // _.|o o |_ ) ) //-(((---(((-------- /// /// Clear graph history and remove the possibility of being able to go back to previously active nodes /// public void ClearHistory() { history ??= new Stack(); tempNodesSet ??= new HashSet(); tempPortsSet ??= new HashSet(); history.Clear(); } // /) // /\___/\ (( // \`@_@'/ )) // {_:Y:.}_// //-------{_}^-'{_}--------- private struct GraphHistory { public FlowNode previousActiveNode { get; set; } public FlowPort previousActivePort { get; set; } public FlowNode activeNode { get; set; } public GraphHistory(FlowNode previousActiveNode, FlowPort previousActivePort, FlowNode activeNode) { this.previousActiveNode = previousActiveNode; this.previousActivePort = previousActivePort; this.activeNode = activeNode; } } #endregion } }