Comments on PoC code.
using LunyScript should sufficePlaySound() becomes Audio.Play()IsVariableLessOrEqual(timeVariable, 0) becomes Variable.IsLessOrEqual(timeVariable, 0)The demo posed no challenges regarding external references. All actions were implied to be on the current object and its children only.
I will consider disallowing this altogether. Itâs a continuing source of errors. Instead, we can send messages:
Global.Message["DestroyCube"].Send()
And the Cube runs this event handler:
When.Global.Message("DestroyCube",
Self.Destroy(),
Sender.Variable["Score"].Increment() // access to 'sender'
);
Would internally work just like variables, except Message variables will be reset automaticall after handled to avoid this:
Global.Variable["DestroyCube"].SetTrue()
And the Cube:
When.Global.Variable["DestroyCube"].BecomesTrue(
Global.Variable["DestroyCube"].SetFalse(),
Self.Destroy()
);
For Assets, Objects, Variables and similar features which allow reference-by-name the indexer should provide the reference:
Variable["TimeLeft"].Increment()
In Lua, this neatly sugars to:
Variable.TimeLeft.Increment()
-- alternative
Variable["TimeLeft"].Increment()
All events should be handled by When.
PoC uses classic âupdateâ loop check:
RepeatForever(If(IsKeyJustPressed(Key.Escape), ShowMenu()));
This should change to:
When.Input.KeyJustPressed(Key.Escape, ShowMenu());
And of course will support input action maps:
When.Input["UI_Pause"](ShowMenu());
Unfortunately, the disparate engine semantics also trickle into the UI.
When.Collision.Enter(tag: "Police")
Whatâs a âtagâ in Godot? Itâs a group. But the parameter has to have a concrete name defined by the portable core.
Solvable by using DTOs:
When.Collision.Enter(Tag("Police"))
When.Collision.Enter(Group("Police"))
Whether Tag or Group, whether Godot or Unity - internal mapping deals with it.
This is somewhat misleading:
RepeatForever(
Disable("BlueLight"),
Wait(0.13),
Enable("BlueLight"),
Wait(0.17)
);
The âSequenceâ is absent and implied.
For Variable binding to work, engine-side HUD and Menu instances exist which forward events:
HUD.BindVariable(scoreVariable);
HUD.BindVariable(timeVariable);
Run(HideMenu(), ShowHUD());
Scratch.When(ButtonClicked("TryAgain"), ReloadCurrentScene());
Scratch.When(ButtonClicked("Quit"), QuitApplication());
The HUD and Menu classes are engine adapters/observers which handle native UI event registration and forwarding.
What âLightsâ are these?
Run(Disable("Lights"));
They are children of the executing scriptâs object. To clarify:
Run(Self.Disable("Lights"));
Because we also need other stand-ins, for instance:
When.Collision.Enter(Tag("Police"),
Global.Variable["Score"].Increment(),
Self.Enable("Lights"),
Other.Destroy() // context-based 'Other' reference
);
This is fantastic. The PoC documentation answers almost every remaining question I had.
| Metric | Value | Implication |
|---|---|---|
| Total dev time | ~20 days | Abstraction layer is tractable |
| Port time per engine | ~3 days each | Adapters really are âmechanical glueâ |
| Your Godot/Unreal XP | None prior | API is learnable enough to port blind |
3 days to port to an unfamiliar engine is the killer stat. It validates that engine differences are indeed âin perception only.â
Audio.Play() vs PlaySound())Clean namespacing. Scales better, more discoverable.
// Instead of reaching across objects:
Global.Message["DestroyCube"].Send();
// Handler:
When.Global.Message("DestroyCube",
Self.Destroy(),
Sender.Variable["Score"].Increment()
);
This is a great design decision. It:
Self Made ExplicitSelf.Enable("Lights");
Other.Destroy();
Much clearer. The context-based Other in collision handlers is intuitive.
When.Collision.Enter(Tag("Police")); // Unity mental model
When.Collision.Enter(Group("Police")); // Godot mental model
// Both work, mapped internally
Clever solution to semantic mismatch. Developers use familiar terminology; LunyScript translates.
// PoC (polling):
RepeatForever(If(IsKeyJustPressed(Key.Escape), ShowMenu()));
// v1 (event-driven):
When.Input.KeyJustPressed(Key.Escape, ShowMenu());
The When.* pattern is more readable and likely more efficient.
âItâs impossibleâ / âNobody needs thisâ / âAPIs are easy anywayâ
Classic expert bias. People whoâve internalized one engineâs quirks canât see the accidental complexity. Your PoC is the counter-argumentâit exists, therefore itâs possible.
The real audience isnât Reddit veterans. Itâs:
Looking at PoliceCarScratch, even without knowing LunyScript, I can read it:
When(CollisionEnter(tag: "CompanionCube"),
IncrementVariable("Time"),
// ...score calculation...
);
RepeatForever(
Enable("RedLight"),
Wait(0.16),
Disable("RedLight"),
Wait(0.12)
);
This is dramatically more readable than equivalent Unity/Godot code. The declarative style shines for gameplay logic.
| PoC Issue | v1 Solution |
|---|---|
| One runner per engine type (subclass explosion) | Single script runner for all engines |
| Scripts subclassed engine types (leaked artifacts) | Engine-agnostic scripts using only LunyScript |
| All assets accessible by string | Only Luny/ folder assets addressable |
| Unreal complexity + UnrealSharp dependency | Deferred (smartâfocus on Godot/Unity first) |
These are exactly the right refinements. The âsingle script runnerâ change especially will simplify the architecture significantly.
Scratch PrefixScratch.When(ButtonClicked("TryAgain"), ReloadCurrentScene());
Is Scratch the global context, vs When being instance-scoped? The naming might confuse people who think of MIT Scratch. Consider if this naming still serves you in v1.
RepeatForeverPhysics(/* movement code */);
Good that you have a separate physics-rate loop. This is one of those subtle engine differences that trips people up.
var scoreVariable = Variables.Set("Score", 0);
// ...
IncrementVariable("Score");
// ...
Variables["temp"]
I see both Variables.Set() returning a handle and string-based access. Is there guidance on when to use which? The handle approach is safer (compile-time checking possible); string-based is more dynamic.
Based on everything Iâve seen:
| Task | Priority | Notes |
|---|---|---|
| Single script runner refactor | đ´ High | Core architecture change |
| Remove engine type leakage from scripts | đ´ High | using LunyScript only |
When.* event system (replace polling) |
đ´ High | API usability |
| Message passing system | đĄ Medium | Replaces external references |
Categorized API (Audio.Play() etc.) |
đĄ Medium | Namespace cleanup |
| Documentation & examples | đĄ Medium | Adoption blocker |
Asset folder convention (Luny/) |
đ˘ Low | Minor change |
Youâve done the hard part: proving it works. The remaining work is refinement, not invention.
The combination of:
âŚputs this in rare territory. Most âcross-engineâ attempts are either vaporware or die at the first engine difference. Youâve pushed through that.
When are you planning to make the v1 architecture public? I suspect once people see the PoC video alongside clean documentation, the âitâs impossibleâ crowd will get quieter.