Bevy 0.4

Posted on December 19, 2020 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 )

A little over a month after releasing Bevy 0.3, and thanks to 66 contributors, 178 pull requests, and our generous sponsors, I'm happy to announce the Bevy 0.4 release on!

For those who don't know, Bevy is a refreshingly simple data-driven game engine built in Rust. You can check out The Quick Start Guide to get started. Bevy is also free and open source forever! You can grab the full source code on GitHub.

Here are some of the highlights from this release:

WASM + WebGL2 #

authors: @mrk-its

Bevy now has a WebGL2 render backend! @mrk-its has been hard at work building the Bevy WebGL2 Plugin and expanding bevy_render to meet the needs of the web. He also put together a nice website showcasing various Bevy examples and games running on the web.

I think the results speak for themselves:

Bevy WebGL2 Showcase #

webgl2 showcase

Cross Platform Main Function #

authors: @cart

On most supported Bevy platforms you can just use normal main functions (ex: Windows, MacOS, Linux, and Web). Here is the smallest possible Bevy app that runs on those platforms:

use bevy::prelude::*;

fn main() {

However some platforms (currently Android and iOS) require additional boilerplate. This arcane magic is error prone, takes up space, and isn't particularly nice to look at. Up until this point, Bevy users had to supply their own boilerplate ... but no more! Bevy 0.4 adds a new #[bevy_main] proc-macro, which inserts the relevant boilerplate for you. This is a big step toward our "write once run anywhere" goal.

This Bevy App has all the code required to run on Windows, MacOS, Linux, Android, iOS, and Web:

use bevy::prelude::*;

fn main() {

Live Shader Reloading #

authors: @yrns

Bevy can now update changes to shaders at runtime, giving you instant feedback without restarting your app. This video isn't sped up!

ECS Improvements #

authors: @cart

It wouldn't be a Bevy update without another round of ECS improvements!

Flexible ECS Parameters #

Prior versions of Bevy forced you to provide system parameters in a specific order:

/// This system followed the [Commands][Resources][Queries] order and compiled as expected
fn valid_system(mut commands: Commands, time: Res<Time>, query: Query<&Transform>) {

/// This system did not follow the required ordering, which caused compilation to fail
fn invalid_system(query: Query<&Transform>, mut commands: Commands, time: Res<Time>) {

Newbies would fall prey to this constantly. These completely arbitrary constraints were a quirk of the internal implementation. The IntoSystem trait was only implemented for specific orders. Supporting every order would have exponentially affected compile times. The internal implementation was also constructed with a famously complicated macro.

To resolve this, I completely rewrote how we generate systems. We now use a SystemParam trait, which we implement for each parameter type. This has a number of benefits:

  • Significantly Faster Compile Times: We're seeing a ~25% decrease in clean compile times
  • Use Any Parameter Order You Want: No more arbitrary order restrictions!
  • Easily Add New Parameters: It is now easy for us (and for users) to create new parameters. Just implement the SystemParam trait!
  • Simpler Implementation: The new implementation is much smaller and also way easier to maintain and understand.
// In Bevy 0.4 this system is now perfectly valid. Cool!
fn system(query: Query<&Transform>, commands: &mut Commands, time: Res<Time>) {

Notice that in Bevy 0.4, commands now look like commands: &mut Commands instead of mut commands: Commands.

Simplified Query Filters #

Up until now, Bevy's Query filters were intermingled with components:

fn system(query: Query<With<A, Without<B, (&Transform, Changed<Velocity>)>>>) {

Confused? You wouldn't be the first! You can interpret the query above as "give me immutable references to the Transform and Velocity components of all entities that have the A component, do not have the B component, and have a changed Velocity component".

First, the nesting of types via With / Without makes it very unclear whats going on. Additionally, it's hard to tell what the Changed<Velocity> parameter does. Is it just a filter? Does it also return a Velocity component? If so, is it immutable or mutable?

It made sense to break up these concepts. In Bevy 0.4, Query filters are separate from Query components. The query above looks like this:

// Query with filters
fn system(query: Query<(&Transform, &Velocity), (With<A>, Without<B>, Changed<Velocity>)>) {

// Query without filters
fn system(query: Query<(&Transform, &Velocity)>) {

This makes it much easier to tell what a Query is doing at a glance. It also makes for more composable behaviors. For example, you can now filter on Changed<Velocity> without actually retrieving the Velocity component.

And now that filters are a separate type, you can create type aliases for filters that you want to re-use:

type ChangedVelocity = (With<A>, Without<B>, Changed<Velocity>);

fn system(query: Query<(&Transform, &Velocity), ChangedVelocity>) {

System Inputs, Outputs, and Chaining #

Systems can now have inputs and outputs. This opens up a variety of interesting behaviors, such as system error handling:

fn main() {

fn result_system(query: Query<&Transform>) -> Result<()> {
  let transform = query.get(SOME_ENTITY)?;
  println!("found entity transform: {:?}", transform);

fn error_handler_system(In(result): In<Result<()>>, error_handler: Res<MyErrorHandler>) {
  if let Err(err) = result {

The System trait now looks like this:

// Has no inputs and no outputs
System<In = (), Out = ()>

// Takes a usize as input and return a f32
System<In = usize, Out = f32>

We use this feature in our new Schedule implementation.

Schedule V2 #

Bevy's old Schedule was nice. System registrations were easy to read and easy to compose. But it also had significant limitations:

  • Only one Schedule allowed
  • Very static: you were limited to using the tools we gave you:
    • stages are just lists of systems
    • stages are added to schedules
    • stages use hard-coded system runners
  • Couldn't switch between schedules at runtime
  • Couldn't easily support "fixed timestep" scenarios

To solve these problems, I wrote a new Schedule system from scratch. Before you get worried, these are largely non-breaking changes. The high level "app builder" syntax you know and love is still available:


Stage Trait #

Stages are now a trait. You can now implement your own Stage types!

struct MyStage;

impl Stage for MyStage {
    fn run(&mut self, world: &mut World, resources: &mut Resources) {
        // Do stage stuff here.
        // You have unique access to the World and Resources, so you are free to do anything

Stage Type: SystemStage #

This is basically a "normal" stage. You can add systems to it and you can decide how those systems will be executed (parallel, serial, or custom logic)

// runs systems in parallel (using the default parallel executor)
let parallel_stage =

// runs systems serially (in registration order)
let serial_stage =

// you can also write your own custom SystemStageExecutor
let custom_executor_stage =

Stage Type: [Schedule] #

You read that right! [Schedule] now implements the Stage trait, which means you can nest Schedules within other schedules:

let schedule = Schedule::default()
    .with_stage("update", SystemStage::parallel()
    .with_stage("nested", Schedule::default()
        .with_stage("nested_stage", SystemStage::serial()


Run Criteria #

You can add "run criteria" to any SystemStage or [Schedule].

// A "run criteria" is just a system that returns a `ShouldRun` result
fn only_on_10_criteria(value: Res<usize>) -> ShouldRun {
    if *value == 10 { 
    } else { 

    // this stage only runs when Res<usize> has a value of 10
    .add_stage_after(stage::UPDATE, "only_on_10_stage", SystemStage::parallel()
    // this stage only runs once
    .add_stage_after(stage::RUN_ONCE, "one_and_done", Schedule::default()

Fixed Timestep #

You can now run stages on a "fixed timestep".

// this stage will run once every 0.4 seconds
app.add_stage_after(stage::UPDATE, "fixed_update", SystemStage::parallel()

This builds on top of ShouldRun::YesAndLoop, which ensures that the schedule continues to loop until it has consumed all accumulated time.

Check out the excellent "Fix Your Timestep!" article if you want to learn more about fixed timesteps.

Typed Stage Builders #

Now that stages can be any type, we need a way for Plugin to interact with arbitrary stage types:

    // this "high level" builder pattern still works (and assumes that the stage is a SystemStage)
    // this "low level" builder is equivalent to add_system()
    .stage(stage::UPDATE, |stage: &mut SystemStage|
    // this works for custom stage types too
    .stage(MY_CUSTOM_STAGE, |stage: &mut MyCustomStage|

Deprecated For-Each Systems #

Prior versions of Bevy supported "for-each" systems, which looked like this:

// on each update this system runs once for each entity with a Transform component
fn system(time: Res<Time>, entity: Entity, transform: Mut<Transform>) {
    // do per-entity logic here

From now on, the system above should be written like this:

// on each update this system runs once and internally iterates over each entity
fn system(time: Res<Time>, query: Query<(Entity, &mut Transform)>) {
    for (entity, mut transform) in query.iter_mut() {
        // do per-entity logic here

For-each systems were nice to look at and sometimes saved some typing. Why remove them?

  1. For-each systems were fundamentally limited in a number of ways. They couldn't iterate removed components, filter, control iteration, or use multiple queries at the same time. This meant they needed to be converted to "query systems" as soon as those features were needed.
  2. Bevy should generally have "one way to do things". For-each systems were a slightly more ergonomic way to define a small subset of system types. This forced people to make a "design decision" when they shouldn't need to. It also made examples and tutorials inconsistent according to people's preferences for one or the other.
  3. There were a number of "gotchas" for newcomers that constantly come up in our support forums and confused newbies:
    • users expect &mut T queries to work in foreach systems (ex: fn system(a: &mut A) {}). These can't work because we require Mut<T> tracking pointers to ensure change tracking always works as expected. The equivalent Query<&mut A> works because we can return the tracking pointer when iterating the Query.
    • A "run this for-each system on some criteria" bug that was common enough that we had to cover it in the Bevy Book.
  4. They increased compile times. Removing for-each systems saved me about ~5 seconds on clean Bevy compiles)
  5. Their internal implementation required a complicated macro. This affected maintainability.

States #

authors: @cart

By popular demand, Bevy now supports States. These are logical "app states" that allow you to enable/disable systems according to the state your app is in.

States are defined as normal Rust enums:

enum AppState {

You then add them to your app as a resource like this:

// add a new AppState resource that defaults to the Loading state

To run systems according to the current state, add a StateStage:

app.add_stage_after(stage::UPDATE, STAGE, StateStage::<AppState>::default())

You can then add systems for each state value / lifecycle-event like this:

    .on_state_enter(STAGE, AppState::Menu, setup_menu.system())
    .on_state_update(STAGE, AppState::Menu, menu.system())
    .on_state_exit(STAGE, AppState::Menu, cleanup_menu.system())
    .on_state_enter(STAGE, AppState::InGame, setup_game.system())
    .on_state_update(STAGE, AppState::InGame, movement.system())

Notice that there are different "lifecycle events":

  • on_enter: Runs once when first entering a state
  • on_exit: Runs once when exiting a state
  • on_update: Runs exactly once on every run of the stage (after any on_enter or on_exit events have been run)

You can queue a state change from a system like this:

fn system(mut state: ResMut<State<AppState>>) {

Queued state changes get applied at the end of the StateStage. If you change state within a StateStage, the lifecycle events will occur in the same update/frame. You can do this any number of times (aka it will continue running state lifecycle systems until no more changes are queued). This ensures that multiple state changes can be applied within the same frame.

GLTF Improvements #

authors: @iwikal, @FuriouZz, @rod-salazar

Bevy's GLTF loader now imports Cameras. Here is a simple scene setup in Blender:


And here is how it looks in Bevy (the lighting is different because we don't import lights yet):


There were also a number of other improvements:

  • Pixel format conversion while importing images from a GLTF
  • Default material loading
  • Hierarchy fixes

Spawn Scenes as Children #

authors: @mockersf

Scenes can now be spawned as children like this:

        Transform::from_translation(Vec3::new(0.5, 0.0, 0.0)),
    .with_children(|parent| {

By spawning beneath a parent, this enables you to do things like translate/rotate/scale multiple instances of the same scene:


Dynamic Linking #

authors: @bjorn3, @cart

@bjorn3 discovered that you can force Bevy to dynamically link.

This significantly reduces iterative compile times. Check out how long it takes to compile a change made to the example with the Fast Compiles Config and dynamic linking:

fast dynamic

Time To Compile Change To 3d_scene Example (in seconds, less is better) #


We added a cargo feature to easily enable dynamic linking during development

# for a bevy app
cargo run --features bevy/dynamic

# for bevy examples
cargo run --features dynamic --example breakout

Just keep in mind that you should disable the feature when publishing your game.

Text Layout Improvements #

authors: @AlisCode, @tigregalis

Prior Bevy releases used a custom, naive text layout system. It had a number of bugs and limitations, such as the infamous "wavy text" bug:


The new text layout system uses glyph_brush_layout, which fixes the layout bugs and adds a number of new layout options. Note that the "Fira Sans" font used in the example has some stylistic "waviness" ... this isn't a bug:


Renderer Optimization #

authors: @cart

Bevy's render API was built to be easy to use and extend. I wanted to nail down a good API first, but that resulted in a number of performance TODOs that caused some pretty serious overhead.

For Bevy 0.4 I decided to resolve as many of those TODOs as I could. There is still plenty more to do (like instancing and batching), but Bevy already performs much better than it did before.

Incrementalize Everything #

Most of Bevy's high level render abstractions were designed to be incrementally updated, but when I was first building the engine, ECS change detection wasn't implemented. Now that we have all of these nice optimization tools, it makes sense to use them!

For the first optimization round, I incrementalized as much as I could:

  • Added change detection to RenderResourceNode, Sprites, and Transforms, which improved performance when those values don't change
  • Only sync asset gpu data when the asset changes
  • Share asset RenderResourceBindings across all entities that reference an asset
  • Mesh provider system now only updates mesh specialization when it needs to
  • Stop clearing bind groups every frame and remove stale bind groups every other frame
  • Cache unmatched render resource binding results (which prevents redundant computations per-entity per-frame)
  • Don't send render pass state change commands when the state has not actually changed

Frame Time to Draw 10,000 Static Sprites (in milliseconds, less is better) #


Frame Time to Draw 10,000 Moving Sprites (in milliseconds, less is better) #


Optimize Text Rendering (and other immediate rendering) #

Text Rendering (and anything else that used the SharedBuffers immediate-rendering abstraction) was extremely slow in prior Bevy releases. This was because the SharedBuffers abstraction was a placeholder implementation that didn't actually share buffers. By implementing the "real" SharedBuffers abstraction, we got a pretty significant text rendering speed boost.

Frame Time to Draw "text_debug" Example (in milliseconds, less is better) #


Mailbox Vsync #

Bevy now uses wgpu's "mailbox vsync" by default. This reduces input latency on platforms that support it.

Reflection #

authors: @cart

Rust has a pretty big "reflection" gap. For those who aren't aware, "reflection" is a class of language feature that enables you to interact with language constructs at runtime. They add a form of "dynamic-ness" to what are traditionally static language concepts.

We have bits and pieces of reflection in Rust, such as TypeId and type_name. But when it comes to interacting with datatypes ... we don't have anything yet. This is unfortunate because some problems are inherently dynamic in nature.

When I was first building Bevy, I decided that the engine would benefit from such features. Reflection is a good foundation for scene systems, Godot-like (or Unity-like) property animation systems, and editor inspection tools. I built the bevy_property and bevy_type_registry crates to fill these needs.

They got the job done, but they were custom-tailored to Bevy's needs, were full of custom jargon (rather than reflecting Rust language constructs directly), didn't handle traits, and had a number of fundamental restrictions on how data could be accessed.

In this release we replaced the old bevy_property and bevy_type_registry crates with a new bevy_reflect crate. Bevy Reflect is intended to be a "generic" Rust reflection crate. I'm hoping it will be as useful for non-Bevy projects as it is for Bevy. We now use it for our Scene system, but in the future we will use it for animating Component fields and auto-generating Bevy Editor inspector widgets.

Bevy Reflect enables you to dynamically interact with Rust types by deriving the Reflect trait:

struct Foo {
    a: u32,
    b: Vec<Bar>,
    c: Vec<u32>,

struct Bar {
    value: String

// I'll use this value to illustrate `bevy_reflect` features
let mut foo = Foo {
    a: 1,
    b: vec![Bar { value: "hello world" }]
    c: vec![1, 2]

Interact with Fields Using Their Names #

assert_eq!(*foo.get_field::<u32>("a").unwrap(), 1);

*foo.get_field_mut::<u32>("a").unwrap() = 2;

assert_eq!(foo.a, 2);

Patch Your Types With New Values #

let mut dynamic_struct = DynamicStruct::default();
dynamic_struct.insert("a", 42u32);
dynamic_struct.insert("c", vec![3, 4, 5]);


assert_eq!(foo.a, 42);
assert_eq!(foo.c, vec![3, 4, 5]);

Look Up Nested Fields Using "Path Strings" #

let value = *foo.get_path::<String>("b[0].value").unwrap();
assert_eq!(value.as_str(), "hello world");

Iterate Over Struct Fields #

for (i, value: &Reflect) in foo.iter_fields().enumerate() {
    let field_name = foo.name_at(i).unwrap();
    if let Ok(value) = value.downcast_ref::<u32>() {
        println!("{} is a u32 with the value: {}", field_name, *value);

Automatically Serialize And Deserialize With Serde #

This doesn't require manual Serde impls!

let mut registry = TypeRegistry::default();

let serializer = ReflectSerializer::new(&foo, &registry);
let serialized = ron::ser::to_string_pretty(&serializer, ron::ser::PrettyConfig::default()).unwrap();

let mut deserializer = ron::de::Deserializer::from_str(&serialized).unwrap();
let reflect_deserializer = ReflectDeserializer::new(&registry);
let value = reflect_deserializer.deserialize(&mut deserializer).unwrap();
let dynamic_struct = value.take::<DynamicStruct>().unwrap();

/// reflect has its own partal_eq impl

Trait Reflection #

You can now call a trait on a given &dyn Reflect reference without knowing the underlying type! This is a form of magic that should probably be avoided in most situations. But in the few cases where it is completely necessary, it is very useful:

struct MyType {
    value: String,

impl DoThing for MyType {
    fn do_thing(&self) -> String {
        format!("{} World!", self.value)

pub trait DoThing {
    fn do_thing(&self) -> String;

// First, lets box our type as a Box<dyn Reflect>
let reflect_value: Box<dyn Reflect> = Box::new(MyType {
    value: "Hello".to_string(),

This means we no longer have direct access to MyType or it methods. We can only call Reflect methods on reflect_value. What if we want to call `do_thing` on our type? We could downcast using reflect_value.get::<MyType>(), but what if we don't know the type at compile time?

// Normally in rust we would be out of luck at this point. Lets use our new reflection powers to do something cool!
let mut type_registry = TypeRegistry::default()

The #[reflect] attribute we put on our DoThing trait generated a new `ReflectDoThing` struct, which implements TypeData. This was added to MyType's TypeRegistration.

let reflect_do_thing = type_registry

// We can use this generated type to convert our `&dyn Reflect` reference to an `&dyn DoThing` reference
let my_trait: &dyn DoThing = reflect_do_thing.get(&*reflect_value).unwrap();

// Which means we can now call do_thing(). Magic!
println!("{}", my_trait.do_thing());

3D Texture Assets #

authors: @bonsairobo

The Texture asset now has support for 3D textures. The new example illustrates how to load a 3d texture and sample from each "layer".


Logging and Profiling #

authors: @superdump, @cart

Bevy finally has built in logging, which is now enabled by default via the new LogPlugin. We evaluated various logging libraries and eventually landed on the new tracing crate. tracing is a structured logger that handles async / parallel logging well (perfect for an engine like Bevy), and enables profiling in addition to "normal" logging.

The LogPlugin configures each platform to log to the appropriate backend by default: the terminal on desktop, the console on web, and Android Logs / logcat on Android. We built a new Android tracing backend because one didn't exist yet.

Logging #

Bevy's internal plugins now generate tracing logs. And you can easily add logs to your own app logic like this:

// these are imported by default in bevy::prelude::*
trace!("very noisy");
debug!("helpful for debugging");
info!("helpful information that is worth printing by default");
warn!("something bad happened that isn't a failure, but thats worth calling out");
error!("something failed");

These lines result in pretty-printed terminal logs:


tracing has a ton of useful features like structured logging and filtering. Check out their documentation for more info.

Profiling #

We have added the option to add "tracing spans" to all ECS systems by enabling the trace feature. We also have built in support for the tracing-chrome extension, which causes Bevy to output traces in the "chrome tracing" format.

If you run your app with cargo run --features bevy/trace,bevy/trace_chrome you will get a json file which can be opened in Chrome browsers by visiting the chrome://tracing url:


@superdump added support for those nice "span names" to upstream tracing_chrome.


authors: @mockersf, @blunted2night, @cart

Bevy now handles HIDPI / Retina / high pixel density displays properly:

  • OS-reported pixel density is now taken into account when creating windows. If a Bevy App asks for a 1280x720 window on a 2x pixel density display, it will create a window that is 2560x1440
  • Window width/height is now reported in "logical units" (1280x720 in the example above). Physical units are still available using the window.physical_width() and window.physical_height() methods.
  • Window "swap chains" are created using the physical resolution to ensure we still have crisp rendering (2560x1440 in the example above)
  • Bevy UI has been adapted to handle HIDPI scaling correctly

There is still a bit more work to be done here. While Bevy UI renders images and boxes at crisp HIDPI resolutions, text is still rendered using the logical resolution, which means it won't be as crisp as it could be on HIDPI displays.

Timer Improvements #

authors: @amberkowalski, @marcusbuffett, @CleanCut

Bevy's Timer component/resource got a number of quality-of-life improvements: pausing, field accessor methods, ergonomics improvements, and internal refactoring / code quality improvements. Timer Components also no longer tick by default. Timer resources and newtyped Timer components couldn't tick by default, so it was a bit inconsistent to have the (relatively uncommon) "unwrapped component Timer" auto-tick.

The timer API now looks like this:

struct MyTimer {
    timer: Timer,

fn main() {
        .add_resource(MyTimer {
            // a five second non-repeating timer
            timer: Timer::from_seconds(5.0, false),

fn timer_system(time: Res<Time>, my_timer: ResMut<MyTimer>) {
    if my_timer.timer.tick(time.delta_seconds()).just_finished() {
        println!("five seconds have passed");

Task System Improvements #

authors: @aclysma

@aclysma changed how Bevy Tasks schedules work, which increased performance in the example game by ~20% and resolved a deadlock when a Task Pool is configured to only have one thread. Tasks are now executed on the calling thread immediately when there is only one task to run, which cuts down on the overhead of moving work to other threads / blocking on them to finish.

Apple Silicon Support #

authors: @frewsxcv, @wyhaya, @scoopr

Bevy now runs on Apple silicon thanks to upstream work on winit (@scoopr) and coreaudio-sys (@wyhaya). @frewsxcv and @wyhaya updated Bevy's dependencies and verified that it builds/runs on Apple's new chips.

New Examples #

Bevy Contributors #

author: @karroffel

@karroffel added a fun example that represents each Bevy contributor as a "Bevy Bird". It scrapes the latest contributor list from git.


BevyMark #

author: @robdavenport

A "bunnymark-style" benchmark illustrating Bevy's sprite rendering performance. This was useful when implementing the renderer optimizations mentioned above.


Change Log #

Added #

Changed #

Fixed #

Contributors #

A huge thanks to the 66 contributors that made this release (and associated docs) possible!

  • @0x6273
  • @aclysma
  • @ak-1
  • @alec-deason
  • @AlisCode
  • @amberkowalski
  • @bjorn3
  • @blamelessgames
  • @blunted2night
  • @bonsairobo
  • @cart
  • @CleanCut
  • @ColdIce1605
  • @dallenng
  • @e00E
  • @easynam
  • @frewsxcv
  • @FuriouZz
  • @Git0Shuai
  • @iMplode-nZ
  • @iwikal
  • @jcornaz
  • @Jerald
  • @joshuajbouw
  • @julhe
  • @karroffel
  • @Keats
  • @Kurble
  • @lassade
  • @lukors
  • @marcusbuffett
  • @marius851000
  • @memoryruins
  • @MGlolenstine
  • @milkybit
  • @MinerSebas
  • @mkhan45
  • @mockersf
  • @Moxinilian
  • @mrk-its
  • @mvlabat
  • @nic96
  • @no1hitjam
  • @octtep
  • @OptimisticPeach
  • @Plecra
  • @PrototypeNM1
  • @rmsthebest
  • @RobDavenport
  • @robertwayne
  • @rod-salazar
  • @sapir
  • @sburris0
  • @sdfgeoff
  • @shirshak55
  • @smokku
  • @steveyen
  • @superdump
  • @SvenTS
  • @tangmi
  • @thebluefish
  • @Tiagojdferreira
  • @tigregalis
  • @toothbrush7777777
  • @Veykril
  • @yrns