About bevy-ecs-components
bevy-ecs-components is a reference skill for defining and managing ECS components in Bevy 0.19. It helps Bevy developers choose component patterns, storage, and lifecycle handling while accounting for version-specific changes.
Key features
- 01Define components and required-component relationships
- 02Handle observers, hooks, and lifecycle events
- 03Choose Table or SparseSet storage
- 04Review Bevy 0.19 component migration details
Use cases
- 01Add components to Bevy game state
- 02Bundle related components with #[require(...)]
- 03Implement reactions to entity-targeted events
- 04Select storage for frequent insertion or iteration
Install
Copy the command and run it in a terminal.
$
npx skills add https:/ / github.com/ chrisgliddon/ bevy-skills --skill bevy-ecs-componentsAfter installing, type/bevy-ecs-componentsin Claude Code to confirm it loaded.
Details
From the project READMEBevy 0.19 — ECS Components
When to use this skill
- Defining a new
Componentfor game state. - Bundling components via
#[require(...)](the modern replacement for "bundles"). - Reacting to component lifecycle: spawning, despawning, inserting, removing.
- Observers — reacting to entity-targeted events with
On<E>. - Choosing storage: Table (default, fast iteration) vs SparseSet (fast add/remove).
Canonical pattern
use bevy::prelude::*;
// 1. Plain components.
#[derive(Component, Default)]
struct Health(f32);
#[derive(Component)]
struct Velocity(Vec3);
// 2. Required components — spawning `Player` auto-spawns the rest.
// `#[require]` accepts `Type` (Default), `Type(args)` (tuple constructor),
// or `Type = expression`.
#[derive(Component)]
#[require(Health = Health(100.0), Velocity = Velocity(Vec3::ZERO), Transform)]
struct Player;
// 3. Sparse storage for components added/removed every frame (e.g. tags
// flipped by gameplay). Default Table storage is faster to iterate.
#[derive(Component)]
#[component(storage = "SparseSet")]
struct Stunned;
// 4. An entity-targeted event reacted to by observers.
#[derive(EntityEvent)]
struct Damage {
entity: Entity, // EntityEvent requires an `entity` field.
amount: f32,
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, spawn_player)
.add_systems(Update, deal_damage)
.add_observer(on_damage)
.run();
}
fn spawn_player(mut commands: Commands) {
commands.spawn(Player);
}
fn deal_damage(mut commands: Commands, query: Query<Entity, With<Player>>) {
for entity in &query {
commands.trigger(Damage { entity, amount: 10.0 });
}
}
// Observer parameter is `On<E>`, not `Trigger<E>` (renamed in 0.17, PR #19596).
fn on_damage(damage: On<Damage>, mut query: Query<&mut Health>) {
let event = damage.event();
if let Ok(mut hp) = query.get_mut(event.entity) {
hp.0 -= event.amount;
}
}
Bevy 0.19 gotchas
Trigger<E>is gone. Observer params areOn<E>in 0.17+. Methods:event(),event_mut(),observer(),original_event_target(),propagate(bool).EntityEvent::set_targetrequiresuse bevy::ecs::entity::SetEntityEventTarget;— not in the prelude.- Storage choice is irrevocable: it is compiled into the component. SparseSet is designed for frequent insertion/removal; Table is the default and usually iterates faster. Benchmark the real workload before changing storage.
#[require(T)]runsT::default(). IfT: !Default, use#[require(T = expression)]or#[require(T = T::new(...))].- Required components are recursive. If
ArequiresBandBrequiresC, insertingAalso insertsC. A constructor specified directly byAwins over an inherited constructor; requirement cycles are invalid. ReplacebecameDiscardin 0.19. Use hookson_add,on_insert,on_discard, andon_remove; the derive attribute is#[component(on_discard = path)].- Hooks run inside
Worldmutations, cannot take arbitrarySystemParams, and cannot despawn the entity they fire on. Use observers when you need flexibility. Resourceis now aComponentsubtrait. Do not derive both. Each resource value lives as a component on a resource entity, and inserting another value of the same resource type can move which entity owns the singleton; keep ordinary entity components and global resources as distinct types unless that behavior is intentional.Bundlederive still exists but most use-cases are better served by#[require(...)]on a "marker" component, which keeps the spawn surface ergonomic.
See also
bevy-ecs-queries— reading components back out.bevy-ecs-systems— observers are themselves systems.bevy-migration-0-17-to-0-18— fullTrigger→Onand event→message rename map.bevy-migration-0-18-to-0-19—Discardlifecycle events and resources-as-components.
Comments
Markdown supported