Usagi: a small 2D engine in Rust that runs Lua 5.5 and ships one-command builds
A simple 2D game engine for rapid prototyping with Lua, featuring live reload and cross-platform export; this repo is a mirror and development happens at: https://codeberg.org/brettchalupa/usagi
At a glance
- What is it?
- Usagi is a 2D game engine for making pixel art games in Lua, with live reload that keeps game state, a single export command for Linux, macOS, Windows and the web, a built-in pause menu with remapping, and hard constraints such as a 320x180 default resolution and one sprites.png. The engine itself is Rust, the scripting is stock Lua, and the licence is the Unlicense.
- Who is it for?
- Adopt Usagi if you are prototyping a 2D pixel art game and want a fixed, small API you can learn in an afternoon, with live reload that does not reset your game state. Do not adopt it for a mobile or VR title, for a polished medium-to-large game, or if you need an arbitrary API, because the README is explicit that the engine is not a fantasy console and not a Love2D replacement.
- Can I use it commercially?
- Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 13 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the engine is and the two rules it imposes
Usagi is a 2D game engine for making pixel art games in Lua 5.5. The README summarises it as simple, and the features list backs that up with a small fixed API: you cannot do everything, but you have got what you need to make a great 2D game.
The design philosophy is stated as constraints to inspire creativity, and the constraints are specific. The default resolution is 320x180. The default sprite grid is 16x16. Textures come from a single sprites.png file. Bring your own sound effects, sprite editor and music tools.
That is an unusual framing and it is the whole point of the engine. A 320x180 canvas with a 16x16 sprite grid is a Pico-8-shaped constraint, and the README says so directly: the engine is a good fit if you want to quickly try out an idea, if you are new to game programming, if you have hit Pico-8's token limit, or if you want something simpler than Love2D.
The rules exist to keep the engine small enough to have a fixed API. A resolution and a sprite grid that do not change mean the renderer, the input layer and the asset format can all be simple, and a Lua developer is not handed a hundred ways to do the same thing.
The engine is written in Rust and the Cargo.toml says the description is a simple 2D game engine for rapid prototyping with Lua, featuring live reload, powered by Rust and sola-raylib. The version in the manifest is 1.4.0-dev, the edition is 2024 and the licence is the Unlicense, which is the public domain dedication. The README says the project is free software by Brett Chalupa and dedicated to the public domain.
Live reload that does not lose game state
Live reload is the first feature in the list and the one that changes how you work.
The command is usagi dev, which watches your code and assets. Saves apply without losing game state, so you can tweak a sprite in your editor and see it update instantly. The project section of the README adds detail about what reloads: the script, sprites and sfx reload on save, and a Reset action re-runs _init.
The distinction between a hot reload that restarts the game and one that preserves state is the difference between a feature you use constantly and one that gets in the way. If every save drops you back to the start of the level, you stop saving. If the save is invisible, you can iterate on a boss fight by nudging one number.
The bootstrap is three commands:
usagi init my_game
cd my_game
usagi devThe real sequence is init, then cd, then dev, and init is what writes the project skeleton. It writes main.lua with _init, _update and _draw stubs,.luarc.json for Lua LSP support,.gitignore, meta/usagi.lua holding API type stubs, and USAGI.md, which is a copy of the documentation.
The last one is interesting. The engine ships a copy of its own documentation into every project, which means the API reference travels with the code and cannot drift from the engine version that made it. The matching update path is usagi refresh, run inside a project, which refreshes the LSP type stubs and the embedded docs without touching main.lua.
And the drawing stub that init writes is the smallest possible program:
function _draw(_dt)
gfx.clear(gfx.COLOR_BLACK)
gfx.text("Hello, Usagi!", 10, 10, gfx.COLOR_WHITE)
endThe signature takes a delta time argument, which tells you the loop is frame-rate aware.
The project layout and the file conventions
A Usagi game is either a single .lua file or a directory with a main.lua in it. Additional .lua files anywhere under the project root load with stock Lua's require, and optional assets live alongside the source code.
The README gives a full multi-file layout, and the conventions in it are the interesting part:
my_game/
main.lua -- required: your game's entry point
sprites.png -- optional: 16×16 sprite sheet (PNG with alpha)
palette.png -- optional: custom palette (1px tall, one color per pixel)
font.png -- optional: custom font (bake with `usagi font bake`)
enemies.lua -- optional: require "enemies"
data/
level.json -- optional: JSON data, loadable with `usagi.read_json("level.json")
dialog.txt -- optional: text data, loadable with `usagi.read_text("dialog.txt")
scenes/
main_menu.lua -- optional: require "scenes.main_menu" - source code can be in folders
sfx/ -- optional: .wav files, file stems become sfx names
jump.wav
coin.wav
music/ -- optional: .ogg/.mp3/.wav/.flac, file stems become track names
overworld.ogg
boss.ogg
shaders/ -- optional: post-process GLSL shaders (advanced; see Shaders)
crt.fs -- desktop GLSL 330
crt_es.fs -- web + Raspberry Pi GLSL ES 100Several conventions come out of that. The sfx and music directories use the file stem as the name, so jump.wav is the jump sound, with no manifest. The data directory holds files loaded through explicit calls, usagi.read_json and usagi.read_text, rather than being loaded implicitly. And the shaders directory carries two variants of the same effect, one in desktop GLSL 330 and one in GLSL ES 100 for the web and Raspberry Pi, which tells you that the web export is a first-class target rather than an afterthought.
The require resolution rules are spelled out. require "name" resolves to name.lua in the project root, falling back to name/init.lua if that misses, and dotted names such as require "world.tiles" become slash-separated paths. The same lookup works inside a fused or exported build, so a multi-file project ships as a single binary or .usagi file with no extra configuration. Modules are cached on require, and the README shows the pattern with a player module returning a table and main.lua pulling it in.
There is a restriction worth noting. Use require only, never loadfile, dofile or io to load .lua files, because those read from the OS filesystem, which the README says breaks the fused build.
Export, the built-in menu and the tools window
The export story is the other headline feature. usagi export packages your game for distribution, producing zips for Linux, macOS, Windows and the web, plus a portable .usagi bundle. One command, four targets.
The Cargo.toml explains how the web target is kept workable, and the reasoning is worth reading because it shows the constraint. The wasm runtime never reaches usagi export, since there is no CLI on the web, and ureq's rustls dependency transitively pulls in ring, which does not build for wasm32-unknown-emscripten. Gating the whole export and templates dependency chain keeps the wasm build green and the wasm binary smaller.
There is also a gles feature flag, which builds against OpenGL ES 2.0 with GLSL 100 instead of desktop GL 3.3, and the comment says it is used by the linux-aarch64 release for better compatibility. That is the same distinction the shaders directory reflects.
The pause menu is described as free, meaning built in rather than something you write. It has sound effect and music volume, a fullscreen toggle, and per-game keyboard and gamepad remapping. The screenshot in the README shows the menu with Continue, Settings, Clear Save Data, Reset Game and Quit options, which means save management and a reset are part of the shell rather than of your game.
Save data itself is one function, saving and loading your game state as a Lua table. For a prototype engine that is the right amount of persistence.
The tools window is separate: usagi tools opens the Usagi tools window with a jukebox and a tile picker. That is a development aid rather than a runtime feature, and the justfile exposes it as a recipe so you can run it against an example project.
Running the binary with no arguments at all opens a small launcher window where you can drop a main.lua or a project folder onto it and Usagi starts usagi dev on that path.
The Rust side: sola-raylib, mlua and a careful dependency split
The Cargo.toml is where the engineering decisions become visible, and it is worth reading closely.
The rendering layer is sola-raylib at version 6.3, with the noscreenshot feature enabled, which is the raylib binding for Rust with the screenshot capability turned off because the engine has its own export path. The Lua layer is mlua 0.11 with three notable features: lua55 for the Lua version, vendored so the interpreter is compiled into the binary rather than linked against a system one, and serde for serialisation.
That vendored choice is what makes the export promise work. A game that links against the system Lua will fail on a machine without it, so the packaged builds would not be portable. Vendoring the interpreter costs binary size and buys self-contained executables.
Two more dependencies have specific jobs. sha2 provides SHA-256 verification for downloaded templates against published sidecars, with a game_id hash as a fallback. And png is required on every target including wasm, because the engine reads the embedded zTXt glyph metadata from font.png at startup; the bake-side encode path is only used by the native usagi font bake subcommand.
The rest of the manifest shows how the project treats itself. The package is named usagi, the keywords are gamedev and lua, the categories are game-engines, and the exclude list drops examples, docs, the justfile and shell scripts from the published crate. The command line is clap 4 with the derive feature, and serde_json handles JSON.
The repository is organised with a justfile rather than a Makefile, and the recipes are a good map of the workflow. just setup installs the git hooks by pointing core.hooksPath at .githooks, so the pre-push hook runs for everyone, and installs cargo-about. just ok runs the three checks in sequence: cargo fmt --check, cargo clippy with warnings denied, and cargo test. just run launches the hello example. The licenses recipe regenerates THIRD_PARTY_LICENSES.md from the current Cargo.lock, and the CI fails if that output drifts from what is committed.
Goals, non-goals and where the project actually lives
The README has a section on goals and non-goals, and the non-goals are the most useful part of it for anyone deciding whether this is the right engine.
Usagi is for rapid 2D pixel-art prototyping in Lua. It is not a fantasy console and it is not a Love2D replacement. It does not target mobile or VR, and it is not built for medium-to-large polished games.
The reasons given for choosing Lua are worth noting: small, widely used in game tooling, and powerful enough to stay out of your way. The Love2D comparison is the key one, since Love2D is the obvious alternative for a Lua 2D engine. Usagi is positioned as simpler than Love2D, and the constraint list is how it achieves that.
The distribution channels are unusual for a game engine and worth noting. The engine downloads come from Codeberg, from GitHub and from itch.io, and the repository itself is a mirror, with development happening at codeberg.org/brettchalupa/usagi. The README's own links section points to the website, a book, a Discord server and a Reddit community, plus a 55 second engine trailer.
There is also a Homebrew tap through the community tap mechanism, with brew tap and brew install usagi as the two commands. The brew tap command points at the Codeberg URL rather than a GitHub one, which is a small reminder that the canonical home is Codeberg.
For development, the repository carries a DEVELOPING.md, a README_DEV.md, a book/ directory, a website/ directory, a web/ directory, a runtime/ directory, templates/ and a scripts/ directory. The examples/ directory is a good index of what the engine can do, with examples for bunny mark, a custom font, a custom menu, a custom sprite size, a demo scene, diagnostics, dialog, easing, effects, input, keyboard, LDTK levels, levels from CSV and from JSON, localization, loveify, menu items, mouse behaviour, mouse physics, mouse UI, multiple files, music and a Notetris clone.
Limits: a fixed API, a narrow platform story, and a single maintainer
The limits here are deliberate rather than accidental, but they are real.
The first is the API ceiling. The README says the API is small and fixed, and you cannot do everything. A 2D engine with a 320x180 default, a 16x16 sprite grid and a single sprites.png will not do 3D, will not do arbitrary resolution without a thought, and will not give you the graphics feature set of a general engine. For a prototype that is exactly right. For a game with a camera system, a lighting model or a particle emitter, it is a rewrite.
The second is the platform story. The export targets are Linux, macOS, Windows and the web, and the engine itself does not target mobile or VR. That last exclusion is stated as a non-goal rather than an oversight, but it is worth noting that the export list has no iOS or Android target, so a project that needs one is out of scope.
The third is the maintenance concentration. The Cargo.toml has a single author, the README is written in the first person, and the project lives on Codeberg with a GitHub mirror. For a tool whose selling point is being small and fixed, that is acceptable. For anything you depend on for a long project, it is a factor.
The fourth is the Rust toolchain requirement for development. Building from source needs a recent Rust toolchain and, for the web target, emscripten plus a wasm target and a static server, per the justfile's web setup recipe. End users downloading a binary never see this, but contributors do, and the engine's Lua API being small does not make the engine itself small to work on.
The fifth is the interpreter version. The engine embeds Lua 5.5 through mlua's lua55 feature. If you have Lua code or libraries that assume 5.4 or 5.3 semantics, you have to check compatibility, and the vendored interpreter means you cannot swap in the system Lua to get a different version.
Usagi against Pico-8, against Love2D and against Godot
Three comparisons make the choice clearer.
The first is against Pico-8, which the README names twice as a reference point. Pico-8 is a fantasy console with a hard token limit, a fixed resolution and a fixed palette, and it has a hard token limit. Usagi shares the constraints idea, gives you a real Lua instead of a Lua dialect, and gives you a native binary with an export path. The README frames the Pico-8 user as the person who has hit the token limit, and Usagi is the answer for someone who wants to keep writing ordinary Lua but still likes the discipline of a small target.
The second is against Love2D, and the README's positioning is explicit: Usagi is simpler than Love2D and not a replacement for it. The difference is the amount of surface. Love2D gives you the whole 2D API and you decide how to structure your game Usagi gives you a fixed subset, with live reload, a pause menu and an export pipeline already done, and you give up everything the fixed subset excludes. If you know what you are building and it fits the constraints, the smaller engine is less work. If you do not know yet, Love2D's generality is worth more than Usagi's conveniences.
The third is against Godot, which is not mentioned in the README and is a different category of thing entirely. Godot is a full game engine with a scene tree, a visual editor, a scripting language that is optional, and 3D. Choosing Usagi over Godot is not a like-for-like decision; it is a decision to prototype a small 2D game without an editor, without a scene system and without 3D, and to get there with a text editor and a Lua file.
The pattern across all three is the same. Usagi is a tool for a specific and narrow activity, and it is honest about the boundaries because the author built it for his own way of working and says so.
Editorial conclusion
Adopt Usagi if you are prototyping a 2D pixel art game and want a fixed, small API you can learn in an afternoon, with live reload that does not reset your game state. Do not adopt it for a mobile or VR title, for a polished medium-to-large game, or if you need an arbitrary API, because the README is explicit that the engine is not a fantasy console and not a Love2D replacement. Verify first that your target resolution and sprite size fit the engine's constraints, that the Lua version your code assumes is 5.5, and that the export target you need is one of the four the README lists.
Frequently asked questions
How do I create and run a new Usagi project?
Run usagi init my_game to write the project skeleton, which includes main.lua with _init, _update and _draw stubs, .luarc.json for Lua LSP support, .gitignore, meta/usagi.lua with API type stubs and a copy of the documentation as USAGI.md. Then cd into the directory and run usagi dev, which watches your code and assets and applies saves without losing game state.
What platforms can Usagi export a game to?
usagi export packages the game as zips for Linux, macOS, Windows and the web, plus a portable .usagi bundle. The engine does not target mobile or VR, which the README lists among its non-goals, and the web build uses a GLSL ES 100 shader variant alongside the desktop GLSL 330 one.
How do multiple Lua files work in a Usagi project?
Use require only. require "name" resolves to name.lua in the project root and falls back to name/init.lua, and dotted names such as require "world.tiles" become slash-separated paths. The same lookup works inside a fused or exported build, so a multi-file project ships as a single binary or .usagi bundle with no extra configuration.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/brettchalupa-usagi)Was this useful?