Migration Guide: 0.12 to 0.13

Bevy relies heavily on improvements in the Rust language and compiler. As a result, the Minimum Supported Rust Version (MSRV) is "the latest stable release" of Rust.

support all types of animation interpolation from gltf #

Animation

When manually specifying an animation VariableCurve, the interpolation type must be specified:

// 0.12
VariableCurve {
    keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
    keyframes: Keyframes::Rotation(vec![
        Quat::IDENTITY,
        Quat::from_axis_angle(Vec3::Y, PI / 2.),
        Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
        Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
        Quat::IDENTITY,
    ]),
},

// 0.13
VariableCurve {
    keyframe_timestamps: vec![0.0, 1.0, 2.0, 3.0, 4.0],
    keyframes: Keyframes::Rotation(vec![
        Quat::IDENTITY,
        Quat::from_axis_angle(Vec3::Y, PI / 2.),
        Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.),
        Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.),
        Quat::IDENTITY,
    ]),
    interpolation: Interpolation::Linear,
},

ReadAssetBytesError::Io exposes failing path #

Assets

The ReadAssetBytesError::Io variant now contains two named fields instead of converting from std::io::Error.

  • path: The requested (failing) path (PathBuf)
  • source: The source std::io::Error

Ensure consistency between Un/Typed AssetId and Handle #

Assets

If you relied on any of the panicking From<Untyped...> implementations, simply call the existing typed methods instead. Alternatively, use the new TryFrom implementation instead to directly expose possible mistakes.

Use impl Into<A> for Assets::add #

Assets

Some into calls that worked previously might now be broken because of the new trait bounds. You need to either remove into or perform the conversion explicitly with from:

// 0.12
let mesh_handle = meshes.add(shape::Cube { size: 1.0 }.into()),

// 0.13
let mesh_handle = meshes.add(shape::Cube { size: 1.0 }),
let mesh_handle = meshes.add(Mesh::from(shape::Cube { size: 1.0 })),

GLTF extension support #

Assets

This will have issues with “asset migrations”, as there is currently no way for .meta files to be migrated. Attempting to migrate .meta files without the new flag will yield the following error:

bevy_asset::server: Failed to deserialize meta for asset test_platform.gltf: Failed to deserialize asset meta: SpannedError { code: MissingStructField { field: "include_source", outer: Some("GltfLoaderSettings") }, position: Position { line: 9, col: 9 } }

This means users who want to migrate their .meta files will have to add the include_source: true, setting to their meta files by hand.

Allow TextureAtlasBuilder in AssetLoader #

Assets
  • For add_texture you need to wrap your AssetId in Some
  • finish now returns the atlas texture image directly instead of a handle. Provide the atlas texture to add on Assets to get a Handle

Remove the ability to ignore global volume #

Audio

The option to ignore the global volume using Volume::Absolute has been removed and Volume now stores the volume level directly, removing the need for the VolumeLevel struct.

Volume::new_absolute and Volume::new_relative were removed. Use Volume::new(0.5).

Optional override for global spatial scale #

Audio

AudioPlugin::spatial_scale has been renamed to default_spatial_scale and the default spatial scale can now be overridden on individual audio sources with PlaybackSettings::spatial_scale.

If you were modifying or reading SpatialScale at run time, use DefaultSpatialScale instead.

// 0.12
app.add_plugins(DefaultPlugins.set(AudioPlugin {
    spatial_scale: SpatialScale::new(AUDIO_SCALE),
    ..default()
}));

// 0.13
app.add_plugins(DefaultPlugins.set(AudioPlugin {
    default_spatial_scale: SpatialScale::new(AUDIO_SCALE),
    ..default()
}));

Add support for updating the tracing subscriber in LogPlugin #

Diagnostics

LogPlugin has a new optional update_subscriber field. Use None or ..default() to match previous behavior.

Replace DiagnosticId by DiagnosticPath #

Diagnostics
- const UNIQUE_DIAG_ID: DiagnosticId = DiagnosticId::from_u128(42);
+ const UNIQUE_DIAG_PATH: DiagnosticPath = DiagnosticPath::const_new("foo/bar");

- Diagnostic::new(UNIQUE_DIAG_ID, "example", 10)
+ Diagnostic::new(UNIQUE_DIAG_PATH).with_max_history_length(10)

- diagnostics.add_measurement(UNIQUE_DIAG_ID, || 42);
+ diagnostics.add_measurement(&UNIQUE_DIAG_ID, || 42);

Use EntityHashMap for EntityMapper #

ECS

If you are using the following types, update their listed methods to use the new EntityHashMap. EntityHashMap has the same methods as the normal HashMap, so you just need to replace the name.

EntityMapper

  • get_map
  • get_mut_map
  • new
  • world_scope

ReflectMapEntities

  • map_all_entities
  • map_entities
  • write_to_world

InstanceInfo

  • entity_map
    • This is a property, not a method.

Update Event send methods to return EventId #

ECS

send / send_default / send_batch

For the following methods:

  • Events::send
  • Events::send_default
  • Events::send_batch
  • EventWriter::send
  • EventWriter::send_default
  • EventWriter::send_batch
  • World::send_event
  • World::send_event_default
  • World::send_event_batch

Ensure calls to these methods either handle the returned value, or suppress the result with ;.

// 0.12
fn send_my_event(mut events: EventWriter<MyEvent>) {
    events.send_default()
}

// 0.13
fn send_my_event(mut events: EventWriter<MyEvent>) {
    events.send_default();
}

This will most likely be noticed within match statements:

// 0.12
match is_pressed {
    true => events.send(PlayerAction::Fire),
//                 ^--^ No longer returns ()
    false => {}
}

// 0.13
match is_pressed {
    true => {
        events.send(PlayerAction::Fire);
    },
    false => {}
}

Optimise Entity with repr align & manual PartialOrd/Ord #

ECS

Any unsafe code relying on field ordering of Entity or sufficiently cursed shenanigans should change to reflect the different internal representation and alignment requirements of Entity.

Split WorldQuery into QueryData and QueryFilter #

ECS

Check #9918 and #10799 for more information.

  • Rename the following trait type usages:
    • Trait’s ExtractComponent type Query to Data.
    • Trait’s GetBatchData type Query to Data.
    • Trait’s ExtractInstance type Query to Data.
  • Rename ReadOnlyWorldQuery to QueryFilter and WorldQuery to QueryData
  • You'll need to update your related derives
// 0.12
#[derive(WorldQuery)]
#[world_query(mutable, derive(Debug))]
struct CustomQuery {
    entity: Entity,
    a: &'static mut ComponentA
}

#[derive(WorldQuery)]
struct QueryFilter {
    _c: With<ComponentC>
}

// 0.13
#[derive(QueryData)]
#[query_data(mutable, derive(Debug))]
struct CustomQuery {
    entity: Entity,
    a: &'static mut ComponentA,
}

#[derive(QueryFilter)]
struct QueryFilter {
    _c: With<ComponentC>
}
  • Replace Option<With<T>> with Has<T>
// 0.12
fn my_system(query: Query<(Entity, Option<With<ComponentA>>)>) {
  for (entity, has_a_option) in query.iter(){
    let has_a:bool = has_a_option.is_some();
    //todo!()
  }
}

// 0.13
fn my_system(query: Query<(Entity, Has<ComponentA>)>) {
  for (entity, has_a) in query.iter(){
    //todo!()
  }
}
  • Fix queries which had filters in the data position or vice versa.
// 0.12
fn my_system(query: Query<(Entity, With<ComponentA>)>) {
  for (entity, _) in query.iter(){
  //todo!()
  }
}

// 0.13
fn my_system(query: Query<Entity, With<ComponentA>>) {
  for entity in query.iter(){
  //todo!()
  }
}

// 0.12
fn my_system(query: Query<AnyOf<(&ComponentA, With<ComponentB>)>>) {
  for (entity, _) in query.iter(){
  //todo!()
  }
}

// 0.13
fn my_system(query: Query<Option<&ComponentA>, Or<(With<ComponentA>, With<ComponentB>)>>) {
  for entity in query.iter(){
  //todo!()
  }
}

Reduced TableRow as Casting #

ECS
  • TableRow::new -> TableRow::from_usize
  • TableRow::index -> TableRow::as_usize
  • TableId::new -> TableId::from_usize
  • TableId::index -> TableId::as_usize

Allow the editing of startup schedules #

ECS
  • Added a new field to MainScheduleOrder, startup_labels, for editing the startup schedule order.

Auto insert sync points #

ECS
  • apply_deferred points are added automatically when there is ordering relationship with a system that has deferred parameters like Commands. If you want to opt out of this you can switch from after, before, and chain to the corresponding ignore_deferred API, after_ignore_deferred, before_ignore_deferred or chain_ignore_deferred for your system/set ordering.
  • You can also set ScheduleBuildSettings::auto_insert_sync_points to false if you want to do it for the whole schedule. Note that in this mode you can still add apply_deferred points manually.
  • For most manual insertions of apply_deferred you should remove them as they cannot be merged with the automatically inserted points and might reduce parallelizability of the system graph.
  • If you were manually deriving SystemParam, you will need to add system_meta.set_has_deferred if you use SystemParam::apply and want sync points auto inserted after use of your SystemParam.

Add insert_state to App. #

ECS

Renamed App::add_state to init_state.

Rename ArchetypeEntity::entity into ArchetypeEntity::id #

ECS

The method ArchetypeEntity::entity has been renamed to ArchetypeEntity::id

Restore support for running fn EntityCommands on entities that might be despawned #

ECS

All Command types in bevy_ecs, such as Spawn, SpawnBatch, Insert, etc., have been made private. Use the equivalent methods on Commands or EntityCommands instead.

If you were working with ChildBuilder, recreate these commands using a closure. For example, you might migrate a Command to insert components like:

// 0.12
parent.add_command(Insert {
    entity: ent_text,
    bundle: Capitalizable,
});

// 0.13
parent.add_command(move |world: &mut World| {
    world.entity_mut(ent_text).insert(Capitalizable);
});

Simplify conditions #

ECS

Some common run conditions that were previously closures and needed to be called are now just systems. Remove the parentheses.

  • resource_exists<T>() -> resource_exists<T>
  • resource_added<T>() -> resource_added<T>
  • resource_changed<T>() -> resource_changed<T>
  • resource_exists_and_changed<T>() -> resource_exists_and_changed<T>
  • state_exists<S: States>() -> state_exists<S: States>
  • state_changed<S: States>() -> state_changed<S: States>
  • any_with_component<T: Component>() -> any_with_component<T: Component>
ECS

The lifetimes for EntityCommands have been simplified.

// 0.12 (Bevy 0.12)
struct MyStruct<'w, 's, 'a> {
     commands: EntityCommands<'w, 's, 'a>,
}

// 0.13 (Bevy 0.13)
struct MyStruct<'a> {
    commands: EntityCommands<'a>,
}

The method EntityCommands::commands now returns Commands rather than &mut Commands.

// 0.12 (Bevy 0.12)
let commands = entity_commands.commands();
commands.spawn(...);

// 0.13 (Bevy 0.13)
let mut commands = entity_commands.commands();
commands.spawn(...);

Deprecated Various Component Methods from Query and QueryState #

ECS

QueryState::get_component_unchecked_mut

Use QueryState::get_unchecked_manual and select for the exact component based on the structure of the exact query as required.

Query::(get_)component(_unchecked)(_mut)

Use Query::get and select for the exact component based on the structure of the exact query as required.

  • For mutable access (_mut), use Query::get_mut
  • For unchecked access (_unchecked), use Query::get_unchecked
  • For panic variants (non-get_), add .unwrap()

For example:

fn system(query: Query<(&A, &B, &C)>) {
    // 0.12
    let b = query.get_component::<B>(entity).unwrap();

    // Alternative 1 (using tuple destructuring)
    let (_, b, _) = query.get(entity).unwrap();

    // Alternative 2 (using tuple item indexing)
    let b = query.get(entity).unwrap().1;
}

System::type_id Consistency #

ECS

If you use System::type_id() on function systems (exclusive or not), ensure you are comparing its value to other System::type_id() calls, or IntoSystem::system_type_id().

This code wont require any changes, because IntoSystem’s are directly compared to each other.

fn test_system() {}

let type_id = test_system.type_id();

// ...

// No change required
assert_eq!(test_system.type_id(), type_id);

Likewise, this code wont, because System’s are directly compared.

fn test_system() {}

let type_id = IntoSystem::into_system(test_system).type_id();

// ...

// No change required
assert_eq!(IntoSystem::into_system(test_system).type_id(), type_id);

The below does require a change, since you’re comparing a System type to a IntoSystem type.

fn test_system() {}

// 0.12
assert_eq!(test_system.type_id(), IntoSystem::into_system(test_system).type_id());

// 0.13
assert_eq!(test_system.system_type_id(), IntoSystem::into_system(test_system).type_id());

System Stepping implemented as Resource #

ECS
Editor
App
Diagnostics

Add a call to Schedule::set_label() for any custom Schedule. This is only required if the Schedule will be stepped

Make the MapEntities trait generic over Mappers, and add a simpler EntityMapper #

ECS
Scenes
  • The existing EntityMapper (notably used to replicate Scenes across different Worlds) has been renamed to SceneEntityMapper
  • The MapEntities trait now works with a generic EntityMapper instead of the specific struct EntityMapper. Calls to fn map_entities(&mut self, entity_mapper: &mut EntityMapper) need to be updated to fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M)
  • The new trait EntityMapper has been added to the prelude

Async channel v2 #

ECS
Tasks

The PipelinedRendering plugin is no longer exported on wasm. If you are including it in your wasm builds you should remove it.

#[cfg(not(target_arch = "wasm32"))]
app.add_plugins(bevy_render::pipelined_rendering::PipelinedRenderingPlugin);

Add First/Pre/Post/Last schedules to the Fixed timestep #

ECS
Time

Usage of RunFixedUpdateLoop should be renamed to RunFixedMainLoop.

ECS
Utils
  • Uses of bevy::utils::{EntityHash, EntityHasher, EntityHashMap, EntityHashSet} now have to be imported from bevy::ecs::entity::hash.
  • Uses of EntityHashMap no longer have to specify the first generic parameter. It is now hardcoded to always be Entity.

Move Circle Gizmos to Their Own File #

Gizmos
  • Change gizmos::CircleBuilder to gizmos::circles::Circle2dBuilder
  • Change gizmos::Circle2dBuilder to gizmos::circles::Circle2dBuilder

move gizmo arcs to their own file #

Gizmos

gizmos::Arc2dBuilder -> gizmos::arcs::Arc2dBuilder

Multiple Configurations for Gizmos #

Gizmos

GizmoConfig is no longer a resource and has to be accessed through GizmoConfigStore resource. The default config group is DefaultGizmoConfigGroup, but consider using your own custom config group if applicable.

Use Direction3d for gizmos.circle normal #

Gizmos

Pass a Direction3d for gizmos.circle normal, eg. Direction3d::new(vec).unwrap_or(default) or potentially Direction3d::new_unchecked(vec) if you know your vec is definitely normalized.

Rename "AddChild" to "PushChild" #

Hierarchy

The struct AddChild has been renamed to PushChild, and the struct AddChildInPlace has been renamed to PushChildInPlace.

Rename Input to ButtonInput #

Input

Users need to rename Input to ButtonInput in their projects.

Add window entity to TouchInput events #

Input

Add a window field when constructing or destructuring a TouchInput struct.

Add delta to CursorMoved event #

Input

You need to add delta to any manually created CursorMoved struct.

Remove Default impl for CubicCurve #

Math
  • Remove CubicCurve from any structs that implement Default.
  • Wrap CubicCurve in a new type and provide your own default.
#[derive(Deref)]
struct MyCubicCurve<P: Point>(pub CubicCurve<P>);

impl Default for MyCubicCurve<Vec2> {
    fn default() -> Self {
        let points = [[
            vec2(-1.0, -20.0),
            vec2(3.0, 2.0),
            vec2(5.0, 3.0),
            vec2(9.0, 8.0),
        ]];

        Self(CubicBezier::new(points).to_curve())
    }
}

Direction: Rename from_normalized to new_unchecked #

Math

Renamed Direction2d::from_normalized and Direction3d::from_normalized to new_unchecked.

Add Capsule2d primitive #

Math

Capsule is now Capsule3d. If you were using it for 2d you need to use Capsule2d

Rename RayTest to RayCast #

Math

RayTest2d and RayTest3d have been renamed to RayCast2d and RayCast3d

Use IntersectsVolume for breakout example collisions #

Physics

sprite::collide_aabb::collide and sprite::collide_aabb::Collision were removed.

// 0.12
let collision = bevy::sprite::collide_aabb::collide(a_pos, a_size, b_pos, b_size);
if collision.is_some() {
    // ...
}

// 0.13
let collision = Aabb2d::new(a_pos.truncate(), a_size / 2.)
    .intersects(&Aabb2d::new(b_pos.truncate(), b_size / 2.));
if collision {
    // ...
}

If you were making use collide_aabb::Collision, see the new collide_with_side function in the breakout example.

Add ReflectFromWorld and replace the FromWorld requirement on ReflectComponent and ReflectBundle with FromReflect #

Reflection
  • Existing uses of ReflectComponent::from_world and ReflectBundle::from_world will have to be changed to ReflectFromWorld::from_world.
  • Users of #[reflect(Component)] and #[reflect(Bundle)] will need to also implement/derive FromReflect.
  • Users of #[reflect(Component)] and #[reflect(Bundle)] may now want to also add FromWorld to the list of reflected traits in case their FromReflect implementation may fail.
  • Users of ReflectComponent will now need to pass a &TypeRegistry to its insert, apply_or_insert and copy methods.

Remove TypeUuid #

Reflection

Convert any uses of #[derive(TypeUuid)] with #[derive(TypePath] for more complex uses see the relevant documentation for more information.

Explicit color conversion methods #

Rendering

Color::from(Vec4) is now Color::rgba_from_array(impl Into<[f32; 4]>) Vec4::from(Color) is now Color::rgba_to_vec4(&self)

// 0.12
let color_vec4 = Vec4::new(0.5, 0.5, 0.5);
let color_from_vec4 = Color::from(color_vec4);

let color_array = [0.5, 0.5, 0.5];
let color_from_array = Color::from(color_array);

// 0.13
let color_vec4 = Vec4::new(0.5, 0.5, 0.5);
let color_from_vec4 = Color::rgba_from_array(color_vec4);

let color_array = [0.5, 0.5, 0.5];
let color_from_array = Color::rgba_from_array(color_array);

Add a depth_bias to Material2d #

Rendering

PreparedMaterial2d has a new depth_bias field. A value of 0.0 can be used to get the previous behavior.

Bind group layout entries #

Rendering

RenderDevice::create_bind_group_layout() doesn’t take a BindGroupLayoutDescriptor anymore. You need to provide the parameters separately

// 0.12
let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor {
    label: Some("post_process_bind_group_layout"),
    entries: &[
        BindGroupLayoutEntry {
            // ...
        },
    ],
});

// 0.13
let layout = render_device.create_bind_group_layout(
 "post_process_bind_group_layout",
    &[
        BindGroupLayoutEntry {
            // ...
        },
    ],
);

Swap material and mesh bind groups #

Rendering
  • Custom 2d and 3d mesh/material shaders should now use bind group 2 @group(2) @binding(x) for their bound resources, instead of bind group 1.
  • Many internal pieces of rendering code have changed so that mesh data is now in bind group 1, and material data is now in bind group 2. Semi-custom rendering setups (that don’t use the Material or Material2d APIs) should adapt to these changes.

light renderlayers #

Rendering

Lights no longer affect all RenderLayers by default, now like cameras and meshes they default to RenderLayers::layer(0). To recover the previous behaviour and have all lights affect all views, add a RenderLayers::all() component to the light entity.

Update to wgpu 0.18 #

Rendering
  • RenderPassDescriptor color_attachments (as well as RenderPassColorAttachment, and RenderPassDepthStencilAttachment) now use StoreOp::Store or StoreOp::Discard instead of a boolean to declare whether or not they should be stored.
  • RenderPassDescriptor now have timestamp_writes and occlusion_query_set fields. These can safely be set to None.
  • ComputePassDescriptor now have a timestamp_writes field. This can be set to None for now.
  • See the wgpu changelog for additional details

Keep track of when a texture is first cleared #

Rendering
  • Remove arguments to ViewTarget::get_color_attachment() and ViewTarget::get_unsampled_color_attachment().
  • Configure clear color on Camera instead of on Camera3d and Camera2d.
  • Moved ClearColor and ClearColorConfig from bevy::core_pipeline::clear_color to bevy::render::camera.
  • ViewDepthTexture must now be created via the new() method

Approximate indirect specular occlusion #

Rendering

Renamed PbrInput::occlusion to diffuse_occlusion, and added specular_occlusion.

Texture Atlas rework #

Rendering

The TextureAtlas asset that previously contained both the atlas layout and image handle was renamed to TextureAtlasLayout with the image handle portion moved to a separate Handle<Image> available from SpriteSheetBundle::texture or AtlasImageBundle::image.

TextureAtlasSprite was removed and replaced by a new component, TextureAtlas, which now holds the atlas index. The Sprite component can be used for flip_x, flip_y, custom_size, anchor, and color.

SpriteSheetBundle now uses a Sprite instead of a TextureAtlasSprite component and a TextureAtlas component instead of a Handle<TextureAtlaslayout>.

DynamicTextureAtlasBuilder::add_texture takes an additional &Handle<Image> parameter.

TextureAtlasLayout::from_grid no longer takes a Handle<Image> parameter.

TextureAtlasBuilder::finish now returns a Result<(TextureAtlasLayout, Image), TextureAtlasBuilderError>.

UiTextureAtlasImage was removed. The AtlasImageBundle is now identical to ImageBundle with an additional TextureAtlas.

  • Sprites
fn my_system(
  mut images: ResMut<Assets<Image>>,
-  mut atlases: ResMut<Assets<TextureAtlas>>,
+  mut atlases: ResMut<Assets<TextureAtlasLayout>>,
  asset_server: Res<AssetServer>
) {
    let texture_handle = asset_server.load("my_texture.png");
-   let layout = TextureAtlas::from_grid(texture_handle, Vec2::new(25.0, 25.0), 5, 5, None, None);
+   let layout = TextureAtlasLayout::from_grid(Vec2::new(25.0, 25.0), 5, 5, None, None);
    let layout_handle = atlases.add(layout);
    commands.spawn(SpriteSheetBundle {
-      sprite: TextureAtlasSprite::new(0),
-      texture_atlas: atlas_handle,
       // the new sprite initialization is covered by the `..default()` expression; however, it is added to showcase migration
+      sprite: Sprite::default()
+      atlas: TextureAtlas {
+         layout: layout_handle,
+         index: 0
+      },
+      texture: texture_handle,
       ..default()
     });
}
  • UI
fn my_system(
  mut images: ResMut<Assets<Image>>,
-  mut atlases: ResMut<Assets<TextureAtlas>>,
+  mut atlases: ResMut<Assets<TextureAtlasLayout>>,
  asset_server: Res<AssetServer>
) {
    let texture_handle = asset_server.load("my_texture.png");
-   let layout = TextureAtlas::from_grid(texture_handle, Vec2::new(25.0, 25.0), 5, 5, None, None);
+   let layout = TextureAtlasLayout::from_grid(Vec2::new(25.0, 25.0), 5, 5, None, None);
    let layout_handle = atlases.add(layout);
    commands.spawn(AtlasImageBundle {
-      texture_atlas_image: UiTextureAtlasImage {
-           index: 0,
-           flip_x: false,
-           flip_y: false,
-       },
-      texture_atlas: atlas_handle,
+      texture_atlas: TextureAtlas {
+         layout: layout_handle,
+         index: 0
+      },
+      image: UiImage {
+           texture: texture_handle,
+           flip_x: false,
+           flip_y: false,
+       },
       ..default()
     });
}
fn animate_sprite(
    time: Res<Time>,
    mut query: Query<(
        &AnimationIndices,
        &mut AnimationTimer,
-       &mut TextureAtlasSprite)>,
+       &mut TextureAtlas)>,
) {
-   for (indices, mut timer, mut sprite) in &mut query {
+   for (indices, mut timer, mut atlas) in &mut query {
        timer.tick(time.delta());
        if timer.just_finished() {
-           sprite.index = if sprite.index == indices.last {
+           atlas.index = if atlas.index == indices.last {
                indices.first
            } else {
-               sprite.index + 1
+               atlas.index + 1
            };
        }
    }
}

Exposure settings (adopted) #

Rendering
  • If using a Skybox or EnvironmentMapLight, use the new brightness and intensity controls to adjust their strength.
  • All 3D scenes will now have different apparent brightnesses due to Bevy implementing proper exposure controls. You will have to adjust the intensity of your lights and/or your camera exposure via the new Exposure component to compensate.

Make DynamicUniformBuffer::push accept an &T instead of T #

Rendering

Users of DynamicUniformBuffer::push now need to pass references to DynamicUniformBuffer::push (e.g. existing uniforms.push(value) will now become uniforms.push(&value))

Customizable camera main texture usage #

Rendering

Add main_texture_usages: Default::default() to your camera bundle.

optimize batch_and_prepare_render_phase #

Rendering

The trait GetBatchData no longer hold associated type Data and Filter get_batch_data query_item type from Self::Data to Entity and return Option<(Self::BufferData, Option<Self::CompareData>)> batch_and_prepare_render_phase should not have a query

Update to wgpu 0.19 and raw-window-handle 0.6 #

Rendering
  • bevy_render::instance_index::get_instance_index() has been removed as the webgl2 workaround is no longer required as it was fixed upstream in wgpu. The BASE_INSTANCE_WORKAROUND shaderdef has also been removed.

  • WebGPU now requires the new webgpu feature to be enabled. The webgpu feature currently overrides the webgl2 feature so you no longer need to disable all default features and re-add them all when targeting webgpu, but binaries built with both the webgpu and webgl2 features will only target the webgpu backend, and will only work on browsers that support WebGPU.

    • Places where you conditionally compiled things for webgl2 need to be updated because of this change, eg:
      • #[cfg(any(not(feature = "webgl"), not(target_arch = "wasm32")))] becomes #[cfg(any(not(feature = "webgl") ,not(target_arch = "wasm32"), feature = "webgpu"))]
      • #[cfg(all(feature = "webgl", target_arch = "wasm32"))] becomes #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
      • if cfg!(all(feature = "webgl", target_arch = "wasm32")) becomes if cfg!(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))
  • create_texture_with_data now also takes a TextureDataOrder. You can probably just set this to TextureDataOrder::default()

  • TextureFormat’s block_size has been renamed to block_copy_size

  • See the wgpu changelog for anything I might’ve missed: https://github.com/gfx-rs/wgpu/blob/trunk/CHANGELOG.md

  • wgpu now surfaces errors at instance creation time, which may have you run into this error (we’ve also seen it with nsight instead of EOSOverlay):

2024-01-27T02:11:58.491767Z ERROR wgpu_hal::vulkan::instance: GENERAL [Loader Message (0x0)]
        loader_get_json: Failed to open JSON file C:\Program Files (x86)\Epic Games\Launcher\Portal\Extras\Overlay\EOSOverlayVkLayer-Win32.json
2024-01-27T02:11:58.492046Z ERROR wgpu_hal::vulkan::instance:   objects: (type: INSTANCE, hndl: 0x1fbe55dc070, name: ?)
2024-01-27T02:11:58.492282Z ERROR wgpu_hal::vulkan::instance: GENERAL [Loader Message (0x0)]
        loader_get_json: Failed to open JSON file C:\Program Files (x86)\Epic Games\Launcher\Portal\Extras\Overlay\EOSOverlayVkLayer-Win64.json
2024-01-27T02:11:58.492525Z ERROR wgpu_hal::vulkan::instance:   objects: (type: INSTANCE, hndl: 0x1fbe55dc070, name: ?)

It just means that the program didn’t properly cleanup their registry keys on an update/uninstall, and vulkan uses those keys to load validation layers. The fix is to backup your registry, then remove the offending keys in "Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers".

RenderGraph Labelization #

Rendering

For Nodes and SubGraphs, instead of using hardcoded strings, you now pass labels, which can be derived with structs and enums.

// 0.12
#[derive(Default)]
struct MyRenderNode;
impl MyRenderNode {
    pub const NAME: &'static str = "my_render_node"
}

render_app
    .add_render_graph_node::<ViewNodeRunner<MyRenderNode>>(
        core_3d::graph::NAME,
        MyRenderNode::NAME,
    )
    .add_render_graph_edges(
        core_3d::graph::NAME,
        &[
            core_3d::graph::node::TONEMAPPING,
            MyRenderNode::NAME,
            core_3d::graph::node::END_MAIN_PASS_POST_PROCESSING,
        ],
    );

// 0.13
use bevy::core_pipeline::core_3d::graph::{Node3d, Core3d};

#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub struct MyRenderLabel;

#[derive(Default)]
struct MyRenderNode;

render_app
    .add_render_graph_node::<ViewNodeRunner<MyRenderNode>>(
        Core3d,
        MyRenderLabel,
    )
    .add_render_graph_edges(
        Core3d,
        (
            Node3d::Tonemapping,
            MyRenderLabel,
            Node3d::EndMainPassPostProcessing,
        ),
    );

If you still want to use dynamic labels, you can easily create those with tuple structs:

#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub struct MyDynamicLabel(&'static str);

SubGraphs #

  • bevy_core_pipeline::core_2d::graph: NAME -> Core2d
  • bevy_core_pipeline::core_3d::graph:NAME -> Core3d
  • bevy_ui::render: draw_ui_graph::NAME -> graph::SubGraphUi

Nodes #

  • bevy_core_pipeline::core_2d::graph:
    • node::MSAA_WRITEBACK -> Node2d::MsaaWriteback node::MAIN_PASS ->Node2d::MainPass node::BLOOM -> Node2d::Bloom node::TONEMAPPING -> Node2d::Tonemapping node::FXAA -> Node2d::Fxaa node::UPSCALING -> Node2d::Upscaling node::CONTRAST_ADAPTIVE_SHARPENING -> Node2d::ContrastAdaptiveSharpening node::END_MAIN_PASS_POST_PROCESSING -> Node2d::EndMainPassPostProcessing
  • bevy_core_pipeline::core_3d::graph:
    • node::MSAA_WRITEBACK -> Node3d::MsaaWriteback node::PREPASS -> Node3d::Prepass node::DEFERRED_PREPASS -> Node3d::DeferredPrepass node::COPY_DEFERRED_LIGHTING_ID -> Node3d::CopyDeferredLightingId node::END_PREPASSES -> Node3d::EndPrepasses node::START_MAIN_PASS -> Node3d::StartMainPass node::MAIN_OPAQUE_PASS -> Node3d::MainOpaquePass node::MAIN_TRANSMISSIVE_PASS -> Node3d::MainTransmissivePass node::MAIN_TRANSPARENT_PASS -> Node3d::MainTransparentPass node::END_MAIN_PASS -> Node3d::EndMainPass node::BLOOM -> Node3d::Bloom node::TONEMAPPING -> Node3d::Tonemapping node::FXAA -> Node3d::Fxaa node::UPSCALING -> Node3d::Upscaling node::CONTRAST_ADAPTIVE_SHARPENING -> Node3d::ContrastAdaptiveSharpening node::END_MAIN_PASS_POST_PROCESSING -> Node3d::EndMainPassPostProcessing
  • bevy_core_pipeline: taa::draw_3d_graph::node::TAA -> Node3d::Taa
  • bevy_pbr: draw_3d_graph::node::SHADOW_PASS -> NodePbr::ShadowPass ssao::draw_3d_graph::node::SCREEN_SPACE_AMBIENT_OCCLUSION -> NodePbr::ScreenSpaceAmbientOcclusion deferred::DEFERRED_LIGHTING_PASS -> NodePbr::DeferredLightingPass
  • bevy_render: main_graph::node::CAMERA_DRIVER -> graph::CameraDriverLabel
  • bevy_ui::render: draw_ui_graph::node::UI_PASS -> graph::Nodeui::UiPass

Gate diffuse and specular transmission behind shader defs #

Rendering

If you were using #ifdef STANDARDMATERIAL_NORMAL_MAP on your shader code, make sure to update the name to STANDARD_MATERIAL_NORMAL_MAP; (with an underscore between STANDARD and MATERIAL)

Async pipeline compilation #

Rendering

Match on the new Creating variant for exhaustive matches of CachedPipelineState

Mesh insert indices #

Rendering
  • Use Mesh::insert_indices or Mesh::with_inserted_indices instead of Mesh::set_indices / Mesh::with_indices.
  • If you have passed None to Mesh::set_indices or Mesh::with_indices you should use Mesh::remove_indices or Mesh::with_removed_indices instead.

wait for render app when main world is dropped #

Rendering

If you were using the pipelined rendering channels, MainToRenderAppSender and RenderToMainAppReceiver, they have been combined into the single resource RenderAppChannels.

Deprecate shapes in bevy_render::mesh::shape #

Rendering

Bevy has previously used rendering-specific types like UVSphere and Quad for primitive mesh shapes. These have now been deprecated to use the geometric primitives newly introduced in version 0.13.

  • bevy_render::mesh::Capsule is deprecated use bevy_math::primitives::dim3::Capsule3d instead;
  • bevy_render::mesh::Cylinder is deprecated use bevy_math::primitives::dim3::Cylinder instead;
  • bevy_render::mesh::Icosphere is deprecated use bevy_math::primitives::dim3::Sphere instead;
  • bevy_render::mesh::Cube is deprecated use bevy_math::primitives::dim3::Cuboid instead;
  • bevy_render::mesh::Box is deprecated use bevy_math::primitives::dim3::Cuboid instead;
  • bevy_render::mesh::Quad is deprecated use bevy_math::primitives::dim2::Rectangle instead;
  • bevy_render::mesh::Plane is deprecated use bevy_math::primitives::dim2::Plane2d or bevy_math::primitives::dim3::Plane3d instead;
  • bevy_render::mesh::RegularPolygon is deprecated use bevy_math::primitives::dim2::RegularPolygon instead;
  • bevy_render::mesh::Circle is deprecated use bevy_math::primitives::dim2::Circle instead;
  • bevy_render::mesh::Torus is deprecated use bevy_math::primitives::dim3::Torus instead;
  • bevy_render::mesh::UVSphere is deprecated use bevy_math::primitives::dim3::Sphere instead;

Some examples:

let before = meshes.add(shape::Box::new(5.0, 0.15, 5.0));
let after = meshes.add(Cuboid::new(5.0, 0.15, 5.0));

let before = meshes.add(shape::Quad::default());
let after = meshes.add(Rectangle::default());

let before = meshes.add(shape::Plane::from_size(5.0));
// The surface normal can now also be specified when using `new`
let after = meshes.add(Plane3d::default().mesh().size(5.0, 5.0));

let before = meshes.add(
    Mesh::try_from(shape::Icosphere {
        radius: 0.5,
        subdivisions: 5,
    })
    .unwrap(),
);
let after = meshes.add(Sphere::new(0.5).mesh().ico(5).unwrap());

Multithreaded render command encoding #

Rendering

RenderContext::new() now takes adapter info

Some render graph and Node related types and methods now have additional lifetime constraints.

Stop extracting mesh entities to the render world. #

Rendering

For efficiency reasons, some meshes in the render world may not have corresponding Entity IDs anymore. As a result, the entity parameter to RenderCommand::render() is now wrapped in an Option. Custom rendering code may need to be updated to handle the case in which no Entity exists for an object that is to be rendered.

New Exposure and Lighting Defaults (and calibrate examples) #

Rendering

The increased Exposure::ev100 means that all existing 3D lighting will need to be adjusted to match (DirectionalLights, PointLights, SpotLights, EnvironmentMapLights, etc). Or alternatively, you can adjust the Exposure::ev100 on your cameras to work nicely with your current lighting values. If you are currently relying on default intensity values, you might need to change the intensity to achieve the same effect. Note that in Bevy 0.12, point/spot lights had a different hard coded ev100 value than directional lights. In Bevy 0.13, they use the same ev100, so if you have both in your scene, the scale between these light types has changed and you will likely need to adjust one or both of them.

Many default lighting values were changed:

  • PointLight's default intensity is now 1_000_000.0
  • SpotLight's default intensity is now 1_000_000.0
  • DirectionalLight's default illuminance is now light_consts::lux::AMBIENT_DAYLIGHT (10_000.)
  • AmbientLight's default brightness is now 80.0

Unload render assets from RAM #

Rendering
Assets

Render assets can now be automatically unloaded from the main world after being prepared for rendering in the render world. This is controlled using RenderAssetUsages bitflags. To mimic old behavior and keep assets around in the main world, use RenderAssetUsages::default().

  • Mesh now requires a new asset_usage field. Set it to RenderAssetUsages::default() to mimic the previous behavior.

  • Image now requires a new asset_usage field. Set it to RenderAssetUsages::default() to mimic the previous behavior.

  • MorphTargetImage::new() now requires a new asset_usage parameter. Set it to RenderAssetUsages::default() to mimic the previous behavior.

  • DynamicTextureAtlasBuilder::add_texture() now requires that the TextureAtlas you pass has an Image with asset_usage: RenderAssetUsages::default(). Ensure you construct the image properly for the texture atlas.

  • The RenderAsset trait has significantly changed, and requires adapting your existing implementations.

    • The trait now requires Clone.
    • The ExtractedAsset associated type has been removed (the type itself is now extracted).
    • The signature of prepare_asset() is slightly different
    • A new asset_usage() method is now required (return RenderAssetUsages::default() to match the previous behavior).
  • Match on the new NoLongerUsed variant for exhaustive matches of AssetEvent.

Split Ray into Ray2d and Ray3d and simplify plane construction #

Rendering
Math

Ray has been renamed to Ray3d.

Ray creation #

// 0.12
Ray {
    origin: Vec3::ZERO,
    direction: Vec3::new(0.5, 0.6, 0.2).normalize(),
}

// 0.13
// Option 1:
Ray3d {
    origin: Vec3::ZERO,
    direction: Direction3d::new(Vec3::new(0.5, 0.6, 0.2)).unwrap(),
}

// Option 2:
Ray3d::new(Vec3::ZERO, Vec3::new(0.5, 0.6, 0.2))

Plane intersections #

// 0.12
let result = ray.intersect_plane(Vec2::X, Vec2::Y);

// 0.13
let result = ray.intersect_plane(Vec2::X, Plane2d::new(Vec2::Y));

Introduce AspectRatio struct #

Rendering
Math

Anywhere where you are currently expecting a f32 when getting aspect ratios, you will now receive a AspectRatio struct. this still holds the same value.

Include UI node size in the vertex inputs for UiMaterial. #

Rendering
UI

This change should be backwards compatible, using the new field is optional.

Optional ImageScaleMode #

Rendering
UI

Re-export futures_lite in bevy_tasks #

Tasks
  • Remove futures_lite from Cargo.toml.
[dependencies]
bevy = "0.12.0"
- futures-lite = "1.4.0"
  • Replace futures_lite imports with bevy::tasks::future.
- use futures_lite::poll_once;
+ use bevy::tasks::future::poll_once;

Rename TextAlignment to JustifyText. #

Text
  • TextAlignment has been renamed to JustifyText
  • TextBundle::with_text_alignment has been renamed to TextBundle::with_text_justify
  • Text::with_alignment has been renamed to Text::with_justify
  • The text_alignment field of TextMeasureInfo has been renamed to justification

Rename Time::<Fixed>::overstep_percentage() and Time::<Fixed>::overstep_percentage_f64() #

Time
  • Time::<Fixed>::overstep_percentage() has been renamed to Time::<Fixed>::overstep_fraction()
  • Time::<Fixed>::overstep_percentage_f64() has been renamed to Time::<Fixed>::overstep_fraction_f64()

Rename Timer::{percent,percent_left} to Timer::{fraction,fraction_remaining} #

Time
  • Timer::percent() has been renamed to Timer::fraction()
  • Timer::percent_left() has been renamed to Timer::fraction_remaining()

return Direction3d from Transform::up and friends #

Transform
Math

Callers of Transform::up() and similar functions may have to dereference the returned Direction3d to get to the inner Vec3.

Make clipped areas of UI nodes non-interactive #

UI

The clipped areas of UI nodes are no longer interactive.

RelativeCursorPosition is now calculated relative to the whole node’s position and size, not only the visible part. Its mouse_over method only returns true when the cursor is over an unclipped part of the node.

RelativeCursorPosition no longer implements Deref and DerefMut.

Create serialize feature for bevy_ui #

UI

If you want to use serialize and deserialize with types from bevy_ui, you need to use the feature serialize in your TOML

[dependencies.bevy]
features = ["serialize"]

Camera-driven UI #

UI

If the world contains more than one camera, insert TargetCamera(Entity) component to each UI root node, where Entity is the ID of the camera you want this specific UI tree to be rendered to. Test for any required adjustments of UI positioning and scaling.

// 0.12
commands.spawn(Camera3dBundle { ... });
commands.spawn(NodeBundle { ... });

// 0.13
let camera = commands.spawn(Camera3dBundle { ... }).id();
commands.spawn((TargetCamera(camera), NodeBundle { ... }));

Remove UiCameraConfig component from all cameras. If it was used to control UI visibility, insert Visibility component on UI root nodes instead.

Change Window scale factor to f32 (adopted) #

Windowing

If manipulating scale_factor, some conversion from f64 to f32 may be necessary.

Update winit dependency to 0.29 #

Windowing
Input

KeyCode changes #

Several KeyCode variants have been renamed and now represent physical keys on the keyboard, replacing ScanCode.

Common examples of the updated variants are as follows:

  • KeyCode::W -> KeyCode::KeyW
  • KeyCode::Up -> KeyCode::ArrowUp
  • KeyCode::Key1 -> KeyCode::Digit1

See the relevant documentation for more information.

ReceivedCharacter changes #

The char field of ReceivedCharacter is now a SmolStr, and could contain multiple characters. See these winit docs for details.

A simple workaround if you need a char is to call .chars().last().

It's now possible to use KeyEvent::logical_key's Character variant instead if you need consistent cross-platform behavior and/or a unified event stream with non-character events.

Remove CanvasParentResizePlugin #

Windowing

Remove uses of Window::fit_canvas_to_parent in favor of CSS properties, for example:

canvas {
  width: 100%;
  height: 100%;
}

Cleanup bevy winit #

Windowing

bevy::window::WindowMoved’s entity field has been renamed to window. This is to be more consistent with other windowing events.

Consider changing usage:

-window_moved.entity
+window_moved.window

Add name to bevy::window::Window #

Windowing

Window has a new name field for specifying the "window class name." If you don't need this, set it to None.

delete methods deprecated in 0.12 #

No area label

Many methods that were tagged as deprecated in 0.12 have now been removed. You should consider fixing the deprecation warnings before migrating to 0.13.

Renamed Accessibility plugin to AccessKitPlugin in bevy_winit #

No area label

bevy_winit::AccessibilityPlugin has been renamed to AccessKitPlugin.

Use TypeIdMap whenever possible #

No area label
  • TypeIdMap now lives in bevy_utils
  • DrawFunctionsInternal::indices now uses a TypeIdMap.

bevy_render: use the non-send marker from bevy_core #

No area label

If you were using bevy::render::view::NonSendMarker or bevy::render::view::window::NonSendMarker, use bevy::core::NonSendMarker instead