Files
reliquary/plugins/bevy/skills/bevy-0.19/SKILL.md
T
g4borg 8988b16bee 🤖 Fix proofreading issues in bevy skills
- Fix `ref` keyword used as variable name in code examples
- Fix "retries next tick" → systems continue on normal schedule
- Fix `State::set()` → `NextState::set()` in upgrade skill
- Fix `SceneRoot` → `WorldAssetRoot` for 0.19 consistency
- Fix Replace lifecycle event description (overwritten, not removed)
- Fix grep patterns (quoting, trailing spaces)
- Remove third-party physics/networking content (not Bevy built-ins)
2026-07-07 17:52:20 +02:00

187 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: bevy-0.19
description: Write and navigate Bevy 0.19 (Rust game engine) code — ECS, plugins, systems, queries, events/messages, hierarchy, input, camera, rendering, assets. Use whenever editing Rust code that imports bevy, or the task involves Bevy components, systems, entity spawning, or game architecture. Pre-training knowledge of Bevy APIs is stale; consult this before writing Bevy code. For version migrations, defer to the bevy-upgrade skill.
---
# Bevy 0.19
Bevy's API changes fundamentally between minor versions. Code that looks correct from memory frequently targets 0.140.17 APIs and either fails to compile or — worse — compiles and silently misbehaves. This skill is ground truth for 0.19.
## Step 0: Confirm the version
Check `Cargo.toml` for the `bevy` version before writing code.
- **0.19** — this skill applies directly.
- **0.18** — most of this skill applies; 0.19-only notes are marked. Do not use 0.19-only APIs.
- **≤0.17, or the task is a version bump/migration** — use the **bevy-upgrade** skill. It owns rename tables and semantic shifts per version transition. Come back here for writing new idiomatic code once the target version is settled.
## Critical traps
The highest-frequency mistakes. When in doubt, trust this table over memory:
| Trap | Wrong (stale) | Correct (0.18/0.19) |
|------|---------------|---------------------|
| Parent component | `Parent` | `ChildOf(entity)` |
| Get parent | `*child_of` (deref) | `child_of.parent()` |
| Child iteration | `children.iter().copied()` | `for child in &children` |
| Recursive despawn | `despawn_recursive()` | `despawn()` (recursive by default) |
| Buffered events | `EventWriter`/`EventReader` | `MessageWriter`/`MessageReader` |
| Run condition | `on_event::<T>()` | `on_message::<T>()` |
| Bundles | `#[derive(Bundle)]` | spawn tuples + `#[require(...)]` |
| System errors | infallible `fn(...)` everywhere | `fn(...) -> Result` where fallible |
| Forward direction | +Z | **-Z** (`Transform::forward()`) |
| GLTF model forward | -Z like Bevy | **+Z** — negate or use `transform.back()` |
| `GlobalTransform` in `Update` | assumed current | stale — propagation runs in `PostUpdate` |
| Material shader bind group | `@group(2)` | `@group(3)` (group 2 is mesh bindings) |
| Same-state `set()` | no-op | fires `OnExit`+`OnEnter`; use `set_if_neq()` |
| `AmbientLight` | one global type | `GlobalAmbientLight` resource vs `AmbientLight` camera component |
| `ShaderRef` import | `bevy::render::render_resource` | `bevy::shader::ShaderRef` |
| `WinitPlugin` | `WinitPlugin::<WakeUp>` | `WinitPlugin` (not generic) |
**0.19-specific:**
| Trap | Detail |
|------|--------|
| Resources are components | Resources live on entities; `Query<Entity>` matches them — filter broad queries with `Without<IsResource>`. Cannot derive both `Component` and `Resource` on one type. |
| Scene renamed | `Scene`/`SceneRoot``WorldAsset`/`WorldAssetRoot` |
| `Ref<T>.clone()` | Returns `Ref<T>`, not the inner value — use `my_ref.deref().clone()` |
| `Assets::get_mut` | Returns `AssetMut<A>` (mutation-tracked), not `&mut A` |
| GLTF materials | Return `GltfMaterial`; use `#Material0/std` label for `StandardMaterial` |
## ECS core
### Components
```rust
#[derive(Component)]
struct Health(f32);
#[derive(Component)]
#[require(Transform, Visibility)] // auto-inserted when Player is added
struct Player;
```
- `#[require(B, C)]` replaces bundles: spawning the marker pulls in its dependencies. Constructors work: `#[require(Speed(5.0))]`, `#[require(Timer = default_timer())]`.
- `#[component(immutable)]` (0.18+) forbids `&mut T` access.
- `#[component(storage = "SparseSet")]` for frequently added/removed components.
- Lifecycle hooks: `#[component(on_add = f)]`, also `on_insert`, `on_replace`, `on_remove`, `on_despawn` — synchronous, inline with the ECS operation.
### Systems
Plain functions of `SystemParam`s. Make fallible systems return `Result` (0.18+) — errors are logged via `BevyError` and the system continues running on its normal schedule; `?` works with anything convertible to `BevyError`:
```rust
fn apply_damage(mut q: Query<&mut Health>, mut reader: MessageReader<Damage>) -> Result {
for msg in reader.read() {
q.get_mut(msg.target)?.0 -= msg.amount;
}
Ok(())
}
```
Useful params beyond the obvious: `Single<D, F>` (exactly one match or panic), `Populated<D, F>` (skip system if empty), `Option<Res<T>>` (resource may not exist — required for render resources in headless contexts), `Local<T>` (per-system persistent state), `RemovedComponents<T>`.
### 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 (custom relationships, debugging heuristics): `references/production-gotchas.md`.
## Code style
Write pragmatic Rust, not defensive Rust:
- Prefer `let Ok(x) = q.single() else { return; }` / `let Some(x) = ... else { return; }` over nested `if let` pyramids.
- Prefer fallible systems with `?` over `unwrap()` chains — a logged error beats a panic in a frame loop.
- Small, single-purpose components; marker components (`struct Player;`) are idiomatic and free.
- Don't hand-roll what the ECS gives you: relationship sync, recursive despawn, required components, run conditions.
## References
Load on demand — each is self-contained:
| File | Read when working on |
|------|----------------------|
| `references/plugins-and-app.md` | Plugin trait/lifecycle, plugin groups, App builder, states, schedules, run conditions |
| `references/ecs.md` | Component/resource/system/query/command details beyond the summary above |
| `references/hierarchy.md` | ChildOf, Children, spawning/detaching, relationships, transform propagation |
| `references/events-observers.md` | Message vs Event APIs, observers, lifecycle events, hooks |
| `references/input.md` | Keyboard, mouse, gamepad, touch |
| `references/camera-rendering.md` | Cameras, coordinates, meshes, materials, shaders, assets, lighting |
| `references/production-gotchas.md` | Silent failures, headless contexts, debugging heuristics |
## Related skill
**bevy-upgrade** — version migration (0.15→…→0.19): rename tables, semantic shifts, step-by-step upgrade workflow. Use it whenever the task is moving code *between* versions rather than writing code *for* one. It may in turn send you back here for post-migration idiomatic cleanup.