diff --git a/docs/workpad/research/bevy-skill-research/00-index.md b/docs/workpad/research/bevy-skill-research/00-index.md new file mode 100644 index 0000000..912c478 --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/00-index.md @@ -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 01–06) — 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 01–06 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 diff --git a/docs/workpad/research/bevy-skill-research/01-plugins.md b/docs/workpad/research/bevy-skill-research/01-plugins.md new file mode 100644 index 0000000..62b4573 --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/01-plugins.md @@ -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::() -> 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::()` — skip a plugin +- `.set(plugin)` — replace a plugin's config +- `.add_before::(plugin)` / `.add_after::(plugin)` — ordering +- `.enable::()` — re-enable disabled + +```rust +DefaultPlugins.disable::().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::()` | 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::()` | Register in `AppTypeRegistry` (reflection) | +| `is_plugin_added::()` | Check if plugin was already added | +| `register_required_components::()` | Component requirements (0.15+) | + +## States (`bevy::state`) + +Three flavors: +- **FreelyMutableState**: Changed via `NextState` resource +- **SubStates**: Removed from world when parent state isn't in required variant +- **ComputedStates**: Derived automatically via `fn compute(sources) -> Option` + +Registration: +- `app.init_state::()` — 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::()`, `resource_added::()`, `resource_changed::()`, `resource_equals(val)` +- `any_with_component::()`, `any_component_removed::()` +- `run_once()`, `not(cond)` +- `on_message::()` (renamed from `on_event` in 0.18) +- `condition_changed(cond)`, `condition_changed_to(cond)` + +Combinators: `.and(cond)`, `.or(cond)` on any condition. diff --git a/docs/workpad/research/bevy-skill-research/02-ecs-core.md b/docs/workpad/research/bevy-skill-research/02-ecs-core.md new file mode 100644 index 0000000..98e773a --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/02-ecs-core.md @@ -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` | Shared read access | +| `ResMut` | Exclusive write access | +| `Option>` / `Option>` | Resources that may not exist | +| `Local<'a, T>` | Per-system private state, persists across calls, default-initialized | +| `NonSend` / `NonSendMut` | For `!Send` resources | + +World methods: `init_resource::()` (uses `Default`), `insert_resource(value)`, `remove_resource::()`. + +**0.19 change**: Resources implement `Component` and live on abstract entities. `Query` now matches resource entities — filter with `Without`. 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`, `DeferredWorld`, `&World`, `()`, `Option`, `Result`, `PhantomData`. + +## Queries + +**Signature**: `Query` where `D: QueryData`, `F: QueryFilter` (default `()`). + +### Data Parameters +- `&T` — read +- `&mut T` — write +- `Entity` — entity ID +- `Has` — returns bool +- Tuples of the above + +### Filters +| Filter | Fires when | +|--------|-----------| +| `With` | Entity has component T | +| `Without` | Entity lacks component T | +| `Added` | First tick after component added | +| `Changed` | 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` — system param alternative to `query.single()`, panics if not exactly one +- `Populated` — 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::()` | 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 diff --git a/docs/workpad/research/bevy-skill-research/03-hierarchy.md b/docs/workpad/research/bevy-skill-research/03-hierarchy.md new file mode 100644 index 0000000..c5cd78e --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/03-hierarchy.md @@ -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::(entity)`, `query.relationship_sources::(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::()` + +## 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 | diff --git a/docs/workpad/research/bevy-skill-research/04-events-observers.md b/docs/workpad/research/bevy-skill-research/04-events-observers.md new file mode 100644 index 0000000..22f9455 --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/04-events-observers.md @@ -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` | `MessageReader` | Pull-based, double-buffered, batch | +| Events | `#[derive(Event)]` | `commands.trigger()` | Observers (`On`) | 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) { + writer.write(Collision { entity_a: a, entity_b: b }); +} + +fn receive(mut reader: MessageReader) { + 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` for direct field access + +### Registration + +```rust +// Global observer +app.add_observer(|on: On| { + println!("{}", on.message); +}); + +// Entity-scoped observer +commands.entity(id).observe(|on: On| { + // 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| { /* entity just got Health */ }); +world.add_observer(|_: On| { /* 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` / `Added` 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 +``` diff --git a/docs/workpad/research/bevy-skill-research/05-input.md b/docs/workpad/research/bevy-skill-research/05-input.md new file mode 100644 index 0000000..40c0047 --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/05-input.md @@ -0,0 +1,99 @@ +# Bevy Skill Research: Input System + +> **Bevy version**: 0.18, with 0.19 notes +> **Date**: 2026-07-07 + +## Keyboard Input (`Res>`) + +| 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` for logical/locale-aware keys. + +Updated by `keyboard_input_system` from `KeyboardInput` events. + +## Mouse Input + +**Buttons**: `Res>` — 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>` — same pressed/just_pressed API. + +**Axes**: `Res>`: +- `get(GamepadAxis) -> Option` — 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>, + mouse: Res>, +) { + 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) { + for ev in motion.read() { + // ev.delta.x, ev.delta.y + } +} +``` + +Note: `MouseMotion` uses `MessageReader` (not `EventReader`) in 0.18+. diff --git a/docs/workpad/research/bevy-skill-research/06-camera-gotchas.md b/docs/workpad/research/bevy-skill-research/06-camera-gotchas.md new file mode 100644 index 0000000..c7d4633 --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/06-camera-gotchas.md @@ -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` 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` | `MessageWriter` (0.18+) | +| Event reader | `EventReader` | `MessageReader` (0.18+) | +| Run condition | `on_event::()` | `on_message::()` (0.18+) | +| Despawn recursive | `despawn_recursive()` | `despawn()` (auto-recursive 0.16+) | +| Bundles | `#[derive(Bundle)]` | Tuples + `#[require(...)]` (0.16+) | +| WinitPlugin | `WinitPlugin::` | `WinitPlugin` (not generic 0.18+) | +| GltfPlugin | `init_asset::()` | `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` now matches resource entities — filter with `Without` | +| Scene renamed | `Scene`/`SceneRoot` → `WorldAsset`/`WorldAssetRoot` | +| Ref clone | `Ref.clone()` returns `Ref`, not inner value. Use `ref.deref().clone()` | +| Assets::get_mut | Returns `AssetMut` with mutation tracking, not `&mut A` directly | +| GLTF materials | Returns `GltfMaterial`, use `#Material0/std` suffix for `StandardMaterial` | diff --git a/docs/workpad/research/bevy-skill-research/07-lessons-from-production.md b/docs/workpad/research/bevy-skill-research/07-lessons-from-production.md new file mode 100644 index 0000000..36d5b49 --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/07-lessons-from-production.md @@ -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>` will panic in headless mode (tests, dedicated servers) because the `Assets` resource for custom render types doesn't exist without `RenderPlugin`. + +```rust +// PANICS in headless: Assets doesn't exist +fn my_system(mut materials: ResMut>) { ... } + +// CORRECT: gracefully skip render logic +fn my_system(materials: Option>>) { + 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` Fires Through UI + +**The trap**: `Res>` 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>) { + 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` is redundant — Bevy's relationship hooks handle synchronization automatically. + +```rust +// WRONG: manual push in an observer +fn on_add_child(trigger: On, 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) { + if ui.button("Spawn").clicked() { + state.spawn_requested = true; + } +} + +fn handle_spawns(mut state: ResMut, 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` 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) diff --git a/docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md b/docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md new file mode 100644 index 0000000..2ffa5a0 --- /dev/null +++ b/docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md @@ -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 diff --git a/plugins/bevy/CLAUDE.md b/plugins/bevy/CLAUDE.md new file mode 100644 index 0000000..1112425 --- /dev/null +++ b/plugins/bevy/CLAUDE.md @@ -0,0 +1 @@ +Bevy game engine skills. The `bevy` skill is the primary implementation helper; `bevy-upgrade` handles version migrations and is invoked by `bevy` when upgrade work is detected. diff --git a/plugins/bevy/README.md b/plugins/bevy/README.md new file mode 100644 index 0000000..5ccad29 --- /dev/null +++ b/plugins/bevy/README.md @@ -0,0 +1,49 @@ +# bevy + +Skills for developing with the [Bevy](https://bevyengine.org/) game engine in Rust. + +Bevy's rapid release cycle (0.16 through 0.19 in ~18 months) introduces fundamental API renames and semantic shifts between versions. LLMs confidently generate code against outdated patterns — `Parent` instead of `ChildOf`, `Event` instead of `Message`, `use_state` idioms that no longer exist. These skills exist to correct that. + +## Skills + +- **bevy-0.19** — Implementation helper for writing correct, idiomatic Bevy 0.19 code. Covers plugins, ECS, hierarchy, events/observers, input, camera, and production gotchas. Triggers on Bevy 0.19 Rust code. Contains a compact always-loaded cheat sheet plus a `references/` folder for deeper topic dives. +- **bevy-upgrade** — Version migration guide for upgrading between Bevy releases. Covers the breaking-change chain (0.16 through 0.19+) with rename tables, semantic shifts, and step-by-step migration patterns. Invoked by the `bevy` skill when an upgrade task is detected, or directly when the user asks to migrate. + +## Research + +The research that informed this plugin's design lives in [`docs/workpad/research/bevy-skill-research/`](../../docs/workpad/research/bevy-skill-research/00-index.md). Key reports: + +| Report | Topic | +|--------|-------| +| [01-plugins](../../docs/workpad/research/bevy-skill-research/01-plugins.md) | Plugin trait lifecycle, dependencies, states, schedules | +| [02-ecs-core](../../docs/workpad/research/bevy-skill-research/02-ecs-core.md) | Components, resources, systems, queries, commands | +| [03-hierarchy](../../docs/workpad/research/bevy-skill-research/03-hierarchy.md) | ChildOf relationships, spawning/querying children | +| [04-events-observers](../../docs/workpad/research/bevy-skill-research/04-events-observers.md) | Message/Event split (0.18), observers, lifecycle triggers | +| [05-input](../../docs/workpad/research/bevy-skill-research/05-input.md) | Keyboard, mouse, gamepad, touch input patterns | +| [06-camera-gotchas](../../docs/workpad/research/bevy-skill-research/06-camera-gotchas.md) | Camera setup, rendering, coordinates | +| [07-lessons-from-production](../../docs/workpad/research/bevy-skill-research/07-lessons-from-production.md) | 15 gotchas from a production Bevy 0.18 multiplayer game | +| [08-landscape-and-design-sketch](../../docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md) | Ecosystem survey, architecture options | + +## Architecture + +This plugin follows **Approach A** from the design research: a compact always-loaded skill (~500 lines) for the critical patterns agents get wrong, with a `references/` folder for deeper topic material. Skills are versioned by Bevy minor version (like `dioxus-0.7`). The `bevy-upgrade` skill is a companion that handles version migration specifically. + +## Install + +``` +/plugin marketplace add git@git.g4b.org:dirigence/reliquary.git +/plugin install bevy@reliquary +``` + +Or in `.claude/settings.json`: + +```json +{ + "extraKnownMarketplaces": { + "reliquary": { + "source": { "source": "git", "url": "git@git.g4b.org:dirigence/reliquary.git" } + } + }, + "enabledPlugins": { "bevy@reliquary": true } +} +``` diff --git a/plugins/bevy/skills/bevy-0.19/SKILL.md b/plugins/bevy/skills/bevy-0.19/SKILL.md new file mode 100644 index 0000000..99a32ef --- /dev/null +++ b/plugins/bevy/skills/bevy-0.19/SKILL.md @@ -0,0 +1,38 @@ +--- +name: bevy-0.19 +description: "TODO: Use the skill-creator skill to write this. See scaffolding goals below." +--- + +# Bevy 0.19 — Implementation Helper + +> **This skill is a scaffold.** Use `/skill-creator` to create the full skill content from the research material. + +## Scaffolding Goals + +1. **Use the `skill-creator:skill-creator` skill** to create the full SKILL.md content +2. Feed it the research reports from [`docs/workpad/research/bevy-skill-research/`](../../../../docs/workpad/research/bevy-skill-research/00-index.md) +3. Follow **Approach A** from the [design sketch](../../../../docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md): compact always-loaded cheat sheet (~500 lines) plus `references/` folder for deep dives +4. The skill should trigger on Bevy 0.19 Rust code (`bevy = "0.19"` in Cargo.toml, `use bevy::prelude::*`, `App::new()`, `#[derive(Component)]`, etc.) +5. Core content priorities (ranked by LLM error frequency): + - Event/Message rename and split (0.18) + - ChildOf replacing Parent (0.16+) + - Fallible systems with `-> Result` (0.18) + - Coordinate system gotchas + - Transform propagation timing + - Resources-as-Components (0.19) +6. Populate `references/` with per-topic deep-dive files derived from research reports 01-07 +7. The skill should know about `bevy-upgrade` and invoke it when upgrade/migration work is detected + +## Reference Material + +The research that feeds this skill lives in the workpad: + +- [00-index.md](../../../../docs/workpad/research/bevy-skill-research/00-index.md) — Research overview and design rationale +- [01-plugins.md](../../../../docs/workpad/research/bevy-skill-research/01-plugins.md) — Plugin lifecycle +- [02-ecs-core.md](../../../../docs/workpad/research/bevy-skill-research/02-ecs-core.md) — ECS fundamentals +- [03-hierarchy.md](../../../../docs/workpad/research/bevy-skill-research/03-hierarchy.md) — Hierarchy and ChildOf +- [04-events-observers.md](../../../../docs/workpad/research/bevy-skill-research/04-events-observers.md) — Events, messages, observers +- [05-input.md](../../../../docs/workpad/research/bevy-skill-research/05-input.md) — Input handling +- [06-camera-gotchas.md](../../../../docs/workpad/research/bevy-skill-research/06-camera-gotchas.md) — Camera and rendering +- [07-lessons-from-production.md](../../../../docs/workpad/research/bevy-skill-research/07-lessons-from-production.md) — Production gotchas +- [08-landscape-and-design-sketch.md](../../../../docs/workpad/research/bevy-skill-research/08-landscape-and-design-sketch.md) — Architecture options diff --git a/plugins/bevy/skills/bevy-upgrade/SKILL.md b/plugins/bevy/skills/bevy-upgrade/SKILL.md new file mode 100644 index 0000000..e0f20bc --- /dev/null +++ b/plugins/bevy/skills/bevy-upgrade/SKILL.md @@ -0,0 +1,33 @@ +--- +name: bevy-upgrade +description: "TODO: Use the skill-creator skill to write this. See scaffolding goals below." +--- + +# Bevy Upgrade — Version Migration Guide + +> **This skill is a scaffold.** Use `/skill-creator` to create the full skill content from the research material. + +## Scaffolding Goals + +1. **Use the `skill-creator:skill-creator` skill** to create the full SKILL.md content +2. Feed it the version-change annotations from research reports 01-06 in [`docs/workpad/research/bevy-skill-research/`](../../../../docs/workpad/research/bevy-skill-research/00-index.md) +3. The skill should trigger when the user asks to upgrade/migrate Bevy versions, or when the `bevy` skill detects upgrade work +4. Core content: + - Breaking-change chain from 0.16 through 0.19+ + - Rename tables (old name -> new name, which version) + - Semantic shifts (behavior changes, not just renames) + - Step-by-step migration patterns for each version bump + - Common upgrade pitfalls and their fixes +5. Populate `references/` with per-version-transition files (e.g., `0.17-to-0.18.md`, `0.18-to-0.19.md`) +6. The skill should be invocable both directly and from the `bevy` skill + +## Reference Material + +Same research as the `bevy` skill — version-change annotations are woven into each report: + +- [01-plugins.md](../../../../docs/workpad/research/bevy-skill-research/01-plugins.md) — Plugin API changes across versions +- [02-ecs-core.md](../../../../docs/workpad/research/bevy-skill-research/02-ecs-core.md) — ECS API renames and new patterns +- [03-hierarchy.md](../../../../docs/workpad/research/bevy-skill-research/03-hierarchy.md) — Parent->ChildOf transition +- [04-events-observers.md](../../../../docs/workpad/research/bevy-skill-research/04-events-observers.md) — Event/Message split +- [05-input.md](../../../../docs/workpad/research/bevy-skill-research/05-input.md) — Input API changes +- [06-camera-gotchas.md](../../../../docs/workpad/research/bevy-skill-research/06-camera-gotchas.md) — Camera API changes