Bevy Engine skill for Codex
Rust ECS games that compile on your Bevy version. Codex reads the Bevy version from Cargo.toml before writing a line and matches its idioms, from required components to messages and observers. It structures the game as plugins with explicit states and system ordering, runs simulation in FixedUpdate, resolves query conflicts with filters or ParamSets, sets compile profiles that keep iteration fast, and proves logic with headless App and World tests.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- bevy-engine Installs to ~/.agents/skills/bevy-engine/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install bevy-engine The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project bevy-engine Installs to .agents/skills/bevy-engine/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/bevy-engine.zipunzip -o bevy-engine.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) bevy-engine PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $bevy-engine, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds games in Rust with the Bevy engine - App and plugins, components with required components, systems and queries, Commands, resources, schedules (Startup, Update, FixedUpdate), states with OnEnter/OnExit, messages and observers, asset loading, system ordering and run conditions, fast compile settings, headless tests and web builds - while tracking Bevy's frequent breaking releases. Use when writing or upgrading a Bevy project, a system does not run or runs in the wrong order, a query conflicts at startup, compile times are slow, or code from a tutorial does not compile against the project's Bevy version.
Use it when
- Building a game or simulation with Bevy in Rust
- Systems run in the wrong order, never run or conflict at startup
- Tutorial or crate code targets a different Bevy version
- Compile times or dev-build performance are painful
Use something else when
- You are designing an ECS from scratch or choosing an ECS library: use ecs-architecture
- The question is Rust borrow checking outside Bevy: use rust-ownership
How to check the result
cargo check, clippy with -D warnings and cargo test pass against the pinned Bevy version, and every game state is entered and exited once in a manual run.
The mistake it prevents
Copying code from a tutorial written for an older Bevy release, so bundles, event types or time methods no longer exist and the fixes cascade.
Done well means: code compiles against the exact Bevy version in Cargo.toml (not a tutorial’s
version), gameplay is split into plugins, systems are small and explicitly ordered where order
matters, simulation runs in FixedUpdate, state-specific entities are cleaned up on exit, and
logic is covered by headless App tests.
Version first
Bevy ships breaking releases every few months. Before writing anything:
- Read
AGENTS.mdandCargo.toml: notebevy = "0.x"and third-party Bevy crates (each supports specific Bevy versions; check their compatibility tables). - Grep the codebase for the idioms in use (
EventWritervsMessageWriter,TriggervsOn) and match them. - When upgrading, go one minor version at a time using the official migration guides on
bevy.org (
0.16 -> 0.17, then0.17 -> 0.18, …). Network is needed to fetch the guides and crates; ask if the sandbox blocks it.
| Topic | Older | Newer |
|---|---|---|
| Spawning sprites, cameras | Bundles (SpriteBundle, Camera2dBundle) | Required components: Sprite, Camera2d, Transform (0.15+) |
| Frame delta | time.delta_seconds() | time.delta_secs() (0.15+) |
| Single entity queries | query.single() panics | query.single() returns Result (0.16+); Single<&T> param skips the system if absent |
| System errors | Panics / unwrap | Systems may return Result and use ? (0.16+) |
| Buffered events | #[derive(Event)], EventWriter::send/write, EventReader | #[derive(Message)], MessageWriter::write, MessageReader::read, add_message (0.17+) |
| Observers | Trigger<OnAdd, T> | On<Add, T> (0.17+) |
| Despawn entities when leaving a state | StateScoped(state) | DespawnOnExit(state) component (0.17+) |
If the version and an API disagree with this table, trust the compiler and the version’s docs
(cargo doc --open -p bevy works offline once dependencies are fetched).
Project setup
[dependencies]
bevy = "0.20" # use the project's version
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3 # optimise dependencies; Bevy is unplayably slow at opt-level 0
- Faster iteration:
cargo run --features bevy/dynamic_linking(desktop dev only, never in release builds) and a fast linker (mold/lld on Linux; check the Bevy setup guide for the current recommendation per OS). - Linux builds need system packages (ALSA and udev development headers); missing ones fail at link time.
App structure
use bevy::prelude::*;
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
enum GameState { #[default] Menu, Playing, Paused }
#[derive(Component)] struct Player;
#[derive(Component)] struct LevelEntity; // marker for cleanup on exit
#[derive(Component, Default)] struct Velocity(Vec2);
#[derive(Message)] struct CoinCollected { value: u32 } // 0.17+ naming
#[derive(Resource, Default)] struct Score(u32);
pub struct GameplayPlugin;
impl Plugin for GameplayPlugin {
fn build(&self, app: &mut App) {
app.init_state::<GameState>()
.init_resource::<Score>()
.add_message::<CoinCollected>()
.add_systems(Startup, spawn_camera)
.add_systems(OnEnter(GameState::Playing), spawn_level)
.add_systems(OnExit(GameState::Playing), despawn_with::<LevelEntity>)
.add_systems(
FixedUpdate,
(player_input, apply_velocity, collect_coins)
.chain()
.run_if(in_state(GameState::Playing)),
)
.add_systems(Update, update_score.run_if(in_state(GameState::Playing)));
}
}
fn main() {
App::new().add_plugins((DefaultPlugins, GameplayPlugin)).run();
}
fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera2d);
}
fn spawn_level(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn((
Player,
LevelEntity,
Sprite::from_image(assets.load("sprites/hero.png")),
Transform::from_xyz(0.0, 0.0, 1.0),
Velocity::default(),
));
}
fn player_input(keys: Res<ButtonInput<KeyCode>>, mut player: Query<&mut Velocity, With<Player>>) -> Result {
let mut v = player.single_mut()?;
let mut dir = Vec2::ZERO;
if keys.pressed(KeyCode::KeyA) { dir.x -= 1.0; }
if keys.pressed(KeyCode::KeyD) { dir.x += 1.0; }
if keys.pressed(KeyCode::KeyW) { dir.y += 1.0; }
if keys.pressed(KeyCode::KeyS) { dir.y -= 1.0; }
v.0 = dir.normalize_or_zero() * 200.0;
Ok(())
}
fn apply_velocity(time: Res<Time>, mut q: Query<(&mut Transform, &Velocity)>) {
for (mut t, v) in &mut q {
t.translation += (v.0 * time.delta_secs()).extend(0.0); // Time is Time<Fixed> in FixedUpdate
}
}
fn collect_coins(/* ... */ mut collected: MessageWriter<CoinCollected>) {
// on overlap: collected.write(CoinCollected { value: 1 });
}
fn update_score(mut reader: MessageReader<CoinCollected>, mut score: ResMut<Score>) {
for msg in reader.read() { score.0 += msg.value; }
}
fn despawn_with<T: Component>(mut commands: Commands, q: Query<Entity, With<T>>) {
for e in &q { commands.entity(e).despawn(); }
}
Decisions
| Need | Use |
|---|---|
| Gameplay simulation, physics, anything needing stable steps | FixedUpdate (default 64 Hz; change with Time::<Fixed>::from_hz) |
| Rendering-side work, UI, camera smoothing | Update |
| Many-to-many decoupled notifications per frame | Messages (MessageWriter/MessageReader) |
| React immediately to something happening to an entity (spawned, clicked, damaged) | Observers (app.add_observer, entity.observe) |
| Data shared by many systems | Resource |
| Per-entity data | Component |
| Order between systems | .chain(), .before()/.after(), or SystemSets configured once |
| Skip systems by mode | States plus run_if(in_state(...)), or custom run conditions |
| Clean up a level when its state ends | DespawnOnExit(GameState::Playing) on spawned entities, or a marker plus an OnExit system as above |
| Physics | A Bevy physics crate (Avian, bevy_rapier) matching the Bevy version |
Query and borrow rules
- Two queries in one system that could access the same component mutably conflict and panic at
startup (“error[B0001]”). Fix with
Without<T>filters to make them disjoint, or aParamSet. - Prefer
Query<&T>(read) over&mut Twherever possible; the scheduler parallelises systems whose accesses do not overlap. Commandsare deferred: an entity spawned withcommands.spawnis not visible to queries until the next sync point. Systems later in a.chain()see it, because chains insert sync points.- Change detection (
Changed<T>,Added<T>,Ref<T>::is_changed) avoids per-frame work for rarely changing data. Mutable access through&mutmarks a component changed even if the value is the same; guard withset_if_neqwhen it matters.
Assets
AssetServer::loadreturns a handle immediately; the asset arrives later. Gate gameplay on a loading state that checksasset_server.is_loaded_with_dependencies(&handle)(or usebevy_asset_loaderif the project has it).- Paths are relative to the
assets/folder next toCargo.toml(when run withcargo run). - Hot reload during development with the
file_watchercargo feature.
Gotchas
just_pressedinput read inFixedUpdatecan be missed or seen twice, because fixed steps do not align with frames. Read edge-triggered input inUpdateinto a resource and consume it inFixedUpdate.- A system registered but not running: check its
run_if, the current state, and that its plugin was added.bevy::dev_toolsand logging (info!) help confirm. - Forgetting to clean up entities on state exit leaves menus or levels stacked.
- Third-party crates lag Bevy releases; upgrading Bevy may be blocked by one dependency. Check before promising an upgrade.
- Avoid
Query::iter().count()and similar per frame on large queries just for UI; keep a counter.
Tests
#[cfg(test)]
mod tests {
use super::*;
use bevy::ecs::system::RunSystemOnce;
#[test]
fn coin_messages_increase_score() {
let mut app = App::new();
app.init_resource::<Score>().add_message::<CoinCollected>().add_systems(Update, update_score);
app.world_mut().write_message(CoinCollected { value: 3 });
app.update();
assert_eq!(app.world().resource::<Score>().0, 3);
}
#[test]
fn velocity_moves_transform() {
let mut world = World::new();
world.insert_resource(Time::<()>::default());
let e = world.spawn((Transform::default(), Velocity(Vec2::X * 10.0))).id();
world.resource_mut::<Time>().advance_by(std::time::Duration::from_secs(1));
world.run_system_once(apply_velocity).unwrap();
assert_eq!(world.get::<Transform>(e).unwrap().translation.x, 10.0);
}
}
Use MinimalPlugins instead of DefaultPlugins in tests that need time or app plumbing; never
open a window in CI.
Verification
cargo check
cargo clippy --all-targets -- -D warnings
cargo test
cargo run --features bevy/dynamic_linking # manual play test
- Web builds: target
wasm32-unknown-unknownwith the project’s tool (Bevy CLI, trunk or wasm-bindgen); check that assets load over HTTP and audio starts after a user gesture. - Upgrades: after each minor bump,
cargo checkclean, tests pass, and play every state once.
Hand-off summary
**Bevy:** 0.20 (Cargo.toml), crates: avian2d (release that supports 0.20)
**Plugins:** GameplayPlugin (states Menu/Playing/Paused, FixedUpdate chain input -> velocity -> coins)
**Messages/observers:** CoinCollected message; observer on Add<Enemy> sets up health bar
**Tests:** 6 headless tests (World + run_system_once, App::update)
**Build:** dev profile opt-level 1 / deps 3; dynamic_linking for local runs only What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Bevy Engine"
short_description: "Rust ECS games that compile on your Bevy version"
default_prompt: "$bevy-engine Set up a Bevy project with menu, playing and paused states" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.