Scaffold bevy plugin with bevy-0.19 and bevy-upgrade skills

Add plugin skeleton with versioned skill directories, TODO SKILL.md
scaffolds with goals for skill-creator, and copy over bevy-skill-research
reports from spacecraft into docs/workpad/research/.
This commit is contained in:
2026-07-07 17:52:00 +02:00
parent 8545738a05
commit 325e278af1
13 changed files with 1211 additions and 0 deletions
@@ -0,0 +1,105 @@
# Bevy Skill Research — Index
> **Date**: 2026-07-07
## What We Want to Create
A **Claude Code skill for Bevy game engine development** — primarily an **implementation helper** that teaches how to correctly write Bevy code: how to structure plugins, compose systems, define components, handle events, manage hierarchy, process input, and avoid the silent failures that Bevy's flexible ECS makes easy to walk into.
The skill is NOT a migration guide or changelog. Its primary job is: **when an agent is writing or modifying Bevy code, it should produce correct, idiomatic code for the current version.**
However, because Bevy's rapid release cycle (0.16→0.17→0.18→0.19 in ~18 months) introduces fundamental renames and semantic shifts, the skill also needs **version-aware reference material** for situations like upgrades. An agent asked to migrate from 0.18 to 0.19 needs to know what changed and why — not just what the current API looks like. So the migration chain knowledge is retained as reference, not thrown away.
## How This Research Evolved
### Initial idea: upgrade-focused
The research started when we saw an agent fetch Bevy's 0.18→0.19 migration guide during an upgrade task. The question was: should a Bevy skill hold the migration chain as its core content, so agents don't have to fetch and parse migration guides at runtime?
### The pivot: implementation-focused
We quickly realized the migration chain is a **secondary use case**. The primary problem — confirmed by blog posts, community discussions, and this project's 1500+ lines of errata — is that LLMs confidently generate Bevy code against **outdated or incorrect patterns** during normal day-to-day implementation work. The skill's core content should teach **how to write correct Bevy code**, organized by topic (plugins, ECS, events, hierarchy, input, camera), not by version transition.
### What we researched
Two complementary knowledge sources, reflecting the two purposes:
1. **Official API truth** (reports 0106) — how Bevy's subsystems actually work in 0.18/0.19, with version-change annotations where LLMs are most likely to use stale patterns. This is the **implementation helper** core.
2. **Production battle scars** (report 07) — gotchas extracted from a production Bevy 0.18 multiplayer game's errata, neutralized into generic examples. These are the "not in any docs" patterns that cost real debugging time. Also **implementation helper** material.
3. **Landscape survey and design options** (report 08) — ecosystem survey confirming no adequate skill exists, plus three architectural approaches with tradeoffs. This informs **how to build the skill**, not what goes in it.
The migration chain knowledge (0.16→0.17→0.18→0.19 breaking changes) is woven into reports 0106 as annotations rather than being a standalone topic — because the skill's primary audience is an agent writing code today, not an agent performing a version migration. But the annotations are there, and a reference folder can hold the full migration details for when that situation arises.
## Why We Did This Research
1. **No adequate public skill exists.** We surveyed the ecosystem (see report 08) and found nothing usable for Bevy 0.18+. The best candidate (bfollington/terma, 154 installs) covers only up to 0.17 and would actively mislead on current APIs.
2. **This project already has better Bevy knowledge than any public skill** — but it's locked in project-specific errata and CLAUDE.md files. A skill would extract and generalize that knowledge for reuse.
3. **The skill needs to serve two modes**: day-to-day implementation (always-loaded, compact) and version migration (on-demand, detailed). The research captures content for both.
4. **The skill design has multiple viable approaches** with real tradeoffs around size, scope, and maintenance burden (report 08). This research captures the options before committing.
## Reports
### API Ground Truth (from official docs and migration guides)
| # | File | Topic |
|---|------|-------|
| 1 | [01-plugins.md](01-plugins.md) | Plugin trait lifecycle, dependencies, states, schedules, run conditions |
| 2 | [02-ecs-core.md](02-ecs-core.md) | Components, resources, systems, queries, commands, ordering |
| 3 | [03-hierarchy.md](03-hierarchy.md) | ChildOf relationships, spawning/querying children, transform propagation |
| 4 | [04-events-observers.md](04-events-observers.md) | Message/Event split (0.18), observers, lifecycle triggers, hooks |
| 5 | [05-input.md](05-input.md) | Keyboard, mouse, gamepad, touch input patterns |
| 6 | [06-camera-gotchas.md](06-camera-gotchas.md) | Camera setup, rendering, coordinates, full LLM-trap table |
### Production Knowledge and Design
| # | File | Topic |
|---|------|-------|
| 7 | [07-lessons-from-production.md](07-lessons-from-production.md) | 15 neutralized gotchas from a production Bevy 0.18 multiplayer game |
| 8 | [08-landscape-and-design-sketch.md](08-landscape-and-design-sketch.md) | Ecosystem survey, skill architecture options, content priorities |
## Skill Design Approaches Under Consideration
### Approach A: Single Compact Skill + Reference Folder
One `bevy` skill file (~500 lines) with the critical "don't get this wrong" content always loaded. Deeper reference docs in a `references/bevy/` folder that the LLM can read on demand.
- **Pro**: Low per-trigger context cost, deep knowledge available when needed
- **Con**: Two things to maintain (skill + references)
### Approach B: Single Comprehensive Skill
Everything in one skill file (~1500 lines). Always loaded, always complete.
- **Pro**: Simple, no secondary lookups needed
- **Con**: Heavy context cost every trigger (~3-4k tokens), may crowd out project-specific context
### Approach C: Multiple Topic Skills
Split into `bevy-ecs`, `bevy-events`, `bevy-rendering` etc. Each triggers independently based on what code is being touched.
- **Pro**: Granular — only loads what's relevant
- **Con**: More files, more trigger descriptions to maintain, fragmented knowledge for cross-cutting concerns
### Current Recommendation
**Approach A** — compact always-loaded cheat sheet with reference folder. The skill triggers broadly on any Bevy work, loads the critical renames/gotchas/decision guides (~500 lines), and points to reference docs for deeper dives on specific topics.
## Content Priority (What LLMs Get Wrong Most)
Ranked by frequency of incorrect generation:
1. Event → Message rename and the Event/Message split (0.18)
2. ChildOf replacing Parent (0.16+), children iteration patterns
3. `-> Result` fallible systems (0.18)
4. Coordinate system (+Y up, -Z forward) vs GLTF (+Z forward)
5. Transform propagation timing (PostUpdate, not Update)
6. Resources-as-Components in 0.19
7. Material bind groups — group 3, not group 2
8. State::set() same-state transitions now fire (0.18)
9. Asset cache poisoning via load_with_settings
10. Schedule-dependent spawning breaking plugin expectations
@@ -0,0 +1,94 @@
# Bevy Skill Research: Plugin Architecture
> **Bevy version**: 0.18, with 0.19 notes
> **Date**: 2026-07-07
## Plugin Trait (`bevy::app::Plugin`)
**Required**:
- `fn build(&self, app: &mut App)` — runs immediately on `add_plugins`
**Optional (provided defaults)**:
- `fn ready(&self, _app: &App) -> bool` — default `true`; override for async init
- `fn finish(&self, _app: &mut App)` — runs after all plugins report `ready()`, before first schedule tick; use for setup that depends on other plugins' async work
- `fn cleanup(&self, _app: &mut App)` — runs after `finish()`, before schedule; remove temp resources
- `fn name(&self) -> &str` — defaults to type name; used for uniqueness check
- `fn is_unique(&self) -> bool` — default `true`; set `false` for multi-instance plugins
Trait bounds: `Downcast + Any + Send + Sync`.
Any `fn(&mut App)` auto-implements `Plugin`.
## Plugin Dependencies
No formal "depends-on" declaration. Patterns:
- `app.is_plugin_added::<T>() -> bool` to guard or panic
- Sub-plugins: call `app.add_plugins(ChildPlugin)` inside `build()`. Bevy deduplicates by name (unique plugins panic on double-add)
- `finish()` is the idiomatic place for cross-plugin setup that needs another plugin's resources to exist
## Plugin Groups
`DefaultPlugins` / `MinimalPlugins` implement `PluginGroup`.
Customization via `PluginGroupBuilder` fluent API:
- `.disable::<T>()` — skip a plugin
- `.set(plugin)` — replace a plugin's config
- `.add_before::<Target>(plugin)` / `.add_after::<Target>(plugin)` — ordering
- `.enable::<T>()` — re-enable disabled
```rust
DefaultPlugins.disable::<AudioPlugin>().set(WindowPlugin { ... })
```
## App Builder Key Methods
| Method | Purpose |
|--------|---------|
| `add_plugins(P)` | Accepts plugin, plugin group, or tuple |
| `insert_resource(R)` | Insert (overwrites existing) |
| `init_resource::<R>()` | Insert via `Default`/`FromWorld` if absent |
| `add_systems(schedule, systems)` | Add to a named schedule |
| `configure_sets(schedule, sets)` | Configure system set ordering |
| `register_type::<T>()` | Register in `AppTypeRegistry` (reflection) |
| `is_plugin_added::<T>()` | Check if plugin was already added |
| `register_required_components::<T, R>()` | Component requirements (0.15+) |
## States (`bevy::state`)
Three flavors:
- **FreelyMutableState**: Changed via `NextState<S>` resource
- **SubStates**: Removed from world when parent state isn't in required variant
- **ComputedStates**: Derived automatically via `fn compute(sources) -> Option<Self>`
Registration:
- `app.init_state::<S>()` — register with default initial
- `app.insert_state(value)` — register with explicit initial
Transition schedules: `OnEnter(S)`, `OnExit(S)`, `OnTransition(S)`
Entity scoping: `DespawnOnEnter(S)`, `DespawnOnExit(S)` (replaced `StateScoped` in 0.16+)
Run condition: `in_state(S) -> impl Condition` (in `bevy::state::condition`)
**0.18 change**: `State::set()` now always triggers `OnEnter`/`OnExit` even for same-state transitions. Use `set_if_neq()` for old behavior.
## Schedules (Built-in Labels)
| Phase | Schedules |
|-------|-----------|
| Startup | `PreStartup`, `Startup`, `PostStartup` (run once) |
| Per-frame | `First`, `PreUpdate`, `Update`, `PostUpdate`, `Last` |
| Fixed timestep | `FixedFirst`, `FixedPreUpdate`, `FixedUpdate`, `FixedPostUpdate`, `FixedLast` (inside `FixedMain`, driven by `RunFixedMainLoop`) |
| Other | `Main`, `SpawnScene` (new) |
## Run Conditions (`bevy::ecs::schedule::common_conditions`)
Key functions:
- `in_state(s)` — state matching
- `resource_exists::<R>()`, `resource_added::<R>()`, `resource_changed::<R>()`, `resource_equals(val)`
- `any_with_component::<T>()`, `any_component_removed::<T>()`
- `run_once()`, `not(cond)`
- `on_message::<E>()` (renamed from `on_event` in 0.18)
- `condition_changed(cond)`, `condition_changed_to(cond)`
Combinators: `.and(cond)`, `.or(cond)` on any condition.
@@ -0,0 +1,106 @@
# Bevy Skill Research: ECS Core
> **Bevy version**: 0.18, with 0.19 notes
> **Date**: 2026-07-07
## Components
**Derive**: `#[derive(Component)]` on structs/enums. Requires `Send + Sync + 'static`.
**Storage**:
- Table (default) — fast iteration
- `#[component(storage = "SparseSet")]` — fast insert/remove
**Immutable components** (0.18+): `#[component(immutable)]` — restricts to `&T` only, no `&mut T`.
**Required components**: `#[require(B, C)]` — auto-inserts `B` and `C` when this component is added. Supports constructors: `B(value)`, `C { field: val }`, function calls, `= expr`.
**Hooks**: `on_add`, `on_insert`, `on_replace`, `on_remove`, `on_despawn` — set via `#[component(on_add = fn_name)]` or self-referential `#[component(on_add)]`.
**Clone behavior**: `#[component(clone_behavior = Ignore)]` to skip during entity clone.
**Bundles removed** (0.16+): Use `#[require(...)]` or spawn with tuples instead of `#[derive(Bundle)]`.
## Resources
| Type | Purpose |
|------|---------|
| `Res<T>` | Shared read access |
| `ResMut<T>` | Exclusive write access |
| `Option<Res<T>>` / `Option<ResMut<T>>` | Resources that may not exist |
| `Local<'a, T>` | Per-system private state, persists across calls, default-initialized |
| `NonSend<T>` / `NonSendMut<T>` | For `!Send` resources |
World methods: `init_resource::<R>()` (uses `Default`), `insert_resource(value)`, `remove_resource::<R>()`.
**0.19 change**: Resources implement `Component` and live on abstract entities. `Query<Entity>` now matches resource entities — filter with `Without<IsResource>`. Cannot derive both `Component` and `Resource` on one type.
## Systems
**Standard**: any `fn(params...) { }` where each param implements `SystemParam`.
**Fallible** (0.18+): `fn system(params...) -> Result` — bare `Result` (alias for `Result<(), BevyError>`). Errors are logged, system continues on next tick. `?` operator works with any error convertible to `BevyError`.
**Exclusive**: `fn system(world: &mut World) { }` — full world access, blocks parallelism.
**Registration**: `app.add_systems(Update, (sys_a, sys_b))` or `app.add_systems(Startup, setup)`.
### Valid SystemParam Types
`Query`, `Res`, `ResMut`, `Commands`, `Local`, `MessageReader`, `MessageWriter`, `ParamSet`, `Single`, `Populated`, `SystemName`, `SystemChangeTick`, `RemovedComponents<T>`, `DeferredWorld`, `&World`, `()`, `Option<T>`, `Result<T, SystemParamValidationError>`, `PhantomData<T>`.
## Queries
**Signature**: `Query<D, F>` where `D: QueryData`, `F: QueryFilter` (default `()`).
### Data Parameters
- `&T` — read
- `&mut T` — write
- `Entity` — entity ID
- `Has<T>` — returns bool
- Tuples of the above
### Filters
| Filter | Fires when |
|--------|-----------|
| `With<T>` | Entity has component T |
| `Without<T>` | Entity lacks component T |
| `Added<T>` | First tick after component added |
| `Changed<T>` | First tick after add or mut deref |
| `Spawned` | First tick after entity spawn (new) |
| `Or<(A, B)>` | Either filter matches |
### Key Methods
- `.iter()` / `.iter_mut()` — iterate all matching
- `.get(entity)` / `.get_mut(entity)` — single entity lookup
- `.single()` — panics if not exactly one match
- `.par_iter()` / `.par_iter_mut()` — parallel iteration
### Specialized System Params
- `Single<D, F>` — system param alternative to `query.single()`, panics if not exactly one
- `Populated<D, F>` — system skips execution if query is empty
### Custom Derives
- `#[derive(QueryData)]` — for complex query data
- `#[derive(QueryFilter)]` — for reusable filter combinations
## Commands
| Method | Purpose |
|--------|---------|
| `commands.spawn((CompA, CompB))` | Spawn entity with tuple, returns `EntityCommands` |
| `commands.spawn_empty()` | Empty entity, chain `.insert(bundle)` |
| `commands.spawn_batch(iter)` | Batch spawn from iterator |
| `commands.entity(e).insert(comp)` | Add component to existing entity |
| `commands.entity(e).insert_if_new(comp)` | Add only if absent |
| `commands.entity(e).remove::<T>()` | Remove component |
| `commands.entity(e).despawn()` | Destroy entity (auto-recursive with children) |
| `commands.queue(custom_command)` | Push custom `Command` impl |
## System Ordering
- `(a, b).chain()``a` runs before `b`
- `.before(system_or_set)` / `.after(system_or_set)` — explicit ordering
- `#[derive(SystemSet)]` — define named sets for grouping
- `app.configure_sets(Update, SetA.before(SetB))` — order sets
- `AmbiguousSystemConflictsWarning` warns about unordered systems with overlapping access
@@ -0,0 +1,70 @@
# Bevy Skill Research: Hierarchy & Relationships
> **Bevy version**: 0.18, with 0.19 notes
> **Date**: 2026-07-07
## ChildOf Replaces Parent (Bevy 0.16+)
- `Parent` renamed to **`ChildOf(pub Entity)`** — "entities with ChildOf are children, not parents"
- `Deref` removed; use **`child_of.parent()`** to get the parent Entity
- `ChildOf` implements `Relationship`; `Children` implements `RelationshipTarget` — they auto-sync via hooks
- Adding `ChildOf(parent)` to child auto-inserts `Children` on parent; removing it auto-updates parent
- Despawning a parent **auto-despawns all descendants** (was `despawn_recursive`, now just `despawn()`)
## Spawning Children
```rust
// Insert ChildOf directly
commands.spawn(ChildOf(parent));
// Inline child on parent
commands.entity(parent).with_child((SpriteBundle::default(), ChildOf(parent)));
// Multiple children via closure
commands.spawn_empty().with_children(|spawner: &mut ChildSpawnerCommands| {
spawner.spawn(MyComponent(255));
let parent = spawner.target_entity(); // was parent_entity()
});
// Add existing entities as children
commands.entity(parent).add_child(child);
commands.entity(parent).add_children(&[child1, child2]);
commands.entity(parent).insert_children(index, &[child1, child2]);
```
## Querying Hierarchy
- **`Children`** derefs to `[Entity]`, implements `IntoIterator`
- Correct iteration: **`for child in &children`** or **`children.into_iter()`**
- Wrong: ~~`children.iter().copied()`~~ — unnecessary, `IntoIterator` yields `Entity` directly
- Methods: `len()`, `is_empty()`, `contains()`, `first()`, `last()`, `sort_by()`
- Query extensions (0.16+): `query.related::<ChildOf>(entity)`, `query.relationship_sources::<Children>(entity)`
## Detaching vs Despawning (0.18 Renames)
| Old name (≤0.17) | New name (0.18+) | Effect |
|-------------------|------------------|--------|
| `remove_children` | **`detach_children`** | Detach only, do NOT despawn |
| `clear_children` | **`detach_all_children`** | Detach all, do NOT despawn |
| `remove_child` | **`detach_child`** | Detach one, do NOT despawn |
To actually despawn children: `despawn_related::<Children>()`
## Transform Propagation
- `GlobalTransform` is auto-propagated through `ChildOf` hierarchy
- Child `Transform` is relative to parent; `GlobalTransform` is computed world-space
- Propagation runs in **`PostUpdate`** — reading `GlobalTransform` in `Update` gives stale (last-frame) values
## Common Mistakes
| Mistake | Correct |
|---------|---------|
| `Parent` component | `ChildOf(entity)` |
| `children.iter().copied()` | `children.into_iter()` or `for child in &children` |
| `*child_of` (deref) | `child_of.parent()` |
| `despawn_recursive()` | `despawn()` (auto-recursive now) |
| `parent_entity()` in spawner | `target_entity()` |
| `clear_children()` | `detach_all_children()` (0.18+) |
| `remove_children()` | `detach_children()` (0.18+) |
| Reading GlobalTransform in Update | Read in PostUpdate, or accept one-frame lag |
@@ -0,0 +1,138 @@
# Bevy Skill Research: Events, Messages & Observers
> **Bevy version**: 0.18, with 0.19 notes
> **Date**: 2026-07-07
## The Event/Message Split (0.18 Breaking Change)
In 0.18, the old `Event`/`EventWriter`/`EventReader` system was **split into two distinct systems**:
| Concept | Trait | Writer | Reader | Model |
|---------|-------|--------|--------|-------|
| Messages | `#[derive(Message)]` | `MessageWriter<E>` | `MessageReader<E>` | Pull-based, double-buffered, batch |
| Events | `#[derive(Event)]` | `commands.trigger()` | Observers (`On<E>`) | Push-based, immediate |
```rust
// Messages — for batch processing, decoupled many-to-many
#[derive(Message)]
struct Collision { entity_a: Entity, entity_b: Entity }
// Events — for push-based observer reactions
#[derive(Event)]
struct Speak { message: String }
```
## Messages (Pull-Based, Buffered)
The renamed successor to the old EventWriter/EventReader pattern.
- `MessageWriter::write(msg)` / `write_batch(iter)` / `write_default()`
- `MessageReader::read()` returns iterator, advances cursor; `len()`, `is_empty()`, `clear()`
- Double-buffered: messages survive one `update()` cycle, cleared on the next
- Multiple readers can run concurrently; writers for the same type cannot
```rust
fn send(mut writer: MessageWriter<Collision>) {
writer.write(Collision { entity_a: a, entity_b: b });
}
fn receive(mut reader: MessageReader<Collision>) {
for collision in reader.read() {
// handle
}
}
```
## Events + Observers (Push-Based, Immediate)
**Observer system parameter**: `On<'w, 't, E, B>` where `B: Bundle` filters by component.
- `event()` / `event_mut()` — access event data
- `trigger()` / `trigger_mut()` — access trigger context
- `observer()` — the observer entity
- Implements `Deref<Target=E>` for direct field access
### Registration
```rust
// Global observer
app.add_observer(|on: On<Speak>| {
println!("{}", on.message);
});
// Entity-scoped observer
commands.entity(id).observe(|on: On<Hit>| {
// only fires for this specific entity
});
```
### Triggering
```rust
commands.trigger(Speak { message: "Hello!".into() });
world.trigger_ref(&mut event); // inspect after observers mutate
```
### EntityEvent (Entity-Targeted Events)
```rust
#[derive(EntityEvent)]
struct Hit {
#[event_target]
entity: Entity,
damage: f32,
}
```
Supports `#[entity_event(propagate)]` for automatic hierarchy traversal (event bubbles up through ChildOf chain).
## Lifecycle Events (Component Triggers)
Built-in events fired on component lifecycle changes:
| Event | Fires when | Order |
|-------|-----------|-------|
| `Add` | Component added to entity that lacked it | First |
| `Insert` | Any insert (add or replace) | After `Add` |
| `Replace` | Component removed regardless of replacement | Before `Remove` |
| `Remove` | Component removed from entity | After `Replace` |
| `Despawn` | Entity despawned (per component) | Last |
```rust
world.add_observer(|_: On<Add, Health>| { /* entity just got Health */ });
world.add_observer(|_: On<Remove, Health>| { /* Health was removed */ });
```
## Low-Level Component Hooks
Synchronous callbacks during the ECS operation itself (not scheduled systems). These run inline — not deferred.
```rust
#[derive(Component)]
#[component(on_add = my_hook)]
#[component(on_insert = my_hook)]
struct MyComponent;
fn my_hook(world: DeferredWorld, ctx: HookContext) { /* ... */ }
```
Available: `on_add`, `on_insert`, `on_replace`, `on_remove`, `on_despawn`.
## When to Use What
| Mechanism | Model | Best for |
|-----------|-------|----------|
| `MessageWriter`/`MessageReader` | Pull, scheduled, buffered | Batch processing, decoupled many-to-many communication |
| `Event` + Observers | Push, immediate | React NOW to something (structural changes, gameplay events) |
| Component hooks | Synchronous, inline | Deterministic callback AS PART OF the ECS operation |
| `Changed<T>` / `Added<T>` filters | Pull, per-tick polling | Check mutations each frame in a normal system |
**Rule of thumb**: Use Messages when multiple systems need to process the same data independently. Use Events + Observers when you need immediate reaction to something happening. Use hooks when the response must be atomic with the ECS operation.
## Migration Notes
**0.17 → 0.18**: `EventWriter``MessageWriter`, `EventReader``MessageReader`, `Events``Messages`. `Event` trait is now observer-only. `on_event` run condition → `on_message`.
**0.18 → 0.19**: `EntityComponentsTrigger` gains archetype fields — destructuring requires `..`:
```rust
let EntityComponentsTrigger { components, .. } = e.trigger(); // 0.19
```
@@ -0,0 +1,99 @@
# Bevy Skill Research: Input System
> **Bevy version**: 0.18, with 0.19 notes
> **Date**: 2026-07-07
## Keyboard Input (`Res<ButtonInput<KeyCode>>`)
| Method | Returns | Purpose |
|--------|---------|---------|
| `pressed(KeyCode)` | `bool` | Held this frame |
| `just_pressed(KeyCode)` | `bool` | First frame pressed |
| `just_released(KeyCode)` | `bool` | First frame released |
| `any_pressed(impl IntoIterator)` | `bool` | Any of these held |
| `any_just_pressed(...)` | `bool` | Any of these just pressed |
| `all_pressed(...)` | `bool` | All of these held |
| `get_pressed()` | `impl Iterator` | Iterate all held keys |
| `get_just_pressed()` | `impl Iterator` | Iterate newly pressed |
| `clear_just_pressed(KeyCode)` | `bool` | Consume the event |
Also available: `ButtonInput<Key>` for logical/locale-aware keys.
Updated by `keyboard_input_system` from `KeyboardInput` events.
## Mouse Input
**Buttons**: `Res<ButtonInput<MouseButton>>` — same API as keyboard.
Variants: `Left`, `Right`, `Middle`, `Back`, `Forward`, `Other(u16)`.
**Motion**:
| Type | Kind | Purpose |
|------|------|---------|
| `MouseMotion` | Event | `delta: Vec2` per raw motion event |
| `AccumulatedMouseMotion` | Resource | Total delta this frame, reset each frame |
| `MouseWheel` | Event | Scroll with `MouseScrollUnit` (Line/Pixel) |
| `AccumulatedMouseScroll` | Resource | Total scroll this frame |
**Cursor position**: Read from `Window` component:
```rust
fn cursor(windows: Query<&Window>) {
if let Ok(window) = windows.single() {
if let Some(pos) = window.cursor_position() {
// pos is Vec2 in logical pixels, origin top-left
}
}
}
```
## Gamepad Input
**Buttons**: `Res<ButtonInput<GamepadButton>>` — same pressed/just_pressed API.
**Axes**: `Res<Axis<GamepadAxis>>`:
- `get(GamepadAxis) -> Option<f32>` — range `[-1.0, 1.0]`
- `GamepadButton` mapped range `[0.0, 1.0]` (triggers are analog)
**Events**: `GamepadButtonStateChangedEvent`, `GamepadButtonChangedEvent`, `GamepadAxisChangedEvent`, `GamepadConnectionEvent`.
**Settings**: `GamepadSettings` component for deadzone/threshold configuration.
## Touch Input
- `TouchInput` (Event) — touch event with `TouchPhase` (Began/Moved/Ended)
- `Touches` (Resource) — query active touches, analogous to ButtonInput pattern
- `ForceTouch` — pressure-sensitive data on supported devices
- Requires `touch` feature flag in 0.18
## Action Mapping
No built-in action mapping system in Bevy 0.18. Community crate `leafwing-input-manager` fills this role.
## 0.18 Change
Input sources (`mouse`, `keyboard`, `gamepad`, `touch`, `gestures`) are now **feature-gated**. Enabled by default with `bevy_window`/`bevy_gilrs`, but must be explicit with `default-features = false`.
## Common Patterns
```rust
fn movement(
keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
) {
if keys.just_pressed(KeyCode::Space) { /* fire */ }
if mouse.pressed(MouseButton::Left) { /* hold fire */ }
let mut dir = Vec3::ZERO;
if keys.pressed(KeyCode::KeyW) { dir.z -= 1.0; }
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; }
}
fn read_motion(mut motion: MessageReader<MouseMotion>) {
for ev in motion.read() {
// ev.delta.x, ev.delta.y
}
}
```
Note: `MouseMotion` uses `MessageReader` (not `EventReader`) in 0.18+.
@@ -0,0 +1,96 @@
# Bevy Skill Research: Camera, Rendering & Common Gotchas
> **Bevy version**: 0.18, with 0.19 notes
> **Date**: 2026-07-07
## Camera Setup
**Camera3d** is a marker component with `#[require(...)]` that auto-inserts: `Camera`, `DebandDither`, `CameraRenderGraph`, `Projection`, `Tonemapping`, `ColorGrading`, `Exposure`. Just spawning `Camera3d` gives a working 3D camera.
Coordinate system: right-handed, X-right, Y-up, Z-back (forward = -Z).
**0.18 change**: `RenderTarget` moved from a `Camera` field to a separate required component.
**0.19 change**: `Hdr` moved from `bevy_render` to `bevy_camera`; `screen_space_specular_transmission_*` fields extracted into `ScreenSpaceTransmission` component.
## Camera Ordering / Multiple Cameras
- `Camera.order` (isize) controls render order. Higher values render later (on top)
- Each camera with `is_active: true` renders independently
- `ClearColorConfig` controls whether a camera clears the previous output
- For overlays: use `ClearColorConfig::None` on higher-order cameras
## Camera Gotchas
- Default `far` plane is 1000 units — objects beyond are culled silently
- Forward is **-Z**, not +Z
- Viewport coordinates are in physical pixels, not logical
- GLTF models use +Z as visual forward (opposite Bevy convention)
## Coordinate System
| Axis | Bevy native | GLTF models |
|------|-------------|-------------|
| Forward | -Z (`Transform::forward()`) | +Z (`Transform::back()`) |
| Up | +Y | +Y |
| Right | +X | -X (from camera perspective) |
**GLTF orientation fix**: Use `look_to(-velocity_dir, Vec3::Y)` (negate direction) or read `transform.back()` for the model's visual forward.
## Rendering / Mesh / Material Gotchas
- **Mesh requires normals** for `StandardMaterial`. Missing normals = invisible mesh, no error
- **GLTF scenes** must be spawned with scene labels (`"model.gltf#Scene0"`), not the raw asset handle
- **Back-face culling** is on by default; incorrect vertex winding = invisible faces
- **Visibility hierarchy**: parent entities need visibility components or children are invisible
- **0.18**: Mesh access methods became `try_*` returning `Result<_, MeshAccessError>`. `MaterialPlugin` fields (`prepass_enabled`, `shadows_enabled`) became `Material` trait methods
- **0.19**: GLTF material loading returns `GltfMaterial` (use `#Material0/std` suffix for `StandardMaterial`). `Assets::get_mut` returns `AssetMut<A>` with mutation tracking. `Scene`/`SceneRoot` renamed to `WorldAsset`/`WorldAssetRoot`
## Transform Gotchas
- Bevy: +Y up, -Z forward, right-handed
- `Transform::forward()` returns -Z, `Transform::back()` returns +Z
- Transform propagation runs in **PostUpdate** — reading `GlobalTransform` in `Update` gives stale (last-frame) values
- **0.18**: `GltfPlugin` coordinate conversion restructured — `use_model_forward_direction` replaced by `convert_coordinates` with `rotate_scene_entity` and `rotate_meshes` flags
## Asset Loading Gotchas
- Assets are NOT ready immediately after `asset_server.load()`. Must check `AssetEvent::LoadedWithDependencies` or use `AssetServer::is_loaded_with_dependencies()`
- **0.18**: `LoadContext::path()` returns `AssetPath` not `Path`. Image `reinterpret_*` methods return `Result`
- **0.19**: `AssetPath::resolve()`/`resolve_embed()` take `&AssetPath` not `&str`; string variants renamed to `resolve_str()`/`resolve_embed_str()`
## Lighting Gotcha
**`AmbientLight` split (0.18)**:
- `GlobalAmbientLight` — resource, affects everything
- `AmbientLight` — camera component, per-camera ambient
## General LLM Traps
These are patterns where LLMs most commonly produce incorrect code:
| Trap | Wrong | Correct |
|------|-------|---------|
| Parent component | `Parent` | `ChildOf(entity)` (0.16+) |
| Child iteration | `children.iter().copied()` | `children.into_iter()` |
| Event writer | `EventWriter<T>` | `MessageWriter<T>` (0.18+) |
| Event reader | `EventReader<T>` | `MessageReader<T>` (0.18+) |
| Run condition | `on_event::<T>()` | `on_message::<T>()` (0.18+) |
| Despawn recursive | `despawn_recursive()` | `despawn()` (auto-recursive 0.16+) |
| Bundles | `#[derive(Bundle)]` | Tuples + `#[require(...)]` (0.16+) |
| WinitPlugin | `WinitPlugin::<WakeUp>` | `WinitPlugin` (not generic 0.18+) |
| GltfPlugin | `init_asset::<Gltf>()` | `GltfPlugin::default()` (0.18+) |
| Material bind group | `@group(2)` in shader | `@group(3)` (group 2 is mesh in 0.18) |
| System error handling | Return nothing | `-> Result` (0.18+) |
| GlobalTransform timing | Read in Update | Read in PostUpdate or accept lag |
| Same-state transition | `state.set(current)` no-ops | Triggers OnExit/OnEnter in 0.18; use `set_if_neq()` |
### 0.19-Specific Traps
| Trap | Detail |
|------|--------|
| Resources as components | `Query<Entity>` now matches resource entities — filter with `Without<IsResource>` |
| Scene renamed | `Scene`/`SceneRoot``WorldAsset`/`WorldAssetRoot` |
| Ref clone | `Ref<T>.clone()` returns `Ref<T>`, not inner value. Use `ref.deref().clone()` |
| Assets::get_mut | Returns `AssetMut<A>` with mutation tracking, not `&mut A` directly |
| GLTF materials | Returns `GltfMaterial`, use `#Material0/std` suffix for `StandardMaterial` |
@@ -0,0 +1,252 @@
# Bevy Skill Research: Lessons from Production
> **Date**: 2026-07-07
> **Source**: Extracted from a production Bevy 0.18 multiplayer game's errata log (~1500 lines of battle-tested gotchas). All examples below are neutralized — project-specific types replaced with generic equivalents.
These are patterns that burned real development time and are likely to recur in any Bevy project. They go beyond "read the docs" — most involve subtle interactions between Bevy subsystems, or behavior that contradicts reasonable assumptions.
---
## 1. Asset Cache Poisoning via `load_with_settings`
**The trap**: `AssetServer` deduplicates by path. If you `load_with_settings` a GLTF with `load_meshes = RenderAssetUsages::empty()` (e.g. for hierarchy extraction), then later `load()` the same path for rendering, Bevy returns the cached version — which has empty meshes. Result: invisible models, zero errors.
```rust
// POISONED: first load strips mesh data
let handle = asset_server.load_with_settings("model.glb", |s: &mut GltfLoaderSettings| {
s.load_meshes = RenderAssetUsages::empty();
});
// STALE: reuses cached empty-mesh version, silently invisible
let scene = asset_server.load("model.glb#Scene0");
```
**Fix**: Never use `load_with_settings` with stripped mesh/material data in a process that also renders those assets. Lightweight loads are only safe in headless servers or tools that never render the same asset.
**Why LLMs get this wrong**: The cache deduplication is invisible — no errors, no warnings, and it depends on load ordering.
---
## 2. Render-Dependent Resources Must Be Optional in Headless Contexts
**The trap**: Systems using `ResMut<Assets<MyCustomMaterial>>` will panic in headless mode (tests, dedicated servers) because the `Assets<T>` resource for custom render types doesn't exist without `RenderPlugin`.
```rust
// PANICS in headless: Assets<MyMaterial> doesn't exist
fn my_system(mut materials: ResMut<Assets<MyMaterial>>) { ... }
// CORRECT: gracefully skip render logic
fn my_system(materials: Option<ResMut<Assets<MyMaterial>>>) {
let Some(ref mut materials) = materials else { return; };
// ...
}
```
**Rule**: Any system that may run in both rendered and headless contexts must wrap render-specific resources in `Option<>`.
---
## 3. `RelativeCursorPosition.normalized` Is Center-Origin
**The trap**: `RelativeCursorPosition.normalized` returns `(0, 0)` at the **center** of the UI node, not the top-left. Range is `(-0.5, -0.5)` to `(0.5, 0.5)`.
```rust
// WRONG assumption: UV origin at top-left
let uv = rel_cursor.normalized.unwrap(); // (0,0) = center!
// CORRECT: convert to [0,1] UV
let uv = rel_cursor.normalized.unwrap() + Vec2::splat(0.5);
```
---
## 4. Raw `ButtonInput<MouseButton>` Fires Through UI
**The trap**: `Res<ButtonInput<MouseButton>>` comes from winit and fires for ALL clicks, including clicks consumed by UI panels. Game world click handlers that read it directly will fire when the user clicks UI buttons.
```rust
// WRONG: fires when clicking UI panels too
fn world_click(mouse: Res<ButtonInput<MouseButton>>) {
if mouse.just_pressed(MouseButton::Left) { /* fires through UI! */ }
}
```
**Fix**: Use a UI-aware click event system. If using bevy_ui, check that the click wasn't consumed by a UI node (e.g. via a full-screen background `Interaction` node at low z-index that only receives clicks that pass through all UI).
---
## 5. `AmbientLight` Split (0.18)
`AmbientLight` was split into two types:
- `GlobalAmbientLight` — a **resource**, affects all cameras
- `AmbientLight` — a **component** on camera entities, per-camera ambient
LLMs trained on pre-0.18 code will use the old single `AmbientLight` type incorrectly.
---
## 6. Custom Relationships: Don't Implement Collection Logic Manually
**The trap**: When creating custom `#[relationship]` / `#[relationship_target]` pairs, implementing manual add/remove logic for the target's `Vec<Entity>` is redundant — Bevy's relationship hooks handle synchronization automatically.
```rust
// WRONG: manual push in an observer
fn on_add_child(trigger: On<Add, MyChildOf>, mut parent_q: Query<&mut MyParent>) {
parent_q.get_mut(trigger.parent).unwrap().children.push(trigger.entity());
}
// CORRECT: the relationship macro handles this automatically
// Just derive the relationship — no manual sync needed
```
---
## 7. `State::set()` Always Triggers Transitions in 0.18
**The trap**: In Bevy 0.18, calling `state.set(CurrentState)` (setting to the same value) now triggers `OnExit` + `OnEnter` for that state. Previously this was a no-op.
```rust
// 0.18: this triggers OnExit(Playing) + OnEnter(Playing) even if already Playing!
next_state.set(GameState::Playing);
// If you want the old no-op behavior:
next_state.set_if_neq(GameState::Playing);
```
**Impact**: Systems in `OnEnter` that do one-time setup (spawn cameras, load levels) will re-run unexpectedly if anything calls `set()` with the current state.
---
## 8. GlobalTransform Is Stale in `Update`
**The trap**: Transform propagation runs in `PostUpdate`. Any system in `Update` that reads `GlobalTransform` gets last frame's value.
```rust
// STALE: GlobalTransform hasn't been propagated yet this frame
fn targeting(q: Query<&GlobalTransform>) {
let pos = q.get(target).unwrap().translation(); // one frame behind
}
```
**Options**:
1. Move the system to `PostUpdate` (after `TransformSystem::TransformPropagate`)
2. Accept the one-frame lag (often fine for gameplay)
3. Compute global position manually from the `Transform` chain (rare, fragile)
---
## 9. Spawning from Non-Standard Schedules Can Break Plugin Expectations
**The trap**: Some plugins (particle systems, audio, etc.) expect entities to be spawned during the standard `Update`/`Startup` schedule cycle. Spawning entities from exclusive schedules (like `EguiPrimaryContextPass`) can result in entities that exist in queries but are never picked up by the plugin's internal systems.
**Symptoms**: Entity has all expected components, no errors logged, but the plugin ignores it completely.
**Fix**: Use a flag/resource pattern — set a flag in the non-standard schedule, spawn the entity in `Update`:
```rust
fn ui_system(mut state: ResMut<MyState>) {
if ui.button("Spawn").clicked() {
state.spawn_requested = true;
}
}
fn handle_spawns(mut state: ResMut<MyState>, mut commands: Commands) {
if state.spawn_requested {
state.spawn_requested = false;
commands.spawn(MyPluginBundle { ... });
}
}
```
---
## 10. Mesh Without Normals Is Silently Invisible
When creating meshes programmatically, forgetting to call `with_computed_normals()` (or manually inserting normals) results in a mesh that spawns successfully but renders as completely invisible. No error, no warning.
```rust
// INVISIBLE: no normals
let mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default())
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions);
// VISIBLE: normals computed
let mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default())
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
.with_computed_flat_normals();
```
---
## 11. Custom Material Shader Bind Groups
In Bevy 0.18, material bind groups are at `@group(3)`, NOT `@group(2)`:
| Group | Contents |
|-------|----------|
| 0 | View bindings |
| 1 | Globals/lights |
| 2 | Mesh bindings (storage buffer) |
| 3 | Material bindings (`AsBindGroup`) |
Using `@group(2)` for material uniforms produces a confusing error: `Storage class Storage doesn't match the shader Uniform` — because group 2 is the mesh storage buffer.
---
## 12. `ShaderRef` Import Path Moved
```rust
// WRONG (pre-0.18):
use bevy::render::render_resource::ShaderRef;
// CORRECT (0.18+):
use bevy::shader::ShaderRef;
```
---
## 13. Physics Plugin Gotchas (Avian3D / Rapier Pattern)
### `CollidingEntities` Must Be Explicitly Added
In Avian3D 0.5, `CollidingEntities` is NOT auto-inserted on entities with a `Collider`. You must explicitly add `CollidingEntities::default()`. Without it, collision queries return nothing — the physics simulation still runs internally, but your queries never see the results.
### `RigidBody` Removal Can Panic with Collider Children
Removing `RigidBody` from a parent entity that has `Collider` children can trigger stale index panics in the physics engine's internal tree. **Workaround**: Despawn collider children before removing `RigidBody` from the parent.
---
## 14. Visibility Hierarchy Requires Components on Parents
If a parent entity lacks visibility components (`Visibility`, `InheritedVisibility`), child entities are invisible regardless of their own visibility settings. This is particularly sneaky when spawning scene hierarchies where intermediate nodes might lack these components.
---
## 15. Server/Client Architecture Traps (Multiplayer-Specific but Broadly Applicable)
These apply to any Bevy project with server-authoritative networking, regardless of networking library:
### Server Entities Must Not Carry Visual Components
In a shared-world architecture (server + client in same process), server entities should be physics/logic only. Visual components (`SceneRoot`, `Mesh3d`, `Sprite`) belong exclusively on client-spawned entities. Mixing them creates:
- Inconsistent visuals between host and remote clients
- Dual maintenance burden on spawn code
- Visual systems accidentally processing server entities
### Server-Only Systems Still Match Client Entities in Shared World
A `run_if(is_server)` gate controls WHETHER a system runs, not WHICH entities it queries. In a shared-world host, server systems will match both server entities and client replica entities unless queries explicitly filter out client markers.
### Shared-World Cooldowns and Timers Race
When server and client systems share a World, they share component instances. A client system resetting a timer will affect the server system's read of that same timer in the same frame. Use `Local<T>` for server-side rate limiting that must be independent of client state.
---
## Summary: Top Gotcha Categories
1. **Silent failures** (invisible meshes, empty queries, missing components) — Bevy often succeeds silently when something is misconfigured
2. **Schedule ordering** (stale GlobalTransform, spawn timing, command flush order)
3. **Cache/dedup surprises** (asset cache poisoning, state transition re-triggering)
4. **API renames that compile but misbehave** (EventWriter→MessageWriter, Parent→ChildOf, group(2)→group(3))
5. **Shared-world entity leakage** (server systems matching client entities in host mode)
@@ -0,0 +1,130 @@
# Bevy Skill Research: Landscape Survey & Design Sketch
> **Date**: 2026-07-07
> **Purpose**: Capture the initial motivation, survey of existing skills, and design approaches before committing to a specific skill architecture.
## Why This Research Exists
The #1 problem with LLM-assisted Bevy development — confirmed by blog posts, community discussions, and this project's own errata — is that **Claude confidently uses outdated Bevy APIs**. Bevy's rapid release cycle (0.16→0.17→0.18→0.19 in ~18 months) introduces fundamental renames and semantic shifts that make pre-training knowledge actively harmful. A dedicated skill would serve as grounded API truth, loaded into context whenever Bevy code is being written.
### The Problem in Practice
From [Toby Hede's blog post](https://tobyhede.com/blog/hard-mode/) (building a Bevy 0.17 space sim): Claude was "nearly always wrong" on unstable APIs without grounding. From [Chier Hu's blog post](https://chierfandev.com/claude-code-for-game-development/): Bevy is a "good conceptual fit" for LLMs due to ECS patterns, but "punished by API instability."
This project's own errata file (1500+ lines) documents dozens of cases where incorrect Bevy/Lightyear patterns were confidently generated, costing hours of debugging per incident.
## Landscape Survey: Existing Bevy Skills
### bfollington/terma — Best Candidate, Outdated
- **Repo**: https://github.com/bfollington/terma
- **Marketplace**: https://crossaitools.com/skills/bfollington/terma/bevy
- **Install**: `npx skills add https://github.com/bfollington/terma --skill bevy`
- **Stats**: 154 installs, 46 stars
- **Coverage**: ECS patterns, component design, system ordering, plugin structure, query optimization
- **Version**: Only covers up to **Bevy 0.17**
- **Verdict**: Would actively mislead on 0.18+ APIs (EventWriter vs MessageWriter, Parent vs ChildOf, WinitPlugin generics, etc.)
### laurigates/claude-plugins — Generic, Outdated
- **Repo**: https://github.com/laurigates/claude-plugins/tree/main/bevy-plugin
- **Marketplace**: https://smithery.ai/skills/laurigates/bevy-game-engine
- **Coverage**: References outdated dependencies (bevy_rapier2d/3d instead of Avian), no version pinning
- **Verdict**: Too generic and outdated to be useful
### Official Bevy — No Claude Integration
- https://github.com/bevyengine/bevy has no `.claude` directory, no `CLAUDE.md`, no `AGENTS.md`
- Bevy has a **no-LLM-contributions policy** for upstream PRs
- Bevy's own docs are authoritative but not formatted for skill consumption
### Curated Lists — No Bevy Skills
- **awesome-claude-skills** (https://github.com/travisvn/awesome-claude-skills) — no Bevy entry
- **awesome-claude-code-and-skills** (https://github.com/GetBindu/awesome-claude-code-and-skills) — no Bevy (lists `htdt/godogen` for Godot/Bevy/Babylon.js, but that's an autonomous agent, not a skill)
- **claude-skill-registry** (https://github.com/majiayu000/claude-skill-registry) — no Bevy skill
### Related Tools (Not Coding Skills)
- **bevy_brp_mcp** (https://github.com/natepiano/bevy_brp) — MCP server for controlling running Bevy apps via Bevy Remote Protocol; runtime tooling, not a coding skill
- **bevy_debugger_mcp** (https://github.com/Ladvien/bevy_debugger_mcp) — natural-language game-state debugging via BRP
### Bottom Line
Nothing publicly available is adequate for Bevy 0.18+. This project's own `CLAUDE.md` / errata is already significantly more detailed and version-accurate than anything published. A new skill would need to be built from scratch.
---
## Design Sketch: What a Proper Bevy Skill Could Look Like
A skill worth building would serve as **grounded API truth** that prevents the #1 problem: Claude confidently using outdated Bevy APIs.
### Core Content Layers
1. **Current API reference** (0.18/0.19) — the breaking changes, correct patterns, what moved where
2. **Migration chain** (0.16 → 0.17 → 0.18 → 0.19) — so it can reason about upgrade context
3. **Pattern library** — ECS idioms, plugin composition, system ordering, queries, relationships
4. **Common traps** — the things Claude gets wrong most (ChildOf vs Parent, WinitPlugin generics, bind groups, Message vs Event, etc.)
5. **Production lessons** — real gotchas from shipping code that go beyond API docs (asset cache poisoning, schedule timing, silent failures)
### Key Design Decisions
#### Scope: Generic Bevy vs Bevy+Ecosystem?
| Approach | Pros | Cons |
|----------|------|------|
| **Generic Bevy only** | Broadly useful, smaller context cost, easier to maintain | Misses ecosystem gotchas (physics, particles, networking) |
| **Bevy + ecosystem** (Avian, Hanabi, egui) | More complete, catches cross-plugin traps | Larger, harder to maintain, ecosystem versions diverge |
| **Bevy core + separate ecosystem skills** | Modular, each triggers independently | More skill files, more trigger logic to maintain |
**Recommendation**: Start with generic Bevy core. Ecosystem skills (Avian, Lightyear, etc.) can be separate and project-specific.
#### Update Strategy
| Approach | Pros | Cons |
|----------|------|------|
| **Hand-curated** from migration guides | Precise, tested, trustworthy | Manual effort each release |
| **Scrape/embed migration guides** | Automated, always current | Raw guides are verbose, may include irrelevant detail |
| **Hybrid**: hand-curated core + reference docs folder | Best of both — compact skill, deep docs on demand | Two things to maintain |
**Recommendation**: Hybrid. The skill itself is a compact cheat sheet (~500 lines). Deeper reference docs live in a folder the skill can point to.
#### Size and Context Budget
A skill is loaded into context every time it triggers. The research produced ~800-1000 lines of reference across 7 reports. This needs compression.
| Approach | Context cost | Coverage |
|----------|-------------|----------|
| **Single file, everything** | ~2-3k tokens | Complete but heavy per-trigger |
| **Compact skill + reference folder** | ~500-800 tokens always, more on demand | Tiered — critical stuff always, detail when needed |
| **Multiple skills** (bevy-ecs, bevy-events, bevy-rendering) | ~300-500 tokens each, only relevant one loads | Granular but fragmented knowledge |
**Recommendation**: Single skill with compact always-loaded content (gotchas, renames, decision guides) plus `references/bevy/` folder for deeper dives. The skill triggers broadly on any Bevy code work, but only the critical "don't get this wrong" content eats context every time.
### Content Priority: What LLMs Get Wrong Most
Ranked by frequency of incorrect generation (from this project's errata and community reports):
1. **Event → Message rename** and the Event/Message split (0.18) — highest confusion rate
2. **ChildOf replacing Parent** (0.16+), children iteration patterns
3. **`-> Result` fallible systems** (0.18) — LLMs still generate infallible signatures
4. **Coordinate system** (+Y up, -Z forward) vs GLTF (+Z forward) — directional code wrong ~50% of the time
5. **Transform propagation timing** (PostUpdate, not Update) — stale reads
6. **Resources-as-Components** in 0.19 — broad query contamination
7. **Material bind groups** — group 3, not group 2
8. **State::set() behavior change** — same-state transitions now fire
9. **Asset cache dedup** — load_with_settings poisoning
10. **Schedule-dependent spawning** — entities invisible to certain plugins
### Trigger Design
The skill should trigger when:
- Any `.rs` file is being edited that imports from `bevy::*`
- User mentions Bevy, ECS, components, systems, plugins, queries
- Code involves entity spawning, hierarchy, events/messages, input handling
- Camera, rendering, material, or shader code is being written
It should NOT trigger for:
- Pure Rust code with no Bevy involvement
- Other game engines (Godot, Unity)
- Non-game Rust projects