>(
group: &mut BenchmarkGroup,
name: &str,
curve: CubicCurve,
diff --git a/crates/bevy_a11y/Cargo.toml b/crates/bevy_a11y/Cargo.toml
index 5ffab33d63..39628ec046 100644
--- a/crates/bevy_a11y/Cargo.toml
+++ b/crates/bevy_a11y/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_a11y"
version = "0.16.0-dev"
edition = "2024"
description = "Provides accessibility support for Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy", "accessibility", "a11y"]
diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs
index 94468c148c..f8c46757dd 100644
--- a/crates/bevy_a11y/src/lib.rs
+++ b/crates/bevy_a11y/src/lib.rs
@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
#![no_std]
diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml
index 11e819806c..9f9cd26587 100644
--- a/crates/bevy_animation/Cargo.toml
+++ b/crates/bevy_animation/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_animation"
version = "0.16.0-dev"
edition = "2024"
description = "Provides animation functionality for Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs
index 688011a32c..593ca04d2e 100644
--- a/crates/bevy_animation/src/gltf_curves.rs
+++ b/crates/bevy_animation/src/gltf_curves.rs
@@ -55,7 +55,7 @@ pub struct CubicKeyframeCurve {
impl Curve for CubicKeyframeCurve
where
- V: VectorSpace,
+ V: VectorSpace,
{
#[inline]
fn domain(&self) -> Interval {
@@ -179,7 +179,7 @@ pub struct WideLinearKeyframeCurve {
impl IterableCurve for WideLinearKeyframeCurve
where
- T: VectorSpace,
+ T: VectorSpace,
{
#[inline]
fn domain(&self) -> Interval {
@@ -289,7 +289,7 @@ pub struct WideCubicKeyframeCurve {
impl IterableCurve for WideCubicKeyframeCurve
where
- T: VectorSpace,
+ T: VectorSpace,
{
#[inline]
fn domain(&self) -> Interval {
@@ -406,7 +406,7 @@ fn cubic_spline_interpolation(
step_duration: f32,
) -> T
where
- T: VectorSpace,
+ T: VectorSpace,
{
let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp;
value_start * (coeffs.x * lerp + 1.0)
@@ -415,7 +415,7 @@ where
+ tangent_in_end * step_duration * lerp * coeffs.w
}
-fn cubic_spline_interpolate_slices<'a, T: VectorSpace>(
+fn cubic_spline_interpolate_slices<'a, T: VectorSpace>(
width: usize,
first: &'a [T],
second: &'a [T],
diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs
index 21ea15f96f..dd68595961 100644
--- a/crates/bevy_animation/src/lib.rs
+++ b/crates/bevy_animation/src/lib.rs
@@ -1,8 +1,8 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
//! Animation for the game engine Bevy
diff --git a/crates/bevy_anti_aliasing/Cargo.toml b/crates/bevy_anti_aliasing/Cargo.toml
index 5a8e48ecb5..c54608a883 100644
--- a/crates/bevy_anti_aliasing/Cargo.toml
+++ b/crates/bevy_anti_aliasing/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_anti_aliasing"
version = "0.16.0-dev"
edition = "2024"
description = "Provides various anti aliasing implementations for Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_anti_aliasing/src/experimental/mod.rs b/crates/bevy_anti_aliasing/src/experimental/mod.rs
deleted file mode 100644
index a8dc522c56..0000000000
--- a/crates/bevy_anti_aliasing/src/experimental/mod.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-//! Experimental rendering features.
-//!
-//! Experimental features are features with known problems, missing features,
-//! compatibility issues, low performance, and/or future breaking changes, but
-//! are included nonetheless for testing purposes.
-
-pub mod taa {
- pub use crate::taa::{TemporalAntiAliasNode, TemporalAntiAliasPlugin, TemporalAntiAliasing};
-}
diff --git a/crates/bevy_anti_aliasing/src/lib.rs b/crates/bevy_anti_aliasing/src/lib.rs
index be09a2e5b2..12b7982cb5 100644
--- a/crates/bevy_anti_aliasing/src/lib.rs
+++ b/crates/bevy_anti_aliasing/src/lib.rs
@@ -2,26 +2,25 @@
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
use bevy_app::Plugin;
use contrast_adaptive_sharpening::CasPlugin;
use fxaa::FxaaPlugin;
use smaa::SmaaPlugin;
+use taa::TemporalAntiAliasPlugin;
pub mod contrast_adaptive_sharpening;
-pub mod experimental;
pub mod fxaa;
pub mod smaa;
-
-mod taa;
+pub mod taa;
#[derive(Default)]
pub struct AntiAliasingPlugin;
impl Plugin for AntiAliasingPlugin {
fn build(&self, app: &mut bevy_app::App) {
- app.add_plugins((FxaaPlugin, CasPlugin, SmaaPlugin));
+ app.add_plugins((FxaaPlugin, SmaaPlugin, TemporalAntiAliasPlugin, CasPlugin));
}
}
diff --git a/crates/bevy_anti_aliasing/src/taa/mod.rs b/crates/bevy_anti_aliasing/src/taa/mod.rs
index efc5051680..0f706146b1 100644
--- a/crates/bevy_anti_aliasing/src/taa/mod.rs
+++ b/crates/bevy_anti_aliasing/src/taa/mod.rs
@@ -62,7 +62,7 @@ impl Plugin for TemporalAntiAliasPlugin {
.add_systems(
Render,
(
- prepare_taa_jitter_and_mip_bias.in_set(RenderSystems::ManageViews),
+ prepare_taa_jitter.in_set(RenderSystems::ManageViews),
prepare_taa_pipelines.in_set(RenderSystems::Prepare),
prepare_taa_history_textures.in_set(RenderSystems::PrepareResources),
),
@@ -113,7 +113,6 @@ impl Plugin for TemporalAntiAliasPlugin {
///
/// # Usage Notes
///
-/// The [`TemporalAntiAliasPlugin`] must be added to your app.
/// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`].
///
/// [Currently](https://github.com/bevyengine/bevy/issues/8423), TAA cannot be used with [`bevy_render::camera::OrthographicProjection`].
@@ -126,11 +125,9 @@ impl Plugin for TemporalAntiAliasPlugin {
///
/// 1. Write particle motion vectors to the motion vectors prepass texture
/// 2. Render particles after TAA
-///
-/// If no [`MipBias`] component is attached to the camera, TAA will add a `MipBias(-1.0)` component.
#[derive(Component, Reflect, Clone)]
#[reflect(Component, Default, Clone)]
-#[require(TemporalJitter, DepthPrepass, MotionVectorPrepass)]
+#[require(TemporalJitter, MipBias, DepthPrepass, MotionVectorPrepass)]
#[doc(alias = "Taa")]
pub struct TemporalAntiAliasing {
/// Set to true to delete the saved temporal history (past frames).
@@ -345,16 +342,11 @@ impl SpecializedRenderPipeline for TaaPipeline {
}
fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut) {
- let mut cameras_3d = main_world.query_filtered::<(
+ let mut cameras_3d = main_world.query::<(
RenderEntity,
&Camera,
&Projection,
- &mut TemporalAntiAliasing,
- ), (
- With,
- With,
- With,
- With,
+ Option<&mut TemporalAntiAliasing>,
)>();
for (entity, camera, camera_projection, mut taa_settings) in
@@ -364,14 +356,12 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut();
@@ -379,13 +369,22 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut,
- mut query: Query<(Entity, &mut TemporalJitter, Option<&MipBias>), With>,
- mut commands: Commands,
+ mut query: Query<
+ &mut TemporalJitter,
+ (
+ With,
+ With,
+ With,
+ With,
+ With,
+ ),
+ >,
) {
- // Halton sequence (2, 3) - 0.5, skipping i = 0
+ // Halton sequence (2, 3) - 0.5
let halton_sequence = [
+ vec2(0.0, 0.0),
vec2(0.0, -0.16666666),
vec2(-0.25, 0.16666669),
vec2(0.25, -0.3888889),
@@ -393,17 +392,12 @@ fn prepare_taa_jitter_and_mip_bias(
vec2(0.125, 0.2777778),
vec2(-0.125, -0.2777778),
vec2(0.375, 0.055555582),
- vec2(-0.4375, 0.3888889),
];
let offset = halton_sequence[frame_count.0 as usize % halton_sequence.len()];
- for (entity, mut jitter, mip_bias) in &mut query {
+ for mut jitter in &mut query {
jitter.offset = offset;
-
- if mip_bias.is_none() {
- commands.entity(entity).insert(MipBias(-1.0));
- }
}
}
diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml
index c892860dce..6b6120f182 100644
--- a/crates/bevy_app/Cargo.toml
+++ b/crates/bevy_app/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_app"
version = "0.16.0-dev"
edition = "2024"
description = "Provides core App functionality for Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
@@ -71,6 +71,12 @@ web = [
"dep:console_error_panic_hook",
]
+hotpatching = [
+ "bevy_ecs/hotpatching",
+ "dep:dioxus-devtools",
+ "dep:crossbeam-channel",
+]
+
[dependencies]
# bevy
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }
@@ -87,8 +93,10 @@ variadics_please = "1.1"
tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false }
cfg-if = "1.0.0"
+dioxus-devtools = { version = "0.7.0-alpha.1", optional = true }
+crossbeam-channel = { version = "0.5.0", optional = true }
-[target.'cfg(any(unix, windows))'.dependencies]
+[target.'cfg(any(all(unix, not(target_os = "horizon")), windows))'.dependencies]
ctrlc = { version = "3.4.4", optional = true }
[target.'cfg(target_arch = "wasm32")'.dependencies]
diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs
index 4878ed9b56..75542da41b 100644
--- a/crates/bevy_app/src/app.rs
+++ b/crates/bevy_app/src/app.rs
@@ -1483,8 +1483,8 @@ mod tests {
component::Component,
entity::Entity,
event::{Event, EventWriter, Events},
+ lifecycle::RemovedComponents,
query::With,
- removal_detection::RemovedComponents,
resource::Resource,
schedule::{IntoScheduleConfigs, ScheduleLabel},
system::{Commands, Query},
diff --git a/crates/bevy_app/src/hotpatch.rs b/crates/bevy_app/src/hotpatch.rs
new file mode 100644
index 0000000000..1f9da40730
--- /dev/null
+++ b/crates/bevy_app/src/hotpatch.rs
@@ -0,0 +1,42 @@
+//! Utilities for hotpatching code.
+extern crate alloc;
+
+use alloc::sync::Arc;
+
+use bevy_ecs::{event::EventWriter, HotPatched};
+#[cfg(not(target_family = "wasm"))]
+use dioxus_devtools::connect_subsecond;
+use dioxus_devtools::subsecond;
+
+pub use dioxus_devtools::subsecond::{call, HotFunction};
+
+use crate::{Last, Plugin};
+
+/// Plugin connecting to Dioxus CLI to enable hot patching.
+#[derive(Default)]
+pub struct HotPatchPlugin;
+
+impl Plugin for HotPatchPlugin {
+ fn build(&self, app: &mut crate::App) {
+ let (sender, receiver) = crossbeam_channel::bounded::(1);
+
+ // Connects to the dioxus CLI that will handle rebuilds
+ // This will open a connection to the dioxus CLI to receive updated jump tables
+ // Sends a `HotPatched` message through the channel when the jump table is updated
+ #[cfg(not(target_family = "wasm"))]
+ connect_subsecond();
+ subsecond::register_handler(Arc::new(move || {
+ sender.send(HotPatched).unwrap();
+ }));
+
+ // Adds a system that will read the channel for new `HotPatched`, and forward them as event to the ECS
+ app.add_event::().add_systems(
+ Last,
+ move |mut events: EventWriter| {
+ if receiver.try_recv().is_ok() {
+ events.write_default();
+ }
+ },
+ );
+ }
+}
diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs
index 743806df71..188ba957f6 100644
--- a/crates/bevy_app/src/lib.rs
+++ b/crates/bevy_app/src/lib.rs
@@ -8,8 +8,8 @@
#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))]
#![forbid(unsafe_code)]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
#![no_std]
@@ -28,21 +28,26 @@ mod main_schedule;
mod panic_handler;
mod plugin;
mod plugin_group;
+mod propagate;
mod schedule_runner;
mod sub_app;
mod task_pool_plugin;
-#[cfg(all(any(unix, windows), feature = "std"))]
+#[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))]
mod terminal_ctrl_c_handler;
+#[cfg(feature = "hotpatching")]
+pub mod hotpatch;
+
pub use app::*;
pub use main_schedule::*;
pub use panic_handler::*;
pub use plugin::*;
pub use plugin_group::*;
+pub use propagate::*;
pub use schedule_runner::*;
pub use sub_app::*;
pub use task_pool_plugin::*;
-#[cfg(all(any(unix, windows), feature = "std"))]
+#[cfg(all(any(all(unix, not(target_os = "horizon")), windows), feature = "std"))]
pub use terminal_ctrl_c_handler::*;
/// The app prelude.
diff --git a/crates/bevy_app/src/panic_handler.rs b/crates/bevy_app/src/panic_handler.rs
index 1021a3dc2e..c35d2333bf 100644
--- a/crates/bevy_app/src/panic_handler.rs
+++ b/crates/bevy_app/src/panic_handler.rs
@@ -1,4 +1,4 @@
-//! This module provides panic handlers for [Bevy](https://bevyengine.org)
+//! This module provides panic handlers for [Bevy](https://bevy.org)
//! apps, and automatically configures platform specifics (i.e. Wasm or Android).
//!
//! By default, the [`PanicHandlerPlugin`] from this crate is included in Bevy's `DefaultPlugins`.
diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs
new file mode 100644
index 0000000000..754ba3140e
--- /dev/null
+++ b/crates/bevy_app/src/propagate.rs
@@ -0,0 +1,551 @@
+use alloc::vec::Vec;
+use core::marker::PhantomData;
+
+use crate::{App, Plugin, Update};
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ hierarchy::ChildOf,
+ lifecycle::RemovedComponents,
+ query::{Changed, Or, QueryFilter, With, Without},
+ relationship::{Relationship, RelationshipTarget},
+ schedule::{IntoScheduleConfigs, SystemSet},
+ system::{Commands, Local, Query},
+};
+
+/// Plugin to automatically propagate a component value to all direct and transient relationship
+/// targets (e.g. [`bevy_ecs::hierarchy::Children`]) of entities with a [`Propagate`] component.
+///
+/// The plugin Will maintain the target component over hierarchy changes, adding or removing
+/// `C` when a relationship `R` (e.g. [`ChildOf`]) is added to or removed from a
+/// relationship tree with a [`Propagate`] source, or if the [`Propagate`] component
+/// is added, changed or removed.
+///
+/// Optionally you can include a query filter `F` to restrict the entities that are updated.
+/// Note that the filter is not rechecked dynamically: changes to the filter state will not be
+/// picked up until the [`Propagate`] component is touched, or the hierarchy is changed.
+/// All members of the tree between source and target must match the filter for propagation
+/// to reach a given target.
+/// Individual entities can be skipped or terminate the propagation with the [`PropagateOver`]
+/// and [`PropagateStop`] components.
+pub struct HierarchyPropagatePlugin<
+ C: Component + Clone + PartialEq,
+ F: QueryFilter = (),
+ R: Relationship = ChildOf,
+>(PhantomData (C, F, R)>);
+
+/// Causes the inner component to be added to this entity and all direct and transient relationship
+/// targets. A target with a [`Propagate`] component of its own will override propagation from
+/// that point in the tree.
+#[derive(Component, Clone, PartialEq)]
+pub struct Propagate(pub C);
+
+/// Stops the output component being added to this entity.
+/// Relationship targets will still inherit the component from this entity or its parents.
+#[derive(Component)]
+pub struct PropagateOver(PhantomData C>);
+
+/// Stops the propagation at this entity. Children will not inherit the component.
+#[derive(Component)]
+pub struct PropagateStop(PhantomData C>);
+
+/// The set in which propagation systems are added. You can schedule your logic relative to this set.
+#[derive(SystemSet, Clone, PartialEq, PartialOrd, Ord)]
+pub struct PropagateSet {
+ _p: PhantomData C>,
+}
+
+/// Internal struct for managing propagation
+#[derive(Component, Clone, PartialEq)]
+pub struct Inherited(pub C);
+
+impl Default
+ for HierarchyPropagatePlugin
+{
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+impl Default for PropagateOver {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+impl Default for PropagateStop {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+impl core::fmt::Debug for PropagateSet {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ f.debug_struct("PropagateSet")
+ .field("_p", &self._p)
+ .finish()
+ }
+}
+
+impl Eq for PropagateSet {}
+impl core::hash::Hash for PropagateSet {
+ fn hash(&self, state: &mut H) {
+ self._p.hash(state);
+ }
+}
+
+impl Default for PropagateSet {
+ fn default() -> Self {
+ Self {
+ _p: Default::default(),
+ }
+ }
+}
+
+impl Plugin
+ for HierarchyPropagatePlugin
+{
+ fn build(&self, app: &mut App) {
+ app.add_systems(
+ Update,
+ (
+ update_source::,
+ update_stopped::,
+ update_reparented::,
+ propagate_inherited::,
+ propagate_output::,
+ )
+ .chain()
+ .in_set(PropagateSet::::default()),
+ );
+ }
+}
+
+/// add/remove `Inherited::` and `C` for entities with a direct `Propagate::`
+pub fn update_source(
+ mut commands: Commands,
+ changed: Query<
+ (Entity, &Propagate),
+ (
+ Or<(Changed>, Without>)>,
+ Without>,
+ ),
+ >,
+ mut removed: RemovedComponents>,
+) {
+ for (entity, source) in &changed {
+ commands
+ .entity(entity)
+ .try_insert(Inherited(source.0.clone()));
+ }
+
+ for removed in removed.read() {
+ if let Ok(mut commands) = commands.get_entity(removed) {
+ commands.remove::<(Inherited, C)>();
+ }
+ }
+}
+
+/// remove `Inherited::` and `C` for entities with a `PropagateStop::`
+pub fn update_stopped(
+ mut commands: Commands,
+ q: Query>, With>, F)>,
+) {
+ for entity in q.iter() {
+ let mut cmds = commands.entity(entity);
+ cmds.remove::<(Inherited, C)>();
+ }
+}
+
+/// add/remove `Inherited::` and `C` for entities which have changed relationship
+pub fn update_reparented(
+ mut commands: Commands,
+ moved: Query<
+ (Entity, &R, Option<&Inherited>),
+ (
+ Changed,
+ Without>,
+ Without>,
+ F,
+ ),
+ >,
+ relations: Query<&Inherited>,
+ orphaned: Query>, Without>, Without, F)>,
+) {
+ for (entity, relation, maybe_inherited) in &moved {
+ if let Ok(inherited) = relations.get(relation.get()) {
+ commands.entity(entity).try_insert(inherited.clone());
+ } else if maybe_inherited.is_some() {
+ commands.entity(entity).remove::<(Inherited, C)>();
+ }
+ }
+
+ for orphan in &orphaned {
+ commands.entity(orphan).remove::<(Inherited, C)>();
+ }
+}
+
+/// add/remove `Inherited::` for targets of entities with modified `Inherited::`
+pub fn propagate_inherited(
+ mut commands: Commands,
+ changed: Query<
+ (&Inherited, &R::RelationshipTarget),
+ (Changed>, Without>, F),
+ >,
+ recurse: Query<
+ (Option<&R::RelationshipTarget>, Option<&Inherited>),
+ (Without>, Without>, F),
+ >,
+ mut removed: RemovedComponents>,
+ mut to_process: Local>)>>,
+) {
+ // gather changed
+ for (inherited, targets) in &changed {
+ to_process.extend(
+ targets
+ .iter()
+ .map(|target| (target, Some(inherited.clone()))),
+ );
+ }
+
+ // and removed
+ for entity in removed.read() {
+ if let Ok((Some(targets), _)) = recurse.get(entity) {
+ to_process.extend(targets.iter().map(|target| (target, None)));
+ }
+ }
+
+ // propagate
+ while let Some((entity, maybe_inherited)) = (*to_process).pop() {
+ let Ok((maybe_targets, maybe_current)) = recurse.get(entity) else {
+ continue;
+ };
+
+ if maybe_current == maybe_inherited.as_ref() {
+ continue;
+ }
+
+ if let Some(targets) = maybe_targets {
+ to_process.extend(
+ targets
+ .iter()
+ .map(|target| (target, maybe_inherited.clone())),
+ );
+ }
+
+ if let Some(inherited) = maybe_inherited {
+ commands.entity(entity).try_insert(inherited.clone());
+ } else {
+ commands.entity(entity).remove::<(Inherited, C)>();
+ }
+ }
+}
+
+/// add `C` to entities with `Inherited::`
+pub fn propagate_output(
+ mut commands: Commands,
+ changed: Query<
+ (Entity, &Inherited, Option<&C>),
+ (Changed>, Without>, F),
+ >,
+) {
+ for (entity, inherited, maybe_current) in &changed {
+ if maybe_current.is_some_and(|c| &inherited.0 == c) {
+ continue;
+ }
+
+ commands.entity(entity).try_insert(inherited.0.clone());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use bevy_ecs::schedule::Schedule;
+
+ use crate::{App, Update};
+
+ use super::*;
+
+ #[derive(Component, Clone, PartialEq, Debug)]
+ struct TestValue(u32);
+
+ #[test]
+ fn test_simple_propagate() {
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::::default());
+
+ let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let intermediate = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagator))
+ .id();
+ let propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(intermediate))
+ .id();
+
+ app.update();
+
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_ok());
+ }
+
+ #[test]
+ fn test_reparented() {
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::::default());
+
+ let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagator))
+ .id();
+
+ app.update();
+
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_ok());
+ }
+
+ #[test]
+ fn test_reparented_with_prior() {
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::::default());
+
+ let propagator_a = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let propagator_b = app.world_mut().spawn(Propagate(TestValue(2))).id();
+ let propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagator_a))
+ .id();
+
+ app.update();
+ assert_eq!(
+ app.world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee),
+ Ok(&TestValue(1))
+ );
+ app.world_mut()
+ .commands()
+ .entity(propagatee)
+ .insert(ChildOf(propagator_b));
+ app.update();
+ assert_eq!(
+ app.world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee),
+ Ok(&TestValue(2))
+ );
+ }
+
+ #[test]
+ fn test_remove_orphan() {
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::::default());
+
+ let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagator))
+ .id();
+
+ app.update();
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_ok());
+ app.world_mut()
+ .commands()
+ .entity(propagatee)
+ .remove::();
+ app.update();
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_err());
+ }
+
+ #[test]
+ fn test_remove_propagated() {
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::::default());
+
+ let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagator))
+ .id();
+
+ app.update();
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_ok());
+ app.world_mut()
+ .commands()
+ .entity(propagator)
+ .remove::>();
+ app.update();
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_err());
+ }
+
+ #[test]
+ fn test_propagate_over() {
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::::default());
+
+ let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let propagate_over = app
+ .world_mut()
+ .spawn(TestValue(2))
+ .insert(ChildOf(propagator))
+ .id();
+ let propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagate_over))
+ .id();
+
+ app.update();
+ assert_eq!(
+ app.world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee),
+ Ok(&TestValue(1))
+ );
+ }
+
+ #[test]
+ fn test_propagate_stop() {
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::::default());
+
+ let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let propagate_stop = app
+ .world_mut()
+ .spawn(PropagateStop::::default())
+ .insert(ChildOf(propagator))
+ .id();
+ let no_propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagate_stop))
+ .id();
+
+ app.update();
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), no_propagatee)
+ .is_err());
+ }
+
+ #[test]
+ fn test_intermediate_override() {
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::::default());
+
+ let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let intermediate = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagator))
+ .id();
+ let propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(intermediate))
+ .id();
+
+ app.update();
+ assert_eq!(
+ app.world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee),
+ Ok(&TestValue(1))
+ );
+
+ app.world_mut()
+ .entity_mut(intermediate)
+ .insert(Propagate(TestValue(2)));
+ app.update();
+ assert_eq!(
+ app.world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee),
+ Ok(&TestValue(2))
+ );
+ }
+
+ #[test]
+ fn test_filter() {
+ #[derive(Component)]
+ struct Marker;
+
+ let mut app = App::new();
+ app.add_schedule(Schedule::new(Update));
+ app.add_plugins(HierarchyPropagatePlugin::>::default());
+
+ let propagator = app.world_mut().spawn(Propagate(TestValue(1))).id();
+ let propagatee = app
+ .world_mut()
+ .spawn_empty()
+ .insert(ChildOf(propagator))
+ .id();
+
+ app.update();
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_err());
+
+ // NOTE: changes to the filter condition are not rechecked
+ app.world_mut().entity_mut(propagator).insert(Marker);
+ app.world_mut().entity_mut(propagatee).insert(Marker);
+ app.update();
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_err());
+
+ app.world_mut()
+ .entity_mut(propagator)
+ .insert(Propagate(TestValue(1)));
+ app.update();
+ assert!(app
+ .world_mut()
+ .query::<&TestValue>()
+ .get(app.world(), propagatee)
+ .is_ok());
+ }
+}
diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml
index 07a45a3f6d..cbb138b0f5 100644
--- a/crates/bevy_asset/Cargo.toml
+++ b/crates/bevy_asset/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_asset"
version = "0.16.0-dev"
edition = "2024"
description = "Provides asset functionality for Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_asset/macros/Cargo.toml b/crates/bevy_asset/macros/Cargo.toml
index 43562ae806..4d99d228d3 100644
--- a/crates/bevy_asset/macros/Cargo.toml
+++ b/crates/bevy_asset/macros/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_asset_macros"
version = "0.16.0-dev"
edition = "2024"
description = "Derive implementations for bevy_asset"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs
index 9fa8eb4381..6e5b488ee0 100644
--- a/crates/bevy_asset/src/assets.rs
+++ b/crates/bevy_asset/src/assets.rs
@@ -437,6 +437,18 @@ impl Assets {
result
}
+ /// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists.
+ ///
+ /// This is the same as [`Assets::get_mut`] except it doesn't emit [`AssetEvent::Modified`].
+ #[inline]
+ pub fn get_mut_untracked(&mut self, id: impl Into>) -> Option<&mut A> {
+ let id: AssetId = id.into();
+ match id {
+ AssetId::Index { index, .. } => self.dense_storage.get_mut(index),
+ AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid),
+ }
+ }
+
/// Removes (and returns) the [`Asset`] with the given `id`, if it exists.
/// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`].
pub fn remove(&mut self, id: impl Into>) -> Option {
@@ -450,6 +462,8 @@ impl Assets {
/// Removes (and returns) the [`Asset`] with the given `id`, if it exists. This skips emitting [`AssetEvent::Removed`].
/// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`].
+ ///
+ /// This is the same as [`Assets::remove`] except it doesn't emit [`AssetEvent::Removed`].
pub fn remove_untracked(&mut self, id: impl Into>) -> Option {
let id: AssetId = id.into();
self.duplicate_handles.remove(&id);
diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs
index 5b680eb191..4b29beae79 100644
--- a/crates/bevy_asset/src/lib.rs
+++ b/crates/bevy_asset/src/lib.rs
@@ -141,8 +141,8 @@
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
#![no_std]
diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs
index 8f4863b885..50bbfb5dfc 100644
--- a/crates/bevy_asset/src/loader.rs
+++ b/crates/bevy_asset/src/loader.rs
@@ -12,7 +12,7 @@ use alloc::{
vec::Vec,
};
use atomicow::CowArc;
-use bevy_ecs::world::World;
+use bevy_ecs::{error::BevyError, world::World};
use bevy_platform::collections::{HashMap, HashSet};
use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
use core::any::{Any, TypeId};
@@ -34,7 +34,7 @@ pub trait AssetLoader: Send + Sync + 'static {
/// The settings type used by this [`AssetLoader`].
type Settings: Settings + Default + Serialize + for<'a> Deserialize<'a>;
/// The type of [error](`std::error::Error`) which could be encountered by this loader.
- type Error: Into>;
+ type Error: Into;
/// Asynchronously loads [`AssetLoader::Asset`] (and any other labeled assets) from the bytes provided by [`Reader`].
fn load(
&self,
@@ -58,10 +58,7 @@ pub trait ErasedAssetLoader: Send + Sync + 'static {
reader: &'a mut dyn Reader,
meta: &'a dyn AssetMetaDyn,
load_context: LoadContext<'a>,
- ) -> BoxedFuture<
- 'a,
- Result>,
- >;
+ ) -> BoxedFuture<'a, Result>;
/// Returns a list of extensions supported by this asset loader, without the preceding dot.
fn extensions(&self) -> &[&str];
@@ -89,10 +86,7 @@ where
reader: &'a mut dyn Reader,
meta: &'a dyn AssetMetaDyn,
mut load_context: LoadContext<'a>,
- ) -> BoxedFuture<
- 'a,
- Result>,
- > {
+ ) -> BoxedFuture<'a, Result> {
Box::pin(async move {
let settings = meta
.loader_settings()
@@ -394,15 +388,15 @@ impl<'a> LoadContext<'a> {
/// result with [`LoadContext::add_labeled_asset`].
///
/// See [`AssetPath`] for more on labeled assets.
- pub fn labeled_asset_scope(
+ pub fn labeled_asset_scope(
&mut self,
label: String,
- load: impl FnOnce(&mut LoadContext) -> A,
- ) -> Handle {
+ load: impl FnOnce(&mut LoadContext) -> Result ,
+ ) -> Result, E> {
let mut context = self.begin_labeled_asset();
- let asset = load(&mut context);
+ let asset = load(&mut context)?;
let loaded_asset = context.finish(asset);
- self.add_loaded_labeled_asset(label, loaded_asset)
+ Ok(self.add_loaded_labeled_asset(label, loaded_asset))
}
/// This will add the given `asset` as a "labeled [`Asset`]" with the `label` label.
@@ -416,7 +410,8 @@ impl<'a> LoadContext<'a> {
///
/// See [`AssetPath`] for more on labeled assets.
pub fn add_labeled_asset(&mut self, label: String, asset: A) -> Handle {
- self.labeled_asset_scope(label, |_| asset)
+ self.labeled_asset_scope(label, |_| Ok::<_, ()>(asset))
+ .expect("the closure returns Ok")
}
/// Add a [`LoadedAsset`] that is a "labeled sub asset" of the root path of this load context.
diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs
index ff5800474d..2b3898cd54 100644
--- a/crates/bevy_asset/src/server/mod.rs
+++ b/crates/bevy_asset/src/server/mod.rs
@@ -1945,7 +1945,7 @@ pub enum AssetLoadError {
pub struct AssetLoaderError {
path: AssetPath<'static>,
loader_name: &'static str,
- error: Arc,
+ error: Arc,
}
impl AssetLoaderError {
diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml
index 84060fe26b..ae5385870d 100644
--- a/crates/bevy_audio/Cargo.toml
+++ b/crates/bevy_audio/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_audio"
version = "0.16.0-dev"
edition = "2024"
description = "Provides audio functionality for Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
@@ -19,12 +19,18 @@ bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" }
# other
+# TODO: Remove `coreaudio-sys` dep below when updating `cpal`.
rodio = { version = "0.20", default-features = false }
tracing = { version = "0.1", default-features = false, features = ["std"] }
[target.'cfg(target_os = "android")'.dependencies]
cpal = { version = "0.15", optional = true }
+[target.'cfg(target_vendor = "apple")'.dependencies]
+# NOTE: Explicitly depend on this patch version to fix:
+# https://github.com/bevyengine/bevy/issues/18893
+coreaudio-sys = { version = "0.2.17", default-features = false }
+
[target.'cfg(target_arch = "wasm32")'.dependencies]
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.
rodio = { version = "0.20", default-features = false, features = [
diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs
index becbf5d1da..e3b5e02569 100644
--- a/crates/bevy_audio/src/lib.rs
+++ b/crates/bevy_audio/src/lib.rs
@@ -1,8 +1,8 @@
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
//! Audio support for the game engine Bevy
diff --git a/crates/bevy_audio/src/volume.rs b/crates/bevy_audio/src/volume.rs
index 3c19d189ef..1f1f417594 100644
--- a/crates/bevy_audio/src/volume.rs
+++ b/crates/bevy_audio/src/volume.rs
@@ -34,7 +34,7 @@ impl GlobalVolume {
#[derive(Clone, Copy, Debug, Reflect)]
#[reflect(Clone, Debug, PartialEq)]
pub enum Volume {
- /// Create a new [`Volume`] from the given volume in linear scale.
+ /// Create a new [`Volume`] from the given volume in the linear scale.
///
/// In a linear scale, the value `1.0` represents the "normal" volume,
/// meaning the audio is played at its original level. Values greater than
@@ -144,7 +144,7 @@ impl Volume {
/// Returns the volume in decibels as a float.
///
- /// If the volume is silent / off / muted, i.e. its underlying linear scale
+ /// If the volume is silent / off / muted, i.e., its underlying linear scale
/// is `0.0`, this method returns negative infinity.
pub fn to_decibels(&self) -> f32 {
match self {
@@ -155,57 +155,95 @@ impl Volume {
/// The silent volume. Also known as "off" or "muted".
pub const SILENT: Self = Volume::Linear(0.0);
-}
-impl core::ops::Add for Volume {
- type Output = Self;
-
- fn add(self, rhs: Self) -> Self {
- use Volume::{Decibels, Linear};
-
- match (self, rhs) {
- (Linear(a), Linear(b)) => Linear(a + b),
- (Decibels(a), Decibels(b)) => Decibels(linear_to_decibels(
- decibels_to_linear(a) + decibels_to_linear(b),
- )),
- // {Linear, Decibels} favors the left hand side of the operation by
- // first converting the right hand side to the same type as the left
- // hand side and then performing the operation.
- (Linear(..), Decibels(db)) => self + Linear(decibels_to_linear(db)),
- (Decibels(..), Linear(l)) => self + Decibels(linear_to_decibels(l)),
- }
+ /// Increases the volume by the specified percentage.
+ ///
+ /// This method works in the linear domain, where a 100% increase
+ /// means doubling the volume (equivalent to +6.02dB).
+ ///
+ /// # Arguments
+ /// * `percentage` - The percentage to increase (50.0 means 50% increase)
+ ///
+ /// # Examples
+ /// ```
+ /// use bevy_audio::Volume;
+ ///
+ /// let volume = Volume::Linear(1.0);
+ /// let increased = volume.increase_by_percentage(100.0);
+ /// assert_eq!(increased.to_linear(), 2.0);
+ /// ```
+ pub fn increase_by_percentage(&self, percentage: f32) -> Self {
+ let factor = 1.0 + (percentage / 100.0);
+ Volume::Linear(self.to_linear() * factor)
}
-}
-impl core::ops::AddAssign for Volume {
- fn add_assign(&mut self, rhs: Self) {
- *self = *self + rhs;
+ /// Decreases the volume by the specified percentage.
+ ///
+ /// This method works in the linear domain, where a 50% decrease
+ /// means halving the volume (equivalent to -6.02dB).
+ ///
+ /// # Arguments
+ /// * `percentage` - The percentage to decrease (50.0 means 50% decrease)
+ ///
+ /// # Examples
+ /// ```
+ /// use bevy_audio::Volume;
+ ///
+ /// let volume = Volume::Linear(1.0);
+ /// let decreased = volume.decrease_by_percentage(50.0);
+ /// assert_eq!(decreased.to_linear(), 0.5);
+ /// ```
+ pub fn decrease_by_percentage(&self, percentage: f32) -> Self {
+ let factor = 1.0 - (percentage / 100.0).clamp(0.0, 1.0);
+ Volume::Linear(self.to_linear() * factor)
}
-}
-impl core::ops::Sub for Volume {
- type Output = Self;
-
- fn sub(self, rhs: Self) -> Self {
- use Volume::{Decibels, Linear};
-
- match (self, rhs) {
- (Linear(a), Linear(b)) => Linear(a - b),
- (Decibels(a), Decibels(b)) => Decibels(linear_to_decibels(
- decibels_to_linear(a) - decibels_to_linear(b),
- )),
- // {Linear, Decibels} favors the left hand side of the operation by
- // first converting the right hand side to the same type as the left
- // hand side and then performing the operation.
- (Linear(..), Decibels(db)) => self - Linear(decibels_to_linear(db)),
- (Decibels(..), Linear(l)) => self - Decibels(linear_to_decibels(l)),
- }
+ /// Scales the volume to a specific linear factor relative to the current volume.
+ ///
+ /// This is different from `adjust_by_linear` as it sets the volume to be
+ /// exactly the factor times the original volume, rather than applying
+ /// the factor to the current volume.
+ ///
+ /// # Arguments
+ /// * `factor` - The scaling factor (2.0 = twice as loud, 0.5 = half as loud)
+ ///
+ /// # Examples
+ /// ```
+ /// use bevy_audio::Volume;
+ ///
+ /// let volume = Volume::Linear(0.8);
+ /// let scaled = volume.scale_to_factor(1.25);
+ /// assert_eq!(scaled.to_linear(), 1.0);
+ /// ```
+ pub fn scale_to_factor(&self, factor: f32) -> Self {
+ Volume::Linear(self.to_linear() * factor)
}
-}
-impl core::ops::SubAssign for Volume {
- fn sub_assign(&mut self, rhs: Self) {
- *self = *self - rhs;
+ /// Creates a fade effect by interpolating between current volume and target volume.
+ ///
+ /// This method performs linear interpolation in the linear domain, which
+ /// provides a more natural-sounding fade effect.
+ ///
+ /// # Arguments
+ /// * `target` - The target volume to fade towards
+ /// * `factor` - The interpolation factor (0.0 = current volume, 1.0 = target volume)
+ ///
+ /// # Examples
+ /// ```
+ /// use bevy_audio::Volume;
+ ///
+ /// let current = Volume::Linear(1.0);
+ /// let target = Volume::Linear(0.0);
+ /// let faded = current.fade_towards(target, 0.5);
+ /// assert_eq!(faded.to_linear(), 0.5);
+ /// ```
+ pub fn fade_towards(&self, target: Volume, factor: f32) -> Self {
+ let current_linear = self.to_linear();
+ let target_linear = target.to_linear();
+ let factor_clamped = factor.clamp(0.0, 1.0);
+
+ let interpolated = current_linear + (target_linear - current_linear) * factor_clamped;
+ Volume::Linear(interpolated)
}
}
@@ -337,8 +375,9 @@ mod tests {
Linear(f32::NEG_INFINITY).to_decibels().is_infinite(),
"Negative infinite linear scale is equivalent to infinite decibels"
);
- assert!(
- Decibels(f32::NEG_INFINITY).to_linear().abs() == 0.0,
+ assert_eq!(
+ Decibels(f32::NEG_INFINITY).to_linear().abs(),
+ 0.0,
"Negative infinity decibels is equivalent to zero linear scale"
);
@@ -361,6 +400,74 @@ mod tests {
);
}
+ #[test]
+ fn test_increase_by_percentage() {
+ let volume = Linear(1.0);
+
+ // 100% increase should double the volume
+ let increased = volume.increase_by_percentage(100.0);
+ assert_eq!(increased.to_linear(), 2.0);
+
+ // 50% increase
+ let increased = volume.increase_by_percentage(50.0);
+ assert_eq!(increased.to_linear(), 1.5);
+ }
+
+ #[test]
+ fn test_decrease_by_percentage() {
+ let volume = Linear(1.0);
+
+ // 50% decrease should halve the volume
+ let decreased = volume.decrease_by_percentage(50.0);
+ assert_eq!(decreased.to_linear(), 0.5);
+
+ // 25% decrease
+ let decreased = volume.decrease_by_percentage(25.0);
+ assert_eq!(decreased.to_linear(), 0.75);
+
+ // 100% decrease should result in silence
+ let decreased = volume.decrease_by_percentage(100.0);
+ assert_eq!(decreased.to_linear(), 0.0);
+ }
+
+ #[test]
+ fn test_scale_to_factor() {
+ let volume = Linear(0.8);
+ let scaled = volume.scale_to_factor(1.25);
+ assert_eq!(scaled.to_linear(), 1.0);
+ }
+
+ #[test]
+ fn test_fade_towards() {
+ let current = Linear(1.0);
+ let target = Linear(0.0);
+
+ // 50% fade should result in 0.5 linear volume
+ let faded = current.fade_towards(target, 0.5);
+ assert_eq!(faded.to_linear(), 0.5);
+
+ // 0% fade should keep current volume
+ let faded = current.fade_towards(target, 0.0);
+ assert_eq!(faded.to_linear(), 1.0);
+
+ // 100% fade should reach target volume
+ let faded = current.fade_towards(target, 1.0);
+ assert_eq!(faded.to_linear(), 0.0);
+ }
+
+ #[test]
+ fn test_decibel_math_properties() {
+ let volume = Linear(1.0);
+
+ // Adding 20dB should multiply linear volume by 10
+ let adjusted = volume * Decibels(20.0);
+ assert_approx_eq(adjusted, Linear(10.0));
+
+ // Subtracting 20dB should divide linear volume by 10
+ let adjusted = volume / Decibels(20.0);
+ assert_approx_eq(adjusted, Linear(0.1));
+ }
+
fn assert_approx_eq(a: Volume, b: Volume) {
const EPSILON: f32 = 0.0001;
@@ -380,52 +487,6 @@ mod tests {
}
}
- #[test]
- fn volume_ops_add() {
- // Linear to Linear.
- assert_approx_eq(Linear(0.5) + Linear(0.5), Linear(1.0));
- assert_approx_eq(Linear(0.5) + Linear(0.1), Linear(0.6));
- assert_approx_eq(Linear(0.5) + Linear(-0.5), Linear(0.0));
-
- // Decibels to Decibels.
- assert_approx_eq(Decibels(0.0) + Decibels(0.0), Decibels(6.0206003));
- assert_approx_eq(Decibels(6.0) + Decibels(6.0), Decibels(12.020599));
- assert_approx_eq(Decibels(-6.0) + Decibels(-6.0), Decibels(0.020599423));
-
- // {Linear, Decibels} favors the left hand side of the operation.
- assert_approx_eq(Linear(0.5) + Decibels(0.0), Linear(1.5));
- assert_approx_eq(Decibels(0.0) + Linear(0.5), Decibels(3.521825));
- }
-
- #[test]
- fn volume_ops_add_assign() {
- // Linear to Linear.
- let mut volume = Linear(0.5);
- volume += Linear(0.5);
- assert_approx_eq(volume, Linear(1.0));
- }
-
- #[test]
- fn volume_ops_sub() {
- // Linear to Linear.
- assert_approx_eq(Linear(0.5) - Linear(0.5), Linear(0.0));
- assert_approx_eq(Linear(0.5) - Linear(0.1), Linear(0.4));
- assert_approx_eq(Linear(0.5) - Linear(-0.5), Linear(1.0));
-
- // Decibels to Decibels.
- assert_eq!(Decibels(0.0) - Decibels(0.0), Decibels(f32::NEG_INFINITY));
- assert_approx_eq(Decibels(6.0) - Decibels(4.0), Decibels(-7.736506));
- assert_eq!(Decibels(-6.0) - Decibels(-6.0), Decibels(f32::NEG_INFINITY));
- }
-
- #[test]
- fn volume_ops_sub_assign() {
- // Linear to Linear.
- let mut volume = Linear(0.5);
- volume -= Linear(0.5);
- assert_approx_eq(volume, Linear(0.0));
- }
-
#[test]
fn volume_ops_mul() {
// Linear to Linear.
diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml
index 9b6d7d8cf6..ca7a7a74f5 100644
--- a/crates/bevy_color/Cargo.toml
+++ b/crates/bevy_color/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_color"
version = "0.16.0-dev"
edition = "2024"
description = "Types for representing and manipulating color values"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy", "color"]
diff --git a/crates/bevy_color/src/lib.rs b/crates/bevy_color/src/lib.rs
index 712da5d7ec..d5d72d1544 100644
--- a/crates/bevy_color/src/lib.rs
+++ b/crates/bevy_color/src/lib.rs
@@ -1,8 +1,8 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
#![no_std]
@@ -262,6 +262,7 @@ macro_rules! impl_componentwise_vector_space {
}
impl bevy_math::VectorSpace for $ty {
+ type Scalar = f32;
const ZERO: Self = Self {
$($element: 0.0,)+
};
diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml
index 304c007104..2d903d2cc4 100644
--- a/crates/bevy_core_pipeline/Cargo.toml
+++ b/crates/bevy_core_pipeline/Cargo.toml
@@ -7,7 +7,7 @@ authors = [
"Carter Anderson ",
]
description = "Provides a core render pipeline for Bevy Engine."
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs
index 9e04614276..6c6bc7ccc7 100644
--- a/crates/bevy_core_pipeline/src/lib.rs
+++ b/crates/bevy_core_pipeline/src/lib.rs
@@ -2,8 +2,8 @@
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
pub mod auto_exposure;
diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs
index 3ae95d71cc..5b5d038fa0 100644
--- a/crates/bevy_core_pipeline/src/oit/mod.rs
+++ b/crates/bevy_core_pipeline/src/oit/mod.rs
@@ -1,7 +1,7 @@
//! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details.
use bevy_app::prelude::*;
-use bevy_ecs::{component::*, prelude::*};
+use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*};
use bevy_math::UVec2;
use bevy_platform::collections::HashSet;
use bevy_platform::time::Instant;
diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml
new file mode 100644
index 0000000000..1627ff9a29
--- /dev/null
+++ b/crates/bevy_core_widgets/Cargo.toml
@@ -0,0 +1,32 @@
+[package]
+name = "bevy_core_widgets"
+version = "0.16.0-dev"
+edition = "2024"
+description = "Unstyled common widgets for Bevy Engine"
+homepage = "https://bevyengine.org"
+repository = "https://github.com/bevyengine/bevy"
+license = "MIT OR Apache-2.0"
+keywords = ["bevy"]
+
+[dependencies]
+# bevy
+bevy_app = { path = "../bevy_app", version = "0.16.0-dev" }
+bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" }
+bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
+bevy_input = { path = "../bevy_input", version = "0.16.0-dev" }
+bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" }
+bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" }
+bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev" }
+
+# other
+accesskit = "0.19"
+
+[features]
+default = []
+
+[lints]
+workspace = true
+
+[package.metadata.docs.rs]
+rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
+all-features = true
diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs
new file mode 100644
index 0000000000..bf88c27143
--- /dev/null
+++ b/crates/bevy_core_widgets/src/core_button.rs
@@ -0,0 +1,141 @@
+use accesskit::Role;
+use bevy_a11y::AccessibilityNode;
+use bevy_app::{App, Plugin};
+use bevy_ecs::query::Has;
+use bevy_ecs::system::ResMut;
+use bevy_ecs::{
+ component::Component,
+ entity::Entity,
+ observer::Trigger,
+ query::With,
+ system::{Commands, Query, SystemId},
+};
+use bevy_input::keyboard::{KeyCode, KeyboardInput};
+use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible};
+use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release};
+use bevy_ui::{InteractionDisabled, Pressed};
+
+/// Headless button widget. This widget maintains a "pressed" state, which is used to
+/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked`
+/// event when the button is un-pressed.
+#[derive(Component, Debug)]
+#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))]
+pub struct CoreButton {
+ /// Optional system to run when the button is clicked, or when the Enter or Space key
+ /// is pressed while the button is focused. If this field is `None`, the button will
+ /// emit a `ButtonClicked` event when clicked.
+ pub on_click: Option,
+}
+
+fn button_on_key_event(
+ mut trigger: Trigger>,
+ q_state: Query<(&CoreButton, Has)>,
+ mut commands: Commands,
+) {
+ if let Ok((bstate, disabled)) = q_state.get(trigger.target().unwrap()) {
+ if !disabled {
+ let event = &trigger.event().input;
+ if !event.repeat
+ && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space)
+ {
+ if let Some(on_click) = bstate.on_click {
+ trigger.propagate(false);
+ commands.run_system(on_click);
+ }
+ }
+ }
+ }
+}
+
+fn button_on_pointer_click(
+ mut trigger: Trigger>,
+ mut q_state: Query<(&CoreButton, Has, Has)>,
+ mut commands: Commands,
+) {
+ if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target().unwrap()) {
+ trigger.propagate(false);
+ if pressed && !disabled {
+ if let Some(on_click) = bstate.on_click {
+ commands.run_system(on_click);
+ }
+ }
+ }
+}
+
+fn button_on_pointer_down(
+ mut trigger: Trigger>,
+ mut q_state: Query<(Entity, Has, Has), With>,
+ focus: Option>,
+ focus_visible: Option>,
+ mut commands: Commands,
+) {
+ if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) {
+ trigger.propagate(false);
+ if !disabled {
+ if !pressed {
+ commands.entity(button).insert(Pressed);
+ }
+ // Clicking on a button makes it the focused input,
+ // and hides the focus ring if it was visible.
+ if let Some(mut focus) = focus {
+ focus.0 = trigger.target();
+ }
+ if let Some(mut focus_visible) = focus_visible {
+ focus_visible.0 = false;
+ }
+ }
+ }
+}
+
+fn button_on_pointer_up(
+ mut trigger: Trigger>,
+ mut q_state: Query<(Entity, Has, Has), With>,
+ mut commands: Commands,
+) {
+ if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) {
+ trigger.propagate(false);
+ if !disabled && pressed {
+ commands.entity(button).remove::();
+ }
+ }
+}
+
+fn button_on_pointer_drag_end(
+ mut trigger: Trigger>,
+ mut q_state: Query<(Entity, Has, Has), With>,
+ mut commands: Commands,
+) {
+ if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) {
+ trigger.propagate(false);
+ if !disabled && pressed {
+ commands.entity(button).remove::();
+ }
+ }
+}
+
+fn button_on_pointer_cancel(
+ mut trigger: Trigger>,
+ mut q_state: Query<(Entity, Has, Has), With>,
+ mut commands: Commands,
+) {
+ if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target().unwrap()) {
+ trigger.propagate(false);
+ if !disabled && pressed {
+ commands.entity(button).remove::();
+ }
+ }
+}
+
+/// Plugin that adds the observers for the [`CoreButton`] widget.
+pub struct CoreButtonPlugin;
+
+impl Plugin for CoreButtonPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_observer(button_on_key_event)
+ .add_observer(button_on_pointer_down)
+ .add_observer(button_on_pointer_up)
+ .add_observer(button_on_pointer_click)
+ .add_observer(button_on_pointer_drag_end)
+ .add_observer(button_on_pointer_cancel);
+ }
+}
diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs
new file mode 100644
index 0000000000..afeed92a1f
--- /dev/null
+++ b/crates/bevy_core_widgets/src/lib.rs
@@ -0,0 +1,27 @@
+//! This crate provides a set of core widgets for Bevy UI, such as buttons, checkboxes, and sliders.
+//! These widgets have no inherent styling, it's the responsibility of the user to add styling
+//! appropriate for their game or application.
+//!
+//! # State Management
+//!
+//! Most of the widgets use external state management: this means that the widgets do not
+//! automatically update their own internal state, but instead rely on the app to update the widget
+//! state (as well as any other related game state) in response to a change event emitted by the
+//! widget. The primary motivation for this is to avoid two-way data binding in scenarios where the
+//! user interface is showing a live view of dynamic data coming from deeper within the game engine.
+
+mod core_button;
+
+use bevy_app::{App, Plugin};
+
+pub use core_button::{CoreButton, CoreButtonPlugin};
+
+/// A plugin that registers the observers for all of the core widgets. If you don't want to
+/// use all of the widgets, you can import the individual widget plugins instead.
+pub struct CoreWidgetsPlugin;
+
+impl Plugin for CoreWidgetsPlugin {
+ fn build(&self, app: &mut App) {
+ app.add_plugins(CoreButtonPlugin);
+ }
+}
diff --git a/crates/bevy_derive/Cargo.toml b/crates/bevy_derive/Cargo.toml
index 1c4cb4adcc..f127dbffb5 100644
--- a/crates/bevy_derive/Cargo.toml
+++ b/crates/bevy_derive/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_derive"
version = "0.16.0-dev"
edition = "2024"
description = "Provides derive implementations for Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_derive/compile_fail/Cargo.toml b/crates/bevy_derive/compile_fail/Cargo.toml
index a9ad3e95e1..e9116dc57b 100644
--- a/crates/bevy_derive/compile_fail/Cargo.toml
+++ b/crates/bevy_derive/compile_fail/Cargo.toml
@@ -2,7 +2,7 @@
name = "bevy_derive_compile_fail"
edition = "2024"
description = "Compile fail tests for Bevy Engine's various macros"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
publish = false
diff --git a/crates/bevy_derive/src/lib.rs b/crates/bevy_derive/src/lib.rs
index e446d0f50d..16a66eb906 100644
--- a/crates/bevy_derive/src/lib.rs
+++ b/crates/bevy_derive/src/lib.rs
@@ -1,9 +1,10 @@
-#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
+//! Assorted proc macro derive functions.
+
#![forbid(unsafe_code)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
extern crate proc_macro;
@@ -188,11 +189,34 @@ pub fn derive_deref_mut(input: TokenStream) -> TokenStream {
derefs::derive_deref_mut(input)
}
+/// Generates the required main function boilerplate for Android.
#[proc_macro_attribute]
pub fn bevy_main(attr: TokenStream, item: TokenStream) -> TokenStream {
bevy_main::bevy_main(attr, item)
}
+/// Adds `enum_variant_index` and `enum_variant_name` functions to enums.
+///
+/// # Example
+///
+/// ```
+/// use bevy_derive::{EnumVariantMeta};
+///
+/// #[derive(EnumVariantMeta)]
+/// enum MyEnum {
+/// A,
+/// B,
+/// }
+///
+/// let a = MyEnum::A;
+/// let b = MyEnum::B;
+///
+/// assert_eq!(0, a.enum_variant_index());
+/// assert_eq!("A", a.enum_variant_name());
+///
+/// assert_eq!(1, b.enum_variant_index());
+/// assert_eq!("B", b.enum_variant_name());
+/// ```
#[proc_macro_derive(EnumVariantMeta)]
pub fn derive_enum_variant_meta(input: TokenStream) -> TokenStream {
enum_variant_meta::derive_enum_variant_meta(input)
diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml
index ad0f2c515c..2250a35393 100644
--- a/crates/bevy_dev_tools/Cargo.toml
+++ b/crates/bevy_dev_tools/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_dev_tools"
version = "0.16.0-dev"
edition = "2024"
description = "Collection of developer tools for the Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_dev_tools/src/lib.rs b/crates/bevy_dev_tools/src/lib.rs
index 1dfd473409..5e826e3f9c 100644
--- a/crates/bevy_dev_tools/src/lib.rs
+++ b/crates/bevy_dev_tools/src/lib.rs
@@ -1,11 +1,11 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
-//! This crate provides additional utilities for the [Bevy game engine](https://bevyengine.org),
+//! This crate provides additional utilities for the [Bevy game engine](https://bevy.org),
//! focused on improving developer experience.
use bevy_app::prelude::*;
diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs
index 79c1c8fff4..d11818dc6a 100644
--- a/crates/bevy_dev_tools/src/picking_debug.rs
+++ b/crates/bevy_dev_tools/src/picking_debug.rs
@@ -94,8 +94,8 @@ impl Plugin for DebugPickingPlugin {
log_event_debug::.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::,
log_pointer_event_debug::,
- log_pointer_event_debug::,
- log_pointer_event_debug::,
+ log_pointer_event_debug::,
+ log_pointer_event_debug::,
log_pointer_event_debug::,
log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy),
log_pointer_event_debug::,
diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml
index 2b89e5759e..708f3b9ee1 100644
--- a/crates/bevy_diagnostic/Cargo.toml
+++ b/crates/bevy_diagnostic/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_diagnostic"
version = "0.16.0-dev"
edition = "2024"
description = "Provides diagnostic functionality for Bevy Engine"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs
index 8c71188bf7..6da673eb18 100644
--- a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs
+++ b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs
@@ -19,8 +19,10 @@ impl Plugin for EntityCountDiagnosticsPlugin {
}
impl EntityCountDiagnosticsPlugin {
+ /// Number of currently allocated entities.
pub const ENTITY_COUNT: DiagnosticPath = DiagnosticPath::const_new("entity_count");
+ /// Updates entity count measurement.
pub fn diagnostic_system(mut diagnostics: Diagnostics, entities: &Entities) {
diagnostics.add_measurement(&Self::ENTITY_COUNT, || entities.count_constructed() as f64);
}
diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs
index 22b6176fa2..a632c1b49a 100644
--- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs
+++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs
@@ -58,10 +58,16 @@ impl Plugin for FrameTimeDiagnosticsPlugin {
}
impl FrameTimeDiagnosticsPlugin {
+ /// Frames per second.
pub const FPS: DiagnosticPath = DiagnosticPath::const_new("fps");
+
+ /// Total frames since application start.
pub const FRAME_COUNT: DiagnosticPath = DiagnosticPath::const_new("frame_count");
+
+ /// Frame time in ms.
pub const FRAME_TIME: DiagnosticPath = DiagnosticPath::const_new("frame_time");
+ /// Updates frame count, frame time and fps measurements.
pub fn diagnostic_system(
mut diagnostics: Diagnostics,
time: Res>,
diff --git a/crates/bevy_diagnostic/src/lib.rs b/crates/bevy_diagnostic/src/lib.rs
index 588b3276f6..707c7c7cc7 100644
--- a/crates/bevy_diagnostic/src/lib.rs
+++ b/crates/bevy_diagnostic/src/lib.rs
@@ -1,13 +1,12 @@
-#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
#![no_std]
-//! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevyengine.org/).
+//! This crate provides a straightforward solution for integrating diagnostics in the [Bevy game engine](https://bevy.org/).
//! It allows users to easily add diagnostic functionality to their Bevy applications, enhancing
//! their ability to monitor and optimize their game's.
diff --git a/crates/bevy_dylib/Cargo.toml b/crates/bevy_dylib/Cargo.toml
index 26aec33b83..a980a35b10 100644
--- a/crates/bevy_dylib/Cargo.toml
+++ b/crates/bevy_dylib/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_dylib"
version = "0.16.0-dev"
edition = "2024"
description = "Force the Bevy Engine to be dynamically linked for faster linking"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["bevy"]
diff --git a/crates/bevy_dylib/src/lib.rs b/crates/bevy_dylib/src/lib.rs
index 1ff40ce3e8..84322813db 100644
--- a/crates/bevy_dylib/src/lib.rs
+++ b/crates/bevy_dylib/src/lib.rs
@@ -1,7 +1,7 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc(
- html_logo_url = "https://bevyengine.org/assets/icon.png",
- html_favicon_url = "https://bevyengine.org/assets/icon.png"
+ html_logo_url = "https://bevy.org/assets/icon.png",
+ html_favicon_url = "https://bevy.org/assets/icon.png"
)]
//! Forces dynamic linking of Bevy.
diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml
index bf71d217d4..27498f58bc 100644
--- a/crates/bevy_ecs/Cargo.toml
+++ b/crates/bevy_ecs/Cargo.toml
@@ -3,7 +3,7 @@ name = "bevy_ecs"
version = "0.16.0-dev"
edition = "2024"
description = "Bevy Engine's entity component system"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
keywords = ["ecs", "game", "bevy"]
@@ -83,6 +83,8 @@ critical-section = [
"bevy_reflect?/critical-section",
]
+hotpatching = ["dep:subsecond"]
+
[dependencies]
bevy_ptr = { path = "../bevy_ptr", version = "0.16.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [
@@ -117,6 +119,7 @@ variadics_please = { version = "1.1", default-features = false }
tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false }
bumpalo = "3"
+subsecond = { version = "0.7.0-alpha.1", optional = true }
concurrent-queue = { version = "2.5.0", default-features = false }
[target.'cfg(not(all(target_has_atomic = "8", target_has_atomic = "16", target_has_atomic = "32", target_has_atomic = "64", target_has_atomic = "ptr")))'.dependencies]
diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md
index c2fdc53d05..ade3866b5d 100644
--- a/crates/bevy_ecs/README.md
+++ b/crates/bevy_ecs/README.md
@@ -340,8 +340,8 @@ let mut world = World::new();
let entity = world.spawn_empty().id();
world.add_observer(|trigger: Trigger, mut commands: Commands| {
- println!("Entity {} goes BOOM!", trigger.target());
- commands.entity(trigger.target()).despawn();
+ println!("Entity {} goes BOOM!", trigger.target().unwrap());
+ commands.entity(trigger.target().unwrap()).despawn();
});
world.flush();
@@ -349,4 +349,4 @@ world.flush();
world.trigger_targets(Explode, entity);
```
-[bevy]: https://bevyengine.org/
+[bevy]: https://bevy.org/
diff --git a/crates/bevy_ecs/compile_fail/Cargo.toml b/crates/bevy_ecs/compile_fail/Cargo.toml
index 48e3857f53..96c48ac6a3 100644
--- a/crates/bevy_ecs/compile_fail/Cargo.toml
+++ b/crates/bevy_ecs/compile_fail/Cargo.toml
@@ -2,7 +2,7 @@
name = "bevy_ecs_compile_fail"
edition = "2024"
description = "Compile fail tests for Bevy Engine's entity component system"
-homepage = "https://bevyengine.org"
+homepage = "https://bevy.org"
repository = "https://github.com/bevyengine/bevy"
license = "MIT OR Apache-2.0"
publish = false
diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs
index 4076819ee3..79c2158644 100644
--- a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs
+++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs
@@ -60,4 +60,4 @@ mod case4 {
pub struct BarTargetOf(Entity);
}
-fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {}
+fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::lifecycle::HookContext) {}
diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs
index 00268cb680..6a693f2ce5 100644
--- a/crates/bevy_ecs/macros/src/component.rs
+++ b/crates/bevy_ecs/macros/src/component.rs
@@ -434,7 +434,7 @@ impl HookAttributeKind {
HookAttributeKind::Path(path) => path.to_token_stream(),
HookAttributeKind::Call(call) => {
quote!({
- fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::component::HookContext) {
+ fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::lifecycle::HookContext) {
(#call)(world, ctx)
}
_internal_hook
@@ -658,7 +658,7 @@ fn hook_register_function_call(
) -> Option {
function.map(|meta| {
quote! {
- fn #hook() -> ::core::option::Option<#bevy_ecs_path::component::ComponentHook> {
+ fn #hook() -> ::core::option::Option<#bevy_ecs_path::lifecycle::ComponentHook> {
::core::option::Option::Some(#meta)
}
}
diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs
index 114aff642b..0a6f9b8884 100644
--- a/crates/bevy_ecs/macros/src/lib.rs
+++ b/crates/bevy_ecs/macros/src/lib.rs
@@ -1,4 +1,5 @@
-#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
+//! Macros for deriving ECS traits.
+
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
extern crate proc_macro;
@@ -28,12 +29,48 @@ enum BundleFieldKind {
const BUNDLE_ATTRIBUTE_NAME: &str = "bundle";
const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore";
+const BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS: &str = "ignore_from_components";
+#[derive(Debug)]
+struct BundleAttributes {
+ impl_from_components: bool,
+}
+
+impl Default for BundleAttributes {
+ fn default() -> Self {
+ Self {
+ impl_from_components: true,
+ }
+ }
+}
+
+/// Implement the `Bundle` trait.
#[proc_macro_derive(Bundle, attributes(bundle))]
pub fn derive_bundle(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let ecs_path = bevy_ecs_path();
+ let mut errors = vec![];
+
+ let mut attributes = BundleAttributes::default();
+
+ for attr in &ast.attrs {
+ if attr.path().is_ident(BUNDLE_ATTRIBUTE_NAME) {
+ let parsing = attr.parse_nested_meta(|meta| {
+ if meta.path.is_ident(BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS) {
+ attributes.impl_from_components = false;
+ return Ok(());
+ }
+
+ Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`")))
+ });
+
+ if let Err(error) = parsing {
+ errors.push(error.into_compile_error());
+ }
+ }
+ }
+
let named_fields = match get_struct_fields(&ast.data, "derive(Bundle)") {
Ok(fields) => fields,
Err(e) => return e.into_compile_error().into(),
@@ -128,7 +165,28 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let struct_name = &ast.ident;
+ let from_components = attributes.impl_from_components.then(|| quote! {
+ // SAFETY:
+ // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order
+ #[allow(deprecated)]
+ unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause {
+ #[allow(unused_variables, non_snake_case)]
+ unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
+ where
+ __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_>
+ {
+ Self{
+ #(#field_from_components)*
+ }
+ }
+ }
+ });
+
+ let attribute_errors = &errors;
+
TokenStream::from(quote! {
+ #(#attribute_errors)*
+
// SAFETY:
// - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order
// - `Bundle::get_components` is exactly once for each member. Rely's on the Component -> Bundle implementation to properly pass
@@ -157,20 +215,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
}
}
- // SAFETY:
- // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order
- #[allow(deprecated)]
- unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause {
- #[allow(unused_variables, non_snake_case)]
- unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self
- where
- __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_>
- {
- Self{
- #(#field_from_components)*
- }
- }
- }
+ #from_components
#[allow(deprecated)]
impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause {
@@ -187,6 +232,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
})
}
+/// Implement the `MapEntities` trait.
#[proc_macro_derive(MapEntities, attributes(entities))]
pub fn derive_map_entities(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
@@ -522,16 +568,19 @@ pub(crate) fn bevy_ecs_path() -> syn::Path {
BevyManifest::shared().get_path("bevy_ecs")
}
+/// Implement the `Event` trait.
#[proc_macro_derive(Event, attributes(event))]
pub fn derive_event(input: TokenStream) -> TokenStream {
component::derive_event(input)
}
+/// Implement the `Resource` trait.
#[proc_macro_derive(Resource)]
pub fn derive_resource(input: TokenStream) -> TokenStream {
component::derive_resource(input)
}
+/// Implement the `Component` trait.
#[proc_macro_derive(
Component,
attributes(component, require, relationship, relationship_target, entities)
@@ -540,6 +589,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
component::derive_component(input)
}
+/// Implement the `FromWorld` trait.
#[proc_macro_derive(FromWorld, attributes(from_world))]
pub fn derive_from_world(input: TokenStream) -> TokenStream {
let bevy_ecs_path = bevy_ecs_path();
diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs
index f12cd03a69..7369028715 100644
--- a/crates/bevy_ecs/src/archetype.rs
+++ b/crates/bevy_ecs/src/archetype.rs
@@ -23,17 +23,22 @@ use crate::{
bundle::BundleId,
component::{ComponentId, Components, RequiredComponentConstructor, StorageType},
entity::{Entity, EntityLocation},
+ event::Event,
observer::Observers,
storage::{ImmutableSparseSet, SparseArray, SparseSet, TableId, TableRow},
};
use alloc::{boxed::Box, vec::Vec};
-use bevy_platform::collections::HashMap;
+use bevy_platform::collections::{hash_map::Entry, HashMap};
use core::{
hash::Hash,
ops::{Index, IndexMut, RangeFrom},
};
use nonmax::NonMaxU32;
+#[derive(Event)]
+#[expect(dead_code, reason = "Prepare for the upcoming Query as Entities")]
+pub(crate) struct ArchetypeCreated(pub ArchetypeId);
+
/// An opaque location within a [`Archetype`].
///
/// This can be used in conjunction with [`ArchetypeId`] to find the exact location
@@ -688,7 +693,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer
///
- /// [`OnAdd`]: crate::world::OnAdd
+ /// [`OnAdd`]: crate::lifecycle::OnAdd
#[inline]
pub fn has_add_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER)
@@ -696,7 +701,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer
///
- /// [`OnInsert`]: crate::world::OnInsert
+ /// [`OnInsert`]: crate::lifecycle::OnInsert
#[inline]
pub fn has_insert_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER)
@@ -704,7 +709,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer
///
- /// [`OnReplace`]: crate::world::OnReplace
+ /// [`OnReplace`]: crate::lifecycle::OnReplace
#[inline]
pub fn has_replace_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER)
@@ -712,7 +717,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer
///
- /// [`OnRemove`]: crate::world::OnRemove
+ /// [`OnRemove`]: crate::lifecycle::OnRemove
#[inline]
pub fn has_remove_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER)
@@ -720,7 +725,7 @@ impl Archetype {
/// Returns true if any of the components in this archetype have at least one [`OnDespawn`] observer
///
- /// [`OnDespawn`]: crate::world::OnDespawn
+ /// [`OnDespawn`]: crate::lifecycle::OnDespawn
#[inline]
pub fn has_despawn_observer(&self) -> bool {
self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER)
@@ -869,6 +874,10 @@ impl Archetypes {
}
/// Gets the archetype id matching the given inputs or inserts a new one if it doesn't exist.
+ ///
+ /// Specifically, it returns a tuple where the first element
+ /// is the [`ArchetypeId`] that the given inputs belong to, and the second element is a boolean indicating whether a new archetype was created.
+ ///
/// `table_components` and `sparse_set_components` must be sorted
///
/// # Safety
@@ -881,7 +890,7 @@ impl Archetypes {
table_id: TableId,
table_components: Vec,
sparse_set_components: Vec,
- ) -> ArchetypeId {
+ ) -> (ArchetypeId, bool) {
let archetype_identity = ArchetypeComponents {
sparse_set_components: sparse_set_components.into_boxed_slice(),
table_components: table_components.into_boxed_slice(),
@@ -889,14 +898,13 @@ impl Archetypes {
let archetypes = &mut self.archetypes;
let component_index = &mut self.by_component;
- *self
- .by_components
- .entry(archetype_identity)
- .or_insert_with_key(move |identity| {
+ match self.by_components.entry(archetype_identity) {
+ Entry::Occupied(occupied) => (*occupied.get(), false),
+ Entry::Vacant(vacant) => {
let ArchetypeComponents {
table_components,
sparse_set_components,
- } = identity;
+ } = vacant.key();
let id = ArchetypeId::new(archetypes.len());
archetypes.push(Archetype::new(
components,
@@ -907,8 +915,10 @@ impl Archetypes {
table_components.iter().copied(),
sparse_set_components.iter().copied(),
));
- id
- })
+ vacant.insert(id);
+ (id, true)
+ }
+ }
}
/// Clears all entities from all archetypes.
diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs
index 6bfa4d5630..f327fbc670 100644
--- a/crates/bevy_ecs/src/bundle.rs
+++ b/crates/bevy_ecs/src/bundle.rs
@@ -2,12 +2,63 @@
//!
//! This module contains the [`Bundle`] trait and some other helper types.
+/// Derive the [`Bundle`] trait
+///
+/// You can apply this derive macro to structs that are
+/// composed of [`Component`]s or
+/// other [`Bundle`]s.
+///
+/// ## Attributes
+///
+/// Sometimes parts of the Bundle should not be inserted.
+/// Those can be marked with `#[bundle(ignore)]`, and they will be skipped.
+/// In that case, the field needs to implement [`Default`] unless you also ignore
+/// the [`BundleFromComponents`] implementation.
+///
+/// ```rust
+/// # use bevy_ecs::prelude::{Component, Bundle};
+/// # #[derive(Component)]
+/// # struct Hitpoint;
+/// #
+/// #[derive(Bundle)]
+/// struct HitpointMarker {
+/// hitpoints: Hitpoint,
+///
+/// #[bundle(ignore)]
+/// creator: Option
+/// }
+/// ```
+///
+/// Some fields may be bundles that do not implement
+/// [`BundleFromComponents`]. This happens for bundles that cannot be extracted.
+/// For example with [`SpawnRelatedBundle`](bevy_ecs::spawn::SpawnRelatedBundle), see below for an
+/// example usage.
+/// In those cases you can either ignore it as above,
+/// or you can opt out the whole Struct by marking it as ignored with
+/// `#[bundle(ignore_from_components)]`.
+///
+/// ```rust
+/// # use bevy_ecs::prelude::{Component, Bundle, ChildOf, Spawn};
+/// # #[derive(Component)]
+/// # struct Hitpoint;
+/// # #[derive(Component)]
+/// # struct Marker;
+/// #
+/// use bevy_ecs::spawn::SpawnRelatedBundle;
+///
+/// #[derive(Bundle)]
+/// #[bundle(ignore_from_components)]
+/// struct HitpointMarker {
+/// hitpoints: Hitpoint,
+/// related_spawner: SpawnRelatedBundle>,
+/// }
+/// ```
pub use bevy_ecs_macros::Bundle;
use crate::{
archetype::{
- Archetype, ArchetypeAfterBundleInsert, ArchetypeId, Archetypes, BundleComponentStatus,
- ComponentStatus, SpawnBundleStatus,
+ Archetype, ArchetypeAfterBundleInsert, ArchetypeCreated, ArchetypeId, Archetypes,
+ BundleComponentStatus, ComponentStatus, SpawnBundleStatus,
},
change_detection::MaybeLocation,
component::{
@@ -15,15 +66,13 @@ use crate::{
RequiredComponents, StorageType, Tick,
},
entity::{Entities, EntitiesAllocator, Entity, EntityLocation},
+ lifecycle::{ON_ADD, ON_INSERT, ON_REMOVE, ON_REPLACE},
observer::Observers,
prelude::World,
query::DebugCheckedUnwrap,
relationship::RelationshipHookMode,
storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
- world::{
- unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REMOVE,
- ON_REPLACE,
- },
+ world::{unsafe_world_cell::UnsafeWorldCell, EntityWorldMut},
};
use alloc::{boxed::Box, vec, vec::Vec};
use bevy_platform::collections::{HashMap, HashSet};
@@ -732,7 +781,7 @@ impl BundleInfo {
}
}
- /// Inserts a bundle into the given archetype and returns the resulting archetype.
+ /// Inserts a bundle into the given archetype and returns the resulting archetype and whether a new archetype was created.
/// This could be the same [`ArchetypeId`], in the event that inserting the given bundle
/// does not result in an [`Archetype`] change.
///
@@ -747,12 +796,12 @@ impl BundleInfo {
components: &Components,
observers: &Observers,
archetype_id: ArchetypeId,
- ) -> ArchetypeId {
+ ) -> (ArchetypeId, bool) {
if let Some(archetype_after_insert_id) = archetypes[archetype_id]
.edges()
.get_archetype_after_bundle_insert(self.id)
{
- return archetype_after_insert_id;
+ return (archetype_after_insert_id, false);
}
let mut new_table_components = Vec::new();
let mut new_sparse_set_components = Vec::new();
@@ -806,7 +855,7 @@ impl BundleInfo {
added,
existing,
);
- archetype_id
+ (archetype_id, false)
} else {
let table_id;
let table_components;
@@ -842,13 +891,14 @@ impl BundleInfo {
};
};
// SAFETY: ids in self must be valid
- let new_archetype_id = archetypes.get_id_or_insert(
+ let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert(
components,
observers,
table_id,
table_components,
sparse_set_components,
);
+
// Add an edge from the old archetype to the new archetype.
archetypes[archetype_id]
.edges_mut()
@@ -860,11 +910,11 @@ impl BundleInfo {
added,
existing,
);
- new_archetype_id
+ (new_archetype_id, is_new_created)
}
}
- /// Removes a bundle from the given archetype and returns the resulting archetype
+ /// Removes a bundle from the given archetype and returns the resulting archetype and whether a new archetype was created.
/// (or `None` if the removal was invalid).
/// This could be the same [`ArchetypeId`], in the event that removing the given bundle
/// does not result in an [`Archetype`] change.
@@ -887,7 +937,7 @@ impl BundleInfo {
observers: &Observers,
archetype_id: ArchetypeId,
intersection: bool,
- ) -> Option {
+ ) -> (Option, bool) {
// Check the archetype graph to see if the bundle has been
// removed from this archetype in the past.
let archetype_after_remove_result = {
@@ -898,9 +948,9 @@ impl BundleInfo {
edges.get_archetype_after_bundle_take(self.id())
}
};
- let result = if let Some(result) = archetype_after_remove_result {
+ let (result, is_new_created) = if let Some(result) = archetype_after_remove_result {
// This bundle removal result is cached. Just return that!
- result
+ (result, false)
} else {
let mut next_table_components;
let mut next_sparse_set_components;
@@ -925,7 +975,7 @@ impl BundleInfo {
current_archetype
.edges_mut()
.cache_archetype_after_bundle_take(self.id(), None);
- return None;
+ return (None, false);
}
}
@@ -953,14 +1003,14 @@ impl BundleInfo {
};
}
- let new_archetype_id = archetypes.get_id_or_insert(
+ let (new_archetype_id, is_new_created) = archetypes.get_id_or_insert(
components,
observers,
next_table_id,
next_table_components,
next_sparse_set_components,
);
- Some(new_archetype_id)
+ (Some(new_archetype_id), is_new_created)
};
let current_archetype = &mut archetypes[archetype_id];
// Cache the result in an edge.
@@ -973,7 +1023,7 @@ impl BundleInfo {
.edges_mut()
.cache_archetype_after_bundle_take(self.id(), result);
}
- result
+ (result, is_new_created)
}
}
@@ -1036,14 +1086,15 @@ impl<'w> BundleInserter<'w> {
// SAFETY: We will not make any accesses to the command queue, component or resource data of this world
let bundle_info = world.bundles.get_unchecked(bundle_id);
let bundle_id = bundle_info.id();
- let new_archetype_id = bundle_info.insert_bundle_into_archetype(
+ let (new_archetype_id, is_new_created) = bundle_info.insert_bundle_into_archetype(
&mut world.archetypes,
&mut world.storages,
&world.components,
&world.observers,
archetype_id,
);
- if new_archetype_id == archetype_id {
+
+ let inserter = if new_archetype_id == archetype_id {
let archetype = &mut world.archetypes[archetype_id];
// SAFETY: The edge is assured to be initialized when we called insert_bundle_into_archetype
let archetype_after_insert = unsafe {
@@ -1103,7 +1154,15 @@ impl<'w> BundleInserter<'w> {
world: world.as_unsafe_world_cell(),
}
}
+ };
+
+ if is_new_created {
+ inserter
+ .world
+ .into_deferred()
+ .trigger(ArchetypeCreated(new_archetype_id));
}
+ inserter
}
/// # Safety
@@ -1133,7 +1192,7 @@ impl<'w> BundleInserter<'w> {
if archetype.has_replace_observer() {
deferred_world.trigger_observers(
ON_REPLACE,
- entity,
+ Some(entity),
archetype_after_insert.iter_existing(),
caller,
);
@@ -1318,7 +1377,7 @@ impl<'w> BundleInserter<'w> {
if new_archetype.has_add_observer() {
deferred_world.trigger_observers(
ON_ADD,
- entity,
+ Some(entity),
archetype_after_insert.iter_added(),
caller,
);
@@ -1336,7 +1395,7 @@ impl<'w> BundleInserter<'w> {
if new_archetype.has_insert_observer() {
deferred_world.trigger_observers(
ON_INSERT,
- entity,
+ Some(entity),
archetype_after_insert.iter_inserted(),
caller,
);
@@ -1355,7 +1414,7 @@ impl<'w> BundleInserter<'w> {
if new_archetype.has_insert_observer() {
deferred_world.trigger_observers(
ON_INSERT,
- entity,
+ Some(entity),
archetype_after_insert.iter_added(),
caller,
);
@@ -1421,7 +1480,7 @@ impl<'w> BundleRemover<'w> {
) -> Option {
let bundle_info = world.bundles.get_unchecked(bundle_id);
// SAFETY: Caller ensures archetype and bundle ids are correct.
- let new_archetype_id = unsafe {
+ let (new_archetype_id, is_new_created) = unsafe {
bundle_info.remove_bundle_from_archetype(
&mut world.archetypes,
&mut world.storages,
@@ -1429,11 +1488,14 @@ impl<'w> BundleRemover<'w> {
&world.observers,
archetype_id,
!require_all,
- )?
+ )
};
+ let new_archetype_id = new_archetype_id?;
+
if new_archetype_id == archetype_id {
return None;
}
+
let (old_archetype, new_archetype) =
world.archetypes.get_2_mut(archetype_id, new_archetype_id);
@@ -1447,13 +1509,20 @@ impl<'w> BundleRemover<'w> {
Some((old.into(), new.into()))
};
- Some(Self {
+ let remover = Self {
bundle_info: bundle_info.into(),
new_archetype: new_archetype.into(),
old_archetype: old_archetype.into(),
old_and_new_table: tables,
world: world.as_unsafe_world_cell(),
- })
+ };
+ if is_new_created {
+ remover
+ .world
+ .into_deferred()
+ .trigger(ArchetypeCreated(new_archetype_id));
+ }
+ Some(remover)
}
/// This can be passed to [`remove`](Self::remove) as the `pre_remove` function if you don't want to do anything before removing.
@@ -1499,7 +1568,7 @@ impl<'w> BundleRemover<'w> {
if self.old_archetype.as_ref().has_replace_observer() {
deferred_world.trigger_observers(
ON_REPLACE,
- entity,
+ Some(entity),
bundle_components_in_archetype(),
caller,
);
@@ -1514,7 +1583,7 @@ impl<'w> BundleRemover<'w> {
if self.old_archetype.as_ref().has_remove_observer() {
deferred_world.trigger_observers(
ON_REMOVE,
- entity,
+ Some(entity),
bundle_components_in_archetype(),
caller,
);
@@ -1675,22 +1744,30 @@ impl<'w> BundleSpawner<'w> {
change_tick: Tick,
) -> Self {
let bundle_info = world.bundles.get_unchecked(bundle_id);
- let new_archetype_id = bundle_info.insert_bundle_into_archetype(
+ let (new_archetype_id, is_new_created) = bundle_info.insert_bundle_into_archetype(
&mut world.archetypes,
&mut world.storages,
&world.components,
&world.observers,
ArchetypeId::EMPTY,
);
+
let archetype = &mut world.archetypes[new_archetype_id];
let table = &mut world.storages.tables[archetype.table_id()];
- Self {
+ let spawner = Self {
bundle_info: bundle_info.into(),
table: table.into(),
archetype: archetype.into(),
change_tick,
world: world.as_unsafe_world_cell(),
+ };
+ if is_new_created {
+ spawner
+ .world
+ .into_deferred()
+ .trigger(ArchetypeCreated(new_archetype_id));
}
+ spawner
}
#[inline]
@@ -1770,7 +1847,7 @@ impl<'w> BundleSpawner<'w> {
if archetype.has_add_observer() {
deferred_world.trigger_observers(
ON_ADD,
- entity,
+ Some(entity),
bundle_info.iter_contributed_components(),
caller,
);
@@ -1785,7 +1862,7 @@ impl<'w> BundleSpawner<'w> {
if archetype.has_insert_observer() {
deferred_world.trigger_observers(
ON_INSERT,
- entity,
+ Some(entity),
bundle_info.iter_contributed_components(),
caller,
);
@@ -2056,7 +2133,9 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) {
#[cfg(test)]
mod tests {
- use crate::{component::HookContext, prelude::*, world::DeferredWorld};
+ use crate::{
+ archetype::ArchetypeCreated, lifecycle::HookContext, prelude::*, world::DeferredWorld,
+ };
use alloc::vec;
#[derive(Component)]
@@ -2105,6 +2184,26 @@ mod tests {
}
}
+ #[derive(Bundle)]
+ #[bundle(ignore_from_components)]
+ struct BundleNoExtract {
+ b: B,
+ no_from_comp: crate::spawn::SpawnRelatedBundle>,
+ }
+
+ #[test]
+ fn can_spawn_bundle_without_extract() {
+ let mut world = World::new();
+ let id = world
+ .spawn(BundleNoExtract {
+ b: B,
+ no_from_comp: Children::spawn(Spawn(C)),
+ })
+ .id();
+
+ assert!(world.entity(id).get::().is_some());
+ }
+
#[test]
fn component_hook_order_spawn_despawn() {
let mut world = World::new();
@@ -2293,4 +2392,23 @@ mod tests {
assert_eq!(a, vec![1]);
}
+
+ #[test]
+ fn new_archetype_created() {
+ let mut world = World::new();
+ #[derive(Resource, Default)]
+ struct Count(u32);
+ world.init_resource::();
+ world.add_observer(|_t: Trigger, mut count: ResMut| {
+ count.0 += 1;
+ });
+
+ let mut e = world.spawn((A, B));
+ e.insert(C);
+ e.remove::();
+ e.insert(A);
+ e.insert(A);
+
+ assert_eq!(world.resource::().0, 3);
+ }
}
diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs
index 85219d44ca..83bee583d7 100644
--- a/crates/bevy_ecs/src/change_detection.rs
+++ b/crates/bevy_ecs/src/change_detection.rs
@@ -898,63 +898,39 @@ impl_debug!(Ref<'w, T>,);
/// Unique mutable borrow of an entity's component or of a resource.
///
-/// This can be used in queries to opt into change detection on both their mutable and immutable forms, as opposed to
-/// `&mut T`, which only provides access to change detection while in its mutable form:
+/// This can be used in queries to access change detection from immutable query methods, as opposed
+/// to `&mut T` which only provides access to change detection from mutable query methods.
///
/// ```rust
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::query::QueryData;
/// #
-/// #[derive(Component, Clone)]
+/// #[derive(Component, Clone, Debug)]
/// struct Name(String);
///
-/// #[derive(Component, Clone, Copy)]
+/// #[derive(Component, Clone, Copy, Debug)]
/// struct Health(f32);
///
-/// #[derive(Component, Clone, Copy)]
-/// struct Position {
-/// x: f32,
-/// y: f32,
-/// };
+/// fn my_system(mut query: Query<(Mut, &mut Health)>) {
+/// // Mutable access provides change detection information for both parameters:
+/// // - `name` has type `Mut`
+/// // - `health` has type `Mut`
+/// for (name, health) in query.iter_mut() {
+/// println!("Name: {:?} (last changed {:?})", name, name.last_changed());
+/// println!("Health: {:?} (last changed: {:?})", health, health.last_changed());
+/// # println!("{}{}", name.0, health.0); // Silence dead_code warning
+/// }
///
-/// #[derive(Component, Clone, Copy)]
-/// struct Player {
-/// id: usize,
-/// };
-///
-/// #[derive(QueryData)]
-/// #[query_data(mutable)]
-/// struct PlayerQuery {
-/// id: &'static Player,
-///
-/// // Reacting to `PlayerName` changes is expensive, so we need to enable change detection when reading it.
-/// name: Mut<'static, Name>,
-///
-/// health: &'static mut Health,
-/// position: &'static mut Position,
-/// }
-///
-/// fn update_player_avatars(players_query: Query) {
-/// // The item returned by the iterator is of type `PlayerQueryReadOnlyItem`.
-/// for player in players_query.iter() {
-/// if player.name.is_changed() {
-/// // Update the player's name. This clones a String, and so is more expensive.
-/// update_player_name(player.id, player.name.clone());
-/// }
-///
-/// // Update the health bar.
-/// update_player_health(player.id, *player.health);
-///
-/// // Update the player's position.
-/// update_player_position(player.id, *player.position);
+/// // Immutable access only provides change detection for `Name`:
+/// // - `name` has type `Ref`
+/// // - `health` has type `&Health`
+/// for (name, health) in query.iter() {
+/// println!("Name: {:?} (last changed {:?})", name, name.last_changed());
+/// println!("Health: {:?}", health);
/// }
/// }
///
-/// # bevy_ecs::system::assert_is_system(update_player_avatars);
-///
-/// # fn update_player_name(player: &Player, new_name: Name) {}
-/// # fn update_player_health(player: &Player, new_health: Health) {}
-/// # fn update_player_position(player: &Player, new_position: Position) {}
+/// # bevy_ecs::system::assert_is_system(my_system);
/// ```
pub struct Mut<'w, T: ?Sized> {
pub(crate) value: &'w mut T,
diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs
index 80e60a8860..338050f5fb 100644
--- a/crates/bevy_ecs/src/component.rs
+++ b/crates/bevy_ecs/src/component.rs
@@ -5,16 +5,17 @@ use crate::{
bundle::BundleInfo,
change_detection::{MaybeLocation, MAX_CHANGE_AGE},
entity::{ComponentCloneCtx, Entity, EntityMapper, SourceComponent},
+ lifecycle::{ComponentHook, ComponentHooks},
query::DebugCheckedUnwrap,
- relationship::RelationshipHookMode,
resource::Resource,
storage::{SparseSetIndex, SparseSets, Table, TableRow},
system::{Local, SystemParam},
- world::{DeferredWorld, FromWorld, World},
+ world::{FromWorld, World},
};
use alloc::boxed::Box;
use alloc::{borrow::Cow, format, vec::Vec};
pub use bevy_ecs_macros::Component;
+use bevy_ecs_macros::Event;
use bevy_platform::sync::Arc;
use bevy_platform::{
collections::{HashMap, HashSet},
@@ -375,7 +376,8 @@ use thiserror::Error;
/// - `#[component(on_remove = on_remove_function)]`
///
/// ```
-/// # use bevy_ecs::component::{Component, HookContext};
+/// # use bevy_ecs::component::Component;
+/// # use bevy_ecs::lifecycle::HookContext;
/// # use bevy_ecs::world::DeferredWorld;
/// # use bevy_ecs::entity::Entity;
/// # use bevy_ecs::component::ComponentId;
@@ -404,7 +406,8 @@ use thiserror::Error;
/// This also supports function calls that yield closures
///
/// ```
-/// # use bevy_ecs::component::{Component, HookContext};
+/// # use bevy_ecs::component::Component;
+/// # use bevy_ecs::lifecycle::HookContext;
/// # use bevy_ecs::world::DeferredWorld;
/// #
/// #[derive(Component)]
@@ -656,244 +659,6 @@ pub enum StorageType {
SparseSet,
}
-/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`.
-pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext);
-
-/// Context provided to a [`ComponentHook`].
-#[derive(Clone, Copy, Debug)]
-pub struct HookContext {
- /// The [`Entity`] this hook was invoked for.
- pub entity: Entity,
- /// The [`ComponentId`] this hook was invoked for.
- pub component_id: ComponentId,
- /// The caller location is `Some` if the `track_caller` feature is enabled.
- pub caller: MaybeLocation,
- /// Configures how relationship hooks will run
- pub relationship_hook_mode: RelationshipHookMode,
-}
-
-/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`].
-///
-/// Hooks are functions that run when a component is added, overwritten, or removed from an entity.
-/// These are intended to be used for structural side effects that need to happen when a component is added or removed,
-/// and are not intended for general-purpose logic.
-///
-/// For example, you might use a hook to update a cached index when a component is added,
-/// to clean up resources when a component is removed,
-/// or to keep hierarchical data structures across entities in sync.
-///
-/// This information is stored in the [`ComponentInfo`] of the associated component.
-///
-/// There is two ways of configuring hooks for a component:
-/// 1. Defining the relevant hooks on the [`Component`] implementation
-/// 2. Using the [`World::register_component_hooks`] method
-///
-/// # Example 2
-///
-/// ```
-/// use bevy_ecs::prelude::*;
-/// use bevy_platform::collections::HashSet;
-///
-/// #[derive(Component)]
-/// struct MyTrackedComponent;
-///
-/// #[derive(Resource, Default)]
-/// struct TrackedEntities(HashSet);
-///
-/// let mut world = World::new();
-/// world.init_resource::();
-///
-/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks
-/// let mut tracked_component_query = world.query::<&MyTrackedComponent>();
-/// assert!(tracked_component_query.iter(&world).next().is_none());
-///
-/// world.register_component_hooks::().on_add(|mut world, context| {
-/// let mut tracked_entities = world.resource_mut::();
-/// tracked_entities.0.insert(context.entity);
-/// });
-///
-/// world.register_component_hooks::().on_remove(|mut world, context| {
-/// let mut tracked_entities = world.resource_mut::();
-/// tracked_entities.0.remove(&context.entity);
-/// });
-///
-/// let entity = world.spawn(MyTrackedComponent).id();
-/// let tracked_entities = world.resource::();
-/// assert!(tracked_entities.0.contains(&entity));
-///
-/// world.despawn(entity);
-/// let tracked_entities = world.resource::();
-/// assert!(!tracked_entities.0.contains(&entity));
-/// ```
-#[derive(Debug, Clone, Default)]
-pub struct ComponentHooks {
- pub(crate) on_add: Option,
- pub(crate) on_insert: Option,
- pub(crate) on_replace: Option,
- pub(crate) on_remove: Option,
- pub(crate) on_despawn: Option,
-}
-
-impl ComponentHooks {
- pub(crate) fn update_from_component(&mut self) -> &mut Self {
- if let Some(hook) = C::on_add() {
- self.on_add(hook);
- }
- if let Some(hook) = C::on_insert() {
- self.on_insert(hook);
- }
- if let Some(hook) = C::on_replace() {
- self.on_replace(hook);
- }
- if let Some(hook) = C::on_remove() {
- self.on_remove(hook);
- }
- if let Some(hook) = C::on_despawn() {
- self.on_despawn(hook);
- }
-
- self
- }
-
- /// Register a [`ComponentHook`] that will be run when this component is added to an entity.
- /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as
- /// adding all of its components.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_add` hook
- pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_add(hook)
- .expect("Component already has an on_add hook")
- }
-
- /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`)
- /// or replaced.
- ///
- /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component).
- ///
- /// # Warning
- ///
- /// The hook won't run if the component is already present and is only mutated, such as in a system via a query.
- /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_insert` hook
- pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_insert(hook)
- .expect("Component already has an on_insert hook")
- }
-
- /// Register a [`ComponentHook`] that will be run when this component is about to be dropped,
- /// such as being replaced (with `.insert`) or removed.
- ///
- /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced,
- /// allowing access to the previous data just before it is dropped.
- /// This hook does *not* run if the entity did not already have this component.
- ///
- /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity).
- ///
- /// # Warning
- ///
- /// The hook won't run if the component is already present and is only mutated, such as in a system via a query.
- /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_replace` hook
- pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_replace(hook)
- .expect("Component already has an on_replace hook")
- }
-
- /// Register a [`ComponentHook`] that will be run when this component is removed from an entity.
- /// Despawning an entity counts as removing all of its components.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_remove` hook
- pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_remove(hook)
- .expect("Component already has an on_remove hook")
- }
-
- /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned.
- ///
- /// # Panics
- ///
- /// Will panic if the component already has an `on_despawn` hook
- pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self {
- self.try_on_despawn(hook)
- .expect("Component already has an on_despawn hook")
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity.
- ///
- /// This is a fallible version of [`Self::on_add`].
- ///
- /// Returns `None` if the component already has an `on_add` hook.
- pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_add.is_some() {
- return None;
- }
- self.on_add = Some(hook);
- Some(self)
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`)
- ///
- /// This is a fallible version of [`Self::on_insert`].
- ///
- /// Returns `None` if the component already has an `on_insert` hook.
- pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_insert.is_some() {
- return None;
- }
- self.on_insert = Some(hook);
- Some(self)
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed
- ///
- /// This is a fallible version of [`Self::on_replace`].
- ///
- /// Returns `None` if the component already has an `on_replace` hook.
- pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_replace.is_some() {
- return None;
- }
- self.on_replace = Some(hook);
- Some(self)
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity.
- ///
- /// This is a fallible version of [`Self::on_remove`].
- ///
- /// Returns `None` if the component already has an `on_remove` hook.
- pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_remove.is_some() {
- return None;
- }
- self.on_remove = Some(hook);
- Some(self)
- }
-
- /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned.
- ///
- /// This is a fallible version of [`Self::on_despawn`].
- ///
- /// Returns `None` if the component already has an `on_despawn` hook.
- pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> {
- if self.on_despawn.is_some() {
- return None;
- }
- self.on_despawn = Some(hook);
- Some(self)
- }
-}
-
/// Stores metadata for a type of component or resource stored in a specific [`World`].
#[derive(Debug, Clone)]
pub struct ComponentInfo {
@@ -2052,7 +1817,7 @@ impl Components {
}
/// Gets the metadata associated with the given component, if it is registered.
- /// This will return `None` if the id is not regiserted or is queued.
+ /// This will return `None` if the id is not registered or is queued.
///
/// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value.
#[inline]
@@ -2400,7 +2165,7 @@ impl Components {
/// * [`World::component_id()`]
#[inline]
pub fn valid_component_id(&self) -> Option {
- self.get_id(TypeId::of::())
+ self.get_valid_id(TypeId::of::())
}
/// Type-erased equivalent of [`Components::valid_resource_id()`].
@@ -2431,7 +2196,7 @@ impl Components {
/// * [`Components::get_resource_id()`]
#[inline]
pub fn valid_resource_id(&self) -> Option {
- self.get_resource_id(TypeId::of::())
+ self.get_valid_resource_id(TypeId::of::())
}
/// Type-erased equivalent of [`Components::component_id()`].
@@ -2616,7 +2381,7 @@ impl Tick {
///
/// Returns `true` if wrapping was performed. Otherwise, returns `false`.
#[inline]
- pub(crate) fn check_tick(&mut self, tick: Tick) -> bool {
+ pub fn check_tick(&mut self, tick: Tick) -> bool {
let age = tick.relative_to(*self);
// This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true
// so long as this check always runs before that can happen.
@@ -2629,6 +2394,41 @@ impl Tick {
}
}
+/// An observer [`Event`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make
+/// use of bevy's periodic checks that clamps ticks to a certain range, preventing overflows and thus
+/// keeping methods like [`Tick::is_newer_than`] reliably return `false` for ticks that got too old.
+///
+/// # Example
+///
+/// Here a schedule is stored in a custom resource. This way the systems in it would not have their change
+/// ticks automatically updated via [`World::check_change_ticks`], possibly causing `Tick`-related bugs on
+/// long-running apps.
+///
+/// To fix that, add an observer for this event that calls the schedule's
+/// [`Schedule::check_change_ticks`](bevy_ecs::schedule::Schedule::check_change_ticks).
+///
+/// ```
+/// use bevy_ecs::prelude::*;
+/// use bevy_ecs::component::CheckChangeTicks;
+///
+/// #[derive(Resource)]
+/// struct CustomSchedule(Schedule);
+///
+/// # let mut world = World::new();
+/// world.add_observer(|tick: Trigger, mut schedule: ResMut| {
+/// schedule.0.check_change_ticks(tick.get());
+/// });
+/// ```
+#[derive(Debug, Clone, Copy, Event)]
+pub struct CheckChangeTicks(pub(crate) Tick);
+
+impl CheckChangeTicks {
+ /// Get the `Tick` that can be used as the parameter of [`Tick::check_tick`].
+ pub fn get(self) -> Tick {
+ self.0
+ }
+}
+
/// Interior-mutable access to the [`Tick`]s for a single component or resource.
#[derive(Copy, Clone, Debug)]
pub struct TickCells<'a> {
diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs
index 741a55bcfd..9983ac2e47 100644
--- a/crates/bevy_ecs/src/entity/clone_entities.rs
+++ b/crates/bevy_ecs/src/entity/clone_entities.rs
@@ -711,7 +711,7 @@ impl<'w> EntityClonerBuilder<'w> {
/// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods.
pub fn allow_by_type_ids(&mut self, ids: impl IntoIterator- ) -> &mut Self {
for type_id in ids {
- if let Some(id) = self.world.components().get_id(type_id) {
+ if let Some(id) = self.world.components().get_valid_id(type_id) {
self.filter_allow(id);
}
}
@@ -746,7 +746,7 @@ impl<'w> EntityClonerBuilder<'w> {
/// Extends the list of components that shouldn't be cloned by type ids.
pub fn deny_by_type_ids(&mut self, ids: impl IntoIterator
- ) -> &mut Self {
for type_id in ids {
- if let Some(id) = self.world.components().get_id(type_id) {
+ if let Some(id) = self.world.components().get_valid_id(type_id) {
self.filter_deny(id);
}
}
@@ -768,7 +768,7 @@ impl<'w> EntityClonerBuilder<'w> {
&mut self,
clone_behavior: ComponentCloneBehavior,
) -> &mut Self {
- if let Some(id) = self.world.components().component_id::
() {
+ if let Some(id) = self.world.components().valid_component_id::() {
self.entity_cloner
.clone_behavior_overrides
.insert(id, clone_behavior);
@@ -793,7 +793,7 @@ impl<'w> EntityClonerBuilder<'w> {
/// Removes a previously set override of [`ComponentCloneBehavior`] for a component in this builder.
pub fn remove_clone_behavior_override(&mut self) -> &mut Self {
- if let Some(id) = self.world.components().component_id::() {
+ if let Some(id) = self.world.components().valid_component_id::() {
self.entity_cloner.clone_behavior_overrides.remove(&id);
}
self
diff --git a/crates/bevy_ecs/src/entity/map_entities.rs b/crates/bevy_ecs/src/entity/map_entities.rs
index 75526868d4..ca9e6c5fb8 100644
--- a/crates/bevy_ecs/src/entity/map_entities.rs
+++ b/crates/bevy_ecs/src/entity/map_entities.rs
@@ -357,7 +357,10 @@ mod tests {
// Next allocated entity should be a further generation on the same index
let entity = world.spawn_empty().id();
assert_eq!(entity.index(), dead_ref.index());
- assert!(entity.generation() > dead_ref.generation());
+ assert!(entity
+ .generation()
+ .cmp_approx(&dead_ref.generation())
+ .is_gt());
}
#[test]
@@ -372,6 +375,9 @@ mod tests {
// Next allocated entity should be a further generation on the same index
let entity = world.spawn_empty().id();
assert_eq!(entity.index(), dead_ref.index());
- assert!(entity.generation() > dead_ref.generation());
+ assert!(entity
+ .generation()
+ .cmp_approx(&dead_ref.generation())
+ .is_gt());
}
}
diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs
index fe1e50ab46..32ec1ea184 100644
--- a/crates/bevy_ecs/src/entity/mod.rs
+++ b/crates/bevy_ecs/src/entity/mod.rs
@@ -166,29 +166,6 @@ impl SparseSetIndex for EntityRow {
///
/// This should be treated as a opaque identifier, and its internal representation may be subject to change.
///
-/// # Ordering
-///
-/// [`EntityGeneration`] implements [`Ord`].
-/// Generations that are later will be [`Greater`](core::cmp::Ordering::Greater) than earlier ones.
-///
-/// ```
-/// # use bevy_ecs::entity::EntityGeneration;
-/// assert!(EntityGeneration::FIRST < EntityGeneration::FIRST.after_versions(400));
-/// let (aliased, did_alias) = EntityGeneration::FIRST.after_versions(400).after_versions_and_could_alias(u32::MAX);
-/// assert!(did_alias);
-/// assert!(EntityGeneration::FIRST < aliased);
-/// ```
-///
-/// Ordering will be incorrect for distant generations:
-///
-/// ```
-/// # use bevy_ecs::entity::EntityGeneration;
-/// // This ordering is wrong!
-/// assert!(EntityGeneration::FIRST > EntityGeneration::FIRST.after_versions(400 + (1u32 << 31)));
-/// ```
-///
-/// This strange behavior needed to account for aliasing.
-///
/// # Aliasing
///
/// Internally [`EntityGeneration`] wraps a `u32`, so it can't represent *every* possible generation.
@@ -216,6 +193,9 @@ impl EntityGeneration {
/// Represents the first generation of an [`EntityRow`].
pub const FIRST: Self = Self(0);
+ /// Non-wrapping difference between two generations after which a signed interpretation becomes negative.
+ const DIFF_MAX: u32 = 1u32 << 31;
+
/// Gets some bits that represent this value.
/// The bits are opaque and should not be regarded as meaningful.
#[inline(always)]
@@ -247,18 +227,48 @@ impl EntityGeneration {
let raw = self.0.overflowing_add(versions);
(Self(raw.0), raw.1)
}
-}
-impl PartialOrd for EntityGeneration {
- fn partial_cmp(&self, other: &Self) -> Option {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for EntityGeneration {
- fn cmp(&self, other: &Self) -> core::cmp::Ordering {
- let diff = self.0.wrapping_sub(other.0);
- (1u32 << 31).cmp(&diff)
+ /// Compares two generations.
+ ///
+ /// Generations that are later will be [`Greater`](core::cmp::Ordering::Greater) than earlier ones.
+ ///
+ /// ```
+ /// # use bevy_ecs::entity::EntityGeneration;
+ /// # use core::cmp::Ordering;
+ /// let later_generation = EntityGeneration::FIRST.after_versions(400);
+ /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&later_generation), Ordering::Less);
+ ///
+ /// let (aliased, did_alias) = EntityGeneration::FIRST.after_versions(400).after_versions_and_could_alias(u32::MAX);
+ /// assert!(did_alias);
+ /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&aliased), Ordering::Less);
+ /// ```
+ ///
+ /// Ordering will be incorrect and [non-transitive](https://en.wikipedia.org/wiki/Transitive_relation)
+ /// for distant generations:
+ ///
+ /// ```should_panic
+ /// # use bevy_ecs::entity::EntityGeneration;
+ /// # use core::cmp::Ordering;
+ /// let later_generation = EntityGeneration::FIRST.after_versions(3u32 << 31);
+ /// let much_later_generation = later_generation.after_versions(3u32 << 31);
+ ///
+ /// // while these orderings are correct and pass assertions...
+ /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&later_generation), Ordering::Less);
+ /// assert_eq!(later_generation.cmp_approx(&much_later_generation), Ordering::Less);
+ ///
+ /// // ... this ordering is not and the assertion fails!
+ /// assert_eq!(EntityGeneration::FIRST.cmp_approx(&much_later_generation), Ordering::Less);
+ /// ```
+ ///
+ /// Because of this, `EntityGeneration` does not implement `Ord`/`PartialOrd`.
+ #[inline]
+ pub const fn cmp_approx(&self, other: &Self) -> core::cmp::Ordering {
+ use core::cmp::Ordering;
+ match self.0.wrapping_sub(other.0) {
+ 0 => Ordering::Equal,
+ 1..Self::DIFF_MAX => Ordering::Greater,
+ _ => Ordering::Less,
+ }
}
}
@@ -1433,6 +1443,24 @@ mod tests {
}
}
+ #[test]
+ fn entity_generation_is_approximately_ordered() {
+ use core::cmp::Ordering;
+
+ let old = EntityGeneration::FIRST;
+ let middle = old.after_versions(1);
+ let younger_before_ord_wrap = middle.after_versions(EntityGeneration::DIFF_MAX);
+ let younger_after_ord_wrap = younger_before_ord_wrap.after_versions(1);
+
+ assert_eq!(middle.cmp_approx(&old), Ordering::Greater);
+ assert_eq!(middle.cmp_approx(&middle), Ordering::Equal);
+ assert_eq!(middle.cmp_approx(&younger_before_ord_wrap), Ordering::Less);
+ assert_eq!(
+ middle.cmp_approx(&younger_after_ord_wrap),
+ Ordering::Greater
+ );
+ }
+
#[test]
fn entity_debug() {
let entity = Entity::from_raw(EntityRow::new(NonMaxU32::new(42).unwrap()));
diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs
index d525ba2e57..bb896c4f09 100644
--- a/crates/bevy_ecs/src/event/base.rs
+++ b/crates/bevy_ecs/src/event/base.rs
@@ -68,7 +68,7 @@ pub trait Event: Send + Sync + 'static {
///
/// # Warning
///
- /// This method should not be overridden by implementors,
+ /// This method should not be overridden by implementers,
/// and should always correspond to the implementation of [`component_id`](Event::component_id).
fn register_component_id(world: &mut World) -> ComponentId {
world.register_component::>()
@@ -82,7 +82,7 @@ pub trait Event: Send + Sync + 'static {
///
/// # Warning
///
- /// This method should not be overridden by implementors,
+ /// This method should not be overridden by implementers,
/// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id).
fn component_id(world: &World) -> Option