Files
g4borg 51e7a2579b 🤖 Add missing fundamentals to bevy-0.19 skill
- 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)
2026-07-07 17:52:30 +02:00

16 KiB
Raw Permalink Blame History

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.140.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/SceneRootWorldAsset/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 T access.
  • #[component(storage = "SparseSet")] for frequently added/removed components.
  • Lifecycle hooks: #[component(on_add = f)], also on_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; prefer Single param or handle the Result.
  • Derive QueryData/QueryFilter for 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:

  1. Broad queries match resources. Query<Entity> or Query<&Name> will include resource entities. Always add Without<IsResource> to gameplay queries that don't filter on a specific game component.
  2. 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.
  3. Observers see resources. Entity-lifecycle observers (on_add, on_remove) fire for resource insertion/removal. This can surprise broad observers like On<Add<Name>>.
  4. 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.rs should be little more than App::new().add_plugins(...).
  • Navigate by the plugin tree: start at App::new(), follow add_plugins recursively; grep add_systems and #[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_state run conditions) for game flow; DespawnOnExit(state) for scoped entities. Remember same-state set() re-fires transitions — one-time setup in OnEnter needs set_if_neq discipline from callers.
  • No formal plugin dependencies exist. Guard with is_plugin_added::<T>(), or do cross-plugin setup in Plugin::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 treat transform.back() as the model's facing.
  • Transform propagation runs in PostUpdate: reading GlobalTransform in Update gives last frame's value. Move the reader to PostUpdate after TransformSystem::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 a Handle<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::LoadedWithDependencies or asset_server.is_loaded_with_dependencies(handle).
  • GLTF scenes must use scene labels ("model.glb#Scene0"), not the raw file handle.
  • 0.19: Scene/SceneRoot renamed to WorldAsset/WorldAssetRoot. Assets::get_mut returns AssetMut<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) or time.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:

  1. Mesh has no normals → invisible. Use .with_computed_flat_normals() on programmatic meshes.
  2. GLTF spawned by raw handle → nothing. Spawn scene labels: "model.glb#Scene0".
  3. Parent lacks Visibility/InheritedVisibility → children invisible.
  4. Object beyond default far plane (1000 units) → culled.
  5. Asset cache poisoning: load_with_settings with stripped meshes caches per path; a later plain load() of the same path returns the stripped version. Never mix stripped and rendered loads of one asset in one process.
  6. Assets aren't ready after load() — gate on AssetEvent::LoadedWithDependencies or is_loaded_with_dependencies().
  7. 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 nested if let pyramids.
  • Prefer fallible systems with ? over unwrap() 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

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.