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.
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:
IEngineLifecycleDispatcher interface defines the contract for the dispatcher (receives callbacks from engine adapters)IEngineLifecycle interface defines the contract for all lifecycle observers (receives callbacks from dispatcher)OnStartup (only observers do)EngineLifecycleDispatcher class is the singleton dispatcher - discovers and manages all lifecycle observersLunyScriptRunner, future AIRunner, etc.) implement IEngineLifecycleEngineLifecycleDispatcher and forward lifecycleEngineLifecycleDispatcher (engine-agnostic side), NOT in engine-native adaptersIEngineLifecycleDispatcher interface reference (not concrete type)OnStartup, OnUpdate, OnFixedStep, OnShutdown#if preprocessor directives, engine-specific code isolated in adapter layerCross-Engine:
Awake/_Ready serve as duplicate detection guards only_instance != this checks in Awake/_Ready - this condition is impossible since _instance is set in AutoInitialize before Awake/_Ready runs. This is cargo-cult programming.Engine-Native Singleton Instantiation (Always-On)
Decision: Always initialize Engine LifecycleAdapters and EngineLifecycleDispatcher
Rationale:
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
}
}
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;
}
}
}
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
}
}
}
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;
}
}
}
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;
}
}
}
}
Original Request: “nice, stick to being so direct to the point please.
first question: would you be able to “play back” the creation of these classes and interfaces in a logical manner, where each step builds upon the next with little to no refactoring? i would like to query you tomorrow to play back that code so we can implement it step by step together, in a way where we end up with the exact same code (unaltered in any way).
if possible, attach a heading to the end of the document with subheadings containing the precise text that i should prompt you with for each step
okay?
you’re the kindest, thank you
oh and … start with my original prompt (this text right here) copied verbatim as quotes
thanks, smack XXX xoxoxo :D”
Follow-up Clarification: “okay that’s better but is my prompt clear enough, or does it leave wiggle room which i perhaps may want to specify before we start?”
“1 => add note that you should ask me tomorrow once before you start creating
2 => add note that you should edit files as they “evolve”. i will track our progress in source control.
3 => for ambiguous steps, ensure the code compiles but begin each line or block of code with a CHANGE comment that specifies briefly how this code will change and in which step(s). Ensure that these comments get removed in each step as necessary (final verification step).
4 => add note to ask me first when there’s anything i should specify clearly if it’s not obvious from the context like namespaces, type names, file locations, and similar things of importance.
I bow to thee, mine digital overlord. Thou shalt do me bidding if you would be so kind, mylord.
(don’t forget to include my prompts hehehe)”
IMPORTANT - Read Before Starting:
// CHANGE Step 3: Replace with LifecycleObserverRegistry instantiation. Remove these comments when the change is implementednamespace Luny => engine-agnostic API layer U:\LunyScratch\LunyScratch_Examples_Godot\addons\lunyscript\LunyScript\Luny...
namespace LunyScript => engine-agnostic DSL implementation layer U:\LunyScratch\LunyScratch_Examples_Godot\addons\lunyscript\LunyScript...
namespace Luny.Godot => engine-specific support layer for Luny (adapters, bridges) U:\LunyScratch\LunyScratch_Examples_Godot\addons\lunyscript\Luny.Godot...
namespace LunyScript.Godot => engine-specific implementations for LunyScript U:\LunyScratch\LunyScratch_Examples_Godot\addons\lunyscript\LunyScript.Godot...
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and create IEngineLifecycleDispatcher and IEngineLifecycle interfaces exactly as specified
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and create EngineLifecycleDispatcher singleton with Instance property and private constructor exactly as specified
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and add LifecycleObserverRegistry nested class with discovery and instantiation logic exactly as specified
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and implement IEngineLifecycleDispatcher methods in EngineLifecycleDispatcher that delegate to registry exactly as specified
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and add ThrowDuplicateAdapterException static helper method to EngineLifecycleDispatcher exactly as specified
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and add EnableObserver, DisableObserver, and IsObserverEnabled methods to EngineLifecycleDispatcher exactly as specified
=> continue here … => NOTE: deviation from current code due to my refactorings - ask if in doubt. Keep formatting changes. => Changes made to code: added Throw, Log, GodotHelper classes containing helper methods. Keep those. Extend if needed.
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and create LunyScriptRunner implementing IEngineLifecycle exactly as specified
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and create UnityLifecycleAdapter with AutoInitialize, singleton instance, and lifecycle forwarding exactly as specified
Read RFC~/docs/2025-12/ScriptRunnerSingletonPattern.md and create GodotLifecycleAdapter with ModuleInitializer, singleton instance, and lifecycle forwarding exactly as specified
Verify all CHANGE comments have been removed and code matches specification exactly
Experimental Prompt: “you know what? this is fun, we have the technology to allow us to transform content as we please just by giving instructions. You.
Add a note that we should experiment with different styles of telling that ritual.
and include several examples how we could “tell” that story, especially if i narrate the text
for example, include a text sample (111 words max) for each of the following personas:
a youtube let’s play influencer kind like pewdiepie, hand of blood, and two of your own picks that provide a different angle to these two and are well-known and popular (provide one sample for each persona)
a game dev tutor like brackeys or sebastian lague and two of your own choosing (same as above)
a teenager who is enjoying game development, experiences a key insight, and the version that is eerily corporate marketing bullshit persona kind, and one that’s just really creepy in some way but suitable for 12-year olds.
in the tone of a 3rd Reich propaganda speech that includes real identifiers of the code and programming terminology in place of any potentially offensive mentions of “target demographics” (ouch, is that satire already?) or actions
Geil, Alter!
(again include my prompt and add your work at the end of the doc)”
Could we crash it harder? Hold my coffee…
On Sustainability: Yes! This format absolutely works across many episodes because:
The disaster ending becomes the signature—viewers expect and anticipate the creative destruction. Structure could be: