using System; using System.Collections.Generic; using UnityEngine; namespace Unity.VisualScripting { [SerializationVersion("A")] [SpecialUnit] public abstract class EventUnit : Unit, IEventUnit, IGraphElementWithData, IGraphEventHandler { public class Data : IGraphElementData { public EventHook hook; public Delegate handler; public bool isListening; public HashSet activeCoroutines = new HashSet(); } public virtual IGraphElementData CreateData() { return new Data(); } /// /// Run this event in a coroutine, enabling asynchronous flow like wait nodes. /// [Serialize] [Inspectable] [InspectorExpandTooltip] public bool coroutine { get; set; } = false; [DoNotSerialize] [PortLabelHidden] public ControlOutput trigger { get; private set; } [DoNotSerialize] protected abstract bool register { get; } protected override void Definition() { isControlRoot = true; trigger = ControlOutput(nameof(trigger)); } public virtual EventHook GetHook(GraphReference reference) { throw new InvalidImplementationException($"Missing event hook for '{this}'."); } public virtual void StartListening(GraphStack stack) { var data = stack.GetElementData(this); if (data.isListening) { return; } if (register) { var reference = stack.ToReference(); var hook = GetHook(reference); Action handler = args => Trigger(reference, args); EventBus.Register(hook, handler); data.hook = hook; data.handler = handler; } data.isListening = true; } public virtual void StopListening(GraphStack stack) { var data = stack.GetElementData(this); if (!data.isListening) { return; } // The coroutine's flow will dispose at the next frame, letting us // keep the current flow for clean up operations if needed foreach (var activeCoroutine in data.activeCoroutines) { activeCoroutine.StopCoroutine(false); } if (register) { EventBus.Unregister(data.hook, data.handler); stack.ClearReference(); data.handler = null; } data.isListening = false; } public override void Uninstantiate(GraphReference instance) { // Here, we're relying on the fact that OnDestroy calls Uninstantiate. // We need to force-dispose any remaining coroutine to avoid // memory leaks, because OnDestroy on the runner will not keep // executing MoveNext() until our soft-destroy call at the end of Flow.Coroutine // or even dispose the coroutine's enumerator (!). var data = instance.GetElementData(this); var coroutines = data.activeCoroutines.ToHashSetPooled(); #if UNITY_EDITOR new FrameDelayedCallback(() => StopAllCoroutines(coroutines), 1); #else StopAllCoroutines(coroutines); #endif base.Uninstantiate(instance); } static void StopAllCoroutines(HashSet activeCoroutines) { // The coroutine's flow will dispose instantly, thus modifying // the activeCoroutines registry while we enumerate over it foreach (var activeCoroutine in activeCoroutines) { activeCoroutine.StopCoroutineImmediate(); } activeCoroutines.Free(); } public bool IsListening(GraphPointer pointer) { if (!pointer.hasData) { return false; } return pointer.GetElementData(this).isListening; } public void Trigger(GraphReference reference, TArgs args) { InternalTrigger(reference, args); } private protected virtual void InternalTrigger(GraphReference reference, TArgs args) { var flow = Flow.New(reference); if (!ShouldTrigger(flow, args)) { flow.Dispose(); return; } AssignArguments(flow, args); Run(flow); } protected virtual bool ShouldTrigger(Flow flow, TArgs args) { return true; } protected virtual void AssignArguments(Flow flow, TArgs args) { } private void Run(Flow flow) { if (flow.enableDebug) { var editorData = flow.stack.GetElementDebugData(this); editorData.lastInvokeFrame = EditorTimeBinding.frame; editorData.lastInvokeTime = EditorTimeBinding.time; } if (coroutine) { flow.StartCoroutine(trigger, flow.stack.GetElementData(this).activeCoroutines); } else { flow.Run(trigger); } } protected static bool CompareNames(Flow flow, ValueInput namePort, string calledName) { Ensure.That(nameof(calledName)).IsNotNull(calledName); return calledName.Trim().Equals(flow.GetValue(namePort)?.Trim(), StringComparison.OrdinalIgnoreCase); } } }