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 crates.io!
For those who don't know, Bevy is a refreshingly simple data-driven game engine built in Rust. You can check out 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:
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:
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() {
App::build().run();
}
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::*;
#[bevy_main]
fn main() {
App::build().run();
}
Bevy can now update changes to shaders at runtime, giving you instant feedback without restarting your app. This video isn't sped up!
It wouldn't be a Bevy update without another round of ECS improvements!
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:
SystemParam
trait!
// 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
.
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>) {
}
Systems can now have inputs and outputs. This opens up a variety of interesting behaviors, such as system error handling:
fn main() {
App::build()
.add_system(result_system.system().chain(error_handler.system()))
.run();
}
fn result_system(query: Query<&Transform>) -> Result<()> {
let transform = query.get(SOME_ENTITY)?;
println!("found entity transform: {:?}", transform);
Ok(())
}
fn error_handler_system(In(result): In<Result<()>>, error_handler: Res<MyErrorHandler>) {
if let Err(err) = result {
error_handler.handle_error(err);
}
}
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.
Bevy's old Schedule was nice. System registrations were easy to read and easy to compose. But it also had significant limitations:
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:
app.add_system(my_system.system())
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
}
}
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 =
SystemStage::parallel()
.with_system(a.system())
.with_system(b.system());
// runs systems serially (in registration order)
let serial_stage =
SystemStage::serial()
.with_system(a.system())
.with_system(b.system());
// you can also write your own custom SystemStageExecutor
let custom_executor_stage =
SystemStage::new(MyCustomExecutor::new())
.with_system(a.system())
.with_system(b.system());
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_system(a.system())
.with_system(b.system())
)
.with_stage("nested", Schedule::default()
.with_stage("nested_stage", SystemStage::serial()
.with_system(b.system())
)
);
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 {
ShouldRun::Yes
} else {
ShouldRun::No
}
}
app
// this stage only runs when Res<usize> has a value of 10
.add_stage_after(stage::UPDATE, "only_on_10_stage", SystemStage::parallel()
.with_run_criteria(only_on_10_criteria.system())
.with_system(my_system.system())
)
// this stage only runs once
.add_stage_after(stage::RUN_ONCE, "one_and_done", Schedule::default()
.with_run_criteria(RunOnce::default())
.with_system(my_system.system())
)
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()
.with_run_criteria(FixedTimestep::step(0.4))
.with_system(my_system.system())
)
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.
Now that stages can be any type, we need a way for Plugins
to interact with arbitrary stage types:
app
// this "high level" builder pattern still works (and assumes that the stage is a SystemStage)
.add_system(some_system.system())
// this "low level" builder is equivalent to add_system()
.stage(stage::UPDATE, |stage: &mut SystemStage|
stage.add_system(some_system.system())
)
// this works for custom stage types too
.stage(MY_CUSTOM_STAGE, |stage: &mut MyCustomStage|
stage.do_custom_thing()
)
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?
&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.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:
#[derive(Clone)]
enum AppState {
Loading,
Menu,
InGame
}
You then add them to your app as a resource like this:
// add a new AppState resource that defaults to the Loading state
app.add_resource(State::new(AppState::Loading))
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:
app
.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":
You can queue a state change from a system like this:
fn system(mut state: ResMut<State<AppState>>) {
state.set_next(AppState::InGame).unwrap();
}
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.
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:
Scenes can now be spawned as children like this:
commands
.spawn((
Transform::from_translation(Vec3::new(0.5, 0.0, 0.0)),
GlobalTransform::default(),
))
.with_children(|parent| {
parent.spawn_scene(asset_server.load("scene.gltf"));
});
By spawning beneath a parent, this enables you to do things like translate/rotate/scale multiple instances of the same scene:
@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 3d_scene.rs
example with the Fast Compiles Config and dynamic linking:
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.
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:
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.
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:
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.
Bevy now uses wgpu's "mailbox vsync" by default. This reduces input latency on platforms that support it.
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 std::any::TypeId
and std::any::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:
#[derive(Reflect)]
struct Foo {
a: u32,
b: Vec<Bar>,
c: Vec<u32>,
}
#[derive(Reflect)]
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]
};
assert_eq!(*foo.get_field::<u32>("a").unwrap(), 1);
*foo.get_field_mut::<u32>("a").unwrap() = 2;
assert_eq!(foo.a, 2);
let mut dynamic_struct = DynamicStruct::default();
dynamic_struct.insert("a", 42u32);
dynamic_struct.insert("c", vec![3, 4, 5]);
foo.apply(&dynamic_struct);
assert_eq!(foo.a, 42);
assert_eq!(foo.c, vec![3, 4, 5]);
let value = *foo.get_path::<String>("b[0].value").unwrap();
assert_eq!(value.as_str(), "hello world");
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);
}
}
This doesn't require manual Serde impls!
let mut registry = TypeRegistry::default();
registry.register::<u32>();
registry.register::<String>();
registry.register::<Bar>();
let serializer = ReflectSerializer::new(&foo, ®istry);
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(®istry);
let value = reflect_deserializer.deserialize(&mut deserializer).unwrap();
let dynamic_struct = value.take::<DynamicStruct>().unwrap();
/// reflect has its own partal_eq impl
assert!(foo.reflect_partial_eq(&dynamic_struct).unwrap());
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:
#[derive(Reflect)]
#[reflect(DoThing)]
struct MyType {
value: String,
}
impl DoThing for MyType {
fn do_thing(&self) -> String {
format!("{} World!", self.value)
}
}
#[reflect_trait]
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()
type_registry.register::<MyType>();
/*
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
.get_type_data::<ReflectDoThing>(reflect_value.type_id())
.unwrap();
// 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());
The Texture asset now has support for 3D textures. The new array_texture.rs
example illustrates how to load a 3d texture and sample from each "layer".
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.
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.
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
.
Bevy now handles HIDPI / Retina / high pixel density displays properly:
window.physical_width()
and window.physical_height()
methods.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.
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() {
App::build()
.add_resource(MyTimer {
// a five second non-repeating timer
timer: Timer::from_seconds(5.0, false),
})
.add_system(timer_system.system())
.run();
}
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");
}
}
@aclysma changed how Bevy Tasks schedules work, which increased performance in the breakout.rs
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.
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.
@karroffel added a fun example that represents each Bevy contributor as a "Bevy Bird". It scrapes the latest contributor list from git.
A "bunnymark-style" benchmark illustrating Bevy's sprite rendering performance. This was useful when implementing the renderer optimizations mentioned above.
bevy_log
EventId
for eventsset_cursor_position
to Window
.dds
, .tga
, and .jpeg
texture formatsAssetIo
implementationTime
state and made members private.Time
's values directly is no longer possible outside of bevy.mailbox
instead of fifo
for vsync on supported systemsTextureAtlasBuilder
into expected Builder conventionsWindow
's width
& height
methods to return f32
Draw::is_visible
or Draw::is_transparent
should now set Visible::is_visible
and Visible::is_transparent
winit
upgraded from version 0.23 to version 0.24instant::Instant
for WASM compatibilityRenderResources
index slicingA huge thanks to the 66 contributors that made this release (and associated docs) possible!