- 0.17 to 0.18
- Immutable Entity Events
- AssetSources now give an async_channel::Sender instead of a crossbeam_channel::Sender
- Feature cleanup
- Removed dummy_white_gpu_image
- Tick-related refactors
- ImageRenderTargets scale_factor field is now an f32
- #[reflect(...)] now supports only parentheses
- Schedule cleanup
- RenderTarget is now a component
- Winit user events removed
- System Combinators
- FunctionSystem Generics
- Removed SimpleExecutor
- Implementations of Reader now must implement Reader::seekable, and AsyncSeekForward is deleted.
- Remove ron re-export from bevy_scene and bevy_asset
- Rename ThinSlicePtr::get() to ThinSlicePtr::get_unchecked()
- AnimationEventTrigger::animation_player has been renamed to AnimationEventTrigger::target
- DragEnter now fires on drag starts
- Remove bevy::ptr::dangling_with_align()
- AssetPlugin now has a use_asset_processor_override field.
- enable_prepass and enable_shadows are now Material methods
- ArchetypeQueryData trait
- Put input sources for bevy_input under features
- TextLayoutInfo's section_rects field has been replaced with run_geometry
- Change Bundle::component_ids and Bundle::get_component_ids to return an iterator.
- BorderRadius has been added to Node and is no longer a component
- Per-RenderPhase Draw Functions
- Generalized Atmospheric Scattering Media
- Image Loader Array Layout
- Renamed bevy_reflect feature documentation to reflect_documentation
- Tilemap Chunk Layout
- Renamed clear_children and clear_related methods to detach_*
- LoadContext::path now returns AssetPath.
- Use Mesh::try_* mesh functions for Assets<Mesh> entries when there can be RenderAssetUsages::RENDER_WORLD-only meshes.
- Custom asset sources now require a reader.
- ExtractedUiNode's stack_index has been renamed to z_order and is now an f32.
- The non-text areas of UI Text nodes are no longer pickable
- Cargo Feature Collections
- LineHeight is now a separate component
- AnimationTarget replaced by separate components
- Virtual Geometry
- BevyManifest::shared is now a scope-like API.
- Entities APIs
- Removed FontAtlasSets
- get_components, get_components_mut_unchecked now return a Result
- AmbientLight split into a component and a resource
- Derive on Resource will fail when using non-static lifetimes
- RenderPipelineDescriptor and ComputePipelineDescriptor now hold a BindGroupLayoutDescriptor
- Gizmos::cuboid has been renamed to Gizmos::cube
- API for working with Relationships and RelationshipTargets in type-erased contexts
- Internal has been removed
- Automatic Aabb updates for sprites and meshes
- Changes to AssetServer and AssetProcessor creation.
- Same State Transitions
- Image::reinterpret_size and Image::reinterpret_stacked_2d_as_array now return a Result
- Changes to the Process trait in bevy_asset.
- BindGroupLayout labels are no longer optional
- Traits AssetLoader, AssetTransformer, AssetSaver, and Process all now require TypePath
- bevy_gizmos rendering split
- Replaced Column with ThinColumn
- Renamed bevy_platform::HashMap::get_many_* to bevy_platform::HashMap::get_disjoint_*
- glTF Coordinate Conversion
- TrackedRenderPass::set_index_buffer no longer takes buffer offset
- BorderRect now has Vec2 fields
Migration Guide: 0.17 to 0.18
For a full list of changes that may require migration, please see below. We recommend keeping this page open as you migrate your code base, and searching within it for any issues you encounter.
If you run into a problem while migrating that was not covered by this migration guide, please open an issue on the bevy-website repo or file a PR directly with the correct migration advice.
Immutable Entity Events #
The mutable methods of EntityEvent (EntityEvent::from and EntityEvent::event_target_mut) have been moved to a separate trait: SetEntityEventTarget
This makes all EntityEvents immutable by default.
SetEntityEventTarget is implemented automatically for propagated events (e.g. #[entity_event(propagate)]).
AssetSources now give an async_channel::Sender instead of a crossbeam_channel::Sender #
Previously, when creating an asset source, AssetSourceBuilder::with_watcher would provide users with a crossbeam_channel::Sender. Now, this has been changed to async_channel::Sender.
If you were previously calling sender.send(AssetSourceEvent::ModifiedAsset("hello".into())), now it would be sender.send_blocking(AssetSourceEvent::ModifiedAsset("hello".into())). These channels are very comparable, so finding an analogous method between crossbeam_channel and async_channel should be straight forward.
Feature cleanup #
animation has been renamed to gltf_animation. bevy_sprite_picking_backend has been renamed to sprite_picking. bevy_ui_picking_backend has been renamed to ui_picking. bevy_mesh_picking_backend has been renamed to mesh_picking.
Removed dummy_white_gpu_image #
MeshPipeline, Mesh2dPipeline and SpritePipeline no longer have dummy_white_gpu_image.
MeshPipeline and Mesh2dPipeline no longer have get_image_texture() in their impl.
The method build_dummy_white_gpu_image() and get_image_texture() can be used if needed.
Tick-related refactors #
TickCells is now ComponentTickCells.
ComponentSparseSet::get_with_ticks now returns Option<(Ptr, ComponentTickCells)> instead of Option<(Ptr, TickCells, MaybeLocation)>.
The following types have been moved from the component module to the change_detection module:
TickComponentTicksComponentTickCellsCheckChangeTicks
ImageRenderTargets scale_factor field is now an f32 #
The scale_factor field on ImageRenderTarget is now an f32 and no longer requires wrapping in FloatOrd.
#[reflect(...)] now supports only parentheses #
Previously, the #[reflect(...)] attribute of the Reflect derive macro supported parentheses, braces, or brackets, to standardize the syntax going forward, it now supports only parentheses.
/// 0.17
#[derive(Clone, Reflect)]
#[reflect[Clone]]
/// 0.18
#[derive(Clone, Reflect)]
#[reflect(Clone)]
/// 0.17
#[derive(Clone, Reflect)]
#[reflect{Clone}]
/// 0.18
#[derive(Clone, Reflect)]
#[reflect(Clone)]
Schedule cleanup #
ScheduleGraph::topsort_graphhas been moved toDiGraph::toposort, and now takes aVec<N>parameter for allocation reuse.ReportCycleswas removed: instead,DiGraphToposortErrors should be immediately wrapped into hierarchy graph or dependency graphScheduleBuildErrorvariants.ScheduleBuildError::HierarchyLoopvariant was removed, useScheduleBuildError::HierarchySort(DiGraphToposortError::Loop())instead.ScheduleBuildError::HierarchyCyclevariant was removed, useScheduleBuildError::HierarchySort(DiGraphToposortError::Cycle())instead.ScheduleBuildError::DependencyLoopvariant was removed, useScheduleBuildError::DependencySort(DiGraphToposortError::Loop())instead.ScheduleBuildError::DependencyCyclevariant was removed, useScheduleBuildError::DependencySort(DiGraphToposortError::Cycle())instead.ScheduleBuildError::CrossDependencynow wraps aDagCrossDependencyError<NodeId>instead of directly holding twoNodeIds. Fetch them from the wrapped struct instead.ScheduleBuildError::SetsHaveOrderButIntersectnow wraps aDagOverlappingGroupError<SystemSetKey>instead of directly holding twoSystemSetKeys. Fetch them from the wrapped struct instead.ScheduleBuildError::SystemTypeSetAmbiguitynow wraps aSystemTypeSetAmbiguityErrorinstead of directly holding aSystemSetKey. Fetch them from the wrapped struct instead.ScheduleBuildWarning::HierarchyRedundancynow wraps aDagRedundancyError<NodeId>instead of directly holding aVec<(NodeId, NodeId)>. Fetch them from the wrapped struct instead.ScheduleBuildWarning::Ambiguitynow wraps aAmbiguousSystemConflictsWarninginstead of directly holding aVec. Fetch them from the wrapped struct instead.ScheduleGraph::conflicting_systemsnow returns a&ConflictingSystemsinstead of a slice. Fetch conflicts from the wrapped struct instead.ScheduleGraph::systems_in_setnow returns a&HashSet<SystemKey>instead of a slice, to reduce redundant allocations.ScheduleGraph::conflicts_to_stringfunctionality has been replaced withConflictingSystems::to_string.ScheduleBuildPass::buildnow takes&mut Dag<SystemKey>instead of&mut DiGraph<SystemKey>, to allow reusing previous toposorts.ScheduleBuildPass::collapse_setnow takes&HashSet<SystemKey>instead of a slice, to reduce redundant allocations.simple_cycles_in_componenthas been changed from a free function into a method onDiGraph.DiGraph::try_into/UnGraph::try_intowas renamed toDiGraph::try_convert/UnGraph::try_convertto prevent overlap with theTryIntotrait, and now makes use ofTryIntoinstead ofTryFromfor conversions.
RenderTarget is now a component #
RenderTarget has been moved from a field on Camera to a separate required component.
When spawning a camera, specify RenderTarget as a component instead of setting camera.target:
// 0.17
commands.spawn((
Camera3d::default(),
Camera {
target: RenderTarget::Image(image_handle.into()),
..default()
},
));
// 0.18
commands.spawn((
Camera3d::default(),
RenderTarget::Image(image_handle.into()),
));
Winit user events removed #
In Bevy 0.17 and earlier, WinitPlugin and EventLoopProxyWrapper was generic over a M: Message type, that could be used to wake up the winit event loop and which was then forwarded to the ECS world. In 0.18 support for this has been removed, and those types are no longer generic.
If you used the default WakeUp type via the event loop proxy, you can still do this by using the new WinitUserEvent type:
// 0.17
fn wakeup_system(event_loop_proxy: Res<EventLoopProxyWrapper<WakeUp>>) -> Result {
event_loop_proxy.send_event(WakeUp)?;
Ok(())
}
// 0.18
fn wakeup_system(event_loop_proxy: Res<EventLoopProxyWrapper>) -> Result {
event_loop_proxy.send_event(WinitUserEvent::WakeUp)?;
Ok(())
}
If you were using it to send information into the ECS world from outside Bevy, you will need to create your own channel and system that forwards the messages.
System Combinators #
CombinatorSystems can be used to combine multiple SystemConditions with logical operators (such as and, or, and xor). Previously, these combinators would propagate any errors made when running the combined systems:
// 0.17
#[derive(Component)]
struct Foo;
// This run condition will fail validation because there is not an entity with `Foo` in the world.
fn fails_validation(_: Single<&Foo>) -> bool {
// ...
}
fn always_true() -> bool {
true
}
let mut world = World::new();
// Because `fails_validation` is invalid, trying to run this combinator system will return an
// error.
assert!(world.run_system_once(fails_validation.or(always_true)).is_err());
This behavior has been changed in Bevy 0.18. Now if one of the combined systems fails, it will be considered to have returned false. The error will not be propagated, and the combinator logic will continue:
// 0.18
let mut world = World::new();
// `fails_validation` is invalid, but it is converted to `false`. Because `always_true` succeeds,
// the combinator returns `true`.
assert_eq!(matches!(world.run_system_once(fails_validation.or(always_true)), Ok(true)));
This affects the following combinators:
| Combinator | Rust Equivalent |
|---|---|
and | a && b |
or | a || b |
xor | a ^ b |
nand | !(a && b) |
nor | !(a || b) |
xnor | !(a ^ b) |
FunctionSystem Generics #
FunctionSystem now has a new generic parameter: In.
Old: FunctionSystem<Marker, Out, F> New: FunctionSystem<Marker, In, Out, F>
Additionally, there's an extra bound on the System and IntoSystem impls related to FunctionSystem:
<F as SystemParamFunction>::In: FromInput<In>
This enabled systems to take as input any compatible type, in addition to the exact one specified by the system function. This shouldn't impact users at all since it only adds functionality, but users writing heavily generic code may want to add a similar bound. See function_system.rs to see how it works in practice.
Removed SimpleExecutor #
Bevy has removed the previously deprecated SimpleExecutor, one of the SystemExecutors in Bevy alongside SingleThreadedExecutor and MultiThreadedExecutor (which aren't going anywhere any time soon). The MultiThreadedExecutor is great at large schedules and async heavy work, and the SingleThreadedExecutor is good at smaller schedules or schedules that have fewer parallelizable systems. So what was SimpleExecutor good at? Not much. That's why it was removed. Removing it reduced some maintenance and consistency burdens on maintainers, allowing them to focus on more exciting features!
If you were using SimpleExecutor, consider upgrading to SingleThreadedExecutor instead, or try MultiThreadedExecutor if it fits the schedule. It's worth mentioning that SimpleExecutor ran deferred commands inbetween each system, regardless of if it was needed. The other executors are more efficient about this, but that means they need extra information about when to run those commands. In most schedules, that information comes from the contents and ordering of systems, via before, after, chain, etc. If a schedule that was previously using SimpleExecutor still needs commands from one system to be applied before another system runs, make sure that ordering is enforced explicitly by these methods, rather than implicitly by the order of add_systems. If you are looking for a quick fix, chain is the easiest way to do this.
Implementations of Reader now must implement Reader::seekable, and AsyncSeekForward is deleted. #
The Reader trait no longer requires implementing AsyncSeekForward and instead requires implementing Reader::seekable, which will cast the Reader to &mut dyn SeekableReader if it supports AsyncSeek (SeekableReader: Reader + AsyncSeek).
// If MyReader implements `AsyncSeek`
impl Reader for MyReader {
fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
Ok(self)
}
}
// If MyReader does not implement `AsyncSeek`
impl Reader for MyReader {
fn seekable(&mut self) -> Result<&mut dyn SeekableReader, ReaderNotSeekableError> {
None
}
}
Since we now just use the AsyncSeek trait, we've deleted the AsyncSeekForward trait. Users of this trait can migrate by calling the AsyncSeek::poll_seek method with SeekFrom::Current(offset), or the AsyncSeekExt::seek method.
Remove ron re-export from bevy_scene and bevy_asset #
The ron crate is no longer re-exported from bevy_scene or bevy_asset. This was done to reduce naming conflicts and improve API clarity.
If you were importing ron through bevy_scene or bevy_asset, you should now add ron as a direct dependency to your project.
This change only affects code that was explicitly importing the ron module. All internal scene serialization and deserialization functionality remains unchanged.
Rename ThinSlicePtr::get() to ThinSlicePtr::get_unchecked() #
ThinSlicePtr::get() has been deprecated in favor of the new ThinSlicePtr::get_unchecked() method in order to more clearly signal that bounds checking is not performed. Beyond the name change, the only difference between these two methods is that get_unchecked() takes &self while get() takes self. In order to migrate, simply rename all usages of get() with get_unchecked():
let slice: &[u32] = &[2, 4, 8];
let thin_slice = ThinSlicePtr::from(slice);
// 0.17
let x = unsafe { thin_slice.get(0) };
// 0.18
let x = unsafe { thin_slice.get_unchecked(0) };
AnimationEventTrigger::animation_player has been renamed to AnimationEventTrigger::target #
This field and its docs strongly suggested that it would point to an entity holding an AnimationPlayer, but that actually depends on how the event was registered.
- If you used
AnimationClip::add_event, the field really did point to theAnimationPlayer - But if you used
AnimationClip::add_event_to_target, this field instead pointed to anAnimationTargetId
To make this more clear, the field was renamed to target and the docs surrounding it improved.
DragEnter now fires on drag starts #
DragEnter now also fires when a drag starts over an already hovered entity.
Remove bevy::ptr::dangling_with_align() #
bevy::ptr::dangling_with_align() has been removed. Use NonNull::without_provenance() instead:
// 0.17
let ptr = dangling_with_align(align);
// 0.18
let ptr = NonNull::without_provenance(align);
AssetPlugin now has a use_asset_processor_override field. #
The AssetPlugin now has a use_asset_processor_override field. If you were previously setting all AssetPlugin fields, you should either use struct update syntax ..Default::default(), or set the new use_asset_processor_override to None.
enable_prepass and enable_shadows are now Material methods #
The MaterialPlugin fields prepass_enabled and shadows_enabled have been replaced by the Material methods enable_prepass and enable_shadows.
Analogous methods have also been added to MaterialExtension
// 0.17
MaterialPlugin::<MyMaterial> {
prepass_enabled: false,
shadows_enabled: false,
}
// 0.18
impl Material for MyMaterial {
/// ...
fn enable_prepass() { false }
fn enable_shadows() { false }
}
ArchetypeQueryData trait #
To support richer querying across relations, Bevy now supports query data that are not archetypal: the query can return entities based on conditions that do not exclusively involve the entity's archetype.
An example of non-archetypal filter is Changed<C>: the entity is filtered based on the archetype (having the component C) but also based on the change ticks of the component.
Code that requires queries to impl ExactSizeIterator may need to replace QueryData bounds with ArchetypeQueryData.
// 0.17
fn requires_exact_size<D: QueryData>(q: Query<D>) -> usize {
q.into_iter().len()
}
// 0.18
fn requires_exact_size<D: ArchetypeQueryData>(q: Query<D>) -> usize {
q.into_iter().len()
}
Manual implementations of QueryData will now need to provide the IS_ARCHETYPAL associated constant. This will be true for most existing queries, although queries that wrap other queries should delegate as appropriate. In addition, queries with IS_ARCHETYPAL = true should implement ArchetypeQueryData.
Put input sources for bevy_input under features #
bevy_input provides primitives for all kinds of input. But on consoles you usually don't have things like touch. On more obscure platforms, like GBA, only gamepad input is needed.
If you use bevy_window or bevy_gilrs, they will automatically enable the necessary features on bevy_input. If you don't depend on them (for example, if you are developing for a platform that isn't supported by these crates), you need to enable the required input sources on the bevy_input / bevy crate manually:
# 0.17
bevy = { version = "0.17", default-features = false }
# 0.18 (enable sources that you actually use):
bevy = { version = "0.18", default-features = false, features = [
"mouse",
"keyboard",
"gamepad",
"touch",
"gestures",
] }
TextLayoutInfo's section_rects field has been replaced with run_geometry #
TextLayoutInfo's section_rects field has been removed. In its place is a new field run_geometry that contains the non-glyph layout geometry for a run of glyphs: the run's span index, bounding rectangle, underline position and thickness, and strikethrough position and thickness. A run in bevy_text is a contiguous sequence of glyphs on the same line that share the same text attributes like font, font size, and line height. The coordinates stored in run_geometry are unscaled and relative to the top left corner of the text layout.
Unlike the tuples of section_rects, RunGeometry does not include an Entity id. To find the corresponding text entity, call the entities method on the root text entity’s ComputedTextBlock component and use the span_index to index into the returned slice.
Change Bundle::component_ids and Bundle::get_component_ids to return an iterator. #
Bundle::component_ids and Bundle::get_component_ids were changed to return an iterator over ComponentId and Option<ComponentId> respectively. In some cases this can avoid allocating.
For implementors:
// 0.17
unsafe impl<C: Component> Bundle for C {
fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)) {
ids(components.register_component::<C>());
}
fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>)) {
ids(components.get_id(TypeId::of::<C>()));
}
}
// 0.18
unsafe impl<C: Component> Bundle for C {
fn component_ids<(
components: &mut ComponentsRegistrator,
// we use a `use` here to explicitly not capture the lifetime of `components`
) -> impl Iterator<Item = ComponentId> + use<C> {
iter::once(components.register_component::<C>())
}
fn get_component_ids(components: &Components) -> impl Iterator<Item = Option<ComponentId>> {
iter::once(components.get_id(TypeId::of::<C>()))
}
}
For consumers:
// 0.17
let mut components = vec![];
MyBundle::component_ids(&mut world.components_registrator(), &mut |id| {
components.push(id);
});
// 0.18
let components: Vec<_> = B::component_ids(&mut world.components_registrator()).collect();
BorderRadius has been added to Node and is no longer a component #
BorderRadius is no longer a component, instead a border_radius: BorderRadius field has been added to Node.
Per-RenderPhase Draw Functions #
This PR makes draw function labels in MaterialProperties per-RenderPhase instead of per-pass. This should only affect users of the low-level "manual Material" API, and not users of the broader Material API. Specifying all draw functions is not mandatory, but users should specify draw functions for all render phases the material may queue to, or the material may not render.
Removed
MaterialDrawFunctionin favor of:MainPassOpaqueDrawFunctionMainPassAlphaMaskDrawFunctionMainPassTransmissiveDrawFunctionMainPassTransparentDrawFunction
Removed
PrepassDrawFunctionin favor of:PrepassOpaqueDrawFunctionPrepassAlphaMaskDrawFunction
Removed
DeferredDrawFunctionin favor of:DeferredOpaqueDrawFunctionDeferredAlphaMaskDrawFunction
Generalized Atmospheric Scattering Media #
Most of the fields on Atmosphere have been removed in favor of a handle to the new ScatteringMedium asset.
// 0.17
pub struct Atmosphere {
pub bottom_radius: f32,
pub top_radius: f32,
pub ground_albedo: Vec3,
// All of these fields have been removed.
pub rayleigh_density_exp_scale: f32,
pub rayleigh_scattering: Vec3,
pub mie_density_exp_scale: f32,
pub mie_scattering: f32,
pub mie_absorption: f32,
pub mie_asymmetry: f32,
pub ozone_layer_altitude: f32,
pub ozone_layer_width: f32,
pub ozone_absorption: Vec3,
}
// 0.18
pub struct Atmosphere {
pub bottom_radius: f32,
pub top_radius: f32,
pub ground_albedo: Vec3,
// This replaces all of the old fields.
pub medium: Handle<ScatteringMedium>,
}
Unfortunately, this means Atmosphere no longer implements Default. Instead, you can still access the default earthlike atmosphere through the EarthlikeAtmosphere resource:
fn setup_camera(
mut commands: Commands,
earthlike_atmosphere: Res<EarthlikeAtmosphere>
) {
commands.spawn((
Camera3d,
earthlike_atmosphere.get(),
));
}
Image Loader Array Layout #
ImageLoader now supports loading array textures using the new ImageLoaderSettings::array_layout setting.
In previous versions, loading an array texture generally required a system that waited for the asset to load, then called Image::reinterpret_stacked_2d_as_array. Now the ImageLoader can do that for you automatically.
use bevy::image::{ImageLoaderSettings, ImageArrayLayout};
let array_texture = asset_server.load_with_settings(
"textures/array_texture.png",
|settings: &mut ImageLoaderSettings| {
// Load the image as a stacked array of 4 textures.
settings.array_layout = Some(ImageArrayLayout::RowCount { rows: 4 });
},
);
Renamed bevy_reflect feature documentation to reflect_documentation #
The documentation feature in bevy_reflect has been renamed to reflect_documentation to clarify that it allows reflecting documentation of rust code at runtime.
Tilemap Chunk Layout #
TilemapChunk and TilemapChunkTileData's default layout has been changed from the origin being in the top left to the origin being in the bottom left.
The previous layout origin didn't align with Bevy's world coordinate system, so when mapping to and from chunk space (to map a world coordinate to a tile) you would have to account for the chunk y coordinate being inverted.
With the origin of the chunk being in the bottom left, you can simply mod world coordinates to get chunk coordinates.
Some other tiling tools have the convention of the origin being at the top left, but it's more important for Bevy's features to be internally consistent as it allows for better ease of use.
Renamed clear_children and clear_related methods to detach_* #
In summary, we renamed clear_* and remove_* methods to detach_*. This should clarify that these methods do not despawn the child entities or related entities.
We renamed several related methods on both EntityCommands and EntityWorldMut:
- The method
EntityCommands::clear_childrenhas been renamed toEntityCommands::detach_all_children. - The method
EntityWorldMut::clear_childrenhas been renamed toEntityWorldMut::detach_all_children. - The method
EntityCommands::remove_childrenhas been renamed toEntityCommands::detach_children. - The method
EntityWorldMut::remove_childrenhas been renamed toEntityWorldMut::detach_children. - The method
EntityCommands::remove_childhas been renamed toEntityCommands::detach_child. - The method
EntityWorldMut::remove_childhas been renamed toEntityWorldMut::detach_child. - The method
EntityCommands::clear_relatedhas been renamed toEntityCommands::detach_all_related. - The method
EntityWorldMut::clear_relatedhas been renamed toEntityWorldMut::detach_all_related.
LoadContext::path now returns AssetPath. #
LoadContext::asset_path has been removed, and LoadContext::path now returns AssetPath. So the migrations are:
load_context.asset_path()->load_context.path()load_context.path()->load_context.path().path()- While this migration will keep your code running, seriously consider whether you need to use the
Pathitself. ThePathdoes not support custom asset sources, so care needs to be taken when using it directly. Consider instead using theAssetPathinstead, along withAssetPath::resolve_embed, to properly support custom asset sources.
- While this migration will keep your code running, seriously consider whether you need to use the
Use Mesh::try_* mesh functions for Assets<Mesh> entries when there can be RenderAssetUsages::RENDER_WORLD-only meshes. #
Previously, the Assets<Mesh> resource would not retain RenderAssetUsages::RENDER_WORLD-only meshes once their data was extracted.
In 0.18, Assets<Mesh> retains RenderAssetUsages::RENDER_WORLD-only meshes, even after their data is extracted. To handle such meshes, Mesh now contains Mesh::try_* functions which return a Result<..., MeshAccessError>. These functions return an Err(MeshAccessError,ExtractedToRenderWorld) when the mesh has already been extracted.
If Assets<Mesh> can contain RenderAssetUsages::RENDER_WORLD-only meshes, the following Mesh functions should be changed to their try_* equivalent and handled appropriately:
// assets: Res<'w, Assets<Mesh>>
let mesh = assets.get(some_mesh_handle).unwrap() // or assets.get_mut(some_mesh_handle).unwrap()
// 0.17
mesh.insert_attribute(...)
mesh.with_inserted_attribute(...)
mesh.remove_attribute(...)
mesh.with_removed_attribute(...)
mesh.contains_attribute(...)
mesh.attribute(...)
mesh.attribute_mut(...)
mesh.attributes(...)
mesh.attributes_mut(...)
mesh.insert_indices(...)
mesh.with_inserted_indices(...)
mesh.indices(...)
mesh.indices_mut(...)
mesh.remove_indices(...)
mesh.with_removed_indices(...)
mesh.duplicate_vertices(...)
mesh.with_duplicated_vertices(...)
mesh.compute_normals(...)
mesh.compute_flat_normals(...)
mesh.compute_smooth_normals(...)
mesh.compute_area_weighted_normals(...)
mesh.compute_custom_smooth_normals(...)
mesh.with_computed_normals(...)
mesh.with_computed_flat_normals(...)
mesh.with_computed_smooth_normals(...)
mesh.with_computed_area_weighted_normals(...)
mesh.with_custom_smooth_normals(...)
mesh.transformed_by(...)
mesh.transform_by(...)
mesh.translated_by(...)
mesh.translate_by(...)
mesh.rotated_by(...)
mesh.rotate_by(...)
mesh.scaled_by(...)
mesh.scale_by(...)
mesh.normalize_joint_weights(...)
// when feature = morph enabled
mesh.has_morph_targets(...)
mesh.set_morph_targets(...)
mesh.morph_targets(...)
mesh.with_morph_targets(...)
mesh.set_morph_target_names(...)
mesh.with_morph_target_names(...)
mesh.morph_target_names(...)
// 0.18
mesh.try_insert_attribute(...)
mesh.try_with_inserted_attribute(...)
mesh.try_remove_attribute(...)
mesh.try_contains_attribute(...)
mesh.try_with_removed_attribute(...)
mesh.try_attribute_option(...) // or mesh.try_attribute(...)
mesh.try_attribute_mut_option(...) // or mesh.try_attribute_mut(...)
mesh.try_attributes(...)
mesh.try_attributes_mut(...)
mesh.try_insert_indices(...)
mesh.try_with_inserted_indices(...)
mesh.try_indices_option(...) // or mesh.try_indices(...)
mesh.try_indices_mut_option(...) // or mesh.try_indices_mut(...)
mesh.try_remove_indices(...)
mesh.try_with_removed_indices(...)
mesh.try_duplicate_vertices(...)
mesh.try_with_duplicated_vertices(...)
mesh.try_compute_normals(...)
mesh.try_compute_flat_normals(...)
mesh.try_compute_smooth_normals(...)
mesh.try_compute_area_weighted_normals(...)
mesh.try_compute_custom_smooth_normals(...)
mesh.try_with_computed_normals(...)
mesh.try_with_computed_flat_normals(...)
mesh.try_with_computed_smooth_normals(...)
mesh.try_with_computed_area_weighted_normals(...)
mesh.try_with_custom_smooth_normals(...)
mesh.try_transformed_by(...)
mesh.try_transform_by(...)
mesh.try_translated_by(...)
mesh.try_translate_by(...)
mesh.try_rotated_by(...)
mesh.try_rotate_by(...)
mesh.try_scaled_by(...)
mesh.try_scale_by(...)
mesh.try_normalize_joint_weights(...)
// when feature = morph enabled
mesh.try_has_morph_targets(...)
mesh.try_set_morph_targets(...)
mesh.try_morph_targets(...)
mesh.try_with_morph_targets(...)
mesh.try_set_morph_target_names(...)
mesh.try_with_morph_target_names(...)
mesh.try_morph_target_names(...)
// the following functions do not have a try_ equivalent, but now panic if
// the mesh data has been extracted to `RenderWorld`.
mesh.get_vertex_size(...)
mesh.get_vertex_buffer_size(...)
mesh.get_index_buffer_bytes(...)
mesh.get_mesh_vertex_buffer_layout(...)
mesh.count_vertices(...)
mesh.create_packed_vertex_buffer_data(...)
mesh.write_packed_vertex_buffer_data(...)
If the calls to Mesh functions (under the 0.17 section) are left unchanged after upgrading to 0.18, they will now panic if the mesh is a RenderAssetUsages::RENDER_WORLD-only mesh that has been extracted to the render world.
Custom asset sources now require a reader. #
Previously, it was possible to create asset sources with no reader, resulting in your asset sources silently being skipped. This is no longer possible, since AssetSourceBuilder must now be given a reader to start. We also slightly changed how sources are expected to be built.
// 0.17
AssetSource::build()
.with_reader(move || /* reader logic */)
.with_writer(move || /* ... */)
.with_processed_reader(move || /* ... */)
.with_processed_writer(move || /* ... */);
// 0.18
AssetSourceBuilder::new(move || /* reader logic */)
.with_writer(move || /* ... */)
.with_processed_reader(move || /* ... */)
.with_processed_writer(move || /* ... */;
ExtractedUiNode's stack_index has been renamed to z_order and is now an f32. #
ExtractedUiNode’s stack_index field has been renamed to z_order and its type changed from u32 to f32. Previously stack_index would be converted into an f32 after extraction during the Render schedule, then offsets would be applied to determine draw order before sorting (lowest value rendered first). For example, a node's fill color is given an offset of 0. and a box shadow is given an offset of -0.1, so that the shadow will be drawn behind the node.
Changing the field to an f32, enables finer control of the UI draw order by allowing these offsets to be applied during extraction, and fixes a bug affecting the ordering of texture-sliced nodes.
The non-text areas of UI Text nodes are no longer pickable #
Only the sections of Text node's containing text are pickable now, the non-text areas of the node do not register pointer hits. To replicate Bevy 0.17's picking behavior, use an intermediate parent node to intercept the pointer hits.
Cargo Feature Collections #
Bevy now has high-level cargo feature collections (ex: 2d, 3d, ui) and mid-level feature collections (ex: 2d_api, 3d_api, default_app, default_platform). This isn't technically a breaking change, but if you were previously disabling Bevy's default features and manually enabling each specific cargo feature you wanted, we highly recommend switching to using the higher level feature collections wherever possible. This will make it much easier to define the functionality you want, and it will reduce the burden of keeping your list of features up to date across Bevy releases.
See the Cargo Feature Collections pull request for a full list of options.
LineHeight is now a separate component #
The line_height field has been removed from TextFont. LineHeight is now a component required by Text, Text2d, and TextSpan.
AnimationTarget replaced by separate components #
The AnimationTarget component has been split into two separate components. AnimationTarget::id is now an AnimationTargetId component, and AnimationTarget::player is now an AnimatedBy component.
This change was made to add flexibility. It's now possible to calculate the AnimationTargetId first, but defer the choice of player until later.
// 0.17
entity.insert(AnimationTarget { id: AnimationTargetId(id), player: player_entity });
// 0.18
entity.insert((AnimationTargetId(id), AnimatedBy(player_entity)));
Virtual Geometry #
Virtual geometry's asset format has changed. You must regenerate MeshletMesh assets.
BevyManifest::shared is now a scope-like API. #
In previous versions of Bevy, BevyManifest returned a mapped RwLock guard. Now, it's a scope-like API:
// 0.16
let manifest = BevyManifest::shared();
let path = manifest.get_path("my_bevy_crate");
// 0.17
let path = BevyManifest::shared(|manifest| {
manifest.get_path("my_bevy_crate")
});
Entities APIs #
Entities are spawned by allocating their id and then giving that id a location within the world. In 0.17, this was done in one stroke through spawn and Entities::flush. In 0.18, the flushing functionality has been removed in favor of spawning individual EntityRows instead. Don't worry, these changes don't affect the common operations like spawn and despawn, but the did impact the peripheral interfaces and error types. For a full explanation of the new entity paradigm, errors and terms, see the new entity module docs. If you want more background for the justification of these changes or more information about where these new terms come from, see pr #19451. This opens up a lot of room for performance improvement but also caused a lot of breaking changes:
Entities rework #
A lot has changed here. First, alloc, free, reserve, reserve_entity, reserve_entities, flush, flush_as_invalid, EntityDoesNotExistError, total_count, used_count, and total_prospective_count have all been removed 😱.
Allocation has moved to the new EntitiesAllocator type, accessible via World::entities_allocator and World::entities_allocator_mut, which have alloc, free, and alloc_many.
Reservation and flushing have been completely removed as they are no longer needed. Instead of reserving an entity and later flushing it, you can EntitiesAllocator::alloc (which does not need mutable access), and World::spawn_at can be used to "flush" the entity.
The counting methods have been reworked in the absence of flushing: len and is_empty now deal with how many entity rows have been allocated (not necessarily the number that have been spawned), and the new count_spawned and any_spawned are similar to the old len and is_empty behavior but are now O(n).
In terms of getting information from Entities, get and contains has been reworked to include non-spawned entities. If you only want spawned entities, get_spawned and contains_spawned are available. Additionally, get now returns Result<Option<EntityLocation>, InvalidEntityError> instead of Option<EntityLocation> for clarity. Entities now may or may not have a location, depending on if it is spawned or not.
EntityDoesNotExistError has been removed and reworked. See the new entity module docs for more, but: When an entity's generation is not up to date with its row, InvalidEntityError is produced. When an entity index's Option<EntityLocation> is None, EntityValidButNotSpawnedError is produced. When an Entity is expected to be spawned but is not (either because its generation is outdated or because its row is not spawned), EntityNotSpawnedError is produced. A few other wrapper error types have slightly changed as well, generally moving from "entity does not exist" to "entity is not spawned".
Entity Ids #
Entity ids previously used "row" terminology, but now use "index" terminology as that more closely specifies its current implementation. As such, all functions and types dealing with the previous EntityRow have had their names 1 to 1 mapped to index. Ex: EntityRow -> EntityIndex, Entity::row -> Entity::index, Entity::from_row -> Entity::from_index, etc. Note that Entity::index did exist before. It served to give the numeric representation of the EntityRow. The same functionality exists, now under Entity::index_u32.
Entity Pointers #
When migrating, entity pointers, like EntityRef, were changed to assume that the entity they point to is spawned. This was not necessarily checked before, so the errors for creating an entity pointer is now EntityNotSpawnedError. This probably will not affect you since creating a pointer to an entity that was not spawned is kinda pointless.
It is still possible to invalidate an EntityWorldMut by despawning it from commands. (Ex: The hook for adding a component to an entity actually despawns the entity it was added to.) If that happens, it may lead to panics, but EntityWorldMut::is_spawned has been added to help detect that.
Entity Commands #
Commands::new_from_entities now also needs &EntitiesAllocator, which can be obtained from UnsafeWorldCell::entities_allocator. Commands::get_entity does not error for non-spawned entities, making it useful to amend an entity you have queued to spawn through commands. If you only want spawned entities, use Commands::get_spawned_entity.
Other entity interactions #
The ArchetypeRow::INVALID and ArchetypeId::INVALID constants have been removed, since they are no longer needed for flushing. If you depended on these, use options instead.
BundleSpawner::spawn_non_existent is now BundleSpawner::construct. World::inspect_entity now errors with EntityNotSpawnedError instead of EntityDoesNotExistError. QueryEntityError::EntityDoesNotExist is now QueryEntityError::NotSpawned. ComputeGlobalTransformError::NoSuchEntity and ComputeGlobalTransformError::MalformedHierarchy now wrap EntityNotSpawnedError.
Removed FontAtlasSets #
FontAtlasSetshas been removed.FontAtlasKeynow newtypes a(AssetId<Font>, u32, FontSmoothing).FontAtlasSetis now a resource. It newtypes aHashMap<FontAtlasKey, Vec<FontAtlas>>and derivesDerefandDerefMut.- Font atlases are looked up directly using a
FontAtlasKey, there's no longer a separateAssetId<Font>toFontAtlasKeymap. remove_dropped_font_atlas_setshas been renamed tofree_unused_font_atlases_system.- The
FontAtlasSetmethodsadd_glyph_to_atlas,get_glyph_atlas_info, andget_outlined_glyph_texturehave been moved into thefont_atlasmodule and reworked into free functions.
get_components, get_components_mut_unchecked now return a Result #
get_components, get_components_mut_unchecked, and into_components_mut_unchecked now return a Result<_, QueryAccessError> instead of an Option.
AmbientLight split into a component and a resource #
The AmbientLight used to be both a component and a resource. In 0.18, we've split this in two separate structs: AmbientLight and GlobalAmbientLight. The resource GlobalAmbientLight is the default ambient light for the entire world and automatically added by LightPlugin. Meanwhile, AmbientLight is a component that can be added to a Camera in order to override the default GlobalAmbientLight. When appropriate, rename AmbientLight to GlobalAmbientLight.
// 0.17
app.insert_resource(AmbientLight {
color: Color::WHITE,
brightness: 2000.,
..default()
});
// 0.18
app.insert_resource(GlobalAmbientLight {
color: Color::WHITE,
brightness: 2000.,
..default()
});
Derive on Resource will fail when using non-static lifetimes #
Any type with #[derive(Resource)] that uses non-static lifetime will no longer compile.
// Will no longer compile in 0.18, `'a` should be `'static`.
#[derive(Resource)]
struct Foo<'a> {
bar: &'a str
}
RenderPipelineDescriptor and ComputePipelineDescriptor now hold a BindGroupLayoutDescriptor #
In previous versions of Bevy, RenderPipelineDescriptor and ComputePipelineDescriptor held a BindGroupLayout to describe the layout of shader bind groups, depending directly on wgpu's BindGroupLayout. Now, they hold a new type BindGroupLayoutDescriptor which holds the BindGroupLayoutEntrys directly. The descriptors are used to create BindGroupLayouts when they are first needed by a pipeline, and cached for reuse.
Concretely, this means wherever you were using a RenderDevice to create a BindGroupLayout to store in a RenderPipelineDescriptor or ComputePipelineDescriptor, you will now create a BindGroupLayoutDescriptor:
// 0.17
let bind_group_layout = render_device.create_bind_group_layout(
// ...
);
commands.insert_resource(MyPipeline {
bind_group_layout,
/// ...
});
// ...
let bind_group = render_context.render_device().create_bind_group(
None
&my_pipeline.bind_group_layout,
// ...
);
// 0.18
let bind_group_layout = BindGroupLayoutDescriptor::new(
// ...
);
commands.insert_resource(MyPipeline {
bind_group_layout,
/// ...
});
// ...
let bind_group = render_context.render_device().create_bind_group(
None
&pipeline_cache.get_bind_group_layout(&my_pipeline.bind_group),
// ...
);
Gizmos::cuboid has been renamed to Gizmos::cube #
Gizmos::cuboid was renamed to Gizmos::cube.
API for working with Relationships and RelationshipTargets in type-erased contexts #
ComponentDescriptor now stores additional data for working with relationships in dynamic contexts. This resulted in changes to ComponentDescriptor::new_with_layout:
- Now requires additional parameter
relationship_accessor, which should be set toNonefor all existing code creatingComponentDescriptors.
Internal has been removed #
The Internal component, previously added as a required component to both one-shot systems and observer entities has been removed.
You can remove all references to it: these entities are no longer hidden by default query filters. If you have tests which rely on a specific number of entities existing in the world, you should refactor them to query for entities with a component that you care about: this is much more robust in general.
This component was previously motivated by two factors:
- A desire to protect users from accidentally modifying engine internals, breaking their app in subtle and complex ways.
- A unified API for entity inspectors, allowing them to readily distinguish between "engine-internal" and "user-defined" entities.
In practice, we found that this increased user friction and confusion without meaningfully improving robustness. Entity inspectors and similar tools can and should define their own entity categorization functionality: simply lumping all "internal" entities together is rarely helpful.
Automatic Aabb updates for sprites and meshes #
Bevy automatically creates an Aabb component for entities containing a mesh or sprite - the Aabb is then used for visibility and picking.
In 0.17 the Aabb was not updated if the mesh or sprite was modified. This has been fixed in 0.18. If you were working around the issue by manually updating or removing the Aabb, then the workaround is no longer needed.
// 0.17: Modify the mesh, and remove the `Aabb` so that it's automatically
// recreated from the modified mesh.
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, new_positions);
entity.remove::<Aabb>();
// 0.18: The `Aabb` will be automatically updated.
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, new_positions);
For users who want more control, 0.18 also adds a NoAutoAabb component. This will disable both automatic creation and automatic update of Aabb components.
entity.insert(NoAutoAabb);
Changes to AssetServer and AssetProcessor creation. #
Previously AssetServers new and new_with_method_check methods would take AssetSources. Now, these methods take Arc<AssetSources>.
// 0.17
AssetServer::new(
sources,
mode,
watching_for_changes,
unapproved_path_mode,
)
// 0.18
AssetServer::new(
// Wrap the sources in an `Arc`.
Arc::new(sources),
mode,
watching_for_changes,
unapproved_path_mode,
)
AssetProcessor::new has also changed. It now returns to you the Arc<AssetSources> which can (and should) be shared with the AssetServer.
// 0.17
let processor = AssetProcessor::new(sources);
// 0.18
let (processor, sources_arc) = AssetProcessor::new(
sources,
// A bool whether the returned sources should listen for changes as asset processing completes.
false,
);
Same State Transitions #
Setting the next state will now always trigger state transitions like OnEnter and OnExit, even if the state is already the same. If you depend on the previous behavior, you can use the set_if_neq method instead.
// 0.17
next_state.set(State::Menu);
// 0.18
next_state.set_if_neq(State::Menu);
Image::reinterpret_size and Image::reinterpret_stacked_2d_as_array now return a Result #
Image::reinterpret_size and Image::reinterpret_stacked_2d_as_array now return a Result instead of panicking.
Previously, calling this method on image assets that did not conform to certain constraints could lead to runtime panics. The new return type makes the API safer and more explicit about the constraints.
To migrate your code, you will need to handle the Result returned by Image::reinterpret_size or Image::reinterpret_stacked_2d_as_array.
Changes to the Process trait in bevy_asset. #
ProcessContext no longer includes asset_bytes. This has been replaced by asset_reader. To maintain current behavior in a Process implementation, you can read all the bytes into a memory Vec.
// 0.17
let bytes = context.asset_bytes();
// 0.18
let reader = context.asset_reader();
let mut bytes = vec![];
reader
.read_to_end(&mut bytes)
.await
.map_err(|err| ProcessError::AssetReaderError {
path: context.path().clone_owned(),
err: err.into(),
})?;
BindGroupLayout labels are no longer optional #
In previous versions of Bevy, the label of a BindGroupLayout was optional. This practically only applies when implementing AsBindGroup manually without the AsBindGroup derive.
If you were previously omitting the label implementation from a impl AsBindGroup, you now must implement it:
impl AsBindGroup for CoolMaterial {
// ...
fn label() -> &'static str {
// It is customary to make the label the name of the type.
"CoolMaterial"
}
}
Traits AssetLoader, AssetTransformer, AssetSaver, and Process all now require TypePath #
The AssetLoader, AssetTransformer, AssetSaver, and Process traits now include a super trait of TypePath. This means if you previously had a loader like:
// 0.17
struct MyFunkyLoader {
add_funk: u32,
}
You will need to add the following derive:
// 0.18
#[derive(TypePath)]
struct MyFunkyLoader {
add_funk: u32,
}
TypePath comes from bevy_reflect, so libraries may also need to add a dependency on bevy_reflect.
bevy_gizmos rendering split #
The rendering backend of bevy_gizmos has been split off into bevy_gizmos_render. If you were using default-features = false and bevy_gizmos and bevy_render, you may want to enable the bevy_gizmos_render feature now.
Replaced Column with ThinColumn #
The low-level Column and ThinColumn types in bevy_ecs have been merged into a single type, now called Column but with the api of ThinColumn. This type does not keep track of its own allocated length, and only provides unsafe methods.
Renamed bevy_platform::HashMap::get_many_* to bevy_platform::HashMap::get_disjoint_* #
Matching both hashbrown and the std library, we've renamed all the get_many_* methods on bevy_platform::HashMap to get_disjoint_*. So rename:
get_many_mut->get_disjoint_mutget_many_unchecked_mut->get_disjoint_unchecked_mutget_many_key_value_mut->get_disjoint_key_value_mutget_many_key_value_unchecked_mut->get_disjoint_key_value_unchecked_mut
glTF Coordinate Conversion #
Bevy 0.17 added options for coordinate conversion of glTF files - GltfPlugin::use_model_forward_direction and GltfLoaderSettings::use_model_forward_direction. In Bevy 0.18 these options have changed. The options are disabled by default, so if you haven't enabled them then your glTFs will work the same as before.
CAUTION: The options are experimental, and their behavior may change in future versions.
The goal of coordinate conversion is to take objects that face forward in the glTF and change them to match the direction of Bevy's Transform::forward. Conversion is necessary because glTF's standard scene forward is +Z, while Bevy's is -Z (although not all glTF files follow the standard, and there are exceptions for cameras and lights).
In 0.17 the conversion was applied to nodes and meshes within glTF scenes. This worked well for some users, but had bugs and didn't work well for other users. In particular, node conversion caused issues with cameras and lights.
In 0.18 there are two changes. Firstly, the use_model_forward_direction option has been renamed to convert_coordinates, and is now a struct with two separate options.
// 0.17
pub struct GltfPlugin {
use_model_forward_direction: bool,
// ...
}
// 0.18
pub struct GltfPlugin {
convert_coordinates: GltfConvertCoordinates,
// ...
}
pub struct GltfConvertCoordinates {
rotate_scene_entity: bool,
rotate_meshes: bool,
}
Secondly, the conversion behavior has changed. Nodes within the glTF scene are no longer converted - instead a new conversion is applied to the scene entity and mesh primitive entities. Whether these changes affect you will depend on how you're using glTFs.
If you never enabled the 0.17 conversion then you don't need to change anything - conversion remains disabled by default in 0.18. To check if you enabled the conversion, search for
use_model_forward_direction.If you simply spawn your glTF via
SceneRootand want it to visually match theTransform::forwardof the entity it's spawned on, then you're still supported. The internals of the scene will be different in 0.18, but the visual result will be the same. The only option you need to enable isGltfConvertCoordinates::rotate_scene_entity.If you want the
Meshassets in your glTF to be converted then you're supported by theGltfConvertCoordinates::rotate_meshesoption. This can be combined with therotate_scene_entityoption if you want both.If you enabled the 0.17 conversion and aren't sure what to enable in 0.18, try enabling both the
rotate_scene_entityandrotate_meshesoptions. This will be closest to the 0.17 behavior.If you tried the 0.17 conversion but found it caused issues with cameras or lights, then the 0.18 conversion should fix these issues.
If you relied on node conversion, you'll find that 0.18 no longer applies that conversion. This change was made to avoid bugs and give other users more options.
If you want to try out glTF coordinate conversion, the simplest method is to set GltfPlugin::convert_coordinates - this option can be set on app startup, and is applied to all glTFs when they're loaded. For an app that uses DefaultPlugins, the example below shows how to enable just scene conversion.
App::new()
.add_plugins(DefaultPlugins.set(GltfPlugin {
convert_coordinates: GltfConvertCoordinates { rotate_scene_entity: true, ..default() },
..default()
}))
.run();
If you want finer control, you can choose the option per-glTF with GltfLoaderSettings.
let handle = asset_server.load_with_settings(
"fox.gltf#Scene0",
|settings: &mut GltfLoaderSettings| {
settings.convert_coordinates = Some(GltfConvertCoordinates { rotate_scene_entity: true, ..default() });
},
);
TrackedRenderPass::set_index_buffer no longer takes buffer offset #
TrackedRenderPass::set_index_buffer no longer takes a separate buffer offset argument, which wasn't actually forwarded to wgpu. You have already needed to pass a BufferSlice that is sliced to the desired offset/size.
// 0.17
pass.set_index_buffer(indices.slice(1..), 1, IndexFormat::Uint32);
// 0.18
pass.set_index_buffer(indices.slice(1..), IndexFormat::Uint32);
BorderRect now has Vec2 fields #
The directional BorderRect fields (left, right, top, and bottom) have been replaced with min_inset and max_inset Vec2 fields.
Using min_inset and max_inset removes the need to interpret top or bottom relative to the coordinate system, so the same logic will work consistently in both UI and 2D.