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

290 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: bevy-0.19
description: 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
```rust
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
```rust
#[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 `SystemParam`s. 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`:
```rust
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:
```rust
#[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.
```rust
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
```rust
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
```rust
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.
```rust
#[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 |
## 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.