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