# 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) { if clicked { state.spawn_requested = true; } } fn handle_spawns(mut state: ResMut, 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>` panics in headless runs (tests, dedicated servers) — the resource doesn't exist without `RenderPlugin`: ```rust fn my_system(materials: Option>>) { 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>` 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` — that duplicates what the macro does and desyncs. ## 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` 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.