- Minimal App::new() starting point with DefaultPlugins/MinimalPlugins - Resources section (Res/ResMut, insert, init, Option for headless) - Resources-as-components 0.19 expansion (broad query traps, dual-derive, observers) - Startup/Update/FixedUpdate schedule overview - Asset loading (handles, scene labels, readiness gates, 0.19 renames) - Time and timers (delta_secs, Timer component pattern)
16 KiB
name, description
| name | description |
|---|---|
| bevy-0.19 | Write and navigate Bevy 0.19 (Rust game engine) code — ECS, plugins, systems, queries, events/messages, hierarchy, input, camera, rendering, assets. Use whenever editing Rust code that imports bevy, or the task involves Bevy components, systems, entity spawning, or game architecture. Pre-training knowledge of Bevy APIs is stale; consult this before writing Bevy code. For version migrations, defer to the bevy-upgrade skill. |
Bevy 0.19
Bevy's API changes fundamentally between minor versions. Code that looks correct from memory frequently targets 0.14–0.17 APIs and either fails to compile or — worse — compiles and silently misbehaves. This skill is ground truth for 0.19.
Step 0: Confirm the version
Check Cargo.toml for the bevy version before writing code.
- 0.19 — this skill applies directly.
- 0.18 — most of this skill applies; 0.19-only notes are marked. Do not use 0.19-only APIs.
- ≤0.17, or the task is a version bump/migration — use the bevy-upgrade skill. It owns rename tables and semantic shifts per version transition. Come back here for writing new idiomatic code once the target version is settled.
Minimal starting point
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, gameplay)
.run();
}
fn setup(mut commands: Commands) {
commands.spawn(Camera3d);
}
fn gameplay() {}
DefaultPlugins includes windowing, rendering, input, audio, and asset loading. For headless (tests, servers): MinimalPlugins.
Critical traps
The highest-frequency mistakes. When in doubt, trust this table over memory:
| Trap | Wrong (stale) | Correct (0.18/0.19) |
|---|---|---|
| Parent component | Parent |
ChildOf(entity) |
| Get parent | *child_of (deref) |
child_of.parent() |
| Child iteration | children.iter().copied() |
for child in &children |
| Recursive despawn | despawn_recursive() |
despawn() (recursive by default) |
| Buffered events | EventWriter/EventReader |
MessageWriter/MessageReader |
| Run condition | on_event::<T>() |
on_message::<T>() |
| Bundles | #[derive(Bundle)] |
spawn tuples + #[require(...)] |
| System errors | infallible fn(...) everywhere |
fn(...) -> Result where fallible |
| Forward direction | +Z | -Z (Transform::forward()) |
| GLTF model forward | -Z like Bevy | +Z — negate or use transform.back() |
GlobalTransform in Update |
assumed current | stale — propagation runs in PostUpdate |
| Material shader bind group | @group(2) |
@group(3) (group 2 is mesh bindings) |
Same-state set() |
no-op | fires OnExit+OnEnter; use set_if_neq() |
AmbientLight |
one global type | GlobalAmbientLight resource vs AmbientLight camera component |
ShaderRef import |
bevy::render::render_resource |
bevy::shader::ShaderRef |
WinitPlugin |
WinitPlugin::<WakeUp> |
WinitPlugin (not generic) |
0.19-specific:
| Trap | Detail |
|---|---|
| Resources are components | Resources live on entities; Query<Entity> matches them — filter broad queries with Without<IsResource>. Cannot derive both Component and Resource on one type. |
| Scene renamed | Scene/SceneRoot → WorldAsset/WorldAssetRoot |
Ref<T>.clone() |
Returns Ref<T>, not the inner value — use my_ref.deref().clone() |
Assets::get_mut |
Returns AssetMut<A> (mutation-tracked), not &mut A |
| GLTF materials | Return GltfMaterial; use #Material0/std label for StandardMaterial |
ECS core
Components
#[derive(Component)]
struct Health(f32);
#[derive(Component)]
#[require(Transform, Visibility)] // auto-inserted when Player is added
struct Player;
#[require(B, C)]replaces bundles: spawning the marker pulls in its dependencies. Constructors work:#[require(Speed(5.0))],#[require(Timer = default_timer())].#[component(immutable)](0.18+) forbids&mut Taccess.#[component(storage = "SparseSet")]for frequently added/removed components.- Lifecycle hooks:
#[component(on_add = f)], alsoon_insert,on_replace,on_remove,on_despawn— synchronous, inline with the ECS operation.
Systems
Plain functions of SystemParams. Make fallible systems return Result (0.18+) — errors are logged via BevyError and the system continues running on its normal schedule; ? works with anything convertible to BevyError:
fn apply_damage(mut q: Query<&mut Health>, mut reader: MessageReader<Damage>) -> Result {
for msg in reader.read() {
q.get_mut(msg.target)?.0 -= msg.amount;
}
Ok(())
}
Useful params beyond the obvious: Single<D, F> (exactly one match or panic), Populated<D, F> (skip system if empty), Option<Res<T>> (resource may not exist — required for render resources in headless contexts), Local<T> (per-system persistent state), RemovedComponents<T>.
Schedules: Startup runs once at launch (initialization, spawning the world). Update runs every frame. FixedUpdate runs at a fixed timestep (framerate-independent simulation). Full schedule list: references/plugins-and-app.md.
Queries
- Data:
&T,&mut T,Entity,Has<T>, tuples. - Filters:
With<T>,Without<T>,Added<T>,Changed<T>,Spawned,Or<(A, B)>. .single()panics unless exactly one match; preferSingleparam or handle theResult.- Derive
QueryData/QueryFilterfor reusable complex queries.
Commands
commands.spawn((CompA, CompB)) with tuples. commands.entity(e).insert(..) / .insert_if_new(..) / .remove::<T>() / .despawn(). Commands are deferred — they apply at the next sync point, not immediately.
Ordering
(a, b).chain(), .before(x) / .after(x), #[derive(SystemSet)] + configure_sets. Unordered systems with overlapping mutable access run in nondeterministic order — order anything where it matters.
Resources
Global singletons accessed by type:
#[derive(Resource)]
struct Score(u32);
// Insert in setup:
app.insert_resource(Score(0));
app.init_resource::<Score>(); // requires Default
// Access in systems:
fn show_score(score: Res<Score>) { /* read */ }
fn add_point(mut score: ResMut<Score>) { score.0 += 1; }
Option<Res<T>>/Option<ResMut<T>>for resources that may not exist — mandatory for render resources in headless-capable systems.Local<T>is per-system private state (default-initialized, persists across runs) — not a resource.
0.19: Resources live on dedicated entities internally. This means Query<Entity> (and other broad queries) match resource entities. Filter with Without<IsResource>. A type cannot derive both Component and Resource. See below.
Details: references/ecs.md.
Resources-as-components (0.19)
In 0.19, Resource is implemented via Component internally. Each resource lives on a hidden entity. This is mostly transparent, but creates real issues:
- Broad queries match resources.
Query<Entity>orQuery<&Name>will include resource entities. Always addWithout<IsResource>to gameplay queries that don't filter on a specific game component. - Cannot dual-derive.
#[derive(Component, Resource)]on one type is a compile error. Pick one role. If you need both a global config and per-entity overrides, use two types. - Observers see resources. Entity-lifecycle observers (
on_add,on_remove) fire for resource insertion/removal. This can surprise broad observers likeOn<Add<Name>>. Res<T>/ResMut<T>still work. The system param API is unchanged — you do not need to query for resources manually. The entity backing is an implementation detail unless you write broad queries or observers.
Hierarchy
ChildOf(parent) on the child is the source of truth; Children on the parent is auto-synced. Despawning a parent despawns descendants.
commands.spawn((Enemy, ChildOf(parent)));
commands.entity(parent).with_children(|s| { s.spawn(Turret); });
commands.entity(parent).add_child(existing);
Detaching without despawning: detach_child / detach_children / detach_all_children (0.18 names). Despawn only the children: despawn_related::<Children>().
Details, custom relationships, transform propagation: references/hierarchy.md.
Events vs Messages — pick correctly
0.18 split the old event system in two. Choosing wrong is the most common architectural error:
| Mechanism | Model | Use for |
|---|---|---|
#[derive(Message)] + MessageWriter/MessageReader |
Pull, double-buffered, batch | Decoupled many-to-many data flow; multiple systems consuming the same stream |
#[derive(Event)] + commands.trigger() + observers (On<E>) |
Push, immediate | React now: gameplay reactions, structural changes |
#[derive(EntityEvent)] + #[event_target] |
Push, per-entity, can propagate up ChildOf |
Entity-targeted reactions (hit, clicked) |
| Component hooks | Synchronous, inline | Response must be atomic with the ECS operation |
Changed<T>/Added<T> filters |
Per-tick polling | Cheap frame-by-frame reaction in a normal system |
Full API, lifecycle events (Add/Insert/Replace/Remove/Despawn), observer registration: references/events-observers.md.
Architecture
How to structure a Bevy project — and how to navigate an unfamiliar one:
- One plugin per feature/domain (
CombatPlugin,InventoryPlugin,UiPlugin), each owning its components, systems, messages, and state.main.rsshould be little more thanApp::new().add_plugins(...). - Navigate by the plugin tree: start at
App::new(), followadd_pluginsrecursively; grepadd_systemsand#[derive(SystemSet)]to map execution order; grep#[derive(Component)]/#[derive(Resource)]per module to map data ownership. - Cross-plugin communication goes through Messages or shared resources — not direct system coupling. A plugin's components are its internals; its messages are its API.
- States (
init_state,OnEnter/OnExit,in_staterun conditions) for game flow;DespawnOnExit(state)for scoped entities. Remember same-stateset()re-fires transitions — one-time setup inOnEnterneedsset_if_neqdiscipline from callers. - No formal plugin dependencies exist. Guard with
is_plugin_added::<T>(), or do cross-plugin setup inPlugin::finish()(runs after all plugins are ready). - Headless-safe systems: anything that may run in tests or dedicated servers must take render resources as
Option<Res<...>>. - Spawn from standard schedules. Entities spawned from exclusive/non-standard schedules (UI passes, custom schedules) can be invisible to plugins that hook the normal cycle — set a flag there, spawn in
Update.
Plugin trait lifecycle, plugin groups, states, run conditions: references/plugins-and-app.md.
Coordinates and transforms
- Right-handed. +Y up, -Z forward, +X right.
Transform::forward()is -Z. - GLTF models face +Z visually — opposite Bevy. Orient with
look_to(-dir, Vec3::Y)or treattransform.back()as the model's facing. - Transform propagation runs in
PostUpdate: readingGlobalTransforminUpdategives last frame's value. Move the reader toPostUpdateafterTransformSystem::TransformPropagate, or accept the one-frame lag deliberately.
Asset loading
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let texture: Handle<Image> = asset_server.load("textures/player.png");
let scene: Handle<WorldAsset> = asset_server.load("models/enemy.glb#Scene0");
commands.spawn((Mesh3d(asset_server.load("mesh.glb#Mesh0/Primitive0")),
MeshMaterial3d(asset_server.load("mesh.glb#Material0/std"))));
}
asset_server.load(path)returns aHandle<T>immediately; the asset loads asynchronously in the background.- Assets live in
Assets<T>collections:fn sys(images: Res<Assets<Image>>)to inspect loaded data. - Gate on readiness with
AssetEvent::LoadedWithDependenciesorasset_server.is_loaded_with_dependencies(handle). - GLTF scenes must use scene labels (
"model.glb#Scene0"), not the raw file handle. - 0.19:
Scene/SceneRootrenamed toWorldAsset/WorldAssetRoot.Assets::get_mutreturnsAssetMut<A>(mutation-tracked).
Caching pitfalls and more: references/camera-rendering.md.
Time and timers
fn move_player(time: Res<Time>, mut q: Query<&mut Transform, With<Player>>) -> Result {
let mut t = q.single_mut()?;
t.translation += Vec3::NEG_Z * 5.0 * time.delta_secs();
Ok(())
}
Res<Time>—time.delta_secs()(f32) ortime.delta()(Duration) for the frame delta.time.elapsed_secs()for total time since app start.
#[derive(Component)]
struct FireRate(Timer);
fn shoot(time: Res<Time>, mut q: Query<&mut FireRate>) {
for mut rate in &mut q {
rate.0.tick(time.delta());
if rate.0.just_finished() { /* fire */ }
}
}
Timer::from_seconds(1.0, TimerMode::Repeating) for recurring; TimerMode::Once for one-shot. Timers do not auto-tick — call .tick(delta) each frame.
Silent-failure checklist
Bevy often succeeds silently when misconfigured. If something "doesn't show up" with no errors:
- Mesh has no normals → invisible. Use
.with_computed_flat_normals()on programmatic meshes. - GLTF spawned by raw handle → nothing. Spawn scene labels:
"model.glb#Scene0". - Parent lacks
Visibility/InheritedVisibility→ children invisible. - Object beyond default far plane (1000 units) → culled.
- Asset cache poisoning:
load_with_settingswith stripped meshes caches per path; a later plainload()of the same path returns the stripped version. Never mix stripped and rendered loads of one asset in one process. - Assets aren't ready after
load()— gate onAssetEvent::LoadedWithDependenciesoris_loaded_with_dependencies(). - Raw
ButtonInput<MouseButton>fires through UI — world click handlers need UI-consumption checks.
More production gotchas (custom relationships, debugging heuristics): references/production-gotchas.md.
Code style
Write pragmatic Rust, not defensive Rust:
- Prefer
let Ok(x) = q.single() else { return; }/let Some(x) = ... else { return; }over nestedif letpyramids. - Prefer fallible systems with
?overunwrap()chains — a logged error beats a panic in a frame loop. - Small, single-purpose components; marker components (
struct Player;) are idiomatic and free. - Don't hand-roll what the ECS gives you: relationship sync, recursive despawn, required components, run conditions.
References
Load on demand — each is self-contained:
| File | Read when working on |
|---|---|
references/plugins-and-app.md |
Plugin trait/lifecycle, plugin groups, App builder, states, schedules, run conditions |
references/ecs.md |
Component/resource/system/query/command details beyond the summary above |
references/hierarchy.md |
ChildOf, Children, spawning/detaching, relationships, transform propagation |
references/events-observers.md |
Message vs Event APIs, observers, lifecycle events, hooks |
references/input.md |
Keyboard, mouse, gamepad, touch |
references/camera-rendering.md |
Cameras, coordinates, meshes, materials, shaders, assets, lighting |
references/production-gotchas.md |
Silent failures, headless contexts, debugging heuristics |
Related skill
bevy-upgrade — version migration (0.15→…→0.19): rename tables, semantic shifts, step-by-step upgrade workflow. Use it whenever the task is moving code between versions rather than writing code for one. It may in turn send you back here for post-migration idiomatic cleanup.