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