Bevy 0.3

Posted on November 3, 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.2, and thanks to 59 contributors, 122 pull requests, and our generous sponsors, I'm happy to announce the Bevy 0.3 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 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:

Initial Android Support #

authors: @enfipy, @PrototypeNM1, @endragor, @naithar

You can try out the Bevy Android example by following the instructions here. While many things work, please note that this is very hot off the presses. Some features will work and others probably won't. Now is a great time to dive in and help us close the gaps!

android

This was a massive group effort that spanned multiple projects:

  • Bevy: rewrote bevy-glsl-to-spirv to support android / static libraries (@PrototypeNM1, @enfipy)
  • Bevy: bevy_asset backend using Android Asset Manager (@enfipy)
  • Bevy: Touch support (@naithar)
  • Bevy: Texture format fix (@enfipy)
  • Bevy: UI touch fixes, touch force, and android example (@enfipy)
  • Cpal: android audio support (@endragor)
  • android-ndk-rs / cargo-apk: fix to support Bevy project structure (@PrototypeNM1)

Initial iOS Support #

authors: @simlay, @MichaelHills, @Dash-L, @naithar

Bevy can now run on iOS!

You can try out the Bevy iOS example by following the instructions here. This one is also hot off the presses: some features will work and others probably won't.

This was another large group effort that spanned multiple projects:

  • Bevy: XCode Project / Example (@simlay with help from @MichaelHills)
  • Bevy: Runtime shader compilation using shaderc (@MichaelHills)
  • Bevy: Rodio upgrade (@Dash-L)
  • Bevy: Touch support (@naithar)
  • Winit: Fix iOS portrait view (@MichaelHills)
  • RustAudio: iOS support (@simlay and @MichaelHills)

Known issues:

WASM Asset Loading #

authors: @mrk-its (and ported to the new AssetIo by @cart)

@mrk-its has been hard at work on expanding Bevy's WASM support. In this release we landed WASM asset loading. You can now load assets when you publish to WASM just like you would on any other platform:

asset_server.load("sprite.png");

If the asset hasn't already been loaded, this will make a fetch() request to retrieve the asset over HTTP.

@mrk-its has also been building a custom WebGL2 bevy_render backend. It is already pretty usable, but its not quite ready yet. Expect more news on this soon!

Touch Input #

authors: @naithar

Bevy now has support for touches:

fn touch_system(touches: Res<Touches>) {
    // you can iterate all current touches and retrieve their state like this:
    for touch in touches.iter() {
        println!("active touch: {:?}", touch);
    }

    for touch in touches.iter_just_pressed() {
        println!("just pressed {:?}", touch);
    }

    for touch in touches.iter_just_released() {
        println!("just released {:?}", touch);
    }

    for touch in touches.iter_just_cancelled() {
        println!("just cancelled {:?}", touch);
    }
}

You can also consume raw touch events using the Events<TouchInput> resource.

Asset System Improvements #

authors: @cart

Asset Handle Reference Counting #

Assets are now automatically freed when their "handle reference count" reaches zero. This means you no longer need to think about freeing assets manually:

// Calling load() now returns a strong handle:
let handle = asset_server.load("sprite.png");

// Note that you no longer need to unwrap() loaded handles. Ergonomics for the win!

// Cloning a handle increases the reference count by one
let second_handle = handle.clone();

// Spawn a sprite and give it our handle
commands.spawn(SpriteComponents {
    material: materials.add(handle.into()),
    ..Default::default()
});

// Later in some other system:
commands.despawn(sprite_entity);

// There are no more active handles to "sprite.png", so it will be freed before the next update

Asset Loaders can now load multiple assets #

In past releases, AssetLoaders could only produce a single asset of a single type. In Bevy 0.3, they can now produce any number of assets for any type. The old behavior was extremely limiting when loading assets like GLTF files, which might produce many meshes, textures, and scenes.

Sub-Asset Loading #

Sometimes you only want to load a specific asset from an asset source. You can now load sub assets like this:

// Mesh0/Primitive0 references the first mesh primitive in "my_scene.gltf"
let mesh = asset_server.load("my_scene.gltf#Mesh0/Primitive0");

AssetIo Trait #

The AssetServer is now backed by the AssetIo trait. This allows us to load assets from whatever storage we want. This means on desktop we now load from the filesystem, on Android we use the Android Asset Manager, and on the web we make HTTP requests using the fetch() API.

Asset Dependencies #

Assets can now depend on other assets, which will automatically be loaded when the original asset is loaded. This is useful when loading something like a "scene" which might reference other asset sources. We utilize this in our new GLTF loader.

Removed AssetServer::load_sync() #

This might rustle some feathers, but AssetServer::load_sync() had to go! This API wasn't WASM friendly, encouraged users to block game execution for the sake of convenience (which causes "hitching"), and was incompatible with the new AssetLoader API. Asset loading is now always asynchronous. Users of load_sync() should instead load() their assets, check load status in their systems, and change game state accordingly.

GLTF Scene Loader #

authors: @cart

Up until this point, the GLTF loader was painfully limited. It could only load the first mesh with a single texture in a GLTF file. For Bevy 0.3, we took advantage of the asset system improvements to write a new GltfLoader that loads GLTF files as Bevy Scenes, along with all meshes and textures in the files.

Here's Bevy loading the Khronos Flight Helmet example, which consists of multiple meshes and textures!

flight helmet

Here is the complete code for a system that loads a GLTF file and spawns it as a scene:

fn load_gltf_system(mut commands: Commands, asset_server: Res<AssetServer>) {
    let scene_handle = asset_server.load("models/FlightHelmet/FlightHelmet.gltf");
    commands.spawn_scene(scene_handle);
}

Bevy ECS Improvements #

authors: @cart

Query Ergonomics #

In this release I finally was able to remove the one thing I truly despised in Bevy ECS. In previous versions of Bevy, iterating over the components in a Query looked like this:

for (a, b) in &mut query.iter() {
    // The `&mut` here just felt so unnatural
}

// Or if you preferred you could do this
for (a, b) in query.iter().iter() {
    // query.iter().iter()? Really???
}

Similarly, retrieving a specific entity's component's looked like this:

if let Ok(mut result) = query.entity(entity) {
    if let Some((a, b)) = result.get() {
        // access components here
    }
}

In Bevy 0.3 you can just do this:

// iteration
for (a, b) in query.iter() {
    // sweet ergonomic bliss
}

// entity lookup
if let Ok((a,b)) = query.get(entity) {
    // boilerplate be gone!
}

You might naturally be thinking something like:

Why did this take so long? Why would removing a single &mut be hard?

It's a long story! In summary:

  • The old API looked the way it did for a reason. It was the result of good design choices that protect against unsafe memory access in a parallel environment.
  • query.iter() didn't actually return an iterator. It returned a wrapper that held an atomic lock on the component storages. The same was true for the type returned by query.entity()
  • Removing these "wrapper types" would have allowed unsafe behavior because another Query could access the same components in a way that violated Rust's mutability rules.
  • Due to the iterator implementation and quirks in the rust compiler, removing the wrapper type tanked iteration performance by about ~2-3x.

Fortunately we finally found ways to solve all of these problems. The newly added QuerySets allow us to completely remove the locks (and wrapper types). And by completely rewriting QueryIter we were able to avoid the performance hit that removing the wrapper incurred. Read on for the details!

100% Lockless Parallel ECS #

Bevy ECS is now completely lock free. In Bevy 0.2, we made direct World access and "for-each" systems lock free. This is possible because the Bevy ECS scheduler ensures that systems only run in parallel in ways that respect Rust's mutability rules.

We couldn't remove locks from Query systems because of systems like this:

fn conflicting_query_system(mut q0: Query<&mut A>, mut q1: Query<(&mut A, &B)>) {
    let a = q0.get_mut(some_entity).unwrap();
    let (another_a, b) = q1.get_mut(some_entity).unwrap();
    // Aaah!!! We have two mutable references to some_entity's A component!
    // Very unsafe!
}

The locks ensured that the second q1.get_mut(some_entity) access panicked, keeping us nice and safe. In Bevy 0.3, a system like conflicting_query_system will fail when the schedule is constructed. By default, systems cannot have conflicting queries.

However there are some cases where a system needs conflicting queries to do what it needs to do. For these cases, we added QuerySets:

fn system(mut queries: QuerySet<(Query<&mut A>, Query<(&mut A, &B)>)>) {
    for a in queries.q0_mut().iter_mut() {
    }

    for (a, b) in queries.q1_mut().iter_mut() {
    }
}

By putting our conflicting Queries in a QuerySet, the Rust borrow checker protects us from unsafe query accesses.

Because of this, we were able to remove all safety checks from query.iter() and query.get(entity), which means these methods are now exactly as fast as their World counterparts (which we made lock-free in Bevy 0.2).

Performance Improvements #

Bevy had a number of nice performance improvements this release:

  • Removed atomic locks from Query access, making Bevy ECS 100% lock free
  • Removed archetype "safety checks" from Query access. At this point we have already verified that the given Query access is safe, so we don't need to check again on every call.
  • Rewrote QueryIter to be simpler (and therefore easier to control optimizations for), which allowed us to remove the iterator wrapper without tanking performance. This also resolved some performance inconsistencies where some system permutations performed optimally and others didn't. Now everything is on the "fast path"!
  • Ported some performance improvements from upstream hecs, which improved iteration over heavily fragmented archetypes and improved component insertion times

Getting an Entity's Component (per 100k, in milliseconds, smaller is better) #

Note: these numbers are for getting a component 100,000 times, not for an individual component lookup

getting an entity's component

This is where the big wins were. By removing locks and safety checks from Query systems, we were able to significantly reduce the cost of retrieving a specific entity's component from within a system.

I included a comparison to Legion ECS (another great archetypal ECS with a parallel scheduler) to illustrate why Bevy's new approach is so cool. Legion exposes a direct "world like" API (called a SubWorld) in its systems. The SubWorld's entry API cannot know ahead of time what types will be passed into it, which means it must do (relatively) expensive safety checks to ensure the user doesn't request access to something they shouldn't.

Bevy's scheduler pre-checks Queries once ahead of time, which allows systems to access their results without any additional checks.

The test was to lookup (and modify) a specific entity's component 100,000 times on each system iteration. Here is a quick rundown of how these tests were performed in each case:

  • bevy (world): Direct World access using world.get_mut::<A>(entity)
  • bevy (system): A system containing a Query<&mut A> that accesses the component using query.get_mut(entity)
  • legion (world): Direct World access using let entry = world.entry(entity); entry.get_component_mut::<A>()
  • legion (system): A system with SubWorld access using let entry = world.entry(entity); entry.get_component_mut::<A>()

It's worth noting that using query.get_component::<T>(entity) instead of query.get(entity) does require safety checks, for the same reason the legion entry API does. We cannot know ahead of time what component type a caller will pass into the method, which means we must check it to make sure it matches the Query.

Additionally, here are some relevant ecs_bench_suite results (omitted benchmarks had no significant change):

Component Insertion (in microseconds, smaller is better) #

component insertion

Component Add/Remove (in milliseconds, smaller is better) #

component add/remove

Fragmented Iteration (in nanoseconds, smaller is better) #

fragmented iteration

Thread Local Resources #

Some resource types cannot (or should not) be passed between threads. This is often true for low level APIs like windowing, input, and audio. It is now possible to add "thread local resources" to the Resources collection, which can only be accessed from the main thread using "thread local systems":

// in your app setup
app.add_thread_local_resource(MyResource);

// a thread local system
fn system(world: &mut World, resources: &mut Resources) {
    let my_resource = resources.get_thread_local::<MyResource>().unwrap();
}

Query API Changes #

First, to improve clarity we renamed query.get::<Component>(entity) to query.get_component::<Component>(entity). We now return the "full" query result for a specific entity using query.get(entity).

To allow multiple concurrent reads of Queries (where it is safe), we added separate query.iter() and query.iter_mut() APIs, as well as query.get(entity) and query.get_mut(entity). Queries that are "read only" can now retrieve their results via an immutable borrow.

Mesh Improvements #

Flexible Mesh Vertex Attributes #

authors: @julhe

Bevy meshes used to require exactly three "vertex attributes": position, normal, and uv. This worked for most things, but there are a number of cases that require other attributes, such as "vertex colors" or "bone weights for animation". Bevy 0.3 adds support for custom vertex attributes. Meshes can define whatever attributes they want and shaders can consume whatever attributes they want!

Here is an example that illustrates how to define a custom shader that consumes a mesh with an added "vertex color" attribute.

custom_vertex_attribute

Index Buffer Specialization #

authors: @termhn

Rendering meshes often involves using vertex "indices" to cut down on duplicate vertex information. Bevy used to hard code the precision of these indices to u16, which was too small for some cases. Now render pipelines can "specialize" based on a configured index buffer, which now defaults to u32 to cover most use cases.

Transform Re-Rewrite #

authors: @MarekLg (with some design help from @AThilenius, @bitshifter, @termhn, and @cart)

Transforms are important to get right. They are used in many slices of the engine, user code touches them constantly, and they are relatively expensive to compute: especially transform hierarchies.

In the last release, we vastly simplified Bevy's transform system to use a consolidated Transform and GlobalTransform instead of multiple separate Translation, Rotation, and Scale components (which were synced to Transform and GlobalTransform). This made the user-facing API/dataflow simpler, as well as the underlying implementation. The Transform component was backed by a 4x4 matrix. I pressed the big green "merge" button ... happy that we had solved the Transform problem once and for all!

It turns out there was still more work to be done! @AThilenius pointed out that using a 4x4 matrix as the source of truth for an affine transform accumulates error over time. Additionally, the Transform API was still a little cumbersome to use. At the suggestion of @termhn we decided to investigate using a "similarity" as the source of truth. This had the following benefits:

  1. no more error accumulation
  2. we could directly expose translation/rotation/scale fields, which simplified the API significantly
  3. cheaper to store and cheaper to compute hierarchies in some cases

We collectively decided this was a good path forward and now we have a re-rewrite that is even better. Yes this is another breaking change, but thats why we label Bevy as being in the "experimentation phase". Now is the time to break things as often as possible to ensure that we find good APIs that will stand the test of time.

This is what the new Transform API looks like in a Bevy ECS system:

fn system(mut transform: Mut<Transform>) {
    // move along the positive x-axis
    transform.translation += Vec3::new(1.0, 0.0, 0.0);

    // rotate 180 degrees (pi) around the y-axis
    transform.rotation *= Quat::from_rotation_y(PI);

    // scale 2x
    transform.scale *= 2.0;
}

Compared to the last version this is easier to use, more correct, and should also be slightly faster.

Gamepad Settings #

authors: @simpuid

The newly added GamepadSettings resource gives developers the ability to customize gamepad settings on a per-controller, per-axis/button basis:

fn system(mut gamepad_settings: ResMut<GamepadSettings>) {
    gamepad_settings.axis_settings.insert(
        GamepadAxis(Gamepad(0), GamepadAxisType::LeftStickX),
        AxisSettings {
            positive_high: 0.8,
            positive_low: 0.01,
            ..Default::default()
        },
    );
}

Plugin Groups #

authors: @cart

If you've used Bevy, you're probably familiar with this part of App initialization:

app.add_default_plugins();

This adds the plugins for all of the "core" engine functionality (rendering, input, audio, windowing, etc). It was straightforward, but also very static. What if you don't want to add all of the default plugins? What if you want to create your own custom set of plugins?

To resolve this, we added PluginGroups, which are ordered collections of plugins that can be individually enabled or disabled:

// This:
app.add_default_plugins()

// Has been replaced by this:
app.add_plugins(DefaultPlugins)

// You can disable specific plugins in a PluginGroup:
app.add_plugins_with(DefaultPlugins, |group| {
    group.disable::<RenderPlugin>()
         .disable::<AudioPlugin>()
});

// And you can create your own PluginGroups:
pub struct HelloWorldPlugins;

impl PluginGroup for HelloWorldPlugins {
    fn build(&mut self, group: &mut PluginGroupBuilder) {
        group.add(PrintHelloPlugin)
             .add(PrintWorldPlugin);
    }
}

app.add_plugins(HelloWorldPlugins);

Dynamic Window Settings #

authors: @mockersf

Bevy provides a backend-agnostic windowing API. Up until this point, window settings could only be set once at app startup. If you wanted to set window settings dynamically, you had to directly interact with window backends (ex: winit).

In this release we added the ability to dynamically set window properties at runtime using the Bevy window abstraction:

// This system dynamically sets the window title to the number of seconds since startup. Because why not?
fn change_title(time: Res<Time>, mut windows: ResMut<Windows>) {
    let window = windows.get_primary_mut().unwrap();
    window.set_title(format!(
        "Seconds since startup: {}", time.seconds_since_startup
    ));
}

Documentation Search-ability #

authors: @memoryruins

The bevy crate documentation search function now returns results for all sub-crates (like bevy_sprite). Due to how documentation is generated for re-exported crates, by default the bevy search index only covered the "prelude". @memoryruins found a way to fix this problem by creating new modules and exporting the contents of each crate within those modules (as opposed to aliasing the crates).

docs

Change Log #

Added #

Changed #

Fixed #

Contributors #

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

  • alec-deason
  • alexb910
  • andrewhickman
  • blunted2night
  • Bobox214
  • cart
  • CGMossa
  • CleanCut
  • ColdIce1605
  • Cupnfish
  • Dash-L
  • DJMcNab
  • EllenNyan
  • enfipy
  • EthanYidong
  • Gregoor
  • HyperLightKitsune
  • ian-h-chamberlain
  • J-F-Liu
  • Jerald
  • jngbsn
  • joshuajbouw
  • julhe
  • kedodrill
  • lberrymage
  • lee-orr
  • liufuyang
  • MarekLg
  • Mautar55
  • memoryruins
  • mjhostet
  • mockersf
  • MrEmanuel
  • mrk-its
  • mtsr
  • naithar
  • navaati
  • ndarilek
  • nic96
  • ocornoc
  • Olaren15
  • PrototypeNM1
  • Ratysz
  • Raymond26
  • robertwayne
  • simlay
  • simpuid
  • smokku
  • stjepang
  • SvenTS
  • sY9sE33
  • termhn
  • tigregalis
  • Vaelint
  • W4RH4WK
  • walterpie
  • will-hart
  • zgotsch
  • Zooce