A Multiplayer-First Server-Rendered Game Engine with Lua Scripting.
curl -fsSL https://cleoselene.com/install | bash
Supported: macOS (Universal: M1/M2/M3 & Intel), Linux (x64). Windows soon.
Code multiplayer games as simple as single-player. No more worrying about client-server state sync or lag compensation. Just one loop, everywhere.
We stream drawing primitives instead of heavy pixels or complex state objects. It's faster than cloud gaming and lighter than traditional networking.
All performance-critical logic (Physics, Pathfinding) runs in Native Rust. Communication is powered by WebRTC for sub-frame latency.
Since the game is entirely server-rendered, the client has zero knowledge of the game state. No wallhacks, no aimbots, no compromise.
Keep your game's internal logic and secrets safe. Since your script never leaves the server, players can't reverse-engineer your code or find hidden content.
Total freedom. This version of the engine is free for personal or commercial use on your own infrastructure. No lock-in, no per-player fees.
Built for the AI era. Use the --debug-mcp flag to let LLMs (Claude, Gemini) inspect variables, evaluate code, and visualize game state in real-time.
Download the binary and run:
cleoselene my_game.lua
A Multiplayer-First Server-Rendered Game Engine with Lua Scripting.
| Flag | Description |
|---|---|
-h, --help |
Print this help manual and exit. |
-V, --version |
Print engine version and exit. |
--port <PORT> |
Port to start the server on (default: 3425). |
--debug |
Enable the remote debug endpoint at /debug. |
--test |
Run the script in headless test mode (init + 1 update) and exit. |
A minimal game script (main.lua) must implement these callbacks:
-- Called once when the server starts
function init()
-- Initialize physics, load assets, setup state
db = api.new_spatial_db(250)
phys = api.new_physics_world(db)
api.load_sound("jump", "assets/jump.wav")
end
-- Called every server frame (typically 30 TPS)
function update(dt)
-- Advance physics simulation
phys:step(dt)
end
-- Called for EACH connected client to generate their frame
function draw(session_id)
api.clear_screen(20, 20, 30)
api.set_color(255, 255, 255)
api.draw_text("Session: " .. session_id, 10, 10)
end
-- Network Events
function on_connect(session_id) end
function on_disconnect(session_id) end
function on_input(session_id, key_code, is_down) end
The engine uses a fixed virtual coordinate system of 800x600. The output is automatically scaled to fit the client screen.
| Method | Description |
|---|---|
api.clear_screen(r, g, b) |
Clears the frame with a background color. |
api.set_color(r, g, b, [a]) |
Sets the current drawing color. |
api.fill_rect(x, y, w, h) |
Draws a filled rectangle. |
api.draw_line(x1, y1, x2, y2, [width]) |
Draws a line. |
api.draw_text(text, x, y) |
Draws text at position. |
api.load_image(name, url) |
Preloads an image/sprite from a URL or local path. |
api.draw_image(name, x, y, [w, h, sx, sy, sw, sh, r, ox, oy]) |
Draws a (sub)image with optional scaling, rotation, and origin. |
api.load_sound(name, url) |
Preloads a sound from a URL/path. |
api.play_sound(name, [loop]) |
Plays a loaded sound. |
api.stop_sound(name) |
Stops a sound. |
api.set_volume(name, volume) |
Sets volume (0.0 to 1.0). |
local db = api.new_spatial_db(cell_size)
local phys = api.new_physics_world(db)
| Method | Description | Returns |
|---|---|---|
db:add_circle(x, y, radius, tag) |
Registers a circular entity. | id |
db:add_segment(x1, y1, x2, y2, tag) |
Registers a line segment. | id |
db:remove(id) |
Removes an entity. | nil |
db:update(id, x, y) |
Teleports an entity. | nil |
db:get_position(id) |
Returns current x, y. |
x, y |
| Method | Description |
|---|---|
phys:add_body(id, props) |
Adds physics: {mass, restitution, drag}. |
phys:set_velocity(id, vx, vy) |
Sets body velocity. |
phys:get_velocity(id) |
Returns vx, vy. |
phys:set_gravity(x, y) |
Sets global gravity. |
phys:step(dt) |
Advances simulation and updates DB. |
phys:get_collision_events() |
Returns list of collisions: {{idA, idB}, ...}. |
| Method | Description |
|---|---|
db:query_range(x, y, r, [tag]) |
Finds entity IDs within radius r. |
db:query_rect(x1, y1, x2, y2, [tag]) |
Finds entity IDs within AABB. |
db:cast_ray(x, y, angle, dist, [tag]) |
Returns id, frac, hit_x, hit_y. |
Start engine with --debug.
/debug)Send POST requests with Lua code to http://localhost:3425/debug.
# Example: Inspect player state
curl -X POST -d "local State = require('state'); return State.players" http://localhost:3425/debug
Start engine with --test.
cleoselene my_game.lua --test
Headless mode: runs init() and one update(0.1) cycle, then exits with code 0 (success) or 1 (error).