https://bevyengine.org/news/bevy-0-5/ bevy logo bevy icon News Features Learn News Community Donate heart icon Get Started Bevy 0.5 Written on April 06, 2021 by Carter Anderson ( [github_gre] @cart [twitter_gr] @cart_cart [youtube_gr] cartdev ) [ante] Screenshot of Ante: a voxel builder game being developed in Bevy by @TheNeikos Thanks to 88 contributors, 283 pull requests, and our generous sponsors, I'm happy to announce the Bevy 0.5 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. Check out Awesome Bevy for a list of community-developed plugins, games, and learning resources. Bevy 0.5 is quite a bit bigger than our past few releases (and took a bit longer) as we have made a number of foundational changes. If you plan on updating your App or Plugin to Bevy 0.5, check out our 0.4 to 0.5 Migration Guide. Here are some of the highlights from this release: Physically Based Rendering (PBR) authors: @StarArawn, @mtsr, @mockersf, @IngmarBitter, @Josh015, @norgate, @cart Bevy now uses PBR shaders when rendering. PBR is a semi-standard approach to rendering that attempts to use approximations of real-world "physically based" lighting and material properties. We largely use techniques from the Filament PBR implementation, but we also incorporate some ideas from Unreal and Disney. Bevy's StandardMaterial now has base_color, roughness, metallic, reflection, and emissive properties. It also now supports textures for base_color, normal_map, metallic_roughness, emissive, and occlusion properties. The new PBR example helps visualize these new material properties: pbr GLTF Improvements PBR Textures authors: @mtsr, @mockersf The GLTF loader now supports normal maps, metallic/roughness, occlusion, and emissive textures. Our "flight helmet" gltf example utilizes the new PBR texture support and looks much nicer as a result: Top-Level GLTF Asset authors: @mockersf Previously it was hard to interact with GLTF assets because scenes / meshes / textures / and materials were only loaded as "sub assets". Thanks to the new top level Gltf asset type, it is now possible to navigate the contents of the GLTF asset: // load GLTF asset on startup fn setup(mut commands: Commands, assets: Res) { let handle = assets.load("flight_helmet.gltf"); commands.insert_resource(handle); } // access GLTF asset at some later point in time fn system(handle: Res>, gltfs: Res>, materials: Res>) { let gltf = gltfs.get(&handle).unwrap(); let material_handle = gltf.named_materials.get("MetalPartsMat").unwrap(); let material = materials.get(material_handle).unwrap(); } Bevy ECS V2 This release marks a huge step forward for Bevy's ECS. It has significant implications for how Bevy Apps are composed and how well they perform: * A full rewrite of the ECS core: + Massively improved performance across the board + "Hybrid" component storage + An "Archetype Graph" for faster archetype changes + Stateful queries that cache results across runs * A brand new parallel System executor: + Support for explicit system ordering + System Labels + System Sets + Improved system "run criteria" + Increased system parallelism * "Reliable" change detection: + Systems will now always detect component changes, even across frames * A rewrite of the State system: + A much more natural "stack-based state machine" model + Direct integration with the new scheduler + Improved "state lifecycle" events Read on for the details! ECS Core Rewrite authors: @cart Up until this point, Bevy used a heavily forked version of hecs for our ECS core. Since Bevy's first release, we've learned a lot about Bevy's ECS needs. We've also collaborated with other ECS project leaders, such as Sander Mertens (lead flecs developer) and Gijs-Jan Roelofs (Xenonauts ECS framework developer). As an "ECS community", we've started to zero in on what the future of ECS could be. Bevy ECS v2 is our first step into that future. It also means that Bevy ECS is no longer a "hecs fork". We are going out on our own! Component Storage (The Problem) Two ECS storage paradigms have gained a lot of traction over the years: * Archetypal ECS: + Stores components in "tables" with static schemas. Each "column" stores components of a given type. Each "row" is an entity. + Each "archetype" has its own table. Adding/removing an entity's component changes the archetype. + Enables super-fast Query iteration due to its cache-friendly data layout + Comes at the cost of more expensive add/remove operations for an Entity's components, because all components need to be copied to the new archetype's "table" + Parallelism-friendly: entities only exist in one archetype at a time so systems that access the same components but in different archetypes can run in parallel + Frameworks: Old Bevy ECS, hecs, legion, flecs, Unity DOTS * Sparse Set ECS: + Stores components of the same type in densely packed arrays, which are sparsely indexed by densely packed unsigned integers (entity ids) + Query iteration is slower than Archetypal ECS (by default) because each entity's component could be at any position in the sparse set. This "random access" pattern isn't cache friendly. Additionally, there is an extra layer of indirection because you must first map the entity id to an index in the component array. + Adding/removing components is a cheap, constant time operation + "Component Packs" are used to optimize iteration performance on a case by case basis (but packs conflict with each other) + Less parallelism friendly: systems need to either lock a whole component storage (not granular) or individual entities (expensive) + Frameworks: Shipyard, EnTT Developers selecting an ECS framework are stuck with a hard choice. Select an "archetypal" framework with "fast iteration everywhere" but without the ability to cheaply add/remove components, or select a "sparse set" framework to cheaply add/remove components but with slower iteration performance or manual (and conflicting) pack optimizations. Hybrid Component Storage (The Solution) In Bevy ECS V2, we get to have our cake and eat it too. It now has both of the component storage types above (and more can be added later if needed): * Tables (aka "archetypal" storage in other frameworks) + The default storage. If you don't configure anything, this is what you get + Fast iteration by default + Slower add/remove operations * Sparse Sets + Opt-in + Slower iteration + Faster add/remove operations These storage types complement each other perfectly. By default Query iteration is fast. If developers know that they want to add/remove a component at high frequencies, they can set the storage to "sparse set": app.register_component( ComponentDescriptor::new::(StorageType::SparseSet) ); Component Add/Remove Benchmark (in milliseconds, less is better) This benchmark illustrates adding and removing a single 4x4 matrix component 10,000 times from an entity that has 5 other 4x4 matrix components. The "other" components are included to help illustrate the cost of "table storage" (used by Bevy 0.4, Bevy 0.5 (Table), and Legion), which requires moving the "other" components to a new table. component add/remove You may have noticed that Bevy 0.5 (Table) is also way faster than Bevy 0.4, even though they both use "table storage". This is largely a result of the new Archetype Graph, which significantly cuts the cost of archetype changes. Stateful Queries and System Parameters World queries (and other system parameters) are now stateful. This allows us to: 1. Cache archetype (and table) matches + This resolves another issue with (naive) archetypal ECS: query performance getting worse as the number of archetypes goes up (and fragmentation occurs). 2. Cache Query Fetch and Filter state + The expensive parts of fetch/filter operations (such as hashing the TypeId to find the ComponentId) now only happen once when the Query is first constructed 3. Incrementally build up state + When new archetypes are added, we only process the new archetypes (no need to rebuild state for old archetypes) As a result, the direct World query api now looks like this: let mut query = world.query::<(&A, &mut B)>(); for (a, mut b) in query.iter_mut(&mut world) { } However for Systems this is a non-breaking change. Query state management is done internally by the relevant SystemParam. We have achieved some pretty significant performance wins as a result of the new Query system. "Sparse" Fragmented Iterator Benchmark (in nanoseconds, less is better) This benchmark runs a query that matches 5 entities within a single archetype and doesn't match 100 other archetypes. This is a reasonable test of "real world" queries in games, which generally have many different entity "types", most of which don't match a given query. This test uses "table storage" across the board. sparse_frag_iter Bevy 0.5 marks a huge improvement for cases like this, thanks to the new "stateful queries". Bevy 0.4 needs to check every archetype each time the iterator is run, whereas Bevy 0.5 amortizes that cost to zero. Fragmented Iterator Benchmark (in milliseconds, less is better) This is the ecs_bench_suite frag_iter benchmark. It runs a query on 27 archetypes with 20 entities each. However unlike the "Sparse Fragmented Iterator Benchmark", there are no "unmatched" archetypes. This test uses "table storage" across the board. frag_iter The gains here compared to the last benchmark are smaller because there aren't any unmatched archetypes. However Bevy 0.5 still gets a nice boost due to better iterator/query impls, amortizing the cost of matched archetypes to zero, and for_each iterators. Uber Fast "for_each" Query Iterators Developers now have the choice to use a fast Query::for_each() iterator, which yields ~1.5-3x iteration speed improvements for "fragmented iteration", and minor ~1.2x iteration speed improvements for unfragmented iteration. fn system(query: Query<(&A, &mut B)>) { // you now have the option to do this for a speed boost query.for_each_mut(|(a, mut b)| { }); // however normal iterators are still available for (a, mut b) in query.iter_mut() { } } We will continue to encourage "normal" iterators as they are more flexible and more "rust idiomatic". But when that extra "oomf" is needed, for_each will be there ... waiting for you :) New Parallel System Executor authors: @Ratysz Bevy's old parallel executor had a number of fundamental limitations: 1. The only way to explicitly define system order was to create new stages. This was both boilerplate-ey and prevented parallelism (because stages run "one by one" in order). We've noticed that system ordering is a common requirement and stages just weren't cutting it. 2. Systems had "implicit" orderings when they accessed conflicting resources. These orderings were hard to reason about. 3. The "implicit orderings" produced execution strategies that often left a lot of parallelism potential on the table. Fortunately @Ratysz has been doing a lot of research in this area and volunteered to contribute a new executor. The new executor solves all of the issues above and also adds a bunch of new usability improvements. The "ordering" rules are now dead-simple: 1. Systems run in parallel by default 2. Systems with explicit orderings defined will respect those orderings Explicit System Dependencies and System Labels authors: @Ratysz, @TheRawMeatball Systems can now be assigned one or more SystemLabels . These labels can then be referenced by other systems (within a stage) to run before or after systems with that label: app .add_system(update_velocity.system().label("velocity")) // The "movement" system will run after "update_velocity" .add_system(movement.system().after("velocity")) This produces an equivalent ordering, but it uses before() instead. app // The "update_velocity" system will run before "movement" .add_system(update_velocity.system().before("movement")) .add_system(movement.system().label("movement")); Any type that implements the SystemLabel trait can be used. In most cases we recommend defining custom types and deriving SystemLabel for them. This prevents typos, allows for encapsulation (when needed), and allows IDEs to autocomplete labels: #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub enum PhysicsSystem { UpdateVelocity, Movement, } app .add_system(update_velocity.system().label(PhysicsSystem::UpdateVelocity)) .add_system(movement.system() .label(PhysicsSystem::Movement) .after(PhysicsSystem::UpdateVelocity) ); Many-to-Many System Labels Many-to-many labels is a powerful concept that makes it easy to take a dependency on many systems that produce a given behavior/outcome. For example, if you have a system that needs to run after all "physics" has finished updating (see the example above), you could label all "physics systems" with the same Physics label: #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub struct Physics; #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub enum PhysicsSystem { UpdateVelocity, Movement, } app .add_system(update_velocity.system() .label(PhysicsSystem::UpdateVelocity) .label(Physics) ) .add_system(movement.system() .label(PhysicsSystem::Movement) .label(Physics) .after(PhysicsSystem::UpdateVelocity) ) .add_system(runs_after_physics.system().after(Physics)); Bevy plugin authors should export labels like this in their public APIs to enable their users to insert systems before/after logic provided by the plugin. System Sets SystemSets are a new way to apply the same configuration to a group of systems, which significantly cuts down on boilerplate. The "physics" example above could be rephrased like this: app .add_system_set(SystemSet::new() // this label is added to all systems in the set .label(Physics) .with_system(update_velocity.system().label(PhysicsSystem::UpdateVelocity)) .with_system(movement.system() .label(PhysicsSystem::Movement) .after(PhysicsSystem::UpdateVelocity) ) ) SystemSets can also use before(Label) and after(Label) to run all systems in the set before/after the given label. This is also very useful for groups of systems that need to run with the same RunCriteria . app // all systems in this set will run once every two seconds .add_system_set(SystemSet::new() .with_run_criteria(FixedTimestep::step(2.0)) .with_system(foo.system()) .with_system(bar.system()) ) Improved Run Criteria Run Criteria are now decoupled from systems and will be re-used when possible. For example, the FixedTimestep criteria in the example above will only be run once per stage run. The executor will re-use the criteria's result for both the foo and bar system. Run Criteria can now also be labeled and referenced by other systems: fn every_other_time(mut has_ran: Local) -> ShouldRun { *has_ran = !*has_ran; if *has_ran { ShouldRun::Yes } else { ShouldRun::No } } app.add_stage(SystemStage::parallel() .with_system_run_criteria(every_other_time.system().label("every_other_time"))) .add_system(foo.system().with_run_criteria("every_other_time")) Results from Run Criteria can also be "piped" into other criteria, which enables interesting composed behaviors: fn once_in_a_blue_moon(In(input): In, moon: Res) -> ShouldRun { if moon.is_blue() { input } else { ShouldRun::No } } app .add_system(foo.with_run_criteria( "every_other_time".pipe(once_in_a_blue_moon.system()) ) Ambiguity Detection and Resolution While the new executor is now much easier to reason about, it does introduce a new class of error: "system order ambiguities". When two systems interact with the same data, but have no explicit ordering defined, the output they produce is non-deterministic (and often not what the author intended). Consider the following app: fn increment_counter(mut counter: ResMut) { *counter += 1; } fn print_every_other_time(counter: Res) { if *counter % 2 == 0 { println!("ran"); } } app .add_system(increment_counter.system()) .add_system(print_every_other_time.system()) The author clearly intended print_every_other_time to run every other update. However, due to the fact that these systems have no order defined, they could run in a different order each update and create a situation where nothing is printed over the course of two updates: UPDATE - increment_counter (counter now equals 1) - print_every_other_time (nothing printed) UPDATE - print_every_other_time (nothing printed) - increment_counter (counter now equals 2) The old executor would have implicitly forced increment_counter to run first because it conflicts with print_every_other_time and it was inserted first. But the new executor requires you to be explicit here (which we believe is a good thing). To help detect this class of error, we built an opt-in tool that detects these ambiguities and logs them: // add this resource to your App to enable ambiguity detection app.insert_resource(ReportExecutionOrderAmbiguities) Then when we run our App, we will see the following message printed to our terminal: Execution order ambiguities detected, you might want to add an explicit dependency relation between some of these systems: * Parallel systems: -- "&app::increment_counter" and "&app::print_every_other_time" conflicts: ["usize"] The ambiguity detector found a conflict and mentions that adding an explicit dependency would resolve the conflict: app .add_system(increment_counter.system().label("increment")) .add_system(print_every_other_time.system().after("increment")) There are some cases where ambiguities are not a bug, such as operations on unordered collection like Assets. This is why we don't enable the detector by default. You are free to just ignore these ambiguities, but if you want to suppress the messages in the detector (without defining a dependency), you can add your systems to an "ambiguity set": app .add_system(a.system().in_ambiguity_set("foo")) .add_system(b.system().in_ambiguity_set("foo")) I want to stress that this is totally optional. Bevy code should be ergonomic and "fun" to write. If sprinkling ambiguity sets everywhere isn't your cup of tea, just don't worry about it! We are also actively seeking feedback on the new executor. We believe that the new implementation is easier to understand and encourages self-documenting code. The improved parallelism is also nice! But we want to hear from users (both new users starting fresh and old users porting their codebases to the new executor). This space is all about design tradeoffs and feedback will help us ensure we made the right calls. Reliable change detection authors: @Davier, @bjorn3, @alice-i-cecile, @cart Global change detection, the ability to run queries on the Changed/ Added status of any ECS component or resource, just got a major usability boost: changes are now detected across frames/updates: // This is still the same change detection api we all know and love, // the only difference is that it "just works" in every situation. fn system(query: Query>) { // iterates all entities whose A component has changed since // the last run of this system for e in query.iter() { } } Global change detection was already a feature that set Bevy apart from other ECS frameworks, but now it is completely "fool proof". It works as expected regardless of system ordering, stage membership, or system run criteria. The old behavior was "systems detect changes that ocurred in systems that ran before them this frame". This was because we used a bool to track when each component/resource is added/modified. This flag was cleared for each component at the end of the frame. As a result, users had to be very careful about order of operations, and using features like "system run criteria" could result in dropped changes if systems didn't run on a given update. We now use a clever "world tick" design that allows systems to detect changes that happened at any point in time since their last run. States V2 authors: @TheRawMeatball The last Bevy release added States, which enabled developers to run groups of ECS systems according to the value of a State resource. Systems could be run according to "state lifecycle events", such as on_enter, on_update, and on_exit. States make things like separate "loading screen" and "in game" logic easier to encode in Bevy ECS. The old implementation largely worked, but it had a number of quirks and limitations. First and foremost, it required adding a new StateStage, which cut down on parallelism, increased boilerplate, and forced ordering where it wasn't required. Additionally, some of the lifecycle events didn't always behave as expected. The new State implementation is built on top of the new parallel executor's SystemSet and RunCriteria features, for a much more natural, flexible, and parallel api that builds on existing concepts instead of creating new ones: #[derive(Debug, Clone, Eq, PartialEq, Hash)] enum AppState { Menu, InGame, } fn main() { App::build() .add_state(AppState::Menu) .add_system_set(SystemSet::on_enter(AppState::Menu).with_system(setup_menu.system())) .add_system_set(SystemSet::on_update(AppState::Menu).with_system(menu_logic.system())) .add_system_set(SystemSet::on_exit(AppState::Menu).with_system(cleanup_menu.system())) .add_system_set(SystemSet::on_enter(AppState::InGame).with_system(setup_game.system())) .add_system_set( SystemSet::on_update(AppState::InGame) .with_system(game_logic.system()) .with_system(more_game_logic.system()) ) .run(); } States now use a "stack-based state machine" model. This opens up a number of options for state transitions: fn system(mut state: ResMut>) { // Queues up a state change that pushes a new state on to the // stack (preserving previous states) state.push(AppState::InGame).unwrap(); // Queues up a state change that removes the current state on // the stack and reverts to the previous state state.pop().unwrap(); // Queues up a state change that overwrites the current state at // the "top" of the stack state.set(AppState::InGame).unwrap(); // Queues up a state change that replaces the entire stack of states state.replace(AppState::InGame).unwrap(); } Just like the old implementation, state changes are applied in the same frame. This means it is possible to transition from states A-> B->C and run the relevant state lifecycle events without skipping frames. This builds on top of "looping run criteria", which we also use for our "fixed timestep" implementation (and which you can use for your own run criteria logic). Event Ergonomics authors: @TheRawMeatball Events now have a first-class shorthand syntax for easier consumption: // Old Bevy 0.4 syntax fn system(mut reader: Local>, events: Res>) { for event in reader.iter(&events) { } } // New Bevy 0.5 syntax fn system(mut reader: EventReader) { for event in reader.iter() { } } There is also now a symmetrical EventWriter api: fn system(mut writer: EventWriter) { writer.send(SomeEvent { ... }) } The old "manual" approach is still possible via ManualEventReader: fn system(mut reader: Local>, events: Res>) { for event in reader.iter(&events) { } } Rich Text authors: @tigregalis Text can now have "sections", each with their own style / formatting. This makes text much more flexible, while still respecting the text layout rules: rich_text This is accomplished using the new "text section" api: commands .spawn_bundle(TextBundle { text: Text { sections: vec![ TextSection { value: "FPS: ".to_string(), style: TextStyle { font: asset_server.load("FiraSans-Bold.ttf"), font_size: 90.0, color: Color::WHITE, }, }, TextSection { value: "60.03".to_string(), style: TextStyle { font: asset_server.load("FiraMono-Medium.ttf"), font_size: 90.0, color: Color::GOLD, }, }, ], ..Default::default() }, ..Default::default() }) HIDPI Text authors: @blunted2night Text is now rendered according to the current monitor's scale factor. This gives nice, crisp text at any resolution. hidpi_text Render Text in 2D World Space authors: @CleanCut, @blunted2night Text can now be spawned into 2D scenes using the new Text2dBundle. This makes it easier to do things like "draw names above players". World To Screen Coordinate Conversions authors: @aevyrie It is now possible to convert world coordinates to a given camera's screen coordinates using the new Camera::world_to_screen() function. Here is an example of this feature being used to position a UI element on top of a moving 3d object. 3D Orthographic Camera authors: @jamadazi Orthographic cameras can now be used in 3D! This is useful for things like CAD applications and isometric games. ortho_3d Orthographic Camera Scaling Modes authors: @jamadazi Prior to Bevy 0.5, Bevy's orthographic camera had only one mode: "window scaling". It would adapt the projection according to the vertical and horizontal size of the window. This works for some styles of games, but other games need arbitrary window-independent scale factors or scale factors defined by either horizontal or vertical window sizes. Bevy 0.5 adds a new ScalingMode option to OrthographicCamera, which enables developers to customize how the projection is calculated. It also adds the ability to "zoom" the camera using OrthographicProjection::scale. Flexible Camera Bindings authors: @cart Bevy used to "hack in" camera bindings for each RenderGraph PassNode. This worked when there was only one binding type (the combined ViewProj matrix), but many shaders require other camera properties, such as the world space position. In Bevy 0.5 we removed the "hack" in favor of the RenderResourceBindings system used elsewhere. This enables shaders to bind arbitrary camera data (with any set or binding index) and only pull in the data they need. The new PBR shaders take advantage of this feature, but custom shaders can also use it. layout(set = 0, binding = 0) uniform CameraViewProj { mat4 ViewProj; }; layout(set = 0, binding = 1) uniform CameraPosition { vec3 CameraPos; }; Render Layers authors: @schell Sometimes you don't want a camera to draw everything in a scene, or you want to temporarily hide a set of things in the scene. Bevy 0.5 adds a RenderLayer system, which gives developers the ability to add entities to layers by adding the RenderLayers component. Cameras can also have a RenderLayers component, which determines what layers they can see. // spawn a sprite on layer 0 commands .spawn_bundle(SpriteBundle { material: materials.add(Color::rgb(1.0, 0.5, 0.5).into()), transform: Transform::from_xyz(0.0, -50.0, 1.0), sprite: Sprite::new(Vec2::new(30.0, 30.0)), }) .insert(RenderLayers::layer(0)); // spawn a sprite on layer 1 commands .spawn_bundle(SpriteBundle { material: materials.add(Color::rgb(1.0, 0.5, 0.5).into()), transform: Transform::from_xyz(0.0, -50.0, 1.0), sprite: Sprite::new(Vec2::new(30.0, 30.0)), }) .insert(RenderLayers::layer(1)); // spawn a camera that only draws the sprite on layer 1 commands .spawn_bundle(OrthographicCameraBundle::new_2d()); .insert(RenderLayers::layer(1)); Sprite Flipping authors: @zicklag Sprites can now be easily (and efficiently) flipped along the x or y axis: sprite_flipping commands.spawn_bundle(SpriteBundle { material: material.clone(), transform: Transform::from_xyz(150.0, 0.0, 0.0), ..Default::default() }); commands.spawn_bundle(SpriteBundle { material, transform: Transform::from_xyz(-150.0, 0.0, 0.0), sprite: Sprite { // Flip the logo to the left flip_x: true, // And don't flip it upside-down ( the default ) flip_y: false, ..Default::default() }, ..Default::default() }); Color Spaces authors: @mockersf Color is now internally represented as an enum, which enables lossless (and correct) color representation. This is a significant improvement over the previous implementation, which internally converted all colors to linear sRGB (which could cause precision issues). Colors are now only converted to linear sRGB when they are sent to the GPU. We also took this opportunity to fix some incorrect color constants defined in the wrong color space. pub enum Color { /// sRGBA color Rgba { /// Red component. [0.0, 1.0] red: f32, /// Green component. [0.0, 1.0] green: f32, /// Blue component. [0.0, 1.0] blue: f32, /// Alpha component. [0.0, 1.0] alpha: f32, }, /// RGBA color in the Linear sRGB colorspace (often colloquially referred to as "linear", "RGB", or "linear RGB"). RgbaLinear { /// Red component. [0.0, 1.0] red: f32, /// Green component. [0.0, 1.0] green: f32, /// Blue component. [0.0, 1.0] blue: f32, /// Alpha component. [0.0, 1.0] alpha: f32, }, /// HSL (hue, saturation, lightness) color with an alpha channel Hsla { /// Hue component. [0.0, 360.0] hue: f32, /// Saturation component. [0.0, 1.0] saturation: f32, /// Lightness component. [0.0, 1.0] lightness: f32, /// Alpha component. [0.0, 1.0] alpha: f32, }, } Wireframes authors: @Neo-Zhixing Bevy can now draw wireframes using the opt-in WireframePlugin wireframe These can either be enabled globally or per-entity by adding the new Wireframe component. Simple 3D Game Example: Alien Cake Addict authors: @mockersf This example serves as a quick introduction to building 3D games in Bevy. It shows how to spawn scenes, respond to input, implement game logic, and handle state transitions. Pick up as many cakes as you can! alien_cake_addict Timer Improvements authors: @kokounet The Timer struct now internally uses Durations instead of using f32 representations of seconds. This both increases precision and makes the api a bit nicer to look at. fn system(mut timer: ResMut, time: Res