Bevy 0.8

Posted on July 30, 2022 by Carter Anderson ( A silhouette of a figure with cat ears waving a tentacle, or Octocat: GitHub's mascot and logo @cart A vector art of a grey bird flying; former logo of X (formerly Twitter) @cart_cart A triangle pointing right in a rounded rectangle; Youtube's logo cartdev )

Thanks to 130 contributors, 461 pull requests, community reviewers, and our generous sponsors, I'm happy to announce the Bevy 0.8 release on crates.io!

For those who don't know, Bevy is a refreshingly simple data-driven game engine built in Rust. You can check out our Quick Start Guide to try it today. It's free and open source forever! You can grab the full source code on GitHub. Check out Bevy Assets for a collection of community-developed plugins, games, and learning resources.

To update an existing Bevy App or Plugin to Bevy 0.8, check out our 0.7 to 0.8 Migration Guide.

Since our last release a few months ago we've added a ton of new features, bug fixes, and quality of life tweaks, but here are some of the highlights:

  • New Material System: Custom shaders are now much easier to define, thanks to the new Material trait and AsBindGroup derive.
  • Camera-driven Rendering: Each Camera now configures what it renders and how it renders it. Easily layer camera renders on top of each other, do split screen, or render to a texture in just a few lines of code.
  • Built-in Shader Modularization: Many built-in shader types and functions are now importable. Notably, custom shaders can now import the PBR shader logic
  • Spot Lights: A new light type that emits light in a cone shape from a fixed point.
  • Visibility Inheritance: Hiding an entity now also hides all of its descendants in the hierarchy.
  • Upgraded to wgpu 0.13: Uses a new, more ergonomic WGSL shader syntax.
  • Automatic Mesh Tangent Generation: If tangents are missing for a mesh, generate them with mikktspace.
  • Renderer Optimizations: Parallel frustum culling and unstable sorts for unbatched render phases yielded some big wins!
  • Scene Bundle: Easily spawn scenes using a normal Bevy bundle and extend them with new components and children.
  • Scripting and Modding Progress: Untyped ECS APIs: A step toward 3rd party scripting language support! Interact with Bevy ECS internals directly via pointers.
  • ECS Query Ergonomics and Usability: Queries now implement IntoIter and mutable queries can be converted to immutable queries.
  • ECS Internals Refactors: Sweeping changes to Bevy ECS internals that make it simpler, safer, and easier to maintain.
  • Reflection Improvements: Support for reflecting more types, ECS resource reflection, untyped reflection, improved internals.
  • Hierarchy Commands: Hierarchy updates now use "transactional commands" to ensure hierarchy consistency at all times.
  • Bevy UI Now Uses Taffy: We've swapped to (and help maintain) a collaborative fork of the now abandoned Stretch UI layout library. Exponential blow-up bug begone!

New Material System #

authors: @cart, @Wrapperup, @johanhelsing

Bevy has a brand new Material system that makes defining custom shaders a breeze. Bevy's previous material system required hundreds of lines of "mid-level" boilerplate. This was never the long term plan, just an intermediate step. In Bevy 0.8, custom shader materials are as simple as this:

#[derive(AsBindGroup, TypeUuid, Clone)]
#[uuid = "f690fdae-d598-45ab-8225-97e2a3f056e0"]
pub struct CoolMaterial {
    #[uniform(0)]
    color: Color,
    #[texture(1)]
    #[sampler(2)]
    color_texture: Handle<Image>,
}

// The Material trait has many optional functions for configuration.
// In this case, all we need to set is a fragment shader. Ergonomics!
impl Material for CoolMaterial {
    fn fragment_shader() -> ShaderRef {
        "cool_material.wgsl".into()
    }
}

And the cool_material.wgsl shader:

struct CoolMaterial {
    color: vec4<f32>,
};

@group(1) @binding(0)
var<uniform> material: CoolMaterial;
@group(1) @binding(1)
var color_texture: texture_2d<f32>;
@group(1) @binding(2)
var color_sampler: sampler;

@fragment
fn fragment(
    #import bevy_pbr::mesh_vertex_output
) -> @location(0) vec4<f32> {
    return material.color * textureSample(color_texture, color_sampler, uv);
}

And just like that, we have a configurable shader material!

This can be used to create whatever effects you want! For example: @DGriffin91 made this cool "glowing orb" effect:

They also made a nice video tutorial outlining how to create this material.

This is thanks to the new AsBindGroup trait / derive, which does all of the hard work of converting the material to GPU-compatible datatypes, writing them to the GPU, and creating the final BindGroup. The AsBindGroup trait is powerful: it supports combining multiple fields into the same uniform binding, configuring texture binding types (2D, 3D, filtering, etc), and more. For details, check out the AsBindGroup docs.

All built-in materials, such as the PBR StandardMaterial have been ported to use this new system. We strive to make Bevy "internals" the same as user code, and this is no exception! Material also works seamlessly with our more advanced shader features, such as shader pipeline specialization.

There is also an equivalent Material2d trait, which enables custom materials in 2D.

Camera-Driven Rendering #

authors: @cart

In previous versions of Bevy, Cameras were selected and run as a part of one "global" RenderGraph. There could only be one "active" camera of a given type, and that camera could only render to one target. The only way to render from multiple perspectives at the same time was to manually extend the render graph with duplicate logic. This was full of complicated low-level renderer boilerplate that wasn't approachable for the average Bevy user.

In Bevy 0.8, each Camera now configures what it renders, how it renders, and what it renders to. An enabled camera will start a new run of a RenderGraph to a specified RenderTarget. The render graph defines modular render logic for the given camera and the render target defines the window or texture the graph will render to.

This makes scenarios that were previously hundreds (or thousands) of lines of code as simple as setting a few fields on the Camera entity:

Render to Textures #

Rendering a Camera to a texture is now a single line of code:

camera.target = RenderTarget::Image(image_handle);

This enables too many scenarios to count: portals, rendering UI to a texture and rendering it in 3d space, in-game security cameras, real-time player portraits rendered in UI widgets ... the sky is the limit!

Here is an example of a "portal effect":

This is accomplished by rendering a second camera to a texture, synchronizing its orientation with the main player's camera, and using that texture in the main camera's scene.

Split Screen #

Each Camera now has an optional Viewport, which if set will draw to a section of a RenderTarget instead of the whole. If you spawn two active cameras and set each camera's Viewport to draw to half of the window, you have simple, painless split screen!

split screen

Layered Rendering #

Cameras can now be layered on top of each other using the new "camera priority" field:

// This camera defaults to priority 0 and is rendered "first" / "at the back" 
commands.spawn_bundle(Camera3dBundle::default());
commands.spawn_bundle(Camera3dBundle {
    camera_3d: Camera3d {
        // don't clear the color while rendering this camera
        clear_color: ClearColorConfig::None,
        ..default()
    },
    camera: Camera {
        // renders after / on top of the main camera
        priority: 1,
        ..default()
    },
    ..default()
});

"Priority" determines the order cameras are drawn in. The "portal" effect in the render to textures example above uses priority to render the "portal" camera first, ensuring it is ready to be used by the main camera.

Here is a simple example of two cameras rendering to the same window:

camera layering

This can be used for things like "custom UI passes", "minimaps", etc.

Ergonomic Target Size Access #

Cameras now store their RenderTarget size locally, which makes retrieving the size much simpler:

// Much nicer than needing to look up the size on the target Window or Image manually,
// like you had to in previous Bevy versions!
let target_size = camera.logical_target_size();
let viewport_size = camera.logical_viewport_size();

This also means that converting world to screen coordinates for a camera is much easier than it used to be:

// Bevy 0.7 (old)
camera.world_to_screen(windows, images, camera_transform, world_position);

// Bevy 0.8 (new)
camera.world_to_viewport(camera_transform, world_position);

New Camera Bundles #

The old OrthographicCameraBundle and PerspectiveCameraBundle have been replaced with Camera3dBundle and Camera2dBundle. In most cases migration should be as simple as replacing the old names with the new ones. 3D cameras default to "perspective" projections, but they can still be switched to orthographic using the new Projection component in the bundle.

No More CameraUiBundle! #

Bevy UI now no longer needs a separate camera entity to work. UI "just works" for all camera types and can be enabled or disabled per-camera using the UiCameraConfig component.

commands
    .spawn_bundle(Camera3dBundle::default())
    .insert(UiCameraConfig {
        show_ui: false,
    });

Custom Render Graphs #

The default 2D and 3D RenderGraphs for each Camera can be overridden by setting the CameraRenderGraph component:

commands.spawn_bundle(Camera3dBundle {
    camera_render_graph: CameraRenderGraph::new(some_custom_graph),
    ..default()
})

This enables you to draw the camera with whatever custom render logic you need! For example, you could replace the built in clustered-forward-rendering with deferred rendering. Note that this generally won't be required: most custom rendering scenarios will be covered by high-level Materials or extending the built-in render graphs. And using the default render graph will ensure maximum compatibility with other plugins.

Enabling / Disabling Cameras #

If a camera is "active", it will render to its RenderTarget. To activate or deactivate a camera, set the new is_active field:

camera.is_active = true;

RenderLayers #

Bevy's existing RenderLayers system can be used to tell a Camera to only render entities on specific layers. Camera-Driven Rendering pairs nicely with this feature. This enables rendering one set of entities to one camera and another set of entities to another camera. We've ported the RenderLayers system to all entities with Visibility, so this will all Just Work™.

Spotlights #

authors: @robtfm

Bevy now has a SpotLight entity type, which emits light in a cone shape from a point in space.

spotlight

Visibility Inheritance #

authors: @james7132, @cart

Visibility in entity hierarchies (using the Visibility component) now propagates down the hierarchy. This is hugely useful, as entities in a game often have many entities nested beneath them. A "player entity" is often made up of many pieces: the player sprite or mesh, what the player is wearing / holding, visual effects, etc.

Visibility inheritance means that you only need to hide the top level "player" entity in your code and everything beneath it will be hidden for you.

This "flight helmet" scene consists of many "pieces" nested under the main helmet entity. Hiding all of these "sub entities" is as now as easy as hiding the top level helmet entity.

fn hide_helmets(mut helmet_visibilities: Query<&mut Visibility, With<Helmet>>) {
    let mut helmet_visibility = helmet_visibilities.single_mut();
    helmet_visibility.is_visible = false;
}

In past versions of Bevy, you had to hide each piece manually!

The "inherited visibility" is computed in the PostUpdate stage and stored on the ComputedVisibility component. ComputedVisibility now has the following functions:

  • is_visible_in_hierarchy(): Whether or not the entity is visible according to "visibility inheritance".
  • is_visible_in_view(): Whether or not the entity is visible in any view. This is used for culling entities in cases like "frustum culling".
  • is_visible(): The canonical place to determine whether or not an entity will be drawn. Combines "view visibility" and "hierarchy visibility".

SpatialBundle and VisibilityBundle #

authors: @mockersf, @rparrett

With the addition of Visibility Inheritance comes the constraint that visibility propagation requires all elements in the hierarchy to have the appropriate visibility components. When constructing scenes, developers often want to group entities under parent "organizational" entities, which exist solely to group entities together, reposition them, and hide them as a unit. These "organizational" entities still require visibility components to propagate the Transform and Visibility to GlobalTransform and ComputedVisibility (respectively).

To make this easy, we've added a new SpatialBundle, which adds the components mentioned above. This allows the entity to configure and propagate visibility and transform data without incurring the cost of actually rendering it.

commands
    // This entity controls the position and visibility of the entities beneath it.
    .spawn_bundle(SpatialBundle {
        transform: Transform::from_xyz(10.0, 20.0, 30.0),
        visibility: Visibility {
            is_visible: true,
        },
        ..default()
    }).with_children(|parent| {
        parent
            .spawn_bundle(TableBundle::default())
            .spawn_bundle(ShopKeeperBundle::default())
            .spawn_bundle(PotionBundle::default());
    });

To ensure visibility and transforms are propagated, make sure the whole hierarchy (root -> leaf) has these components:

commands
    .spawn_bundle(SpatialBundle::default())
    .with_children(|parent| {
        parent
            .spawn_bundle(SpatialBundle::default())
            .with_children(|parent| {
                parent.spawn_bundle(SpatialBundle::default());
            });
    });

If you know you don't need Transform propagation (or your entity already has those components), you can instead use the new VisibilityBundle, which only adds the components required for visibility propagation.

Built-in Shader Modularization #

authors: Rob Swain (@superdump)

In Bevy 0.8, we've started modularizing our built-in shaders. Notably, this means you can now import and run the built-in PBR shader / lighting logic:

#import bevy_pbr::utils
#import bevy_pbr::clustered_forward
#import bevy_pbr::lighting
#import bevy_pbr::shadows
#import bevy_pbr::pbr_types
#import bevy_pbr::pbr_functions

@fragment
fn fragment(in: FragmentInput) -> @location(0) vec4<f32> {
    var pbr_input: PbrInput = pbr_input_new();
    // set the base color to red
    pbr_input.material.base_color = vec4<f32>(1.0, 0.0, 0.0, 1.0);
    /* set other PbrInput fields here */
    return tone_mapping(pbr(pbr_input))
}

We've also modularized our mesh and view binding shader logic. When paired with the New Material System, users can now reasonably write custom PBR materials without re-defining all of the PBR logic in their shaders. The new Array Texture Example illustrates how to define a custom PBR shader material using these new APIs:

array texture

We also plan on evolving the user experience here. Now that we've broken everything up and modularized it, we'll work on reducing the amount of boilerplate required to extend this logic (cutting down on imports, removing the need to set all PbrInput fields, etc).

wgpu 0.13: New WGSL Shader Syntax #

authors: @mockersf, wgpu and naga contributors

We've updated to the latest and greatest wgpu version. wgpu 0.13 brings plenty of fixes and improvements, but the most visible change is the new more ergonomic WGSL "attribute" syntax:

Bindings now look like this:

// wgpu 0.12 (old)
[[group(1), binding(0)]]
var<uniform> material: Material;

// wgpu 0.13 (new)
@group(1) @binding(0)
var<uniform> material: Material;

Shader stage entry points and inputs now look like this:

// wgpu 0.12 (old)
[[stage(vertex)]]
fn vertex([[location(0)]] position: vec3<f32>, [[location(1)]] normal: vec3<f32>) -> VertexOutput {
}

// wgpu 0.13 (new)
@vertex
fn vertex(@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>) -> VertexOutput {
}

Much easier on the eyes! (and the fingers!)

Scene Bundle #

authors: @mockersf

In previous versions of Bevy scenes were spawned like this:

commands.spawn_scene(asset_server.load("some.gltf#Scene0"));

This worked, but it made it difficult to actually use the scene in practice. Repositioning the scene required either manually grabbing the scene's entity id and then repositioning it when it spawns, or creating a parent entity with the relevant transform components and "spawning the scene as a child". Additionally, it was challenging to extend scenes with new components.

In Bevy 0.8, we've added a SceneBundle, which brings scene spawning in line with our other entity construction APIs:

commands.spawn_bundle(SceneBundle {
    scene: asset_server.load("some.gltf#Scene0"),
    ..default()
})

When the scene loads, it will automatically spawn underneath the entity created by the spawn_bundle command. This makes it much easier to transform the scene and add new components at the "root":

commands
    .spawn_bundle(SceneBundle {
        scene: asset_server.load("player.gltf#Scene0"),
        transform: Transform::from_xyz(10.0, 20.0, 30.0),
        ..default()
    })
    .insert(Player::default());

Parallel Frustum Culling #

authors: @aevyrie

Bevy uses "frustum culling" to skip drawing entities that are outside of the camera's view. In Bevy 0.8, frustum culling is now done in parallel. When culling thousands of entities, this yields significant performance improvements:

frustum culling system time vs number of entities culled (lower is better) #

parallel frustum culling

Note that "parallel b" stand for "parallel batch size" (number of entities in each batch). We selected 1024 as our batch size in Bevy 0.8, as that performs better.

Automatic Mesh Tangent Generation #

authors: Rob Swain (@superdump), @jakobhellermann, @DJMcNab

Vertex tangents are used in tandem with normal maps to give meshes more detailed normals when rendering them. Some imported meshes have normal maps, but don't have vertex tangents calculated. Bevy can now automatically generate vertex tangents for Meshes that are missing them using the defacto industry-standard MikkTSpace library / algorithm (Godot, Unity, Unreal, and Blender all use this).

We have started maintaining our own fork of the gltf-rs/mikktspace crate so we can:

  • update dependencies at the speed required for Bevy;
  • start reining in the unsafe code, as it currently uses unsafe Rust code auto-generated from the original mikktspace.h written in C.

Default to Linear Texture Filtering #

authors: @aevyrie, @cart

Images in Bevy now use linear texture filtering by default, which is more in line with the rest of the gamedev ecosystem (Unity and Godot both default to filtered textures).

This means that textures that require unfiltered pixels (such as "pixel art" sprites) must override this default, either per-image:

image.sampler_descriptor = ImageSampler::nearest();

or globally using the new ImageSettings resource:

app.insert_resource(ImageSettings::default_nearest())

With that, we get crisp pixel art:

sprite

New GlobalTransform Matrix Representation #

authors: @HackerFoo

The internal representation of the GlobalTransform component (representing the "world space" transform for an entity) has been changed from a "similarity" (translation Vec3 / rotation Quat / scale Vec3) to an "affine 3D transform" (Mat3A and a Vec3 translation).

Notably, this allows for shear to be represented. Shear is a controversial topic. Engine and physics programmers tend to hate it. Artists tend to love it. Given that most artist tools and game engines support shear in their equivalent types, we believe it is important to provide this as an option.

ShaderType derive #

authors: @teoxoy

Bevy 0.8 now uses the ShaderType trait / derive (provided by the encase crate) to easily convert Rust data types to GPU-compatible shader data types.

// ShaderType requires each field to implement ShaderType,
// which Bevy's math types and Color type implement.
#[derive(ShaderType)]
struct SpriteData {
    position: Vec2,
    color: Color,
}

And on the WGSL shader side it looks like this:

struct SpriteData {
    position: vec2<f32>,
    color: vec4<f32>,
};

@group(1) @binding(0)
var<uniform> sprite_data: SpriteData;

This trait can be used in the new Material system if you need to define custom shader uniform or buffer bindings.

ShaderType replaces the AsStd140 and AsStd430 traits / derives (provided by the crevice crate) used in previous Bevy versions. This simplifies (and clarifies) the "how to transfer data to the GPU" story in Bevy, while also adding new features, such as support for unbounded Rust vecs (for storage buffers) and configurable dynamic offsets for uniform and storage buffers.

For example, Bevy's built-in lighting pipeline was adjusted to take advantage of this:

#[derive(ShaderType)]
pub struct GpuPointLightsStorage {
    #[size(runtime)]
    data: Vec<GpuPointLight>,
}

Render Phase Sorting Optimization #

authors: @james7132, Rob Swain (@superdump)

Bevy uses "render phases" to collect per-entity render logic for a render pass. These phases can be sorted to control draw order for a variety of reasons: back-to-front depth sorting for transparency correctness and front-to-back sorting for early fragment discarding during opaque rendering.

When possible, Bevy 0.8 now uses "unstable sorts" (currently "radix sort"), which yields a significant performance improvement:

many_cubes stress test "opaque phase" sort times (in milliseconds, less is better) #

unstable sort

Vertex Colors #

authors: @HackerFoo, @thebracket

Bevy's 2D and 3D pipelines and Materials now support vertex colors, if a given Mesh provides them. The PBR StandardMaterial and the 2D ColorMaterial build on this and will use the vertex colors if they are set:

vertex colors

Regular Polygon and Circle Mesh Primitives #

authors: @rparrett

Bevy now has Circle and RegularPolygon Mesh shapes:

shapes

let pentagon = RegularPolygon::new(10., 5);
let handle = meshes.add(Mesh::from(pentagon));
commands.spawn_bundle(ColorMesh2dBundle {
    mesh: handle.into(),
    ..default()
});

Scripting and Modding Progress: Untyped ECS APIs #

authors: @jakobhellermann

Bevy officially only supports Rust as the "one true way to define app logic". We have very good reasons for this and that philosophy likely won't change any time soon. But we do want to provide the tools needed for the community to build 3rd party scripting / modding plugins for their languages of choice.

When we released Bevy ECS V2, we intentionally built our internal ECS storage with these cases in mind. But we didn't expose public APIs that made it possible to interact with ECS data without normal Rust types.

Bevy 0.8 adds public "untyped" ECS APIs that enable retrieving lifetimed pointers to component and resource data using ComponentId instead of actual Rust types.

let health_ptr: Ptr = world.entity(player).get_by_id(heath_component_id).unwrap();

These, when combined with our reflection APIs, provide the tools needed to start building scripting support!

@jakobhellermann has started building their own JavaScript / TypeScript plugin for Bevy. Note that:

  1. This plugin is still very much a work in progress and is not ready to be used in projects.
  2. This is an unofficial community effort. Bevy will not be adding official JavaScript/TypeScript support.

Here is a TypeScript snippet from their repository that queries Bevy ECS data from the script:

const ballQuery = world.query({
    components: [ballId, transformId, velocityId],
});
for (const item of ballQuery) {
    let [ball, transform, velocity] = item.components;
    velocity = velocity[0];

    info(velocity.toString());
}

Query IntoIter #

authors: @TheRawMeatball

Bevy ECS Queries now implement the IntoIterator trait, which provides access to Rust's nicer iteration syntax:

// In previous Bevy versions, iter/iter_mut had to be called manually:
fn system(mut players: Query<&mut Player>) {
    for player in players.iter() {
    }
    
    for mut player in players.iter_mut() {
    }
}

// In Bevy 0.8, you have the option to use this syntax:
fn system(mut players: Query<&mut Player>) {
    for player in &players {
    }
    
    for mut player in &mut players {
    }
}

Query::iter_many #

authors: @devil-ira

It is now possible to pass a list of entities to a Query to iterate over using Query::iter_many, which is significantly more ergonomic than calling get(entity) on each individual entity:

#[derive(Component)]
struct BlueTeam {
    members: Vec<Entity>,
}

fn system(blue_team: Res<BlueTeam>, players: Query<&Player>) {
    info!("Blue Team Assemble!");
    for player in players.iter_many(&blue_team.members) {
        info!("{}", player.name);
    }    
}

There is also a Query::iter_many_mut, which provides similar functionality for mutable queries. But to ensure aliased mutability is not allowed, it does not implement iterator. Instead, use this pattern:

let mut iter = players.iter_many_mut(&blue_team.members);
while let Some(mut player) = iter.fetch_next() {
    player.score += 1;
}

Convert Mutable Queries to Read-only #

authors: @harudagondi

Mutable Queries can now be converted to their read-only versions, which makes it easier to build and use abstractions over queries:

fn system(mut players: Query<&mut Player>) {
    for mut player in &mut players {
        // mutate players here
    }

    log_players(players.to_readonly());
}

fn log_players(players: Query<&Players>) {
    for player in &players {
        info!("{player:?}");
    }
}

ECS "lifetimed pointers" #

authors: @TheRawMeatball, @BoxyUwU, @jakobhellermann

Bevy ECS has been refactored to use lifetimed, type-erased pointers instead of "raw pointers", which significantly improved the safety and legibility of our internals without compromising performance or flexibility.

From a high level, this enables us to "retain" the lifetime of World borrows throughout our internals while still using type-erased APIs to support scenarios like 3rd party scripting languages.

By retaining this lifetime, we can rely more on Rust's borrow checker to yell at us when we are doing something unsound. And, as it happens, this caught a number of soundness bugs!

ECS Query Internals Refactors #

authors: @BoxyUwU

@BoxyUwU has been hard at work refactoring our Query internals to be simpler and easier to read:

  • ReadOnlyFetch was replaced with ReadOnlyWorldQuery, moving this trait constraint "up a level", making it easier to express in the type system.
  • "QF Fetch generics" were removed from Query and QueryState methods and types in favor of WorldQuery usage, making filters and normal fetches consistent in the type system and simpler to express.

We have more changes in this vein planned for the next release. Bevy ECS internals are becoming considerably easier to understand!

ECS Optimizations #

authors: @DJMcNab, @james7132

Bevy ECS had a number of optimizations this time around:

  • @james7132 sped up entity moves between tables by reducing the number of copies conducted. For larger components, this is a huge win. The many_cubes stress test saw a ~16% speed boost in the prepare_uniform_components system, which relies heavily on commands / table moves.
  • @DJMcNab removed no-op drop function calls for ECS storage internals, which reduced the dropping time for the many_cubes stress test from ~150μs to ~80μs.
  • @james7132 changed ComponentSparseSet indices from usize to u32, which makes them use less space / making some operations more cache friendly. Sparse Set iteration became ~15% faster with this change.

Label Optimizations #

authors: @JoJoJet

Bevy relies on "labels" to identify things like systems, stages, and apps. This is useful for things like defining system dependencies. The traits SystemLabel, StageLabel, and AppLabel build on the same underlying "typed label" system. It enables developers to define custom labels while retaining type safety. Much better than using something like a string or an integer label!

In Bevy 0.8 we've optimized the internal representation of labels by removing boxing / trait objects in favor of a single cheap-to-copy-and-compare "system label id" type.

This new representation sped up schedule construction by ~30%!

time to prepare and compute schedule with 100 systems (in milliseconds, less is better) #

label bench

This change also removed a number of trait requirements from our label derives:

// Old (Bevy 0.7)
#[derive(SystemLabel, Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum MovementSystem {
    Velocity,
    Gravity,
}

// New (Bevy 0.8)
#[derive(SystemLabel, Clone)]
enum MovementSystem {
    Velocity,
    Gravity,
}

Hierarchy Commands #

authors: @james7132

Bevy's entity hierarchy system is based on two components: Parent (which points to an entity's parent) and Children (which points to a list of the entity's children). This separation is important, as it makes it easy and cheap to query for "hierarchy roots":

fn system(roots: Query<Entity, Without<Parent>>) { }

In past versions of Bevy, we built a complex system to "maintain" the integrity of the hierarchy. As Parent / Children components were added/removed/changed for an entity, we would do our best to sync everything up across the hierarchy.

However this meant that for a given point in time, the hierarchy could be "out of sync" and incorrect.

Our solution to this problem is to remove the deferred "hierarchy maintenance system" in favor of making hierarchy changes "transactional". Hierarchy changes are now done via transactional Commands, and directly modifying the component fields individually is no longer possible. This ensures that for a given point in time, the hierarchy is "correct".

For most Bevy developers this is a non-breaking change, as most hierarchies are already constructed using with_children commands:

commands
    .spawn_bundle(SpatialBundle::default())
    .with_children(|parent| {
        parent.spawn_bundle(SpriteBundle {
            texture: player_texture,
            ..default()
        });
        parent.spawn_bundle(SpriteBundle {
            texture: hat_texture,
            ..default()
        });
    });

However for logic that adds/removes child entities from parents at runtime, the following commands must be used:

// removes the given children from the parent
commands.entity(some_parent).remove_children(&[child1, child2]);
// pushes the given children to the "end" of the parent's Children list
commands.entity(some_parent).push_children(&[child3, child4]);
// inserts the given children into the parent's Children list at the given index 
commands.entity(some_parent).insert_children(1, &[child5]);

We've also added HierarchyEvent, which makes it possible for developers to track changes in the hierarchy.

There are still a couple of small holes to close, but staying on the "happy path" is much easier now:

  • removing only one of the components is possible (although heavily discouraged)
  • adding default values of only one of the components manually is still possible (although heavily discouraged)

We're discussing ways to resolve this class of problem, such as Archetype Rules / Invariants.

Bevy Reflection Improvements #

Bevy's "Rust reflection" system bevy_reflect is a core, foundational piece of Bevy's scene system. It provides a way to dynamically interact with Rust types at run-time without knowing their actual types. We've invested heavily in it this release to prepare for scripting support and scene system improvements.

bevy_reflect aims to be a "generic" Rust reflection system. It can be used without Bevy. We believe it fills a very real gap in the Rust ecosystem and we encourage the wider Rust community to use it (and contribute!).

"Untyped" Reflection #

authors: @jakobhellermann

The Reflect derives now automatically add a new ReflectFromPtr struct to the TypeRegistry for each reflected type. This enables using the new untyped ECS APIs in combination with the reflection system. This helps enable things like 3rd party scripting and modding.

Default Trait Reflection #

authors: @jakobhellermann

It is now possible to construct Reflect types using their Default trait impl, provided they register it as part of the Reflect derive:

#[derive(Reflect, Default)]
#[reflect(Default)]
struct MyStruct {
    foo: String,
    bar: usize,
}

let registration = type_registry.get(TypeId::of::<MyStruct>()).unwrap();
let reflect_default = registration.data::<ReflectDefault>().unwrap();
// This contains a MyStruct instance with default values
let my_struct: Box<dyn Reflect> = reflect_default.default();

This enables constructing components for entities without any compile time information, which is useful for dynamic scenarios like scripting and scenes.

Array Reflection #

authors: @NathanSWard, @MrGVSV

Bevy's reflection system now supports reflecting Rust arrays, which can be interacted with in a type-erased manner using the new Array trait.

#[derive(Reflect)]
struct Sprite {
    position: [f32; 2],
}

let sprite = Sprite {
    position: [0.1, 0.2],
};

let position = sprite.field("position").unwrap();
if let ReflectRef::Array(array) = position.reflect_ref() {
    let x = array.get(0).unwrap();
    assert_eq!(x.downcast_ref::<f32>(), Some(&0.1));
}

Static TypeInfo #

authors: @MrGVSV

The Reflect trait provides dynamic access to a specific instance of a type, but some scenarios (such as deserializing) benefit from knowing type information before we have an instance of a type. This opens the doors to building a Reflect-based serde alternative (or at the very least, a serde-less Bevy Scene deserializer).

Bevy 0.8 adds the ability to retrieve TypeInfo for any type implementing Reflect:

#[derive(Reflect)]
struct Foo {
    a: f32,
    b: usize,
}

let info = Foo::type_info();
if let TypeInfo::Struct(info) = info {
  assert!(info.is::<Foo>());
  assert_eq!(info.type_name(), std::any::type_name::<Foo>(),);
  assert!(info.field("a").unwrap().is::<f32>());
  assert!(info.field_at(1).unwrap().is::<usize>());
}

Note that type_info() returns &'static TypeInfo: it will lazily allocate and store TypeInfo the first time it is requested, then reuse that on each subsequent request.

Generic types also support TypeInfo:

#[derive(Reflect)]
struct Foo<T> {
    value: T
}

let info = Foo::<f32>::type_info();
if let TypeInfo::Struct(info) = info {
  assert!(info.field("value").unwrap().is::<f32>());
}

Resource Reflection #

authors: @Shatur

Bevy ECS resources can now be reflected:

#[derive(Reflect)]
#[reflect(Resource)]
struct Scoreboard {
    points: usize,
}

This registers a ReflectResource in the TypeRegistry entry for the type, enabling type-erased read/write operations for the resource in an ECS World.

Pretty Reflect Debug Formatting #

authors: @MrGVSV

"Debug printing" Reflect references now provides pretty / useful outputs.

Consider the following example:

#[derive(Reflect)]
struct Foo {
    a: f32,
    b: Bar,
}

#[derive(Reflect)]
struct Bar {
    x: String,
    y: usize,
}
let foo = Foo {
    a: 42.0,
    b: Bar {
        x: "hello".to_string(),
        y: 123,
    },
};

let foo_reflect: &dyn Reflect = &foo;
println!("{:#?}", foo_reflect);

In previous versions of Bevy, this would have printed:

Reflect(my_crate::Foo)

In Bevy 0.8, it prints:

my_crate::Foo {
    a: 42.0,
    b: my_crate::Bar {
        x: "hello",
        y: 123,
    },
}

Much better!

bevy_reflect Internal Refactors #

authors: @MrGVSV, @PROMETHIA-27, @jakobhellermann

Now that bevy_reflect is starting to get some serious investment and usage, we've invested time in reworking the internals to make them easier to maintain and extend:

  • Reflect Derive Reorganization: the derive logic was broken up into smaller, more maintainable pieces. "Metadata structs" were added to collect and organize derive inputs. (@MrGVSV)
  • The Reflect trait is now safe to implement: Soundness no longer hinges on the implementor doing the right thing, thanks to some changes to the Reflect interface. As a result, we were able to remove the unsafe keyword from the Reflect trait. (@PROMETHIA-27)
  • Serialize logic is now implemented using TypeRegistry type data like other reflected trait logic, rather than being hard-coded into Reflect impls. (@jakobhellermann)

Render World Extract #

authors: @DJMcNab, @cart

Note: The renderer APIs discussed here are for developers of advanced custom rendering features and core Bevy renderer developers. If this seems verbose or the intent is confusing, don't worry!

Bevy's new renderer "extracts" data needed for rendering from the "main" Bevy app, which enables parallel pipelined rendering. To facilitate this, in previous versions of Bevy we made the ECS RenderStage::Extract "special" (and more than a little bit weird). Systems in that stage ran on the "main" app world, but applied the system Commands to the "render" app world.

This accomplished the goal, but it:

  1. Was confusing: render feature developers had to "know" that this stage behaved differently from the other "normal" ECS stages in the schedule. Implicitly, Commands behaved differently and the ECS data access was "flipped". Using "normal" entity spawning APIs would not work as expected because the Commands parameter internally still used the main app's Entities collection.
  2. Prevented parallelism: directly modifying existing "render world" resources required exclusive access to ResMut<RenderWorld>, which prevented these systems from running in parallel. Making this access parallel required unnecessary allocations using Commands, which for "large" (or incrementally updated) extractions was inefficient.
// Old: Bevy 0.7 (limited parallelism)
fn extract_score(score: Res<Score>, mut render_world: ResMut<RenderWorld>) {
    *render_world.resource_mut::<ExtractedScore>() = ExtractedScore::from(score);
}

// Old: Bevy 0.7 (unnecessary allocations / prevents incremental updates)
fn extract_score(mut commands: Commands, score: Res<Score>) {
    commands.insert_resource(ExtractedScore::from(&score));
}

In Bevy 0.8, we've made the extract stage "normal". It runs directly on the "render world", just like the other render app stages. To "extract" data from the main app world, just wrap the relevant system parameters in the new Extract type to retrieve that parameter from the main app world instead:

// New: Bevy 0.8 (parallel and not weird!)
fn extract_score(mut extracted_score: ResMut<ExtractedScore>, score: Extract<Res<Score>>) {
    *extracted_score = ExtractedScore::from(&score);
}

The extract system is now parallel, the data access is consistent with other renderer ECS stages, and the intent of the system is clearer.

ExtractResource Trait and Plugin #

authors: Rob Swain (@superdump)

Some ECS resources have very simple extract logic:

fn extract_cool_color(mut extracted_cool_color: ResMut<CoolColor>, cool_color: Extract<Res<CoolColor>>) {
    *extracted_cool_color = cool_color.clone();
}

Rather than force developers to write this out manually, Bevy 0.8 now provides the ExtractResource trait / derive:

#[derive(ExtractResource, Clone)]
struct CoolColor {
    color: Color,
}

Then, just add the ExtractResourcePlugin<CoolColor> to your App and the resource will be automatically extracted.

ExtractResource can also be implemented manually if you need custom logic (or the type needs to change):

impl ExtractResource for ExtractedCoolColor {
    type Source = CoolColor;
    fn extract_resource(source: &CoolColor) -> Self {
        Self {
            color: source.color.as_rgba_linear(),
        }
    }
}

Taffy migration: a refreshed UI layout library #

authors: @alice-i-cecile, @jkelleyrtp, @Weibye, @TimJentzsch, @colepoirier

Bevy's moved off the abandoned stretch UI layout crate and onto its new community-maintained hard fork: taffy. Together with the Dioxus team, we've dramatically cleaned up the code base, solved a critical performance issue with deep UI trees and freshened up the docs.

We're looking forward to its continued maintenance and development as the team continues to improve its performance, fix bugs, and add support for alternative layout paradigms.

ECS Soundness / Correctness Improvements #

authors: @TheRawMeatball, @BoxyUwU, @SkiFire13, @jakobhellermann

Bevy ECS received a solid number of soundness and correctness bug fixes this release:

  • Removed EntityMut::get_unchecked: The only way to soundly use this API is already encapsulated within EntityMut::get. Therefore there was no reason to keep this unsafe API around.
  • Fixed unsoundness with some Or/AnyOf/Option component access: Previous Bevy versions allowed unsound versions of these queries. We now properly prevent those uses.
  • Improve soundness of CommandQueue: It is now sound to store Commands with padding or uninitialized bytes. A "use after move" case was also removed.
  • Fix some memory leaks detected by Miri: Miri detected a case that leaks in our BlobVec drop impl. That is now fixed.
  • Lint for missing SAFETY comments in bevy_ecs: We now require safety comments for unsafe code blocks in bevy_ecs.

As Bevy ECS matures, our bar for unsafe code blocks and soundness must also mature. Bevy ECS will probably never be 100% free of unsafe code blocks because we are modeling parallel data access that Rust cannot reason about without our help. But we are committed to both removing as much unsafe code as we can and improving the quality of the unsafe code that remains.

Android Progress: We aren't there yet, but we're closer! #

authors: @mockersf

Bevy now "kind of" runs on Android again!

However Android support is not ready yet. There are issues with how we manage render contexts that must be resolved that sometimes break rendering at startup and always break rendering when apps are minimized. Audio also doesn't work yet.

Here is the load_gltf Bevy example running on my Pixel 6:

android

That being said, this is an important step forward, as Bevy developers can now build, deploy, (and in some cases test) Bevy apps on Android!

If you are itching to test Bevy on mobile platforms, our iOS support is much more polished. Bevy developers have already started publishing Bevy-based iOS apps to the Apple App Store!

CI / Build System Improvements #

authors: @mockersf, @NiklasEi

As always, Bevy's CI had plenty of improvements this cycle:

  • Examples are now run in WASM when validating builds. Screenshots are taken and stored as part of the build outputs to ensure rendering works (@mockersf).
  • The Bevy examples are now run on a Windows VM once per day to ensure they aren't broken (@mockersf).
  • License files are now automatically added to all published crates (@NiklasEi).
  • There is now a workflow to automatically generate a PR with version number bumps for all Bevy crates (@mockersf).
  • To make the occasional nightly Rust breakage less disruptive, we've parameterized the nightly toolchain to make it easier to pin to a specific nightly. (@mockersf)

Example: Post Processing #

authors: @Vrixyz

We've added a new example that shows how to use the new Camera Driven Rendering and Shader Materials to build a "chromatic aberration" post processing effects using a full screen quad and "render to texture".

post processing

We plan on building more formal post processing APIs in future releases, but this approach is relatively straightforward and opens up a lot of doors. Much simpler than extending the lower level RenderGraph!

Run it by cloning the Bevy repo and running cargo run --release --example post_processing.

Example: Many Animated Foxes #

authors: @superdump

This is a fun stress test of our Skeletal Animation System that renders many animated foxes walking around in a circle.

many foxes

Run it by cloning the Bevy repo and running cargo run --release --example many_foxes.

WASM Example Build Tool #

authors: @mockersf

We've built a tool to make it easier to build and run Bevy's examples in your browser:

In the root of the Bevy repo, run the following command:

cargo run -p build-wasm-example -- lighting

This will run the cargo build and wasm-bindgen commands, and place the output in the examples/wasm folder. Run your favorite "local webserver" command there, such as python3 -m http.server and open that url in your browser!

Website: Improved Examples Page #

authors: @doup, @ickk

The Bevy WASM Examples pages have been reworked:

  • They now display loading bars while Bevy App content and assets load
  • The design / layout of the page is now much nicer

examples page

Bevy Org Changes #

As Bevy grows, we are constantly re-evaluating our development process to accommodate an ever-increasing volume of development. We're long past the point where I can make every decision and I've been slowly delegating responsibilities as my trust in the knowledge and experience of our community members grows.

This release cycle, there were two major changes to the Bevy Org:

  1. All existing developers with "delegated merge rights" (@mockersf and @alice-i-cecile) now have the title "maintainer".
  2. Rob Swain (@superdump) is now a maintainer. You will probably recognize them from their work on Bevy's renderer. They've been a veritable force of nature, driving forward clustered forward rendering, directional and point light shadows, visibility / frustum culling, alpha blending, compressed GPU textures, and more. Rob has demonstrated deep understanding of rendering algorithms, Bevy internals, and Bevy project direction. I'm certainly looking forward to what they build next!

Being a "maintainer" now works like this:

  1. Maintainers now have no (hard) limits on the "area" of PRs they can merge. No more limits on "docs only", "rendering only", etc. It is now each maintainers' responsibility to evaluate their areas of comfort. This does increase risk to an extent, but I think it's an important step to allow maintainers to grow organically.
  2. Maintainers can merge "relatively uncontroversial" PRs with at least two community approvals. Maintainers will collectively decide and enforce what is uncontroversial. Controversial PRs should be labeled with the S-Controversial label. Note that "two community approvals" is the minimum requirement. Maintainers are responsible for ensuring the appropriate people have approved a PR.
  3. Maintainers can merge "completely trivial" PRs without two community approvals. Some examples of "completely trivial": typo fixes, removing unused dependencies or code, and small "API consistency" fixes.
  4. Controversial Decision Making on a Timer: For all controversial PRs (including RFCs), if two maintainers approve, the PR can be labeled with a S-Ready-For-Final-Review label. As soon as this label is added and I have been pinged, a clock starts. If I have not responded with actionable feedback, a "snooze button" / "we aren't ready for this yet", or a veto within a month and a half (45 days), maintainers are free to merge the PR. This gives me the ability to dictate project direction in areas where that is important while also empowering maintainers to move things forward in parallel when that makes sense. We will be calibrating this approach as we go to make sure we strike the right balance between progress, quality, and consistent vision.
  5. I still reserve the right to veto all code changes and make unilateral code changes. This includes reverting "controversial changes" merged via (4).

We've used this process for most of the last cycle and I'm loving how it is working so far: more trust, more eyes on each decision, faster development velocity, no more trivial fixes sitting in limbo. I still get to enforce consistent vision when that matters, but the community is empowered to drive efforts forward.

What's Next? #

  • Post Processing: We have a lot of post processing work in the pipeline (some of it almost made it in to this release). The next release will make it easier to write post processing effects (thanks to intermediate HDR textures and a separate tonemapping step), and it will also include built-in effects like bloom and upscaling.
  • Asset Preprocessing: We will be investing heavily in our asset pipeline, with a focus on:
    1. Pre-processing assets to do expensive work "during development time", so Bevy Apps can be deployed with assets that are prettier, smaller, and/or faster to load.
    2. Enabling configuring assets with .meta files. For example, you could define a texture compression level, the filter it should use, or the target format.
  • Scene System Improvements: This release saw a lot of investment in Reflection. We can now build the next iteration of the scene system on top of it, with a nicer scene format, nested scenes, and improved workflows.
  • Bevy UI Improvements: In preparation for the visual Bevy Editor, we will be improving the capabilities and user experince of Bevy UI.
  • Bevy Jam #2: Bevy Jam #1 was a massive success: 74 entries, 1,618 ratings, and lots of good community vibes. Now that Bevy 0.8 is released, it's time to jam again! We'll release details on this soon. To stay in the loop, follow @BevyEngine on Twitter and join the Official Bevy Discord.

Support Bevy #

Sponsorships help make our work on Bevy sustainable. If you believe in Bevy's mission, consider sponsoring us ... every bit helps!

  • Carter Anderson (@cart): Full-time lead developer, project manager, and creator of Bevy. Focused on building out core engine systems, guiding project direction, and managing the community.
  • Alice Cecile (@alice-i-cecile): Full-time technical project manager, mad scientist, and documentation lead. While she regularly leads expeditions into new domains, ECS will always be home base.
  • François Mockers (@mockersf): CI whisperer. Making sure everything is running smoothly and improving Bevy one PR at a time.
  • Rob Swain (@superdump): Wielder of light. Turning data into shiny with massive parallelism. Currently hobby hacking so please donate to/sponsor the rest of the team. ❤️

Contributors #

A huge thanks to the 130 contributors that made this release (and associated docs) possible! In random order:

  • @spooky-th-ghost
  • @afonsolage
  • @mlodato517
  • @ostwilkens
  • @mattwilkinsonn
  • @geckoxx
  • @SarthakSingh31
  • @zicklag
  • @teoxoy
  • @nsarlin
  • @HackerFoo
  • @elijaharita
  • @inact1v1ty
  • @bjorn3
  • @james-j-obrien
  • @Daniikk1012
  • @nihohit
  • @Ku95
  • @superdump
  • @dilyankostov
  • @SkiFire13
  • @edwardvear
  • @jakobhellermann
  • @KDecay
  • @TheRawMeatball
  • @fadhliazhari
  • @colepoirier
  • @brandon-reinhart
  • @Davier
  • @ManevilleF
  • @Bobox214
  • @RalfJung
  • @robtfm
  • @its-danny
  • @alice-i-cecile
  • @MonaMayrhofer
  • @yilinwei
  • @MrGVSV
  • @ickshonpe
  • @bzm3r
  • @nagisa
  • @fgiordana
  • @DJMcNab
  • @oliverpauffley
  • @64kramsystem
  • @alteous
  • @maniwani
  • @hoshino111
  • @Kanabenki
  • @JoJoJet
  • @x-52
  • @djeedai
  • @BoxyUwU
  • @MDeiml
  • @GarettCooper
  • @hymm
  • @mockersf
  • @nebkor
  • @2ne1ugly
  • @BGR360
  • @SUPERCILEX
  • @CGMossa
  • @infmagic2047
  • @CleanCut
  • @YoshieraHuang
  • @kornelski
  • @mdickopp
  • @SpecificProtagonist
  • @PROMETHIA-27
  • @eiei114
  • @Hoidigan
  • @Wcubed
  • @adsick
  • @nicopap
  • @siph
  • @C-BJ
  • @tamasfe
  • @object71
  • @LegNeato
  • @Elabajaba
  • @bytemuck
  • @AronDerenyi
  • @makspll
  • @cryscan
  • @NiklasEi
  • @grace125
  • @NathanSWard
  • @IceSentry
  • @Vrixyz
  • @Piturnah
  • @its-justus
  • @dataphract
  • @thomas992
  • @Olexorus
  • @ShadowCurse
  • @LoipesMas
  • @ImDanTheDev
  • @johanhelsing
  • @wrapperup
  • @james7132
  • @rebelroad-reinhart
  • @SuperSamus
  • @manokara
  • @Nilirad
  • @NeoRaider
  • @thebracket
  • @sarkahn
  • @MrPicklePinosaur
  • @Shatur
  • @themasch
  • @devil-ira
  • @fluunke
  • @DGriffin91
  • @aevyrie
  • @henryksloan
  • @bwasty
  • @MiniaczQ
  • @rparrett
  • @komadori
  • @ChristopherBiscardi
  • @dtaralla
  • @Sheepyhead
  • @TethysSvensson
  • @Neopallium
  • @FraserLee
  • @cart
  • @Obdzen
  • @oddfacade
  • @CAD97
  • @XBagon

Full Change Log #

Added #

Changed #

Fixed #