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