🤖 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:
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.14–0.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.
|
||||||
@@ -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.16–0.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.16–0.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.
|
||||||
Reference in New Issue
Block a user