Getting Started
You need a recent Rust toolchain (edition 2024) and this repository cloned somewhere.
cargo run -p kooch_editor
That opens the Hub, where projects are created and opened. Everything else follows from there:
- Your First Project — the whole loop end to end: a component, a system, and Play. Start here.
- Creating a Project — what the scaffold generates and why each file exists.
- The Editor — the panels, how Play works, and what is not built yet.
The rest of this page is the handful of things that are easy to trip over and do not belong to any one of those.
One engine per machine, shared by every project
The editor materialises the engine once per version in
~/.local/share/kooch/<version>/engine
and every project’s Cargo.toml points at it:
kooch = { path = "/home/you/.local/share/kooch/0.1.0/engine" }
Nothing is copied into the project. Two projects on the same engine version share one directory; two versions coexist, so a project pinned to an older engine keeps building after the editor updates.
⚠️ That path is absolute and $HOME differs per user, so a project that
changes machines names a directory that is not there. The editor owns
that line — it owns the directory it names — and rewrites it when a
project opens. Nothing to do by hand.
KOOCH_ENGINE_HOME overrides the base, for CI and for portable installs
that must not write to the user’s data directory.
It costs no build time. A project always compiled the engine from source; this only changes where the source is.
When that directory is replaced
Every materialised engine records which source tree it came from, in a
.kooch-engine-stamp beside it — the version, plus a digest of every
file. An editor compares its own source against that stamp, and says
so rather than acting on it:
Engine 0.1.0 — same version, different source than this editor ships
[ Install ] [ Keep ]
Install replaces the directory, leaving one copy behind, never two. Keep leaves it alone. Nothing is replaced under a project that was about to be built, which is what used to happen with only a log line to show for it.
🔴 A missing engine is installed without asking: there is nothing to keep, and a project that cannot build at all is not a choice worth offering.
🔴 What Keep cannot promise: engines are named major.minor.patch and
replaced in place, so keeping one holds until something else installs
over it — updating from another project, for instance. Two engines with
the same version have nowhere separate to live.
⚠️ Installing is refused while a build is running. Renaming the directory cargo is reading produces an error about a missing file in a crate nobody touched.
What is on this machine
Settings lists every installed engine, marks the one this editor ships and the one the open project uses, and removes the rest. Those two cannot be removed: both are named by a manifest, and deleting one leaves it pointing at nothing.
New versions are not created from there. The version is the engine’s
own major.minor.patch, so a new directory appears when an editor
shipping that version opens a project.
🔴 Without it, a new editor never updated the engine. The directory
is named after the engine version, that version used to be 0.1.0 for
every development build, and the old check only asked whether
Cargo.toml, crates and src existed — which is true of every copy of
the engine ever made. So a freshly installed editor found the directory,
called it current, and every project on the machine went on compiling
against weeks-old source with nothing said.
The version moves on every pull request now, automatically:
.github/workflows/version.yml bumps [workspace.package] in the PR’s
own branch — major for a ! or a BREAKING CHANGE:, minor for feat:,
patch for everything else. A number that sits still is what made all
three mechanisms that depend on it — this directory’s name, the
BuildStamp a project compares itself against, and the pipeline cache —
blind at once. Label a PR no-version-bump to opt out.
⚠️ One directory per version, and they are not cleaned up automatically. A version a week now means a directory a week; Settings is where the unused ones are removed.
⚠️ The build right after a replacement is a full rebuild, since every
engine source file is now newer than the project’s target/. That cost
is why installing is a question rather than something that happens while
you are opening a project to look at a scene.
A version this editor does not ship is never touched: that directory is what a pinned project builds against, and differing from the source in hand is the reason it exists rather than a reason to overwrite it.
Checking a copy that went wrong
The comparison above catches a stale engine, not a damaged one: deleting a file from a copy does not change what the copy claims to be.
KOOCH_VERIFY_ENGINE=1 kooch_editor
re-reads the whole tree, compares it against its own stamp, and re-copies when they differ. Off by default because it reads 8 MB every time a project opens.
⚠️ Rust is still required to build a project. Gameplay is native Rust compiled into the game, so the toolchain is not optional the way it is in an engine whose gameplay is a script.
Why the source is on disk at all
Because Rust has no stable ABI. A precompiled rlib links only against
the exact compiler and the exact dependency versions that built it, and
cargo does not model binary dependencies — which is why no Rust engine
ships binaries, Bevy included. The only route to “binary, no source” is
an extern "C" API in the shape of Godot’s GDExtension, and it costs the
typed ECS.
So the engine’s source is protected the way Unreal protects theirs: by licence, not by hiding it.
The licence is not optional
LICENSE.md is vendored with the engine, and the facade compiles it in:
#![allow(unused)]
fn main() {
pub const LICENSE: &str = include_str!("../LICENSE.md");
}
A game links the engine as an rlib, so that text is inside every
shipped executable. It is not a file someone has to remember to copy;
removing it means not building.
Packaging the editor
cargo build --release -p kooch_editor
cargo run --release --features editor --example package_editor -- dist/
dist/
kooch_editor the binary
engine/ 7.7 MB — the source it materialises for projects
.kooch-engine-stamp which tree this is, so an install can tell
whether it is newer than what is on the machine
assets/ what the editor itself renders with
engine_vendor::vendor_source looks in three places, in order:
KOOCH_ENGINE_SOURCE, engine/ next to the executable, and the engine
root — which only resolves when running from the engine’s own tree.
⚠️ package_editor refuses a binary older than the source. It once
shipped an editor built before this feature existed, and the AppImage
made from it wrote its own mount point into a project — a directory that
stops existing when the app closes.
⚠️ It packages for the platform it runs on. An editor for Windows
means running it on Windows, the same conclusion Bevy’s release workflow
reaches: metis is vendored C, which makes cross-compiling more than a
target flag.
Developing the engine itself
When the editor runs out of the engine’s own target/, project creation
points the manifest at the live clone and materialises nothing —
otherwise every engine change would need a re-materialise before the game
could see it. The check is where the executable is, not where the
source is.
Loading a scene
The boot scene is resolved in this order:
SceneBootstrapPlugin::with_scene(path), if yourmain.rssets one explicitly.--scene <path>on the command line — absolute, or relative to the working directory.scenes/default.scenebeside the executable.- The same, relative to the working directory.
So cargo run from the project root just works: the default path resolves because
the working directory is the project. A different level is
cargo run -- --scene scenes/Level1.scene.
🔴 Why the executable comes first
A shipped game is opened by double-clicking it, and that leaves the working directory
wherever the desktop felt like — your home, or /. Resolved against the cwd alone, a
released game starts with an empty scene and no error: the file was not missing from the
package, it was never looked for in the package.
So a packaged game keeps its content beside the binary:
dist/
mygame the executable
scenes/ default.scene, and the rest
assets/ everything the scenes reference, by GUID (.meta included)
The cwd stays as the fallback because that is what a plain cargo run inside a project
relies on — there the executable lives in target/debug/ and has no scenes/ beside it.
⚠️ The .meta sidecars are not optional. A scene references its assets by GUID, and the
GUID lives in the .meta next to each file. A copy that filters by extension and leaves them
behind produces a game that loads its scene and renders nothing.
Component registration runs before the scene loads
SceneBootstrapPlugin loads at Stage::First, which runs after every Stage::Startup
system has completed on the first frame. Component registration is a Startup system, so the
registry is fully populated by the time the scene is deserialised.
Flip that order and you get unknown component type: … and a scene that does not load. The
generated registrations.rs already puts registration in Startup; this matters only if you
register something by hand.
A scene with no camera renders black
The editor’s own camera is filtered out of a saved scene, so a scene needs to spawn its own
PerspectiveCamera. The default scene template includes one. Build a scene from scratch
without one and you get the clear-to-black fallback.
This is deliberate, not a bug. Injecting the editor camera as a temporary play camera — what Unity and Unreal do — is a possible future change, not current behaviour.
Running the game
cargo run
DefaultPlugins is the group that makes this a game rather than a collection of crates:
| Plugin | Role |
|---|---|
CorePlugin | Time, the AppExit event |
EcsPlugin | Storage, SceneManager, built-in components, transform propagation |
WindowPlugin | Winit window and GPU surface |
RenderPlugin | The mesh and sky pipelines |
SceneBootstrapPlugin | Loads the boot scene at startup |
Your project’s own main.rs is what runs, so any plugin you add there is picked up.