using System.Linq;
using System.Collections.Generic;
using NodeCanvas.Framework;
using ParadoxNotion.Design;
using UnityEngine;
using Logger = ParadoxNotion.Services.Logger;
using ParadoxNotion;
namespace NodeCanvas.StateMachines
{
/// Use FSMs to create state like behaviours
[GraphInfo(
packageName = "NodeCanvas",
docsURL = "https://nodecanvas.paradoxnotion.com/documentation/",
resourcesURL = "https://nodecanvas.paradoxnotion.com/downloads/",
forumsURL = "https://nodecanvas.paradoxnotion.com/forums-page/"
)]
[CreateAssetMenu(menuName = "ParadoxNotion/NodeCanvas/FSM Asset")]
public class FSM : Graph
{
///Transition Calling Mode (see "EnterState")
public enum TransitionCallMode
{
Normal = 0,
Stacked = 1,
Clean = 2,
}
private List updatableNodes;
private IStateCallbackReceiver[] callbackReceivers;
private Stack stateStack;
private bool enterStartStateFlag;
public event System.Action onStateEnter;
public event System.Action onStateUpdate;
public event System.Action onStateExit;
public event System.Action onStateTransition;
///The current FSM state
public FSMState currentState { get; private set; }
///The previous FSM state
public FSMState previousState { get; private set; }
///The current state name. Null if none
public string currentStateName => currentState != null ? currentState.name : null;
///The previous state name. Null if none
public string previousStateName => previousState != null ? previousState.name : null;
public override System.Type baseNodeType => typeof(FSMNode);
public override bool requiresAgent => true;
public override bool requiresPrimeNode => true;
public override bool isTree => false;
public override bool allowBlackboardOverrides => true;
sealed public override bool canAcceptVariableDrops => false;
public sealed override PlanarDirection flowDirection => PlanarDirection.Auto;
///----------------------------------------------------------------------------------------------
protected override void OnGraphInitialize() {
//we may be loading in async
ThreadSafeInitCall(GatherCallbackReceivers);
updatableNodes = new List();
for ( var i = 0; i < allNodes.Count; i++ ) {
if ( allNodes[i] is IUpdatable ) {
updatableNodes.Add((IUpdatable)allNodes[i]);
}
}
}
protected override void OnGraphStarted() {
stateStack = new Stack();
enterStartStateFlag = true;
}
protected override void OnGraphUpdate() {
if ( enterStartStateFlag ) {
//use a flag so that other nodes can do stuff on graph started
enterStartStateFlag = false;
EnterState((FSMState)primeNode, TransitionCallMode.Normal);
}
if ( currentState != null ) {
//Update defer IUpdatables
for ( var i = 0; i < updatableNodes.Count; i++ ) {
updatableNodes[i].Update();
}
//this can only happen if FSM stoped just now (from the above update)
if ( currentState == null ) { Stop(false); return; }
//Update current state
currentState.Execute(agent, blackboard);
//this can only happen if FSM stoped just now (from the above update)
if ( currentState == null ) { Stop(false); return; }
if ( onStateUpdate != null && currentState.status == Status.Running ) {
onStateUpdate(currentState);
}
//this can only happen if FSM stoped just now (from the above update)
if ( currentState == null ) { Stop(false); return; }
//state has nowhere to go..
if ( currentState.status != Status.Running && currentState.outConnections.Count == 0 ) {
//...but we have a stacked state -> pop return to it
if ( stateStack.Count > 0 ) {
var popState = stateStack.Pop();
EnterState(popState, TransitionCallMode.Normal);
return;
}
//...and no updatables -> stop
if ( !updatableNodes.Any(n => n.status == Status.Running) ) {
Stop(true);
return;
}
}
}
//if null state, stop.
if ( currentState == null ) {
Stop(false);
return;
}
}
protected override void OnGraphStoped() {
if ( currentState != null ) {
if ( onStateExit != null ) {
onStateExit(currentState);
}
}
previousState = null;
currentState = null;
stateStack = null;
}
///Enter a state providing the state itself
public bool EnterState(FSMState newState, TransitionCallMode callMode) {
if ( !isRunning ) {
Logger.LogWarning("Tried to EnterState on an FSM that was not running", LogTag.EXECUTION, this);
return false;
}
if ( newState == null ) {
Logger.LogWarning("Tried to Enter Null State", LogTag.EXECUTION, this);
return false;
}
if ( currentState != null ) {
if ( onStateExit != null ) { onStateExit(currentState); }
currentState.Reset(false);
if ( callMode == TransitionCallMode.Stacked ) {
stateStack.Push(currentState);
if ( stateStack.Count > 5 ) {
Logger.LogWarning("State stack exceeds 5. Ensure that you are not cycling stack calls", LogTag.EXECUTION, this);
}
}
}
if ( callMode == TransitionCallMode.Clean ) {
stateStack.Clear();
}
previousState = currentState;
currentState = newState;
if ( onStateTransition != null ) { onStateTransition(currentState); }
if ( onStateEnter != null ) { onStateEnter(currentState); }
currentState.Execute(agent, blackboard);
return true;
}
///Trigger a state to enter by it's name. Returns the state found and entered if any
public FSMState TriggerState(string stateName, TransitionCallMode callMode) {
var state = GetStateWithName(stateName);
if ( state != null ) {
EnterState(state, callMode);
return state;
}
Logger.LogWarning("No State with name '" + stateName + "' found on FSM '" + name + "'", LogTag.EXECUTION, this);
return null;
}
///Get all State Names
public string[] GetStateNames() {
return allNodes.Where(n => n is FSMState).Select(n => n.name).ToArray();
}
///Get a state by it's name
public FSMState GetStateWithName(string name) {
return (FSMState)allNodes.Find(n => n is FSMState && n.name == name);
}
//Gather IStateCallbackReceivers and subscribe them to state events
void GatherCallbackReceivers() {
if ( agent == null ) { return; }
callbackReceivers = agent.gameObject.GetComponents();
if ( callbackReceivers.Length > 0 ) {
onStateEnter += (x) => { foreach ( var m in callbackReceivers ) m.OnStateEnter(x); };
onStateUpdate += (x) => { foreach ( var m in callbackReceivers ) m.OnStateUpdate(x); };
onStateExit += (x) => { foreach ( var m in callbackReceivers ) m.OnStateExit(x); };
}
}
public FSMState PeekStack() {
return stateStack != null && stateStack.Count > 0 ? stateStack.Peek() : null;
}
///----------------------------------------------------------------------------------------------
///---------------------------------------UNITY EDITOR-------------------------------------------
#if UNITY_EDITOR
[UnityEditor.MenuItem("Tools/ParadoxNotion/NodeCanvas/Create/State Machine Asset", false, 1)]
static void Editor_CreateGraph() {
var newGraph = EditorUtils.CreateAsset();
UnityEditor.Selection.activeObject = newGraph;
}
#endif
}
}