
Bevy
- 231 installs
- 52 repo stars
- Updated March 4, 2026
- bfollington/terma
Implement gameplay systems, rendering, ECS patterns, and performance tuning in Rust games using the Bevy engine from the terma skill pack.
About
Guides Claude through Bevy-based Rust game development: entity-component-system design, rendering pipelines, asset loading, UI, and gameplay systems. Suited to indie or prototype games where you want a modern ECS engine instead of hand-rolling loops and draw calls.
- Rust ECS patterns
- 2D/3D rendering setup
- asset and scene pipelines
- gameplay system composition
- performance-minded engine usage
Bevy by the numbers
- 231 all-time installs (skills.sh)
- Ranked #86 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bfollington/terma --skill bevyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 231 |
|---|---|
| repo stars | ★ 52 |
| Last updated | March 4, 2026 |
| Repository | bfollington/terma ↗ |
What it does
Implement gameplay systems, rendering, ECS patterns, and performance tuning in Rust games using the Bevy engine from the terma skill pack.
Files
Bevy Game Development Skill
A specialized skill for developing games and applications using the Bevy game engine, based on real-world experience building complex Bevy projects.
When to Use This Skill
Invoke this skill when:
- Implementing features in a Bevy game or application
- Designing component architectures for ECS
- Creating or debugging Bevy systems
- Working with Bevy's UI system
- Building and testing Bevy projects
- Troubleshooting common Bevy issues
- Organizing project structure for Bevy applications
Before You Start: Essential Bevy Tips
⚠️ Bevy 0.17 Breaking Changes
If working with Bevy 0.17, be aware of significant API changes:
- Material handles now wrapped in
MeshMaterial3d<T>(notHandle<T>) - Event system replaced with observer pattern (
commands.trigger(),add_observer()) - Color arithmetic operations removed (use component extraction)
See `references/bevy_specific_tips.md` for complete Bevy 0.17 migration guide and examples.
Consult Bevy Registry Examples First
The registry examples are your bible. Always check them before implementing new features.
Location:
~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy-0.17.1/examplesThere are MANY examples covering all aspects of Bevy development. Review relevant examples to understand best practices and working patterns.
Use Plugin Structure
Break your app into discrete modules using plugins. This improves organization and makes code discoverable.
pub struct CombatPlugin;
impl Plugin for CombatPlugin {
fn build(&self, app: &mut App) {
app
.add_event::<DamageEvent>()
.add_systems(Update, (process_damage, check_death));
}
}See references/bevy_specific_tips.md for detailed plugin patterns and examples.
Design Before Coding
Pure ECS demands careful data modeling. It's hard to search a massive list of systems in one file!
Before implementing: 1. Design the data model (entities, components, events, systems) 2. Check Bevy examples for similar patterns 3. Review docs and existing code 4. Create a plugin for the feature domain
See references/bevy_specific_tips.md for domain-driven design guidance.
Core Development Principles
Think in ECS Terms
Bevy is an Entity Component System (ECS) engine. Always think in terms of data (components) and transformations (systems), not objects and methods.
Separation of Concerns:
- Components = Pure data, no logic
- Systems = Pure logic, operate on components
- Events = Communication between systems
- Resources = Global state (use sparingly)
Component-Driven Design
Keep components focused:
// ✅ GOOD: Small, focused components
#[derive(Component)]
pub struct Health { pub current: f32, pub max: f32 }
#[derive(Component)]
pub struct Armor { pub defense: f32 }
// ❌ BAD: Monolithic component
#[derive(Component)]
pub struct CombatStats {
pub health: f32,
pub armor: f32,
pub strength: f32,
// ... wastes memory for entities that only have some stats
}Add helper methods via impl blocks:
impl Health {
pub fn is_alive(&self) -> bool {
self.current > 0.0
}
pub fn percentage(&self) -> f32 {
self.current / self.max
}
}For detailed component patterns, see references/ecs_patterns.md.
System Design and Ordering
Order systems by dependencies:
.add_systems(
Update,
(
// 1. Input processing
handle_input,
// 2. State changes
process_events,
update_state,
// 3. Derive properties from state
calculate_derived_values,
// 4. Visual updates
update_materials,
update_animations,
// 5. UI updates (must run last)
update_ui_displays,
),
)Use change detection to optimize:
// Only process entities where Health changed
pub fn update_health_bar(
query: Query<(&Health, &mut HealthBar), Changed<Health>>,
) {
for (health, mut bar) in query.iter_mut() {
bar.width = health.percentage() * 100.0;
}
}For detailed query patterns and system design, see references/ecs_patterns.md.
Build and Testing Workflow
Build Commands
Development (faster iteration):
cargo build --features bevy/dynamic_linking- Uses dynamic linking for faster compile times
- 2-3x faster than release builds
- Only use during development
- CRITICAL: Always use this for development builds
Quick Check:
cargo check- Fastest way to verify compilation
- Use after every significant change
Release (production):
cargo build --release- Full optimization
- Use for final testing and distribution
Build Management - CRITICAL
DO NOT delete target binaries freely! Bevy takes minutes to rebuild from scratch.
- Avoid
cargo cleanunless absolutely necessary - Each clean rebuild costs valuable development time
- Be mindful of versions, targets, and crate dependencies getting tangled
- Bevy is under active development - stick to one version per project
See references/bevy_specific_tips.md for detailed build optimization and version management.
Testing Workflow
1. After component changes: Run cargo check 2. After system changes: Run cargo check then cargo build --features bevy/dynamic_linking 3. Manual testing:
- Does the game launch?
- Do the new features work?
- Are console logs showing expected output?
- Do visual changes appear correctly?
Validation points - Let the user test at these milestones:
- New entity spawned
- New mechanic implemented
- Visual effects added
- Major system changes
UI Development in Bevy
Bevy uses a flexbox-like layout system. Follow the marker component pattern:
1. Create marker components:
#[derive(Component)]
pub struct HealthBar;
#[derive(Component)]
pub struct ScoreDisplay;2. Setup in Startup:
pub fn setup_ui(mut commands: Commands) {
commands.spawn((
HealthBar,
Node {
position_type: PositionType::Absolute,
left: Val::Px(10.0),
top: Val::Px(10.0),
width: Val::Px(200.0),
height: Val::Px(20.0),
..default()
},
BackgroundColor(Color::srgba(0.8, 0.2, 0.2, 0.9)),
));
}3. Update in Update:
pub fn update_health_ui(
health: Query<&Health, With<Player>>,
mut ui: Query<&mut Node, With<HealthBar>>,
) {
if let (Ok(health), Ok(mut node)) = (health.get_single(), ui.get_single_mut()) {
node.width = Val::Px(health.percentage() * 200.0);
}
}For detailed UI patterns including positioning, styling, and text updates, see references/ui_development.md.
Incremental Development Strategy
Phase-Based Development
Break features into phases:
Phase 1: Foundation - Core components and basic systems Phase 2: Content - Add entities and populate world Phase 3: Polish - UI improvements and visual effects Phase 4: Advanced Features - Complex mechanics and AI
Iteration Pattern
1. Plan → 2. Implement → 3. Build → 4. Test → 5. Refine
↑ ↓
←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←Each phase should have: 1. Clear success criteria (checklist of what works) 2. Manual test cases (step-by-step testing procedures) 3. User validation points (when to let user test)
Performance Optimization
When to Optimize
For prototypes (7-100 entities):
- No optimization needed
- Change detection is sufficient
- Focus on features, not performance
For production (100+ entities):
- Use spatial partitioning for proximity queries
- Batch material updates
- Consider Fixed timestep for physics
- Profile before optimizing
Query Optimization Tips
1. Use change detection: Query<&Component, Changed<Component>> 2. Filter early: Query<&A, (With<B>, Without<C>)> instead of filtering in loops 3. Check resource changes: Return early if resource hasn't changed
Common Pitfalls to Avoid
Critical mistakes and their solutions are documented in `references/common_pitfalls.md`. Key pitfalls include:
1. Forgetting to register systems in main.rs 2. Borrowing conflicts (use get_many_mut for multiple mutations) 3. Not using Changed<T> for expensive operations 4. Wrong system ordering (input → state → derived → visual → UI) 5. Entity queries after despawn (use if let Ok() pattern) 6. Material/asset handle confusion (store handles properly)
Review references/common_pitfalls.md before implementing complex features.
Using Subagents for Complex Features
When implementing multi-step features, use the plan-implementer subagent with this structure:
Goal: [One sentence describing end state]
Current State: [What exists now]
Requirements: [Numbered list of what to build]
Implementation Steps: [Suggested approach]
Success Criteria: [How to verify it works]
Notes: [Important context, edge cases, design principles]Example:
Implement Health System
Goal: Implement a health system with damage, healing, and death mechanics.
Current State:
- Player entity exists
- No health tracking yet
Requirements:
1. Create Health component with current/max values
2. Create DamageEvent for dealing damage
3. Create system to process damage events
4. Add death detection when health reaches 0
5. Add visual health bar UI
Implementation Steps:
1. Create Health component in src/components/properties.rs
2. Create DamageEvent in src/events.rs
3. Create process_damage system in src/systems/combat.rs
4. Create check_death system
5. Create health bar UI in src/systems/ui/health_bar.rs
6. Register all systems in main.rs in correct order
Success Criteria:
- Player spawns with Health component
- Damage events reduce health
- Health bar updates when health changes
- Entity despawns when health reaches 0
- Code compiles without errorsProject Structure Reference
For details on recommended file organization, module structure, and component file patterns, see references/project_structure.md.
References
This skill includes detailed reference documentation:
references/bevy_specific_tips.md- START HERE: Registry examples, plugin structure, build optimization, version management, domain-driven design for ECSreferences/ecs_patterns.md- Component design patterns, query patterns, and common ECS design patterns (Derivation, State Machine, Threshold/Trigger, Event-Driven, Initialization)references/ui_development.md- Bevy UI hierarchy, component patterns, layout tips, positioning, styling, and text updatesreferences/common_pitfalls.md- Common mistakes and their solutions (system registration, borrowing conflicts, change detection, system ordering, entity queries, asset handles)references/project_structure.md- Recommended file organization, module structure, component file patterns, and change detection
Load these references as needed to inform implementation decisions.
Additional Resources
Bevy Documentation:
- Official Bevy Book: https://bevyengine.org/learn/book/
- Bevy Examples: https://github.com/bevyengine/bevy/tree/main/examples (also in
~/.cargo/registry/...) - Bevy Cheat Book: https://bevy-cheatbook.github.io/
- Plugin Guide: https://bevy.org/learn/quick-start/getting-started/plugins/
- System Sets: https://bevy-cheatbook.github.io/programming/system-sets.html
- Setup & Optimization: https://bevy.org/learn/quick-start/getting-started/setup/
ECS Design Principles:
- Prefer composition over inheritance
- One component = one concern
- Systems should be pure functions
- Use events to decouple systems
- Design data model before coding
- Check registry examples first
---
Remember: Think in terms of data (components) and transformations (systems), not objects and methods. Always consult registry examples and design your data model before diving into implementation. This is the key to effective Bevy development.
Bevy-Specific Development Tips
Bevy 0.17 Specific Changes
Important: Bevy 0.17 introduced several breaking API changes. If you encounter compilation errors related to materials, events, or colors, refer to this section.
Material Component Wrapper
In Bevy 0.17, material handles are wrapped in MeshMaterial3d<T>:
// ❌ Bevy 0.15/0.16 - This will fail in 0.17
Query<&Handle<StandardMaterial>>
// ✅ Bevy 0.17 - Use the wrapper component
Query<&MeshMaterial3d<StandardMaterial>>
// Access the inner handle with .0
fn update_materials(
query: Query<&MeshMaterial3d<StandardMaterial>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
for material_3d in query.iter() {
if let Some(material) = materials.get_mut(&material_3d.0) {
material.emissive = LinearRgba::RED;
}
}
}Error symptoms:
Handle<StandardMaterial> is not a Component- Query trait bounds not satisfied
Solution: Always use MeshMaterial3d<T> wrapper when querying material components.
Observer Pattern (Replaces Events)
Bevy 0.17 introduces observers as a replacement for the event system:
// ❌ Old event pattern (Bevy 0.15/0.16)
#[derive(Event)]
struct SpellCastEvent { spell_name: String }
app.add_event::<SpellCastEvent>()
.add_systems(Update, handle_spell_cast);
fn handle_spell_cast(mut events: EventReader<SpellCastEvent>) {
for event in events.read() {
info!("Cast: {}", event.spell_name);
}
}
fn cast_spell(mut events: EventWriter<SpellCastEvent>) {
events.send(SpellCastEvent { spell_name: "Fireball".into() });
}
// ✅ Bevy 0.17 observer pattern
#[derive(Event, Clone)] // Must derive Clone!
struct SpellCastEvent { spell_name: String }
app.add_observer(handle_spell_cast); // Observer, not system
fn handle_spell_cast(
trigger: Trigger<SpellCastEvent>, // Trigger parameter
// ... other system params
) {
let event = trigger.event();
info!("Cast: {}", event.spell_name);
}
fn cast_spell(mut commands: Commands) {
commands.trigger(SpellCastEvent { spell_name: "Fireball".into() });
}Key differences:
- Events must derive `Clone` in addition to
Event - Use
add_observer(handler)instead ofadd_event()+add_systems() - Handler takes
Trigger<T>as first parameter, use.event()to access data - Trigger with
commands.trigger()instead ofEventWriter::send() - Observers are not systems - they're called directly when triggered
Error symptoms:
MyEvent is not a Messagemethod 'send' not found for MessageWritermethod 'read' not found
Solution: Migrate to the observer pattern as shown above.
Color Operations
Direct color arithmetic operations aren't supported in Bevy 0.17:
// ❌ Doesn't compile
let emissive = color * 0.5;
let darker = color - 0.2;
// ✅ Extract components manually
let emissive = Color::srgb(
color.to_srgba().red * 0.5,
color.to_srgba().green * 0.5,
color.to_srgba().blue * 0.5,
);
// Or use LinearRgba for math operations
let linear = color.to_linear();
let dimmed = LinearRgba::rgb(
linear.red * 0.5,
linear.green * 0.5,
linear.blue * 0.5,
);Error symptoms:
cannot multiply Color by {float}no implementation for Color * f32
Solution: Convert to component form or use LinearRgba for mathematical operations.
---
Using Bevy Registry Examples
The registry examples are your bible. Bevy ships with extensive examples that demonstrate best practices and patterns.
Location:
~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bevy-0.17.1/examplesWhen to consult registry examples:
- Before implementing a new feature type
- When unsure about API usage
- To see working patterns for complex systems
- To understand how plugins should be structured
- For reference implementations of common game mechanics
How to use them: 1. Browse the examples directory for relevant use cases 2. Study the complete implementation (not just snippets) 3. Note how they structure components, systems, and plugins 4. Adapt patterns to your specific needs
There are MANY examples covering:
- 2D/3D rendering
- Animation
- Audio
- Input handling
- UI systems
- Physics
- Scenes and assets
- And much more
Always refer to examples before diving into implementation.
Plugin Structure
Break your app into discrete modules using plugins whenever possible.
Why use plugins:
- Organizes code by feature/domain
- Makes systems reusable
- Improves code discoverability
- Enables modular development
- Follows Bevy best practices
Plugin pattern:
use bevy::prelude::*;
pub struct CombatPlugin;
impl Plugin for CombatPlugin {
fn build(&self, app: &mut App) {
app
.add_event::<DamageEvent>()
.add_systems(Startup, setup_combat)
.add_systems(Update, (
process_damage,
check_death,
update_health_bars,
));
}
}
// In main.rs
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(CombatPlugin)
.add_plugins(MovementPlugin)
.add_plugins(UIPlugin)
.run();
}References:
- Plugin guide: https://bevy.org/learn/quick-start/getting-started/plugins/
- System sets: https://bevy-cheatbook.github.io/programming/system-sets.html
Build Performance and Optimization
Dynamic Linking
Always use dynamic linking during development:
cargo build --features bevy/dynamic_linkingWhy:
- 2-3x faster compile times
- Critical for iteration speed
- Only affects development builds
Setup in `.cargo/config.toml`:
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
[target.x86_64-apple-darwin]
rustflags = ["-C", "link-arg=-fuse-ld=/usr/local/opt/llvm/bin/ld64.lld"]Optimization levels - See: https://bevy.org/learn/quick-start/getting-started/setup/
For faster dev builds, add to Cargo.toml:
[profile.dev]
opt-level = 1
[profile.dev.package."*"]
opt-level = 3Build Management
CRITICAL: Do not delete target binaries freely!
Bevy takes minutes to rebuild from scratch. Be mindful of:
1. Target directory management:
- Avoid
cargo cleanunless absolutely necessary - Incremental builds are your friend
- Each clean rebuild costs valuable development time
2. Version and dependency management:
- Bevy is under active development
- Be mindful of the version you are using
- Dependencies can get tangled easily
- Version mismatches can force complete rebuilds
- Stick to one Bevy version per project when possible
3. Crate dependencies:
- Adding/removing dependencies triggers rebuilds
- Changing feature flags triggers rebuilds
- Plan dependency changes carefully
- Batch dependency updates when possible
Best practices:
- Use
cargo checkfor quick validation (no binary) - Use
cargo build --features bevy/dynamic_linkingfor testing - Only use
cargo cleanwhen dealing with corrupted build artifacts - Keep a stable
Cargo.lockfor consistent builds
Domain-Driven Design for ECS
Pure ECS structure demands careful data modeling.
Think Before You Code
Because it's hard to search a massive list of systems in one file, you must:
1. Design the data model first:
- What entities exist in your domain?
- What components do they need?
- What behaviors (systems) operate on them?
- How do components relate?
2. Refer to docs and existing code:
- Check Bevy examples for similar patterns
- Review the official docs for component design
- Look at existing project code for consistency
- Understand the domain before implementing
3. Use bounded contexts:
- Group related components together
- Create plugins per domain area
- Keep systems focused on single responsibilities
- Avoid cross-domain coupling
Example Domain Modeling Process
Bad approach:
❌ Start coding immediately
❌ Add systems to one giant file
❌ Discover missing components mid-implementation
❌ Hard to navigate, hard to maintainGood approach:
✅ Define the domain (e.g., "Combat System")
✅ List entities (Player, Enemy, Projectile)
✅ List components (Health, Damage, Armor)
✅ List events (DamageEvent, DeathEvent)
✅ List systems (process_damage, check_death, spawn_projectile)
✅ Check examples for similar implementations
✅ Create CombatPlugin
✅ Implement incrementally
✅ Test at each stepFile Organization for Discoverability
src/
├── main.rs # App setup only
├── plugins/
│ ├── mod.rs
│ ├── combat.rs # CombatPlugin
│ ├── movement.rs # MovementPlugin
│ └── inventory.rs # InventoryPlugin
├── components/
│ ├── mod.rs
│ ├── combat.rs # Health, Armor, Damage
│ ├── movement.rs # Velocity, Speed
│ └── inventory.rs # Inventory, Item
└── events.rs # All game eventsBenefits:
- Easy to find related code
- Clear domain boundaries
- Plugin-based modularity
- Searchable by feature/domain
Version Management
Bevy is under active development.
1. Check your Bevy version:
cargo tree | grep bevy2. Stay on one version per project:
- Avoid mixing Bevy versions
- Update all Bevy crates together
- Test thoroughly after version updates
3. API changes between versions:
- Read the migration guide when updating
- Bevy's API evolves rapidly
- Code from older versions may not work
- Examples are version-specific
4. When seeking help:
- Always mention your Bevy version
- Check if examples match your version
- Look for version-specific documentation
Summary Checklist
Before implementing:
- [ ] Check registry examples for similar features
- [ ] Design the data model (entities, components, events, systems)
- [ ] Create a plugin for the feature domain
- [ ] Review existing code for patterns
During development:
- [ ] Use
cargo build --features bevy/dynamic_linking - [ ] Avoid
cargo cleanunless necessary - [ ] Test incrementally
- [ ] Keep systems focused and organized
After implementation:
- [ ] Verify the feature works
- [ ] Check for code organization issues
- [ ] Document domain-specific patterns
- [ ] Update plugin structure if needed
Common Bevy Pitfalls Reference
1. Using Old Event System in Bevy 0.17
❌ Problem:
// Bevy 0.15/0.16 event system doesn't work in 0.17
#[derive(Event)]
struct MyEvent { data: String }
app.add_event::<MyEvent>()
.add_systems(Update, handle_event);
fn handle_event(mut events: EventReader<MyEvent>) { /* ... */ }
fn trigger(mut events: EventWriter<MyEvent>) { /* ... */ }Symptoms:
- Compilation error:
MyEvent is not a Message method 'send' not found for MessageWritermethod 'read' not found for MessageReader
✅ Solution: Migrate to the observer pattern:
// Bevy 0.17 observer pattern
#[derive(Event, Clone)] // Must derive Clone!
struct MyEvent { data: String }
app.add_observer(handle_event); // Use observer, not system
fn handle_event(
trigger: Trigger<MyEvent>, // Trigger parameter
// ... other params
) {
let event = trigger.event();
}
fn trigger_event(mut commands: Commands) {
commands.trigger(MyEvent { data: "test".into() });
}See references/bevy_specific_tips.md for complete migration guide.
2. Querying Material Handles in Bevy 0.17
❌ Problem:
// Bevy 0.15/0.16 pattern doesn't work in 0.17
Query<&Handle<StandardMaterial>>Symptoms:
Handle<StandardMaterial> is not a Component- Query trait bounds not satisfied
✅ Solution: Use the MeshMaterial3d wrapper:
Query<&MeshMaterial3d<StandardMaterial>>
// Access handle with .0
for material_3d in query.iter() {
if let Some(material) = materials.get_mut(&material_3d.0) {
material.emissive = color;
}
}3. Forgetting to Register Systems
❌ Problem:
// Created system but forgot to add to app
pub fn my_new_system() { /* ... */ }✅ Solution: Always add to main.rs:
.add_systems(Update, my_new_system)2. Borrowing Conflicts
❌ Problem:
// Can't have multiple mutable borrows
mut query1: Query<&mut Transform>,
mut query2: Query<&mut Transform>, // Error!✅ Solution:
// Use get_many_mut for specific entities
mut query: Query<&mut Transform>,
if let Ok([mut a, mut b]) = query.get_many_mut([entity_a, entity_b]) {
// Can mutate both
}3. Infinite Loops with Events
❌ Problem:
// System reads and writes same event type
fn system(
mut events: EventWriter<MyEvent>,
reader: EventReader<MyEvent>,
) {
for event in reader.read() {
events.send(MyEvent); // Infinite loop!
}
}✅ Solution: Use different event types or add termination condition.
4. Not Using Changed<T>
❌ Problem:
// Runs every frame for every entity
fn system(query: Query<&BigFive>) {
for traits in query.iter() {
// Expensive calculation every frame
}
}✅ Solution:
// Only runs when BigFive changes
fn system(query: Query<&BigFive, Changed<BigFive>>) {
for traits in query.iter() {
// Only when needed
}
}5. Entity Queries After Despawn
❌ Problem:
commands.entity(entity).despawn();
// Later in same system
let component = query.get(entity).unwrap(); // Crash!✅ Solution: Commands apply at end of stage. Use Ok() pattern:
if let Ok(component) = query.get(entity) {
// Safe
}6. Material/Asset Handle Confusion
❌ Problem:
// Created material but didn't store handle
materials.add(StandardMaterial { .. }); // Handle dropped!✅ Solution:
let material_handle = materials.add(StandardMaterial { .. });
commands.spawn((
MeshMaterial3d(material_handle),
// ...
));7. System Ordering Issues
❌ Problem:
// UI updates before state changes
.add_systems(Update, (
update_ui,
process_input, // Wrong order!
))✅ Solution: Order systems by dependencies:
.add_systems(Update, (
// Input processing
process_input,
// State changes
update_state,
// UI updates (reads state)
update_ui,
))8. Not Filtering Queries Early
❌ Problem:
// Filter in loop (inefficient)
Query<(&A, Option<&B>, Option<&C>)>
// Then check in loop✅ Solution:
// Filter in query (efficient)
Query<&A, (With<B>, Without<C>)>Bevy ECS Patterns Reference
Component Design Patterns
Component Types
1. Data Components Store game state, always derive Component:
#[derive(Component, Clone, Debug)]
pub struct BigFive {
pub openness: f32,
pub conscientiousness: f32,
pub extraversion: f32,
pub agreeableness: f32,
pub neuroticism: f32,
}2. Marker Components Used for queries and categorization:
#[derive(Component)]
pub struct Player;
#[derive(Component)]
pub struct NPC;
#[derive(Component)]
pub struct Burning; // Marker: entity is on fire3. Tag Components Temporary state or UI markers:
#[derive(Component)]
pub struct HoveredEntity;
#[derive(Component)]
pub struct InspectedEntity;Component Best Practices
✅ DO:
- Keep components focused on single responsibility
- Use
Option<T>for components that may not always be present - Derive
Clonefor components that need to be copied - Add helper methods via
implblocks - Use archetypal patterns for common configurations
impl BigFive {
pub fn temperature(&self) -> f32 {
self.extraversion * 500.0 + 20.0
}
pub fn fire() -> Self {
Self {
openness: 0.8,
conscientiousness: -0.7,
extraversion: 0.9,
agreeableness: -0.5,
neuroticism: 0.7,
}
}
}❌ DON'T:
- Put logic in components
- Store references to other entities directly (use
EntityIDs) - Create deeply nested component hierarchies
- Use components as function parameters
Query Patterns
1. Basic Query
fn system(query: Query<&ComponentA>) {
for component in query.iter() {
// Process each entity
}
}2. Multi-Component Query
fn system(query: Query<(&ComponentA, &ComponentB, &mut ComponentC)>) {
for (a, b, mut c) in query.iter_mut() {
// Read a, b; mutate c
}
}3. Optional Components
fn system(query: Query<(&Name, Option<&BigFive>)>) {
for (name, maybe_traits) in query.iter() {
if let Some(traits) = maybe_traits {
// Has BigFive
} else {
// Doesn't have BigFive
}
}
}4. Filtered Query
fn system(
query: Query<(&BigFive, &Name), (With<Player>, Without<NPC>)>
) {
// Only entities that have Player and don't have NPC
}5. Multiple Mutable Access
fn system(
mut query: Query<&mut BigFive>,
events: EventReader<CastSpellEvent>,
) {
for event in events.read() {
if let Ok([mut source, mut target]) =
query.get_many_mut([event.source, event.target])
{
// Can mutate both at once
}
}
}Common Design Patterns
Derivation Pattern
Problem: Some properties should be calculated from others.
Solution:
// Source of truth
#[derive(Component)]
pub struct BigFive {
pub extraversion: f32,
// ...
}
impl BigFive {
pub fn temperature(&self) -> f32 {
self.extraversion * 500.0 + 20.0
}
}
// Cached derived value
#[derive(Component)]
pub struct Temperature {
pub degrees: f32,
}
// System to sync
pub fn derive_temperature(
mut query: Query<(&BigFive, &mut Temperature), Changed<BigFive>>,
) {
for (traits, mut temp) in query.iter_mut() {
temp.degrees = traits.temperature();
}
}State Machine Pattern
Problem: Entities need to change behavior based on state.
Solution:
#[derive(Component)]
pub enum NPCState {
Idle,
Patrolling,
Investigating,
Attacking,
}
pub fn npc_behavior(
mut query: Query<(&mut Transform, &NPCState)>,
) {
for (mut transform, state) in query.iter_mut() {
match state {
NPCState::Idle => { /* ... */ }
NPCState::Patrolling => { /* ... */ }
NPCState::Investigating => { /* ... */ }
NPCState::Attacking => { /* ... */ }
}
}
}Threshold/Trigger Pattern
Problem: Need to detect when values cross boundaries.
Solution:
pub fn check_thresholds(
mut query: Query<(Entity, &BigFive, &Name), Changed<BigFive>>,
mut commands: Commands,
) {
for (entity, traits, name) in query.iter() {
// Check threshold
if traits.extraversion > 0.6 {
commands.entity(entity).insert(Burning {
intensity: traits.extraversion,
});
println!("{} IGNITES!", name);
}
// Remove if below threshold
if traits.extraversion <= 0.6 {
commands.entity(entity).remove::<Burning>();
}
}
}Event-Driven Pattern
Problem: Systems need to communicate without tight coupling.
Solution:
// Define event
#[derive(Event)]
pub struct SpellCastEvent {
pub caster: Entity,
pub target: Entity,
pub spell_type: SpellType,
}
// Writer system
pub fn cast_spell(
input: Res<ButtonInput<KeyCode>>,
mut events: EventWriter<SpellCastEvent>,
) {
if input.just_pressed(KeyCode::Space) {
events.send(SpellCastEvent { /* ... */ });
}
}
// Reader system
pub fn process_spells(
mut events: EventReader<SpellCastEvent>,
mut query: Query<&mut BigFive>,
) {
for event in events.read() {
// Process spell
}
}Initialization Pattern
Pattern: Initialize derived components
// Entities spawned with BigFive but missing Temperature/Mass
pub fn initialize_derived_physics(
mut commands: Commands,
query: Query<(Entity, &BigFive), (Without<Temperature>, Without<Mass>)>,
) {
for (entity, traits) in query.iter() {
commands.entity(entity).insert((
Temperature { degrees: traits.temperature() },
Mass { kilograms: traits.mass() },
));
}
}Bevy Project Structure Reference
Standard Bevy Layout
src/
├── main.rs # App setup, plugin registration, system scheduling
├── components/
│ ├── mod.rs
│ ├── properties.rs # Core data components
│ ├── effects.rs # State marker components
│ ├── ui.rs # UI marker components
│ └── [domain].rs # Domain-specific components
├── systems/
│ ├── mod.rs
│ ├── [feature].rs # Feature systems (one file per major feature)
│ └── ui/
│ ├── mod.rs
│ └── [ui_feature].rs
├── events.rs # Game events and messages
└── resources.rs # Global resourcesKey Principles
1. Separation of Concerns
- Components = Pure data, no logic
- Systems = Pure logic, operate on components
- Events = Communication between systems
- Resources = Global state (use sparingly)
2. Module Organization
// Good: Grouped by feature
src/systems/personality_physics.rs
src/systems/thresholds.rs
src/systems/spells.rs
// Bad: Grouped by system type
src/systems/update_systems.rs
src/systems/query_systems.rs3. Component Files Keep related components together:
// src/components/effects.rs
#[derive(Component)]
pub struct Burning { pub intensity: f32 }
#[derive(Component)]
pub struct Frozen;
#[derive(Component)]
pub struct Dissolving { pub progress: f32 }System Ordering
Systems run in the order they're added. Use comments to make dependencies clear:
.add_systems(
Update,
(
// Input processing
spell_input,
// State changes (modifies data)
process_spell_casts,
// Derive properties from state
derive_physics_from_personality,
// Check for threshold crossings
threshold_reactions,
// Visual updates (reads state, updates rendering)
visual_threshold_effects,
update_temperature_visuals,
// UI updates (must run last)
update_inspect_display,
update_hover_tooltip,
),
)Change Detection
Use Changed<T> to avoid unnecessary processing:
// ✅ GOOD: Only process when BigFive changes
pub fn threshold_reactions(
mut query: Query<(Entity, &BigFive, &Name), Changed<BigFive>>,
mut commands: Commands,
) {
for (entity, traits, name) in query.iter() {
if traits.extraversion > 0.6 {
commands.entity(entity).insert(Burning {
intensity: traits.extraversion,
});
println!("{} IGNITES!", name);
}
}
}
// ❌ BAD: Runs every frame for all entities
pub fn threshold_reactions(
mut query: Query<(Entity, &BigFive, &Name)>,
mut commands: Commands,
) {
// Wasteful!
}Bevy UI Development Reference
Bevy UI Hierarchy
Bevy UI uses a flexbox-like layout system:
commands
.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(10.0),
top: Val::Px(10.0),
width: Val::Px(300.0),
padding: UiRect::all(Val::Px(10.0)),
flex_direction: FlexDirection::Column,
..default()
},
BackgroundColor(Color::srgba(0.1, 0.1, 0.1, 0.9)),
))
.with_children(|parent| {
parent.spawn((
Text::new("Title"),
TextFont { font_size: 16.0, ..default() },
TextColor(Color::WHITE),
));
});UI Component Pattern
1. Marker Components for UI Elements
#[derive(Component)]
pub struct SpellBar;
#[derive(Component)]
pub struct HoverTooltip;
#[derive(Component)]
pub struct InspectPanel;2. Setup System (Startup)
pub fn setup_ui(mut commands: Commands) {
commands.spawn((
SpellBar,
Node { /* layout */ },
BackgroundColor(/* color */),
))
.with_children(|parent| {
// Child elements
});
}3. Update System (Update)
pub fn update_ui(
state: Res<GameState>,
mut query: Query<&mut Text, With<SpellBar>>,
) {
for mut text in query.iter_mut() {
**text = format!("State: {:?}", state);
}
}UI Best Practices
Layout Tips
- Use
Val::Px()for fixed sizes - Use
Val::Percent()for responsive layouts - Use
flex_direction: FlexDirection::Columnfor vertical stacking - Use
flex_direction: FlexDirection::Rowfor horizontal stacking - Use
justify_contentandalign_itemsfor alignment
Positioning
Absolute positioning (HUD elements):
Node {
position_type: PositionType::Absolute,
left: Val::Px(10.0),
top: Val::Px(10.0),
..default()
}Centered element:
Node {
position_type: PositionType::Absolute,
left: Val::Percent(50.0),
top: Val::Percent(50.0),
margin: UiRect {
left: Val::Px(-150.0), // Half of width
top: Val::Px(-100.0), // Half of height
..default()
},
width: Val::Px(300.0),
height: Val::Px(200.0),
..default()
}Visibility Control
// Show/hide with Display
mut node: Query<&mut Node, With<Panel>>
// Hide
node.display = Display::None;
// Show
node.display = Display::Flex;Color and Styling
// Background
BackgroundColor(Color::srgba(0.1, 0.1, 0.1, 0.9))
// Border
BorderColor::all(Color::srgba(0.3, 0.6, 0.9, 1.0))
// Highlight on selection
*bg_color = BackgroundColor(Color::srgba(0.2, 0.4, 0.6, 1.0));
*border_color = BorderColor::all(Color::srgba(0.3, 0.6, 0.9, 1.0));Text Updates
// Update text content
**text = "New content".to_string();
// Or with formatting
**text = format!("Value: {:.2}", value);
// Multi-line text
**text = "Line 1\nLine 2\nLine 3".to_string();