Script Runner Singleton Pattern

We’ve designed a clean, lazy-initialization singleton pattern for the LunyScript runner that only creates itself when first accessed, avoiding overhead when unused. The architecture uses a dispatcher pattern: EngineLifecycleDispatcher is the singleton that receives engine lifecycle callbacks and dispatches them to registered domain runners (LunyScriptRunner, AIRunner, etc.) via the IEngineLifecycle interface.

TODOs

TODO: Considerations

Key Design Decisions

Instantiation Timeline

Runtime starts (Unity/Godot engine initialization)
  ↓
Auto-initialization triggers (before first scene load)
  ↓
Unity-/GodotLifecycleAdapter instantiates via engine autoload mechanism
  ↓
LifecycleAdapter instantiates new GameObject with UnityLifecycleAdapter component
LifecycleAdapter instantiates new GodotLifecycleAdapter Node and adds it to SceneTree (deferred)
  ↓
LifecycleAdapter Awake/_Ready() instantiates EngineLifecycleDispatcher via Instance property (keeps dispatcher reference)  
  ↓
EngineLifecycleDispatcher constructor instantiates and registers domain runners (LunyScriptRunner, etc.)
  ↓
LifecycleDispatcher calls OnStartup() on each runner
  ↓
System ready - heartbeat event dispatch begins (OnUpdate/OnFixedStep)

Architecture:

Cross-Engine:

Engine-Native Singleton Instantiation (Always-On)

Decision: Always initialize Engine LifecycleAdapters and EngineLifecycleDispatcher

Rationale:

Interface Definitions

namespace Luny
{
    public interface IEngineLifecycleDispatcher
    {
        // Dispatcher interface - receives callbacks from engine adapters
        void OnUpdate(double deltaTime);
        void OnFixedStep(double fixedDeltaTime);
        void OnShutdown();
    }

    public interface IEngineLifecycle
    {
        // Lifecycle observer interface - receives callbacks from dispatcher
        void OnStartup();
        void OnUpdate(double deltaTime);
        void OnFixedStep(double fixedDeltaTime);
        void OnShutdown();
        // add remaining lifecycle callbacks here
    }
}

EngineLifecycleDispatcher (Singleton Dispatcher)

namespace Luny
{
    public sealed class EngineLifecycleDispatcher : IEngineLifecycleDispatcher
    {
        private static EngineLifecycleDispatcher _instance;

        private readonly LifecycleObserverRegistry _registry;

        public static EngineLifecycleDispatcher Instance
        {
            get
            {
                if (_instance == null)
                {
                    _instance = new EngineLifecycleDispatcher();
                }
                return _instance;
            }
        }

        private EngineLifecycleDispatcher()
        {
            _registry = new LifecycleObserverRegistry();
        }

        public void EnableObserver<T>() where T : IEngineLifecycle => _registry.EnableObserver<T>();

        public void DisableObserver<T>() where T : IEngineLifecycle => _registry.DisableObserver<T>();

        public bool IsObserverEnabled<T>() where T : IEngineLifecycle => _registry.IsObserverEnabled<T>();

        public void OnUpdate(double deltaTime)
        {
            foreach (var observer in _registry.GetEnabledObservers())
            {
                observer.OnUpdate(deltaTime);
            }
        }

        public void OnFixedStep(double fixedDeltaTime)
        {
            foreach (var observer in _registry.GetEnabledObservers())
            {
                observer.OnFixedStep(fixedDeltaTime);
            }
        }

        public void OnShutdown()
        {
            foreach (var observer in _registry.GetEnabledObservers())
            {
                observer.OnShutdown();
            }
        }

        public static void ThrowDuplicateAdapterException(string adapterTypeName, string existingObjectName, long existingInstanceId, string duplicateObjectName, long duplicateInstanceId)
        {
            throw new System.InvalidOperationException(
                $"Duplicate {adapterTypeName} singleton detected! " +
                $"Existing: Name='{existingObjectName}' InstanceID={existingInstanceId}, " +
                $"Duplicate: Name='{duplicateObjectName}' InstanceID={duplicateInstanceId}");
        }

        private sealed class LifecycleObserverRegistry
        {
            private readonly Dictionary<Type, IEngineLifecycle> _registeredObservers = new();
            private readonly List<IEngineLifecycle> _enabledObservers = new();

            public LifecycleObserverRegistry()
            {
                DiscoverAndInstantiateObservers();
            }

            private void DiscoverAndInstantiateObservers()
            {
                var observerTypes = AppDomain.CurrentDomain.GetAssemblies()
                    .SelectMany(a => a.GetTypes())
                    .Where(t => typeof(IEngineLifecycle).IsAssignableFrom(t)
                                && !t.IsAbstract
                                && t != typeof(EngineLifecycleDispatcher));

                foreach (var type in observerTypes)
                {
                    var instance = (IEngineLifecycle)Activator.CreateInstance(type);
                    _registeredObservers[type] = instance;
                    _enabledObservers.Add(instance); // All observers enabled by default
                    instance.OnStartup(); // Initialize immediately after instantiation
                }
            }

            public void EnableObserver<T>() where T : IEngineLifecycle
            {
                if (_registeredObservers.TryGetValue(typeof(T), out var observer) && !_enabledObservers.Contains(observer))
                {
                    _enabledObservers.Add(observer);
                }
            }

            public void DisableObserver<T>() where T : IEngineLifecycle
            {
                if (_registeredObservers.TryGetValue(typeof(T), out var observer))
                {
                    _enabledObservers.Remove(observer);
                }
            }

            public bool IsObserverEnabled<T>() where T : IEngineLifecycle
            {
                return _registeredObservers.TryGetValue(typeof(T), out var observer) && _enabledObservers.Contains(observer);
            }

            public IEnumerable<IEngineLifecycle> GetEnabledObservers() => _enabledObservers;
        }
    }
}

LunyScriptRunner (Lifecycle Observer)

namespace LunyScript
{
    public sealed class LunyScriptRunner : IEngineLifecycle
    {
        // Engine-agnostic script execution logic
        // State machines, behavior trees, script lifecycle, etc.

        public void OnStartup()
        {
            // Initialize runner systems
        }

        public void OnUpdate(double deltaTime)
        {
            // Process per-frame logic
        }

        public void OnFixedStep(double fixedDeltaTime)
        {
            // Process fixed timestep logic
        }

        public void OnShutdown()
        {
            // Cleanup runner systems
        }
    }
}

Unity Implementation (Ultra-Thin Adapter)

namespace Luny.Unity
{
    using UnityEngine;

    internal sealed class UnityLifecycleAdapter : MonoBehaviour
    {
        private static UnityLifecycleAdapter _instance;

        private IEngineLifecycleDispatcher _dispatcher;

        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        private static void AutoInitialize()
        {
            // Force creation before first scene loads
            var go = new GameObject(nameof(UnityLifecycleAdapter));
            _instance = go.AddComponent<UnityLifecycleAdapter>();
            DontDestroyOnLoad(go);
        }

        private void Awake()
        {
            if (_instance != null)
            {
                EngineLifecycleDispatcher.ThrowDuplicateAdapterException(
                    nameof(UnityLifecycleAdapter),
                    _instance.gameObject.name,
                    _instance.GetInstanceID(),
                    gameObject.name,
                    GetInstanceID());
            }

            // Only responsibility: create EngineLifecycleDispatcher singleton
            _dispatcher = EngineLifecycleDispatcher.Instance;
        }

        // Only responsibility: forward Unity lifecycle to EngineLifecycleDispatcher
        private void Update() => _dispatcher.OnUpdate(Time.deltaTime);

        private void FixedUpdate() => _dispatcher.OnFixedStep(Time.fixedDeltaTime);

        private void OnApplicationQuit()
        {
            _dispatcher?.OnShutdown();
            _dispatcher = null;
            _instance = null;
        }
    }
}

Godot Implementation (Ultra-Thin Adapter)

namespace Luny.Godot
{
    using Godot;

    internal sealed partial class GodotLifecycleAdapter : Node
    {
        private static GodotLifecycleAdapter _instance;

        private IEngineLifecycleDispatcher _dispatcher;

        [System.Runtime.CompilerServices.ModuleInitializer]
        internal static void AutoInitialize()
        {
            // Deferred to first frame since Godot isn't ready yet
            Callable.From(() =>
            {
                if (_instance == null)
                {
                    _instance = new GodotLifecycleAdapter
                    {
                        Name = nameof(GodotLifecycleAdapter)
                    };
                    var root = Engine.GetMainLoop() as SceneTree;
                    root?.Root.CallDeferred(nameof(add_child), _instance);
                }
            }).CallDeferred();
        }

        public override void _Ready()
        {
            if (_instance != null)
            {
                EngineLifecycleDispatcher.ThrowDuplicateAdapterException(
                    nameof(GodotLifecycleAdapter),
                    _instance.Name,
                    _instance.GetInstanceId(),
                    Name,
                    GetInstanceId());
            }

            // Only responsibility: create EngineLifecycleDispatcher singleton
            _dispatcher = EngineLifecycleDispatcher.Instance;
        }

        // Only responsibility: forward Godot lifecycle to EngineLifecycleDispatcher
        public override void _Process(double delta) => _dispatcher.OnUpdate(delta);

        public override void _PhysicsProcess(double delta) => _dispatcher.OnFixedStep(delta);

        public override void _Notification(int what)
        {
            if (what == NotificationWMCloseRequest)
            {
                _dispatcher?.OnShutdown();
                _dispatcher = null;
                _instance = null;
            }
        }
    }
}