Migration Guides

Draft Page

The page is still under development (Tracking Issue: 2582). This page and all pages under it will be hidden from search engines and menus!

Migration Guide: 0.19 to 0.20

Like every major Bevy release, Bevy 0.20 comes with its own set of breaking changes. The most important changes to be aware of are listed below:

  1. TODO

This list prioritizes changes which are sweeping overhauls or break in confusing or quiet ways.

For a full list of changes that may require migration, please see below. We recommend keeping this page open as you migrate your code base, and searching within it for any issues you encounter.

If you run into a problem while migrating that was not covered by this migration guide, please open an issue on the bevy-website repo or file a PR directly with the correct migration advice.

BSN Syntax Improvements. #

PRs:#25318

BSN landed with a few idiosyncrasies that caused friction in practice. We made some changes to BSN's syntax this cycle in the interest of improving its ergonomics and clarity.

All scene references now require @ prefixes:

// Before
bsn! {
    scene_variable
    scene_function()
    {scene_expression}
}

// After
bsn! {
    @scene_variable
    @scene_function()
    @{scene_expression}
}

This freed us up to make component values much easier to work with.

// Before
bsn! {
    template_value(component_variable)
    template_value(component_function())
}

// After
bsn! {
    component_variable
    component_function()
}

Enums no longer require VariantDefaults or FromTemplate, provided they implement Default and Clone:

// Before
#[derive(Component, Default, Clone, VariantDefaults)]
enum Foo {
    A { x: u32, y: u32 },
    #[default]
    B,
}

bsn! {
    Foo::B
}

// After
#[derive(Component, Default, Clone)]
enum Foo {
    A { x: u32, y: u32 },
    #[default]
    B,
}

bsn! {
    Foo::B
}

If you were using an enum that didn't support VariantDefaults, you can remove the template_value wrapper:

// Before
bsn! {
    template_value(Foo::A)
}
// After
bsn! {
    Foo::A
}

The "variant defaults" pattern, which relied on defining individual "default" constructors for each variant is what allowed "individual enum field value patching" (ex: VariantDefaults and FromTemplate would define Foo::a_default() and Foo::b_default() in the example above). This is no longer supported, as the weirdness factor (and Rust ecosystem compatibility challenges) were too costly. When working with enums in BSN, you must now specify each field in the enum, just like you would in normal Rust (which doesn't have support for individual enum variant defaults).

// Before (y field is initialized to its default value)
bsn! {
    Foo::A { x: 1 }
}

// After (y field must be manually specified)
bsn! {
    Foo::A { x: 1, y: 0 }
}

The "builder pattern" previously required a template_value wrapper. This can now be removed:

// Before
bsn! {
    template_value(Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y))
}
// After
bsn! {
    Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y)
}

Additionally, you can now remove the template_value wrapper in cases like this:

// Before
bsn! {
    template_value(node.clone())
}
// After
bsn! {
    node.clone()
}

In general, you should now be able to remove all template_value instances from your BSN declarations!

List syntax in BSN has also been improved. BSN used to use commas to separate entities, with optional () around entities to make the boundaries clearer. This resulted in a lot of syntax noise, line noise, and over-indentation:

bsn! {
    Node 
    Children [
        (
            #OkButton
            @button("Ok")
        ),
        (
            #CancelButton
            @button("Cancel")
        ),
    ]
}

To avoid this, many developers opted for this syntax instead, which made it very hard to visually distinguish entities:

bsn! {
    Node 
    Children [
        #OkButton
        @button("Ok"),
        #CancelButton
        @button("Cancel"),
    ]
}

BSN now uses -- to separate entities:

bsn! {
    Node 
    Children [
        #OkButton
        @button("Ok")
        --
        #CancelButton
        @button("Cancel")
    ]
}

This gives us the best of all worlds: entities are visually distinct, and there is no over-indentation, line noise, or syntax noise. Both () and , have been deprecated in this context.

Using [] and () for bsn_list! (and bsn!) is now discouraged / warned against (ex: bsn_list! []), as it can result in poor rustfmt autoformatting. Instead, use bsn_list! {}, which is the only syntax that rustfmt won't touch. Don't worry, we plan to build a BSN auto-formatter!

bsn_list! {
    #Ok @button("Ok")
    --
    #Cancel @button("Cancel")
}

FontSource generic family variants #

PRs:#24378

The generic font family variants on FontSource, such as FontSource::SansSerif and FontSource::Monospace, have been replaced by a new GenericFontFamily enum. Use the corresponding FontSource constructor methods, or convert GenericFontFamily into a FontSource.

// Old
TextFont {
    font: FontSource::SansSerif,
    ..default()
}

// New
TextFont {
    font: FontSource::sans_serif(),
    ..default()
}

FontCx::set_generic_family now takes a GenericFontFamily instead of a parley::GenericFamily.

Tonemapping and DebandDither moved to bevy_render::view #

PRs:#25480

Tonemapping and DebandDither moved from bevy_core_pipeline::tonemapping to bevy_render::view. Rust imports still work through re-exports, but the reflected type paths changed: update scene files and Bevy Remote Protocol component keys that name them.

  • bevy_core_pipeline::tonemapping::Tonemapping is now bevy_render::view::Tonemapping
  • bevy_core_pipeline::tonemapping::DebandDither is now bevy_render::view::DebandDither

MeshAabb::compute_aabb renamed to get_aabb #

PRs:#21926

MeshAabb trait's compute_aabb method has been renamed to get_aabb.

split out bevy_shape from bevy_math #

PRs:#25302

bevy_shape is a new crate centered around the geometric primitives provided by bevy. These primtiives and related traits have been split out from bevy_math and are now available from different import paths than they used to be.

Notably:

  • all the bevy_math::primitives::* are now exposed either on the top level via bevy_shape::* or in the bevy_shape::prelude::*
  • the following traits have also moved from bevy_math into bevy_shape
    • Primitive2d & Primitive3d
    • Bounded2d & Bounded3d
    • BoundingVolume & IntersectsVolume
    • ToRing
    • Inset
    • ShapeSample

If you use the bevy::prelude::*, there should be nothing you have to change as all of this is still included in the general prelude. Otherwise you might need to include a dependency on bevy_shape now and import your desired structures from there.

Note that Bounded2d, Bounded3d, BoundingVolume & IntersectsVolume are also included in the prelude now, which wasn't the case before. You can check, if the imports of these in your code base are still necessary.

Val::Em and Val::Rem #

PRs:#25231

Val has two new variants, Val::Em and Val::Rem, which size a length relative to a font size. Val::Em resolves against the font size of the node it is set on, Val::Rem against the RemSize resource. The em and rem helper functions construct them, alongside the existing px, percent, vw and vh.

Resolving a Val now needs both of those font sizes, so the following methods take two additional arguments, em_size: EmSize and rem_size: RemSize:

  • Val::resolve
  • Val2::resolve
  • UiPosition::resolve
  • CornerRadius::resolve
  • RadialGradientShape::resolve
  • UiTransform::compute_affine
  • BorderRadius::resolve
// 0.19
let physical = val.resolve(scale_factor, physical_base_value, physical_target_size)?;

// 0.20
let physical = val.resolve(
    scale_factor,
    physical_base_value,
    physical_target_size,
    em_size,
    rem_size,
)?;

ComputedNode has new em_size and rem_size fields holding the values that were used to lay the node out, so when resolving a Val against an existing node you can take them from there (box_shadow for example).

Node now requires EmSize (from bevy_text, re-exported in bevy_ui::prelude), the per-node font size that Val::Em resolves against. If the node has a TextFont, EmSize is recomputed when TextFont, RemSize, or render-target info changes; values you set persist until then. If the node does not have a TextFont component then the value is yours to set and is left alone; it defaults to DEFAULT_REM_SIZE_PX, which matches the default RemSize but does not track changes to it. Propagating EmSize is the responsibility of an app, not bevy_ui.

Use GridTrack::em, GridTrack::rem, RepeatedGridTrack::em and RepeatedGridTrack::rem to construct grid tracks sized in these units using new MinTrackSizingFunction::Em, MinTrackSizingFunction::Rem, MaxTrackSizingFunction::Em and MaxTrackSizingFunction::Rem variants.

FontSize::eval now takes a RemSize rather than an f32:

// 0.19
let size = font_size.eval(logical_viewport_size, rem_size_px);

// 0.20
let size = font_size.eval(logical_viewport_size, RemSize(rem_size_px));

Order independent transparency changes to support premultiplied alpha modes and per-material opt-out #

  1. OIT now can be opt-out per material via the new enable_oit method on Material and MaterialExtension. The associated ShaderDef is MATERIAL_OIT_ENABLED. The original OIT_ENABLED is used for mesh_view_bindings.wgsl and is enabled per camera/view. So custom oit-compatible material shaders should gate oit_draw behind MATERIAL_OIT_ENABLED instead of OIT_ENABLED.
  2. The oit_draw function now expects alpha-premultiplied color to support AlphaMode::Premultiplied and AlphaMode::Add in addition to AlphaMode::Blend modes.
// BEFORE
#ifdef OIT_ENABLED
#import bevy_core_pipeline::oit::oit_draw
#endif // OIT_ENABLED

#ifdef OIT_ENABLED
    let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
    if alpha_mode != pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE {
        // The fragments will only be drawn during the oit resolve pass.
        oit_draw(in.position, out.color);
        discard;
    }
#endif // OIT_ENABLED

// AFTER
#ifdef MATERIAL_OIT_ENABLED
#import bevy_core_pipeline::oit::oit_draw
#endif // MATERIAL_OIT_ENABLED

#ifdef MATERIAL_OIT_ENABLED
    let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS;
    if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_BLEND {
        // The fragments will only be drawn during the oit resolve pass.
        oit_draw(in.position, vec4(out.color.rgb * out.color.a, out.color.a));
        discard;
    }
    // Both `Premultiplied` and `Add` colors are premultiplied in `premultiply_alpha()`
    if alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_PREMULTIPLIED
        || alpha_mode == pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_ADD {
        // The fragments will only be drawn during the oit resolve pass.
        oit_draw(in.position, out.color);
        discard;
    }
#endif // MATERIAL_OIT_ENABLED

Migrate Sprite to use Mesh2d + SpriteMaterial #

PRs:#25432

The Sprite rendering backend was migrated to use the Mesh2d and Material2d infrastructure.

The Sprite component now has a new alpha_mode field but is otherwise unchanged. It defaults to Blend which is what the old backend was using but now you can use Opaque or Mask(f32) when it makes sense for your use case.

The old backend has not been removed yet since Text2d still relies on it but if you were relying on it you should consider moving away from it.

The draw order for Sprite that shared the same Z-level might be different in some cases. In general, you should always specify a Z-level to any sprite that could overlap but it's possible it worked before but that was not part of the contract.

Exclusive function systems have been unified with regular function systems #

PRs:#25507

ExclusiveFunctionSystem, ExclusiveSystemParam, and ExclusiveSystemParamFunction have been removed. Exclusive function systems now use the same code path as regular function systems: FunctionSystem, SystemParam, and SystemParamFunction.

&mut World now implements SystemParam, which means it is no longer required to be the first parameter of a function system. Instead, it may appear anywhere in the parameter list, so long as it does not conflict with other system parameters (same as before). Generally speaking, this means systems' "exclusivity" is now tracked at runtime, rather than compile time.

All ExclusiveSystemParams that did not previously implement SystemParam now implement it, including &mut QueryState and &mut SystemState.

It is no longer possible to use WorldId in an exclusive system, since the SystemParam implementation needs to read it from the World. If you were using WorldId in an exclusive system, consider calling World::id() as the first line of the system. Alternately, use Local<WorldId> as a parameter, which will be automatically populated with the correct ID.

System::is_exclusive() has been removed. Use SystemAccess::is_exclusive() instead, which is created by System::initialize() and stored in SystemWithAccess.

System::initialize() now returns a SystemAccess rather than a FilteredAccessSet. SystemAccess is a superset of FilteredAccessSet that also tracks whether the system is exclusive, or requires no access to the world at all. If you require access to a FilteredAccessSet, call SystemAccess::require_shared_access(system_meta).

ExclusiveMarker has been removed. If you need to mark a system as exclusive, consider piping the system into a fn(&mut World) system, which will automatically mark it as exclusive.

EditableText readonly and display-only modes #

Previously, the EditableText component functioned as both a holder for the state of a editable text, and as a standalone widget, with observers and keyboard mappings. These two functions have been separated: to make a complete, working text input widget, you will now need to insert both an EditableText and a TextInput component.

CalculatedClip now stores transformed clip rectangles #

PRs:#24148

CalculatedClip is now an enum with Rects and FullyClipped variants.

  • Rects is the list of the clipping regions inherited by a UI entity, each is defined by a Rect along with an Affine2 transform.
  • FullyClipped means that the UI entity is clipped completely and will neither be rendered nor pickable.

CalculatedClip::contains_point can be used to test whether a point in physical-pixel coordinates is clipped.

TextReader changes to support inline boxes #

PRs:#27510

TextReader::iter and get now return items as (Entity, usize, TextLayoutItem). TextLayoutItem is an enum with Text and InlineBox variants. The InlineBox variant refers to space created using the new InlineBox component.

TextReader's text, font, color, line_height, letter_spacing, and their safe get_* equivalents have been removed. Instead use TextReader::get and match on the returned TextLayoutItem:

let text = match reader.get(root_entity, index) {
    Some((_, _, TextLayoutItem::Text { text, .. })) => Some(text),
    _ => None,
};

ReflectFromPtr::as_reflect is renamed to ptr_as_reflect #

PRs:#25754

ReflectFromPtr::as_reflect and ReflectFromPtr::as_reflect_mut have been replaced with versions that accept &dyn Any. The previous methods still exist, renamed to ptr_as_reflect and ptr_as_reflect_mut, respectively.

meta_transform accessor on UntypedHandle has been deleted. #

PRs:#25509

The meta_transform accessor on UntypedHandle has been deleted. If you were relying on this, come chat with us so we can learn about your use-case!

Escape in a text input releases focus and propagates #

PRs:#25105

Pressing Escape while in a focused EditableText used to collapse the selection and consume the event: focus stayed in the field, and no ancestor or window-level Escape handler could run while a text input was focused. Escape now collapses the selection, clears InputFocus, and is no longer consumed: the FocusedInput<KeyboardInput> event continues to bubble to ancestor observers and the window.

If you handle Escape on an ancestor of a text input (a dialog's cancel action, for example) or on the window, that handler now runs on the same press that blurs the field, where previously it never ran at all. This matches platform and web behavior, where Escape inside a dialog's input closes the dialog in one press.

If you would rather have two-step behavior (first Escape only blurs the field, the next one reaches your handler), skip presses that originated inside a text input. Note that InputFocus is already cleared by the time your observer runs, and the event's target field is rewritten at each propagation hop, so test the trigger's original target instead:

fn on_escape(
    input: On<FocusedInput<KeyboardInput>>,
    text_inputs: Query<(), With<EditableText>>,
) {
    if text_inputs.contains(input.original_event_target()) {
        // This press blurred a text input; wait for the next one.
        return;
    }
    // cancel / close / navigate back ...
}

split out bevy_curve from bevy_math #

PRs:#25380

bevy_curve is a new crate providing the data structures and traits to create and sample mathematical curves. This functionality is nothing new and was originally included in bevy_math. In an effort to clean up bevy_math it was split out in its own crate.

The new crate means two things:

  • the curve feature in bevy_math (which was default-enabled) is gone now
  • imports have to be adjusted
    • from bevy_math::curve::* -> bevy_curve::*
    • from bevy_math::cubic_splines::* -> bevy_curve::cubic_splines::*
    • bevy_math::Curve was a top-level export, which is gone now and also just bevy_curve::Curve now.

The crate is in the bevy default feature set, so if you use this, nothing should change. All the imports should be exported via bevy::prelude::* in that case.

If you disable the default features, you have to enable bevy/bevy_curve now instead of bevy_math/curve.

MeshTag(x) is now MeshTag::new(x) #

PRs:#24922

Creating a new MeshTag is now done with e.g. MeshTag::new(12345) instead of e.g. MeshTag(12345).

Likewise, accessing the value of a MeshTag is now done with tag.value instead of tag.0. (It still implements the Deref and DerefMut traits, so you can use the * operator instead if you wish.)

This was done because MeshTag now has an optional type ID (which you can supply with MeshTag::with_type if you wish). You can use this type ID to help identify instances in which your application accidentally overwrites one mesh tag with another. Note that the type ID is only stored in debug mode.

CustomAttributes::with_attribute has been replaced by a builder #

PRs:#24171

Previously, CustomAttributes were created like this:

let custom_attributes = CustomAttributes::default()
    .with_attribute("my attribute");
    .with_attribute(123);

Now, CustomAttributes are created with CustomAttributesBuilder:

let custom_attributes = CustomAttributesBuilder::new()
    .attribute("my attribute")
    .attribute(123)
    .build();

This change was a side effect of memory optimizations internal to CustomAttributes.

Font::from_bytes no longer takes a family name #

PRs:#24362

Font::from_bytes no longer takes a family name. Loaded font assets are now automatically registered with an internal asset-specific alias for handle lookups, and with their embedded family name from the font data.

DownsampleShaders::general is now a Handle<Shader> #

PRs:#25744

DownsampleShaders::general is now a single Handle<Shader> instead of a HashMap<TextureFormat, Handle<Shader>>.

Code that looked up a shader in DownsampleShaders::general and queued its own compute pipelines should specialize the new DownsamplePipeline resource instead. It adds the required shader defs itself. Build a DownsamplePipelineKey for each pass, then get the pipeline from SpecializedComputePipelines<DownsamplePipeline> and the bind group layout from the key:

let first = DownsamplePipelineKey {
    texture_format: TextureFormat::Rgba16Float,
    array_texture: true, // for cubemaps and other 2D array textures
    combine_bind_groups: can_combine_downsampling_bind_groups(&render_adapter, &render_device),
    pass: DownsamplePass::First,
};
let second = DownsamplePipelineKey { pass: DownsamplePass::Second, ..first };

let first_pipeline = specialized_pipelines.specialize(&pipeline_cache, &downsample_pipeline, first);
let first_layout = first.bind_group_layout();

If you do this in a RenderStartup system, order it after init_gpu_resource::<DownsamplePipeline> and init_gpu_resource::<SpecializedComputePipelines<DownsamplePipeline>>, since both resources are created there.

The FIRST_PASS and SECOND_PASS shader defs in downsample.wesl are now SPLIT_BIND_GROUP_FIRST and SPLIT_BIND_GROUP_SECOND.

bevy_ui::widget::TextScroll has been replaced by EditableText::viewport #

PRs:#24634

bevy_ui::widget::TextScroll has been removed. Editable text scroll state is now stored in EditableText::viewport, using the new bevy_text::TextViewport type. EditableText::viewport.offset is the direct replacement for TextScroll.

The scroll_editable_text system has also been removed, cursor reveal behavior is now handled automatically when TextEdits are applied.

A new system sync_editable_text_viewports in bevy_ui synchronizes each EditableText's viewport size with the size of its respective ComputedNode.

resolve_font_source #

PRs:#24378

The resolve_font_source function has been removed. Use FontSource::resolve_font_family in its place.

// Old
let family = resolve_font_source(&text_font, fonts)?;

// New
let family = text_font.font.resolve_font_family(fonts)?;

UnpreparedBindGroup is now BindGroupBuilder #

PRs:#25058

The UnpreparedBindGroup structure is now known as BindGroupBuilder, and AsBindGroup::unprepared_bind_group is now known as AsBindGroup::build_bind_group.

If you're using #[derive(AsBindGroup)] to provide the implementation of AsBindGroup, then you shouldn't need to do anything in order to migrate, as the implementation of that derive macro has been updated accordingly. However, if you manually implement AsBindGroup, you may need to rename unprepared_bind_group to build_bind_group and write UnpreparedBindingResources to the new output parameter instead of returning a new UnpreparedBindGroup. Generally, the contents of that method can be identical other than using UnpreparedBindingResources and having to write to the output parameter; the exception is that UnpreparedBindingResource::Data now no longer takes a vector itself and instead specifies byte ranges in the shared BindGroupBuilder::data_buffer.

The primary motivation for this change was to reduce allocations. This occurs in two ways. First, the asset preparation infrastructure can reuse a single BindGroupBuilder when multiple materials need to be prepared instead of calling the unprepared_bind_group method, which allocates, again and again. Second, any UnpreparedBindingResource::Data resources inside the BindGroupBuilder can now reference a single Vec (which is cleared instead of reallocated as materials are prepared) rather than having to allocate anew for each material.

Moved Observer events B Bundle generic into the event type #

PRs:#24013

The B: Bundle type parameter has been removed from On<E, B> in observer systems. Lifecycle events (Add/Insert/Discard/Remove/Despawn) have been updated to have this B: Bundle directly on them.

// Bevy 0.19
world.add_observer(|on: On<Add, A>| {
    // ...
});

// Bevy 0.20
world.add_observer(|on: On<Add<A>>| {
    // ...
});

For custom event types that previously made use of B: Bundle, its recommended to do the following:

// Bevy 0.19

#[derive(Event)]
pub struct Foo;

#[derive(Component)]
pub struct Bar;

world.add_observer(|on: On<Foo, Bar>| {
    // ...
});

// Bevy 0.20

#[derive(Event)]
pub struct FooEvent;

#[derive(Component)]
pub struct Bar;

pub struct Foo<B: Bundle>(PhantomData<B>);

impl<B: Bundle> EventPattern for Foo<B> {
    type Event = FooEvent;
    type Components = B;
}

world.add_observer(|on: On<Foo<Bar>>| {
    // ...
});

For lifecycle observers watching dynamic components, you now need to modify On<Add> to On<Add<()>>:

// Bevy 0.19
world.spawn(
    Observer::new(|_: On<Add>| {
        // ...
    })
    .with_component(component_id),
);

// Bevy 0.20
world.spawn(
    Observer::new(|_: On<Add<()>>| {
        // ...
    })
    .with_component(component_id),
);

WorldQuery trait no longer contains default implementations. #

PRs:#25175

In previous Bevy versions, WorldQuery::init_nested_access and WorldQuery::update_archetypes had default implementations. Most WorldQuery implementations do not need these methods, so defaulting them seems reasonable. However, these methods play an important role in the correctness and even soundness of WorldQuery implementations.

To reduce the chances of invalid WorldQuery implementations, WorldQuery now requires users to implement these two methods manually. To maintain existing behavior, just implement the two methods with empty bodies, like:

impl WorldQuery {
    // ... the previous WorldQuery implementation, no changes

    // Add these two methods.

    fn init_nested_access(
        _state: &Self::State,
        _system_name: Option<&str>,
        _component_access_set: &mut FilteredAccessSet,
        _world: UnsafeWorldCell,
    ) {
    }

    fn update_archetypes(_state: &mut Self::State, _world: UnsafeWorldCell) {}
}

macOS app activation now follows Window::focused on startup and window creation #

PRs:#24702

On macOS, apps now request activation (to become the active/frontmost app) on startup only if the WindowPlugin::primary_window (or any additional Window entities available before WinitPlugin::build) has focused: true. This allows apps to avoid stealing focus from the user by setting focused: false.

In addition, apps now request activation when a visible Window is created after startup with focused: true. Set focused: false if you'd prefer the app to remain inactive.

Apps that only use the default WindowPlugin::primary_window, which is initially focused: true by default, are unchanged.

with_luminance no longer clamps in bevy_color #

PRs:#25394

LinearRgba::with_luminance now scales the components to the target luminance and does not clamp the result, so HDR and wide-gamut values survive. A saturated color or a target above 1.0 can produce components outside [0.0, 1.0]. Operations that convert through it, such as Srgba::with_luminance and Color::with_luminance, change the same way. The Laba to Lcha conversion now retains the color's full gamut.

If you were using these methods to bring colors back into SDR range, you'll now need to clamp explicitly with c.red.clamp(0., 1.). Alternatively, convert through ColorToPacked, which still quantizes colors to [0, 1].

Flat Pointer Events #

PRs:#25337

Pointer events are now "flattened". For example Pointer<Press> is now PointerPress. These events no longer use (or implement) Deref to access the inner Press fields. Instead they are stored directly on the PointerPress event. Pointer is now non-generic, and is a field stored on each pointer event.

// Before
fn on_press(press: On<Pointer<Press>>) {
  info!("pressed {} {:?} {:?}", press.entity,  press.pointer_id, press.pointer_location.position);
} 

// After
fn on_press(press: On<PointerPress>) {
  info!("pressed {} {:?} {:?}", press.entity, press.pointer.id,  press.pointer.position);
}

pointer.position is just a Vec2, rather than a Location. Location isn't used much in practice. Consumers that need a Location can now use pointer.location().

Developers that want to write code that is generic on pointer events should now use the new PointerEvent trait:

// Before
fn on_pointer_event<E: Debug + Clone + Reflect>(event: On<Pointer<E>>) {
}

// After
fn on_pointer_event<E: PointerEvent>(event: On<E>) {
}

Opt-in ScreenSpaceTransmission #

PRs:#25201

ScreenSpaceTransmission is no longer a required component of Camera3d so it's disabled by default. This may change how your scenes with translucent or transparent materials are rendered! Now you should add ScreenSpaceTransmission to the Camera3d to enable screen space specular transmission on it.

RenderDebugOverlay added to default plugins and now has optional keybindings #

PRs:#24891

RenderDebugOverlay is now added to the default plugins, disabled by default. enable it below with the default keybindings:

App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(RenderDebugOverlayKeybindings {
            enable_keybindings: true,
            ..Default::default()
        })

keybindings are set to KeyCode::F1 for cycling modes and KeyCode::F2 for cycling opacity by default.

keybindings are configurable during runtime by changing the RenderDebugOverlay resource:

    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(RenderDebugOverlayKeybindings {
            enable_keybindings: true,
            cycle_mode: KeyCode::F3,
            cycle_opacity: KeyCode::F4,
        })

Keybindings can be changed at runtime, for example:

fn change_keybindings(mut keybindings: ResMut<RenderDebugOverlayKeybindings>) {
    keybindings.enable_keybindings = true;
    keybindings.cycle_mode = KeyCode::F5;
    keybindings.cycle_opacity = KeyCode::F6;
}

FilteredResources and similar structs have been deprecated #

PRs:#25331

FilteredResources, FilteredResourcesMut, FilteredResourcesBuilder, FilteredResourcesMutBuilder, FilteredResourcesParamBuilder, and FilteredResourcesMutParamBuilder, have been deprecated in favor of QueryBuilder and QueryParamBuilder.

The API has changed somewhat, below we provide an example.

// 0.19
let system = 
    FilteredResourcesParamBuilder::new(|builder| {
        builder.add_read::<ResA>();
    })
    .build_state(&mut world)
    .build_system(resource_system);

fn resource_system(filtered: FilteredResources) {
   let resource_a: Ref<ResA> = filtered.get::<ResA>().unwrap();
}

// 0.20
let system =
    QueryParamBuilder::new(|builder| {
        builder.data::<Ref<ResA>>().with::<IsResource>();
    })
    .build_state(&mut world)
    .build_system(resource_system);

fn resource_system(query: Query<FilteredEntityRef>) {
    let entity: FilteredEntityRef = query.single().unwrap(); // Or use `Single<FilteredEntityRef>` as a parameter!
    let resource_a: &A = entity.get::<A>().unwrap();
    // Or with change tracking
    let resource_a: Ref<A> = entity.get_ref::<A>().unwrap();
    // Or by ID
    let resource: Ptr = entity.get_by_id(component_id).unwrap();
    let change_ticks: ComponentTicks = entity.get_change_ticks_by_id(component_id).unwrap();
}

So instead of a FilteredResourcesParamBuilder that provides a FilteredResourcesBuilder, which resolves to FilteredResources, we have a QueryParamBuilder that provides a QueryBuilder that resolves to a Query. The Mut variants also turn into Query, QueryParam, and QueryParamBuilder. Most of the migration should be rather straightforward, but there are some specifics we need to clear up. Firstly, when is adding .with::<IsResource> necessary? In general, .with::<IsResource> is used to stop system conflicts. Take a look at the following example:

// 0.20
fn resource_system(resource_query: Query<FilteredEntityRef>, broad_query: Query<EntityMut>) {}

let system = (
    QueryParamBuilder::new(|builder| {
        builder.data::<&mut ResA>();
    }),
    ParamBuilder,
)
    .build_state(&mut world)
    .build_system(resource_system); // panic!

Here, .build_system panics, because broad_query also has mutable access to ResA, just as resource_query does. In order to avoid conflicts, you can add an IsResource filter, like so:

// 0.20
fn resource_system(resource_query: Query<FilteredEntityRef>, broad_query: Query<EntityMut, Without<IsResource>>) {}

let system = (
    QueryParamBuilder::new(|builder| {
        builder.data::<&mut ResA>().with::<IsResource>();
    }),
    ParamBuilder,
)
    .build_state(&mut world)
    .build_system(resource_system); // works!

Adding IsResource is therefor only occasionally necessary, as these conflicts arise. Still, since a resource entity always has an IsResource marker attached, it can't hurt.

Secondly, there's the issue of dealing with multiple resources. Given a Query<FilteredEntityRef> with multiple resources, how do you extract the desired resource. For this, you'd have to know what Entity the resource is stored on. For this purpose, we provide the ResourceEntities system parameter. Querying multiple resources ends up looking as follows:

// 0.20
#[test]
let system = (
    QueryParamBuilder::new(|builder| {
        builder.data::<EntityRef>();
        builder.with::<IsResource>();
        builder.or(|builder| {
            builder.with::<ResA>();
            builder.with::<ResB>();
        });
    }),
    ParamBuilder,
    ParamBuilder,
)
    .build_state(&mut world)
    .build_system(resource_system);

fn resource_system(
    query: Query<FilteredEntityRef>,
    resource_entities: &ResourceEntities,
    components: &Components,
) {
    // this can be done for every resource separately. 
    let component_id = components.get_id(TypeId::of::<ResA>()).unwrap();
    let entity = resource_entities.get(component_id).unwrap();
    let entity_ref: FilteredEntityRef = query.get(entity).unwrap();
    let value = entity_ref_a.get::<ResA>().unwrap();
}

Several built-in schedules now order their system sets weakly #

PRs:#25128

A number of Bevy's built-in schedules previously ordered their top-level system sets with .chain(), a "must finish before" ordering: every system in a set had to finish before any system in the next set could start. These sets are now ordered with the new .chain_weak() (and in a few places .after_weak()/.before_weak()), which keeps an ordering only between systems whose data accesses actually conflict and leaves non-conflicting systems in adjacent sets unordered, letting them run in any order for better parallelism.

The affected orderings are:

  • Render schedule — the RenderSystems sets (ExtractCommands, PrepareMeshes, Queue, Prepare, Render, Cleanup, PostCleanup, …) and the PrepareResources*, QueueMeshes/QueueSweep, and asset-preparation sub-orderings within them.
  • RenderGraph schedule — the RenderGraphSystems sets (Begin, Render, Submit, Finish).
  • Core2d and Core3d schedules — the pass sets (Prepass, MainPass, EarlyPostProcess, PostProcess).
  • ExtractSchedule — the UI extract sets (RenderUiSystems), MeshExtractionSystems (now after_weak(extract_visibility_ranges)), and DirtySpecializationSystems (now ordered with before_weak).
  • PostUpdate — the UI UiSystems sets (CameraUpdateSystems, Prepare, Propagate, Content, Layout, PostLayout).

For most users this changes nothing. When two weakly-ordered systems actually conflict on their tracked data access, a normal ordering is kept between them so the earlier one still runs first. Deferred-effect producers (Commands) keep their sync-point ordering, and exclusive systems are treated as always conflicting. In practice almost every ordering above is still enforced this way — the change only relaxes ordering between systems that have no data dependency at all.

However, if you added a custom system to one of these sets and relied on a system in an earlier set finishing before yours starts — where that dependency is not expressed through tracked ECS access (for example, communication through interior mutability on a Res<T>, a channel, an atomic, or global/NonSend state) — that ordering is no longer guaranteed. Restore a strict ordering explicitly:

// Before: relied on the implicit strict ordering between these render sets.
app.add_systems(Render, my_system.in_set(RenderSystems::Prepare));

// After: request the strict ordering you need explicitly.
app.add_systems(
    Render,
    my_system
        .in_set(RenderSystems::Prepare)
        .after(some_earlier_system),
);

Prefer expressing the dependency through the ECS (have the producer write a ResMut<T> and the consumer read Res<T>) so the scheduler can see it and order the systems for you.

WESL Shaders #

Bevy's shaders are now written in WESL and the naga_oil preprocessor is gone. Custom shaders in the naga_oil dialect need to be translated to WESL and renamed from .wgsl to .wesl. Plain WGSL files with no preprocessor directives keep working.

// BEFORE
#import bevy_pbr::forward_io::VertexOutput
#import "shaders/util.wgsl"::hsv_to_rgb

#ifdef VERTEX_COLORS
var<private> tint: vec4<f32>;
#endif

@group(2) @binding(#{MATERIAL_BINDING}) var<uniform> color: vec4<f32>;

// AFTER
import bevy_pbr::render::forward_io::VertexOutput;
import super::util::hsv_to_rgb;

@if(VERTEX_COLORS)
var<private> tint: vec4<f32>;

@group(2) @binding(constants::MATERIAL_BINDING) var<uniform> color: vec4<f32>;
  • Imports end with a semicolon and come first in the file, before any declaration or enable directive.
  • Module names now match the shader's path in its crate: bevy_pbr::mesh_view_bindings is bevy_pbr::render::mesh_view_bindings, bevy_pbr::prepass_utils is bevy_pbr::prepass::utils, and so on.
  • @if/@elif/@else attach to whole declarations, struct members, function parameters, imports and statements. Boolean shader defs become conditional compilation flags, and Int/UInt defs are readable as constants::NAME and enable a flag of the same name.
  • #define_import_path is gone. Shaders loaded from embedded:// are importable at their crate and file path (embedded://bevy_foo/bar.wesl is bevy_foo::bar), anything else at its asset path.

The shader_format_wesl cargo feature is gone, WESL support is always enabled. GLSL support has also been removed. SPIR-V passthrough is unchanged.

Some Entity::PLACEHOLDER have been replaced with Option<Entity> #

PRs:#25119

A number of variables and functions that used Entity::PLACEHOLDER as a null value have been changed to use None instead:

  • UiCameraMapper::current_camera()
  • RetainedViewEntity::auxiliary_entity
// Bevy 0.19
let camera: Entity = camera_mapper.current_camera();
if camera != Entity::PLACEHOLDER {
    ...
}

// Bevy 0.20
if let Some(camera) = camera_mapper.current_camera() {
    ...
}

Tonemapping::None is now a full passthrough #

Tonemapping::None is now a full passthrough. ColorGrading and DebandDither no longer apply under it, and negative color channels are no longer clamped to zero. Camera3d enables DebandDither by default, so a Camera3d without Hdr that used Tonemapping::None renders differently. If you used Tonemapping::None to turn off the tone curve, use the new Tonemapping::Linear instead. It applies no tone curve and keeps grading, dither, and the clamp.

// 0.19
commands.spawn((Camera3d::default(), Tonemapping::None));

// 0.20
commands.spawn((Camera3d::default(), Tonemapping::Linear));

Camera2d now defaults to Tonemapping::Linear. No change is needed for 2D cameras. A Camera2d with Hdr now runs the tonemapping pass.

Bevy logs a warning for a camera that combines Tonemapping::None with DebandDither::Enabled or a non-default ColorGrading.

Some large error variants are boxed #

Some large error variants are boxed to avoid clippy::result_large_err:

  • AssetLoadError::RequestedHandleTypeMismatch now is a Box<RequestedHandleTypeMismatchError>
  • LoadDirectError::LoadError::error now is a Box<AssetLoadError>

Component ID constants are more type safe #

PRs:#25779

ComponentId constants like ADD, INSERT, DISCARD, REMOVE, DESPAWN, and IS_RESOURCE are now ComponentId constants rather than usize, to improve type safety. Use ComponentId::index() if you need the underlying usize value.

ui::widgets::Button and ui::Interaction are Deprecated #

PRs:#25197

ui::widgets::Button, available via the ui::prelude, has been deprecated in favor of ui_widgets::Button. ui::Interaction has been deprecated in favor of the picking::hover::Hovered and ui::Pressed components.

View the updated button.rs example for updated Button usage patterns and how to use the Hovered and Pressed components.

Extract Extract #

Extraction used to be specific of Main World to Render World, but will now be generic

  • Use TemporaryRenderEntity::default() instead of TemporaryRenderEntity
  • When using extraction related traits e.g. SyncComponent, ExtractComponent and ExtractResource, you must specify the AppLabel for the target world.

Before:

impl SyncComponent for TemporalAntiAliasing { ... }

#[derive(Component, ExtractComponent)]
pub struct Foo { ... }

After:

impl SyncComponent<RenderApp> for TemporalAntiAliasing { ... }

#[derive(Component, ExtractComponent)]
#[extract_app(RenderApp)]
pub struct Foo { ... }

You can now extract a component from the main subapp to multiple subapps. To extract a component to multiple subapps, list them as arguments to extract_app:

#[derive(Component, Clone, Debug, ExtractComponent)]
#[extract_app(RenderApp, AudioApp)]
struct SomeComponent;

All of the above has moved to the new crate bevy_extract.

Most extraction parts are re-exported by bevy_render .

Some migrations are needed:

  • bevy_render::extract_plugin::extract() has moved to bevy_extract::extract_plugin::extract()

&strs must now have a static lifetime to be converted to Name #

PRs:#24544

A From<&str> implementation for Name has been replaced with a From<&'static str> implementation for Name. This was done to avoid unexpected allocations.

If you do not mind the extra allocation, you can use Name::new(non_static_str.to_owned()) for previous behavior.

Ptr::as_ptr now returns a *const u8. #

PRs:#25745

In previous versions, Ptr::as_ptr returned a *mut u8. However, Ptr is intended to be like an immutable borrow. To make these semantics clearer, Ptr::as_ptr now returns *const u8.

To maintain the previous behavior, simply cast the *const u8 to *mut u8. For example: my_ptr.as_ptr().cast_mut().

SpriteMaterial and SpriteMaterialPlugin rename #

PRs:#25415
  • bevy_sprite_render::SpriteMaterial has been renamed to SpriteMeshMaterial. Note that this type is rarely used outside of the sprite's implementation.
  • bevy_sprite_render::SpriteMaterialPlugin has been renamed to SpriteMeshMaterialPlugin. Note that this plugin is usually added by the SpriteMeshPlugin or SpriteRenderPlugin instead.

define_label! no longer defines an Interner #

PRs:#24445

The macro define_label!() no longer takes a parameter for the name of an Interner, and that interner is no longer a public static item. Calls like

bevy::ecs::define_label!(
    /// Documentation
    ThingLabel,
    THING_LABEL_INTERNER
);

must be changed to

bevy::ecs::define_label!(
    /// Documentation
    ThingLabel,
);

If you were calling Interner::intern() on the defined interner, then replace those calls with calls to the .intern() method of the defined label trait.

cursor module moved from bevy_feathers to bevy_picking #

PRs:#25294

The bevy_feathers cursor module, containing EntityCursor, DefaultCursor, OverrideCursor, and CursorIconPlugin have been moved from bevy_feathers::cursor to bevy_picking::cursor.

The custom_cursor feature has also been moved to bevy_picking.

Before:

use bevy_feathers::cursor::{CursorIconPlugin, DefaultCursor, EntityCursor, OverrideCursor};

After:

use bevy_picking::cursor::{CursorIconPlugin, DefaultCursor, EntityCursor, OverrideCursor};

MainEntityHashMap/MainEntityHashSet alias update #

PRs:#18408

MainEntityHashSet/MainEntityHashMap are now aliases of EntityEquivalentHashSet/EntityEquivalentHashMap and implement EntitySet.

As they are no longer aliases of the hashbrown types, some associated functions have to use the proper names or aliases in place of HashMap. Example: HashMap::default -> MainEntityHashMap::default

Types associated with any of EntityHashSet, EntityHashMap, EntityIndexSet, EntityIndexMap now have an additional K generic. To maintain the previous meaning, use Entity for K.

TextFont::default() font size is now FontSize::Rem(1.) #

PRs:#25231

TextFont::default() now uses FontSize::Rem(1.) instead of FontSize::Px(20.), so that the RemSize resource actually sets the default font size. With the default RemSize of 20 logical pixels this renders identically, but text left at the default size now scales when RemSize changes. To keep a fixed size, set it explicitly:

// 0.20
TextFont {
    font_size: FontSize::Px(20.),
    ..default()
}

Add scrubbing / dragging to number_input widget #

The API for the FeathersNumberInput has changed. To programmatically update the value, instead of triggering an UpdateNumberInput event, you should insert a NumberInputValue component. This makes it easier to specify the initial value at creation.

// BEFORE
commands.trigger(UpdateNumberInput {
    entity: input_ent,
    value: NumberInputValue::F32(new_value),
});

// AFTER
commands
    .entity(input_ent)
    .insert(NumberInputValue::F32(new_value));

Shader bevy_pbr::utils::{octahedral_encode, octahedral_decode, octahedral_decode_signed} are moved #

PRs:#21926

Shader functions bevy_pbr::utils::{octahedral_encode, octahedral_decode, octahedral_decode_signed} are moved to bevy_render::utils::{octahedral_encode, octahedral_decode, octahedral_decode_signed}

// BEFORE
#import bevy_pbr::utils::{octahedral_encode, octahedral_decode, octahedral_decode_signed}

// AFTER
#import bevy_render::utils::{octahedral_encode, octahedral_decode, octahedral_decode_signed}

FromType replaced by CreateTypeData #

PRs:#13723

FromType<T> has been replaced by CreateTypeData<T, Input = ()>. This was done to better communicate what the trait was for (i.e. creating type data), as well as make it possible to pass in additional input when registering type data.

Implementors of FromType<T> will need to update their implementation:

// BEFORE
impl<T> FromType<T> for ReflectMyTrait {
  fn from_type() -> Self {
    // ...
  }
}

// AFTER
impl<T> CreateTypeData<T> for ReflectMyTrait {
  fn create_type_data(input: ()) -> Self {
    // ...
  }
}

Additionally, any calls made to FromType::from_type will need to be updated as well:

// BEFORE
<ReflectMyTrait as FromType<Foo>>::from_type()

// AFTER
<ReflectMyTrait as CreateTypeData<Foo>>::create_type_data(())

AssetId::invalid() and AssetId::INVALID_UUID have been deprecated #

PRs:#24392

AssetId::invalid() and AssetId::INVALID_UUID have been deprecated. This is part of an effort to reduce special cases and optimize asset lookups.

If you were using AssetId::invalid() as a null value, the recommended solution is to change your variable to be Option<AssetId> and use None instead of AssetId::invalid().

Before:

struct MyImageResource(AssetId<Image>);

world.insert_resource(MyImageResource(AssetId::invalid());

...

let resource = world.resource::<MyImageResource>()?;
let asset = assets.get(resource.0)?;

After:

struct MyImageResource(Option<AssetId<Image>>);

world.insert_resource(MyImageResource(None));

...

let resource = world.resource::<MyImageResource>()?;
let asset = assets.get(resource.0?)?;

In some cases it may be possible to use AssetId::default() instead. But note that the default ID is not guaranteed to be a null value - an asset can be registered with the default ID.

Changes have been made to DepthAttachment, ViewDepthTexture and ViewPrepassTextures::depth to accommodate stencil support #

PRs:#24725
  • The original DepthAttachment has been renamed to DepthStencilViewAttachment, which holds a new DepthStencilViews instead of a TextureView. It requires specifying views for the combined depth-stencil, depth-only, and stencil-only aspects, as well as a clear value for stencil.
  • A new DepthStencilAttachment has been introduced, containing a CachedTexture and its corresponding DepthStencilViewAttachment, along with an optional depth texture and views from the previous frame.
  • ViewDepthTexture has been renamed to ViewDepthStencilTexture which holds a DepthStencilAttachment instead of a Texture.
  • ViewPrepassTextures::depth is now Option<DepthStencilAttachment> instead of Option<ColorAttachment>.

To ensure compatibility with future custom depth formats, such as combined depth-stencil and stencil-only formats, choose between a single-aspect view (for resource binding in shaders) and an all-aspect view (for render attachments) based on how it will be used.

ScheduleBuildSettings now includes a shuffle_seed field. #

PRs:#25094

ScheduleBuildSettings now includes an additional shuffle_seed field if the debug feature is enabled on bevy or bevy_ecs. Set this to None if you are exhaustively listing out fields.

FeathersColorPlane's vertical axis now increases upward #

PRs:#25446

The vertical axis of the Feathers color plane widget has been flipped, so that increasing values go up rather than down in screen coordinates.

FocusCause has a new Auto variant #

PRs:#25059

FocusCause has a new Auto variant, which is emitted when an AutoFocus component causes an entity to gain focus. Exhaustive matches on FocusCause must handle the new variant.

Atmosphere now supports multiple cameras #

PRs:#23113

Atmosphere now works correctly with multiple cameras. No action is required for most users.

init_atmosphere_buffer has been removed, and AtmosphereBuffer has been changed from a Resource to a Component attached to each camera entity.

If you were directly accessing AtmosphereBuffer as a resource in a render world system, you'll need to query for it as a component on camera entities instead.

CompressedImageSaver improvements #

The compressed_image_saver Cargo feature has been reworked. The old behavior (Basis Universal UASTC compression) has been moved to a new feature called compressed_image_saver_universal, and the compressed_image_saver feature now uses the ctt library to compress textures into BCn (desktop) or ASTC (mobile) formats instead.

If you were using the compressed_image_saver feature and want to keep the previous Basis Universal behavior, rename the feature in your Cargo.toml:

# Before
bevy = { version = "0.19", features = ["compressed_image_saver"] }

# After (keeps old Basis Universal behavior)
bevy = { version = "0.20", features = ["compressed_image_saver_universal"] }

Alternatively, keep using compressed_image_saver to get the new BCn/ASTC compression backend. This produces higher-quality output and supports a wider range of input formats, but does not support all platforms in a single file like UASTC does. We recommend sticking to compressed_image_saver_universal when targeting the web.

CompressedImageSaverError has a new variant CompressionFailed. If you were matching exhaustively on this enum, add a branch for it.

In Bevy 0.19, ImagePlugin registered a default compressed image processor for PNG files. This meant PNG files were automatically compressed if asset processing was enabled, and the processor wasn't overridden by a .meta file. In Bevy 0.20, JPEG files have been added to the default processor. The extensions that the default processor uses can also be overridden by ImagePlugin::default_compressed_image_processor_extensions - to revert back to the Bevy 0.19 PNG-only behavior:

App::new().add_plugins(
    DefaultPlugins.set(ImagePlugin {
        default_compressed_image_processor_extensions: ["png".into()].into(),
        ..Default::default()
    }),
)

PartialReflect::to_dynamic and its helpers now return a Result #

PRs:#24748

PartialReflect::to_dynamic now returns Result<Box<dyn PartialReflect>, ReflectCloneError> rather than panicking. These methods will fail if any value stored inside is an opaque type whose reflect_clone fails, including nested opaque values.

In order to make that change properly robust, the per-kind helpers are now fallible as well, returning Result<_, ReflectCloneError>:

  • Struct::to_dynamic_struct
  • TupleStruct::to_dynamic_tuple_struct
  • Tuple::to_dynamic_tuple
  • List::to_dynamic_list
  • Array::to_dynamic_array
  • Map::to_dynamic_map
  • Set::to_dynamic_set
  • Enum::to_dynamic_enum

Similarly, DynamicEnum::from and DynamicEnum::from_ref have been deprecated in favor of try_from equivalents, which now return Result<DynamicEnum, ReflectCloneError>.

Finally, PartialReflect::try_apply (and apply) build dynamic values internally when applying a value onto a larger collection or a different enum variant. That conversion can now fail (previously it would panic), so ApplyError has grown a new CloneError(ReflectCloneError) variant.

The migration here should be easy: if you were okay with panicking before, just call .unwrap(): all panicking cases have been replaced with an error, and no new failing paths were added.

However, if your code was defensively guarding against the old panic, you can now handle the returned Result directly instead and simplify your error handling.

WgpuWrapper has been removed #

PRs:#25512

WgpuWrapper has been removed, and its uses have been replaced with either new types (e.g. WgpuErrorSource) or removed from the public API (e.g. RenderQueue).

For RenderQueue, RenderAdapter, RenderInstance and RenderAdapterInfo in particular their only field holding a WgpuWrapper is no longer public. However they still Deref/DerefMut to their wgpu type, and if you were constructing/deconstructing them directly you can instead call new and into_inner to do so.

NextState::set_if_neq renamed to set_if_different #

PRs:#24676

NextState::set_if_neq and related methods and enum variants have been renamed to avoid naming conflicts with Mut::set_if_neq / ReflectMut::set_if_neq which have a different meaning.

The following names have changed:

  • NextState::set_if_neq is now NextState::set_if_different
  • NextState::PendingIfNeq is now NextState::PendingIfDifferent
  • CommandsStatesExt::set_state_if_neq is now CommandsStatesExt::set_state_if_different
  • ReflectFreelyMutableState::set_next_state_if_neq is now ReflectFreelyMutableState::set_next_state_if_different
  • ReflectFreelyMutableStateFns::set_next_state_if_neq is now ReflectFreelyMutableStateFns::set_next_state_if_different

Deprecated compatibility wrappers have been added for the renamed methods (NextState::set_if_neq, CommandsStatesExt::set_state_if_neq, and ReflectFreelyMutableState::set_next_state_if_neq) to allow existing code to compile with deprecation warnings.

Explicit TypeId map aliases #

PRs:#25053

TypeIdHashMap and TypeIdIndexMap have been added for code that maps TypeId values. Use TypeIdHashMap when iteration order is unimportant and average O(1) removal is desired. Use TypeIdIndexMap when insertion-order iteration is required.

TypeIdMap remains an alias for the ordered TypeIdIndexMap, but is deprecated so users can choose the appropriate behavior explicitly. TypeIdMapEntry is likewise deprecated in favor of TypeIdHashMapEntry or TypeIdIndexMapEntry. Use TypeIdHashMapExt for the generic convenience methods on TypeIdHashMap; the existing TypeIdMapExt continues to work with ordered maps.

Query iterators from "many" entities now iterate over Result instead of QueryData::Item. #

PRs:#25200

The following iterators now return Result<QueryData::Item<'w, 's>, QueryEntityError> instead of QueryData::Item<'w, 's> as the Iterator::Item.

  • QueryManyIter
  • QueryManyUniqueIter
  • QuerySortedManyIter

These iterators are created by the following methods.

  • Query::iter_many
  • Query::iter_many_mut
  • Query::iter_many_unique
  • Query::iter_many_unique_mut
  • QueryManyIter::sort*

Likewise, the following parallel iterator methods now have Result<QueryData::Item<'w, 's>, QueryEntityError> instead of QueryData::Item<'w, 's> as the closure argument.

  • QueryParManyIter::for_each
  • QueryParManyIter::for_each_init,
  • QueryParManyUniqueIter::for_each
  • QueryParManyUniqueIter::for_each_init,

These parallel iterators are created by the following methods.

  • Query::par_iter_many
  • Query::par_iter_many_unique
  • Query::par_iter_many_unique_mut

These changes were made to allow for full flexibility in handling not matched or not spawned entities. This allows easier debugging by making these errors visible through a panic or logging instead of only giving the option to silently ignore these errors.

For QueryManyIter and QueryManyUniqueIter users can migrate using

  • QueryManyIter::matched
  • QueryManyUniqueIter::matched

which provide the same behavior as before.

// 0.19
fn my_system(entity_list: Res<MyEntityList>, my_component_query: Query<&MyComponent>) {
    for my_component in my_component_query.iter_many(entity_list.iter()) {
        // ...
    }
}

// 0.20
fn my_system(entity_list: Res<MyEntityList>, my_component_query: Query<&MyComponent>) {
    for my_component in my_component_query.iter_many(entity_list.iter()).matched() {
        // ...
    }
}

// 0.19
fn my_mutation_system(entity_list: Res<MyEntityList>, mut my_component_query: Query<&mut MyComponent>) {
    let mut iter = my_component_query.iter_many_mut(entity_list.iter());
    while let Some(my_component) in iter.fetch_next() {
        // ...
    }
}

// 0.20
fn my_mutation_system(entity_list: Res<MyEntityList>, mut my_component_query: Query<&mut MyComponent>) {
    let mut iter = my_component_query.iter_many_mut(entity_list.iter()).matched();
    while let Some(my_component) in iter.fetch_next() {
        // ...
    }
}

For QuerySortedManyIter you can only use .flat_map(Result::ok).

// 0.19
fn my_system(entity_list: Res<MyEntityList>, my_component_query: Query<&MyComponent>) {
    for my_component in my_component_query.iter_many(entity_list.iter())
        .sort::<&MyComponent>()
    {
        // ...
    }
}

// 0.20
fn my_system(entity_list: Res<MyEntityList>, my_component_query: Query<&MyComponent>) {
    for my_component in my_component_query.iter_many(entity_list.iter())
        .sort::<&MyComponent>()
        .flat_map(Result::ok)
    {
        // ...
    }
}

For QueryParManyIter and QueryParManyUniqueIter you need to return early when there is an error to get the same matched behavior as before.

// 0.19
fn my_system(entity_list: Res<MyEntityList>, my_component_query: Query<&MyComponent>) {
    my_component_query.par_iter_many(entity_list.iter()).for_each(|my_component| {
        // ...
    });
}

// 0.20
fn my_system(entity_list: Res<MyEntityList>, my_component_query: Query<&MyComponent>) {
    my_component_query.par_iter_many(entity_list.iter()).for_each(|my_component| {
        let Ok(my_component) = my_component else {
            return;
        };
        // ...
    });
}

Access::reads_and_writes has been renamed to reads. #

PRs:#24778

Previously Access contained (at least) reads_and_writes and writes. This was a confusing, because it suggested that the former also implied write access. We've now renamed reads_and_writes to just reads (since write access implies read access).

As such, the following members have been renamed:

  • Access::try_reads_and_writes -> Access::try_reads
  • UnboundedAccessError::read_and_writes_inverted -> UnboundedAccessError::reads_inverted

ViewTarget::compositing_space is replaced by ResolvedCompositingSpace #

PRs:#25481

Cameras stacked on one render target now share one compositing space, the single space any member requests through CompositingSpace. A stack whose cameras request conflicting spaces falls back to linear compositing and logs a warning.

ViewTarget::compositing_space and ExtractedCamera::compositing_space have been removed. Render-world code should query Option<&ResolvedCompositingSpace> on the view entity instead.

Deprecate DeferredWorld::query #

For consistency with other QueryState methods, DeferredWorld::query has been deprecated. Instead, QueryState::query_mut now takes impl Into<DeferredWorld>, so it can be called with &mut World or &mut DeferredWorld or DeferredWorld.

let world: DeferredWorld = ...;
let query_state: QueryState<D, F> = ...;
// 0.19
let query: Query<D, F> = world.query(&mut query_state);
// 0.20
let query: Query<D, F> = query_state.query_mut(&mut world);

bevy-settings errors on useless SettingsGroups #

PRs:#25548

The SettingsGroup trait now has trait bounds Resource + Reflect + Default, as opposed to (originally) Resource. This is because types that implement SettingsGroup will not gain any of the benefits of SettingsGroup (ie automatic saving and loading caused by the system included in the settings plugin) unless the type is Reflect + Default.

To migrate code:

  • Implement the Reflect and Default traits on your settings group type.
  • Annotate your settings group type with #[reflect(Default, SettingsGroup)].

If your code was already working, and the type implementing SettingsGroup was already saving and loading, you should not need to change anything in your code. The changes were intended to avoid breaking any already functioning code.

BorderRadius_and_ResolvedBorderRadius_fields_are_now_2d #

PRs:#24779

In order to support elliptical nodes, the fields of BorderRadius are now CornerRadiuss and the fields of ResolvedBorderRadius are now Vec2s.

Before:

BorderRadius {
    pub top_left: px(10.),
    pub top_right: percent(20.),
    pub bottom_right: zero(),
    pub bottom_left: vh(5.),
}

After:

BorderRadius {
    pub top_left: CornerRadius::circular(px(10.)),
    pub top_right: CornerRadius::circular(percent(20.)),
    pub bottom_right: CornerRadius::circular(zero()),
    pub bottom_left: CornerRadius::circular(vh(5.)),
}

CornerRadius implements From<Val>, so you can also use into:

BorderRadius {
    pub top_left: px(10.).into(),
    pub top_right: percent(20.).into(),
    pub bottom_right: zero().into(),
    pub bottom_left: vh(5.).into(),
}

Circular corner radius is represented by setting either CornerRadius::x or CornerRadius::y to Val::Auto.

The BorderRadius constructor and update functions are no longer const. This is so that the parameters can take any type implementing Into<CornerRadius>:

let n = BorderRadius::top_right(vh(10.));
let m = BorderRadius::top_right([px(10.), px(20.)]);

BorderRadius::resolve_single_corner has been removed, use CornerRadius::resolve instead.

ShaderBuffer stores typed data in an AlignedVec with an explicit buffer size #

ShaderBuffer CPU data is now stored in an AlignedVec whose alignment can be configured at runtime. The allocation is guaranteed to be aligned to the element type used to fill it, which allows typed reads and writes through bytemuck::cast_slice / cast_slice_mut without extra copies or unaligned access.

The GPU buffer size is now explicit and decoupled from the CPU data length: ShaderBufferData::Initialized carries a buffer_size that can differ from the length of data, so a larger GPU allocation can be kept while CPU data is cleared or resized, avoiding reallocations.

When creating the GPU buffer, buffer_size always wins over the data length: the data is truncated when it is longer than buffer_size, and the buffer is zero-filled when it is shorter.

The public fields of ShaderBuffer have changed:

  • data: Option<Vec<u8>> is now data: ShaderBufferData (Initialized { data, buffer_size } or Uninitialized(size)).
  • buffer_description: wgpu::BufferDescriptor<'static> has been replaced by the label: Cow<'static, str> and buffer_usage: BufferUsages fields.

The following methods changed:

  • ShaderBuffer::new now takes a Vec<T> of bytemuck::NoUninit elements instead of a byte slice:

    // Bevy 0.19
    let buffer = ShaderBuffer::new(&bytes, RenderAssetUsages::default());
    
    // Bevy 0.20
    let buffer = ShaderBuffer::new(bytes, RenderAssetUsages::default());
  • ShaderBuffer::set_data which uses encase::ShaderType, has been removed. Build the buffer with new or fill an existing one with extend / extend_from_slice instead:

    // Bevy 0.19
    let mut buffer = ShaderBuffer::default();
    buffer.set_data(my_struct);
    
    // Bevy 0.20
    let mut buffer = ShaderBuffer::default();
    buffer.extend([my_struct]);
  • From<T> which uses encase::ShaderType, is replaced by From<Vec<T>> which reuses the memory:

    // Bevy 0.19
    let buffer = ShaderBuffer::from(my_struct);
    
    // Bevy 0.20
    let buffer = ShaderBuffer::from(vec![my_struct]);
  • ShaderBuffer::with_size now takes a u64 instead of a usize.

  • ShaderBuffer::resize_in_place has been removed. Use ShaderBuffer::resize_buffer to resize the GPU buffer without touching the CPU data, or resize for both.

  • ShaderBuffer::cast_slice / cast_slice_mut provide typed access to the data (new in 0.20). In 0.19 the data was only reachable as raw bytes through the data field; casting that Vec<u8> was not guaranteed to be aligned.

  • ShaderBuffer::buffer_size() returns the GPU buffer size; use buffer_size() == 0 to check for a zero-sized buffer.

ReflectFromPtr::from_ptr and ReflectFromPtr::from_ptr_mut replaced with ReflectFromPtr::raw_pointer_cast #

PRs:#25754

Previously, ReflectFromPtr had two methods, from_ptr and from_ptr_mut, which returned the function pointers for casting from a Ptr to a &dyn Reflect, and from a PtrMut to a &mut dyn Reflect, respectively. These were very constrained to these two particular casts and required going into a reference (which can have soundness implications).

Now, we have one ReflectFromPtr::raw_pointer_cast. This simply does a cast from an arbitrary pointer, into the same pointer as a trait object for the type used to construct the ReflectFromPtr.

It is not possible to maintain the previous type, however at the callsite of the function pointer, you can do:

// Before

let from_ptr = reflect_from_ptr.from_ptr();
let from_ptr_mut = reflect_from_ptr.from_ptr_mut();

let my_ptr: Ptr = todo!();
// SAFETY: Because I said so!
let reflect: &dyn Reflect = unsafe { (from_ptr)(my_ptr) };

let my_ptr_mut: PtrMut = todo!();
// SAFETY: I'm doubling down!
let reflect_mut: &mut dyn Reflect = unsafe { (from_ptr_mut)(my_ptr_mut) };

// After

let raw_pointer_cast = reflect_from_ptr.raw_pointer_cast();

let my_ptr: Ptr = todo!();
let reflect_ptr: *const dyn Reflect = raw_pointer_cast(my_ptr.as_ptr().cast::<()>().cast_mut()).cast_const();
// SAFETY: Same reasoning as before.
let reflect: &dyn Reflect = unsafe { &*reflect_ptr };

let my_ptr_mut: PtrMut = todo!();
let reflect_ptr_mut: *mut dyn Reflect = raw_pointer_cast(my_ptr_mut.as_ptr().cast());
// SAFETY: Same reasoning as before.
let reflect_mut: &mut dyn Reflect = unsafe { &mut *reflect_ptr_mut };

If you would like to "robustify" your safety comments, it may be useful to note that ReflectFromPtr::raw_pointer_cast promises not to modify the passed-in pointer.

Use ECS for render world window data #

PRs:#25005

The window data used in the render world is now stored directly as a component on a render world entity associated to the window.

If you were using ExtractedWindows or ExtractedWindowSurfaces you can now use Query<&ExtractedWindow> or Query<&SurfaceData>.

If you were relying on ExtractedWindows::primary you can now use Query<&ExtractedWindow, With<PrimaryWindow>>.

Contextual theming #

PRs:#24969

The Feathers ThemeProps structure has significantly changed; if you have created a custom theme, you will need to reorganize it based on semantic tokens.

For a quick and easy port to start off from, you can create SemanticToken’s for every ThemeToken you have used in your custom theme. Then, create a map from these ThemeTokens to your SemanticTokens, and map these SemanticTokens to your desired colors.

From there, you can start combining any identical colors used to the same SemanticToken where it makes sense in your application.

Expose system accesses and filters in BRP schedule.graph #

PRs:#24743

bevy_dev_tools::SystemData added fields filtered_accesses

For example,

pub fn prepare_atmosphere_probe_components(
    probes: Query<(Entity, &AtmosphereEnvironmentMapLight), (Without<AtmosphereEnvironmentMap>,)>,
    mut commands: Commands,
    mut images: ResMut<Assets<Image>>,
)

Generates the below from schedule.graph in BRP.

Note the values in reads, etc., are indexes into components array.

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "schedule_data": {
      "name": "Update",
      "systems": [
        {
          "name": "bevy_pbr::atmosphere::environment::prepare_atmosphere_probe_components",
          "apply_deferred": false,
          "deferred": true,
          "exclusive": false,
          "filtered_accesses": [
            {
              "access": {
                "archetypal": [],
                "reads": [
                  3 // AtmosphereEnvironmentMapLight
                ],
                "reads_inverted": false,
                "writes": [],
                "writes_inverted": false
              },
              "filter_sets": [
                {
                  "with": [
                    3 // AtmosphereEnvironmentMapLight
                  ],
                  "without": [
                    4, // Disabled
                    6 // AtmosphereEnvironmentMap
                  ]
                }
              ]
            },
            {
              "access": {
                "archetypal": [],
                "reads": [
                  2 // Assets<Image>
                ],
                "reads_inverted": false,
                "writes": [
                  2 // Assets<Image>
                ],
                "writes_inverted": false
              },
              "filter_sets": [
                {
                  "with": [
                    0, // IsResource
                    2 // Assets<Image>
                  ],
                  "without": []
                }
              ]
            }
          ]
        },
        ...
      ],
      "components": [
        {
          "name": "bevy_ecs::resource::IsResource",
          "required": []
        },
        {
          "name": "bevy_ui_widgets::dialog::DialogStack",
          "required": [
            0
          ]
        },
        {
          "name": "bevy_asset::assets::Assets<bevy_image::image::Image>",
          "required": [
            0
          ]
        },
        {
          "name": "bevy_light::probe::AtmosphereEnvironmentMapLight",
          "required": []
        },
        {
          "name": "bevy_ecs::entity_disabling::Disabled",
          "required": []
        },
        {
          "name": "bevy_render::sync_world::SyncToRenderWorld",
          "required": []
        },
        {
          "name": "bevy_pbr::atmosphere::environment::AtmosphereEnvironmentMap",
          "required": [
            5
          ]
        },
        ...
      ],
      ...
    }
  }
}

RenderAppChannels::new requires a MainThreadExecutor #

PRs:#25722

RenderAppChannels::new now takes a MainThreadExecutor as its third argument to avoid shutdown deadlocks. Pass a clone of the executor shared by the main and render worlds.

ComponentInfo no longer stores the component ID #

PRs:#25774

ComponentInfo::id() has been removed, and ComponentInfo no longer stores the component ID. Component IDs are already used as keys by the collections that store ComponentInfo instances; if you need both values, retain the ComponentId when retrieving the ComponentInfo from the collection:

// 0.19
let info = components.get_info(component_id).unwrap();
let id = info.id();

// 0.20
let id = component_id;
let info = components.get_info(id).unwrap();

SortedCamera::hdr removed: camera indices count per render target #

PRs:#25479

In a camera stack that mixes Hdr on one render target, the upper camera used to overwrite the lower camera's output. Its blit now auto-detects alpha blending and composites over the base. Set an explicit blend_state in CameraOutputMode::Write to keep the old replace behavior. Single cameras and uniform-Hdr stacks are unaffected.

SortedCamera::hdr has been removed, and sorted_camera_index_for_target now counts per render target alone. A render-world system that read the field should read ExtractedCamera::hdr on the view entity instead.