Usagi is a simple 2D game engine for quickly prototyping simple games with Lua 5.4. It features live-reloading as your change your game code and assets. Its API is clear, consistent, and familiar.
Usagi is built with Rust and sola-raylib.
WARNING: Usagi is very early in development and not stable. APIs and commands will change.
Usagi is made by Brett Chalupa and dedicated to the public domain.
Download the latest Usagi build for your operating system.
You can also install Usagi with cargo if you have
the Rust toolchain installed:
cargo install --git https://github.com/brettchalupa/usagi.git
NOTE: the Windows build and installing via cargo don’t support the web
target with usagi compile yet.
More ways of installing Usagi will be added in the future.
You now have the usagi CLI that you can run from your shell (usagi.exe on
Windows). Create hello.lua and run usagi dev hello.lua. Then edit the new
file by adding:
function _draw()
gfx.clear(gfx.COLOR_WHITE)
gfx.text("Hello, Usagi!", 10, 10, gfx.COLOR_BLACK)
end
When you save hello.lua, the Usagi runtime automatically reloads it. Make
changes to the text and see it live update.
In most traditional game development environments, you would need to restart your game’s executable after making changes. Usagi lets you focus on coding and making art without losing the current game state, allowing for much faster iteration cycles.
Need to revise a sprite quickly? Just open it in Aseprite, tweak it, save it, and see it update in the context of your game.
Usagi does not aim to be anything more than a rapid development engine for simple, lower res 2D games. It doesn’t intend to support mobile platforms or touch or VR. It doesn’t aim to replace Love2D or Pico-8 or Picotron. It’s not a fantasy console. It’s a command-line program and suite of tools to help you make games quickly.
Usagi is great for those learning game programming. And for those who to use something more flexible than Pico-8/Picotron but more constrained than Love2D.
Why Lua: Lua is a widely-used language in game programming, and it’s quite simple yet surprisingly powerful, making it a good fit for Usagi.
If you want to build a medium-to-large polished game, Usagi would not be a good fit.
A Usagi game is either a single .lua file or a directory with a main.lua in
it. Optional assets live alongside:
my_game/
main.lua -- required: your game
sprites.png -- optional: 16×16 sprite sheet (PNG with alpha)
sfx/ -- optional: .wav files, file stems become sfx names
jump.wav
coin.wav
Run with:
usagi dev path/to/my_game for live-reload development (script, sprites, and
sfx reload on save; F5 resets state).usagi run path/to/my_game to run without live-reload.usagi tools [path] opens the Usagi tools window (jukebox, tile picker). See
the Tools section below.usagi compile path/to/my_game produces a standalone executable for the host
platform with the game’s script, sprites, and sfx fused into the binary. No
Usagi install needed to run the output. See the Compile section below.While developing Usagi itself, replace usagi with cargo run -- (for example
cargo run -- dev examples/hello_usagi.lua).
Usagi embraces a few constraints inspired by Pico-8 and Pyxel to help focus on prototyping rather than making polished high-resolution graphics. These may change in the future or be configurable.
main.lua or whatever you named your file is supportedsprites.png is the only image file for textures that
can be loadedgfx.spr uses the index based on this
sized spriteYou currently must bring your own sound effects and sprite editor. A sprite
editor could be nice in the future as part of the usagi tools.
Here’s what Usagi will support as it heads towards 1.0 release:
usagi compile creates exports for all target plaforms (Linux, macOS,
Windows, and web)Define any of these as globals; Usagi calls them:
_config() — optional. Called once at startup, before the window opens;
returns a config table. Currently supports title (defaults to “Usagi”) and
pixel_perfect (defaults to true, set false to stretch the game to fill
the window instead of integer-scaling with bars)._init() — once at start, and when the user presses F5. Put state setup
here._update(dt) — each frame, before draw. dt is seconds since last frame._draw(dt) — each frame, after update. dt same as above.function _config()
return { title = "Snake", pixel_perfect = true }
end
_config() runs before the runtime is fully alive (the window doesn’t exist
yet), so its return value is read once at startup and cached. Editing
_config() while the game is running won’t update the title or any future
config field on save; restart the session to pick up changes.
gfxDrawing. Positions are in game-space pixels (320×180). Colors are palette indices 0-15; use the named constants.
gfx.clear(color) — fill the screen.gfx.rect(x, y, w, h, color) — filled rectangle.gfx.text(text, x, y, color) — default font, 8px tall.gfx.spr(index, x, y) — draw the 16×16 sprite at index (1 = top-left) from
sprites.png.gfx.COLOR_BLACK, COLOR_DARK_BLUE, COLOR_DARK_PURPLE, COLOR_DARK_GREEN,
COLOR_BROWN, COLOR_DARK_GRAY, COLOR_LIGHT_GRAY, COLOR_WHITE,
COLOR_RED, COLOR_ORANGE, COLOR_YELLOW, COLOR_GREEN, COLOR_BLUE,
COLOR_INDIGO, COLOR_PINK, COLOR_PEACH — the Pico-8 palette, indices
0-15.inputAbstract input actions. Each action is a union over keyboard, gamepad buttons, and the left analog stick; the first connected gamepad is used.
input.pressed(action) — true only the frame the action first went down. Use
for one-shot actions (fire, jump, menu select).input.down(action) — true while the action is held. Use for movement.| Action | Keyboard | Gamepad |
|---|---|---|
LEFT |
arrow left / A | dpad left / left stick left |
RIGHT |
arrow right / D | dpad right / left stick right |
UP |
arrow up / W | dpad up / left stick up |
DOWN |
arrow down / S | dpad down / left stick down |
CONFIRM |
Z / J | south + west face (Xbox A/X, PS Cross/Square) |
CANCEL |
X / K | east + north face (Xbox B/Y, PS Circle/Triangle) |
input.pressed is edge-detected on keyboard and gamepad buttons but not on
analog sticks; track stick state in Lua if you need that.
sfxsfx.play(name) — play sfx/<name>.wav. Unknown names silently no-op.
Playing a sound while it’s already playing restarts it.usagiEngine-level info.
usagi.GAME_W, usagi.GAME_H — game render dimensions (320, 180).usagi.IS_DEV — true when running under usagi dev; false under
usagi
run and inside compiled binaries. Useful for gating debug overlays,
dev menus, verbose logging:
if usagi.IS_DEV then
gfx.text("debug", 0, 0, gfx.COLOR_GREEN)
end
Sequence-style APIs (gfx.spr, and any future sound/tile indexing) are
1-based to match Lua conventions (ipairs, t[1], string.sub).
gfx.spr(1, ...) draws the top-left sprite.
Enum-like constants (palette colors, key codes) keep their conventional
numbering. gfx.COLOR_RED is 8 because that’s its Pico-8 number, not because
it’s the 9th color.
Usagi watches the running script file and re-executes it when you save. The new
_update and _draw take effect on the next frame — your current game state is
preserved across the reload so you can tweak logic mid-play without losing
progress.
_init() is not called on a save-triggered reload._init() to reinitialize state.dev,
off in run.The chunk re-executes on save, so any top-level local bindings get fresh nil
values each time — callbacks that captured them as upvalues will see nil and
crash. The pattern:
_init.local.See examples/hello_usagi.lua and examples/input.lua for the layout.
usagi tools [path] opens a 1280×720 window with a tab bar for the available
tools. The path is optional; pass a project directory (or a .lua file) to load
its sprites.png and sfx/ assets. Without a path the tools open with empty
state.
Switch tools via the tab buttons or with 1 (Jukebox) / 2 (TilePicker).
Both tools live-reload their assets: drop a new WAV in sfx/ or save a new
sprites.png and the tools pick it up on the next frame.
Lists every .wav in <project>/sfx/ and lets you audition them. Selected
sounds play automatically on selection change (Pico-8 SFX editor style), so you
can just arrow through the list to hear each one.
Shows <project>/sprites.png with a 1-based grid overlay matching gfx.spr.
Click any tile to copy its index to the clipboard (paste it straight into your
Lua code).
usagi compile <path> packages a game for distribution. NOTE: compile
will be improved soon to support cross-platform compilation. Right now it only
supports compiling for the current operating system. Web builds won’t work
unless you install Usagi from source and follow the Web Builds section below. By
default it produces all artifacts in one export directory:
$ usagi compile examples/snake
[usagi] compiled snake-export/snake (3 file(s), 37125 bytes bundled)
[usagi] wrote snake-export/snake.usagi (3 file(s), 37125 bytes)
[usagi] wrote snake-export/web/ (3 game file(s), 37125 bundle bytes; runtime from embedded)
[usagi] export ready at snake-export/
$ tree snake-export
snake-export
├── snake # native fused executable (./snake to run)
├── snake.usagi # portable bundle (usagi run snake.usagi)
└── web/ # zip and upload to itch.io
├── index.html
├── usagi.{js,wasm}
└── game.usagi
Or pick one with --target {all,exe,bundle,web}:
$ usagi compile examples/snake --target bundle
$ usagi compile examples/snake --target web
$ usagi compile examples/snake --target exe
Notes:
./<name>-export/ for all, ./<name> for exe,
./<name>.usagi for bundle, ./<name>-web/ for web. <name> is the
project directory name (or the script’s stem for flat .lua files).
-o <path> overrides.usagi produces a Linux executable, a Windows one produces a
.exe, etc. .usagi bundles are platform-agnostic.--target web (and the web/ slice of --target all) needs the wasm
runtime. Release builds of usagi embed it at compile time, so an
installed usagi Just Works. Debug builds (cargo run) fall back to reading
the runtime from target/web/. See docs/web-build.md for
the build dance._init()..usagi file is the same bundle bytes
without the footer.Usagi compiles to wasm via emscripten so games can run in a browser. See docs/web-build.md for setup, the build/dev loop, debugging tips, and the (non-obvious) wasm exception ABI requirements.
just run - run hello_usagi examplejust ok - run all checksjust fmt - format Rust codejust serve-web - build and serve the web build at http://localhost:3535
(requires emcc on PATH; see docs/web-build.md)Usagi’s source code is dedicated to the public domain. You can see the full details in UNLICENSE.