Back to Home

LuaJIT Scratch analog: 12k scripts at 30 FPS

Article breaks down the development of a high-performance visual interpreter on LuaJIT and LÖVE2D capable of executing 12289 scripts on weak hardware. Describes modular architecture, block structure, recursive interpretation and EXE build. Example code preserved for analysis by middle/senior developers.

12 thousand Scratch scripts on LuaJIT: code breakdown
Advertisement 728x90

High-Performance Visual Interpreter with LuaJIT and LÖVE2D: 12,000 Scripts on Low-End Hardware

A newcomer has implemented a visual block programming editor capable of running 12,289 scripts with IF, ROTATE, and WHILE TRUE blocks on a single core of a low-end laptop at a stable 30 FPS. The project is built on LuaJIT with the LÖVE2D library, ensuring cross-platform compatibility: Windows, macOS, Linux, Android, iOS. The interpreter is separate from the editor, and a standalone EXE build is already functional.

The choice of LuaJIT is due to its bytecode and JIT compilation for high execution speed. LÖVE2D was used for its simple syntax and the author's experience with Roblox Studio. VS Code served as the primary editor.

Project Architecture

The project is divided into modules for scalability:

Google AdInline article slot
  • Main.lua — the main editor.
  • Micropaint.lua — the sprite editor.
  • GameLoader.lua — the game loader.
  • BlockList.lua — the block registry (518 lines, with a rendering function).

GUI is implemented manually via love.graphics without external libraries. Example button rendering:

--BUTTON: Load
love.graphics.rectangle("line", 230, 10, 120, 40)
love.graphics.print("Load", 240, 10)
--BUTTON: Create
love.graphics.rectangle("line", 360, 10, 100, 40)
love.graphics.print("Create", 370, 10)

Click handling with detailed comments:

--BUTTON Create
if x > 360 and x < 460 and y > 10 and y < 50 then
    createNewGame()
end
--BUTTON LOAD
if x > 230 and x < 350 and y > 10 and y < 50 then
    if gload() == "suc" then
        GameLoaded = true
        love.window.showMessageBox("READY!","Game named "..game.name.." loaded")
    else
        love.window.showMessageBox("ERROR","An error occurred during loading")
    end
end

Comments simplify maintenance, though GUI libraries would have made the code cleaner.

Google AdInline article slot

Block Structure and Rendering

Blocks are described in tables with metadata. Example random number block:

t[#t+1] = {
    type = 3, --BLOCK TYPE 1 - REGULAR 2 - EXECUTION 3 - DATA
    isStarter = false, --IS THIS A SCRIPT START BLOCK???
    DisplayName = "Random Value (from, to)", -- DISPLAY NAME
    name = "random", -- SYSTEM NAME
    args = 2, -- NUMBER OF ARGUMENTS
    containBlocksIn = false, -- CONTAINS BLOCKS INSIDE
    color = {0, 0, 0}
}

Blocks support nesting: arguments can be other blocks. Rendering via RenderBlock() with an attempt at recursion (partially implemented). White squares are input fields.

The interpreter runs in a single thread for simplicity. Blocks are divided into types: regular, execution, data. Argument execution is recursive, with results in args[i].data.

Google AdInline article slot

Block Interpretation

The interpreter code checks name and computes the value:

if name == "getX" then --Get X
        return sprite.pos.x
    elseif name == "getY" then --Get Y
        return sprite.pos.y
    elseif name == "random" then --Random
        return love.math.random(tonumber(args[1].data),tonumber(args[2].data))
    elseif name == "getrotation" then --Rotation
        return sprite.rotation
    elseif name == "getcostume" then --Costume
        return sprite.costumeNumber
    elseif name == "getsize" then --Size
        return sprite.size
    elseif name == "mathplus" then --Plus
        return tonumber(args[1].data) + tonumber(args[2].data)
    elseif name == "mathminus" then --Minus
        return tonumber(args[1].data) - tonumber(args[2].data)
    elseif name == "mathmultiply" then --Multiply
        return tonumber(args[1].data) * tonumber(args[2].data)
    elseif name == "mathdivide" then --Divide
        return tonumber(args[1].data) / tonumber(args[2].data)
end

Lua arrays are indexed from 1. LÖVE2D does not support Russian out of the box — system fonts (Comic Sans and others) were used via love.graphics.newFont.

Build and Deployment

EXE build:

  • Love Fusion packages DLLs and files into an EXE.
  • Enigma Virtual Box combines everything into a single executable.

APK/HTML support is planned. Demonstration: 12,300 threads with IF/ROTATE/WHILE TRUE at 30 FPS.

Key Points:

  • LuaJIT + LÖVE2D enable 12k+ scripts on a weak CPU.
  • Modular architecture: interpreter separate from editor.
  • Manual GUI via love.graphics, nested blocks with recursive execution.
  • Cross-platform build to EXE/APK/HTML.
  • Blocks are typed (data/execution), arguments computed recursively.

— Editorial Team

Advertisement 728x90

Read Next