Migrating an existing project
If you already have a Bevy 0.18 game, you can wire jackdaw in without starting from scratch. The diff is small, but there are a few gotchas. This page walks through it.
The end state matches what cargo generate produces from the
game-static template:
src/lib.rsholds yourMyGamePlugin.src/main.rsis a thin standalone runner.src/bin/editor.rsis the editor + game binary.- Scene data lives in
assets/scene.jsn.
You can read the templates directly at
templates/game-static/
to compare against your project as you go.
1. Bump bevy to 0.18
If your project is on an older bevy, bump it first and get
cargo run working again. Jackdaw doesn’t have a story for
older versions.
2. Cargo.toml deltas
Add these lines:
[features]
default = []
editor = ["dep:jackdaw"]
[dependencies]
bevy = { version = "0.18", features = ["file_watcher"] }
jackdaw = { version = "0.4", default-features = false, optional = true }
jackdaw_runtime = "0.4"
ctrlc = "3"
[[bin]]
name = "editor"
required-features = ["editor"]
Notes:
bevy/file_watcheris what powers hot-reload ofassets/scene.jsnin the standalone runner.jackdawis optional and gated behindeditor. Without that feature your standalone game has no editor deps.jackdaw_runtimeis the small runtime-only crate that loads scenes from.jsn. Always present.ctrlcclaims SIGINT and SIGTERM before wgpu and gilrs swallow them. Without it, Ctrl+C in your terminal won’t kill the game.
If 0.4 isn’t on crates.io yet (it isn’t at the time of writing),
patch to a local checkout:
[patch.crates-io]
jackdaw = { path = "/path/to/jackdaw" }
jackdaw_runtime = { path = "/path/to/jackdaw/crates/jackdaw_runtime" }
3. Move gameplay into a plugin
If everything currently lives in main.rs, that won’t fly. The
editor binary needs to add its own plugins on top of yours, so
your gameplay has to be reachable as a Plugin.
In src/lib.rs:
#![allow(unused)]
fn main() {
use bevy::prelude::*;
use jackdaw_runtime::prelude::*;
#[derive(Default)]
pub struct MyGamePlugin;
impl Plugin for MyGamePlugin {
fn build(&self, app: &mut App) {
// your systems, observers, resources
}
}
}
Anything you used to write inline in main() after
App::new() moves into build(). With one important
exception, see step 5.
4. Standalone main.rs
Replace your main.rs with something close to this:
use bevy::prelude::*;
use jackdaw_runtime::prelude::*;
fn main() -> AppExit {
let _ = ctrlc::set_handler(|| std::process::exit(130));
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(JackdawPlugin)
.add_plugins(your_crate::MyGamePlugin)
.add_systems(Startup, spawn_initial_scene)
.run()
}
fn spawn_initial_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(JackdawSceneRoot(asset_server.load("scene.jsn")));
}
JackdawPlugin is what spawns the entities listed in
assets/scene.jsn. Without it your runtime has no scene.
5. Editor binary
Create src/bin/editor.rs:
use bevy::prelude::*;
use jackdaw::prelude::*;
use jackdaw::project_select::PendingAutoOpen;
use std::path::PathBuf;
fn main() -> AppExit {
let _ = ctrlc::set_handler(|| std::process::exit(130));
let project = std::env::var_os("JACKDAW_PROJECT")
.map(PathBuf::from)
.or_else(|| std::env::current_dir().ok());
let mut app = App::new();
app.add_plugins(DefaultPlugins)
.add_plugins((PhysicsPlugins::default(), EnhancedInputPlugin))
.add_plugins(EditorPlugins::default())
.add_plugins(your_crate::MyGamePlugin);
if let Some(root) = project.filter(|p| p.is_dir()) {
app.insert_resource(PendingAutoOpen { path: root, skip_build: true });
}
app.run()
}
The important bit is adding PhysicsPlugins and
EnhancedInputPlugin here, not in MyGamePlugin. Both the
editor and your game need them, so if MyGamePlugin adds them
too you’ll get a “plugin already added” panic.
Same applies to any other ambient plugin (AhoyPlugins, your
own UI plugins, etc): they go next to DefaultPlugins, not in
MyGamePlugin.
6. Move authored data into the scene
If your existing game spawns entities in code (lights, cameras,
level geometry), pick the ones that should be authorable in the
editor and move them out. They’ll live in assets/scene.jsn
instead.
For each component you want to author in the editor, derive
Reflect:
#![allow(unused)]
fn main() {
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct PlayerSpawn;
}
That’s all you need. The component shows up in the editor’s Add Component picker. See Custom Components for the full story.
7. Try it
cargo run # standalone
cargo editor # editor + game
The editor opens, picks up your project, and you can author
scene data. The standalone reads the same scene.jsn.
Common gotchas
“plugin already added” panic on cargo editor. Either
MyGamePlugin is adding DefaultPlugins, PhysicsPlugins,
EnhancedInputPlugin, or some other plugin the editor already
added. Move it to main.rs / editor.rs.
Component doesn’t show in the picker. Probably missing
#[derive(Reflect)] or #[reflect(Component)]. If both are
present and it still doesn’t show, check if it has
@EditorHidden somewhere (it shouldn’t, for your own types).
Scene loads but observer queries return wrong values. If
you have an On<Insert, T> observer that reads
GlobalTransform, it should work. The scene loader runs
transform propagation inline before user component inserts.
If you see weird positions, file a bug.
Standalone game crashes on scene load. Most likely your
Cargo.toml has panic = "abort" and a reflected component
in your scene file no longer matches its current type
definition. The deserialize step returns errors cleanly, but a
genuinely panicking insert will kill the process. Fix the
schema drift, don’t try to swallow the panic.