using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Unity.VisualScripting { public abstract class State : GraphElement, IState { public class Data : IGraphElementData { public bool isActive; public bool hasEntered; } public class DebugData : IStateDebugData { public int lastEnterFrame { get; set; } public float lastExitTime { get; set; } public Exception runtimeException { get; set; } } public IGraphElementData CreateData() { return new Data(); } public IGraphElementDebugData CreateDebugData() { return new DebugData(); } [Serialize] public bool isStart { get; set; } [DoNotSerialize] public virtual bool canBeSource => true; [DoNotSerialize] public virtual bool canBeDestination => true; public override void BeforeRemove() { base.BeforeRemove(); Disconnect(); } public override void Instantiate(GraphReference instance) { base.Instantiate(instance); var data = instance.GetElementData(this); if (this is IGraphEventListener listener && data.isActive) { listener.StartListening(instance); } else if (isStart && !data.hasEntered && graph.IsListening(instance)) { using (var flow = Flow.New(instance)) { OnEnter(flow, StateEnterReason.Start); } } } public override void Uninstantiate(GraphReference instance) { if (this is IGraphEventListener listener) { listener.StopListening(instance); } base.Uninstantiate(instance); } #region Poutine protected void CopyFrom(State source) { base.CopyFrom(source); isStart = source.isStart; width = source.width; } #endregion #region Transitions public IEnumerable outgoingTransitions => graph?.transitions.WithSource(this) ?? Enumerable.Empty(); public IEnumerable incomingTransitions => graph?.transitions.WithDestination(this) ?? Enumerable.Empty(); protected List outgoingTransitionsNoAlloc => graph?.transitions.WithSourceNoAlloc(this) ?? Empty.list; public IEnumerable transitions => LinqUtility.Concat(outgoingTransitions, incomingTransitions); public void Disconnect() { foreach (var transition in transitions.ToArray()) { graph.transitions.Remove(transition); } } #endregion #region Lifecycle public virtual void OnEnter(Flow flow, StateEnterReason reason) { var data = flow.stack.GetElementData(this); if (data.isActive) // Prevent re-entry from Any State { return; } data.isActive = true; data.hasEntered = true; foreach (var transition in outgoingTransitionsNoAlloc) { // Start listening for the transition's events // before entering the state in case OnEnterState // actually instantly triggers a transition via event // http://support.ludiq.io/topics/261-event-timing-issue/ (transition as IGraphEventListener)?.StartListening(flow.stack); } if (flow.enableDebug) { var editorData = flow.stack.GetElementDebugData(this); editorData.lastEnterFrame = EditorTimeBinding.frame; } OnEnterImplementation(flow); foreach (var transition in outgoingTransitionsNoAlloc) { try { transition.OnEnter(flow); } catch (Exception ex) { transition.HandleException(flow.stack, ex); throw; } } } public virtual void OnExit(Flow flow, StateExitReason reason) { var data = flow.stack.GetElementData(this); if (!data.isActive) { return; } OnExitImplementation(flow); data.isActive = false; if (flow.enableDebug) { var editorData = flow.stack.GetElementDebugData(this); editorData.lastExitTime = EditorTimeBinding.time; } foreach (var transition in outgoingTransitionsNoAlloc) { try { transition.OnExit(flow); } catch (Exception ex) { transition.HandleException(flow.stack, ex); throw; } } } protected virtual void OnEnterImplementation(Flow flow) { } protected virtual void UpdateImplementation(Flow flow) { } protected virtual void FixedUpdateImplementation(Flow flow) { } protected virtual void LateUpdateImplementation(Flow flow) { } protected virtual void OnExitImplementation(Flow flow) { } public virtual void OnBranchTo(Flow flow, IState destination) { } #endregion #region Widget public const float DefaultWidth = 170; [Serialize] public Vector2 position { get; set; } [Serialize] public float width { get; set; } = DefaultWidth; #endregion } }