51e7a2579b
- Minimal App::new() starting point with DefaultPlugins/MinimalPlugins - Resources section (Res/ResMut, insert, init, Option for headless) - Resources-as-components 0.19 expansion (broad query traps, dual-derive, observers) - Startup/Update/FixedUpdate schedule overview - Asset loading (handles, scene labels, readiness gates, 0.19 renames) - Time and timers (delta_secs, Timer component pattern)
290 lines
16 KiB
Markdown
290 lines
16 KiB
Markdown
---
|
||
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.14–0.17 APIs and either fails to compile or — worse — compiles and silently misbehaves. This skill is ground truth for 0.19.
|
||
|
||
## Step 0: Confirm the version
|
||
|
||
Check `Cargo.toml` for the `bevy` version before writing code.
|
||
|
||
- **0.19** — this skill applies directly.
|
||
- **0.18** — most of this skill applies; 0.19-only notes are marked. Do not use 0.19-only APIs.
|
||
- **≤0.17, or the task is a version bump/migration** — use the **bevy-upgrade** skill. It owns rename tables and semantic shifts per version transition. Come back here for writing new idiomatic code once the target version is settled.
|
||
|
||
## Minimal starting point
|
||
|
||
```rust
|
||
use bevy::prelude::*;
|
||
|
||
fn main() {
|
||
App::new()
|
||
.add_plugins(DefaultPlugins)
|
||
.add_systems(Startup, setup)
|
||
.add_systems(Update, gameplay)
|
||
.run();
|
||
}
|
||
|
||
fn setup(mut commands: Commands) {
|
||
commands.spawn(Camera3d);
|
||
}
|
||
|
||
fn gameplay() {}
|
||
```
|
||
|
||
`DefaultPlugins` includes windowing, rendering, input, audio, and asset loading. For headless (tests, servers): `MinimalPlugins`.
|
||
|
||
## Critical traps
|
||
|
||
The highest-frequency mistakes. When in doubt, trust this table over memory:
|
||
|
||
| Trap | Wrong (stale) | Correct (0.18/0.19) |
|
||
|------|---------------|---------------------|
|
||
| Parent component | `Parent` | `ChildOf(entity)` |
|
||
| Get parent | `*child_of` (deref) | `child_of.parent()` |
|
||
| Child iteration | `children.iter().copied()` | `for child in &children` |
|
||
| Recursive despawn | `despawn_recursive()` | `despawn()` (recursive by default) |
|
||
| Buffered events | `EventWriter`/`EventReader` | `MessageWriter`/`MessageReader` |
|
||
| Run condition | `on_event::<T>()` | `on_message::<T>()` |
|
||
| Bundles | `#[derive(Bundle)]` | spawn tuples + `#[require(...)]` |
|
||
| System errors | infallible `fn(...)` everywhere | `fn(...) -> Result` where fallible |
|
||
| Forward direction | +Z | **-Z** (`Transform::forward()`) |
|
||
| GLTF model forward | -Z like Bevy | **+Z** — negate or use `transform.back()` |
|
||
| `GlobalTransform` in `Update` | assumed current | stale — propagation runs in `PostUpdate` |
|
||
| Material shader bind group | `@group(2)` | `@group(3)` (group 2 is mesh bindings) |
|
||
| Same-state `set()` | no-op | fires `OnExit`+`OnEnter`; use `set_if_neq()` |
|
||
| `AmbientLight` | one global type | `GlobalAmbientLight` resource vs `AmbientLight` camera component |
|
||
| `ShaderRef` import | `bevy::render::render_resource` | `bevy::shader::ShaderRef` |
|
||
| `WinitPlugin` | `WinitPlugin::<WakeUp>` | `WinitPlugin` (not generic) |
|
||
|
||
**0.19-specific:**
|
||
|
||
| Trap | Detail |
|
||
|------|--------|
|
||
| Resources are components | Resources live on entities; `Query<Entity>` matches them — filter broad queries with `Without<IsResource>`. Cannot derive both `Component` and `Resource` on one type. |
|
||
| Scene renamed | `Scene`/`SceneRoot` → `WorldAsset`/`WorldAssetRoot` |
|
||
| `Ref<T>.clone()` | Returns `Ref<T>`, not the inner value — use `my_ref.deref().clone()` |
|
||
| `Assets::get_mut` | Returns `AssetMut<A>` (mutation-tracked), not `&mut A` |
|
||
| GLTF materials | Return `GltfMaterial`; use `#Material0/std` label for `StandardMaterial` |
|
||
|
||
## ECS core
|
||
|
||
### Components
|
||
|
||
```rust
|
||
#[derive(Component)]
|
||
struct Health(f32);
|
||
|
||
#[derive(Component)]
|
||
#[require(Transform, Visibility)] // auto-inserted when Player is added
|
||
struct Player;
|
||
```
|
||
|
||
- `#[require(B, C)]` replaces bundles: spawning the marker pulls in its dependencies. Constructors work: `#[require(Speed(5.0))]`, `#[require(Timer = default_timer())]`.
|
||
- `#[component(immutable)]` (0.18+) forbids `&mut T` access.
|
||
- `#[component(storage = "SparseSet")]` for frequently added/removed components.
|
||
- Lifecycle hooks: `#[component(on_add = f)]`, also `on_insert`, `on_replace`, `on_remove`, `on_despawn` — synchronous, inline with the ECS operation.
|
||
|
||
### Systems
|
||
|
||
Plain functions of `SystemParam`s. Make fallible systems return `Result` (0.18+) — errors are logged via `BevyError` and the system continues running on its normal schedule; `?` works with anything convertible to `BevyError`:
|
||
|
||
```rust
|
||
fn apply_damage(mut q: Query<&mut Health>, mut reader: MessageReader<Damage>) -> Result {
|
||
for msg in reader.read() {
|
||
q.get_mut(msg.target)?.0 -= msg.amount;
|
||
}
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
Useful params beyond the obvious: `Single<D, F>` (exactly one match or panic), `Populated<D, F>` (skip system if empty), `Option<Res<T>>` (resource may not exist — required for render resources in headless contexts), `Local<T>` (per-system persistent state), `RemovedComponents<T>`.
|
||
|
||
Schedules: `Startup` runs once at launch (initialization, spawning the world). `Update` runs every frame. `FixedUpdate` runs at a fixed timestep (framerate-independent simulation). Full schedule list: `references/plugins-and-app.md`.
|
||
|
||
### Queries
|
||
|
||
- Data: `&T`, `&mut T`, `Entity`, `Has<T>`, tuples.
|
||
- Filters: `With<T>`, `Without<T>`, `Added<T>`, `Changed<T>`, `Spawned`, `Or<(A, B)>`.
|
||
- `.single()` panics unless exactly one match; prefer `Single` param or handle the `Result`.
|
||
- Derive `QueryData`/`QueryFilter` for reusable complex queries.
|
||
|
||
### Commands
|
||
|
||
`commands.spawn((CompA, CompB))` with tuples. `commands.entity(e).insert(..)` / `.insert_if_new(..)` / `.remove::<T>()` / `.despawn()`. Commands are deferred — they apply at the next sync point, not immediately.
|
||
|
||
### Ordering
|
||
|
||
`(a, b).chain()`, `.before(x)` / `.after(x)`, `#[derive(SystemSet)]` + `configure_sets`. Unordered systems with overlapping mutable access run in nondeterministic order — order anything where it matters.
|
||
|
||
### Resources
|
||
|
||
Global singletons accessed by type:
|
||
|
||
```rust
|
||
#[derive(Resource)]
|
||
struct Score(u32);
|
||
|
||
// Insert in setup:
|
||
app.insert_resource(Score(0));
|
||
app.init_resource::<Score>(); // requires Default
|
||
|
||
// Access in systems:
|
||
fn show_score(score: Res<Score>) { /* read */ }
|
||
fn add_point(mut score: ResMut<Score>) { score.0 += 1; }
|
||
```
|
||
|
||
- `Option<Res<T>>` / `Option<ResMut<T>>` for resources that may not exist — mandatory for render resources in headless-capable systems.
|
||
- `Local<T>` is per-system private state (default-initialized, persists across runs) — not a resource.
|
||
|
||
**0.19**: Resources live on dedicated entities internally. This means `Query<Entity>` (and other broad queries) match resource entities. Filter with `Without<IsResource>`. A type cannot derive both `Component` and `Resource`. See below.
|
||
|
||
Details: `references/ecs.md`.
|
||
|
||
### Resources-as-components (0.19)
|
||
|
||
In 0.19, `Resource` is implemented via `Component` internally. Each resource lives on a hidden entity. This is mostly transparent, but creates real issues:
|
||
|
||
1. **Broad queries match resources.** `Query<Entity>` or `Query<&Name>` will include resource entities. Always add `Without<IsResource>` to gameplay queries that don't filter on a specific game component.
|
||
2. **Cannot dual-derive.** `#[derive(Component, Resource)]` on one type is a compile error. Pick one role. If you need both a global config and per-entity overrides, use two types.
|
||
3. **Observers see resources.** Entity-lifecycle observers (`on_add`, `on_remove`) fire for resource insertion/removal. This can surprise broad observers like `On<Add<Name>>`.
|
||
4. **`Res<T>` / `ResMut<T>` still work.** The system param API is unchanged — you do not need to query for resources manually. The entity backing is an implementation detail unless you write broad queries or observers.
|
||
|
||
## Hierarchy
|
||
|
||
`ChildOf(parent)` on the child is the source of truth; `Children` on the parent is auto-synced. Despawning a parent despawns descendants.
|
||
|
||
```rust
|
||
commands.spawn((Enemy, ChildOf(parent)));
|
||
commands.entity(parent).with_children(|s| { s.spawn(Turret); });
|
||
commands.entity(parent).add_child(existing);
|
||
```
|
||
|
||
Detaching without despawning: `detach_child` / `detach_children` / `detach_all_children` (0.18 names). Despawn only the children: `despawn_related::<Children>()`.
|
||
|
||
Details, custom relationships, transform propagation: `references/hierarchy.md`.
|
||
|
||
## Events vs Messages — pick correctly
|
||
|
||
0.18 split the old event system in two. Choosing wrong is the most common architectural error:
|
||
|
||
| Mechanism | Model | Use for |
|
||
|-----------|-------|---------|
|
||
| `#[derive(Message)]` + `MessageWriter`/`MessageReader` | Pull, double-buffered, batch | Decoupled many-to-many data flow; multiple systems consuming the same stream |
|
||
| `#[derive(Event)]` + `commands.trigger()` + observers (`On<E>`) | Push, immediate | React *now*: gameplay reactions, structural changes |
|
||
| `#[derive(EntityEvent)]` + `#[event_target]` | Push, per-entity, can `propagate` up `ChildOf` | Entity-targeted reactions (hit, clicked) |
|
||
| Component hooks | Synchronous, inline | Response must be atomic with the ECS operation |
|
||
| `Changed<T>`/`Added<T>` filters | Per-tick polling | Cheap frame-by-frame reaction in a normal system |
|
||
|
||
Full API, lifecycle events (`Add`/`Insert`/`Replace`/`Remove`/`Despawn`), observer registration: `references/events-observers.md`.
|
||
|
||
## Architecture
|
||
|
||
How to structure a Bevy project — and how to navigate an unfamiliar one:
|
||
|
||
- **One plugin per feature/domain** (`CombatPlugin`, `InventoryPlugin`, `UiPlugin`), each owning its components, systems, messages, and state. `main.rs` should be little more than `App::new().add_plugins(...)`.
|
||
- **Navigate by the plugin tree**: start at `App::new()`, follow `add_plugins` recursively; grep `add_systems` and `#[derive(SystemSet)]` to map execution order; grep `#[derive(Component)]`/`#[derive(Resource)]` per module to map data ownership.
|
||
- **Cross-plugin communication** goes through Messages or shared resources — not direct system coupling. A plugin's components are its internals; its messages are its API.
|
||
- **States** (`init_state`, `OnEnter`/`OnExit`, `in_state` run conditions) for game flow; `DespawnOnExit(state)` for scoped entities. Remember same-state `set()` re-fires transitions — one-time setup in `OnEnter` needs `set_if_neq` discipline from callers.
|
||
- **No formal plugin dependencies exist.** Guard with `is_plugin_added::<T>()`, or do cross-plugin setup in `Plugin::finish()` (runs after all plugins are ready).
|
||
- **Headless-safe systems**: anything that may run in tests or dedicated servers must take render resources as `Option<Res<...>>`.
|
||
- **Spawn from standard schedules.** Entities spawned from exclusive/non-standard schedules (UI passes, custom schedules) can be invisible to plugins that hook the normal cycle — set a flag there, spawn in `Update`.
|
||
|
||
Plugin trait lifecycle, plugin groups, states, run conditions: `references/plugins-and-app.md`.
|
||
|
||
## Coordinates and transforms
|
||
|
||
- Right-handed. **+Y up, -Z forward, +X right.** `Transform::forward()` is -Z.
|
||
- GLTF models face +Z visually — opposite Bevy. Orient with `look_to(-dir, Vec3::Y)` or treat `transform.back()` as the model's facing.
|
||
- Transform propagation runs in `PostUpdate`: reading `GlobalTransform` in `Update` gives last frame's value. Move the reader to `PostUpdate` after `TransformSystem::TransformPropagate`, or accept the one-frame lag deliberately.
|
||
|
||
## Asset loading
|
||
|
||
```rust
|
||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||
let texture: Handle<Image> = asset_server.load("textures/player.png");
|
||
let scene: Handle<WorldAsset> = asset_server.load("models/enemy.glb#Scene0");
|
||
commands.spawn((Mesh3d(asset_server.load("mesh.glb#Mesh0/Primitive0")),
|
||
MeshMaterial3d(asset_server.load("mesh.glb#Material0/std"))));
|
||
}
|
||
```
|
||
|
||
- `asset_server.load(path)` returns a `Handle<T>` immediately; the asset loads asynchronously in the background.
|
||
- Assets live in `Assets<T>` collections: `fn sys(images: Res<Assets<Image>>)` to inspect loaded data.
|
||
- Gate on readiness with `AssetEvent::LoadedWithDependencies` or `asset_server.is_loaded_with_dependencies(handle)`.
|
||
- GLTF scenes must use **scene labels** (`"model.glb#Scene0"`), not the raw file handle.
|
||
- **0.19**: `Scene`/`SceneRoot` renamed to `WorldAsset`/`WorldAssetRoot`. `Assets::get_mut` returns `AssetMut<A>` (mutation-tracked).
|
||
|
||
Caching pitfalls and more: `references/camera-rendering.md`.
|
||
|
||
## Time and timers
|
||
|
||
```rust
|
||
fn move_player(time: Res<Time>, mut q: Query<&mut Transform, With<Player>>) -> Result {
|
||
let mut t = q.single_mut()?;
|
||
t.translation += Vec3::NEG_Z * 5.0 * time.delta_secs();
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
- `Res<Time>` — `time.delta_secs()` (f32) or `time.delta()` (Duration) for the frame delta.
|
||
- `time.elapsed_secs()` for total time since app start.
|
||
|
||
```rust
|
||
#[derive(Component)]
|
||
struct FireRate(Timer);
|
||
|
||
fn shoot(time: Res<Time>, mut q: Query<&mut FireRate>) {
|
||
for mut rate in &mut q {
|
||
rate.0.tick(time.delta());
|
||
if rate.0.just_finished() { /* fire */ }
|
||
}
|
||
}
|
||
```
|
||
|
||
`Timer::from_seconds(1.0, TimerMode::Repeating)` for recurring; `TimerMode::Once` for one-shot. Timers do not auto-tick — call `.tick(delta)` each frame.
|
||
|
||
## Silent-failure checklist
|
||
|
||
Bevy often succeeds silently when misconfigured. If something "doesn't show up" with no errors:
|
||
|
||
1. **Mesh has no normals** → invisible. Use `.with_computed_flat_normals()` on programmatic meshes.
|
||
2. **GLTF spawned by raw handle** → nothing. Spawn scene labels: `"model.glb#Scene0"`.
|
||
3. **Parent lacks `Visibility`/`InheritedVisibility`** → children invisible.
|
||
4. **Object beyond default far plane (1000 units)** → culled.
|
||
5. **Asset cache poisoning**: `load_with_settings` with stripped meshes caches per *path*; a later plain `load()` of the same path returns the stripped version. Never mix stripped and rendered loads of one asset in one process.
|
||
6. **Assets aren't ready after `load()`** — gate on `AssetEvent::LoadedWithDependencies` or `is_loaded_with_dependencies()`.
|
||
7. **Raw `ButtonInput<MouseButton>` fires through UI** — world click handlers need UI-consumption checks.
|
||
|
||
More production gotchas (custom relationships, debugging heuristics): `references/production-gotchas.md`.
|
||
|
||
## Code style
|
||
|
||
Write pragmatic Rust, not defensive Rust:
|
||
|
||
- Prefer `let Ok(x) = q.single() else { return; }` / `let Some(x) = ... else { return; }` over nested `if let` pyramids.
|
||
- Prefer fallible systems with `?` over `unwrap()` chains — a logged error beats a panic in a frame loop.
|
||
- Small, single-purpose components; marker components (`struct Player;`) are idiomatic and free.
|
||
- Don't hand-roll what the ECS gives you: relationship sync, recursive despawn, required components, run conditions.
|
||
|
||
## References
|
||
|
||
Load on demand — each is self-contained:
|
||
|
||
| File | Read when working on |
|
||
|------|----------------------|
|
||
| `references/plugins-and-app.md` | Plugin trait/lifecycle, plugin groups, App builder, states, schedules, run conditions |
|
||
| `references/ecs.md` | Component/resource/system/query/command details beyond the summary above |
|
||
| `references/hierarchy.md` | ChildOf, Children, spawning/detaching, relationships, transform propagation |
|
||
| `references/events-observers.md` | Message vs Event APIs, observers, lifecycle events, hooks |
|
||
| `references/input.md` | Keyboard, mouse, gamepad, touch |
|
||
| `references/camera-rendering.md` | Cameras, coordinates, meshes, materials, shaders, assets, lighting |
|
||
| `references/production-gotchas.md` | Silent failures, headless contexts, debugging heuristics |
|
||
|
||
## Related skill
|
||
|
||
**bevy-upgrade** — version migration (0.15→…→0.19): rename tables, semantic shifts, step-by-step upgrade workflow. Use it whenever the task is moving code *between* versions rather than writing code *for* one. It may in turn send you back here for post-migration idiomatic cleanup.
|