🚧 Historical Archive: This PoC demonstrates initial feasibility. Not production-ready.
A vertical slice from scratch in under 20 days: Started with Unity, then ported to Godot (no XP) and Unreal (no XP) in 3 days each.
Watch the video on YouTube (1:20)
It does!
It’s really not rocket science: Engines aren’t that different. They all make games using the same basic set of high-level features. LunyScript aims to standardize only that high-level functionality. Because these features are so fundamental, they don’t change at all.
I asked a provocative question on reddit and a follow-up and the responses were uniformly:
I call this cognitive bias, and my fault: I did not explain it as well as I can now. Also: wrong audience.
→ How LunyScript unifies different engine architectures
→ API design philosophy and principles
→ Code comparison: LunyScript vs Engine scrips
→ More Design details
Note: Physics behaviour will deviate between physics engines, requires scaling values.
The PoC demonstrates LunyScript orchestrating essential gameplay systems across all three engines:
| System | Features Demonstrated |
|---|---|
| Input | Keyboard input detection |
| Physics | Rigidbody movement, forces, velocity control |
| Collision | Collision detection events, collision filtering |
| Assets | Prefab addressing, instantiation |
| Scene Graph | Object create/destroy, find children by name, transform |
| UI | Text display, Variable binding, Button press events |
| Variables | Game state and progression, timer & score |
| Audio | Sound effect playback |
Scope: High-level gameplay scripting - orchestrating game logic, behaviors, and interactions. LunyScript is not a game engine API replacement.
LunyScript will have a different architecture in key aspects:



Repos need to be cloned recursively: git clone --recursive ...
| Engine | Repository |
|---|---|
| Godot | LunyScratch_Examples_Godot |
| Unity | LunyScratch_Examples_Unity |
| Unreal | LunyScratch_Examples_Unreal |
Note: I can’t guarantee that these projects will open and run without errors for everyone. I will not maintain them.
This is the script for the “Police Car” which acts as both player controller and overall game state.
⚠️ Note: API in this PoC represents an early first draft. Final API will differ in key aspects. It will not leak engine details into the script.
using Godot;
using LunyScratch;
using System;
using static LunyScratch.Blocks;
using Key = LunyScratch.Key;
public sealed partial class PoliceCarScratch : ScratchRigidbody3D
{
[Export] private Single _turnSpeed = 70f;
[Export] private Single _moveSpeed = 16f;
[Export] private Single _deceleration = 0.85f;
[Export] private Int32 _startTimeInSeconds = 5;
protected override void OnScratchReady()
{
var progressVar = GlobalVariables["Progress"];
var scoreVariable = Variables.Set("Score", 0);
var timeVariable = Variables.Set("Time", _startTimeInSeconds);
// Handle UI State
HUD.BindVariable(scoreVariable);
HUD.BindVariable(timeVariable);
Run(HideMenu(), ShowHUD());
RepeatForever(If(IsKeyJustPressed(Key.Escape), ShowMenu()));
// must run globally because we Disable() the car and thus all object sequences will stop updating
Scratch.When(ButtonClicked("TryAgain"), ReloadCurrentScene());
Scratch.When(ButtonClicked("Quit"), QuitApplication());
// tick down time, and eventually game over
RepeatForever(Wait(1), DecrementVariable("Time"),
If(IsVariableLessOrEqual(timeVariable, 0),
ShowMenu(), SetCameraTrackingTarget(null), Wait(0.5), DisableComponent()));
// Use RepeatForeverPhysics for physics-based movement
var enableBrakeLights = Sequence(Enable("BrakeLight1"), Enable("BrakeLight2"));
var disableBrakeLights = Sequence(Disable("BrakeLight1"), Disable("BrakeLight2"));
RepeatForeverPhysics(
// Forward/Backward movement
If(IsKeyPressed(Key.W),
MoveForward(_moveSpeed), disableBrakeLights)
.Else(If(IsKeyPressed(Key.S),
MoveBackward(_moveSpeed), enableBrakeLights)
.Else(SlowDownMoving(_deceleration), disableBrakeLights)
),
// Steering
If(IsCurrentSpeedGreater(0.1),
If(IsKeyPressed(Key.A), TurnLeft(_turnSpeed)),
If(IsKeyPressed(Key.D), TurnRight(_turnSpeed)))
);
// add score and time on ball collision
When(CollisionEnter(tag: "CompanionCube"),
IncrementVariable("Time"),
// add 'power of three' times the progress to score
SetVariable(Variables["temp"], progressVar),
MultiplyVariable(Variables["temp"], progressVar),
MultiplyVariable(Variables["temp"], progressVar),
AddVariable(scoreVariable, Variables["temp"]));
// blinking signal lights
RepeatForever(
Enable("RedLight"),
Wait(0.16),
Disable("RedLight"),
Wait(0.12)
);
RepeatForever(
Disable("BlueLight"),
Wait(0.13),
Enable("BlueLight"),
Wait(0.17)
);
// Helpers
// don't play minicube sound too often
RepeatForever(DecrementVariable(GlobalVariables["MiniCubeSoundTimeout"]));
// increment progress (score increment) every so often
RepeatForever(IncrementVariable(progressVar), Wait(15), PlaySound());
}
}