On.* vs When.* API RefactorDate: 2026-02-05
Status: Design Complete - Ready for Technical Specification
Related: Coroutine & Timer Design
This document defines the semantic distinction between On.* and When.* event APIs in LunyScript. This refactor replaces the current When.Self.* pattern with a clearer, more intuitive model for beginners and designers.
| Prefix | Meaning | Scope |
|---|---|---|
On.* |
Events about objectâs own state | Local to context object |
When.* |
Events from external sources | External/broadcast events |
On.* = âWhen something about me changesâ (objectâs own events)When.* = âWhen something happens in the worldâ (external events)On.* API (Object Lifecycle Events)Events concerning the object itself. Uses singular form (matches engine convention, reads naturally: âOn enable, doâŚâ).
On.Created(blocks); // once when object instantiated
On.Enabled(blocks); // each time object becomes enabled
On.Ready(blocks); // once before first frame/step processing
On.Disabled(blocks); // each time object becomes disabled
On.Destroyed(blocks); // once when object is destroyed
| Previous | NOW | Rationale |
|---|---|---|
When.Self.Updates() |
â
On.FrameUpdate() |
Clearer, matches âevery frameâ mental model |
When.Self.LateUpdates() |
â
On.FrameEnd() |
Much clearer than Unityâs âLateUpdateâ jargon |
When.Self.Steps() |
â
On.Heartbeat() |
Evocative of steady, rhythmic, predictable timing |
// On.* scheme but Every.* would work nicely for these too
On.FrameUpdate(blocks); // every render frame (variable rate)
On.FrameEnd(blocks); // after frame processing complete
On.Heartbeat(blocks); // fixed interval (physics/logic rate)
On.Heartbeat()?Heartbeat is the least technical, most widely understood term that indicates a rhythmic processing. This is in contrast to frame updates which are not expected to be steady.
| Name | Issue |
|---|---|
Step |
Step of what? Too vague |
FixedStep |
âFixedâ begs explanation |
LogicStep |
Implies only âlogicâ runs here (physics too, but not input!!) |
PhysicsStep |
It has far wider uses than just physics simulation |
SimulationStep |
Lengthy and implies something is âsimulatedâ here |
Tick |
Often associated with frame-based updates, generic |
Heartbeat |
â Clear, evocative, universal metaphor for steady rhythm |
When.* API (External Events)Events from external sources that the object listens to. The object doesnât cause these events; it reacts to them.
When.Scene().Loads(blocks); // any scene loaded
When.Scene("Level1").Loads(blocks); // specific scene loaded
When.Scene().Unloads(blocks); // any scene unloaded
When.Scene("Level1").Unloads(blocks); // specific scene unloaded
When.Input("jump").Pressed(blocks);
When.Input("fire").Released(blocks);
When.Input(AnyKey).Pressed(blocks); // where's the 'Any' key?
When.Input(AnyMouse).Pressed(blocks);
When.Input(LeftMouse).Pressed(blocks); // alias: LMB
When.ContactWith("x")
.Begins(blocks)
.Stays(blocks)
.Ends(blocks);
When.Button("Start").Clicked(blocks);
When.Slider("Volume").Changes(blocks);
LunyScript does not allow running events on a âglobalâ scope from an objectâs script. This avoids many problems:
The solution is simple and straightforward: User creates a separate object and script (perhaps named âGlobalâ) with controlled lifetime.
Alternatively, a singleton script can be created by overriding the Singleton property and returning true:
public class ManagersManagerManagingManagers : LunyScript
{
// Return true to mark script as Singleton:
public override bool Singleton => true;
}
A singleton script:
A singleton object is treated as follows in engines:
GetTree().Root.AddChild(scriptNode);gameObject.DontDestroyOnLoad();| Current (to deprecate) | New |
|---|---|
When.Self.Created() |
On.Created() |
When.Self.Enabled() |
On.Enabled() |
When.Self.Ready() |
On.Ready() |
When.Self.Disabled() |
On.Disabled() |
When.Self.Destroyed() |
On.Destroyed() |
When.Self.Updates() |
On.FrameUpdate() |
When.Self.LateUpdates() |
On.FrameEnd() |
When.Self.Steps() |
On.Heartbeat() |
When.Scene.Loads() |
When.Scene("").Loads() |
When.Scene.Unloads() |
When.Scene("").Unloads() |
Timers and coroutines (see Coroutine & Timer Design) integrate naturally with lifecycle events:
var countdown = Coroutine("countdown").Duration(5).Seconds()
.OnUpdate(Debug.Log("..."))
.Elapsed(Debug.Log("done"));
On.Enabled(countdown.Start()); // restarts, instead of resume
OnApi Structurepublic readonly struct OnApi
{
private readonly ILunyScript _script;
internal OnApi(ILunyScript script) => _script = script;
// Lifecycle
public IScriptSequenceBlock Created(params IScriptActionBlock[] blocks) => ...
public IScriptSequenceBlock Enabled(params IScriptActionBlock[] blocks) => ...
public IScriptSequenceBlock Ready(params IScriptActionBlock[] blocks) => ...
public IScriptSequenceBlock Disabled(params IScriptActionBlock[] blocks) => ...
public IScriptSequenceBlock Destroyed(params IScriptActionBlock[] blocks) => ...
// Update
public IScriptSequenceBlock FrameUpdate(params IScriptActionBlock[] blocks) => ...
public IScriptSequenceBlock FrameEnd(params IScriptActionBlock[] blocks) => ...
public IScriptSequenceBlock Heartbeat(params IScriptActionBlock[] blocks) => ...
}
| Category | API | Examples |
|---|---|---|
| Object lifecycle | On.* |
On.Created(), On.Enabled(), On.Destroyed() |
| Update loops | On.* |
On.FrameUpdate(), On.FrameEnd(), On.Heartbeat() |
| Scene events | When.Scene().* |
When.Scene().Loads(), When.Scene().Unloads() |
| Input events | When.Input().* |
When.Input().KeyPressed() (future) |
| Collision events | When.ContactWith().* |
When.ContactWith().Entered() (future) |
This design provides:
Heartbeat, FrameUpdate, FrameEnd