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