🤖 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
+1 -1
View File
@@ -1 +1 @@
Bevy game engine skills. The `bevy` skill is the primary implementation helper; `bevy-upgrade` handles version migrations and is invoked by `bevy` when upgrade work is detected. Bevy game engine skills. The `bevy-0.19` skill is the primary implementation helper; `bevy-upgrade` handles version migrations and is invoked by `bevy-0.19` when upgrade work is detected.
+1 -1
View File
@@ -7,7 +7,7 @@ Bevy's rapid release cycle (0.16 through 0.19 in ~18 months) introduces fundamen
## Skills ## Skills
- **bevy-0.19** — Implementation helper for writing correct, idiomatic Bevy 0.19 code. Covers plugins, ECS, hierarchy, events/observers, input, camera, and production gotchas. Triggers on Bevy 0.19 Rust code. Contains a compact always-loaded cheat sheet plus a `references/` folder for deeper topic dives. - **bevy-0.19** — Implementation helper for writing correct, idiomatic Bevy 0.19 code. Covers plugins, ECS, hierarchy, events/observers, input, camera, and production gotchas. Triggers on Bevy 0.19 Rust code. Contains a compact always-loaded cheat sheet plus a `references/` folder for deeper topic dives.
- **bevy-upgrade** — Version migration guide for upgrading between Bevy releases. Covers the breaking-change chain (0.16 through 0.19+) with rename tables, semantic shifts, and step-by-step migration patterns. Invoked by the `bevy` skill when an upgrade task is detected, or directly when the user asks to migrate. - **bevy-upgrade** — Version migration guide for upgrading between Bevy releases. Covers the breaking-change chain (0.16 through 0.19+) with rename tables, semantic shifts, and step-by-step migration patterns. Invoked by the `bevy-0.19` skill when an upgrade task is detected, or directly when the user asks to migrate.
## Research ## Research
+176 -28
View File
@@ -1,38 +1,186 @@
--- ---
name: bevy-0.19 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 Check `Cargo.toml` for the `bevy` version before writing code.
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
## 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 The highest-frequency mistakes. When in doubt, trust this table over memory:
- [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 | Trap | Wrong (stale) | Correct (0.18/0.19) |
- [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 | Parent component | `Parent` | `ChildOf(entity)` |
- [05-input.md](../../../../docs/workpad/research/bevy-skill-research/05-input.md) — Input handling | Get parent | `*child_of` (deref) | `child_of.parent()` |
- [06-camera-gotchas.md](../../../../docs/workpad/research/bevy-skill-research/06-camera-gotchas.md) — Camera and rendering | Child iteration | `children.iter().copied()` | `for child in &children` |
- [07-lessons-from-production.md](../../../../docs/workpad/research/bevy-skill-research/07-lessons-from-production.md) — Production gotchas | Recursive despawn | `despawn_recursive()` | `despawn()` (recursive by default) |
- [08-landscape-and-design-sketch.md](../../../../docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md) — Architecture options | 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.
+42 -23
View File
@@ -1,33 +1,52 @@
--- ---
name: bevy-upgrade name: bevy-upgrade
description: "TODO: Use the skill-creator skill to write this. See scaffolding goals below." description: Migrate Bevy (Rust game engine) projects between versions, 0.15 through 0.19. Rename tables, semantic behavior shifts, and a per-bump migration workflow. Use when the task bumps the bevy version in Cargo.toml, fixes compile errors after an upgrade, or modernizes code using removed APIs (Parent, EventWriter/EventReader, despawn_recursive, derive(Bundle), StateScoped). For writing new code on the current version, defer to the bevy-0.19 skill.
--- ---
# Bevy Upgrade — Version Migration Guide # Bevy Upgrade
> **This skill is a scaffold.** Use `/skill-creator` to create the full skill content from the research material. Bevy ships breaking changes every minor release. Upgrades are tractable if run as a disciplined loop — one minor version at a time, mechanical renames before semantic fixes, behavior shifts verified by running the game, not just compiling it.
## Scaffolding Goals ## Workflow
1. **Use the `skill-creator:skill-creator` skill** to create the full SKILL.md content 1. **Establish current and target versions.** Read `Cargo.toml` (`bevy` plus ecosystem crates: physics, UI, networking — each is pinned to a Bevy minor and must be bumped in lockstep).
2. Feed it the version-change annotations from research reports 01-06 in [`docs/workpad/research/bevy-skill-research/`](../../../../docs/workpad/research/bevy-skill-research/00-index.md) 2. **Upgrade one minor version at a time.** Skipping versions compounds renames and makes errors unattributable. For each bump:
3. The skill should trigger when the user asks to upgrade/migrate Bevy versions, or when the `bevy` skill detects upgrade work 1. Bump `bevy` and every Bevy-ecosystem crate to versions compatible with the target. If an ecosystem crate has no compatible release, stop and surface it — that blocks the bump.
4. Core content: 2. `cargo check`, then apply the **rename table** for the transition (see references) across all errors. These are mechanical.
- Breaking-change chain from 0.16 through 0.19+ 3. Fix remaining compile errors using the transition's **API shape changes** (signatures, moved modules, new required destructuring).
- Rename tables (old name -> new name, which version) 4. Audit for the transition's **semantic shifts** — changes that compile clean but behave differently. Grep for the listed patterns; these do not announce themselves.
- Semantic shifts (behavior changes, not just renames) 5. `cargo check` clean → run the game / test suite and smoke-test the areas the semantic shifts touch.
- Step-by-step migration patterns for each version bump 3. **Anything not covered by the references**: fetch the official migration guide at `https://bevy.org/learn/migration-guides/<from>-to-<to>/` and work through it — the references cover the high-frequency changes, not every niche API.
- Common upgrade pitfalls and their fixes 4. **Post-migration cleanup**: once on the target version, use the **bevy-0.19** skill to rewrite awkward mechanically-ported code into current idioms (e.g. old buffered events that should now be observers, manual child bookkeeping that relationships handle).
5. Populate `references/` with per-version-transition files (e.g., `0.17-to-0.18.md`, `0.18-to-0.19.md`)
6. The skill should be invocable both directly and from the `bevy` skill
## Reference Material ## Transition references
Same research as the `bevy` skill — version-change annotations are woven into each report: Load only the transitions on your path:
- [01-plugins.md](../../../../docs/workpad/research/bevy-skill-research/01-plugins.md) — Plugin API changes across versions | File | Covers |
- [02-ecs-core.md](../../../../docs/workpad/research/bevy-skill-research/02-ecs-core.md) — ECS API renames and new patterns |------|--------|
- [03-hierarchy.md](../../../../docs/workpad/research/bevy-skill-research/03-hierarchy.md) — Parent->ChildOf transition | `references/0.15-to-0.17.md` | 0.15→0.16 and 0.16→0.17: `Parent``ChildOf`, recursive `despawn`, bundle removal, `StateScoped` replacement |
- [04-events-observers.md](../../../../docs/workpad/research/bevy-skill-research/04-events-observers.md) — Event/Message split | `references/0.17-to-0.18.md` | The largest transition: Event/Message split, fallible systems, state-transition semantics, renames across hierarchy/render/input |
- [05-input.md](../../../../docs/workpad/research/bevy-skill-research/05-input.md) — Input API changes | `references/0.18-to-0.19.md` | Resources-as-components, `Scene``WorldAsset`, asset API changes |
- [06-camera-gotchas.md](../../../../docs/workpad/research/bevy-skill-research/06-camera-gotchas.md) — Camera API changes
## Detecting the source version from code
When `Cargo.toml` is ambiguous (workspace inheritance, git deps), the code itself dates the project:
| You see | Version is |
|---------|-----------|
| `Parent`, `despawn_recursive()`, `#[derive(Bundle)]`, `StateScoped` | ≤0.15 |
| `ChildOf` but `EventWriter`/`EventReader`/`on_event` | 0.160.17 |
| `MessageWriter`/`MessageReader`, `-> Result` systems | 0.18+ |
| `WorldAsset`/`WorldAssetRoot`, `Without<IsResource>` filters | 0.19 |
## The traps of upgrading
- **Renames that compile but misbehave**: some old names still exist with new meaning. `Event` compiles in 0.18+ but means observer-events, not buffered messages — a mechanical `EventWriter → MessageWriter` port is right, but a type left as `#[derive(Event)]` and "written" nowhere fires no observers silently.
- **Behavior shifts need runtime verification**: `State::set()` same-state re-firing (0.18), transform-propagation reads, `Query<Entity>` matching resource entities (0.19). `cargo check` proves nothing here — run it.
- **Ecosystem crates lag.** A Bevy bump is only as ready as its slowest dependency. Check compatibility before starting, not after.
- **Don't modernize mid-bump.** During a bump, make the minimal change that satisfies the new API; idiomatic rewrites happen after the chain is complete (with the bevy-0.19 skill). Mixing the two makes regressions unattributable.
## Related skill
**bevy-0.19** — writing correct, idiomatic code on the current version. Use it after the migration chain completes, and whenever the task turns from porting into feature work.
@@ -0,0 +1,36 @@
# 0.15 → 0.16 → 0.17
The 0.16 transition restructured hierarchy and spawning; 0.17 was comparatively quiet. Both are covered here. For changes not listed, consult the official guides: `https://bevy.org/learn/migration-guides/0-15-to-0-16/` and `.../0-16-to-0-17/`.
## Renames & removals (mechanical)
| Old (≤0.15) | New (0.16+) | Notes |
|-------------|-------------|-------|
| `Parent` component | `ChildOf(pub Entity)` | Lives on the child; entities *with* `ChildOf` are children |
| `*parent` (Deref to Entity) | `child_of.parent()` | Deref removed |
| `despawn_recursive()` | `despawn()` | `despawn()` is now recursive by default |
| `despawn_descendants()` | `despawn_related::<Children>()` | |
| `#[derive(Bundle)]` | spawn tuples + `#[require(...)]` | Bundles removed as a user-facing pattern |
| `StateScoped(state)` | `DespawnOnExit(state)` / `DespawnOnEnter(state)` | |
| `parent_entity()` (child spawner) | `target_entity()` | |
## API shape changes
- **Hierarchy is relationship-based**: `ChildOf` implements `Relationship`, `Children` implements `RelationshipTarget`; they sync automatically via hooks. Delete any manual `Children` bookkeeping — it now fights the engine.
- **Spawning children**: `commands.spawn((Comp, ChildOf(parent)))` works directly; `with_children(|spawner| ...)` remains. `add_child` / `add_children` / `insert_children` attach existing entities.
- **`Children` iteration** yields `Entity` via `IntoIterator`: `for child in &children`. Ports of `children.iter().copied()` compile but are noise — simplify during cleanup.
- **Required components** replace bundles: `#[require(Transform, Visibility)]` on a marker pulls in dependencies at spawn. When porting a `Bundle` struct, its fields usually become either a spawn tuple at call sites or `#[require(...)]` on the primary component.
- Query relationship extensions appear: `query.related::<ChildOf>(entity)`, `query.relationship_sources::<Children>(entity)`.
## Semantic shifts (audit, don't just compile)
- **`despawn()` is recursive.** Code that relied on `despawn()` orphaning children (intentionally detaching them) now destroys them. If detach-then-despawn was the intent, `detach_all_children()` first (name as of 0.18; `clear_children()` in 0.160.17).
- **Adding `ChildOf` has side effects**: parent's `Children` updates immediately via hooks, and lifecycle observers on `Children` fire. Ordering assumptions around manual parent updates are void.
## Porting checklist
1. `grep -rn "Parent\b"` — replace with `ChildOf`, fix accessors to `.parent()`.
2. `grep -rn "despawn_recursive\|despawn_descendants"` — rename; then audit remaining plain `despawn()` calls for the orphaning assumption.
3. `grep -rn "derive(Bundle)"` — convert to tuples/`#[require]`.
4. `grep -rn "StateScoped"` — replace with `DespawnOnExit`/`DespawnOnEnter`.
5. Remove manual child-list maintenance (observers/systems pushing into `Children`).
@@ -0,0 +1,57 @@
# 0.17 → 0.18
The largest recent transition. Its centerpiece — the Event/Message split — is architectural, not just a rename. Official guide for the long tail: `https://bevy.org/learn/migration-guides/0-17-to-0-18/`.
## The Event/Message split
The old buffered event system was split in two:
| Old (≤0.17) | New (0.18) |
|-------------|-----------|
| `#[derive(Event)]` (buffered use) | `#[derive(Message)]` |
| `EventWriter<T>` / `.send(e)` | `MessageWriter<T>` / `.write(m)` |
| `EventReader<T>` | `MessageReader<T>` |
| `Events<T>` | `Messages<T>` |
| `on_event::<T>()` run condition | `on_message::<T>()` |
`Event` still exists but now means **observer-events only** (`commands.trigger()` + `On<E>` observers, immediate push). This is the transition's biggest trap: a type ported to `#[derive(Event)]` and written with a `MessageWriter` won't compile, but a type left as `Event` with a forgotten reader silently does nothing.
Port rule: every buffered event → `Message`. Only convert to `Event`+observer deliberately, during post-migration cleanup, where immediate reaction is actually wanted.
New in 0.18: `#[derive(EntityEvent)]` with `#[event_target]` for entity-targeted events, optionally `#[entity_event(propagate)]` to bubble up `ChildOf`.
## Renames & moves (mechanical)
| Old | New |
|-----|-----|
| `remove_child` / `remove_children` / `clear_children` | `detach_child` / `detach_children` / `detach_all_children` |
| `bevy::render::render_resource::ShaderRef` | `bevy::shader::ShaderRef` |
| `WinitPlugin::<WakeUp>` | `WinitPlugin` (no longer generic) |
| `AmbientLight` (single type) | `GlobalAmbientLight` **resource** + `AmbientLight` **camera component** |
| `Camera.target` field | `RenderTarget` required component |
| `GltfPlugin` `use_model_forward_direction` | `convert_coordinates` + `rotate_scene_entity` / `rotate_meshes` flags |
## API shape changes
- **Fallible systems**: systems may return `Result` (= `Result<(), BevyError>`); errors log and the system retries next tick. Adopt during the bump wherever it deletes `unwrap()`s cheaply; systematically during cleanup.
- **Mesh accessors** became `try_*` returning `Result<_, MeshAccessError>`.
- **`MaterialPlugin` fields** `prepass_enabled` / `shadows_enabled` became `Material` trait methods.
- **`LoadContext::path()`** returns `AssetPath`, not `Path`; image `reinterpret_*` methods return `Result`.
- **Immutable components**: `#[component(immutable)]` available.
- **Input feature gates**: `mouse`, `keyboard`, `gamepad`, `touch`, `gestures` are now cargo features. Default-on, but builds with `default-features = false` must list them — otherwise input silently stops arriving.
## Semantic shifts (audit, don't just compile)
- **`NextState::set()` same-state transitions fire.** `set(Playing)` while in `Playing` now triggers `OnExit(Playing)` + `OnEnter(Playing)` (previously a no-op). One-time setup in `OnEnter` (spawn camera, load level) re-runs. Audit every `next_state.set(...)` reachable while already in that state; use `set_if_neq()` where the old behavior is wanted.
- **Material shader bind groups**: material bindings are `@group(3)`; `@group(2)` is the mesh storage buffer. Old custom shaders using `@group(2)` fail with `Storage class Storage doesn't match the shader Uniform`. Update all `.wgsl` material shaders.
- **`MouseMotion`, `MouseWheel` and other input streams are Messages now** — readers must be `MessageReader`.
## Porting checklist
1. `grep -rn "EventWriter\|EventReader\|on_event\|Events<"` — rename to Message equivalents; `.send(``.write(`.
2. `grep -rn "derive(Event)"` — decide per type: buffered (→ `Message`) or observed (stays `Event`, needs a trigger + observer).
3. `grep -rn "remove_child\|clear_children"` — rename to `detach_*`.
4. `grep -rn "next_state.set\|NextState"` — audit same-state re-fire.
5. `grep -rn "@group(2)" --include=*.wgsl` — move material bindings to group 3.
6. `grep -rn "AmbientLight"` — split into resource vs camera component usage.
7. Check `Cargo.toml` for `default-features = false` on bevy → add input features.
@@ -0,0 +1,36 @@
# 0.18 → 0.19
Smaller than 0.17→0.18 but with one deep semantic change: resources became components. Official guide for the long tail: `https://bevy.org/learn/migration-guides/0-18-to-0-19/`.
## Renames & moves (mechanical)
| Old (0.18) | New (0.19) |
|------------|-----------|
| `Scene` / `SceneRoot` | `WorldAsset` / `WorldAssetRoot` |
| `AssetPath::resolve(&str)` / `resolve_embed(&str)` | `resolve_str()` / `resolve_embed_str()` (`resolve()`/`resolve_embed()` now take `&AssetPath`) |
| `Hdr` in `bevy_render` | `bevy_camera` |
| `Camera` screen-space specular transmission fields | `ScreenSpaceTransmission` component |
## API shape changes
- **`Assets::get_mut`** returns `AssetMut<A>` (mutation-tracked), not `&mut A`. Deref for access; code storing the `&mut A` needs restructuring.
- **`Ref<T>.clone()`** returns `Ref<T>`, not the inner value. Old code relying on clone-through must use `ref.deref().clone()`.
- **GLTF material loading** returns `GltfMaterial`; request a `StandardMaterial` with the `#Material0/std` label suffix.
- **`EntityComponentsTrigger`** gained archetype fields — exhaustive destructuring breaks; add `..`:
`let EntityComponentsTrigger { components, .. } = e.trigger();`
## Semantic shifts (audit, don't just compile)
- **Resources are components on abstract entities.** Consequences:
- `Query<Entity>` and other very broad queries now match resource entities. Any "iterate all entities" logic (cleanup sweeps, entity counts, serialization, debug overlays) silently includes resources — filter with `Without<IsResource>`.
- A type can no longer derive both `Component` and `Resource`. Split such types or pick one role.
- **Scene rename is semantic-adjacent**: `WorldAsset` naming reflects the same data model, but grep for the old names in strings/reflection paths, not just types.
## Porting checklist
1. `grep -rn "SceneRoot\|Scene>" ` — rename to `WorldAssetRoot`/`WorldAsset`.
2. `grep -rn "Query<Entity[,>]"` and other broad queries — add `Without<IsResource>` where resources must not appear.
3. `grep -rn "derive(Component" | grep "Resource"` — find dual-derive types; split them.
4. `grep -rn "get_mut" ` on `Assets<...>` — adapt to `AssetMut`.
5. `grep -rn "EntityComponentsTrigger {"` — add `..` to destructuring.
6. GLTF material handles — append `/std` labels where `StandardMaterial` is expected.