Proof of Concept Demo (October 2025)

🚧 Historical Archive: This PoC demonstrates initial feasibility. Not production-ready.

Overview

A vertical slice from scratch in under 20 days (including all LunyScript abstractions): Started with Unity, then ported to Godot (no XP) and Unreal (no XP) in 3 days each.

Watch the video on YouTube (1:20)


🧐This can’t possibly work!!🤥

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.

The engine differences are in perception only. Technically, they are minute.

Engines have trees of Nodes, GameObjects, Actors. Nodes are components. GameObjects and Actors contain components. The lifecycle events differ slightly: trap, and fire at desired order with minimal overhead.

SQL, jQuery, OpenXR, et al unified such implementation artifacts. Why not engine APIs?

I asked a provocative question on reddit and a follow-up and the responses were uniformly:

I call this cognitive bias, expert bias, and my fault: I did not explain it as well as I can now. Also: wrong audience.

So, How Does It Work?

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.


What’s Implemented

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.

Learnings

LunyScript will have a different architecture in key aspects:


Media

LunyScript Demo

Godot

Godot Editor Godot Playmode

Unity

Unity Editor Unity Playmode

Unreal

Unreal Editor Unreal Playmode


Source Code Repositories

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.


Example: Player Controller Script

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());
    }
}

← Back to Main Documentation