🤖 Flesh out bevy-0.19 and bevy-upgrade skills from research

Replace scaffold placeholders with full skill content:
- bevy-0.19: ECS cheat sheet, critical traps table, architecture guide, and 7 reference files
- bevy-upgrade: version migration workflow, transition references for 0.15→0.19, and 3 per-bump reference files
This commit is contained in:
2026-07-07 17:52:10 +02:00
parent 325e278af1
commit c661027d19
14 changed files with 927 additions and 53 deletions
+176 -28
View File
@@ -1,38 +1,186 @@
---
name: bevy-0.19
description: "TODO: Use the skill-creator skill to write this. See scaffolding goals below."
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 — Implementation Helper
# Bevy 0.19
> **This skill is a scaffold.** Use `/skill-creator` to create the full skill content from the research material.
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.
## Scaffolding Goals
## Step 0: Confirm the version
1. **Use the `skill-creator:skill-creator` skill** to create the full SKILL.md content
2. Feed it the research reports from [`docs/workpad/research/bevy-skill-research/`](../../../../docs/workpad/research/bevy-skill-research/00-index.md)
3. Follow **Approach A** from the [design sketch](../../../../docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md): compact always-loaded cheat sheet (~500 lines) plus `references/` folder for deep dives
4. The skill should trigger on Bevy 0.19 Rust code (`bevy = "0.19"` in Cargo.toml, `use bevy::prelude::*`, `App::new()`, `#[derive(Component)]`, etc.)
5. Core content priorities (ranked by LLM error frequency):
- Event/Message rename and split (0.18)
- ChildOf replacing Parent (0.16+)
- Fallible systems with `-> Result` (0.18)
- Coordinate system gotchas
- Transform propagation timing
- Resources-as-Components (0.19)
6. Populate `references/` with per-topic deep-dive files derived from research reports 01-07
7. The skill should know about `bevy-upgrade` and invoke it when upgrade/migration work is detected
Check `Cargo.toml` for the `bevy` version before writing code.
## Reference Material
- **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.
The research that feeds this skill lives in the workpad:
## Critical traps
- [00-index.md](../../../../docs/workpad/research/bevy-skill-research/00-index.md) — Research overview and design rationale
- [01-plugins.md](../../../../docs/workpad/research/bevy-skill-research/01-plugins.md) — Plugin lifecycle
- [02-ecs-core.md](../../../../docs/workpad/research/bevy-skill-research/02-ecs-core.md) — ECS fundamentals
- [03-hierarchy.md](../../../../docs/workpad/research/bevy-skill-research/03-hierarchy.md) — Hierarchy and ChildOf
- [04-events-observers.md](../../../../docs/workpad/research/bevy-skill-research/04-events-observers.md) — Events, messages, observers
- [05-input.md](../../../../docs/workpad/research/bevy-skill-research/05-input.md) — Input handling
- [06-camera-gotchas.md](../../../../docs/workpad/research/bevy-skill-research/06-camera-gotchas.md) — Camera and rendering
- [07-lessons-from-production.md](../../../../docs/workpad/research/bevy-skill-research/07-lessons-from-production.md) — Production gotchas
- [08-landscape-and-design-sketch.md](../../../../docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md) — Architecture options
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 `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 log and the system retries next tick; `?` 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>`.
### 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.
## 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.
## 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 (physics, multiplayer/shared-world, custom relationships): `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, physics-plugin traps, headless/server architecture |
## 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.
@@ -0,0 +1,71 @@
# Camera, Rendering, Assets
## Camera setup
`Camera3d` is a marker with `#[require(...)]` pulling in `Camera`, `DebandDither`, `CameraRenderGraph`, `Projection`, `Tonemapping`, `ColorGrading`, `Exposure`. Spawning bare `Camera3d` yields a working 3D camera.
- **0.18**: `RenderTarget` is a separate required component, no longer a `Camera` field.
- **0.19**: `Hdr` moved from `bevy_render` to `bevy_camera`; screen-space specular transmission fields moved into a `ScreenSpaceTransmission` component.
### Multiple cameras / ordering
- `Camera.order: isize` — higher renders later (on top).
- Every camera with `is_active: true` renders independently.
- Overlay cameras need `ClearColorConfig::None` or they wipe the layer below.
### Camera gotchas
- Default far plane is **1000 units** — anything beyond is silently culled.
- Forward is **-Z**.
- Viewport coordinates are **physical** pixels; cursor position is **logical**.
## Coordinate system
Right-handed:
| Axis | Bevy | GLTF models |
|------|------|-------------|
| Forward | -Z (`Transform::forward()`) | +Z (`Transform::back()`) |
| Up | +Y | +Y |
| Right | +X | -X from camera perspective |
GLTF fix: orient with `look_to(-direction, Vec3::Y)` or treat `transform.back()` as the model's visual forward.
**0.18**: `GltfPlugin`'s `use_model_forward_direction` was replaced by `convert_coordinates` with `rotate_scene_entity` and `rotate_meshes` flags.
## Meshes & materials
- `StandardMaterial` needs **normals**; a mesh without them spawns fine and renders invisible. Programmatic meshes: `.with_computed_flat_normals()` (or insert `ATTRIBUTE_NORMAL`).
- Back-face culling is on by default — wrong winding = invisible faces.
- GLTF scenes spawn via **scene labels** (`"model.glb#Scene0"`), not the raw asset handle.
- Visibility inherits: parents without `Visibility`/`InheritedVisibility` make children invisible — common with hand-built hierarchies around loaded scenes.
- **0.18**: mesh accessors became `try_*` returning `Result<_, MeshAccessError>`; `MaterialPlugin` fields (`prepass_enabled`, `shadows_enabled`) became `Material` trait methods.
- **0.19**: GLTF material loading returns `GltfMaterial` — use the `#Material0/std` label for a `StandardMaterial`. `Assets::get_mut` returns mutation-tracked `AssetMut<A>`. `Scene`/`SceneRoot` renamed to `WorldAsset`/`WorldAssetRoot`.
## Custom material shaders — bind groups
Material bindings are `@group(3)` (0.18+), not `@group(2)`:
| Group | Contents |
|-------|----------|
| 0 | view |
| 1 | globals/lights |
| 2 | mesh (storage buffer) |
| 3 | material (`AsBindGroup`) |
`@group(2)` for material uniforms produces the confusing `Storage class Storage doesn't match the shader Uniform` error — group 2 is the mesh storage buffer.
Import: `use bevy::shader::ShaderRef;` (moved out of `bevy::render::render_resource` in 0.18).
## Lighting
`AmbientLight` split (0.18):
- `GlobalAmbientLight`**resource**, all cameras.
- `AmbientLight`**component** on a camera entity, per-camera.
## Assets
- Assets are not ready right after `asset_server.load()`. Gate on `AssetEvent::LoadedWithDependencies` or `AssetServer::is_loaded_with_dependencies()`.
- **Cache poisoning**: the server dedups by path. A `load_with_settings` that strips data (e.g. `load_meshes = RenderAssetUsages::empty()`) poisons the cache; a later `load()` of the same path returns the stripped asset — invisible models, zero errors. Never mix stripped and rendered loads of the same path in one process; stripped loads belong to headless tools only.
- **0.18**: `LoadContext::path()` returns `AssetPath`, not `Path`; image `reinterpret_*` return `Result`.
- **0.19**: `AssetPath::resolve()`/`resolve_embed()` take `&AssetPath`; the string-taking variants are `resolve_str()`/`resolve_embed_str()`.
@@ -0,0 +1,87 @@
# ECS Core Reference
## Components
`#[derive(Component)]` on structs/enums; requires `Send + Sync + 'static`.
Storage:
- Table (default) — fast iteration.
- `#[component(storage = "SparseSet")]` — fast insert/remove; use for tag-like components that churn.
Attributes:
- `#[component(immutable)]` (0.18+) — only `&T` access, never `&mut T`. Good for identity-like data.
- `#[require(B, C)]` — auto-insert `B` and `C` when this component is added. Constructor forms: `B(value)`, `C { field: val }`, function calls, `= expr`. This replaced `#[derive(Bundle)]` (removed 0.16+); spawn with tuples instead.
- Hooks: `#[component(on_add = f)]`, `on_insert`, `on_replace`, `on_remove`, `on_despawn`. Hook signature: `fn(DeferredWorld, HookContext)`. Hooks run synchronously inside the ECS operation.
- `#[component(clone_behavior = Ignore)]` — skip during entity clone.
## Resources
| Param | Access |
|-------|--------|
| `Res<T>` | shared read |
| `ResMut<T>` | exclusive write |
| `Option<Res<T>>` / `Option<ResMut<T>>` | may not exist (mandatory for render resources in headless-capable systems) |
| `Local<T>` | per-system private state, default-initialized, persists across runs |
| `NonSend<T>` / `NonSendMut<T>` | `!Send` resources (main-thread only) |
World/App: `init_resource::<R>()`, `insert_resource(value)`, `remove_resource::<R>()`.
**0.19**: Resources implement `Component` and live on dedicated entities. Consequences:
- `Query<Entity>` (and other very broad queries) match resource entities — filter with `Without<IsResource>`.
- A type cannot derive both `Component` and `Resource`.
## Systems
Any `fn(params...)` where every param is a `SystemParam`.
- **Fallible** (0.18+): `fn system(...) -> Result` (alias for `Result<(), BevyError>`). Errors are logged; the system runs again next tick. `?` converts anything convertible to `BevyError`. Prefer this over `unwrap()`.
- **Exclusive**: `fn system(world: &mut World)` — full access, blocks parallelism; use sparingly.
Valid `SystemParam` types include: `Query`, `Res`, `ResMut`, `Commands`, `Local`, `MessageReader`, `MessageWriter`, `ParamSet`, `Single`, `Populated`, `SystemName`, `SystemChangeTick`, `RemovedComponents<T>`, `DeferredWorld`, `&World`, `Option<P>`, `Result<P, SystemParamValidationError>`.
- `Single<D, F>` — resolves to exactly one entity or the system doesn't run correctly (panics); the param-level version of `query.single()`.
- `Populated<D, F>` — the system is skipped when the query is empty. Cheap way to avoid per-frame no-op runs.
## Queries
`Query<D, F>``D: QueryData`, `F: QueryFilter` (default `()`).
Data: `&T`, `&mut T`, `Entity`, `Has<T>`, tuples.
Filters:
| Filter | Matches |
|--------|---------|
| `With<T>` / `Without<T>` | presence / absence |
| `Added<T>` | first tick after the component was added |
| `Changed<T>` | first tick after add or mutable deref |
| `Spawned` | first tick after entity spawn |
| `Or<(A, B)>` | either |
Note `Changed<T>` triggers on `&mut` deref even without an actual value change — deref only when writing.
Methods: `.iter()` / `.iter_mut()`, `.get(entity)` / `.get_mut(entity)`, `.single()` (Result-returning; panicking use is a bug magnet), `.par_iter()` / `.par_iter_mut()`.
Reusable shapes: `#[derive(QueryData)]`, `#[derive(QueryFilter)]`.
## Commands
| Call | Effect |
|------|--------|
| `commands.spawn((A, B))` | spawn with component tuple, returns `EntityCommands` |
| `commands.spawn_empty()` | empty entity, chain `.insert(...)` |
| `commands.spawn_batch(iter)` | batch spawn |
| `commands.entity(e).insert(c)` | add/replace component |
| `commands.entity(e).insert_if_new(c)` | add only if absent |
| `commands.entity(e).remove::<T>()` | remove component |
| `commands.entity(e).despawn()` | despawn entity **and descendants** |
| `commands.queue(cmd)` | custom `Command` impl |
Commands are deferred to the next sync point — a spawn is not queryable in the same system.
## Ordering
- `(a, b).chain()` — sequential.
- `.before(x)` / `.after(x)` — explicit constraints against systems or sets.
- `#[derive(SystemSet)]` + `app.configure_sets(Update, SetA.before(SetB))` — named groups; the scalable way to order across plugins.
- Systems with overlapping mutable access and no ordering run nondeterministically; Bevy warns about ambiguities. Order anything where results depend on it.
@@ -0,0 +1,105 @@
# Events, Messages, Observers & Hooks
0.18 split the old single event system into **Messages** (pull, buffered) and **Events** (push, observed). They are different traits with different derives — using the wrong one won't compile, and porting old code mechanically to the wrong side is an architecture bug.
## Messages — pull-based, double-buffered
The renamed successor of `EventWriter`/`EventReader`. Use for decoupled many-to-many data flow where several systems batch-process the same stream.
```rust
#[derive(Message)]
struct Collision { a: Entity, b: Entity }
fn produce(mut writer: MessageWriter<Collision>) {
writer.write(Collision { a, b }); // also: write_batch(iter), write_default()
}
fn consume(mut reader: MessageReader<Collision>) {
for c in reader.read() { /* each reader has its own cursor */ }
}
```
- Double-buffered: a message survives one `update()` cycle and is dropped on the next. A reader that runs before the writer in the same frame sees it next frame.
- Multiple readers run concurrently; multiple writers of the same type cannot.
- Reader extras: `len()`, `is_empty()`, `clear()`.
- Run condition: `on_message::<M>()`.
## Events — push-based, immediate, observed
```rust
#[derive(Event)]
struct Speak { text: String }
// Global observer
app.add_observer(|on: On<Speak>| println!("{}", on.text));
// Entity-scoped observer — fires only for this entity
commands.entity(id).observe(|on: On<Hit>| { /* ... */ });
// Trigger
commands.trigger(Speak { text: "hi".into() });
world.trigger_ref(&mut event); // inspect the event after observers mutate it
```
Observer param `On<'w, 't, E, B>` (`B: Bundle` filters by component):
- `event()` / `event_mut()` — event data (also `Deref<Target = E>` for direct field access)
- `trigger()` / `trigger_mut()` — trigger context
- `observer()` — the observer entity
Observers run immediately during the trigger, not at a schedule point.
## EntityEvent — entity-targeted
```rust
#[derive(EntityEvent)]
struct Hit {
#[event_target]
entity: Entity,
damage: f32,
}
```
`#[entity_event(propagate)]` makes the event bubble up the `ChildOf` chain — useful for UI-like hit forwarding.
**0.19**: `EntityComponentsTrigger` gained archetype fields — destructure with `..`:
`let EntityComponentsTrigger { components, .. } = e.trigger();`
## Lifecycle events
Built-in events observers can watch per component:
| Event | Fires | Order |
|-------|-------|-------|
| `Add` | component added where absent | first |
| `Insert` | any insert (add or replace) | after `Add` |
| `Replace` | component removed, regardless of replacement | before `Remove` |
| `Remove` | component removed | after `Replace` |
| `Despawn` | entity despawned (per component) | last |
```rust
world.add_observer(|on: On<Add, Health>| { /* entity just gained Health */ });
```
## Component hooks — synchronous, inline
For callbacks that must be atomic with the ECS operation itself (not deferred, not scheduled):
```rust
#[derive(Component)]
#[component(on_add = init_health_bar)]
struct Health(f32);
fn init_health_bar(world: DeferredWorld, ctx: HookContext) { /* ... */ }
```
Available: `on_add`, `on_insert`, `on_replace`, `on_remove`, `on_despawn`.
## Choosing the mechanism
| Mechanism | Model | Reach for it when |
|-----------|-------|-------------------|
| Messages | pull, buffered, scheduled | several systems independently process a stream; frame-batched work |
| Event + observer | push, immediate | something must react *now* (gameplay reactions, structural responses) |
| EntityEvent | push, targeted, can propagate | the reaction belongs to a specific entity |
| Component hook | synchronous, inline | the response must not be separable from the ECS operation |
| `Changed<T>` / `Added<T>` | per-tick polling | cheap frame-wise checks inside a normal system |
@@ -0,0 +1,65 @@
# Hierarchy & Relationships
## ChildOf / Children (0.16+ model)
- `ChildOf(pub Entity)` lives on the **child** and points at the parent. (`Parent` no longer exists.)
- Get the parent via `child_of.parent()` — the old `Deref` was removed.
- `ChildOf` implements `Relationship`; `Children` implements `RelationshipTarget`. They auto-sync via hooks: adding `ChildOf(p)` to a child inserts/updates `Children` on `p`; removing it updates the parent. Never maintain `Children` manually.
- Despawning a parent despawns all descendants — `despawn()` is recursive by default (`despawn_recursive` is gone).
## Spawning children
```rust
// Direct: ChildOf in the spawn tuple
commands.spawn((Turret, ChildOf(ship)));
// Closure for several children
commands.entity(ship).with_children(|spawner| {
spawner.spawn(Turret);
let ship = spawner.target_entity(); // was parent_entity()
});
// Attach existing entities
commands.entity(ship).add_child(turret);
commands.entity(ship).add_children(&[a, b]);
commands.entity(ship).insert_children(index, &[a, b]);
```
## Querying
- `Children` derefs to `[Entity]` and implements `IntoIterator` yielding `Entity`.
- Iterate: `for child in &children` — not `children.iter().copied()`.
- Methods: `len()`, `is_empty()`, `contains()`, `first()`, `last()`, `sort_by()`.
- Relationship-generic query extensions: `query.related::<ChildOf>(entity)`, `query.relationship_sources::<Children>(entity)`.
## Detach vs despawn (0.18 names)
| Method | Effect |
|--------|--------|
| `detach_child(e)` | unparent one child, keep it alive |
| `detach_children(&[..])` | unparent several |
| `detach_all_children()` | unparent all |
| `despawn_related::<Children>()` | despawn the children themselves |
The pre-0.18 names (`remove_child`, `remove_children`, `clear_children`) are gone.
## Custom relationships
`#[relationship]` / `#[relationship_target]` derive pairs get the same auto-sync as `ChildOf`/`Children`. Do **not** write observers that manually push/remove entities in the target collection — the hooks already do it, and manual sync causes duplicates or desync.
## Transform propagation
- Child `Transform` is parent-relative; `GlobalTransform` is computed world-space through the `ChildOf` chain.
- Propagation runs in **`PostUpdate`** (`TransformSystem::TransformPropagate`). A system in `Update` reading `GlobalTransform` sees last frame's value.
- Options: schedule the reader in `PostUpdate` after propagation; accept the one-frame lag (often fine); or (rarely) walk the `Transform` chain manually.
## Quick corrections
| Stale | Correct |
|-------|---------|
| `Parent` | `ChildOf(entity)` |
| `*child_of` | `child_of.parent()` |
| `children.iter().copied()` | `for child in &children` |
| `despawn_recursive()` | `despawn()` |
| `parent_entity()` (in spawner) | `target_entity()` |
| `remove_children` / `clear_children` | `detach_children` / `detach_all_children` |
@@ -0,0 +1,71 @@
# Input
## Keyboard — `Res<ButtonInput<KeyCode>>`
| Method | Purpose |
|--------|---------|
| `pressed(k)` | held this frame |
| `just_pressed(k)` / `just_released(k)` | edge detection |
| `any_pressed(iter)` / `any_just_pressed(iter)` / `all_pressed(iter)` | multi-key checks |
| `get_pressed()` / `get_just_pressed()` | iterate held / newly pressed |
| `clear_just_pressed(k)` | consume the press |
`ButtonInput<Key>` exists for logical/locale-aware keys (text-ish input); `KeyCode` is physical layout.
## Mouse
Buttons: `Res<ButtonInput<MouseButton>>` — same API. Variants: `Left`, `Right`, `Middle`, `Back`, `Forward`, `Other(u16)`.
Motion and scroll:
| Type | Kind | Purpose |
|------|------|---------|
| `MouseMotion` | Message | raw per-event `delta: Vec2` — read with `MessageReader<MouseMotion>` |
| `AccumulatedMouseMotion` | Resource | frame total, reset each frame — usually what you want |
| `MouseWheel` | Message | scroll with `MouseScrollUnit` (Line/Pixel) |
| `AccumulatedMouseScroll` | Resource | frame total scroll |
Cursor position — from the `Window` component, logical pixels, origin top-left:
```rust
fn cursor(windows: Query<&Window>) -> Result {
if let Some(pos) = windows.single()?.cursor_position() { /* ... */ }
Ok(())
}
```
**UI click-through trap**: `ButtonInput<MouseButton>` is raw winit input — it fires even when the click landed on a UI panel. World-click handlers must verify the click wasn't consumed by UI (e.g. a full-screen background `Interaction` node that only receives clicks passing through all UI).
## Gamepad
- Buttons: `Res<ButtonInput<GamepadButton>>` — same pressed/just_pressed API; analog buttons also map to `[0.0, 1.0]`.
- Axes: `Res<Axis<GamepadAxis>>``get(axis) -> Option<f32>` in `[-1.0, 1.0]`.
- Messages: `GamepadButtonStateChangedEvent`, `GamepadButtonChangedEvent`, `GamepadAxisChangedEvent`, `GamepadConnectionEvent`.
- Deadzones/thresholds: `GamepadSettings` component.
## Touch
- `TouchInput` message with `TouchPhase` (Began/Moved/Ended).
- `Touches` resource — active-touch queries, analogous to `ButtonInput`.
- `ForceTouch` for pressure-capable devices.
- Requires the `touch` feature flag.
## Feature gating (0.18+)
Input sources (`mouse`, `keyboard`, `gamepad`, `touch`, `gestures`) are feature-gated. They're on by default via `bevy_window`/`bevy_gilrs`, but with `default-features = false` they must be listed explicitly — missing input with no errors usually means a missing feature.
## Action mapping
Bevy has no built-in action-mapping layer. The community standard is `leafwing-input-manager`; check the project's `Cargo.toml` before hand-rolling bindings.
## Pattern
```rust
fn movement(keys: Res<ButtonInput<KeyCode>>) {
let mut dir = Vec3::ZERO;
if keys.pressed(KeyCode::KeyW) { dir.z -= 1.0; } // forward is -Z
if keys.pressed(KeyCode::KeyS) { dir.z += 1.0; }
if keys.pressed(KeyCode::KeyA) { dir.x -= 1.0; }
if keys.pressed(KeyCode::KeyD) { dir.x += 1.0; }
}
```
@@ -0,0 +1,86 @@
# Plugins, App, States, Schedules
## Plugin trait (`bevy::app::Plugin`)
Required:
- `fn build(&self, app: &mut App)` — runs immediately on `add_plugins`.
Optional (defaults provided):
- `fn ready(&self, _: &App) -> bool` — default `true`; override for async init.
- `fn finish(&self, _: &mut App)` — runs after all plugins report `ready()`, before the first schedule tick. The idiomatic place for cross-plugin setup that needs another plugin's resources to exist.
- `fn cleanup(&self, _: &mut App)` — runs after `finish()`; remove temp resources.
- `fn name(&self) -> &str` — defaults to type name; used for the uniqueness check.
- `fn is_unique(&self) -> bool` — default `true`; return `false` for multi-instance plugins.
Any `fn(&mut App)` auto-implements `Plugin`.
## Plugin dependencies
There is no formal depends-on declaration. Patterns:
- `app.is_plugin_added::<T>()` to guard or panic early with a clear message.
- Sub-plugins: call `app.add_plugins(ChildPlugin)` inside `build()`. Bevy deduplicates by name; unique plugins panic on double-add.
- Cross-plugin wiring that needs the other plugin's resources → `finish()`.
## Plugin groups
`DefaultPlugins` / `MinimalPlugins` implement `PluginGroup`. Customize via the builder:
```rust
DefaultPlugins
.disable::<AudioPlugin>()
.set(WindowPlugin { /* config */ })
.add_before::<RenderPlugin>(MyPlugin)
```
`.disable::<T>()`, `.enable::<T>()`, `.set(plugin)`, `.add_before::<Target>(p)`, `.add_after::<Target>(p)`.
## App builder key methods
| Method | Purpose |
|--------|---------|
| `add_plugins(P)` | Plugin, plugin group, or tuple of either |
| `insert_resource(R)` | Insert, overwriting existing |
| `init_resource::<R>()` | Insert via `Default`/`FromWorld` if absent |
| `add_systems(schedule, systems)` | Add to a schedule |
| `configure_sets(schedule, sets)` | Order system sets |
| `register_type::<T>()` | Reflection registry |
| `is_plugin_added::<T>()` | Plugin presence check |
| `register_required_components::<T, R>()` | Runtime component requirements |
## States
Three flavors:
- **FreelyMutableState** — changed via the `NextState<S>` resource.
- **SubStates** — exist only while the parent state is in a required variant.
- **ComputedStates** — derived: `fn compute(sources) -> Option<Self>`.
Registration: `app.init_state::<S>()` (default initial) or `app.insert_state(value)`.
Transition schedules: `OnEnter(S)`, `OnExit(S)`, `OnTransition(S)`.
Entity scoping: `DespawnOnEnter(S)`, `DespawnOnExit(S)` (replaced `StateScoped`, 0.16+).
Run condition: `in_state(S)`.
**0.18 behavior change**: `NextState::set()` fires `OnExit`+`OnEnter` even when setting the current state. Use `set_if_neq()` for no-op-on-same. One-time setup in `OnEnter` systems will re-run if any caller `set()`s the active state.
## Schedules
| Phase | Schedules |
|-------|-----------|
| Startup (once) | `PreStartup`, `Startup`, `PostStartup` |
| Per-frame | `First`, `PreUpdate`, `Update`, `PostUpdate`, `Last` |
| Fixed timestep | `FixedFirst`, `FixedPreUpdate`, `FixedUpdate`, `FixedPostUpdate`, `FixedLast` (inside `FixedMain`, driven by `RunFixedMainLoop`) |
| Other | `Main`, `SpawnScene` |
Gameplay simulation that must be framerate-independent goes in `FixedUpdate`. Transform propagation and most engine bookkeeping run in `PostUpdate`.
## Run conditions (`common_conditions`)
- `in_state(s)`
- `resource_exists::<R>()`, `resource_added::<R>()`, `resource_changed::<R>()`, `resource_equals(val)`
- `any_with_component::<T>()`, `any_component_removed::<T>()`
- `on_message::<M>()` (renamed from `on_event`, 0.18)
- `run_once()`, `not(cond)`, `condition_changed(cond)`, `condition_changed_to(cond)`
Combine with `.and(cond)` / `.or(cond)`.
@@ -0,0 +1,93 @@
# Production Gotchas
Battle-tested traps from shipped Bevy code. Most are silent — no error, no warning, wrong behavior. Grouped by category; scan the headers when debugging "it just doesn't work".
## Silent failures
### Asset cache poisoning via `load_with_settings`
`AssetServer` dedups by path. Loading a GLTF with stripped settings (e.g. `load_meshes = RenderAssetUsages::empty()` for hierarchy extraction) caches the stripped version; a later plain `load("model.glb#Scene0")` returns it — invisible models, zero errors, order-dependent.
Rule: a process that renders an asset must never also `load_with_settings`-strip that asset. Stripped loads are for headless servers/tools only.
### Mesh without normals
Spawns successfully, renders invisible. Use `.with_computed_flat_normals()` or insert `ATTRIBUTE_NORMAL` on programmatic meshes.
### Visibility hierarchy
Parents lacking `Visibility`/`InheritedVisibility` make all children invisible regardless of the children's settings. Sneaky in hand-assembled hierarchies around loaded scenes.
### Spawning from non-standard schedules
Plugins (particles, audio, …) often hook the standard `Update`/`Startup` cycle. Entities spawned from exclusive or custom schedules (e.g. a UI pass) can exist in queries yet be ignored by the plugin — all components present, no errors, nothing happens.
Fix: set a flag in the odd schedule, do the actual spawn in `Update`:
```rust
fn ui_system(mut state: ResMut<MyState>) {
if clicked { state.spawn_requested = true; }
}
fn handle_spawns(mut state: ResMut<MyState>, mut commands: Commands) {
if std::mem::take(&mut state.spawn_requested) {
commands.spawn(/* ... */);
}
}
```
## Scheduling & state
### `GlobalTransform` stale in `Update`
Propagation runs in `PostUpdate`; `Update` readers see last frame. Move the reader to `PostUpdate` after `TransformSystem::TransformPropagate`, or accept the lag deliberately.
### `State::set()` re-fires same-state transitions (0.18)
`next_state.set(Playing)` while already `Playing` fires `OnExit(Playing)` + `OnEnter(Playing)`. One-time setup in `OnEnter` (cameras, level loads) re-runs. Use `set_if_neq()` when re-firing isn't wanted.
## Headless / server contexts
### Render resources must be `Option`
`ResMut<Assets<MyMaterial>>` panics in headless runs (tests, dedicated servers) — the resource doesn't exist without `RenderPlugin`:
```rust
fn my_system(materials: Option<ResMut<Assets<MyMaterial>>>) {
let Some(mut materials) = materials else { return; };
// ...
}
```
Any system that can run both rendered and headless needs this.
## UI interaction
### Raw mouse input fires through UI
`Res<ButtonInput<MouseButton>>` is winit-level; it fires for clicks consumed by UI panels. World-click handlers need a UI-consumption check (e.g. a full-screen low-z `Interaction` node that receives only clicks passing through all UI).
### `RelativeCursorPosition.normalized` is center-origin
`(0, 0)` is the node's **center**; range is `(-0.5, -0.5)``(0.5, 0.5)`. For `[0, 1]` UV: `normalized + Vec2::splat(0.5)`.
## Relationships
Custom `#[relationship]` / `#[relationship_target]` pairs auto-sync via hooks. Do not write observers that manually push/remove entities in the target `Vec<Entity>` — that duplicates what the macro does and desyncs.
## Physics plugins (Avian / Rapier pattern)
- **`CollidingEntities` is not auto-inserted** with `Collider` (Avian3D 0.5). Without adding `CollidingEntities::default()`, collision queries return nothing while the simulation runs fine internally.
- **Removing `RigidBody` from a parent with `Collider` children** can panic with stale internal-tree indices. Despawn collider children first.
## Shared-world multiplayer (server + client in one process)
- **Server entities carry no visual components.** `SceneRoot`/`Mesh3d`/`Sprite` belong on client-spawned entities only; mixing causes host-vs-remote visual drift and double maintenance.
- **`run_if(is_server)` gates the system, not the query.** In a shared world, server systems still match client replica entities — queries must filter client markers explicitly.
- **Shared components race.** Server and client systems in one World share component instances; a client system resetting a timer changes what the server system reads the same frame. Use `Local<T>` for server-side state that must stay independent.
## Debugging heuristics
1. Invisible things: check normals, visibility hierarchy, far plane (1000), asset cache poisoning, scene-label spawning.
2. Empty query results: check the component is actually inserted (physics markers), spawn schedule, `Without<IsResource>` on broad queries (0.19).
3. Behavior fires unexpectedly / twice: same-state transitions, raw input through UI, ambiguous system order.
4. Compiles-but-wrong after touching old code: stale API from a previous Bevy version — cross-check the bevy-upgrade skill's rename tables.