⚠️ Bevy 0.17+ Breaking Changes
Material handles wrapped in
MeshMaterial3d<T>, notHandle<T>Observer pattern replaces event system (
commands.trigger(),add_observer())Eventsplit intoMessage(buffered) andEvent(observers)EventWriter/EventReaderreplaced byMessageWriter/MessageReader(message.write()/messages.read())Observer trigger API changed:
// Old commands.add_observer(|trigger: Trigger<OnAdd, Player>| { info!("Spawned player {}", trigger.target()); }); // New commands.add_observer(|add: On<Add, Player>| { info!("Spawned player {}", add.entity); });Color arithmetic removed; use component extraction instead
⚠️ Bevy 0.18+ Breaking Changes
RenderTargetis now a required component onCamera, not aCamerafield:// Old Camera { target: RenderTarget::Image(handle.into()), ..default() } // New commands.spawn((Camera3d::default(), RenderTarget::Image(handle.into())));BorderRadiusis now a field onNode, not a componentLineHeightis now a required component onText/Text2d/TextSpan; removed fromTextFontAmbientLightresource renamed toGlobalAmbientLight;AmbientLightis now a component onCameraclear_children→detach_all_children,remove_children→detach_children,remove_child→detach_child(same onEntityCommandsandEntityWorldMut)AnimationTarget { id, player }replaced by separateAnimationTargetId(id)andAnimatedBy(player_entity)componentsnext_state.set(...)now always firesOnEnter/OnExit; useset_if_neqfor the old behaviourMaterialPluginfieldsprepass_enabled/shadows_enabledreplaced byMaterialtrait methodsenable_prepass()/enable_shadows()SimpleExecutorremoved; useSingleThreadedExecutorinstead#[reflect(...)]now only supports parentheses, not braces or bracketsAssetLoader,AssetSaver,AssetTransformer,Processnow require#[derive(TypePath)]ronno longer re-exported frombevy_sceneorbevy_asset; add it as a direct dependency- Feature renames:
animation→gltf_animation,bevy_sprite_picking_backend→sprite_picking,bevy_ui_picking_backend→ui_picking,bevy_mesh_picking_backend→mesh_picking
General
- Never delete target binaries — Bevy rebuilds take minutes
Footguns
despawn()orphans children, considerdespawn_recursive()instead- Commands are deferred — world mutations apply at end of schedule; don't read back in the same system what you wrote via commands
- Use
Changed<T>andAdded<T>query filters to skip unchanged components — omitting these is the most common Bevy performance mistake - Use observers (
OnAdd,OnRemove) for component lifecycle reactions; don't poll for these inUpdate
Naming
- No unnecessary abbreviations:
positionnotpos - ECS systems: name ends in
_system - Message handlers: name starts with
handle_, ends in_message
ECS
- Think in data (components) and transformations (systems), not objects and methods
- Components = pure data, no logic
- Systems = pure logic, operate on components
- Events/Messages = communication between systems
- Resources = global state; use sparingly
- Keep components small and focused; one large component defeats ECS cache locality
System Design
Plugin structure
- Break the app into discrete modules using plugins
- All plugin structs must have a
///doc comment explaining their purpose and scope
/// Handles damage processing and death detection.
pub struct CombatPlugin;
impl Plugin for CombatPlugin {
fn build(&self, app: &mut App) {
app
.add_event::<DamageEvent>()
.add_systems(Update, (process_damage, check_death));
}
}
System sets
- Use run conditions (
run_if(in_state(...))) to skip whole systems - Use
OnEnter/OnExitschedules for state transitions, not flags checked inUpdate
System ordering
.add_systems(
Update,
(
// 1. Input
handle_input,
// 2. State changes
process_events,
update_state,
// 3. Derived values
calculate_derived_values,
// 4. Visuals
update_materials,
update_animations,
// 5. UI (last)
update_ui_displays,
),
)