diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index c4f6e2bd0f..92ea9273f1 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -custom: https://bevyengine.org/donate/ +custom: https://bevy.org/donate/ diff --git a/.github/ISSUE_TEMPLATE/docs_improvement.md b/.github/ISSUE_TEMPLATE/docs_improvement.md index 4bc84c5fc9..f4b6f2019e 100644 --- a/.github/ISSUE_TEMPLATE/docs_improvement.md +++ b/.github/ISSUE_TEMPLATE/docs_improvement.md @@ -10,4 +10,4 @@ assignees: '' Provide a link to the documentation and describe how it could be improved. In what ways is it incomplete, incorrect, or misleading? -If you have suggestions on exactly what the new docs should say, feel free to include them here. Alternatively, make the changes yourself and [create a pull request](https://bevyengine.org/learn/contribute/helping-out/writing-docs/) instead. +If you have suggestions on exactly what the new docs should say, feel free to include them here. Alternatively, make the changes yourself and [create a pull request](https://bevy.org/learn/contribute/helping-out/writing-docs/) instead. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37db848558..d2410b57d2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -293,7 +293,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.32.0 + uses: crate-ci/typos@v1.33.1 - name: Typos info if: failure() run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8a04fadc94..e732cbe66d 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -82,7 +82,7 @@ jobs: - name: Finalize documentation run: | echo "" > target/doc/index.html - echo "dev-docs.bevyengine.org" > target/doc/CNAME + echo "dev-docs.bevy.org" > target/doc/CNAME echo $'User-Agent: *\nDisallow: /' > target/doc/robots.txt rm target/doc/.lock diff --git a/.github/workflows/welcome.yml b/.github/workflows/welcome.yml index 96a287981d..87df34b932 100644 --- a/.github/workflows/welcome.yml +++ b/.github/workflows/welcome.yml @@ -43,5 +43,5 @@ jobs: repo: context.repo.repo, body: `**Welcome**, new contributor! - Please make sure you've read our [contributing guide](https://bevyengine.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨` + Please make sure you've read our [contributing guide](https://bevy.org/learn/contribute/introduction) and we look forward to reviewing your pull request shortly ✨` }) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73bc77c455..887318f22a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ # Contributing to Bevy If you'd like to help build Bevy, start by reading this -[introduction](https://bevyengine.org/learn/contribute/introduction). Thanks for contributing! +[introduction](https://bevy.org/learn/contribute/introduction). Thanks for contributing! diff --git a/Cargo.toml b/Cargo.toml index 9602ef33fb..32891dd1be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" categories = ["game-engines", "graphics", "gui", "rendering"] description = "A refreshingly simple data-driven game engine and app framework" exclude = ["assets/", "tools/", ".github/", "crates/", "examples/wasm/assets/"] -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" keywords = ["game", "engine", "gamedev", "graphics", "bevy"] license = "MIT OR Apache-2.0" repository = "https://github.com/bevyengine/bevy" @@ -72,6 +72,7 @@ allow_attributes_without_reason = "warn" [workspace.lints.rust] missing_docs = "warn" +mismatched_lifetime_syntaxes = "allow" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } unsafe_code = "deny" unsafe_op_in_unsafe_fn = "warn" @@ -133,6 +134,7 @@ default = [ "bevy_audio", "bevy_color", "bevy_core_pipeline", + "bevy_core_widgets", "bevy_anti_aliasing", "bevy_gilrs", "bevy_gizmos", @@ -291,6 +293,9 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] +# Headless widget collection for Bevy UI. +bevy_core_widgets = ["bevy_internal/bevy_core_widgets"] + # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -534,6 +539,9 @@ libm = ["bevy_internal/libm"] # Enables use of browser APIs. Note this is currently only applicable on `wasm32` architectures. web = ["bevy_internal/web"] +# Enable hotpatching of Bevy systems +hotpatching = ["bevy_internal/hotpatching"] + [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } @@ -3539,6 +3547,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_transform" +path = "examples/ui/ui_transform.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_transform] +name = "UI Transform" +description = "An example demonstrating how to translate, rotate and scale UI elements." +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" @@ -3922,6 +3941,16 @@ description = "A simple 2D screen shake effect" category = "Camera" wasm = true +[[example]] +name = "2d_on_ui" +path = "examples/camera/2d_on_ui.rs" +doc-scrape-examples = true + +[package.metadata.example.2d_on_ui] +name = "2D on Bevy UI" +description = "Shows how to render 2D objects on top of Bevy UI" +category = "Camera" +wasm = true [package.metadata.example.fps_overlay] name = "FPS overlay" @@ -4401,3 +4430,37 @@ name = "Cooldown" description = "Example for cooldown on button clicks" category = "Usage" wasm = true + +[[example]] +name = "hotpatching_systems" +path = "examples/ecs/hotpatching_systems.rs" +doc-scrape-examples = true +required-features = ["hotpatching"] + +[package.metadata.example.hotpatching_systems] +name = "Hotpatching Systems" +description = "Demonstrates how to hotpatch systems" +category = "ECS (Entity Component System)" +wasm = false + +[[example]] +name = "core_widgets" +path = "examples/ui/core_widgets.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets] +name = "Core Widgets" +description = "Demonstrates use of core (headless) widgets in Bevy UI" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "core_widgets_observers" +path = "examples/ui/core_widgets_observers.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets_observers] +name = "Core Widgets (w/Observers)" +description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers" +category = "UI (User Interface)" +wasm = true diff --git a/README.md b/README.md index 1daeadda5d..db0a3d57b1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# [![Bevy](assets/branding/bevy_logo_light_dark_and_dimmed.svg)](https://bevyengine.org) +# [![Bevy](assets/branding/bevy_logo_light_dark_and_dimmed.svg)](https://bevy.org) [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) [![Crates.io](https://img.shields.io/crates/v/bevy.svg)](https://crates.io/crates/bevy) @@ -13,7 +13,7 @@ Bevy is a refreshingly simple data-driven game engine built in Rust. It is free ## WARNING -Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released [approximately once every 3 months](https://bevyengine.org/news/bevy-0-6/#the-train-release-schedule). We provide [migration guides](https://bevyengine.org/learn/migration-guides/), but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment. +Bevy is still in the early stages of development. Important features are missing. Documentation is sparse. A new version of Bevy containing breaking changes to the API is released [approximately once every 3 months](https://bevy.org/news/bevy-0-6/#the-train-release-schedule). We provide [migration guides](https://bevy.org/learn/migration-guides/), but we can't guarantee migrations will always be easy. Use only if you are willing to work in this environment. **MSRV:** Bevy relies heavily on improvements in the Rust language and compiler. As a result, the Minimum Supported Rust Version (MSRV) is generally close to "the latest stable release" of Rust. @@ -29,15 +29,15 @@ As a result, the Minimum Supported Rust Version (MSRV) is generally close to "th ## About -* **[Features](https://bevyengine.org):** A quick overview of Bevy's features. -* **[News](https://bevyengine.org/news/)**: A development blog that covers our progress, plans and shiny new features. +* **[Features](https://bevy.org):** A quick overview of Bevy's features. +* **[News](https://bevy.org/news/)**: A development blog that covers our progress, plans and shiny new features. ## Docs -* **[Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction):** Bevy's official Quick Start Guide. The best place to start learning Bevy. +* **[Quick Start Guide](https://bevy.org/learn/quick-start/introduction):** Bevy's official Quick Start Guide. The best place to start learning Bevy. * **[Bevy Rust API Docs](https://docs.rs/bevy):** Bevy's Rust API docs, which are automatically generated from the doc comments in this repo. * **[Official Examples](https://github.com/bevyengine/bevy/tree/latest/examples):** Bevy's dedicated, runnable examples, which are great for digging into specific concepts. -* **[Community-Made Learning Resources](https://bevyengine.org/assets/#learning)**: More tutorials, documentation, and examples made by the Bevy community. +* **[Community-Made Learning Resources](https://bevy.org/assets/#learning)**: More tutorials, documentation, and examples made by the Bevy community. ## Community @@ -46,11 +46,11 @@ Before contributing or participating in discussions with the community, you shou * **[Discord](https://discord.gg/bevy):** Bevy's official discord server. * **[Reddit](https://reddit.com/r/bevy):** Bevy's official subreddit. * **[GitHub Discussions](https://github.com/bevyengine/bevy/discussions):** The best place for questions about Bevy, answered right here! -* **[Bevy Assets](https://bevyengine.org/assets/):** A collection of awesome Bevy projects, tools, plugins and learning materials. +* **[Bevy Assets](https://bevy.org/assets/):** A collection of awesome Bevy projects, tools, plugins and learning materials. ### Contributing -If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevyengine.org/learn/contribute/introduction)**. +If you'd like to help build Bevy, check out the **[Contributor's Guide](https://bevy.org/learn/contribute/introduction)**. For simple problems, feel free to [open an issue](https://github.com/bevyengine/bevy/issues) or [PR](https://github.com/bevyengine/bevy/pulls) and tackle it yourself! @@ -58,9 +58,9 @@ For more complex architecture decisions and experimental mad science, please ope ## Getting Started -We recommend checking out the [Quick Start Guide](https://bevyengine.org/learn/quick-start/introduction) for a brief introduction. +We recommend checking out the [Quick Start Guide](https://bevy.org/learn/quick-start/introduction) for a brief introduction. -Follow the [Setup guide](https://bevyengine.org/learn/quick-start/getting-started/setup) to ensure your development environment is set up correctly. +Follow the [Setup guide](https://bevy.org/learn/quick-start/getting-started/setup) to ensure your development environment is set up correctly. Once set up, you can quickly try out the [examples](https://github.com/bevyengine/bevy/tree/latest/examples) by cloning this repo and running the following commands: ```sh @@ -84,7 +84,7 @@ fn main() { ### Fast Compiles -Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by [following the instructions here](https://bevyengine.org/learn/quick-start/getting-started/setup). +Bevy can be built just fine using default configuration on stable Rust. However for really fast iterative compiles, you should enable the "fast compiles" setup by [following the instructions here](https://bevy.org/learn/quick-start/getting-started/setup). ## [Bevy Cargo Features][cargo_features] @@ -96,7 +96,7 @@ This [list][cargo_features] outlines the different cargo features supported by B Bevy is the result of the hard work of many people. A huge thanks to all Bevy contributors, the many open source projects that have come before us, the [Rust gamedev ecosystem](https://arewegameyet.rs/), and the many libraries we build on. -A huge thanks to Bevy's [generous sponsors](https://bevyengine.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevyengine.org/donate/) if you like what we're building. +A huge thanks to Bevy's [generous sponsors](https://bevy.org). Bevy will always be free and open source, but it isn't free to make. Please consider [sponsoring our work](https://bevy.org/donate/) if you like what we're building. This project is tested with BrowserStack. diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index a95cb4a821..70f3cb6703 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -32,7 +32,7 @@ fn segment_ease(c: &mut Criterion) { fn curve_position(c: &mut Criterion) { /// A helper function that benchmarks calling [`CubicCurve::position()`] over a generic [`VectorSpace`]. - fn bench_curve( + fn bench_curve>( 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 { world.component_id::>() diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index c4e36dc4fc..dfc32e60db 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -10,8 +10,9 @@ use crate::reflect::{ReflectComponent, ReflectFromWorld}; use crate::{ bundle::Bundle, - component::{Component, HookContext}, + component::Component, entity::Entity, + lifecycle::HookContext, relationship::{RelatedSpawner, RelatedSpawnerCommands}, system::EntityCommands, world::{DeferredWorld, EntityWorldMut, FromWorld, World}, @@ -440,7 +441,7 @@ pub fn validate_parent_has_component( let name: Option = None; warn!( "warning[B0004]: {}{name} with the {ty_name} component has a parent without {ty_name}.\n\ - This will cause inconsistent behaviors! See: https://bevyengine.org/learn/errors/b0004", + This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004", caller.map(|c| format!("{c}: ")).unwrap_or_default(), ty_name = ShortName::of::(), name = name.map_or_else( diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 6e2d1cbc97..45548d17c5 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -13,8 +13,8 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![expect(unsafe_code, reason = "Unsafe code is used to improve performance.")] #![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] @@ -41,6 +41,7 @@ pub mod event; pub mod hierarchy; pub mod intern; pub mod label; +pub mod lifecycle; pub mod name; pub mod never; pub mod observer; @@ -48,7 +49,6 @@ pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; pub mod relationship; -pub mod removal_detection; pub mod resource; pub mod schedule; pub mod spawn; @@ -59,6 +59,9 @@ pub mod world; pub use bevy_ptr as ptr; +#[cfg(feature = "hotpatching")] +use event::Event; + /// The ECS prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. @@ -73,12 +76,12 @@ pub mod prelude { error::{BevyError, Result}, event::{Event, EventMutator, EventReader, EventWriter, Events}, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, + lifecycle::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, RemovedComponents}, name::{Name, NameOrEntity}, observer::{Observer, Trigger}, query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, related, relationship::RelationshipTarget, - removal_detection::RemovedComponents, resource::Resource, schedule::{ common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, @@ -93,7 +96,7 @@ pub mod prelude { }, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, - FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, + FromWorld, World, }, }; @@ -123,6 +126,13 @@ pub mod __macro_exports { pub use alloc::vec::Vec; } +/// Event sent when a hotpatch happens. +/// +/// Systems should refresh their inner pointers. +#[cfg(feature = "hotpatching")] +#[derive(Event, Default)] +pub struct HotPatched; + #[cfg(test)] mod tests { use crate::{ diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs new file mode 100644 index 0000000000..e228b416c3 --- /dev/null +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -0,0 +1,606 @@ +//! This module contains various tools to allow you to react to component insertion or removal, +//! as well as entity spawning and despawning. +//! +//! There are four main ways to react to these lifecycle events: +//! +//! 1. Using component hooks, which act as inherent constructors and destructors for components. +//! 2. Using [observers], which are a user-extensible way to respond to events, including component lifecycle events. +//! 3. Using the [`RemovedComponents`] system parameter, which offers an event-style interface. +//! 4. Using the [`Added`] query filter, which checks each component to see if it has been added since the last time a system ran. +//! +//! [observers]: crate::observer +//! [`Added`]: crate::query::Added +//! +//! # Types of lifecycle events +//! +//! There are five types of lifecycle events, split into two categories. First, we have lifecycle events that are triggered +//! when a component is added to an entity: +//! +//! - [`OnAdd`]: Triggered when a component is added to an entity that did not already have it. +//! - [`OnInsert`]: Triggered when a component is added to an entity, regardless of whether it already had it. +//! +//! When both events occur, [`OnAdd`] hooks are evaluated before [`OnInsert`]. +//! +//! Next, we have lifecycle events that are triggered when a component is removed from an entity: +//! +//! - [`OnReplace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value. +//! - [`OnRemove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed. +//! - [`OnDespawn`]: Triggered for each component on an entity when it is despawned. +//! +//! [`OnReplace`] hooks are evaluated before [`OnRemove`], then finally [`OnDespawn`] hooks are evaluated. +//! +//! [`OnAdd`] and [`OnRemove`] are counterparts: they are only triggered when a component is added or removed +//! from an entity in such a way as to cause a change in the component's presence on that entity. +//! Similarly, [`OnInsert`] and [`OnReplace`] are counterparts: they are triggered when a component is added or replaced +//! on an entity, regardless of whether this results in a change in the component's presence on that entity. +//! +//! To reliably synchronize data structures using with component lifecycle events, +//! you can combine [`OnInsert`] and [`OnReplace`] to fully capture any changes to the data. +//! This is particularly useful in combination with immutable components, +//! to avoid any lifecycle-bypassing mutations. +//! +//! ## Lifecycle events and component types +//! +//! Despite the absence of generics, each lifecycle event is associated with a specific component. +//! When defining a component hook for a [`Component`] type, that component is used. +//! When listening to lifecycle events for observers, the `B: Bundle` generic is used. +//! +//! Each of these lifecycle events also corresponds to a fixed [`ComponentId`], +//! which are assigned during [`World`] initialization. +//! For example, [`OnAdd`] corresponds to [`ON_ADD`]. +//! This is used to skip [`TypeId`](core::any::TypeId) lookups in hot paths. +use crate::{ + change_detection::MaybeLocation, + component::{Component, ComponentId, ComponentIdFor, Tick}, + entity::Entity, + event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, + relationship::RelationshipHookMode, + storage::SparseSet, + system::{Local, ReadOnlySystemParam, SystemMeta, SystemParam}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, +}; + +use derive_more::derive::Into; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +use core::{ + fmt::Debug, + iter, + marker::PhantomData, + ops::{Deref, DerefMut}, + option, +}; + +/// 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`](crate::component::ComponentInfo) of the associated component. +/// +/// There are 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 +/// +/// ``` +/// 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 needs to be combined with immutable components to serve as a 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 needs to be combined with immutable components to serve as a 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) + } +} + +/// [`ComponentId`] for [`OnAdd`] +pub const ON_ADD: ComponentId = ComponentId::new(0); +/// [`ComponentId`] for [`OnInsert`] +pub const ON_INSERT: ComponentId = ComponentId::new(1); +/// [`ComponentId`] for [`OnReplace`] +pub const ON_REPLACE: ComponentId = ComponentId::new(2); +/// [`ComponentId`] for [`OnRemove`] +pub const ON_REMOVE: ComponentId = ComponentId::new(3); +/// [`ComponentId`] for [`OnDespawn`] +pub const ON_DESPAWN: ComponentId = ComponentId::new(4); + +/// Trigger emitted when a component is inserted onto an entity that does not already have that +/// component. Runs before `OnInsert`. +/// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnAdd; + +/// Trigger emitted when a component is inserted, regardless of whether or not the entity already +/// had that component. Runs after `OnAdd`, if it ran. +/// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnInsert; + +/// Trigger emitted when a component is removed from an entity, regardless +/// of whether or not it is later replaced. +/// +/// Runs before the value is replaced, so you can still access the original component data. +/// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnReplace; + +/// Trigger emitted when a component is removed from an entity, and runs before the component is +/// removed, so you can still access the component data. +/// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnRemove; + +/// Trigger emitted for each component on an entity when it is despawned. +/// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. +#[derive(Event, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +pub struct OnDespawn; + +/// Wrapper around [`Entity`] for [`RemovedComponents`]. +/// Internally, `RemovedComponents` uses these as an `Events`. +#[derive(Event, Debug, Clone, Into)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] +pub struct RemovedComponentEntity(Entity); + +/// Wrapper around a [`EventCursor`] so that we +/// can differentiate events between components. +#[derive(Debug)] +pub struct RemovedComponentReader +where + T: Component, +{ + reader: EventCursor, + marker: PhantomData, +} + +impl Default for RemovedComponentReader { + fn default() -> Self { + Self { + reader: Default::default(), + marker: PhantomData, + } + } +} + +impl Deref for RemovedComponentReader { + type Target = EventCursor; + fn deref(&self) -> &Self::Target { + &self.reader + } +} + +impl DerefMut for RemovedComponentReader { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.reader + } +} + +/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. +#[derive(Default, Debug)] +pub struct RemovedComponentEvents { + event_sets: SparseSet>, +} + +impl RemovedComponentEvents { + /// Creates an empty storage buffer for component removal events. + pub fn new() -> Self { + Self::default() + } + + /// For each type of component, swaps the event buffers and clears the oldest event buffer. + /// In general, this should be called once per frame/update. + pub fn update(&mut self) { + for (_component_id, events) in self.event_sets.iter_mut() { + events.update(); + } + } + + /// Returns an iterator over components and their entity events. + pub fn iter(&self) -> impl Iterator)> { + self.event_sets.iter() + } + + /// Gets the event storage for a given component. + pub fn get( + &self, + component_id: impl Into, + ) -> Option<&Events> { + self.event_sets.get(component_id.into()) + } + + /// Sends a removal event for the specified component. + pub fn send(&mut self, component_id: impl Into, entity: Entity) { + self.event_sets + .get_or_insert_with(component_id.into(), Default::default) + .send(RemovedComponentEntity(entity)); + } +} + +/// A [`SystemParam`] that yields entities that had their `T` [`Component`] +/// removed or have been despawned with it. +/// +/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). +/// +/// Unlike hooks or observers (see the [lifecycle](crate) module docs), +/// this does not allow you to see which data existed before removal. +/// +/// If you are using `bevy_ecs` as a standalone crate, +/// note that the [`RemovedComponents`] list will not be automatically cleared for you, +/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). +/// +/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is +/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. +/// For the main world, this is delayed until after all `SubApp`s have run. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::system::IntoSystem; +/// # use bevy_ecs::lifecycle::RemovedComponents; +/// # +/// # #[derive(Component)] +/// # struct MyComponent; +/// fn react_on_removal(mut removed: RemovedComponents) { +/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); +/// } +/// # bevy_ecs::system::assert_is_system(react_on_removal); +/// ``` +#[derive(SystemParam)] +pub struct RemovedComponents<'w, 's, T: Component> { + component_id: ComponentIdFor<'s, T>, + reader: Local<'s, RemovedComponentReader>, + event_sets: &'w RemovedComponentEvents, +} + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIter<'a> = iter::Map< + iter::Flatten>>>, + fn(RemovedComponentEntity) -> Entity, +>; + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIterWithId<'a> = iter::Map< + iter::Flatten>>, + fn( + (&RemovedComponentEntity, EventId), + ) -> (Entity, EventId), +>; + +fn map_id_events( + (entity, id): (&RemovedComponentEntity, EventId), +) -> (Entity, EventId) { + (entity.clone().into(), id) +} + +// For all practical purposes, the api surface of `RemovedComponents` +// should be similar to `EventReader` to reduce confusion. +impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { + /// Fetch underlying [`EventCursor`]. + pub fn reader(&self) -> &EventCursor { + &self.reader + } + + /// Fetch underlying [`EventCursor`] mutably. + pub fn reader_mut(&mut self) -> &mut EventCursor { + &mut self.reader + } + + /// Fetch underlying [`Events`]. + pub fn events(&self) -> Option<&Events> { + self.event_sets.get(self.component_id.get()) + } + + /// Destructures to get a mutable reference to the `EventCursor` + /// and a reference to `Events`. + /// + /// This is necessary since Rust can't detect destructuring through methods and most + /// usecases of the reader uses the `Events` as well. + pub fn reader_mut_with_events( + &mut self, + ) -> Option<( + &mut RemovedComponentReader, + &Events, + )> { + self.event_sets + .get(self.component_id.get()) + .map(|events| (&mut *self.reader, events)) + } + + /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the + /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events + /// that happened before now. + pub fn read(&mut self) -> RemovedIter<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read(events).cloned()) + .into_iter() + .flatten() + .map(RemovedComponentEntity::into) + } + + /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. + pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read_with_id(events)) + .into_iter() + .flatten() + .map(map_id_events) + } + + /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. + pub fn len(&self) -> usize { + self.events() + .map(|events| self.reader.len(events)) + .unwrap_or(0) + } + + /// Returns `true` if there are no events available to read. + pub fn is_empty(&self) -> bool { + self.events() + .is_none_or(|events| self.reader.is_empty(events)) + } + + /// Consumes all available events. + /// + /// This means these events will not appear in calls to [`RemovedComponents::read()`] or + /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. + pub fn clear(&mut self) { + if let Some((reader, events)) = self.reader_mut_with_events() { + reader.clear(events); + } + } +} + +// SAFETY: Only reads World removed component events +unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} + +// SAFETY: no component value access. +unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { + type State = (); + type Item<'w, 's> = &'w RemovedComponentEvents; + + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + + #[inline] + unsafe fn get_param<'w, 's>( + _state: &'s mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + _change_tick: Tick, + ) -> Self::Item<'w, 's> { + world.removed_components() + } +} diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index 2c2d42b1c9..bd45072a5a 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -1,8 +1,7 @@ use crate::{ - component::{ - Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType, - }, + component::{Component, ComponentCloneBehavior, Mutable, StorageType}, entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent}, + lifecycle::{ComponentHook, HookContext}, world::World, }; use alloc::vec::Vec; diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 61b4b4eaf7..f7db84c5a9 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1,4 +1,136 @@ -//! Types for creating and storing [`Observer`]s +//! Observers are a push-based tool for responding to [`Event`]s. +//! +//! ## Observer targeting +//! +//! Observers can be "global", listening for events that are both targeted at and not targeted at any specific entity, +//! or they can be "entity-specific", listening for events that are targeted at specific entities. +//! +//! They can also be further refined by listening to events targeted at specific components +//! (instead of using a generic event type), as is done with the [`OnAdd`] family of lifecycle events. +//! +//! When entities are observed, they will receive an [`ObservedBy`] component, +//! which will be updated to track the observers that are currently observing them. +//! +//! Currently, [observers cannot be retargeted after spawning](https://github.com/bevyengine/bevy/issues/19587): +//! despawn and respawn an observer as a workaround. +//! +//! ## Writing observers +//! +//! Observers are systems which implement [`IntoObserverSystem`] that listen for [`Event`]s matching their +//! type and target(s). +//! To write observer systems, use the [`Trigger`] system parameter as the first parameter of your system. +//! This parameter provides access to the specific event that triggered the observer, +//! as well as the entity that the event was targeted at, if any. +//! +//! Observers can request other data from the world, +//! such as via a [`Query`] or [`Res`]. Commonly, you might want to verify that +//! the entity that the observable event is targeting has a specific component, +//! or meets some other condition. +//! [`Query::get`] or [`Query::contains`] on the [`Trigger::target`] entity +//! is a good way to do this. +//! +//! [`Commands`] can also be used inside of observers. +//! This can be particularly useful for triggering other observers! +//! +//! ## Spawning observers +//! +//! Observers can be spawned via [`World::add_observer`], or the equivalent app method. +//! This will cause an entity with the [`Observer`] component to be created, +//! which will then run the observer system whenever the event it is watching is triggered. +//! +//! You can control the targets that an observer is watching by calling [`Observer::watch_entity`] +//! once the entity is spawned, or by manually spawning an entity with the [`Observer`] component +//! configured with the desired targets. +//! +//! Observers are fundamentally defined as "entities which have the [`Observer`] component" +//! allowing you to add it manually to existing entities. +//! At first, this seems convenient, but only one observer can be added to an entity at a time, +//! regardless of the event it responds to: like always, components are unique. +//! +//! Instead, a better way to achieve a similar aim is to +//! use the [`EntityWorldMut::observe`] / [`EntityCommands::observe`] method, +//! which spawns a new observer, and configures it to watch the entity it is called on. +//! Unfortunately, observers defined in this way +//! [currently cannot be spawned as part of bundles](https://github.com/bevyengine/bevy/issues/14204). +//! +//! ## Triggering observers +//! +//! Observers are most commonly triggered by [`Commands`], +//! via [`Commands::trigger`] (for untargeted events) or [`Commands::trigger_targets`] (for targeted events). +//! Like usual, equivalent methods are available on [`World`], allowing you to reduce overhead when working with exclusive world access. +//! +//! If your observer is configured to watch for a specific component or set of components instead, +//! you can pass in [`ComponentId`]s into [`Commands::trigger_targets`] by using the [`TriggerTargets`] trait. +//! As discussed in the [`Trigger`] documentation, this use case is rare, and is currently only used +//! for [lifecycle](crate::lifecycle) events, which are automatically emitted. +//! +//! ## Observer bubbling +//! +//! When events are targeted at an entity, they can optionally bubble to other targets, +//! typically up to parents in an entity hierarchy. +//! +//! This behavior is controlled via [`Event::Traversal`] and [`Event::AUTO_PROPAGATE`], +//! with the details of the propagation path specified by the [`Traversal`](crate::traversal::Traversal) trait. +//! +//! When auto-propagation is enabled, propagaion must be manually stopped to prevent the event from +//! continuing to other targets. +//! This can be done using the [`Trigger::propagate`] method on the [`Trigger`] system parameter inside of your observer. +//! +//! ## Observer timing +//! +//! Observers are triggered via [`Commands`], which imply that they are evaluated at the next sync point in the ECS schedule. +//! Accordingly, they have full access to the world, and are evaluated sequentially, in the order that the commands were sent. +//! +//! To control the relative ordering of observers sent from different systems, +//! order the systems in the schedule relative to each other. +//! +//! Currently, Bevy does not provide [a way to specify the ordering of observers](https://github.com/bevyengine/bevy/issues/14890) +//! listening to the same event relative to each other. +//! +//! Commands sent by observers are [currently not immediately applied](https://github.com/bevyengine/bevy/issues/19569). +//! Instead, all queued observers will run, and then all of the commands from those observers will be applied. +//! Careful use of [`Schedule::apply_deferred`] may help as a workaround. +//! +//! ## Lifecycle events and observers +//! +//! It is important to note that observers, just like [hooks](crate::lifecycle::ComponentHooks), +//! can listen to and respond to [lifecycle](crate::lifecycle) events. +//! Unlike hooks, observers are not treated as an "innate" part of component behavior: +//! they can be added or removed at runtime, and multiple observers +//! can be registered for the same lifecycle event for the same component. +//! +//! The ordering of hooks versus observers differs based on the lifecycle event in question: +//! +//! - when adding components, hooks are evaluated first, then observers +//! - when removing components, observers are evaluated first, then hooks +//! +//! This allows hooks to act as constructors and destructors for components, +//! as they always have the first and final say in the component's lifecycle. +//! +//! ## Cleaning up observers +//! +//! Currently, observer entities are never cleaned up, even if their target entity(s) are despawned. +//! This won't cause any runtime overhead, but is a waste of memory and can result in memory leaks. +//! +//! If you run into this problem, you could manually scan the world for observer entities and despawn them, +//! by checking if the entity in [`Observer::descriptor`] still exists. +//! +//! ## Observers vs buffered events +//! +//! By contrast, [`EventReader`] and [`EventWriter`] ("buffered events"), are pull-based. +//! They require periodically polling the world to check for new events, typically in a system that runs as part of a schedule. +//! +//! This imposes a small overhead, making observers a better choice for extremely rare events, +//! but buffered events can be more efficient for events that are expected to occur multiple times per frame, +//! as it allows for batch processing of events. +//! +//! The difference in timing is also an important consideration: +//! buffered events are evaluated at fixed points during schedules, +//! while observers are evaluated as soon as possible after the event is triggered. +//! +//! This provides more control over the timing of buffered event evaluation, +//! but allows for a more ad hoc approach with observers, +//! and enables indefinite chaining of observers triggering other observers (for both better and worse!). mod entity_observer; mod runner; @@ -29,6 +161,17 @@ use smallvec::SmallVec; /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the /// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also /// contains event propagation information. See [`Trigger::propagate`] for more information. +/// +/// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. +/// The entity involved *does not* have to have these components, but the observer will only be +/// triggered if the event matches the components in `B`. +/// +/// This is used to to avoid providing a generic argument in your event, as is done for [`OnAdd`] +/// and the other lifecycle events. +/// +/// Providing multiple components in this bundle will cause this event to be triggered by any +/// matching component in the bundle, +/// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). pub struct Trigger<'w, E, B: Bundle = ()> { event: &'w mut E, propagate: &'w mut bool, @@ -68,20 +211,8 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { } /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. It may - /// be [`Entity::PLACEHOLDER`]. - /// - /// Observable events can target specific entities. When those events fire, they will trigger - /// any observers on the targeted entities. In this case, the `target()` and `observer()` are - /// the same, because the observer that was triggered is attached to the entity that was - /// targeted by the event. - /// - /// However, it is also possible for those events to bubble up the entity hierarchy and trigger - /// observers on *different* entities, or trigger a global observer. In these cases, the - /// observing entity is *different* from the entity being targeted by the event. - /// - /// This is an important distinction: the entity reacting to an event is not always the same as - /// the entity triggered by the event. - pub fn target(&self) -> Entity { + /// be [`None`] if the trigger is not for a particular entity. + pub fn target(&self) -> Option { self.trigger.target } @@ -172,10 +303,14 @@ impl<'w, E, B: Bundle> DerefMut for Trigger<'w, E, B> { } } -/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. Targets can be of type [`Entity`] or [`ComponentId`]. +/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. /// /// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination /// will run. +/// +/// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. +/// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, +/// allowing you to trigger events for multiple targets at once. pub trait TriggerTargets { /// The components the trigger should target. fn components(&self) -> impl Iterator + Clone + '_; @@ -280,7 +415,9 @@ all_tuples!( T ); -/// A description of what an [`Observer`] observes. +/// Store information about what an [`Observer`] observes. +/// +/// This information is stored inside of the [`Observer`] component, #[derive(Default, Clone)] pub struct ObserverDescriptor { /// The events the observer is watching. @@ -331,7 +468,9 @@ impl ObserverDescriptor { } } -/// Event trigger metadata for a given [`Observer`], +/// Metadata about a specific [`Event`] which triggered an observer. +/// +/// This information is exposed via methods on the [`Trigger`] system parameter. #[derive(Debug)] pub struct ObserverTrigger { /// The [`Entity`] of the observer handling the trigger. @@ -341,7 +480,7 @@ pub struct ObserverTrigger { /// The [`ComponentId`]s the trigger targeted. components: SmallVec<[ComponentId; 2]>, /// The entity the trigger targeted. - pub target: Entity, + pub target: Option, /// The location of the source code that triggered the observer. pub caller: MaybeLocation, } @@ -357,6 +496,8 @@ impl ObserverTrigger { type ObserverMap = EntityHashMap; /// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component. +/// +/// This is stored inside of [`CachedObservers`]. #[derive(Default, Debug)] pub struct CachedComponentObservers { // Observers listening to triggers targeting this component @@ -366,6 +507,8 @@ pub struct CachedComponentObservers { } /// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger. +/// +/// This is stored inside of [`Observers`], specialized for each kind of observer. #[derive(Default, Debug)] pub struct CachedObservers { // Observers listening for any time this trigger is fired @@ -376,7 +519,13 @@ pub struct CachedObservers { entity_observers: EntityHashMap, } -/// Metadata for observers. Stores a cache mapping trigger ids to the registered observers. +/// An internal lookup table tracking all of the observers in the world. +/// +/// Stores a cache mapping trigger ids to the registered observers. +/// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field, +/// saving lookups for the most common triggers. +/// +/// This is stored as a field of the [`World`]. #[derive(Default, Debug)] pub struct Observers { // Cached ECS observers to save a lookup most common triggers. @@ -385,12 +534,14 @@ pub struct Observers { on_replace: CachedObservers, on_remove: CachedObservers, on_despawn: CachedObservers, - // Map from trigger type to set of observers + // Map from trigger type to set of observers listening to that trigger cache: HashMap, } impl Observers { pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers { + use crate::lifecycle::*; + match event_type { ON_ADD => &mut self.on_add, ON_INSERT => &mut self.on_insert, @@ -402,6 +553,8 @@ impl Observers { } pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + use crate::lifecycle::*; + match event_type { ON_ADD => Some(&self.on_add), ON_INSERT => Some(&self.on_insert), @@ -416,7 +569,7 @@ impl Observers { pub(crate) fn invoke( mut world: DeferredWorld, event_type: ComponentId, - target: Entity, + target: Option, components: impl Iterator + Clone, data: &mut T, propagate: &mut bool, @@ -455,8 +608,8 @@ impl Observers { observers.map.iter().for_each(&mut trigger_observer); // Trigger entity observers listening for this kind of trigger - if target != Entity::PLACEHOLDER { - if let Some(map) = observers.entity_observers.get(&target) { + if let Some(target_entity) = target { + if let Some(map) = observers.entity_observers.get(&target_entity) { map.iter().for_each(&mut trigger_observer); } } @@ -469,8 +622,8 @@ impl Observers { .iter() .for_each(&mut trigger_observer); - if target != Entity::PLACEHOLDER { - if let Some(map) = component_observers.entity_map.get(&target) { + if let Some(target_entity) = target { + if let Some(map) = component_observers.entity_map.get(&target_entity) { map.iter().for_each(&mut trigger_observer); } } @@ -479,6 +632,8 @@ impl Observers { } pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { + use crate::lifecycle::*; + match event_type { ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), @@ -695,7 +850,7 @@ impl World { unsafe { world.trigger_observers_with_data::<_, E::Traversal>( event_id, - Entity::PLACEHOLDER, + None, targets.components(), event_data, false, @@ -708,7 +863,7 @@ impl World { unsafe { world.trigger_observers_with_data::<_, E::Traversal>( event_id, - target_entity, + Some(target_entity), targets.components(), event_data, E::AUTO_PROPAGATE, @@ -999,20 +1154,20 @@ mod tests { world.add_observer( |obs: Trigger, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); - commands.entity(obs.target()).insert(B); + commands.entity(obs.target().unwrap()).insert(B); }, ); world.add_observer( |obs: Trigger, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); - commands.entity(obs.target()).remove::(); + commands.entity(obs.target().unwrap()).remove::(); }, ); world.add_observer( |obs: Trigger, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); - commands.entity(obs.target()).remove::(); + commands.entity(obs.target().unwrap()).remove::(); }, ); world.add_observer(|_: Trigger, mut res: ResMut| { @@ -1181,7 +1336,7 @@ mod tests { }; world.spawn_empty().observe(system); world.add_observer(move |obs: Trigger, mut res: ResMut| { - assert_eq!(obs.target(), Entity::PLACEHOLDER); + assert_eq!(obs.target(), None); res.observed("event_a"); }); @@ -1208,7 +1363,7 @@ mod tests { .observe(|_: Trigger, mut res: ResMut| res.observed("a_1")) .id(); world.add_observer(move |obs: Trigger, mut res: ResMut| { - assert_eq!(obs.target(), entity); + assert_eq!(obs.target().unwrap(), entity); res.observed("a_2"); }); @@ -1628,7 +1783,7 @@ mod tests { world.add_observer( |trigger: Trigger, query: Query<&A>, mut res: ResMut| { - if query.get(trigger.target()).is_ok() { + if query.get(trigger.target().unwrap()).is_ok() { res.observed("event"); } }, @@ -1651,7 +1806,7 @@ mod tests { fn observer_modifies_relationship() { fn on_add(trigger: Trigger, mut commands: Commands) { commands - .entity(trigger.target()) + .entity(trigger.target().unwrap()) .with_related_entities::(|rsc| { rsc.spawn_empty(); }); diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 520147d438..61cc0973f3 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -2,8 +2,9 @@ use alloc::{boxed::Box, vec}; use core::any::Any; use crate::{ - component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType}, + component::{ComponentId, Mutable, StorageType}, error::{ErrorContext, ErrorHandler}, + lifecycle::{ComponentHook, HookContext}, observer::{ObserverDescriptor, ObserverTrigger}, prelude::*, query::DebugCheckedUnwrap, @@ -123,8 +124,8 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// struct Explode; /// /// 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(); @@ -157,7 +158,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # struct Explode; /// world.entity_mut(e1).observe(|trigger: Trigger, mut commands: Commands| { /// println!("Boom!"); -/// commands.entity(trigger.target()).despawn(); +/// commands.entity(trigger.target().unwrap()).despawn(); /// }); /// /// world.entity_mut(e2).observe(|trigger: Trigger, mut commands: Commands| { @@ -371,6 +372,11 @@ fn observer_system_runner>( // and is never exclusive // - system is the same type erased system from above unsafe { + // Always refresh hotpatch pointers + // There's no guarantee that the `HotPatched` event would still be there once the observer is triggered. + #[cfg(feature = "hotpatching")] + (*system).refresh_hotpatch(); + match (*system).validate_param_unsafe(world) { Ok(()) => { if let Err(err) = (*system).run_unsafe(trigger, world) { @@ -405,7 +411,7 @@ fn observer_system_runner>( } } -/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::component::ComponentHooks::on_add`). +/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`). /// /// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters /// erased. diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 68c059fc45..68bdf40b5f 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -47,6 +47,8 @@ use variadics_please::all_tuples; /// - **[`Ref`].** /// Similar to change detection filters but it is used as a query fetch parameter. /// It exposes methods to check for changes to the wrapped component. +/// - **[`Mut`].** +/// Mutable component access, with change detection data. /// - **[`Has`].** /// Returns a bool indicating whether the entity has the specified component. /// diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 3522118fbc..00334cb6d0 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -11,10 +11,10 @@ pub use relationship_query::*; pub use relationship_source_collection::*; use crate::{ - component::{Component, HookContext, Mutable}, + component::{Component, Mutable}, entity::{ComponentCloneCtx, Entity, SourceComponent}, error::{ignore, CommandWithEntity, HandleError}, - system::entity_command::{self}, + lifecycle::HookContext, world::{DeferredWorld, EntityWorldMut}, }; use log::warn; @@ -223,50 +223,24 @@ pub trait RelationshipTarget: Component + Sized { /// The `on_replace` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection. // note: think of this as "on_drop" - fn on_replace(mut world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { + fn on_replace(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { let (entities, mut commands) = world.entities_and_commands(); let relationship_target = entities.get(entity).unwrap().get::().unwrap(); for source_entity in relationship_target.iter() { - if entities.get(source_entity).is_ok() { - commands.queue( - entity_command::remove::() - .with_entity(source_entity) - .handle_error_with(ignore), - ); - } else { - warn!( - "{}Tried to despawn non-existent entity {}", - caller - .map(|location| format!("{location}: ")) - .unwrap_or_default(), - source_entity - ); - } + commands + .entity(source_entity) + .remove::(); } } /// The `on_despawn` component hook that despawns entities stored in an entity's [`RelationshipTarget`] when /// that entity is despawned. // note: think of this as "on_drop" - fn on_despawn(mut world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { + fn on_despawn(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { let (entities, mut commands) = world.entities_and_commands(); let relationship_target = entities.get(entity).unwrap().get::().unwrap(); for source_entity in relationship_target.iter() { - if entities.get(source_entity).is_ok() { - commands.queue( - entity_command::despawn() - .with_entity(source_entity) - .handle_error_with(ignore), - ); - } else { - warn!( - "{}Tried to despawn non-existent entity {}", - caller - .map(|location| format!("{location}: ")) - .unwrap_or_default(), - source_entity - ); - } + commands.entity(source_entity).despawn(); } } diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 5a23214463..fc6d1f1183 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -1,6 +1,7 @@ use crate::{ bundle::Bundle, entity::{hash_set::EntityHashSet, Entity}, + prelude::Children, relationship::{ Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget, }, @@ -302,6 +303,15 @@ impl<'w> EntityWorldMut<'w> { self } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityWorldMut::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::(); + self + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// @@ -467,6 +477,14 @@ impl<'a> EntityCommands<'a> { }) } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityCommands::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::() + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs deleted file mode 100644 index 64cc63a7ce..0000000000 --- a/crates/bevy_ecs/src/removal_detection.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Alerting events when a component is removed from an entity. - -use crate::{ - component::{Component, ComponentId, ComponentIdFor, Tick}, - entity::Entity, - event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, - prelude::Local, - storage::SparseSet, - system::{ReadOnlySystemParam, SystemMeta, SystemParam}, - world::{unsafe_world_cell::UnsafeWorldCell, World}, -}; - -use derive_more::derive::Into; - -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; -use core::{ - fmt::Debug, - iter, - marker::PhantomData, - ops::{Deref, DerefMut}, - option, -}; - -/// Wrapper around [`Entity`] for [`RemovedComponents`]. -/// Internally, `RemovedComponents` uses these as an `Events`. -#[derive(Event, Debug, Clone, Into)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] -pub struct RemovedComponentEntity(Entity); - -/// Wrapper around a [`EventCursor`] so that we -/// can differentiate events between components. -#[derive(Debug)] -pub struct RemovedComponentReader -where - T: Component, -{ - reader: EventCursor, - marker: PhantomData, -} - -impl Default for RemovedComponentReader { - fn default() -> Self { - Self { - reader: Default::default(), - marker: PhantomData, - } - } -} - -impl Deref for RemovedComponentReader { - type Target = EventCursor; - fn deref(&self) -> &Self::Target { - &self.reader - } -} - -impl DerefMut for RemovedComponentReader { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.reader - } -} - -/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. -#[derive(Default, Debug)] -pub struct RemovedComponentEvents { - event_sets: SparseSet>, -} - -impl RemovedComponentEvents { - /// Creates an empty storage buffer for component removal events. - pub fn new() -> Self { - Self::default() - } - - /// For each type of component, swaps the event buffers and clears the oldest event buffer. - /// In general, this should be called once per frame/update. - pub fn update(&mut self) { - for (_component_id, events) in self.event_sets.iter_mut() { - events.update(); - } - } - - /// Returns an iterator over components and their entity events. - pub fn iter(&self) -> impl Iterator)> { - self.event_sets.iter() - } - - /// Gets the event storage for a given component. - pub fn get( - &self, - component_id: impl Into, - ) -> Option<&Events> { - self.event_sets.get(component_id.into()) - } - - /// Sends a removal event for the specified component. - pub fn send(&mut self, component_id: impl Into, entity: Entity) { - self.event_sets - .get_or_insert_with(component_id.into(), Default::default) - .send(RemovedComponentEntity(entity)); - } -} - -/// A [`SystemParam`] that yields entities that had their `T` [`Component`] -/// removed or have been despawned with it. -/// -/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). -/// -/// Note that this does not allow you to see which data existed before removal. -/// If you need this, you will need to track the component data value on your own, -/// using a regularly scheduled system that requests `Query<(Entity, &T), Changed>` -/// and stores the data somewhere safe to later cross-reference. -/// -/// If you are using `bevy_ecs` as a standalone crate, -/// note that the `RemovedComponents` list will not be automatically cleared for you, -/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). -/// -/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is -/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. -/// For the main world, this is delayed until after all `SubApp`s have run. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # use bevy_ecs::component::Component; -/// # use bevy_ecs::system::IntoSystem; -/// # use bevy_ecs::removal_detection::RemovedComponents; -/// # -/// # #[derive(Component)] -/// # struct MyComponent; -/// fn react_on_removal(mut removed: RemovedComponents) { -/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); -/// } -/// # bevy_ecs::system::assert_is_system(react_on_removal); -/// ``` -#[derive(SystemParam)] -pub struct RemovedComponents<'w, 's, T: Component> { - component_id: ComponentIdFor<'s, T>, - reader: Local<'s, RemovedComponentReader>, - event_sets: &'w RemovedComponentEvents, -} - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIter<'a> = iter::Map< - iter::Flatten>>>, - fn(RemovedComponentEntity) -> Entity, ->; - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIterWithId<'a> = iter::Map< - iter::Flatten>>, - fn( - (&RemovedComponentEntity, EventId), - ) -> (Entity, EventId), ->; - -fn map_id_events( - (entity, id): (&RemovedComponentEntity, EventId), -) -> (Entity, EventId) { - (entity.clone().into(), id) -} - -// For all practical purposes, the api surface of `RemovedComponents` -// should be similar to `EventReader` to reduce confusion. -impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { - /// Fetch underlying [`EventCursor`]. - pub fn reader(&self) -> &EventCursor { - &self.reader - } - - /// Fetch underlying [`EventCursor`] mutably. - pub fn reader_mut(&mut self) -> &mut EventCursor { - &mut self.reader - } - - /// Fetch underlying [`Events`]. - pub fn events(&self) -> Option<&Events> { - self.event_sets.get(self.component_id.get()) - } - - /// Destructures to get a mutable reference to the `EventCursor` - /// and a reference to `Events`. - /// - /// This is necessary since Rust can't detect destructuring through methods and most - /// usecases of the reader uses the `Events` as well. - pub fn reader_mut_with_events( - &mut self, - ) -> Option<( - &mut RemovedComponentReader, - &Events, - )> { - self.event_sets - .get(self.component_id.get()) - .map(|events| (&mut *self.reader, events)) - } - - /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the - /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events - /// that happened before now. - pub fn read(&mut self) -> RemovedIter<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read(events).cloned()) - .into_iter() - .flatten() - .map(RemovedComponentEntity::into) - } - - /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. - pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read_with_id(events)) - .into_iter() - .flatten() - .map(map_id_events) - } - - /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. - pub fn len(&self) -> usize { - self.events() - .map(|events| self.reader.len(events)) - .unwrap_or(0) - } - - /// Returns `true` if there are no events available to read. - pub fn is_empty(&self) -> bool { - self.events() - .is_none_or(|events| self.reader.is_empty(events)) - } - - /// Consumes all available events. - /// - /// This means these events will not appear in calls to [`RemovedComponents::read()`] or - /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. - pub fn clear(&mut self) { - if let Some((reader, events)) = self.reader_mut_with_events() { - reader.clear(events); - } - } -} - -// SAFETY: Only reads World removed component events -unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} - -// SAFETY: no component value access. -unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { - type State = (); - type Item<'w, 's> = &'w RemovedComponentEvents; - - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} - - #[inline] - unsafe fn get_param<'w, 's>( - _state: &'s mut Self::State, - _system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - _change_tick: Tick, - ) -> Self::Item<'w, 's> { - world.removed_components() - } -} diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 2b31ad50c7..45eb4febb2 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -11,8 +11,18 @@ pub type BoxedCondition = Box>; /// A system that determines if one or more scheduled systems should run. /// -/// Implemented for functions and closures that convert into [`System`](System) -/// with [read-only](crate::system::ReadOnlySystemParam) parameters. +/// `SystemCondition` is sealed and implemented for functions and closures with +/// [read-only](crate::system::ReadOnlySystemParam) parameters that convert into +/// [`System`](System), [`System>`](System) or +/// [`System>`](System). +/// +/// `SystemCondition` offers a private method +/// (called by [`run_if`](crate::schedule::IntoScheduleConfigs::run_if) and the provided methods) +/// that converts the implementing system into a condition (system) returning a bool. +/// Depending on the output type of the implementing system: +/// - `bool`: the implementing system is used as the condition; +/// - `Result<(), BevyError>`: the condition returns `true` if and only if the implementing system returns `Ok(())`; +/// - `Result`: the condition returns `true` if and only if the implementing system returns `Ok(true)`. /// /// # Marker type parameter /// @@ -31,7 +41,7 @@ pub type BoxedCondition = Box>; /// ``` /// /// # Examples -/// A condition that returns true every other time it's called. +/// A condition that returns `true` every other time it's called. /// ``` /// # use bevy_ecs::prelude::*; /// fn every_other_time() -> impl SystemCondition<()> { @@ -54,7 +64,7 @@ pub type BoxedCondition = Box>; /// # assert!(!world.resource::().0); /// ``` /// -/// A condition that takes a bool as an input and returns it unchanged. +/// A condition that takes a `bool` as an input and returns it unchanged. /// /// ``` /// # use bevy_ecs::prelude::*; @@ -71,8 +81,30 @@ pub type BoxedCondition = Box>; /// # world.insert_resource(DidRun(false)); /// # app.run(&mut world); /// # assert!(world.resource::().0); -pub trait SystemCondition: - sealed::SystemCondition +/// ``` +/// +/// A condition returning a `Result<(), BevyError>` +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] struct Player; +/// fn player_exists(q_player: Query<(), With>) -> Result { +/// Ok(q_player.single()?) +/// } +/// +/// # let mut app = Schedule::default(); +/// # #[derive(Resource)] struct DidRun(bool); +/// # fn my_system(mut did_run: ResMut) { did_run.0 = true; } +/// app.add_systems(my_system.run_if(player_exists)); +/// # let mut world = World::new(); +/// # world.insert_resource(DidRun(false)); +/// # app.run(&mut world); +/// # assert!(!world.resource::().0); +/// # world.spawn(Player); +/// # app.run(&mut world); +/// # assert!(world.resource::().0); +pub trait SystemCondition: + sealed::SystemCondition { /// Returns a new run condition that only returns `true` /// if both this one and the passed `and` return `true`. @@ -371,28 +403,61 @@ pub trait SystemCondition: } } -impl SystemCondition for F where - F: sealed::SystemCondition +impl SystemCondition for F where + F: sealed::SystemCondition { } mod sealed { - use crate::system::{IntoSystem, ReadOnlySystem, SystemInput}; + use crate::{ + error::BevyError, + system::{IntoSystem, ReadOnlySystem, SystemInput}, + }; - pub trait SystemCondition: - IntoSystem + pub trait SystemCondition: + IntoSystem { // This associated type is necessary to let the compiler // know that `Self::System` is `ReadOnlySystem`. - type ReadOnlySystem: ReadOnlySystem; + type ReadOnlySystem: ReadOnlySystem; + + fn into_condition_system(self) -> impl ReadOnlySystem; } - impl SystemCondition for F + impl SystemCondition for F where F: IntoSystem, F::System: ReadOnlySystem, { type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self) + } + } + + impl SystemCondition> for F + where + F: IntoSystem, Marker>, + F::System: ReadOnlySystem, + { + type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self.map(|result| result.is_ok())) + } + } + + impl SystemCondition> for F + where + F: IntoSystem, Marker>, + F::System: ReadOnlySystem, + { + type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self.map(|result| matches!(result, Ok(true)))) + } } } @@ -402,9 +467,9 @@ pub mod common_conditions { use crate::{ change_detection::DetectChanges, event::{Event, EventReader}, + lifecycle::RemovedComponents, prelude::{Component, Query, With}, query::QueryFilter, - removal_detection::RemovedComponents, resource::Resource, system::{In, IntoSystem, Local, Res, System, SystemInput}, }; diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index f1a48e432b..4826d0a66d 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -14,8 +14,8 @@ use crate::{ system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, ScheduleSystem, System}, }; -fn new_condition(condition: impl SystemCondition) -> BoxedCondition { - let condition_system = IntoSystem::into_system(condition); +fn new_condition(condition: impl SystemCondition) -> BoxedCondition { + let condition_system = condition.into_condition_system(); assert!( condition_system.is_send(), "SystemCondition `{}` accesses `NonSend` resources. This is not currently supported.", @@ -447,7 +447,7 @@ pub trait IntoScheduleConfigs(self, condition: impl SystemCondition) -> ScheduleConfigs { + fn run_if(self, condition: impl SystemCondition) -> ScheduleConfigs { self.into_configs().run_if(condition) } @@ -535,7 +535,7 @@ impl> IntoScheduleCo self } - fn run_if(mut self, condition: impl SystemCondition) -> ScheduleConfigs { + fn run_if(mut self, condition: impl SystemCondition) -> ScheduleConfigs { self.run_if_dyn(new_condition(condition)); self } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index ef7c639038..38b85c1ca5 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -18,9 +18,9 @@ use crate::{ component::{ComponentId, Tick}, error::{BevyError, ErrorContext, Result}, prelude::{IntoSystemSet, SystemSet}, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, - system::{ScheduleSystem, System, SystemIn, SystemParamValidationError}, + system::{ScheduleSystem, System, SystemIn, SystemParamValidationError, SystemStateFlags}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; @@ -162,35 +162,14 @@ impl System for ApplyDeferred { Cow::Borrowed("bevy_ecs::apply_deferred") } - fn component_access(&self) -> &Access { - // This system accesses no components. - const { &Access::new() } - } - fn component_access_set(&self) -> &FilteredAccessSet { + // This system accesses no components. const { &FilteredAccessSet::new() } } - fn is_send(&self) -> bool { - // Although this system itself does nothing on its own, the system - // executor uses it to apply deferred commands. Commands must be allowed - // to access non-send resources, so this system must be non-send for - // scheduling purposes. - false - } - - fn is_exclusive(&self) -> bool { - // This system is labeled exclusive because it is used by the system - // executor to find places where deferred commands should be applied, - // and commands can only be applied with exclusive access to the world. - true - } - - fn has_deferred(&self) -> bool { - // This system itself doesn't have any commands to apply, but when it - // is pulled from the schedule to be ran, the executor will apply - // deferred commands from other systems. - false + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } unsafe fn run_unsafe( @@ -203,6 +182,10 @@ impl System for ApplyDeferred { Ok(()) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) {} + fn run(&mut self, _input: SystemIn<'_, Self>, _world: &mut World) -> Self::Out { // This system does nothing on its own. The executor will apply deferred // commands from other systems instead of running this system. diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index e5fd94d2dd..62a10298c9 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -19,6 +19,8 @@ use crate::{ system::ScheduleSystem, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; @@ -443,6 +445,14 @@ impl ExecutorState { return; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !context + .environment + .world_cell + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + // can't borrow since loop mutably borrows `self` let mut ready_systems = core::mem::take(&mut self.ready_systems_copy); @@ -460,6 +470,11 @@ impl ExecutorState { // Therefore, no other reference to this system exists and there is no aliasing. let system = unsafe { &mut *context.environment.systems[system_index].get() }; + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + if !self.can_run(system_index, conditions) { // NOTE: exclusive systems with ambiguities are susceptible to // being significantly displaced here (compared to single-threaded order) diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 584c5a1073..d9069aa6e8 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -16,6 +16,8 @@ use crate::{ }, world::World, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; @@ -60,6 +62,12 @@ impl SystemExecutor for SimpleExecutor { self.completed_systems |= skipped_systems; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] let name = schedule.systems[system_index].name(); @@ -120,6 +128,11 @@ impl SystemExecutor for SimpleExecutor { #[cfg(feature = "trace")] should_run_span.exit(); + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + // system has either been skipped or will run self.completed_systems.insert(system_index); @@ -186,6 +199,12 @@ fn evaluate_and_fold_conditions( world: &mut World, error_handler: ErrorHandler, ) -> bool { + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + #[expect( clippy::unnecessary_fold, reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." @@ -208,6 +227,10 @@ fn evaluate_and_fold_conditions( return false; } } + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + condition.refresh_hotpatch(); + } __rust_begin_short_backtrace::readonly_run(&mut **condition, world) }) .fold(true, |acc, res| acc && res) diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 0076103637..68af623b40 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -12,6 +12,8 @@ use crate::{ schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, world::World, }; +#[cfg(feature = "hotpatching")] +use crate::{event::Events, HotPatched}; use super::__rust_begin_short_backtrace; @@ -60,6 +62,12 @@ impl SystemExecutor for SingleThreadedExecutor { self.completed_systems |= skipped_systems; } + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] let name = schedule.systems[system_index].name(); @@ -121,6 +129,11 @@ impl SystemExecutor for SingleThreadedExecutor { #[cfg(feature = "trace")] should_run_span.exit(); + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + system.refresh_hotpatch(); + } + // system has either been skipped or will run self.completed_systems.insert(system_index); @@ -204,6 +217,12 @@ fn evaluate_and_fold_conditions( world: &mut World, error_handler: ErrorHandler, ) -> bool { + #[cfg(feature = "hotpatching")] + let should_update_hotpatch = !world + .get_resource::>() + .map(Events::is_empty) + .unwrap_or(true); + #[expect( clippy::unnecessary_fold, reason = "Short-circuiting here would prevent conditions from mutating their own state as needed." @@ -226,6 +245,10 @@ fn evaluate_and_fold_conditions( return false; } } + #[cfg(feature = "hotpatching")] + if should_update_hotpatch { + condition.refresh_hotpatch(); + } __rust_begin_short_backtrace::readonly_run(&mut **condition, world) }) .fold(true, |acc, res| acc && res) diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 81912d2f72..8c5aa1d6fb 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -29,6 +29,7 @@ mod tests { use alloc::{string::ToString, vec, vec::Vec}; use core::sync::atomic::{AtomicU32, Ordering}; + use crate::error::BevyError; pub use crate::{ prelude::World, resource::Resource, @@ -49,10 +50,10 @@ mod tests { struct SystemOrder(Vec); #[derive(Resource, Default)] - struct RunConditionBool(pub bool); + struct RunConditionBool(bool); #[derive(Resource, Default)] - struct Counter(pub AtomicU32); + struct Counter(AtomicU32); fn make_exclusive_system(tag: u32) -> impl FnMut(&mut World) { move |world| world.resource_mut::().0.push(tag) @@ -252,12 +253,13 @@ mod tests { } mod conditions { + use crate::change_detection::DetectChanges; use super::*; #[test] - fn system_with_condition() { + fn system_with_condition_bool() { let mut world = World::default(); let mut schedule = Schedule::default(); @@ -276,6 +278,47 @@ mod tests { assert_eq!(world.resource::().0, vec![0]); } + #[test] + fn system_with_condition_result_unit() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems( + make_function_system(0).run_if(|| Err::<(), BevyError>(core::fmt::Error.into())), + ); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(1).run_if(|| Ok(()))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![1]); + } + + #[test] + fn system_with_condition_result_bool() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems(( + make_function_system(0).run_if(|| Err::(core::fmt::Error.into())), + make_function_system(1).run_if(|| Ok(false)), + )); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(2).run_if(|| Ok(true))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![2]); + } + #[test] fn systems_with_distributive_condition() { let mut world = World::default(); @@ -874,7 +917,6 @@ mod tests { } #[test] - #[ignore = "Known failing but fix is non-trivial: https://github.com/bevyengine/bevy/issues/4381"] fn filtered_components() { let mut world = World::new(); world.spawn(A); diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 144ce6516c..c4e61356d0 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -166,7 +166,7 @@ impl Schedules { writeln!(message, "{}", components.get_name(*id).unwrap()).unwrap(); } - info!("{}", message); + info!("{message}"); } /// Adds one or more systems to the [`Schedule`] matching the provided [`ScheduleLabel`]. @@ -558,7 +558,7 @@ impl Schedule { /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub fn check_change_ticks(&mut self, change_tick: Tick) { for system in &mut self.executable.systems { if !is_apply_deferred(system) { system.check_change_tick(change_tick); @@ -1418,26 +1418,24 @@ impl ScheduleGraph { if system_a.is_exclusive() || system_b.is_exclusive() { conflicting_systems.push((a, b, Vec::new())); } else { - let access_a = system_a.component_access(); - let access_b = system_b.component_access(); - if !access_a.is_compatible(access_b) { - match access_a.get_conflicts(access_b) { - AccessConflicts::Individual(conflicts) => { - let conflicts: Vec<_> = conflicts - .ones() - .map(ComponentId::get_sparse_set_index) - .filter(|id| !ignored_ambiguities.contains(id)) - .collect(); - if !conflicts.is_empty() { - conflicting_systems.push((a, b, conflicts)); - } - } - AccessConflicts::All => { - // there is no specific component conflicting, but the systems are overall incompatible - // for example 2 systems with `Query` - conflicting_systems.push((a, b, Vec::new())); + let access_a = system_a.component_access_set(); + let access_b = system_b.component_access_set(); + match access_a.get_conflicts(access_b) { + AccessConflicts::Individual(conflicts) => { + let conflicts: Vec<_> = conflicts + .ones() + .map(ComponentId::get_sparse_set_index) + .filter(|id| !ignored_ambiguities.contains(id)) + .collect(); + if !conflicts.is_empty() { + conflicting_systems.push((a, b, conflicts)); } } + AccessConflicts::All => { + // there is no specific component conflicting, but the systems are overall incompatible + // for example 2 systems with `Query` + conflicting_systems.push((a, b, Vec::new())); + } } } } @@ -1707,10 +1705,7 @@ impl ScheduleGraph { match self.settings.hierarchy_detection { LogLevel::Ignore => unreachable!(), LogLevel::Warn => { - error!( - "Schedule {schedule_label:?} has redundant edges:\n {}", - message - ); + error!("Schedule {schedule_label:?} has redundant edges:\n {message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::HierarchyRedundancy(message)), @@ -1912,7 +1907,7 @@ impl ScheduleGraph { match self.settings.ambiguity_detection { LogLevel::Ignore => Ok(()), LogLevel::Warn => { - warn!("Schedule {schedule_label:?} has ambiguities.\n{}", message); + warn!("Schedule {schedule_label:?} has ambiguities.\n{message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::Ambiguity(message)), diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 4974be5d43..2243e5019f 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -60,7 +60,93 @@ define_label!( ); define_label!( - /// Types that identify logical groups of systems. + /// System sets are tag-like labels that can be used to group systems together. + /// + /// This allows you to share configuration (like run conditions) across multiple systems, + /// and order systems or system sets relative to conceptual groups of systems. + /// To control the behavior of a system set as a whole, use [`Schedule::configure_sets`](crate::prelude::Schedule::configure_sets), + /// or the method of the same name on `App`. + /// + /// Systems can belong to any number of system sets, reflecting multiple roles or facets that they might have. + /// For example, you may want to annotate a system as "consumes input" and "applies forces", + /// and ensure that your systems are ordered correctly for both of those sets. + /// + /// System sets can belong to any number of other system sets, + /// allowing you to create nested hierarchies of system sets to group systems together. + /// Configuration applied to system sets will flow down to their members (including other system sets), + /// allowing you to set and modify the configuration in a single place. + /// + /// Systems sets are also useful for exposing a consistent public API for dependencies + /// to hook into across versions of your crate, + /// allowing them to add systems to a specific set, or order relative to that set, + /// without leaking implementation details of the exact systems involved. + /// + /// ## Defining new system sets + /// + /// To create a new system set, use the `#[derive(SystemSet)]` macro. + /// Unit structs are a good choice for one-off sets. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// struct PhysicsSystems; + /// ``` + /// + /// When you want to define several related system sets, + /// consider creating an enum system set. + /// Each variant will be treated as a separate system set. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// ``` + /// + /// By convention, the listed order of the system set in the enum + /// corresponds to the order in which the systems are run. + /// Ordering must be explicitly added to ensure that this is the case, + /// but following this convention will help avoid confusion. + /// + /// ### Adding systems to system sets + /// + /// To add systems to a system set, call [`in_set`](crate::prelude::IntoScheduleConfigs::in_set) on the system function + /// while adding it to your app or schedule. + /// + /// Like usual, these methods can be chained with other configuration methods like [`before`](crate::prelude::IntoScheduleConfigs::before), + /// or repeated to add systems to multiple sets. + /// + /// ```rust + /// use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// + /// fn target_selection() {} + /// + /// fn enemy_damage_calculation() {} + /// + /// fn player_damage_calculation() {} + /// + /// let mut schedule = Schedule::default(); + /// // Configuring the sets to run in order. + /// schedule.configure_sets((CombatSystems::TargetSelection, CombatSystems::DamageCalculation, CombatSystems::Cleanup).chain()); + /// + /// // Adding a single system to a set. + /// schedule.add_systems(target_selection.in_set(CombatSystems::TargetSelection)); + /// + /// // Adding multiple systems to a set. + /// schedule.add_systems((player_damage_calculation, enemy_damage_calculation).in_set(CombatSystems::DamageCalculation)); + /// ``` #[diagnostic::on_unimplemented( note = "consider annotating `{Self}` with `#[derive(SystemSet)]`" )] diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs index 222dfdfcaf..b6de7c8215 100644 --- a/crates/bevy_ecs/src/schedule/stepping.rs +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -475,9 +475,8 @@ impl Stepping { Some(state) => state.clear_behaviors(), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } }, @@ -486,9 +485,8 @@ impl Stepping { Some(state) => state.set_behavior(system, behavior), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } @@ -498,9 +496,8 @@ impl Stepping { Some(state) => state.clear_behavior(system), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 50dbfad7ea..6caa002deb 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -127,26 +127,15 @@ where self.name.clone() } - fn component_access(&self) -> &crate::query::Access { - self.system.component_access() - } - fn component_access_set( &self, ) -> &crate::query::FilteredAccessSet { self.system.component_access_set() } - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.system.flags() } #[inline] @@ -161,6 +150,12 @@ where }) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut crate::prelude::World) { self.system.apply_deferred(world); diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index f9c96c6284..6536c9cc1e 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -604,7 +604,7 @@ unsafe impl<'w, 's, T: FnOnce(&mut FilteredResourcesBuilder)> if !conflicts.is_empty() { let accesses = conflicts.format_conflict_list(world); let system_name = &meta.name; - panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002"); + panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); } if access.has_read_all_resources() { @@ -663,7 +663,7 @@ unsafe impl<'w, 's, T: FnOnce(&mut FilteredResourcesMutBuilder)> if !conflicts.is_empty() { let accesses = conflicts.format_conflict_list(world); let system_name = &meta.name; - panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002"); + panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); } if access.has_read_all_resources() { diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 0faade39ee..95fea44985 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -4,7 +4,7 @@ use core::marker::PhantomData; use crate::{ component::{ComponentId, Tick}, prelude::World, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn, SystemParamValidationError}, world::unsafe_world_cell::UnsafeWorldCell, @@ -144,24 +144,13 @@ where self.name.clone() } - fn component_access(&self) -> &Access { - self.component_access_set.combined_access() - } - fn component_access_set(&self) -> &FilteredAccessSet { &self.component_access_set } - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( @@ -182,6 +171,13 @@ where ) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.a.refresh_hotpatch(); + self.b.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.a.apply_deferred(world); @@ -363,24 +359,13 @@ where self.name.clone() } - fn component_access(&self) -> &Access { - self.component_access_set.combined_access() - } - fn component_access_set(&self) -> &FilteredAccessSet { &self.component_access_set } - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( @@ -392,6 +377,13 @@ where self.b.run_unsafe(value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.a.refresh_hotpatch(); + self.b.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.a.apply_deferred(world); self.b.apply_deferred(world); diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 8277fcb0f9..1cbdb5b07d 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,6 +1,6 @@ use crate::{ component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, IntoSystem, @@ -13,7 +13,7 @@ use alloc::{borrow::Cow, vec, vec::Vec}; use core::marker::PhantomData; use variadics_please::all_tuples; -use super::SystemParamValidationError; +use super::{SystemParamValidationError, SystemStateFlags}; /// A function system that runs with exclusive [`World`] access. /// @@ -26,6 +26,8 @@ where F: ExclusiveSystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, param_state: Option<::State>, system_meta: SystemMeta, // NOTE: PhantomData T> gives this safe Send/Sync impls @@ -58,6 +60,11 @@ where fn into_system(func: Self) -> Self::System { ExclusiveFunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current( + >::run, + ) + .ptr_address(), param_state: None, system_meta: SystemMeta::new::(), marker: PhantomData, @@ -80,33 +87,18 @@ where self.system_meta.name.clone() } - #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - #[inline] fn component_access_set(&self) -> &FilteredAccessSet { &self.system_meta.component_access_set } #[inline] - fn is_send(&self) -> bool { - // exclusive systems should have access to non-send resources + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred // the executor runs exclusive systems on the main thread, so this // field reflects that constraint - false - } - - #[inline] - fn is_exclusive(&self) -> bool { - true - } - - #[inline] - fn has_deferred(&self) -> bool { // exclusive systems have no deferred system params - false + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } #[inline] @@ -125,6 +117,20 @@ where self.param_state.as_mut().expect(PARAM_MESSAGE), &self.system_meta, ); + + #[cfg(feature = "hotpatching")] + let out = { + let mut hot_fn = + subsecond::HotFn::current(>::run); + // SAFETY: + // - pointer used to call is from the current jump table + unsafe { + hot_fn + .try_call_with_ptr(self.current_ptr, (&mut self.func, world, input, params)) + .expect("Error calling hotpatched system. Run a full rebuild") + } + }; + #[cfg(not(feature = "hotpatching"))] let out = self.func.run(world, input, params); world.flush(); @@ -134,6 +140,17 @@ where }) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + let new = subsecond::HotFn::current(>::run) + .ptr_address(); + if new != self.current_ptr { + log::debug!("system {} hotpatched", self.name()); + } + self.current_ptr = new; + } + #[inline] fn apply_deferred(&mut self, _world: &mut World) { // "pure" exclusive systems do not have any buffers to apply. diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 49bcf1cc78..7ad7f27e2b 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -1,7 +1,7 @@ use crate::{ component::{ComponentId, Tick}, prelude::FromWorld, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ReadOnlySystemParam, System, SystemIn, SystemInput, SystemParam, @@ -17,7 +17,9 @@ use variadics_please::all_tuples; #[cfg(feature = "trace")] use tracing::{info_span, Span}; -use super::{IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError}; +use super::{ + IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError, SystemStateFlags, +}; /// The metadata of a [`System`]. #[derive(Clone)] @@ -29,8 +31,7 @@ pub struct SystemMeta { pub(crate) component_access_set: FilteredAccessSet, // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // SystemParams from overriding each other - is_send: bool, - has_deferred: bool, + flags: SystemStateFlags, pub(crate) last_run: Tick, #[cfg(feature = "trace")] pub(crate) system_span: Span, @@ -44,8 +45,7 @@ impl SystemMeta { Self { name: name.into(), component_access_set: FilteredAccessSet::default(), - is_send: true, - has_deferred: false, + flags: SystemStateFlags::empty(), last_run: Tick::new(0), #[cfg(feature = "trace")] system_span: info_span!("system", name = name), @@ -78,7 +78,7 @@ impl SystemMeta { /// Returns true if the system is [`Send`]. #[inline] pub fn is_send(&self) -> bool { - self.is_send + !self.flags.intersects(SystemStateFlags::NON_SEND) } /// Sets the system to be not [`Send`]. @@ -86,20 +86,20 @@ impl SystemMeta { /// This is irreversible. #[inline] pub fn set_non_send(&mut self) { - self.is_send = false; + self.flags |= SystemStateFlags::NON_SEND; } /// Returns true if the system has deferred [`SystemParam`]'s #[inline] pub fn has_deferred(&self) -> bool { - self.has_deferred + self.flags.intersects(SystemStateFlags::DEFERRED) } /// Marks the system as having deferred buffers like [`Commands`](`super::Commands`) /// This lets the scheduler insert [`ApplyDeferred`](`crate::prelude::ApplyDeferred`) systems automatically. #[inline] pub fn set_has_deferred(&mut self) { - self.has_deferred = true; + self.flags |= SystemStateFlags::DEFERRED; } /// Returns a reference to the [`FilteredAccessSet`] for [`ComponentId`]. @@ -306,6 +306,9 @@ impl SystemState { ) -> FunctionSystem { FunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: Some(FunctionSystemState { param: self.param_state, world_id: self.world_id, @@ -519,6 +522,8 @@ where F: SystemParamFunction, { func: F, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFnPtr, state: Option>, system_meta: SystemMeta, // NOTE: PhantomData T> gives this safe Send/Sync impls @@ -558,6 +563,9 @@ where fn clone(&self) -> Self { Self { func: self.func.clone(), + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: None, system_meta: SystemMeta::new::(), marker: PhantomData, @@ -578,6 +586,9 @@ where fn into_system(func: Self) -> Self::System { FunctionSystem { func, + #[cfg(feature = "hotpatching")] + current_ptr: subsecond::HotFn::current(>::run) + .ptr_address(), state: None, system_meta: SystemMeta::new::(), marker: PhantomData, @@ -609,29 +620,14 @@ where self.system_meta.name.clone() } - #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - #[inline] fn component_access_set(&self) -> &FilteredAccessSet { &self.system_meta.component_access_set } #[inline] - fn is_send(&self) -> bool { - self.system_meta.is_send - } - - #[inline] - fn is_exclusive(&self) -> bool { - false - } - - #[inline] - fn has_deferred(&self) -> bool { - self.system_meta.has_deferred + fn flags(&self) -> SystemStateFlags { + self.system_meta.flags } #[inline] @@ -653,11 +649,35 @@ where // will ensure that there are no data access conflicts. let params = unsafe { F::Param::get_param(&mut state.param, &self.system_meta, world, change_tick) }; + + #[cfg(feature = "hotpatching")] + let out = { + let mut hot_fn = subsecond::HotFn::current(>::run); + // SAFETY: + // - pointer used to call is from the current jump table + unsafe { + hot_fn + .try_call_with_ptr(self.current_ptr, (&mut self.func, input, params)) + .expect("Error calling hotpatched system. Run a full rebuild") + } + }; + #[cfg(not(feature = "hotpatching"))] let out = self.func.run(input, params); + self.system_meta.last_run = change_tick; out } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + let new = subsecond::HotFn::current(>::run).ptr_address(); + if new != self.current_ptr { + log::debug!("system {} hotpatched", self.name()); + } + self.current_ptr = new; + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { let param_state = &mut self.state.as_mut().expect(Self::ERROR_UNINITIALIZED).param; diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index a16207a612..7417d10208 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -97,7 +97,7 @@ //! - [`EventWriter`](crate::event::EventWriter) //! - [`NonSend`] and `Option` //! - [`NonSendMut`] and `Option` -//! - [`RemovedComponents`](crate::removal_detection::RemovedComponents) +//! - [`RemovedComponents`](crate::lifecycle::RemovedComponents) //! - [`SystemName`] //! - [`SystemChangeTick`] //! - [`Archetypes`](crate::archetype::Archetypes) (Provides Archetype metadata) @@ -408,10 +408,10 @@ mod tests { component::{Component, Components}, entity::{Entities, Entity}, error::Result, + lifecycle::RemovedComponents, name::Name, - prelude::{AnyOf, EntityRef, Trigger}, + prelude::{AnyOf, EntityRef, OnAdd, Trigger}, query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, - removal_detection::RemovedComponents, resource::Resource, schedule::{ common_conditions::resource_exists, ApplyDeferred, IntoScheduleConfigs, Schedule, @@ -421,7 +421,7 @@ mod tests { Commands, In, InMut, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, ResMut, Single, StaticSystemParam, System, SystemState, }, - world::{DeferredWorld, EntityMut, FromWorld, OnAdd, World}, + world::{DeferredWorld, EntityMut, FromWorld, World}, }; use super::ScheduleSystem; @@ -1166,7 +1166,9 @@ mod tests { x.initialize(&mut world); y.initialize(&mut world); - let conflicts = x.component_access().get_conflicts(y.component_access()); + let conflicts = x + .component_access_set() + .get_conflicts(y.component_access_set()); let b_id = world .components() .get_resource_id(TypeId::of::()) @@ -1630,7 +1632,7 @@ mod tests { #[test] #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_world_and_entity_mut_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" + expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_world_and_entity_mut_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" )] fn assert_world_and_entity_mut_system_does_conflict_first() { fn system(_query: &World, _q2: Query) {} @@ -1648,7 +1650,7 @@ mod tests { #[test] #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_ref_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" + expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_ref_and_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" )] fn assert_entity_ref_and_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} @@ -1657,7 +1659,7 @@ mod tests { #[test] #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" + expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_entity_mut_system_does_conflict::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" )] fn assert_entity_mut_system_does_conflict() { fn system(_query: Query, _q2: Query) {} @@ -1666,7 +1668,7 @@ mod tests { #[test] #[should_panic( - expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001" + expected = "error[B0001]: Query in system bevy_ecs::system::tests::assert_deferred_world_and_entity_ref_system_does_conflict_first::system accesses component(s) in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001" )] fn assert_deferred_world_and_entity_ref_system_does_conflict_first() { fn system(_world: DeferredWorld, _query: Query) {} diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index d3138151c9..4891a39d45 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -6,7 +6,7 @@ use crate::{ error::Result, never::Never, prelude::{Bundle, Trigger}, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{Fallible, Infallible}, system::{input::SystemIn, System}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -116,29 +116,14 @@ where self.observer.name() } - #[inline] - fn component_access(&self) -> &Access { - self.observer.component_access() - } - #[inline] fn component_access_set(&self) -> &FilteredAccessSet { self.observer.component_access_set() } #[inline] - fn is_send(&self) -> bool { - self.observer.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.observer.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.observer.has_deferred() + fn flags(&self) -> super::SystemStateFlags { + self.observer.flags() } #[inline] @@ -151,6 +136,12 @@ where Ok(()) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.observer.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.observer.apply_deferred(world); diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index b732461d5f..1fb08c488c 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -2012,17 +2012,67 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { self.as_nop().get(entity).is_ok() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new [`Query`] giving more + /// restrictive access to the entities matched by the current query. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// A transmute is valid only if `NewD` has a subset of the read, write, and required access + /// of the current query. A precise description of the access required by each parameter + /// type is given in the table below, but typical uses are to: + /// * Remove components, e.g. `Query<(&A, &B)>` to `Query<&A>`. + /// * Retrieve an existing component with reduced or equal access, e.g. `Query<&mut A>` to `Query<&A>` + /// or `Query<&T>` to `Query>`. + /// * Add parameters with no new access, for example adding an `Entity` parameter. + /// + /// Note that since filter terms are dropped, non-archetypal filters like + /// [`Added`], [`Changed`] and [`Spawned`] will not be respected. To maintain or change filter + /// terms see [`Self::transmute_lens_filtered`]. + /// + /// |`QueryData` parameter type|Access required| + /// |----|----| + /// |[`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], [`PhantomData`]|No access| + /// |[`EntityMut`]|Read and write access to all components, but no required access| + /// |[`EntityRef`]|Read access to all components, but no required access| + /// |`&T`, [`Ref`]|Read and required access to `T`| + /// |`&mut T`, [`Mut`]|Read, write and required access to `T`| + /// |[`Option`], [`AnyOf<(D, ...)>`]|Read and write access to `T`, but no required access| + /// |Tuples of query data and
`#[derive(QueryData)]` structs|The union of the access of their subqueries| + /// |[`FilteredEntityRef`], [`FilteredEntityMut`]|Determined by the [`QueryBuilder`] used to construct them. Any query can be transmuted to them, and they will receive the access of the source query. When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data| + /// + /// `transmute_lens` drops filter terms, but [`Self::transmute_lens_filtered`] supports returning a [`QueryLens`] with a new + /// filter type - the access required by filter parameters are as follows. + /// + /// |`QueryFilter` parameter type|Access required| + /// |----|----| + /// |[`Added`], [`Changed`]|Read and required access to `T`| + /// |[`With`], [`Without`]|No access| + /// |[`Or<(T, ...)>`]|Read access of the subqueries, but no required access| + /// |Tuples of query filters and `#[derive(QueryFilter)]` structs|The union of the access of their subqueries| + /// + /// [`Added`]: crate::query::Added + /// [`Added`]: crate::query::Added + /// [`AnyOf<(D, ...)>`]: crate::query::AnyOf + /// [`&Archetype`]: crate::archetype::Archetype + /// [`Changed`]: crate::query::Changed + /// [`Changed`]: crate::query::Changed + /// [`EntityMut`]: crate::world::EntityMut + /// [`EntityLocation`]: crate::entity::EntityLocation + /// [`EntityRef`]: crate::world::EntityRef + /// [`FilteredEntityRef`]: crate::world::FilteredEntityRef + /// [`FilteredEntityMut`]: crate::world::FilteredEntityMut + /// [`Has`]: crate::query::Has + /// [`Mut`]: crate::world::Mut + /// [`Or<(T, ...)>`]: crate::query::Or + /// [`QueryBuilder`]: crate::query::QueryBuilder + /// [`Ref`]: crate::world::Ref + /// [`SpawnDetails`]: crate::query::SpawnDetails + /// [`Spawned`]: crate::query::Spawned + /// [`With`]: crate::query::With + /// [`Without`]: crate::query::Without /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `D` + /// This will panic if the access required by `NewD` is not a subset of that required by + /// the original fetch `D`. /// /// ## Example /// @@ -2061,30 +2111,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, - /// you can also make limited changes to the types of parameters. - /// The new query must have a subset of the *read*, *write*, and *required* access of the original query. - /// - /// * `&mut T` and [`Mut`](crate::change_detection::Mut) have read, write, and required access to `T` - /// * `&T` and [`Ref`](crate::change_detection::Ref) have read and required access to `T` - /// * [`Option`] and [`AnyOf<(D, ...)>`](crate::query::AnyOf) have the read and write access of the subqueries, but no required access - /// * Tuples of query data and `#[derive(QueryData)]` structs have the union of the access of their subqueries - /// * [`EntityMut`](crate::world::EntityMut) has read and write access to all components, but no required access - /// * [`EntityRef`](crate::world::EntityRef) has read access to all components, but no required access - /// * [`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, - /// so can be added to any query - /// * [`FilteredEntityRef`](crate::world::FilteredEntityRef) and [`FilteredEntityMut`](crate::world::FilteredEntityMut) - /// have access determined by the [`QueryBuilder`](crate::query::QueryBuilder) used to construct them. - /// Any query can be transmuted to them, and they will receive the access of the source query. - /// When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data. - /// * [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) filters have read and required access to `T` - /// * [`With`](crate::query::With) and [`Without`](crate::query::Without) filters have no access at all, - /// so can be added to any query - /// * Tuples of query filters and `#[derive(QueryFilter)]` structs have the union of the access of their subqueries - /// * [`Or<(F, ...)>`](crate::query::Or) filters have the read access of the subqueries, but no required access - /// /// ### Examples of valid transmutes /// /// ```rust @@ -2161,28 +2187,21 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// // Nested inside of an `Or` filter, they have the same access as `Option<&T>`. /// assert_valid_transmute_filtered::, (), Entity, Or<(Changed, With)>>(); /// ``` - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`SpawnDetails`]: crate::query::SpawnDetails - /// [`&Archetype`]: crate::archetype::Archetype - /// [`Has`]: crate::query::Has #[track_caller] pub fn transmute_lens(&mut self) -> QueryLens<'_, NewD> { self.transmute_lens_filtered::() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new `Query` giving more restrictive + /// access to the entities matched by the current query. + /// /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// See [`Self::transmute_lens`] for a description of allowed transmutes. /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `Q` + /// This will panic if `NewD` is not a subset of the original fetch `D` /// /// ## Example /// @@ -2221,22 +2240,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, you can also - /// make limited changes to the types of parameters. - /// - /// * Can always add/remove [`Entity`] - /// * Can always add/remove [`EntityLocation`] - /// * Can always add/remove [`&Archetype`] - /// * `Ref` <-> `&T` - /// * `&mut T` -> `&T` - /// * `&mut T` -> `Ref` - /// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef) - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`&Archetype`]: crate::archetype::Archetype - /// /// # See also /// /// - [`transmute_lens`](Self::transmute_lens) to convert to a lens using a mutable borrow of the [`Query`]. @@ -2247,6 +2250,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens`] but also includes a [`QueryFilter`] type. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), @@ -2262,10 +2267,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens_inner`] but also includes a [`QueryFilter`] type. /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they + /// are in the type signature. /// /// # See also /// @@ -2623,6 +2631,36 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Populated<'w, 's, D, F> { } } +impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Populated<'w, 's, D, F> { + type Item = as IntoIterator>::Item; + + type IntoIter = as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl<'a, 'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'a Populated<'w, 's, D, F> { + type Item = <&'a Query<'w, 's, D, F> as IntoIterator>::Item; + + type IntoIter = <&'a Query<'w, 's, D, F> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.deref().into_iter() + } +} + +impl<'a, 'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'a mut Populated<'w, 's, D, F> { + type Item = <&'a mut Query<'w, 's, D, F> as IntoIterator>::Item; + + type IntoIter = <&'a mut Query<'w, 's, D, F> as IntoIterator>::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.deref_mut().into_iter() + } +} + #[cfg(test)] mod tests { use crate::{prelude::*, query::QueryEntityError}; diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 962ff94a2a..5e05a5ada9 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -3,12 +3,12 @@ use alloc::{borrow::Cow, vec::Vec}; use crate::{ component::{ComponentId, Tick}, error::Result, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, system::{input::SystemIn, BoxedSystem, System, SystemInput}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; -use super::{IntoSystem, SystemParamValidationError}; +use super::{IntoSystem, SystemParamValidationError, SystemStateFlags}; /// A wrapper system to change a system that returns `()` to return `Ok(())` to make it into a [`ScheduleSystem`] pub struct InfallibleSystemWrapper>(S); @@ -33,29 +33,14 @@ impl> System for InfallibleSystemWrapper { self.0.type_id() } - #[inline] - fn component_access(&self) -> &Access { - self.0.component_access() - } - #[inline] fn component_access_set(&self) -> &FilteredAccessSet { self.0.component_access_set() } #[inline] - fn is_send(&self) -> bool { - self.0.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.0.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.0.has_deferred() + fn flags(&self) -> SystemStateFlags { + self.0.flags() } #[inline] @@ -68,6 +53,12 @@ impl> System for InfallibleSystemWrapper { Ok(()) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.0.refresh_hotpatch(); + } + #[inline] fn apply_deferred(&mut self, world: &mut World) { self.0.apply_deferred(world); @@ -158,24 +149,13 @@ where self.system.name() } - fn component_access(&self) -> &Access { - self.system.component_access() - } - fn component_access_set(&self) -> &FilteredAccessSet { self.system.component_access_set() } - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( @@ -186,6 +166,12 @@ where self.system.run_unsafe(&mut self.value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } @@ -261,24 +247,13 @@ where self.system.name() } - fn component_access(&self) -> &Access { - self.system.component_access() - } - fn component_access_set(&self) -> &FilteredAccessSet { self.system.component_access_set() } - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( @@ -293,6 +268,12 @@ where self.system.run_unsafe(value, world) } + #[cfg(feature = "hotpatching")] + #[inline] + fn refresh_hotpatch(&mut self) { + self.system.refresh_hotpatch(); + } + fn apply_deferred(&mut self, world: &mut World) { self.system.apply_deferred(world); } diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 00731a47eb..716bcfb58a 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -2,13 +2,14 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17353." )] +use bitflags::bitflags; use core::fmt::Debug; use log::warn; use thiserror::Error; use crate::{ component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -19,6 +20,18 @@ use core::any::TypeId; use super::{IntoSystem, SystemParamValidationError}; +bitflags! { + /// Bitflags representing system states and requirements. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct SystemStateFlags: u8 { + /// Set if system cannot be sent across threads + const NON_SEND = 1 << 0; + /// Set if system requires exclusive World access + const EXCLUSIVE = 1 << 1; + /// Set if system has deferred buffers. + const DEFERRED = 1 << 2; + } +} /// An ECS system that can be added to a [`Schedule`](crate::schedule::Schedule) /// /// Systems are functions with all arguments implementing @@ -44,20 +57,29 @@ pub trait System: Send + Sync + 'static { TypeId::of::() } - /// Returns the system's component [`Access`]. - fn component_access(&self) -> &Access; - /// Returns the system's component [`FilteredAccessSet`]. fn component_access_set(&self) -> &FilteredAccessSet; + /// Returns the [`SystemStateFlags`] of the system. + fn flags(&self) -> SystemStateFlags; + /// Returns true if the system is [`Send`]. - fn is_send(&self) -> bool; + #[inline] + fn is_send(&self) -> bool { + !self.flags().intersects(SystemStateFlags::NON_SEND) + } /// Returns true if the system must be run exclusively. - fn is_exclusive(&self) -> bool; + #[inline] + fn is_exclusive(&self) -> bool { + self.flags().intersects(SystemStateFlags::EXCLUSIVE) + } /// Returns true if system has deferred buffers. - fn has_deferred(&self) -> bool; + #[inline] + fn has_deferred(&self) -> bool { + self.flags().intersects(SystemStateFlags::DEFERRED) + } /// Runs the system with the given input in the world. Unlike [`System::run`], this function /// can be called in parallel with other systems and may break Rust's aliasing rules @@ -76,6 +98,10 @@ pub trait System: Send + Sync + 'static { unsafe fn run_unsafe(&mut self, input: SystemIn<'_, Self>, world: UnsafeWorldCell) -> Self::Out; + /// Refresh the inner pointer based on the latest hot patch jump table + #[cfg(feature = "hotpatching")] + fn refresh_hotpatch(&mut self); + /// Runs the system with the given input in the world. /// /// For [read-only](ReadOnlySystem) systems, see [`run_readonly`], which can be called using `&World`. @@ -452,7 +478,7 @@ mod tests { let result = world.run_system_once(system); assert!(matches!(result, Err(RunSystemError::InvalidParams { .. }))); - let expected = "System bevy_ecs::system::system::tests::run_system_once_invalid_params::system did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist"; + let expected = "System bevy_ecs::system::system::tests::run_system_once_invalid_params::system did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens."; assert_eq!(expected, result.unwrap_err().to_string()); } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 0a2bd20428..ee38591d97 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -151,7 +151,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// let mut world = World::new(); /// let err = world.run_system_cached(|param: MyParam| {}).unwrap_err(); /// let expected = "Parameter `MyParam::foo` failed validation: Custom Message"; -/// assert!(err.to_string().ends_with(expected)); +/// assert!(err.to_string().contains(expected)); /// ``` /// /// ## Builders @@ -380,7 +380,7 @@ fn assert_component_access_compatibility( if !accesses.is_empty() { accesses.push(' '); } - panic!("error[B0001]: Query<{}, {}> in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001", ShortName(query_type), ShortName(filter_type)); + panic!("error[B0001]: Query<{}, {}> in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001", ShortName(query_type), ShortName(filter_type)); } // SAFETY: Relevant query ComponentId access is applied to SystemMeta. If @@ -728,7 +728,7 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { let combined_access = system_meta.component_access_set.combined_access(); assert!( !combined_access.has_resource_write(component_id), - "error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", + "error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", core::any::type_name::(), system_meta.name, ); @@ -801,11 +801,11 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { let combined_access = system_meta.component_access_set.combined_access(); if combined_access.has_resource_write(component_id) { panic!( - "error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", + "error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", core::any::type_name::(), system_meta.name); } else if combined_access.has_resource_read(component_id) { panic!( - "error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", + "error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", core::any::type_name::(), system_meta.name); } system_meta @@ -1357,7 +1357,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { let combined_access = system_meta.component_access_set.combined_access(); assert!( !combined_access.has_resource_write(component_id), - "error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", + "error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", core::any::type_name::(), system_meta.name, ); @@ -1430,11 +1430,11 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { let combined_access = system_meta.component_access_set.combined_access(); if combined_access.has_component_write(component_id) { panic!( - "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", + "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", core::any::type_name::(), system_meta.name); } else if combined_access.has_component_read(component_id) { panic!( - "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/b0002", + "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", core::any::type_name::(), system_meta.name); } system_meta @@ -2578,7 +2578,11 @@ impl Display for SystemParamValidationError { ShortName(&self.param), self.field, self.message - ) + )?; + if !self.skipped { + write!(fmt, "\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens.")?; + } + Ok(()) } } diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index ffce1214e6..7fdc22076e 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -880,7 +880,7 @@ mod tests { result, Err(RegisteredSystemError::InvalidParams { .. }) )); - let expected = format!("System {id:?} did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist"); + let expected = format!("System {id:?} did not run due to failed parameter validation: Parameter `Res` failed validation: Resource does not exist\nIf this is an expected state, wrap the parameter in `Option` and handle `None` when it happens, or wrap the parameter in `When` to skip the system when it happens."); assert_eq!(expected, result.unwrap_err().to_string()); } diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs deleted file mode 100644 index ea2899c5f9..0000000000 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Internal components used by bevy with a fixed component id. -//! Constants are used to skip [`TypeId`] lookups in hot paths. -use super::*; -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; - -/// [`ComponentId`] for [`OnAdd`] -pub const ON_ADD: ComponentId = ComponentId::new(0); -/// [`ComponentId`] for [`OnInsert`] -pub const ON_INSERT: ComponentId = ComponentId::new(1); -/// [`ComponentId`] for [`OnReplace`] -pub const ON_REPLACE: ComponentId = ComponentId::new(2); -/// [`ComponentId`] for [`OnRemove`] -pub const ON_REMOVE: ComponentId = ComponentId::new(3); -/// [`ComponentId`] for [`OnDespawn`] -pub const ON_DESPAWN: ComponentId = ComponentId::new(4); - -/// Trigger emitted when a component is inserted onto an entity that does not already have that -/// component. Runs before `OnInsert`. -/// See [`crate::component::ComponentHooks::on_add`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnAdd; - -/// Trigger emitted when a component is inserted, regardless of whether or not the entity already -/// had that component. Runs after `OnAdd`, if it ran. -/// See [`crate::component::ComponentHooks::on_insert`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnInsert; - -/// Trigger emitted when a component is inserted onto an entity that already has that component. -/// Runs before the value is replaced, so you can still access the original component data. -/// See [`crate::component::ComponentHooks::on_replace`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnReplace; - -/// Trigger emitted when a component is removed from an entity, and runs before the component is -/// removed, so you can still access the component data. -/// See [`crate::component::ComponentHooks::on_remove`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnRemove; - -/// Trigger emitted for each component on an entity when it is despawned. -/// See [`crate::component::ComponentHooks::on_despawn`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnDespawn; diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 4ead2ded85..3e6525dc9e 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -3,9 +3,10 @@ use core::ops::Deref; use crate::{ archetype::Archetype, change_detection::{MaybeLocation, MutUntyped}, - component::{ComponentId, HookContext, Mutable}, + component::{ComponentId, Mutable}, entity::Entity, event::{Event, EventId, Events, SendBatchIds}, + lifecycle::{HookContext, ON_INSERT, ON_REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, query::{DebugCheckedUnwrap, QueryData, QueryFilter}, @@ -16,7 +17,7 @@ use crate::{ world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, }; -use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLACE}; +use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World}; /// A [`World`] reference that disallows structural ECS changes. /// This includes initializing resources, registering components or spawning entities. @@ -164,7 +165,7 @@ impl<'w> DeferredWorld<'w> { if archetype.has_replace_observer() { self.trigger_observers( ON_REPLACE, - entity, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -204,7 +205,7 @@ impl<'w> DeferredWorld<'w> { if archetype.has_insert_observer() { self.trigger_observers( ON_INSERT, - entity, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -747,7 +748,7 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers( &mut self, event: ComponentId, - target: Entity, + target: Option, components: impl Iterator + Clone, caller: MaybeLocation, ) { @@ -770,7 +771,7 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers_with_data( &mut self, event: ComponentId, - mut target: Entity, + target: Option, components: impl Iterator + Clone, data: &mut E, mut propagate: bool, @@ -778,18 +779,20 @@ impl<'w> DeferredWorld<'w> { ) where T: Traversal, { + Observers::invoke::<_>( + self.reborrow(), + event, + target, + components.clone(), + data, + &mut propagate, + caller, + ); + let Some(mut target) = target else { return }; + loop { - Observers::invoke::<_>( - self.reborrow(), - event, - target, - components.clone(), - data, - &mut propagate, - caller, - ); if !propagate { - break; + return; } if let Some(traverse_to) = self .get_entity(target) @@ -801,6 +804,15 @@ impl<'w> DeferredWorld<'w> { } else { break; } + Observers::invoke::<_>( + self.reborrow(), + event, + Some(target), + components.clone(), + data, + &mut propagate, + caller, + ); } } diff --git a/crates/bevy_ecs/src/world/entity_fetch.rs b/crates/bevy_ecs/src/world/entity_fetch.rs index 07c711d0c4..b851840ee5 100644 --- a/crates/bevy_ecs/src/world/entity_fetch.rs +++ b/crates/bevy_ecs/src/world/entity_fetch.rs @@ -200,6 +200,7 @@ unsafe impl WorldEntityFetch for Entity { type Mut<'w> = EntityWorldMut<'w>; type DeferredMut<'w> = EntityMut<'w>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -209,6 +210,7 @@ unsafe impl WorldEntityFetch for Entity { Ok(unsafe { EntityRef::new(ecell) }) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -220,6 +222,7 @@ unsafe impl WorldEntityFetch for Entity { Ok(unsafe { EntityWorldMut::new(world, self, location) }) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -239,6 +242,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { type Mut<'w> = [EntityMut<'w>; N]; type DeferredMut<'w> = [EntityMut<'w>; N]; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -246,6 +250,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { <&Self>::fetch_ref(&self, cell) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -253,6 +258,7 @@ unsafe impl WorldEntityFetch for [Entity; N] { <&Self>::fetch_mut(&self, cell) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -270,6 +276,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { type Mut<'w> = [EntityMut<'w>; N]; type DeferredMut<'w> = [EntityMut<'w>; N]; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -287,6 +294,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -313,6 +321,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity; N] { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -332,6 +341,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { type Mut<'w> = Vec>; type DeferredMut<'w> = Vec>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -346,6 +356,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -369,6 +380,7 @@ unsafe impl WorldEntityFetch for &'_ [Entity] { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, @@ -388,6 +400,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { type Mut<'w> = EntityHashMap>; type DeferredMut<'w> = EntityHashMap>; + #[inline] unsafe fn fetch_ref( self, cell: UnsafeWorldCell<'_>, @@ -401,6 +414,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { Ok(refs) } + #[inline] unsafe fn fetch_mut( self, cell: UnsafeWorldCell<'_>, @@ -414,6 +428,7 @@ unsafe impl WorldEntityFetch for &'_ EntityHashSet { Ok(refs) } + #[inline] unsafe fn fetch_deferred_mut( self, cell: UnsafeWorldCell<'_>, diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 080eacffd2..785ddde585 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -14,15 +14,13 @@ use crate::{ EntityEquivalent, EntityIdLocation, EntityLocation, }, event::Event, + lifecycle::{ON_DESPAWN, ON_REMOVE, ON_REPLACE}, observer::Observer, query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, relationship::RelationshipHookMode, resource::Resource, system::IntoObserverSystem, - world::{ - error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World, - ON_DESPAWN, ON_REMOVE, ON_REPLACE, - }, + world::{error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World}, }; use alloc::vec::Vec; use bevy_platform::collections::{HashMap, HashSet}; @@ -463,6 +461,7 @@ impl<'w> EntityMut<'w> { /// - `cell` must have permission to mutate every component of the entity. /// - No accesses to any of the entity's components may exist /// at the same time as the returned [`EntityMut`]. + #[inline] pub(crate) unsafe fn new(cell: UnsafeEntityCell<'w>) -> Self { Self { cell } } @@ -1022,6 +1021,7 @@ impl<'w> From> for EntityMut<'w> { } impl<'a> From<&'a mut EntityWorldMut<'_>> for EntityMut<'a> { + #[inline] fn from(entity: &'a mut EntityWorldMut<'_>) -> Self { // SAFETY: `EntityWorldMut` guarantees exclusive access to the entire world. unsafe { EntityMut::new(entity.as_unsafe_entity_cell()) } @@ -1135,6 +1135,7 @@ impl<'w> EntityWorldMut<'w> { } } + #[inline(always)] fn as_unsafe_entity_cell_readonly(&self) -> UnsafeEntityCell<'_> { let last_change_tick = self.world.last_change_tick; let change_tick = self.world.read_change_tick(); @@ -1146,6 +1147,8 @@ impl<'w> EntityWorldMut<'w> { change_tick, ) } + + #[inline(always)] fn as_unsafe_entity_cell(&mut self) -> UnsafeEntityCell<'_> { let last_change_tick = self.world.last_change_tick; let change_tick = self.world.change_tick(); @@ -1157,6 +1160,8 @@ impl<'w> EntityWorldMut<'w> { change_tick, ) } + + #[inline(always)] fn into_unsafe_entity_cell(self) -> UnsafeEntityCell<'w> { let last_change_tick = self.world.last_change_tick; let change_tick = self.world.change_tick(); @@ -1197,6 +1202,7 @@ impl<'w> EntityWorldMut<'w> { } /// Gets read-only access to all of the entity's components. + #[inline] pub fn as_readonly(&self) -> EntityRef<'_> { EntityRef::from(self) } @@ -1208,6 +1214,7 @@ impl<'w> EntityWorldMut<'w> { } /// Gets non-structural mutable access to all of the entity's components. + #[inline] pub fn as_mutable(&mut self) -> EntityMut<'_> { EntityMut::from(self) } @@ -2403,7 +2410,7 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_despawn_observer() { deferred_world.trigger_observers( ON_DESPAWN, - self.entity, + Some(self.entity), archetype.components(), caller, ); @@ -2417,7 +2424,7 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_replace_observer() { deferred_world.trigger_observers( ON_REPLACE, - self.entity, + Some(self.entity), archetype.components(), caller, ); @@ -2432,7 +2439,7 @@ impl<'w> EntityWorldMut<'w> { if archetype.has_remove_observer() { deferred_world.trigger_observers( ON_REMOVE, - self.entity, + Some(self.entity), archetype.components(), caller, ); @@ -2670,14 +2677,14 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn entry<'a, T: Component>(&'a mut self) -> Entry<'w, 'a, T> { + pub fn entry<'a, T: Component>(&'a mut self) -> ComponentEntry<'w, 'a, T> { if self.contains::() { - Entry::Occupied(OccupiedEntry { + ComponentEntry::Occupied(OccupiedComponentEntry { entity_world: self, _marker: PhantomData, }) } else { - Entry::Vacant(VacantEntry { + ComponentEntry::Vacant(VacantComponentEntry { entity_world: self, _marker: PhantomData, }) @@ -2920,14 +2927,14 @@ impl<'w> EntityWorldMut<'w> { /// This `enum` can only be constructed from the [`entry`] method on [`EntityWorldMut`]. /// /// [`entry`]: EntityWorldMut::entry -pub enum Entry<'w, 'a, T: Component> { +pub enum ComponentEntry<'w, 'a, T: Component> { /// An occupied entry. - Occupied(OccupiedEntry<'w, 'a, T>), + Occupied(OccupiedComponentEntry<'w, 'a, T>), /// A vacant entry. - Vacant(VacantEntry<'w, 'a, T>), + Vacant(VacantComponentEntry<'w, 'a, T>), } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { /// Provides in-place mutable access to an occupied entry. /// /// # Examples @@ -2946,17 +2953,17 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { #[inline] pub fn and_modify)>(self, f: F) -> Self { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { f(entry.get_mut()); - Entry::Occupied(entry) + ComponentEntry::Occupied(entry) } - Entry::Vacant(entry) => Entry::Vacant(entry), + ComponentEntry::Vacant(entry) => ComponentEntry::Vacant(entry), } } } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { - /// Replaces the component of the entry, and returns an [`OccupiedEntry`]. +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { + /// Replaces the component of the entry, and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// @@ -2975,13 +2982,13 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(entry.get(), &Comp(2)); /// ``` #[inline] - pub fn insert_entry(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert_entry(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { entry.insert(component); entry } - Entry::Vacant(entry) => entry.insert(component), + ComponentEntry::Vacant(entry) => entry.insert(component), } } @@ -3007,10 +3014,10 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 8); /// ``` #[inline] - pub fn or_insert(self, default: T) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert(self, default: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default), } } @@ -3031,15 +3038,15 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 4); /// ``` #[inline] - pub fn or_insert_with T>(self, default: F) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert_with T>(self, default: F) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default()), } } } -impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component + Default> ComponentEntry<'w, 'a, T> { /// Ensures the entry has this component by inserting the default value if empty, and /// returns a mutable reference to this component in the entry. /// @@ -3057,42 +3064,42 @@ impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 0); /// ``` #[inline] - pub fn or_default(self) -> OccupiedEntry<'w, 'a, T> { + pub fn or_default(self) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(Default::default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(Default::default()), } } } -/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. +/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`OccupiedComponentEntry`] enum. /// /// The contained entity must have the component type parameter if we have this struct. -pub struct OccupiedEntry<'w, 'a, T: Component> { +pub struct OccupiedComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a reference to the component in the entry. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.get().0, 5); /// } /// ``` #[inline] pub fn get(&self) -> &T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get::().unwrap() } @@ -3101,14 +3108,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.insert(Comp(10)); /// } /// @@ -3124,14 +3131,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.take(), Comp(5)); /// } /// @@ -3139,30 +3146,30 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn take(self) -> T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.take().unwrap() } } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a mutable reference to the component in the entry. /// - /// If you need a reference to the `OccupiedEntry` which may outlive the destruction of - /// the `Entry` value, see [`into_mut`]. + /// If you need a reference to the [`OccupiedComponentEntry`] which may outlive the destruction of + /// the [`OccupiedComponentEntry`] value, see [`into_mut`]. /// /// [`into_mut`]: Self::into_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.get_mut().0 += 10; /// assert_eq!(o.get().0, 15); /// @@ -3174,28 +3181,28 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn get_mut(&mut self) -> Mut<'_, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut::().unwrap() } - /// Converts the `OccupiedEntry` into a mutable reference to the value in the entry with + /// Converts the [`OccupiedComponentEntry`] into a mutable reference to the value in the entry with /// a lifetime bound to the `EntityWorldMut`. /// - /// If you need multiple references to the `OccupiedEntry`, see [`get_mut`]. + /// If you need multiple references to the [`OccupiedComponentEntry`], see [`get_mut`]. /// /// [`get_mut`]: Self::get_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// o.into_mut().0 += 10; /// } /// @@ -3203,40 +3210,40 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn into_mut(self) -> Mut<'a, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut().unwrap() } } -/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. -pub struct VacantEntry<'w, 'a, T: Component> { +/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`ComponentEntry`] enum. +pub struct VacantComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { - /// Inserts the component into the `VacantEntry` and returns an `OccupiedEntry`. +impl<'w, 'a, T: Component> VacantComponentEntry<'w, 'a, T> { + /// Inserts the component into the [`VacantComponentEntry`] and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn_empty(); /// - /// if let Entry::Vacant(v) = entity.entry::() { + /// if let ComponentEntry::Vacant(v) = entity.entry::() { /// v.insert(Comp(10)); /// } /// /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 10); /// ``` #[inline] - pub fn insert(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { self.entity_world.insert(component); - OccupiedEntry { + OccupiedComponentEntry { entity_world: self.entity_world, _marker: PhantomData, } @@ -3247,7 +3254,7 @@ impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { /// /// To define the access when used as a [`QueryData`](crate::query::QueryData), /// use a [`QueryBuilder`](crate::query::QueryBuilder) or [`QueryParamBuilder`](crate::system::QueryParamBuilder). -/// The `FilteredEntityRef` must be the entire `QueryData`, and not nested inside a tuple with other data. +/// The [`FilteredEntityRef`] must be the entire [`QueryData`](crate::query::QueryData), and not nested inside a tuple with other data. /// /// ``` /// # use bevy_ecs::{prelude::*, world::FilteredEntityRef}; @@ -3355,7 +3362,11 @@ impl<'w> FilteredEntityRef<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get(&self) -> Option<&'w T> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3369,7 +3380,11 @@ impl<'w> FilteredEntityRef<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get_ref(&self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3381,7 +3396,11 @@ impl<'w> FilteredEntityRef<'w> { /// detection in custom runtimes. #[inline] pub fn get_change_ticks(&self) -> Option { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_read(id) // SAFETY: We have read access @@ -3719,7 +3738,11 @@ impl<'w> FilteredEntityMut<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get_mut>(&mut self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_write(id) // SAFETY: We have write access @@ -3747,7 +3770,11 @@ impl<'w> FilteredEntityMut<'w> { /// - `T` must be a mutable component #[inline] pub unsafe fn into_mut_assume_mutable(self) -> Option> { - let id = self.entity.world().components().get_id(TypeId::of::())?; + let id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; self.access .has_component_write(id) // SAFETY: @@ -3977,7 +4004,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -3997,7 +4024,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -4077,7 +4104,11 @@ where /// detection in custom runtimes. #[inline] pub fn get_change_ticks(&self) -> Option { - let component_id = self.entity.world().components().get_id(TypeId::of::())?; + let component_id = self + .entity + .world() + .components() + .get_valid_id(TypeId::of::())?; let components = self.entity.world().components(); (!bundle_contains_component::(components, component_id)) .then(|| { @@ -4246,7 +4277,7 @@ where C: Component, { let components = self.entity.world().components(); - let id = components.component_id::()?; + let id = components.valid_component_id::()?; if bundle_contains_component::(components, id) { None } else { @@ -4805,7 +4836,8 @@ mod tests { use core::panic::AssertUnwindSafe; use std::sync::OnceLock; - use crate::component::{HookContext, Tick}; + use crate::component::Tick; + use crate::lifecycle::HookContext; use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, @@ -4829,7 +4861,7 @@ mod tests { let entity = world.spawn(TestComponent(42)).id(); let component_id = world .components() - .get_id(core::any::TypeId::of::()) + .get_valid_id(core::any::TypeId::of::()) .unwrap(); let entity = world.entity(entity); @@ -4846,7 +4878,7 @@ mod tests { let entity = world.spawn(TestComponent(42)).id(); let component_id = world .components() - .get_id(core::any::TypeId::of::()) + .get_valid_id(core::any::TypeId::of::()) .unwrap(); let mut entity_mut = world.entity_mut(entity); @@ -5802,7 +5834,9 @@ mod tests { let entity = world .spawn_empty() .observe(|trigger: Trigger, mut commands: Commands| { - commands.entity(trigger.target()).insert(TestComponent(0)); + commands + .entity(trigger.target().unwrap()) + .insert(TestComponent(0)); }) .id(); @@ -5822,7 +5856,7 @@ mod tests { let mut world = World::new(); world.add_observer( |trigger: Trigger, mut commands: Commands| { - commands.entity(trigger.target()).despawn(); + commands.entity(trigger.target().unwrap()).despawn(); }, ); let entity = world.spawn_empty().id(); diff --git a/crates/bevy_ecs/src/world/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs index a9fac308fa..ed3672bef9 100644 --- a/crates/bevy_ecs/src/world/filtered_resource.rs +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -157,7 +157,7 @@ impl<'w, 's> FilteredResources<'w, 's> { let component_id = self .world .components() - .resource_id::() + .valid_resource_id::() .ok_or(ResourceFetchError::NotRegistered)?; if !self.access.has_resource_read(component_id) { return Err(ResourceFetchError::NoResourceAccess(component_id)); @@ -474,7 +474,7 @@ impl<'w, 's> FilteredResourcesMut<'w, 's> { let component_id = self .world .components() - .resource_id::() + .valid_resource_id::() .ok_or(ResourceFetchError::NotRegistered)?; // SAFETY: THe caller ensures that there are no conflicting borrows. unsafe { self.get_mut_by_id_unchecked(component_id) } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 5f80d06bce..0f4021cbfd 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,7 +1,6 @@ //! Defines the [`World`] and APIs for accessing it directly. pub(crate) mod command_queue; -mod component_constants; mod deferred_world; mod entity_fetch; mod entity_ref; @@ -21,14 +20,16 @@ pub use crate::{ use crate::{ entity::{ConstructedEntityDoesNotExistError, ConstructionError, EntitiesAllocator}, error::{DefaultErrorHandler, ErrorHandler}, + lifecycle::{ComponentHooks, ON_ADD, ON_DESPAWN, ON_INSERT, ON_REMOVE, ON_REPLACE}, + prelude::{OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace}, }; pub use bevy_ecs_macros::FromWorld; -pub use component_constants::*; pub use deferred_world::DeferredWorld; pub use entity_fetch::{EntityFetcher, WorldEntityFetch}; pub use entity_ref::{ - DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, - Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, TryFromFilteredError, VacantEntry, + ComponentEntry, DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, + EntityWorldMut, FilteredEntityMut, FilteredEntityRef, OccupiedComponentEntry, + TryFromFilteredError, VacantComponentEntry, }; pub use filtered_resource::*; pub use identifier::WorldId; @@ -42,17 +43,17 @@ use crate::{ }, change_detection::{MaybeLocation, MutUntyped, TicksMut}, component::{ - Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentIds, ComponentInfo, + CheckChangeTicks, Component, ComponentDescriptor, ComponentId, ComponentIds, ComponentInfo, ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, entity::{Entities, Entity, EntityDoesNotExistError}, entity_disabling::DefaultQueryFilters, event::{Event, EventId, Events, SendBatchIds}, + lifecycle::RemovedComponentEvents, observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, relationship::RelationshipHookMode, - removal_detection::RemovedComponentEvents, resource::Resource, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, @@ -542,7 +543,7 @@ impl World { /// Retrieves the [required components](RequiredComponents) for the given component type, if it exists. pub fn get_required_components(&self) -> Option<&RequiredComponents> { - let id = self.components().component_id::()?; + let id = self.components().valid_component_id::()?; let component_info = self.components().get_info(id)?; Some(component_info.required_components()) } @@ -1591,7 +1592,7 @@ impl World { /// assert!(!transform.is_changed()); /// ``` /// - /// [`RemovedComponents`]: crate::removal_detection::RemovedComponents + /// [`RemovedComponents`]: crate::lifecycle::RemovedComponents pub fn clear_trackers(&mut self) { self.removed_components.update(); self.last_change_tick = self.increment_change_tick(); @@ -1770,7 +1771,7 @@ impl World { /// since the last call to [`World::clear_trackers`]. pub fn removed(&self) -> impl Iterator + '_ { self.components - .get_id(TypeId::of::()) + .get_valid_id(TypeId::of::()) .map(|component_id| self.removed_with_id(component_id)) .into_iter() .flatten() @@ -1919,7 +1920,7 @@ impl World { /// Removes the resource of a given type and returns it, if it exists. Otherwise returns `None`. #[inline] pub fn remove_resource(&mut self) -> Option { - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, _, _) = self.storages.resources.get_mut(component_id)?.remove()?; // SAFETY: `component_id` was gotten via looking up the `R` type unsafe { Some(ptr.read::()) } @@ -1938,7 +1939,7 @@ impl World { /// thread than where the value was inserted from. #[inline] pub fn remove_non_send_resource(&mut self) -> Option { - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, _, _) = self .storages .non_send_resources @@ -1952,7 +1953,7 @@ impl World { #[inline] pub fn contains_resource(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.storages.resources.get(component_id)) .is_some_and(ResourceData::is_present) } @@ -1970,7 +1971,7 @@ impl World { #[inline] pub fn contains_non_send(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.storages.non_send_resources.get(component_id)) .is_some_and(ResourceData::is_present) } @@ -1993,7 +1994,7 @@ impl World { /// was called. pub fn is_resource_added(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .is_some_and(|component_id| self.is_resource_added_by_id(component_id)) } @@ -2024,7 +2025,7 @@ impl World { /// was called. pub fn is_resource_changed(&self) -> bool { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .is_some_and(|component_id| self.is_resource_changed_by_id(component_id)) } @@ -2049,7 +2050,7 @@ impl World { /// Retrieves the change ticks for the given resource. pub fn get_resource_change_ticks(&self) -> Option { self.components - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .and_then(|component_id| self.get_resource_change_ticks_by_id(component_id)) } @@ -2709,7 +2710,7 @@ impl World { let last_change_tick = self.last_change_tick(); let change_tick = self.change_tick(); - let component_id = self.components.get_resource_id(TypeId::of::())?; + let component_id = self.components.get_valid_resource_id(TypeId::of::())?; let (ptr, mut ticks, mut caller) = self .storages .resources @@ -3090,6 +3091,8 @@ impl World { schedules.check_change_ticks(change_tick); } + self.trigger(CheckChangeTicks(change_tick)); + self.last_check_tick = change_tick; } @@ -3877,7 +3880,7 @@ mod tests { world.insert_resource(TestResource(42)); let component_id = world .components() - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .unwrap(); let resource = world.get_resource_by_id(component_id).unwrap(); @@ -3893,7 +3896,7 @@ mod tests { world.insert_resource(TestResource(42)); let component_id = world .components() - .get_resource_id(TypeId::of::()) + .get_valid_resource_id(TypeId::of::()) .unwrap(); { diff --git a/crates/bevy_ecs/src/world/reflect.rs b/crates/bevy_ecs/src/world/reflect.rs index fdd8b28142..5ecdf88156 100644 --- a/crates/bevy_ecs/src/world/reflect.rs +++ b/crates/bevy_ecs/src/world/reflect.rs @@ -70,7 +70,7 @@ impl World { entity: Entity, type_id: TypeId, ) -> Result<&dyn Reflect, GetComponentReflectError> { - let Some(component_id) = self.components().get_id(type_id) else { + let Some(component_id) = self.components().get_valid_id(type_id) else { return Err(GetComponentReflectError::NoCorrespondingComponentId( type_id, )); @@ -158,7 +158,7 @@ impl World { )); }; - let Some(component_id) = self.components().get_id(type_id) else { + let Some(component_id) = self.components().get_valid_id(type_id) else { return Err(GetComponentReflectError::NoCorrespondingComponentId( type_id, )); diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 1aa0e0e947..1f3e851ef1 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -11,10 +11,10 @@ use crate::{ EntityIdLocation, EntityLocation, }, error::{DefaultErrorHandler, ErrorHandler}, + lifecycle::RemovedComponentEvents, observer::Observers, prelude::Component, query::{DebugCheckedUnwrap, ReadOnlyQueryData}, - removal_detection::RemovedComponentEvents, resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, @@ -39,7 +39,7 @@ use thiserror::Error; /// /// This alone is not enough to implement bevy systems where multiple systems can access *disjoint* parts of the world concurrently. For this, bevy stores all values of /// resources and components (and [`ComponentTicks`]) in [`UnsafeCell`]s, and carefully validates disjoint access patterns using -/// APIs like [`System::component_access`](crate::system::System::component_access). +/// APIs like [`System::component_access_set`](crate::system::System::component_access_set). /// /// A system then can be executed using [`System::run_unsafe`](crate::system::System::run_unsafe) with a `&World` and use methods with interior mutability to access resource values. /// @@ -406,7 +406,7 @@ impl<'w> UnsafeWorldCell<'w> { /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_resource(self) -> Option<&'w R> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures `self` has permission to access the resource // caller also ensure that no mutable reference to the resource exists unsafe { @@ -424,7 +424,7 @@ impl<'w> UnsafeWorldCell<'w> { /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_resource_ref(self) -> Option> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures `self` has permission to access the resource // caller also ensures that no mutable reference to the resource exists @@ -476,7 +476,7 @@ impl<'w> UnsafeWorldCell<'w> { /// - no mutable reference to the resource exists at the same time #[inline] pub unsafe fn get_non_send_resource(self) -> Option<&'w R> { - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: caller ensures that `self` has permission to access `R` // caller ensures that no mutable reference exists to `R` unsafe { @@ -519,7 +519,7 @@ impl<'w> UnsafeWorldCell<'w> { #[inline] pub unsafe fn get_resource_mut(self) -> Option> { self.assert_allows_mutable_access(); - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: // - caller ensures `self` has permission to access the resource mutably // - caller ensures no other references to the resource exist @@ -583,7 +583,7 @@ impl<'w> UnsafeWorldCell<'w> { #[inline] pub unsafe fn get_non_send_resource_mut(self) -> Option> { self.assert_allows_mutable_access(); - let component_id = self.components().get_resource_id(TypeId::of::())?; + let component_id = self.components().get_valid_resource_id(TypeId::of::())?; // SAFETY: // - caller ensures that `self` has permission to access the resource // - caller ensures that the resource is unaliased @@ -833,7 +833,7 @@ impl<'w> UnsafeEntityCell<'w> { /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get(self) -> Option<&'w T> { - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct (T component_id + T::STORAGE_TYPE) // - `location` is valid @@ -859,7 +859,7 @@ impl<'w> UnsafeEntityCell<'w> { pub unsafe fn get_ref(self) -> Option> { let last_change_tick = self.last_run; let change_tick = self.this_run; - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct (T component_id + T::STORAGE_TYPE) @@ -891,7 +891,7 @@ impl<'w> UnsafeEntityCell<'w> { /// - no other mutable references to the component exist at the same time #[inline] pub unsafe fn get_change_ticks(self) -> Option { - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - entity location is valid @@ -975,7 +975,7 @@ impl<'w> UnsafeEntityCell<'w> { ) -> Option> { self.world.assert_allows_mutable_access(); - let component_id = self.world.components().get_id(TypeId::of::())?; + let component_id = self.world.components().get_valid_id(TypeId::of::())?; // SAFETY: // - `storage_type` is correct diff --git a/crates/bevy_encase_derive/Cargo.toml b/crates/bevy_encase_derive/Cargo.toml index b2f1b92d82..60a6515fd8 100644 --- a/crates/bevy_encase_derive/Cargo.toml +++ b/crates/bevy_encase_derive/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_encase_derive" version = "0.16.0-dev" edition = "2024" description = "Bevy derive macro for encase" -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_encase_derive/src/lib.rs b/crates/bevy_encase_derive/src/lib.rs index 15fbdca6a8..d882cb5cae 100644 --- a/crates/bevy_encase_derive/src/lib.rs +++ b/crates/bevy_encase_derive/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" )] use bevy_macro_utils::BevyManifest; diff --git a/crates/bevy_gilrs/Cargo.toml b/crates/bevy_gilrs/Cargo.toml index 864df285d9..0afb6babbf 100644 --- a/crates/bevy_gilrs/Cargo.toml +++ b/crates/bevy_gilrs/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_gilrs" version = "0.16.0-dev" edition = "2024" description = "Gamepad system made using Gilrs 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_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index 66cc0e3328..db1b404abc 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/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" )] //! Systems and type definitions for gamepad handling in Bevy. diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 3a264c6244..97a41f15b6 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_gizmos" version = "0.16.0-dev" edition = "2024" description = "Provides gizmos 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_gizmos/macros/Cargo.toml b/crates/bevy_gizmos/macros/Cargo.toml index b38a3c5374..e15d0367b2 100644 --- a/crates/bevy_gizmos/macros/Cargo.toml +++ b/crates/bevy_gizmos/macros/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_gizmos_macros" version = "0.16.0-dev" edition = "2024" description = "Derive implementations for bevy_gizmos" -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_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index a59a80e89e..de48d94e42 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/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" )] //! This crate adds an immediate mode drawing api to Bevy for visual debugging. diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index cc67047c23..05b16d1fc9 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_gltf" version = "0.16.0-dev" edition = "2024" description = "Bevy Engine GLTF loading" -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_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 87818a21c2..97600f3ed0 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/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" )] //! Plugin providing an [`AssetLoader`](bevy_asset::AssetLoader) and type definitions diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 9bdeb23f26..abc555002a 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1043,71 +1043,75 @@ fn load_material( is_scale_inverted: bool, ) -> Handle { let material_label = material_label(material, is_scale_inverted); - load_context.labeled_asset_scope(material_label.to_string(), |load_context| { - let pbr = material.pbr_metallic_roughness(); + load_context + .labeled_asset_scope::<_, ()>(material_label.to_string(), |load_context| { + let pbr = material.pbr_metallic_roughness(); - // TODO: handle missing label handle errors here? - let color = pbr.base_color_factor(); - let base_color_channel = pbr - .base_color_texture() - .map(|info| uv_channel(material, "base color", info.tex_coord())) - .unwrap_or_default(); - let base_color_texture = pbr - .base_color_texture() - .map(|info| texture_handle(&info.texture(), load_context)); + // TODO: handle missing label handle errors here? + let color = pbr.base_color_factor(); + let base_color_channel = pbr + .base_color_texture() + .map(|info| uv_channel(material, "base color", info.tex_coord())) + .unwrap_or_default(); + let base_color_texture = pbr + .base_color_texture() + .map(|info| texture_handle(&info.texture(), load_context)); - let uv_transform = pbr - .base_color_texture() - .and_then(|info| info.texture_transform().map(texture_transform_to_affine2)) - .unwrap_or_default(); + let uv_transform = pbr + .base_color_texture() + .and_then(|info| info.texture_transform().map(texture_transform_to_affine2)) + .unwrap_or_default(); - let normal_map_channel = material - .normal_texture() - .map(|info| uv_channel(material, "normal map", info.tex_coord())) - .unwrap_or_default(); - let normal_map_texture: Option> = - material.normal_texture().map(|normal_texture| { - // TODO: handle normal_texture.scale - texture_handle(&normal_texture.texture(), load_context) + let normal_map_channel = material + .normal_texture() + .map(|info| uv_channel(material, "normal map", info.tex_coord())) + .unwrap_or_default(); + let normal_map_texture: Option> = + material.normal_texture().map(|normal_texture| { + // TODO: handle normal_texture.scale + texture_handle(&normal_texture.texture(), load_context) + }); + + let metallic_roughness_channel = pbr + .metallic_roughness_texture() + .map(|info| uv_channel(material, "metallic/roughness", info.tex_coord())) + .unwrap_or_default(); + let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { + warn_on_differing_texture_transforms( + material, + &info, + uv_transform, + "metallic/roughness", + ); + texture_handle(&info.texture(), load_context) }); - let metallic_roughness_channel = pbr - .metallic_roughness_texture() - .map(|info| uv_channel(material, "metallic/roughness", info.tex_coord())) - .unwrap_or_default(); - let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { - warn_on_differing_texture_transforms( - material, - &info, - uv_transform, - "metallic/roughness", - ); - texture_handle(&info.texture(), load_context) - }); + let occlusion_channel = material + .occlusion_texture() + .map(|info| uv_channel(material, "occlusion", info.tex_coord())) + .unwrap_or_default(); + let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| { + // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) + texture_handle(&occlusion_texture.texture(), load_context) + }); - let occlusion_channel = material - .occlusion_texture() - .map(|info| uv_channel(material, "occlusion", info.tex_coord())) - .unwrap_or_default(); - let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| { - // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) - texture_handle(&occlusion_texture.texture(), load_context) - }); + let emissive = material.emissive_factor(); + let emissive_channel = material + .emissive_texture() + .map(|info| uv_channel(material, "emissive", info.tex_coord())) + .unwrap_or_default(); + let emissive_texture = material.emissive_texture().map(|info| { + // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) + warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); + texture_handle(&info.texture(), load_context) + }); - let emissive = material.emissive_factor(); - let emissive_channel = material - .emissive_texture() - .map(|info| uv_channel(material, "emissive", info.tex_coord())) - .unwrap_or_default(); - let emissive_texture = material.emissive_texture().map(|info| { - // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) - warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); - texture_handle(&info.texture(), load_context) - }); - - #[cfg(feature = "pbr_transmission_textures")] - let (specular_transmission, specular_transmission_channel, specular_transmission_texture) = - material + #[cfg(feature = "pbr_transmission_textures")] + let ( + specular_transmission, + specular_transmission_channel, + specular_transmission_texture, + ) = material .transmission() .map_or((0.0, UvChannel::Uv0, None), |transmission| { let specular_transmission_channel = transmission @@ -1127,152 +1131,156 @@ fn load_material( ) }); - #[cfg(not(feature = "pbr_transmission_textures"))] - let specular_transmission = material - .transmission() - .map_or(0.0, |transmission| transmission.transmission_factor()); + #[cfg(not(feature = "pbr_transmission_textures"))] + let specular_transmission = material + .transmission() + .map_or(0.0, |transmission| transmission.transmission_factor()); - #[cfg(feature = "pbr_transmission_textures")] - let ( - thickness, - thickness_channel, - thickness_texture, - attenuation_distance, - attenuation_color, - ) = material.volume().map_or( - (0.0, UvChannel::Uv0, None, f32::INFINITY, [1.0, 1.0, 1.0]), - |volume| { - let thickness_channel = volume - .thickness_texture() - .map(|info| uv_channel(material, "thickness", info.tex_coord())) - .unwrap_or_default(); - let thickness_texture: Option> = - volume.thickness_texture().map(|thickness_texture| { - texture_handle(&thickness_texture.texture(), load_context) - }); + #[cfg(feature = "pbr_transmission_textures")] + let ( + thickness, + thickness_channel, + thickness_texture, + attenuation_distance, + attenuation_color, + ) = material.volume().map_or( + (0.0, UvChannel::Uv0, None, f32::INFINITY, [1.0, 1.0, 1.0]), + |volume| { + let thickness_channel = volume + .thickness_texture() + .map(|info| uv_channel(material, "thickness", info.tex_coord())) + .unwrap_or_default(); + let thickness_texture: Option> = + volume.thickness_texture().map(|thickness_texture| { + texture_handle(&thickness_texture.texture(), load_context) + }); - ( - volume.thickness_factor(), - thickness_channel, - thickness_texture, - volume.attenuation_distance(), - volume.attenuation_color(), - ) - }, - ); - - #[cfg(not(feature = "pbr_transmission_textures"))] - let (thickness, attenuation_distance, attenuation_color) = - material - .volume() - .map_or((0.0, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { ( volume.thickness_factor(), + thickness_channel, + thickness_texture, volume.attenuation_distance(), volume.attenuation_color(), ) - }); + }, + ); - let ior = material.ior().unwrap_or(1.5); + #[cfg(not(feature = "pbr_transmission_textures"))] + let (thickness, attenuation_distance, attenuation_color) = + material + .volume() + .map_or((0.0, f32::INFINITY, [1.0, 1.0, 1.0]), |volume| { + ( + volume.thickness_factor(), + volume.attenuation_distance(), + volume.attenuation_color(), + ) + }); - // Parse the `KHR_materials_clearcoat` extension data if necessary. - let clearcoat = - ClearcoatExtension::parse(load_context, document, material).unwrap_or_default(); + let ior = material.ior().unwrap_or(1.5); - // Parse the `KHR_materials_anisotropy` extension data if necessary. - let anisotropy = - AnisotropyExtension::parse(load_context, document, material).unwrap_or_default(); + // Parse the `KHR_materials_clearcoat` extension data if necessary. + let clearcoat = + ClearcoatExtension::parse(load_context, document, material).unwrap_or_default(); - // Parse the `KHR_materials_specular` extension data if necessary. - let specular = - SpecularExtension::parse(load_context, document, material).unwrap_or_default(); + // Parse the `KHR_materials_anisotropy` extension data if necessary. + let anisotropy = + AnisotropyExtension::parse(load_context, document, material).unwrap_or_default(); - // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels - let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); - let emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); + // Parse the `KHR_materials_specular` extension data if necessary. + let specular = + SpecularExtension::parse(load_context, document, material).unwrap_or_default(); - StandardMaterial { - base_color: Color::linear_rgba(color[0], color[1], color[2], color[3]), - base_color_channel, - base_color_texture, - perceptual_roughness: pbr.roughness_factor(), - metallic: pbr.metallic_factor(), - metallic_roughness_channel, - metallic_roughness_texture, - normal_map_channel, - normal_map_texture, - double_sided: material.double_sided(), - cull_mode: if material.double_sided() { - None - } else if is_scale_inverted { - Some(Face::Front) - } else { - Some(Face::Back) - }, - occlusion_channel, - occlusion_texture, - emissive, - emissive_channel, - emissive_texture, - specular_transmission, - #[cfg(feature = "pbr_transmission_textures")] - specular_transmission_channel, - #[cfg(feature = "pbr_transmission_textures")] - specular_transmission_texture, - thickness, - #[cfg(feature = "pbr_transmission_textures")] - thickness_channel, - #[cfg(feature = "pbr_transmission_textures")] - thickness_texture, - ior, - attenuation_distance, - attenuation_color: Color::linear_rgb( - attenuation_color[0], - attenuation_color[1], - attenuation_color[2], - ), - unlit: material.unlit(), - alpha_mode: alpha_mode(material), - uv_transform, - clearcoat: clearcoat.clearcoat_factor.unwrap_or_default() as f32, - clearcoat_perceptual_roughness: clearcoat.clearcoat_roughness_factor.unwrap_or_default() - as f32, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_channel: clearcoat.clearcoat_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_texture: clearcoat.clearcoat_texture, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_channel: clearcoat.clearcoat_roughness_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_texture: clearcoat.clearcoat_roughness_texture, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_channel: clearcoat.clearcoat_normal_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, - anisotropy_strength: anisotropy.anisotropy_strength.unwrap_or_default() as f32, - anisotropy_rotation: anisotropy.anisotropy_rotation.unwrap_or_default() as f32, - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_channel: anisotropy.anisotropy_channel, - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_texture: anisotropy.anisotropy_texture, - // From the `KHR_materials_specular` spec: - // - reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5, - #[cfg(feature = "pbr_specular_textures")] - specular_channel: specular.specular_channel, - #[cfg(feature = "pbr_specular_textures")] - specular_texture: specular.specular_texture, - specular_tint: match specular.specular_color_factor { - Some(color) => Color::linear_rgb(color[0] as f32, color[1] as f32, color[2] as f32), - None => Color::WHITE, - }, - #[cfg(feature = "pbr_specular_textures")] - specular_tint_channel: specular.specular_color_channel, - #[cfg(feature = "pbr_specular_textures")] - specular_tint_texture: specular.specular_color_texture, - ..Default::default() - } - }) + // We need to operate in the Linear color space and be willing to exceed 1.0 in our channels + let base_emissive = LinearRgba::rgb(emissive[0], emissive[1], emissive[2]); + let emissive = base_emissive * material.emissive_strength().unwrap_or(1.0); + + Ok(StandardMaterial { + base_color: Color::linear_rgba(color[0], color[1], color[2], color[3]), + base_color_channel, + base_color_texture, + perceptual_roughness: pbr.roughness_factor(), + metallic: pbr.metallic_factor(), + metallic_roughness_channel, + metallic_roughness_texture, + normal_map_channel, + normal_map_texture, + double_sided: material.double_sided(), + cull_mode: if material.double_sided() { + None + } else if is_scale_inverted { + Some(Face::Front) + } else { + Some(Face::Back) + }, + occlusion_channel, + occlusion_texture, + emissive, + emissive_channel, + emissive_texture, + specular_transmission, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_channel, + #[cfg(feature = "pbr_transmission_textures")] + specular_transmission_texture, + thickness, + #[cfg(feature = "pbr_transmission_textures")] + thickness_channel, + #[cfg(feature = "pbr_transmission_textures")] + thickness_texture, + ior, + attenuation_distance, + attenuation_color: Color::linear_rgb( + attenuation_color[0], + attenuation_color[1], + attenuation_color[2], + ), + unlit: material.unlit(), + alpha_mode: alpha_mode(material), + uv_transform, + clearcoat: clearcoat.clearcoat_factor.unwrap_or_default() as f32, + clearcoat_perceptual_roughness: clearcoat + .clearcoat_roughness_factor + .unwrap_or_default() as f32, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel: clearcoat.clearcoat_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture: clearcoat.clearcoat_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel: clearcoat.clearcoat_roughness_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture: clearcoat.clearcoat_roughness_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel: clearcoat.clearcoat_normal_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture: clearcoat.clearcoat_normal_texture, + anisotropy_strength: anisotropy.anisotropy_strength.unwrap_or_default() as f32, + anisotropy_rotation: anisotropy.anisotropy_rotation.unwrap_or_default() as f32, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_channel: anisotropy.anisotropy_channel, + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_texture: anisotropy.anisotropy_texture, + // From the `KHR_materials_specular` spec: + // + reflectance: specular.specular_factor.unwrap_or(1.0) as f32 * 0.5, + #[cfg(feature = "pbr_specular_textures")] + specular_channel: specular.specular_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: specular.specular_texture, + specular_tint: match specular.specular_color_factor { + Some(color) => { + Color::linear_rgb(color[0] as f32, color[1] as f32, color[2] as f32) + } + None => Color::WHITE, + }, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_channel: specular.specular_color_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_tint_texture: specular.specular_color_texture, + ..Default::default() + }) + }) + .unwrap() } /// Loads a glTF node. diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index 10fa026a6b..7f8128e365 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_image" version = "0.16.0-dev" edition = "2024" description = "Provides image types 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_image/src/dynamic_texture_atlas_builder.rs b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs index e8b812194a..3e1c8fd542 100644 --- a/crates/bevy_image/src/dynamic_texture_atlas_builder.rs +++ b/crates/bevy_image/src/dynamic_texture_atlas_builder.rs @@ -5,8 +5,11 @@ use guillotiere::{size2, Allocation, AtlasAllocator}; use thiserror::Error; use tracing::error; +/// An error produced by [`DynamicTextureAtlasBuilder`] when trying to add a new +/// texture to a [`TextureAtlasLayout`]. #[derive(Debug, Error)] pub enum DynamicTextureAtlasBuilderError { + /// Unable to allocate space within the atlas for the new texture #[error("Couldn't allocate space to add the image requested")] FailedToAllocateSpace, /// Attempted to add a texture to an uninitialized atlas diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 8dbe693286..e7548bb2bd 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -22,7 +22,10 @@ use wgpu_types::{ TextureViewDescriptor, }; +/// Trait used to provide default values for Bevy-external types that +/// do not implement [`Default`]. pub trait BevyDefault { + /// Returns the default value for a type. fn bevy_default() -> Self; } diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 0ef1213b46..fe086db674 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -81,19 +81,35 @@ impl ImageLoader { } } +/// How to determine an image's format when loading. #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub enum ImageFormatSetting { + /// Determine the image format from its file extension. + /// + /// This is the default. #[default] FromExtension, + /// Declare the image format explicitly. Format(ImageFormat), + /// Guess the image format by looking for magic bytes at the + /// beginning of its data. Guess, } +/// Settings for loading an [`Image`] using an [`ImageLoader`]. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageLoaderSettings { + /// How to determine the image's format. pub format: ImageFormatSetting, + /// Specifies whether image data is linear + /// or in sRGB space when this is not determined by + /// the image format. pub is_srgb: bool, + /// [`ImageSampler`] to use when rendering - this does + /// not affect the loading of the image data. pub sampler: ImageSampler, + /// Where the asset will be used - see the docs on + /// [`RenderAssetUsages`] for details. pub asset_usage: RenderAssetUsages, } @@ -108,11 +124,14 @@ impl Default for ImageLoaderSettings { } } +/// An error when loading an image using [`ImageLoader`]. #[non_exhaustive] #[derive(Debug, Error)] pub enum ImageLoaderError { - #[error("Could load shader: {0}")] + /// An error occurred while trying to load the image bytes. + #[error("Failed to load image bytes: {0}")] Io(#[from] std::io::Error), + /// An error occurred while trying to decode the image bytes. #[error("Could not load texture file: {0}")] FileTexture(#[from] FileTextureError), } @@ -170,7 +189,7 @@ impl AssetLoader for ImageLoader { /// An error that occurs when loading a texture from a file. #[derive(Error, Debug)] -#[error("Error reading image file {path}: {error}, this is an error in `bevy_render`.")] +#[error("Error reading image file {path}: {error}.")] pub struct FileTextureError { error: TextureError, path: String, diff --git a/crates/bevy_input/Cargo.toml b/crates/bevy_input/Cargo.toml index f6752abb05..6b805b83bf 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_input" version = "0.16.0-dev" edition = "2024" description = "Provides input 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_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 67c8995179..653af4c991 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -1,12 +1,12 @@ #![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] -//! Input functionality for the [Bevy game engine](https://bevyengine.org/). +//! Input functionality for the [Bevy game engine](https://bevy.org/). //! //! # Supported input devices //! diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index 0b2ca53830..e7ff3f6fe8 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_input_focus" version = "0.16.0-dev" edition = "2024" description = "Keyboard focus management" -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_input_focus/src/autofocus.rs b/crates/bevy_input_focus/src/autofocus.rs index 72024418d2..f6a862db88 100644 --- a/crates/bevy_input_focus/src/autofocus.rs +++ b/crates/bevy_input_focus/src/autofocus.rs @@ -1,6 +1,6 @@ //! Contains the [`AutoFocus`] component and related machinery. -use bevy_ecs::{component::HookContext, prelude::*, world::DeferredWorld}; +use bevy_ecs::{lifecycle::HookContext, prelude::*, world::DeferredWorld}; use crate::InputFocus; diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 44ff0ef645..436e838fb1 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/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] @@ -369,7 +369,7 @@ mod tests { use alloc::string::String; use bevy_ecs::{ - component::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, + lifecycle::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, }; use bevy_input::{ keyboard::{Key, KeyCode}, @@ -394,7 +394,7 @@ mod tests { trigger: Trigger>, mut query: Query<&mut GatherKeyboardEvents>, ) { - if let Ok(mut gather) = query.get_mut(trigger.target()) { + if let Ok(mut gather) = query.get_mut(trigger.target().unwrap()) { if let Key::Character(c) = &trigger.input.logical_key { gather.0.push_str(c.as_str()); } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 819b8dab8e..e22702e348 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_internal" version = "0.16.0-dev" edition = "2024" description = "An internal Bevy crate used to facilitate optional dynamic linking via the 'dynamic_linking' feature" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["game", "engine", "gamedev", "graphics", "bevy"] @@ -344,6 +344,8 @@ web = [ "bevy_tasks/web", ] +hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] + [dependencies] # bevy (no_std) bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ @@ -393,6 +395,7 @@ bevy_color = { path = "../bevy_color", optional = true, version = "0.16.0-dev", "bevy_reflect", ] } bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.16.0-dev" } +bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.16.0-dev" } bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.16.0-dev" } bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index db1152a362..93ba3cb889 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -19,7 +19,7 @@ plugin_group! { #[cfg(feature = "bevy_window")] bevy_a11y:::AccessibilityPlugin, #[cfg(feature = "std")] - #[custom(cfg(any(unix, windows)))] + #[custom(cfg(any(all(unix, not(target_os = "horizon")), windows)))] bevy_app:::TerminalCtrlCHandlerPlugin, #[cfg(feature = "bevy_asset")] bevy_asset:::AssetPlugin, @@ -66,6 +66,8 @@ plugin_group! { bevy_dev_tools:::DevToolsPlugin, #[cfg(feature = "bevy_ci_testing")] bevy_dev_tools::ci_testing:::CiTestingPlugin, + #[cfg(feature = "hotpatching")] + bevy_app::hotpatch:::HotPatchPlugin, #[plugin_group] #[cfg(feature = "bevy_picking")] bevy_picking:::DefaultPickingPlugins, diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 07dd936ab1..b5446d6b85 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/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] @@ -29,6 +29,8 @@ pub use bevy_audio as audio; pub use bevy_color as color; #[cfg(feature = "bevy_core_pipeline")] pub use bevy_core_pipeline as core_pipeline; +#[cfg(feature = "bevy_core_widgets")] +pub use bevy_core_widgets as core_widgets; #[cfg(feature = "bevy_dev_tools")] pub use bevy_dev_tools as dev_tools; pub use bevy_diagnostic as diagnostic; diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index 32902a2dda..fce602f7ad 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_log" version = "0.16.0-dev" edition = "2024" description = "Provides logging 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"] @@ -45,7 +45,7 @@ bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = fa ] } [target.'cfg(target_os = "ios")'.dependencies] -tracing-oslog = "0.2" +tracing-oslog = "0.3" [lints] workspace = true diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index aa8092c834..7a80a21cc3 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -1,10 +1,10 @@ #![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" )] -//! This crate provides logging functions and configuration for [Bevy](https://bevyengine.org) +//! This crate provides logging functions and configuration for [Bevy](https://bevy.org) //! apps, and automatically configures platform specific log handlers (i.e. Wasm or Android). //! //! The macros provided for logging are reexported from [`tracing`](https://docs.rs/tracing), diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index 36be752349..bc5989f0f3 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_macro_utils" version = "0.16.0-dev" edition = "2024" description = "A collection of utils 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_macro_utils/src/lib.rs b/crates/bevy_macro_utils/src/lib.rs index aa386101f1..209a92f5de 100644 --- a/crates/bevy_macro_utils/src/lib.rs +++ b/crates/bevy_macro_utils/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" )] //! A collection of helper types and functions for working on macros within the Bevy ecosystem. diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 7aae1ec74b..c420c3fe3c 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_math" version = "0.16.0-dev" edition = "2024" description = "Provides math 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_math/src/common_traits.rs b/crates/bevy_math/src/common_traits.rs index 4e127f4026..b249b34618 100644 --- a/crates/bevy_math/src/common_traits.rs +++ b/crates/bevy_math/src/common_traits.rs @@ -1,6 +1,6 @@ //! This module contains abstract mathematical traits shared by types used in `bevy_math`. -use crate::{ops, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; +use crate::{ops, DVec2, DVec3, DVec4, Dir2, Dir3, Dir3A, Quat, Rot2, Vec2, Vec3, Vec3A, Vec4}; use core::{ fmt::Debug, ops::{Add, Div, Mul, Neg, Sub}, @@ -9,7 +9,7 @@ use variadics_please::all_tuples_enumerated; /// A type that supports the mathematical operations of a real vector space, irrespective of dimension. /// In particular, this means that the implementing type supports: -/// - Scalar multiplication and division on the right by elements of `f32` +/// - Scalar multiplication and division on the right by elements of `Self::Scalar` /// - Negation /// - Addition and subtraction /// - Zero @@ -19,16 +19,16 @@ use variadics_please::all_tuples_enumerated; /// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`. /// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`. /// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`. -/// - (Compatibility of multiplication) For all `a, b: f32`, `v: Self`, `v * (a * b) == (v * a) * b`. +/// - (Compatibility of multiplication) For all `a, b: Self::Scalar`, `v: Self`, `v * (a * b) == (v * a) * b`. /// - (Multiplicative identity) For all `v: Self`, `v * 1.0 == v`. -/// - (Distributivity for vector addition) For all `a: f32`, `u, v: Self`, `(u + v) * a == u * a + v * a`. -/// - (Distributivity for scalar addition) For all `a, b: f32`, `v: Self`, `v * (a + b) == v * a + v * b`. +/// - (Distributivity for vector addition) For all `a: Self::Scalar`, `u, v: Self`, `(u + v) * a == u * a + v * a`. +/// - (Distributivity for scalar addition) For all `a, b: Self::Scalar`, `v: Self`, `v * (a + b) == v * a + v * b`. /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. pub trait VectorSpace: - Mul - + Div + Mul + + Div + Add + Sub + Neg @@ -37,6 +37,9 @@ pub trait VectorSpace: + Clone + Copy { + /// The scalar type of this vector space. + type Scalar: ScalarField; + /// The zero vector, which is the identity of addition for the vector space type. const ZERO: Self; @@ -47,29 +50,99 @@ pub trait VectorSpace: /// Note that the value of `t` is not clamped by this function, so extrapolating outside /// of the interval `[0,1]` is allowed. #[inline] - fn lerp(self, rhs: Self, t: f32) -> Self { - self * (1. - t) + rhs * t + fn lerp(self, rhs: Self, t: Self::Scalar) -> Self { + self * (Self::Scalar::ONE - t) + rhs * t } } impl VectorSpace for Vec4 { + type Scalar = f32; const ZERO: Self = Vec4::ZERO; } impl VectorSpace for Vec3 { + type Scalar = f32; const ZERO: Self = Vec3::ZERO; } impl VectorSpace for Vec3A { + type Scalar = f32; const ZERO: Self = Vec3A::ZERO; } impl VectorSpace for Vec2 { + type Scalar = f32; const ZERO: Self = Vec2::ZERO; } -impl VectorSpace for f32 { +impl VectorSpace for DVec4 { + type Scalar = f64; + const ZERO: Self = DVec4::ZERO; +} + +impl VectorSpace for DVec3 { + type Scalar = f64; + const ZERO: Self = DVec3::ZERO; +} + +impl VectorSpace for DVec2 { + type Scalar = f64; + const ZERO: Self = DVec2::ZERO; +} + +// Every scalar field is a 1-dimensional vector space over itself. +impl VectorSpace for T { + type Scalar = Self; + const ZERO: Self = Self::ZERO; +} + +/// A type that supports the operations of a scalar field. An implementation should support: +/// - Addition and subtraction +/// - Multiplication and division +/// - Negation +/// - Zero (additive identity) +/// - One (multiplicative identity) +/// +/// Within the limitations of floating point arithmetic, all the following are required to hold: +/// - (Associativity of addition) For all `u, v, w: Self`, `(u + v) + w == u + (v + w)`. +/// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`. +/// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`. +/// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`. +/// - (Associativity of multiplication) For all `u, v, w: Self`, `(u * v) * w == u * (v * w)`. +/// - (Commutativity of multiplication) For all `u, v: Self`, `u * v == v * u`. +/// - (Multiplicative identity) For all `v: Self`, `v * Self::ONE == v`. +/// - (Multiplicative inverse) For all `v: Self`, `v / v == v * v.inverse() == Self::ONE`. +/// - (Distributivity over addition) For all `a, b: Self`, `u, v: Self`, `(u + v) * a == u * a + v * a`. +pub trait ScalarField: + Mul + + Div + + Add + + Sub + + Neg + + Default + + Debug + + Clone + + Copy +{ + /// The additive identity. + const ZERO: Self; + /// The multiplicative identity. + const ONE: Self; + + /// The multiplicative inverse of this element. This is equivalent to `1.0 / self`. + fn recip(self) -> Self { + Self::ONE / self + } +} + +impl ScalarField for f32 { const ZERO: Self = 0.0; + const ONE: Self = 1.0; +} + +impl ScalarField for f64 { + const ZERO: Self = 0.0; + const ONE: Self = 1.0; } /// A type consisting of formal sums of elements from `V` and `W`. That is, @@ -84,24 +157,24 @@ impl VectorSpace for f32 { #[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] pub struct Sum(pub V, pub W); -impl Mul for Sum +impl Mul for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { type Output = Self; - fn mul(self, rhs: f32) -> Self::Output { + fn mul(self, rhs: F) -> Self::Output { Sum(self.0 * rhs, self.1 * rhs) } } -impl Div for Sum +impl Div for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { type Output = Self; - fn div(self, rhs: f32) -> Self::Output { + fn div(self, rhs: F) -> Self::Output { Sum(self.0 / rhs, self.1 / rhs) } } @@ -149,11 +222,12 @@ where } } -impl VectorSpace for Sum +impl VectorSpace for Sum where - V: VectorSpace, - W: VectorSpace, + V: VectorSpace, + W: VectorSpace, { + type Scalar = F; const ZERO: Self = Sum(V::ZERO, W::ZERO); } @@ -162,32 +236,32 @@ where /// relationships hold, within the limitations of floating point arithmetic: /// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`. /// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`. -/// - (Absolute homogeneity) For all `c: f32`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`. +/// - (Absolute homogeneity) For all `c: Self::Scalar`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`. /// - (Triangle inequality) For all `v, w: Self`, `(v + w).norm() <= v.norm() + w.norm()`. /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. pub trait NormedVectorSpace: VectorSpace { /// The size of this element. The return value should always be nonnegative. - fn norm(self) -> f32; + fn norm(self) -> Self::Scalar; /// The squared norm of this element. Computing this is often faster than computing /// [`NormedVectorSpace::norm`]. #[inline] - fn norm_squared(self) -> f32 { + fn norm_squared(self) -> Self::Scalar { self.norm() * self.norm() } /// The distance between this element and another, as determined by the norm. #[inline] - fn distance(self, rhs: Self) -> f32 { + fn distance(self, rhs: Self) -> Self::Scalar { (rhs - self).norm() } /// The squared distance between this element and another, as determined by the norm. Note that /// this is often faster to compute in practice than [`NormedVectorSpace::distance`]. #[inline] - fn distance_squared(self, rhs: Self) -> f32 { + fn distance_squared(self, rhs: Self) -> Self::Scalar { (rhs - self).norm_squared() } } @@ -245,10 +319,55 @@ impl NormedVectorSpace for f32 { fn norm(self) -> f32 { ops::abs(self) } +} + +impl NormedVectorSpace for DVec4 { + #[inline] + fn norm(self) -> f64 { + self.length() + } #[inline] - fn norm_squared(self) -> f32 { - self * self + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for DVec3 { + #[inline] + fn norm(self) -> f64 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for DVec2 { + #[inline] + fn norm(self) -> f64 { + self.length() + } + + #[inline] + fn norm_squared(self) -> f64 { + self.length_squared() + } +} + +impl NormedVectorSpace for f64 { + #[inline] + #[cfg(feature = "std")] + fn norm(self) -> f64 { + f64::abs(self) + } + + #[inline] + #[cfg(all(any(feature = "libm", feature = "nostd-libm"), not(feature = "std")))] + fn norm(self) -> f64 { + libm::fabs(self) } } @@ -353,7 +472,7 @@ pub trait StableInterpolate: Clone { // VectorSpace type, but the "natural from the semantics" part is less clear in general. impl StableInterpolate for V where - V: NormedVectorSpace, + V: NormedVectorSpace, { #[inline] fn interpolate_stable(&self, other: &Self, t: f32) -> Self { @@ -462,10 +581,13 @@ impl HasTangent for V { type Tangent = V; } -impl HasTangent for (M, N) +impl HasTangent for (M, N) where - M: HasTangent, - N: HasTangent, + F: ScalarField, + U: VectorSpace, + V: VectorSpace, + M: HasTangent, + N: HasTangent, { type Tangent = Sum; } diff --git a/crates/bevy_math/src/cubic_splines/curve_impls.rs b/crates/bevy_math/src/cubic_splines/curve_impls.rs index 85fd9fb6ad..c21763db4e 100644 --- a/crates/bevy_math/src/cubic_splines/curve_impls.rs +++ b/crates/bevy_math/src/cubic_splines/curve_impls.rs @@ -10,7 +10,7 @@ use super::{CubicCurve, RationalCurve}; // -- CubicSegment -impl Curve

for CubicSegment

{ +impl> Curve

for CubicSegment

{ #[inline] fn domain(&self) -> Interval { Interval::UNIT @@ -22,7 +22,7 @@ impl Curve

for CubicSegment

{ } } -impl SampleDerivative

for CubicSegment

{ +impl> SampleDerivative

for CubicSegment

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -32,7 +32,7 @@ impl SampleDerivative

for CubicSegment

{ } } -impl SampleTwoDerivatives

for CubicSegment

{ +impl> SampleTwoDerivatives

for CubicSegment

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -46,7 +46,7 @@ impl SampleTwoDerivatives

for CubicSegment

{ // -- CubicCurve #[cfg(feature = "alloc")] -impl Curve

for CubicCurve

{ +impl> Curve

for CubicCurve

{ #[inline] fn domain(&self) -> Interval { // The non-emptiness invariant guarantees that this succeeds. @@ -61,7 +61,7 @@ impl Curve

for CubicCurve

{ } #[cfg(feature = "alloc")] -impl SampleDerivative

for CubicCurve

{ +impl> SampleDerivative

for CubicCurve

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -72,7 +72,7 @@ impl SampleDerivative

for CubicCurve

{ } #[cfg(feature = "alloc")] -impl SampleTwoDerivatives

for CubicCurve

{ +impl> SampleTwoDerivatives

for CubicCurve

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -85,7 +85,7 @@ impl SampleTwoDerivatives

for CubicCurve

{ // -- RationalSegment -impl Curve

for RationalSegment

{ +impl> Curve

for RationalSegment

{ #[inline] fn domain(&self) -> Interval { Interval::UNIT @@ -97,7 +97,7 @@ impl Curve

for RationalSegment

{ } } -impl SampleDerivative

for RationalSegment

{ +impl> SampleDerivative

for RationalSegment

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -107,7 +107,7 @@ impl SampleDerivative

for RationalSegment

{ } } -impl SampleTwoDerivatives

for RationalSegment

{ +impl> SampleTwoDerivatives

for RationalSegment

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { @@ -121,7 +121,7 @@ impl SampleTwoDerivatives

for RationalSegment

{ // -- RationalCurve #[cfg(feature = "alloc")] -impl Curve

for RationalCurve

{ +impl> Curve

for RationalCurve

{ #[inline] fn domain(&self) -> Interval { // The non-emptiness invariant guarantees the success of this. @@ -136,7 +136,7 @@ impl Curve

for RationalCurve

{ } #[cfg(feature = "alloc")] -impl SampleDerivative

for RationalCurve

{ +impl> SampleDerivative

for RationalCurve

{ #[inline] fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative

{ WithDerivative { @@ -147,7 +147,7 @@ impl SampleDerivative

for RationalCurve

{ } #[cfg(feature = "alloc")] -impl SampleTwoDerivatives

for RationalCurve

{ +impl> SampleTwoDerivatives

for RationalCurve

{ #[inline] fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives

{ WithTwoDerivatives { diff --git a/crates/bevy_math/src/cubic_splines/mod.rs b/crates/bevy_math/src/cubic_splines/mod.rs index 0f4082bd09..1b04603a73 100644 --- a/crates/bevy_math/src/cubic_splines/mod.rs +++ b/crates/bevy_math/src/cubic_splines/mod.rs @@ -68,7 +68,7 @@ impl CubicBezier

{ } #[cfg(feature = "alloc")] -impl CubicGenerator

for CubicBezier

{ +impl> CubicGenerator

for CubicBezier

{ type Error = CubicBezierError; #[inline] @@ -176,7 +176,7 @@ impl CubicHermite

{ } #[cfg(feature = "alloc")] -impl CubicGenerator

for CubicHermite

{ +impl> CubicGenerator

for CubicHermite

{ type Error = InsufficientDataError; #[inline] @@ -202,7 +202,7 @@ impl CubicGenerator

for CubicHermite

{ } #[cfg(feature = "alloc")] -impl CyclicCubicGenerator

for CubicHermite

{ +impl> CyclicCubicGenerator

for CubicHermite

{ type Error = InsufficientDataError; #[inline] @@ -313,7 +313,7 @@ impl CubicCardinalSpline

{ } #[cfg(feature = "alloc")] -impl CubicGenerator

for CubicCardinalSpline

{ +impl> CubicGenerator

for CubicCardinalSpline

{ type Error = InsufficientDataError; #[inline] @@ -351,7 +351,7 @@ impl CubicGenerator

for CubicCardinalSpline

{ } #[cfg(feature = "alloc")] -impl CyclicCubicGenerator

for CubicCardinalSpline

{ +impl> CyclicCubicGenerator

for CubicCardinalSpline

{ type Error = InsufficientDataError; #[inline] @@ -471,7 +471,7 @@ impl CubicBSpline

{ } #[cfg(feature = "alloc")] -impl CubicGenerator

for CubicBSpline

{ +impl> CubicGenerator

for CubicBSpline

{ type Error = InsufficientDataError; #[inline] @@ -494,7 +494,7 @@ impl CubicGenerator

for CubicBSpline

{ } #[cfg(feature = "alloc")] -impl CyclicCubicGenerator

for CubicBSpline

{ +impl> CyclicCubicGenerator

for CubicBSpline

{ type Error = InsufficientDataError; #[inline] @@ -620,7 +620,7 @@ pub struct CubicNurbs { } #[cfg(feature = "alloc")] -impl CubicNurbs

{ +impl> CubicNurbs

{ /// Build a Non-Uniform Rational B-Spline. /// /// If provided, weights must be the same length as the control points. Defaults to equal weights. @@ -781,7 +781,7 @@ impl CubicNurbs

{ } #[cfg(feature = "alloc")] -impl RationalGenerator

for CubicNurbs

{ +impl> RationalGenerator

for CubicNurbs

{ type Error = InsufficientDataError; #[inline] @@ -962,7 +962,7 @@ pub struct CubicSegment { pub coeff: [P; 4], } -impl CubicSegment

{ +impl> CubicSegment

{ /// Instantaneous position of a point at parametric value `t`. #[inline] pub fn position(&self, t: f32) -> P { @@ -1184,7 +1184,7 @@ pub struct CubicCurve { } #[cfg(feature = "alloc")] -impl CubicCurve

{ +impl> CubicCurve

{ /// Create a new curve from a collection of segments. If the collection of segments is empty, /// a curve cannot be built and `None` will be returned instead. pub fn from_segments(segments: impl IntoIterator>) -> Option { @@ -1347,7 +1347,7 @@ pub struct RationalSegment { /// The width of the domain of this segment. pub knot_span: f32, } -impl RationalSegment

{ +impl> RationalSegment

{ /// Instantaneous position of a point at parametric value `t` in `[0, 1]`. #[inline] pub fn position(&self, t: f32) -> P { @@ -1484,7 +1484,7 @@ pub struct RationalCurve { } #[cfg(feature = "alloc")] -impl RationalCurve

{ +impl> RationalCurve

{ /// Create a new curve from a collection of segments. If the collection of segments is empty, /// a curve cannot be built and `None` will be returned instead. pub fn from_segments(segments: impl IntoIterator>) -> Option { diff --git a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs index a499526b78..9e3686b5aa 100644 --- a/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs +++ b/crates/bevy_math/src/curve/derivatives/adaptor_impls.rs @@ -208,10 +208,12 @@ where // -- ZipCurve -impl SampleDerivative<(S, T)> for ZipCurve +impl SampleDerivative<(S, T)> for ZipCurve where - S: HasTangent, - T: HasTangent, + U: VectorSpace, + V: VectorSpace, + S: HasTangent, + T: HasTangent, C: SampleDerivative, D: SampleDerivative, { @@ -225,10 +227,12 @@ where } } -impl SampleTwoDerivatives<(S, T)> for ZipCurve +impl SampleTwoDerivatives<(S, T)> for ZipCurve where - S: HasTangent, - T: HasTangent, + U: VectorSpace, + V: VectorSpace, + S: HasTangent, + T: HasTangent, C: SampleTwoDerivatives, D: SampleTwoDerivatives, { @@ -248,9 +252,10 @@ where // -- GraphCurve -impl SampleDerivative<(f32, T)> for GraphCurve +impl SampleDerivative<(f32, T)> for GraphCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, { fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative<(f32, T)> { @@ -262,9 +267,10 @@ where } } -impl SampleTwoDerivatives<(f32, T)> for GraphCurve +impl SampleTwoDerivatives<(f32, T)> for GraphCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, { fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives<(f32, T)> { @@ -321,9 +327,10 @@ where // -- CurveReparamCurve -impl SampleDerivative for CurveReparamCurve +impl SampleDerivative for CurveReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, D: SampleDerivative, { @@ -349,9 +356,10 @@ where } } -impl SampleTwoDerivatives for CurveReparamCurve +impl SampleTwoDerivatives for CurveReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, D: SampleTwoDerivatives, { @@ -386,9 +394,10 @@ where // -- LinearReparamCurve -impl SampleDerivative for LinearReparamCurve +impl SampleDerivative for LinearReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleDerivative, { fn sample_with_derivative_unchecked(&self, t: f32) -> WithDerivative { @@ -413,9 +422,10 @@ where } } -impl SampleTwoDerivatives for LinearReparamCurve +impl SampleTwoDerivatives for LinearReparamCurve where - T: HasTangent, + V: VectorSpace, + T: HasTangent, C: SampleTwoDerivatives, { fn sample_with_two_derivatives_unchecked(&self, t: f32) -> WithTwoDerivatives { diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index c0b452e001..91908ee80b 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -32,7 +32,7 @@ pub trait Ease: Sized { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve; } -impl Ease for V { +impl> Ease for V { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve { FunctionCurve::new(Interval::EVERYWHERE, move |t| V::lerp(start, end, t)) } diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 20d458db72..070483e777 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -9,8 +9,8 @@ #![cfg_attr(any(docsrs, docsrs_dep), feature(rustdoc_internals))] #![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_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index 3be0ead1da..c17bc6fa76 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -40,11 +40,12 @@ use core::f32::consts::{PI, TAU}; -use crate::{ops, primitives::*, NormedVectorSpace, Vec2, Vec3}; +use crate::{ops, primitives::*, NormedVectorSpace, ScalarField, Vec2, Vec3}; use rand::{ distributions::{Distribution, WeightedIndex}, Rng, }; +use rand_distr::uniform::SampleUniform; /// Exposes methods to uniformly sample a variety of primitive shapes. pub trait ShapeSample { @@ -281,22 +282,24 @@ impl ShapeSample for Cuboid { } /// Interior sampling for triangles which doesn't depend on the ambient dimension. -fn sample_triangle_interior( - vertices: [P; 3], - rng: &mut R, -) -> P { +fn sample_triangle_interior(vertices: [P; 3], rng: &mut R) -> P +where + P: NormedVectorSpace, + P::Scalar: SampleUniform + PartialOrd, + R: Rng + ?Sized, +{ let [a, b, c] = vertices; let ab = b - a; let ac = c - a; // Generate random points on a parallelepiped and reflect so that // we can use the points that lie outside the triangle - let u = rng.gen_range(0.0..=1.0); - let v = rng.gen_range(0.0..=1.0); + let u = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); + let v = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); - if u + v > 1. { - let u1 = 1. - v; - let v1 = 1. - u; + if u + v > P::Scalar::ONE { + let u1 = P::Scalar::ONE - v; + let v1 = P::Scalar::ONE - u; a + (ab * u1 + ac * v1) } else { a + (ab * u + ac * v) @@ -304,16 +307,18 @@ fn sample_triangle_interior( } /// Boundary sampling for triangles which doesn't depend on the ambient dimension. -fn sample_triangle_boundary( - vertices: [P; 3], - rng: &mut R, -) -> P { +fn sample_triangle_boundary(vertices: [P; 3], rng: &mut R) -> P +where + P: NormedVectorSpace, + P::Scalar: SampleUniform + PartialOrd + for<'a> ::core::ops::AddAssign<&'a P::Scalar>, + R: Rng + ?Sized, +{ let [a, b, c] = vertices; let ab = b - a; let ac = c - a; let bc = c - b; - let t = rng.gen_range(0.0..=1.0); + let t = rng.gen_range(P::Scalar::ZERO..=P::Scalar::ONE); if let Ok(dist) = WeightedIndex::new([ab.norm(), ac.norm(), bc.norm()]) { match dist.sample(rng) { diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index 2ccb65cdb4..a235fea5ef 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_mesh" version = "0.16.0-dev" edition = "2024" description = "Provides mesh types 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_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index fbca931fe2..7428504adc 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -9,7 +9,7 @@ authors = [ ] description = "Mikkelsen tangent space algorithm" documentation = "https://docs.rs/bevy" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "Zlib AND (MIT OR Apache-2.0)" keywords = ["bevy", "3D", "graphics", "algorithm", "tangent"] diff --git a/crates/bevy_mikktspace/src/lib.rs b/crates/bevy_mikktspace/src/lib.rs index ee5f149a8c..f74e05098b 100644 --- a/crates/bevy_mikktspace/src/lib.rs +++ b/crates/bevy_mikktspace/src/lib.rs @@ -13,8 +13,8 @@ )] #![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_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 82642812b4..41a058b522 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_pbr" version = "0.16.0-dev" edition = "2024" description = "Adds PBR rendering to 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_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 12785f3e78..945bc9c55b 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -2,8 +2,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" )] extern crate alloc; diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index cb7aecc2cb..567bbce674 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -37,9 +37,9 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, + lifecycle::RemovedComponents, query::{Changed, Or}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs, system::{Query, Res, ResMut}, diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index e779dbc841..2592936ddd 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -551,7 +551,7 @@ pub(crate) fn add_light_view_entities( trigger: Trigger, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target()) { + if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { v.insert(LightViewEntities::default()); } } @@ -561,7 +561,7 @@ pub(crate) fn extracted_light_removed( trigger: Trigger, mut commands: Commands, ) { - if let Ok(mut v) = commands.get_entity(trigger.target()) { + if let Ok(mut v) = commands.get_entity(trigger.target().unwrap()) { v.try_remove::(); } } @@ -571,7 +571,7 @@ pub(crate) fn remove_light_view_entities( query: Query<&LightViewEntities>, mut commands: Commands, ) { - if let Ok(entities) = query.get(trigger.target()) { + if let Ok(entities) = query.get(trigger.target().unwrap()) { for v in entities.0.values() { for e in v.iter().copied() { if let Ok(mut v) = commands.get_entity(e) { diff --git a/crates/bevy_picking/Cargo.toml b/crates/bevy_picking/Cargo.toml index f02e5237aa..b42f0db14c 100644 --- a/crates/bevy_picking/Cargo.toml +++ b/crates/bevy_picking/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_picking" version = "0.16.0-dev" edition = "2024" description = "Provides screen picking functionality for Bevy Engine" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 9a5bc51bab..f9083433be 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -31,7 +31,7 @@ //! //! The events this module defines fall into a few broad categories: //! + Hovering and movement: [`Over`], [`Move`], and [`Out`]. -//! + Clicking and pressing: [`Pressed`], [`Released`], and [`Click`]. +//! + Clicking and pressing: [`Press`], [`Release`], and [`Click`]. //! + Dragging and dropping: [`DragStart`], [`Drag`], [`DragEnd`], [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. //! //! When received by an observer, these events will always be wrapped by the [`Pointer`] type, which contains @@ -171,7 +171,7 @@ pub struct Out { /// Fires when a pointer button is pressed over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Pressed { +pub struct Press { /// Pointer button pressed to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -181,7 +181,7 @@ pub struct Pressed { /// Fires when a pointer button is released over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Released { +pub struct Release { /// Pointer button lifted to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -208,6 +208,11 @@ pub struct Move { /// Information about the picking intersection. pub hit: HitData, /// The change in position since the last move event. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub delta: Vec2, } @@ -228,8 +233,18 @@ pub struct Drag { /// Pointer button pressed and moved to trigger this event. pub button: PointerButton, /// The total distance vector of a drag, measured from drag start to the current position. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub distance: Vec2, /// The change in position since the last drag event. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub delta: Vec2, } @@ -240,6 +255,11 @@ pub struct DragEnd { /// Pointer button pressed, moved, and released to trigger this event. pub button: PointerButton, /// The vector of drag movement measured from start to final pointer position. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using methods on [`Camera`](bevy_render::camera::Camera) to convert from screen-space to + /// world-space. pub distance: Vec2, } @@ -296,8 +316,20 @@ pub struct DragDrop { #[reflect(Clone, PartialEq)] pub struct DragEntry { /// The position of the pointer at drag start. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using [`Camera::viewport_to_world`](bevy_render::camera::Camera::viewport_to_world) or + /// [`Camera::viewport_to_world_2d`](bevy_render::camera::Camera::viewport_to_world_2d) to + /// convert from screen-space to world-space. pub start_pos: Vec2, /// The latest position of the pointer during this drag, used to compute deltas. + /// + /// This is stored in screen pixels, not world coordinates. Screen pixels go from top-left to + /// bottom-right, whereas (in 2D) world coordinates go from bottom-left to top-right. Consider + /// using [`Camera::viewport_to_world`](bevy_render::camera::Camera::viewport_to_world) or + /// [`Camera::viewport_to_world_2d`](bevy_render::camera::Camera::viewport_to_world_2d) to + /// convert from screen-space to world-space. pub latest_pos: Vec2, } @@ -368,7 +400,7 @@ impl PointerState { pub struct PickingEventWriters<'w> { cancel_events: EventWriter<'w, Pointer>, click_events: EventWriter<'w, Pointer>, - pressed_events: EventWriter<'w, Pointer>, + pressed_events: EventWriter<'w, Pointer>, drag_drop_events: EventWriter<'w, Pointer>, drag_end_events: EventWriter<'w, Pointer>, drag_enter_events: EventWriter<'w, Pointer>, @@ -380,7 +412,7 @@ pub struct PickingEventWriters<'w> { move_events: EventWriter<'w, Pointer>, out_events: EventWriter<'w, Pointer>, over_events: EventWriter<'w, Pointer>, - released_events: EventWriter<'w, Pointer>, + released_events: EventWriter<'w, Pointer>, } /// Dispatches interaction events to the target entities. @@ -390,7 +422,7 @@ pub struct PickingEventWriters<'w> { /// + [`DragEnter`] → [`Over`]. /// + Any number of any of the following: /// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`]. -/// + For each button press: [`Pressed`] or [`Click`] → [`Released`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. +/// + For each button press: [`Press`] or [`Click`] → [`Release`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. /// + For each pointer cancellation: [`Cancel`]. /// /// Additionally, across multiple frames, the following are also strictly @@ -398,7 +430,7 @@ pub struct PickingEventWriters<'w> { /// + When a pointer moves over the target: /// [`Over`], [`Move`], [`Out`]. /// + When a pointer presses buttons on the target: -/// [`Pressed`], [`Click`], [`Released`]. +/// [`Press`], [`Click`], [`Release`]. /// + When a pointer drags the target: /// [`DragStart`], [`Drag`], [`DragEnd`]. /// + When a pointer drags something over the target: @@ -420,7 +452,7 @@ pub struct PickingEventWriters<'w> { /// In the context of UI, this is especially problematic. Additional hierarchy-aware /// events will be added in a future release. /// -/// Both [`Click`] and [`Released`] target the entity hovered in the *previous frame*, +/// Both [`Click`] and [`Release`] target the entity hovered in the *previous frame*, /// rather than the current frame. This is because touch pointers hover nothing /// on the frame they are released. The end effect is that these two events can /// be received sequentially after an [`Out`] event (but always on the same frame @@ -577,7 +609,7 @@ pub fn pointer_events( pointer_id, location.clone(), hovered_entity, - Pressed { + Press { button, hit: hit.clone(), }, @@ -614,12 +646,12 @@ pub fn pointer_events( commands.trigger_targets(click_event.clone(), hovered_entity); event_writers.click_events.write(click_event); } - // Always send the Released event + // Always send the Release event let released_event = Pointer::new( pointer_id, location.clone(), hovered_entity, - Released { + Release { button, hit: hit.clone(), }, diff --git a/crates/bevy_picking/src/hover.rs b/crates/bevy_picking/src/hover.rs index 6347568c02..dbb6ee942e 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -14,7 +14,7 @@ use crate::{ }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::prelude::*; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_platform::collections::HashMap; use bevy_reflect::prelude::*; @@ -208,18 +208,6 @@ pub fn update_interactions( mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>, mut interact: Query<&mut PickingInteraction>, ) { - // Clear all previous hover data from pointers and entities - for (pointer, _, mut pointer_interaction) in &mut pointers { - pointer_interaction.sorted_entities.clear(); - if let Some(previously_hovered_entities) = previous_hover_map.get(pointer) { - for entity in previously_hovered_entities.keys() { - if let Ok(mut interaction) = interact.get_mut(*entity) { - *interaction = PickingInteraction::None; - } - } - } - } - // Create a map to hold the aggregated interaction for each entity. This is needed because we // need to be able to insert the interaction component on entities if they do not exist. To do // so we need to know the final aggregated interaction state to avoid the scenario where we set @@ -239,13 +227,29 @@ pub fn update_interactions( } // Take the aggregated entity states and update or insert the component if missing. - for (hovered_entity, new_interaction) in new_interaction_state.drain() { + for (&hovered_entity, &new_interaction) in new_interaction_state.iter() { if let Ok(mut interaction) = interact.get_mut(hovered_entity) { - *interaction = new_interaction; + interaction.set_if_neq(new_interaction); } else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) { entity_commands.try_insert(new_interaction); } } + + // Clear all previous hover data from pointers that are no longer hovering any entities. + // We do this last to preserve change detection for picking interactions. + for (pointer, _, _) in &mut pointers { + let Some(previously_hovered_entities) = previous_hover_map.get(pointer) else { + continue; + }; + + for entity in previously_hovered_entities.keys() { + if !new_interaction_state.contains_key(entity) { + if let Ok(mut interaction) = interact.get_mut(*entity) { + interaction.set_if_neq(PickingInteraction::None); + } + } + } + } } /// Merge the interaction state of this entity into the aggregated map. @@ -275,3 +279,285 @@ fn merge_interaction_states( new_interaction_state.insert(*hovered_entity, new_interaction); } } + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// enters or leaves an entity. Users should insert this component on an entity to indicate interest +/// in knowing about hover state changes. +/// +/// The component's boolean value will be `true` whenever the pointer is currently directly hovering +/// over the entity, or any of the entity's descendants (as defined by the [`ChildOf`] +/// relationship). This is consistent with the behavior of the CSS `:hover` pseudo-class, which +/// applies to the element and all of its descendants. +/// +/// The contained boolean value is guaranteed to only be mutated when the pointer enters or leaves +/// the entity, allowing Bevy change detection to be used efficiently. This is in contrast to the +/// [`HoverMap`] resource, which is updated every frame. +/// +/// Typically, a simple hoverable entity or widget will have this component added to it. More +/// complex widgets can have this component added to each hoverable part. +/// +/// The computational cost of keeping the `Hovered` components up to date is relatively cheap, and +/// linear in the number of entities that have the [`Hovered`] component inserted. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct Hovered(pub bool); + +impl Hovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// is directly hovering over an entity. Users should insert this component on an entity to indicate +/// interest in knowing about hover state changes. +/// +/// This is similar to [`Hovered`] component, except that it does not include descendants in the +/// hover state. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct DirectlyHovered(pub bool); + +impl DirectlyHovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// Uses [`HoverMap`] changes to update [`Hovered`] components. +pub fn update_is_hovered( + hover_map: Option>, + mut hovers: Query<(Entity, &Hovered)>, + parent_query: Query<&ChildOf>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + // Algorithm: for each entity having a `Hovered` component, we want to know if the current + // entry in the hover map is "within" (that is, in the set of descenants of) that entity. Rather + // than doing an expensive breadth-first traversal of children, instead start with the hovermap + // entry and search upwards. We can make this even cheaper by building a set of ancestors for + // the hovermap entry, and then testing each `Hovered` entity against that set. + + // A set which contains the hovered for the current pointer entity and its ancestors. The + // capacity is based on the likely tree depth of the hierarchy, which is typically greater for + // UI (because of layout issues) than for 3D scenes. A depth of 32 is a reasonable upper bound + // for most use cases. + let mut hover_ancestors = EntityHashSet::with_capacity(32); + if let Some(map) = hover_map.get(&PointerId::Mouse) { + for hovered_entity in map.keys() { + hover_ancestors.insert(*hovered_entity); + hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity)); + } + } + + // For each hovered entity, it is considered "hovering" if it's in the set of hovered ancestors. + for (entity, hoverable) in hovers.iter_mut() { + let is_hovering = hover_ancestors.contains(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(Hovered(is_hovering)); + } + } +} + +/// Uses [`HoverMap`] changes to update [`DirectlyHovered`] components. +pub fn update_is_directly_hovered( + hover_map: Option>, + hovers: Query<(Entity, &DirectlyHovered)>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + if let Some(map) = hover_map.get(&PointerId::Mouse) { + // It's hovering if it's in the HoverMap. + for (entity, hoverable) in hovers.iter() { + let is_hovering = map.contains_key(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(DirectlyHovered(is_hovering)); + } + } + } else { + // No hovered entity, reset all hovers. + for (entity, hoverable) in hovers.iter() { + if hoverable.0 { + commands.entity(entity).insert(DirectlyHovered(false)); + } + } + } +} + +#[cfg(test)] +mod tests { + use bevy_render::camera::Camera; + + use super::*; + + #[test] + fn update_is_hovered_memoized() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world.spawn(Hovered(false)).add_child(hovered_child).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_hovered).is_ok()); + + // Check to insure that the hovered entity has the Hovered component set to true + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_self() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_entity = world.spawn(DirectlyHovered(false)).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_entity, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the hovered entity has the DirectlyHovered component set to true + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_child() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world + .spawn(DirectlyHovered(false)) + .add_child(hovered_child) + .id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the DirectlyHovered component is still false + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 53387e84c8..6a7576f132 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -55,13 +55,13 @@ //! // Spawn your entity here, e.g. a Mesh. //! // When dragged, mutate the `Transform` component on the dragged target entity: //! .observe(|trigger: Trigger>, mut transforms: Query<&mut Transform>| { -//! let mut transform = transforms.get_mut(trigger.target()).unwrap(); +//! let mut transform = transforms.get_mut(trigger.target().unwrap()).unwrap(); //! let drag = trigger.event(); //! transform.rotate_local_y(drag.delta.x / 50.0); //! }) //! .observe(|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(); //! }) //! .observe(|trigger: Trigger>, mut events: EventWriter| { //! events.write(Greeting); @@ -170,6 +170,7 @@ pub mod window; use bevy_app::{prelude::*, PluginGroupBuilder}; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; +use hover::{update_is_directly_hovered, update_is_hovered}; /// The picking prelude. /// @@ -392,6 +393,7 @@ impl Plugin for PickingPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -414,7 +416,7 @@ impl Plugin for InteractionPlugin { .init_resource::() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_event::>() .add_event::>() @@ -425,11 +427,16 @@ impl Plugin for InteractionPlugin { .add_event::>() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_systems( PreUpdate, - (generate_hovermap, update_interactions, pointer_events) + ( + generate_hovermap, + update_interactions, + (update_is_hovered, update_is_directly_hovered), + pointer_events, + ) .chain() .in_set(PickingSystems::Hover), ); diff --git a/crates/bevy_platform/Cargo.toml b/crates/bevy_platform/Cargo.toml index 44c680394d..7a4313af39 100644 --- a/crates/bevy_platform/Cargo.toml +++ b/crates/bevy_platform/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_platform" version = "0.16.0-dev" edition = "2024" description = "Provides common platform agnostic APIs, as well as platform-specific features 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_platform/src/lib.rs b/crates/bevy_platform/src/lib.rs index d5871defb4..0dac76b011 100644 --- a/crates/bevy_platform/src/lib.rs +++ b/crates/bevy_platform/src/lib.rs @@ -1,13 +1,13 @@ #![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] //! Platform compatibility support for first-party [Bevy] engine crates. //! -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ cfg::std! { extern crate std; diff --git a/crates/bevy_ptr/Cargo.toml b/crates/bevy_ptr/Cargo.toml index 0f56880bd4..b6e72e24f0 100644 --- a/crates/bevy_ptr/Cargo.toml +++ b/crates/bevy_ptr/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_ptr" version = "0.16.0-dev" edition = "2024" description = "Utilities for working with untyped pointers in a more safe way" -homepage = "https://bevyengine.org" +homepage = "https://bevy.org" repository = "https://github.com/bevyengine/bevy" license = "MIT OR Apache-2.0" keywords = ["bevy", "no_std"] diff --git a/crates/bevy_ptr/src/lib.rs b/crates/bevy_ptr/src/lib.rs index 1580f3f926..704d60d675 100644 --- a/crates/bevy_ptr/src/lib.rs +++ b/crates/bevy_ptr/src/lib.rs @@ -3,8 +3,8 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![expect(unsafe_code, reason = "Raw pointers are inherently unsafe.")] #![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 core::{ diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 8fff0a331f..8827fc695b 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_reflect" version = "0.16.0-dev" edition = "2024" description = "Dynamically interact with rust types" -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_reflect/compile_fail/Cargo.toml b/crates/bevy_reflect/compile_fail/Cargo.toml index 178711c5d0..e3cb14ec2d 100644 --- a/crates/bevy_reflect/compile_fail/Cargo.toml +++ b/crates/bevy_reflect/compile_fail/Cargo.toml @@ -2,7 +2,7 @@ name = "bevy_reflect_compile_fail" edition = "2024" description = "Compile fail tests for Bevy Engine's reflection 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_reflect/derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml index ad6ec8cd2f..a3685941cc 100644 --- a/crates/bevy_reflect/derive/Cargo.toml +++ b/crates/bevy_reflect/derive/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_reflect_derive" version = "0.16.0-dev" edition = "2024" description = "Derive implementations for bevy_reflect" -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_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index ab2fcc6b15..0f399afd59 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -8,8 +8,8 @@ )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![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" )] //! Reflection in Rust. @@ -521,7 +521,7 @@ //! and displaying it in error messages. //! //! [Reflection]: https://en.wikipedia.org/wiki/Reflective_programming -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ //! [limitations]: #limitations //! [`bevy_reflect`]: crate //! [introspection]: https://en.wikipedia.org/wiki/Type_introspection diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 9eb3a3c281..04e4a2a4b0 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -135,34 +135,37 @@ where /// Applies a reflected value to this value. /// - /// If a type implements an [introspection subtrait], then the semantics of this + /// If `Self` implements a [reflection subtrait], then the semantics of this /// method are as follows: - /// - If `T` is a [`Struct`], then the value of each named field of `value` is + /// - If `Self` is a [`Struct`], then the value of each named field of `value` is /// applied to the corresponding named field of `self`. Fields which are /// not present in both structs are ignored. - /// - If `T` is a [`TupleStruct`] or [`Tuple`], then the value of each + /// - If `Self` is a [`TupleStruct`] or [`Tuple`], then the value of each /// numbered field is applied to the corresponding numbered field of /// `self.` Fields which are not present in both values are ignored. - /// - If `T` is an [`Enum`], then the variant of `self` is `updated` to match + /// - If `Self` is an [`Enum`], then the variant of `self` is `updated` to match /// the variant of `value`. The corresponding fields of that variant are /// applied from `value` onto `self`. Fields which are not present in both /// values are ignored. - /// - If `T` is a [`List`] or [`Array`], then each element of `value` is applied + /// - If `Self` is a [`List`] or [`Array`], then each element of `value` is applied /// to the corresponding element of `self`. Up to `self.len()` items are applied, /// and excess elements in `value` are appended to `self`. - /// - If `T` is a [`Map`], then for each key in `value`, the associated + /// - If `Self` is a [`Map`], then for each key in `value`, the associated /// value is applied to the value associated with the same key in `self`. /// Keys which are not present in `self` are inserted. - /// - If `T` is none of these, then `value` is downcast to `T`, cloned, and + /// - If `Self` is a [`Set`], then each element of `value` is applied to the corresponding + /// element of `Self`. If an element of `value` does not exist in `Self` then it is + /// cloned and inserted. + /// - If `Self` is none of these, then `value` is downcast to `Self`, cloned, and /// assigned to `self`. /// - /// Note that `Reflect` must be implemented manually for [`List`]s and - /// [`Map`]s in order to achieve the correct semantics, as derived + /// Note that `Reflect` must be implemented manually for [`List`]s, + /// [`Map`]s, and [`Set`]s in order to achieve the correct semantics, as derived /// implementations will have the semantics for [`Struct`], [`TupleStruct`], [`Enum`] - /// or none of the above depending on the kind of type. For lists and maps, use the - /// [`list_apply`] and [`map_apply`] helper functions when implementing this method. + /// or none of the above depending on the kind of type. For lists, maps, and sets, use the + /// [`list_apply`], [`map_apply`], and [`set_apply`] helper functions when implementing this method. /// - /// [introspection subtrait]: crate#the-introspection-subtraits + /// [reflection subtrait]: crate#the-reflection-subtraits /// [`Struct`]: crate::Struct /// [`TupleStruct`]: crate::TupleStruct /// [`Tuple`]: crate::Tuple @@ -170,17 +173,19 @@ where /// [`List`]: crate::List /// [`Array`]: crate::Array /// [`Map`]: crate::Map + /// [`Set`]: crate::Set /// [`list_apply`]: crate::list_apply /// [`map_apply`]: crate::map_apply + /// [`set_apply`]: crate::set_apply /// /// # Panics /// /// Derived implementations of this method will panic: - /// - If the type of `value` is not of the same kind as `T` (e.g. if `T` is + /// - If the type of `value` is not of the same kind as `Self` (e.g. if `Self` is /// a `List`, while `value` is a `Struct`). - /// - If `T` is any complex type and the corresponding fields or elements of + /// - If `Self` is any complex type and the corresponding fields or elements of /// `self` and `value` are not of the same type. - /// - If `T` is an opaque type and `self` cannot be downcast to `T` + /// - If `Self` is an opaque type and `value` cannot be downcast to `Self` fn apply(&mut self, value: &dyn PartialReflect) { PartialReflect::try_apply(self, value).unwrap(); } diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index 173555675f..ca84c2916e 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_remote" version = "0.16.0-dev" edition = "2024" description = "The Bevy Remote Protocol" -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_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 159ec47744..28bb506bc2 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -8,9 +8,9 @@ use bevy_ecs::{ entity::Entity, event::EventCursor, hierarchy::ChildOf, + lifecycle::RemovedComponentEntity, query::QueryBuilder, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, - removal_detection::RemovedComponentEntity, system::{In, Local}, world::{EntityRef, EntityWorldMut, FilteredEntityRef, World}, }; @@ -570,7 +570,8 @@ pub fn process_remote_get_watching_request( ); continue; }; - let Some(component_id) = world.components().get_id(type_registration.type_id()) else { + let Some(component_id) = world.components().get_valid_id(type_registration.type_id()) + else { let err = BrpError::component_error(format!("Unknown component: `{component_path}`")); if strict { return Err(err); @@ -1312,7 +1313,7 @@ fn get_component_ids( let type_id = type_registration.type_id(); world .components() - .get_id(type_id) + .get_valid_id(type_id) .map(|component_id| (type_id, component_id)) }); if let Some((type_id, component_id)) = maybe_component_tuple { diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 01f1e59861..d9775e9c8f 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_render" version = "0.16.0-dev" edition = "2024" description = "Provides rendering 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"] @@ -119,7 +119,7 @@ wesl = { version = "0.1.2", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. -naga_oil = { version = "0.17", default-features = false, features = [ +naga_oil = { version = "0.17.1", default-features = false, features = [ "test_shader", ] } @@ -127,7 +127,7 @@ naga_oil = { version = "0.17", default-features = false, features = [ proptest = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -naga_oil = "0.17" +naga_oil = "0.17.1" js-sys = "0.3" web-sys = { version = "0.3.67", features = [ 'Blob', diff --git a/crates/bevy_render/macros/Cargo.toml b/crates/bevy_render/macros/Cargo.toml index c3fc40b23e..74348598fe 100644 --- a/crates/bevy_render/macros/Cargo.toml +++ b/crates/bevy_render/macros/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_render_macros" version = "0.16.0-dev" edition = "2024" description = "Derive implementations for bevy_render" -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_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 2828486fd4..fd3b8cb4b2 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -22,9 +22,10 @@ use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, - component::{Component, HookContext}, + component::Component, entity::{ContainsEntity, Entity}, event::EventReader, + lifecycle::HookContext, prelude::With, query::Has, reflect::ReflectComponent, @@ -715,12 +716,13 @@ impl Camera { } } -/// Control how this camera outputs once rendering is completed. +/// Control how this [`Camera`] outputs once rendering is completed. #[derive(Debug, Clone, Copy)] pub enum CameraOutputMode { /// Writes the camera output to configured render target. Write { /// The blend state that will be used by the pipeline that writes the intermediate render textures to the final render target texture. + /// If not set, the output will be written as-is, ignoring `clear_color` and the existing data in the final render target texture. blend_state: Option, /// The clear color operation to perform on the final render target texture. clear_color: ClearColorConfig, @@ -1060,6 +1062,7 @@ pub fn camera_system( #[reflect(opaque)] #[reflect(Component, Default, Clone)] pub struct CameraMainTextureUsages(pub TextureUsages); + impl Default for CameraMainTextureUsages { fn default() -> Self { Self( @@ -1070,6 +1073,13 @@ impl Default for CameraMainTextureUsages { } } +impl CameraMainTextureUsages { + pub fn with(mut self, usages: TextureUsages) -> Self { + self.0 |= usages; + self + } +} + #[derive(Component, Debug)] pub struct ExtractedCamera { pub target: Option, @@ -1101,6 +1111,7 @@ pub fn extract_cameras( Option<&ColorGrading>, Option<&Exposure>, Option<&TemporalJitter>, + Option<&MipBias>, Option<&RenderLayers>, Option<&Projection>, Has, @@ -1123,6 +1134,7 @@ pub fn extract_cameras( color_grading, exposure, temporal_jitter, + mip_bias, render_layers, projection, no_indirect_drawing, @@ -1134,6 +1146,7 @@ pub fn extract_cameras( ExtractedView, RenderVisibleEntities, TemporalJitter, + MipBias, RenderLayers, Projection, NoIndirectDrawing, @@ -1220,14 +1233,26 @@ pub fn extract_cameras( if let Some(temporal_jitter) = temporal_jitter { commands.insert(temporal_jitter.clone()); + } else { + commands.remove::(); + } + + if let Some(mip_bias) = mip_bias { + commands.insert(mip_bias.clone()); + } else { + commands.remove::(); } if let Some(render_layers) = render_layers { commands.insert(render_layers.clone()); + } else { + commands.remove::(); } if let Some(perspective) = projection { commands.insert(perspective.clone()); + } else { + commands.remove::(); } if no_indirect_drawing @@ -1237,6 +1262,8 @@ pub fn extract_cameras( ) { commands.insert(NoIndirectDrawing); + } else { + commands.remove::(); } }; } @@ -1337,6 +1364,12 @@ impl TemporalJitter { /// Camera component specifying a mip bias to apply when sampling from material textures. /// /// Often used in conjunction with antialiasing post-process effects to reduce textures blurriness. -#[derive(Default, Component, Reflect)] +#[derive(Component, Reflect, Clone)] #[reflect(Default, Component)] pub struct MipBias(pub f32); + +impl Default for MipBias { + fn default() -> Self { + Self(-1.0) + } +} diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_render/src/camera/clear_color.rs index 157bcf8998..6183a1d4de 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_render/src/camera/clear_color.rs @@ -6,7 +6,9 @@ use bevy_reflect::prelude::*; use derive_more::derive::From; use serde::{Deserialize, Serialize}; -/// For a camera, specifies the color used to clear the viewport before rendering. +/// For a camera, specifies the color used to clear the viewport +/// [before rendering](crate::camera::Camera::clear_color) +/// or when [writing to the final render target texture](crate::camera::Camera::output_mode). #[derive(Reflect, Serialize, Deserialize, Copy, Clone, Debug, Default, From)] #[reflect(Serialize, Deserialize, Default, Clone)] pub enum ClearColorConfig { @@ -21,10 +23,15 @@ pub enum ClearColorConfig { None, } -/// A [`Resource`] that stores the color that is used to clear the screen between frames. +/// A [`Resource`] that stores the default color that cameras use to clear the screen between frames. /// /// This color appears as the "background" color for simple apps, /// when there are portions of the screen with nothing rendered. +/// +/// Individual cameras may use [`Camera.clear_color`] to specify a different +/// clear color or opt out of clearing their viewport. +/// +/// [`Camera.clear_color`]: crate::camera::Camera::clear_color #[derive(Resource, Clone, Debug, Deref, DerefMut, ExtractResource, Reflect)] #[reflect(Resource, Default, Debug, Clone)] pub struct ClearColor(pub Color); diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index d520990f93..a20315c099 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -9,8 +9,8 @@ )] #![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![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" )] #[cfg(target_pointer_width = "16")] diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index eb2d4de626..c171cf3957 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -78,6 +78,9 @@ pub struct MeshAllocator { /// WebGL 2. On this platform, we must give each vertex array its own /// buffer, because we can't adjust the first vertex when we perform a draw. general_vertex_slabs_supported: bool, + + /// Additional buffer usages to add to any vertex or index buffers created. + pub extra_buffer_usages: BufferUsages, } /// Tunable parameters that customize the behavior of the allocator. @@ -348,6 +351,7 @@ impl FromWorld for MeshAllocator { mesh_id_to_index_slab: HashMap::default(), next_slab_id: default(), general_vertex_slabs_supported, + extra_buffer_usages: BufferUsages::empty(), } } } @@ -598,7 +602,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: len as u64, - usage: buffer_usages | BufferUsages::COPY_DST, + usage: buffer_usages | BufferUsages::COPY_DST | self.extra_buffer_usages, mapped_at_creation: true, }); { @@ -835,7 +839,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: slab.current_slot_capacity as u64 * slab.element_layout.slot_size(), - usage: buffer_usages, + usage: buffer_usages | self.extra_buffer_usages, mapped_at_creation: false, }); diff --git a/crates/bevy_render/src/render_resource/bind_group_entries.rs b/crates/bevy_render/src/render_resource/bind_group_entries.rs index 3aaf46183f..cc8eb188de 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -147,6 +147,13 @@ impl<'a> IntoBinding<'a> for &'a TextureView { } } +impl<'a> IntoBinding<'a> for &'a wgpu::TextureView { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self) + } +} + impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -161,6 +168,13 @@ impl<'a> IntoBinding<'a> for &'a Sampler { } } +impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::SamplerArray(self) + } +} + impl<'a> IntoBinding<'a> for BindingResource<'a> { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -175,6 +189,13 @@ impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> { } } +impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::BufferArray(self) + } +} + pub trait IntoBindingArray<'b, const N: usize> { fn into_array(self) -> [BindingResource<'b>; N]; } diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index bc4a7d306d..41affa4349 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -568,4 +568,8 @@ pub mod binding_types { } .into_bind_group_layout_entry_builder() } + + pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder { + BindingType::AccelerationStructure.into_bind_group_layout_entry_builder() + } } diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index b777d96290..aecf27173d 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -38,18 +38,21 @@ pub use wgpu::{ BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, TextureDataOrder, }, - AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, BindGroupDescriptor, - BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, + AccelerationStructureFlags, AccelerationStructureGeometryFlags, + AccelerationStructureUpdateMode, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, + AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, Blas, BlasBuildEntry, BlasGeometries, + BlasGeometrySizeDescriptors, BlasTriangleGeometry, BlasTriangleGeometrySizeDescriptor, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, - DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face, Features as WgpuFeatures, - FilterMode, FragmentState as RawFragmentState, FrontFace, ImageSubresourceRange, IndexFormat, - Limits as WgpuLimits, LoadOp, Maintain, MapMode, MultisampleState, Operations, Origin3d, - PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, - PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, - RenderPassDepthStencilAttachment, RenderPassDescriptor, + CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, + Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, + FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, Maintain, MapMode, + MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, + PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, + RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, @@ -57,8 +60,9 @@ pub use wgpu::{ TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, - TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, - VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, + TextureViewDimension, Tlas, TlasInstance, TlasPackage, VertexAttribute, + VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, + VertexStepMode, COPY_BUFFER_ALIGNMENT, }; pub use crate::mesh::VertexBufferLayout; diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 81c693b01f..f2cfbcd9d0 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -163,7 +163,7 @@ pub async fn initialize_renderer( if adapter_info.device_type == DeviceType::Cpu { warn!( "The selected adapter is using a driver that only supports software rendering. \ - This is likely to be very slow. See https://bevyengine.org/learn/errors/b0006/" + This is likely to be very slow. See https://bevy.org/learn/errors/b0006/" ); } diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index f6c2f87593..dc7aadafbc 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,6 +1,7 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHash; +use bevy_ecs::lifecycle::{OnAdd, OnRemove}; use bevy_ecs::{ component::Component, entity::{ContainsEntity, Entity, EntityEquivalent}, @@ -9,7 +10,7 @@ use bevy_ecs::{ reflect::ReflectComponent, resource::Resource, system::{Local, Query, ResMut, SystemState}, - world::{Mut, OnAdd, OnRemove, World}, + world::{Mut, World}, }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -94,14 +95,14 @@ impl Plugin for SyncWorldPlugin { app.init_resource::(); app.add_observer( |trigger: Trigger, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target())); + pending.push(EntityRecord::Added(trigger.target().unwrap())); }, ); app.add_observer( |trigger: Trigger, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target()) { + if let Ok(e) = query.get(trigger.target().unwrap()) { pending.push(EntityRecord::Removed(*e)); }; }, @@ -219,10 +220,10 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl EntityRecord::Added(e) => { if let Ok(mut main_entity) = world.get_entity_mut(e) { match main_entity.entry::() { - bevy_ecs::world::Entry::Occupied(_) => { + bevy_ecs::world::ComponentEntry::Occupied(_) => { panic!("Attempting to synchronize an entity that has already been synchronized!"); } - bevy_ecs::world::Entry::Vacant(entry) => { + bevy_ecs::world::ComponentEntry::Vacant(entry) => { let id = render_world.spawn(MainEntity(e)).id(); entry.insert(RenderEntity(id)); @@ -490,10 +491,11 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, + lifecycle::{OnAdd, OnRemove}, observer::Trigger, query::With, system::{Query, ResMut}, - world::{OnAdd, OnRemove, World}, + world::World, }; use super::{ @@ -512,14 +514,14 @@ mod tests { main_world.add_observer( |trigger: Trigger, mut pending: ResMut| { - pending.push(EntityRecord::Added(trigger.target())); + pending.push(EntityRecord::Added(trigger.target().unwrap())); }, ); main_world.add_observer( |trigger: Trigger, mut pending: ResMut, query: Query<&RenderEntity>| { - if let Ok(e) = query.get(trigger.target()) { + if let Ok(e) = query.get(trigger.target().unwrap()) { pending.push(EntityRecord::Removed(*e)); }; }, diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 43533b5354..13b8ac74d4 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -3,8 +3,8 @@ mod render_layers; use core::any::TypeId; -use bevy_ecs::component::HookContext; use bevy_ecs::entity::EntityHashSet; +use bevy_ecs::lifecycle::HookContext; use bevy_ecs::world::DeferredWorld; use derive_more::derive::{Deref, DerefMut}; pub use range::*; diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 2559d3b8d2..80f89ce936 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -10,9 +10,9 @@ use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::{ component::Component, entity::{Entity, EntityHashMap}, + lifecycle::RemovedComponents, query::{Changed, With}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs as _, system::{Local, Query, Res, ResMut}, diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 3bb913c859..8a6fe517fd 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_scene" version = "0.16.0-dev" edition = "2024" description = "Provides scene 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_scene/src/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index 057f0afd34..c749af52ae 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -354,7 +354,7 @@ impl<'w> DynamicSceneBuilder<'w> { let original_world_dqf_id = self .original_world .components() - .get_resource_id(TypeId::of::()); + .get_valid_resource_id(TypeId::of::()); let type_registry = self.original_world.resource::().read(); diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index a507a58aaf..9b0845f80f 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/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" )] //! Provides scene definition, instantiation and serialization/deserialization. diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 1daa0158b3..456cb62225 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -721,7 +721,7 @@ mod tests { .expect("Failed to run dynamic scene builder system.") } - fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Entity) { + fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Option) { // Add observer app.world_mut().add_observer( move |trigger: Trigger, @@ -773,7 +773,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -792,7 +792,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -816,7 +816,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] @@ -840,7 +840,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 8fa5bae2cc..99d526b1e8 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_sprite" version = "0.16.0-dev" edition = "2024" description = "Provides sprite 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_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 771eb473fd..882ec5857c 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -2,8 +2,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" )] //! Provides 2D sprite rendering functionality. diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml index 654218fb28..fd780fc486 100644 --- a/crates/bevy_state/Cargo.toml +++ b/crates/bevy_state/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_state" version = "0.16.0-dev" edition = "2024" description = "Finite state machines for Bevy" -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_state/macros/src/lib.rs b/crates/bevy_state/macros/src/lib.rs index f461f0ead2..3c5a2d0674 100644 --- a/crates/bevy_state/macros/src/lib.rs +++ b/crates/bevy_state/macros/src/lib.rs @@ -1,6 +1,7 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +//! Macros for deriving `States` and `SubStates` traits. + extern crate proc_macro; mod states; @@ -8,11 +9,15 @@ mod states; use bevy_macro_utils::BevyManifest; use proc_macro::TokenStream; +/// Implements the `States` trait for a type - see the trait +/// docs for an example usage. #[proc_macro_derive(States, attributes(states))] pub fn derive_states(input: TokenStream) -> TokenStream { states::derive_states(input) } +/// Implements the `SubStates` trait for a type - see the trait +/// docs for an example usage. #[proc_macro_derive(SubStates, attributes(states, source))] pub fn derive_substates(input: TokenStream) -> TokenStream { states::derive_substates(input) diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 07c20b9750..ad162a7ef7 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_tasks" version = "0.16.0-dev" edition = "2024" description = "A task executor 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"] @@ -15,7 +15,12 @@ default = ["std", "async_executor"] ## Enables multi-threading support. ## Without this feature, all tasks will be run on a single thread. -multi_threaded = ["std", "dep:async-channel", "dep:concurrent-queue"] +multi_threaded = [ + "std", + "dep:async-channel", + "dep:concurrent-queue", + "async_executor", +] ## Uses `async-executor` as a task execution backend. ## This backend is incompatible with `no_std` targets. diff --git a/crates/bevy_tasks/README.md b/crates/bevy_tasks/README.md index b03d2fcf97..04815df35e 100644 --- a/crates/bevy_tasks/README.md +++ b/crates/bevy_tasks/README.md @@ -38,6 +38,6 @@ The determining factor for what kind of work should go in each pool is latency r To enable `no_std` support in this crate, you will need to disable default features, and enable the `edge_executor` and `critical-section` features. -[bevy]: https://bevyengine.org +[bevy]: https://bevy.org [rayon]: https://github.com/rayon-rs/rayon [async-executor]: https://github.com/stjepang/async-executor diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index ae684a4eb5..ce9aa78883 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -1,8 +1,8 @@ #![doc = include_str!("../README.md")] #![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_text/Cargo.toml b/crates/bevy_text/Cargo.toml index 5134ecda84..9a8c8e8eee 100644 --- a/crates/bevy_text/Cargo.toml +++ b/crates/bevy_text/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_text" version = "0.16.0-dev" edition = "2024" description = "Provides text 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_text/src/bounds.rs b/crates/bevy_text/src/bounds.rs index db2ceb0b28..1c0833b443 100644 --- a/crates/bevy_text/src/bounds.rs +++ b/crates/bevy_text/src/bounds.rs @@ -5,7 +5,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// The maximum width and height of text. The text will wrap according to the specified size. /// /// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the -/// specified [`JustifyText`](crate::text::JustifyText). +/// specified [`Justify`](crate::text::Justify). /// /// Note: only characters that are completely out of the bounds will be truncated, so this is not a /// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 2bc74a1aa7..b36f5fa2bb 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -61,7 +61,7 @@ pub use text_access::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, JustifyText, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, + Font, Justify, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, TextFont, TextLayout, TextSpan, }; } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 50b983f929..dd6ca77246 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -16,8 +16,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText, - LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, + error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak, + PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -88,7 +88,7 @@ impl TextPipeline { fonts: &Assets, text_spans: impl Iterator, linebreak: LineBreak, - justify: JustifyText, + justify: Justify, bounds: TextBounds, scale_factor: f64, computed: &mut ComputedTextBlock, @@ -201,7 +201,7 @@ impl TextPipeline { // Workaround for alignment not working for unbounded text. // See https://github.com/pop-os/cosmic-text/issues/343 - if bounds.width.is_none() && justify != JustifyText::Left { + if bounds.width.is_none() && justify != Justify::Left { let dimensions = buffer_dimensions(buffer); // `set_size` causes a re-layout to occur. buffer.set_size(font_system, Some(dimensions.x), bounds.height); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index e9e78e3ed2..ccfdb2a372 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -1,8 +1,3 @@ -pub use cosmic_text::{ - self, FamilyOwned as FontFamily, Stretch as FontStretch, Style as FontStyle, - Weight as FontWeight, -}; - use crate::{Font, TextLayoutInfo, TextSpanAccess, TextSpanComponent}; use bevy_asset::Handle; use bevy_color::Color; @@ -121,19 +116,19 @@ impl Default for ComputedTextBlock { pub struct TextLayout { /// The text's internal alignment. /// Should not affect its position within a container. - pub justify: JustifyText, + pub justify: Justify, /// How the text should linebreak when running out of the bounds determined by `max_size`. pub linebreak: LineBreak, } impl TextLayout { /// Makes a new [`TextLayout`]. - pub const fn new(justify: JustifyText, linebreak: LineBreak) -> Self { + pub const fn new(justify: Justify, linebreak: LineBreak) -> Self { Self { justify, linebreak } } - /// Makes a new [`TextLayout`] with the specified [`JustifyText`]. - pub fn new_with_justify(justify: JustifyText) -> Self { + /// Makes a new [`TextLayout`] with the specified [`Justify`]. + pub fn new_with_justify(justify: Justify) -> Self { Self::default().with_justify(justify) } @@ -148,8 +143,8 @@ impl TextLayout { Self::default().with_no_wrap() } - /// Returns this [`TextLayout`] with the specified [`JustifyText`]. - pub const fn with_justify(mut self, justify: JustifyText) -> Self { + /// Returns this [`TextLayout`] with the specified [`Justify`]. + pub const fn with_justify(mut self, justify: Justify) -> Self { self.justify = justify; self } @@ -251,7 +246,7 @@ impl From for TextSpan { /// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)] -pub enum JustifyText { +pub enum Justify { /// Leftmost character is immediately to the right of the render position. /// Bounds start from the render position and advance rightwards. #[default] @@ -268,13 +263,13 @@ pub enum JustifyText { Justified, } -impl From for cosmic_text::Align { - fn from(justify: JustifyText) -> Self { +impl From for cosmic_text::Align { + fn from(justify: Justify) -> Self { match justify { - JustifyText::Left => cosmic_text::Align::Left, - JustifyText::Center => cosmic_text::Align::Center, - JustifyText::Right => cosmic_text::Align::Right, - JustifyText::Justified => cosmic_text::Align::Justified, + Justify::Left => cosmic_text::Align::Left, + Justify::Center => cosmic_text::Align::Center, + Justify::Right => cosmic_text::Align::Right, + Justify::Justified => cosmic_text::Align::Justified, } } } @@ -359,8 +354,8 @@ impl Default for TextFont { /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size -#[derive(Debug, Clone, Copy, Reflect)] -#[reflect(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(Debug, Clone, PartialEq)] pub enum LineHeight { /// Set line height to a specific number of pixels Px(f32), diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5069804df8..8d4a926e1b 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -51,7 +51,7 @@ use bevy_window::{PrimaryWindow, Window}; /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, Text2d, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, Text2d, TextLayout, TextFont, TextColor, TextSpan}; /// # /// # let font_handle: Handle = Default::default(); /// # let mut world = World::default(); @@ -73,7 +73,7 @@ use bevy_window::{PrimaryWindow, Window}; /// // With text justification. /// world.spawn(( /// Text2d::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans diff --git a/crates/bevy_time/Cargo.toml b/crates/bevy_time/Cargo.toml index 520782b519..494725b08c 100644 --- a/crates/bevy_time/Cargo.toml +++ b/crates/bevy_time/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_time" version = "0.16.0-dev" edition = "2024" description = "Provides time 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_time/src/lib.rs b/crates/bevy_time/src/lib.rs index 858da0d7e4..6e7b3b3991 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -2,8 +2,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] diff --git a/crates/bevy_transform/Cargo.toml b/crates/bevy_transform/Cargo.toml index 8d5ca38e30..6801f5734b 100644 --- a/crates/bevy_transform/Cargo.toml +++ b/crates/bevy_transform/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_transform" version = "0.16.0-dev" edition = "2024" description = "Provides transform 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_transform/src/lib.rs b/crates/bevy_transform/src/lib.rs index 4d107346f7..0e29231256 100644 --- a/crates/bevy_transform/src/lib.rs +++ b/crates/bevy_transform/src/lib.rs @@ -1,8 +1,8 @@ #![doc = include_str!("../README.md")] #![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_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index da68c63cfe..5224d90296 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_ui" version = "0.16.0-dev" edition = "2024" description = "A custom ECS-driven UI framework built specifically 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_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 637ed943f2..78e80717a2 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -1,6 +1,7 @@ use crate::{ experimental::UiChildren, prelude::{Button, Label}, + ui_transform::UiGlobalTransform, widget::{ImageNode, TextUiReader}, ComputedNode, }; @@ -13,11 +14,9 @@ use bevy_ecs::{ system::{Commands, Query}, world::Ref, }; -use bevy_math::Vec3Swizzles; -use bevy_render::camera::CameraUpdateSystems; -use bevy_transform::prelude::GlobalTransform; use accesskit::{Node, Rect, Role}; +use bevy_render::camera::CameraUpdateSystems; fn calc_label( text_reader: &mut TextUiReader, @@ -40,12 +39,12 @@ fn calc_bounds( mut nodes: Query<( &mut AccessibilityNode, Ref, - Ref, + Ref, )>, ) { for (mut accessible, node, transform) in &mut nodes { if node.is_changed() || transform.is_changed() { - let center = transform.translation().xy(); + let center = transform.translation; let half_size = 0.5 * node.size; let min = center - half_size; let max = center + half_size; diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 9242bf1380..f55cbb92b8 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,18 +1,21 @@ -use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack}; +use crate::{ + picking_backend::clip_check_recursive, ui_transform::UiGlobalTransform, ComputedNode, + ComputedNodeTarget, Node, UiStack, +}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, + hierarchy::ChildOf, prelude::{Component, With}, query::QueryData, reflect::ReflectComponent, system::{Local, Query, Res}, }; use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility}; -use bevy_transform::components::GlobalTransform; use bevy_window::{PrimaryWindow, Window}; use smallvec::SmallVec; @@ -67,12 +70,12 @@ impl Default for Interaction { } } -/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right -/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.) +/// A component storing the position of the mouse relative to the node, (0., 0.) being the center and (0.5, 0.5) being the bottom-right +/// If the mouse is not over the node, the value will go beyond the range of (-0.5, -0.5) to (0.5, 0.5) /// /// It can be used alongside [`Interaction`] to get the position of the press. /// -/// The component is updated when it is in the same entity with [`Node`](crate::Node). +/// The component is updated when it is in the same entity with [`Node`]. #[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( @@ -81,8 +84,8 @@ impl Default for Interaction { reflect(Serialize, Deserialize) )] pub struct RelativeCursorPosition { - /// Visible area of the Node relative to the size of the entire Node. - pub normalized_visible_node_rect: Rect, + /// True if the cursor position is over an unclipped area of the Node. + pub cursor_over: bool, /// Cursor position relative to the size and position of the Node. /// A None value indicates that the cursor position is unknown. pub normalized: Option, @@ -90,9 +93,8 @@ pub struct RelativeCursorPosition { impl RelativeCursorPosition { /// A helper function to check if the mouse is over the node - pub fn mouse_over(&self) -> bool { - self.normalized - .is_some_and(|position| self.normalized_visible_node_rect.contains(position)) + pub fn cursor_over(&self) -> bool { + self.cursor_over } } @@ -133,11 +135,10 @@ pub struct State { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, + transform: &'static UiGlobalTransform, interaction: Option<&'static mut Interaction>, relative_cursor_position: Option<&'static mut RelativeCursorPosition>, focus_policy: Option<&'static FocusPolicy>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -154,6 +155,8 @@ pub fn ui_focus_system( touches_input: Res, ui_stack: Res, mut node_query: Query, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: Query<&ChildOf>, ) { let primary_window = primary_window.iter().next(); @@ -234,46 +237,30 @@ pub fn ui_focus_system( } let camera_entity = node.target_camera.camera()?; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); - - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); - let cursor_position = camera_cursor_positions.get(&camera_entity); + let contains_cursor = cursor_position.is_some_and(|point| { + node.node.contains_point(*node.transform, *point) + && clip_check_recursive(*point, *entity, &clipping_query, &child_of_query) + }); + // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // (-0.5, -0.5) is the top-left corner, (0.5, 0.5) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. - let relative_cursor_position = cursor_position.and_then(|cursor_position| { + let normalized_cursor_position = cursor_position.and_then(|cursor_position| { // ensure node size is non-zero in all dimensions, otherwise relative position will be // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to // false positives for mouse_over (#12395) - (node_rect.size().cmpgt(Vec2::ZERO).all()) - .then_some((*cursor_position - node_rect.min) / node_rect.size()) + node.node.normalize_point(*node.transform, *cursor_position) }); // If the current cursor position is within the bounds of the node's visible area, consider it for // clicking let relative_cursor_position_component = RelativeCursorPosition { - normalized_visible_node_rect: visible_rect.normalize(node_rect), - normalized: relative_cursor_position, + cursor_over: contains_cursor, + normalized: normalized_cursor_position, }; - let contains_cursor = relative_cursor_position_component.mouse_over() - && cursor_position.is_some_and(|point| { - pick_rounded_rect( - *point - node_rect.center(), - node_rect.size(), - node.node.border_radius, - ) - }); - // Save the relative cursor position to the correct component if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position { @@ -284,7 +271,8 @@ pub fn ui_focus_system( Some(*entity) } else { if let Some(mut interaction) = node.interaction { - if *interaction == Interaction::Hovered || (relative_cursor_position.is_none()) + if *interaction == Interaction::Hovered + || (normalized_cursor_position.is_none()) { interaction.set_if_neq(Interaction::None); } @@ -334,26 +322,3 @@ pub fn ui_focus_system( } } } - -// Returns true if `point` (relative to the rectangle's center) is within the bounds of a rounded rectangle with -// the given size and border radius. -// -// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. -pub(crate) fn pick_rounded_rect( - point: Vec2, - size: Vec2, - border_radius: ResolvedBorderRadius, -) -> bool { - let [top, bottom] = if point.x < 0. { - [border_radius.top_left, border_radius.bottom_left] - } else { - [border_radius.top_right, border_radius.bottom_right] - }; - let r = if point.y < 0. { top } else { bottom }; - - let corner_to_point = point.abs() - 0.5 * size; - let q = corner_to_point + r; - let l = q.max(Vec2::ZERO).length(); - let m = q.max_element().min(0.); - l + m - r < 0. -} diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs new file mode 100644 index 0000000000..f91cdaee59 --- /dev/null +++ b/crates/bevy_ui/src/interaction_states.rs @@ -0,0 +1,74 @@ +/// This module contains components that are used to track the interaction state of UI widgets. +use bevy_a11y::AccessibilityNode; +use bevy_ecs::{ + component::Component, + lifecycle::{OnAdd, OnInsert, OnRemove}, + observer::Trigger, + world::DeferredWorld, +}; + +/// A component indicating that a widget is disabled and should be "grayed out". +/// This is used to prevent user interaction with the widget. It should not, however, prevent +/// the widget from being updated or rendered, or from acquiring keyboard focus. +/// +/// For apps which support a11y: if a widget (such as a slider) contains multiple entities, +/// the `InteractionDisabled` component should be added to the root entity of the widget - the +/// same entity that contains the `AccessibilityNode` component. This will ensure that +/// the a11y tree is updated correctly. +#[derive(Component, Debug, Clone, Copy, Default)] +pub struct InteractionDisabled; + +pub(crate) fn on_add_disabled( + trigger: Trigger, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_disabled(); + } +} + +pub(crate) fn on_remove_disabled( + trigger: Trigger, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_disabled(); + } +} + +/// Component that indicates whether a button or widget is currently in a pressed or "held down" +/// state. +#[derive(Component, Default, Debug)] +pub struct Pressed; + +/// Component that indicates whether a checkbox or radio button is in a checked state. +#[derive(Component, Default, Debug)] +#[component(immutable)] +pub struct Checked(pub bool); + +impl Checked { + /// Returns whether the checkbox or radio button is currently checked. + pub fn get(&self) -> bool { + self.0 + } +} + +pub(crate) fn on_insert_is_checked(trigger: Trigger, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + let checked = entity.get::().unwrap().get(); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(match checked { + true => accesskit::Toggled::True, + false => accesskit::Toggled::False, + }); + } +} + +pub(crate) fn on_remove_is_checked(trigger: Trigger, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target().unwrap()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::False); + } +} diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 079e73bb49..53c03113b9 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -448,6 +448,8 @@ impl RepeatedGridTrack { #[cfg(test)] mod tests { + use bevy_math::Vec2; + use super::*; #[test] @@ -523,7 +525,7 @@ mod tests { grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), }; - let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.)); + let viewport_values = LayoutContext::new(1.0, Vec2::new(800., 600.)); let taffy_style = from_node(&node, &viewport_values, false); assert_eq!(taffy_style.display, taffy::style::Display::Flex); assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox); @@ -661,7 +663,7 @@ mod tests { #[test] fn test_into_length_percentage() { use taffy::style::LengthPercentage; - let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.)); + let context = LayoutContext::new(2.0, Vec2::new(800., 600.)); let cases = [ (Val::Auto, LengthPercentage::Length(0.)), (Val::Percent(1.), LengthPercentage::Percent(0.01)), diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index b38241a95a..484acbd4af 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,5 +1,6 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::{UiGlobalTransform, UiTransform}, BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node, Outline, OverflowAxis, ScrollPosition, }; @@ -7,14 +8,14 @@ use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, entity::Entity, hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, query::With, - removal_detection::RemovedComponents, system::{Commands, Query, ResMut}, world::Ref, }; -use bevy_math::Vec2; + +use bevy_math::{Affine2, Vec2}; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use thiserror::Error; use tracing::warn; use ui_surface::UiSurface; @@ -81,9 +82,10 @@ pub fn ui_layout_system( )>, computed_node_query: Query<(Entity, Option>), With>, ui_children: UiChildren, - mut node_transform_query: Query<( + mut node_update_query: Query<( &mut ComputedNode, - &mut Transform, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -175,7 +177,8 @@ with UI components as a child of an entity without UI components, your UI layout &mut ui_surface, true, computed_target.physical_size().as_vec2(), - &mut node_transform_query, + Affine2::IDENTITY, + &mut node_update_query, &ui_children, computed_target.scale_factor.recip(), Vec2::ZERO, @@ -190,9 +193,11 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface: &mut UiSurface, inherited_use_rounding: bool, target_size: Vec2, - node_transform_query: &mut Query<( + mut inherited_transform: Affine2, + node_update_query: &mut Query<( &mut ComputedNode, - &mut Transform, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -206,13 +211,14 @@ with UI components as a child of an entity without UI components, your UI layout ) { if let Ok(( mut node, - mut transform, + transform, + mut global_transform, style, maybe_layout_config, maybe_border_radius, maybe_outline, maybe_scroll_position, - )) = node_transform_query.get_mut(entity) + )) = node_update_query.get_mut(entity) { let use_rounding = maybe_layout_config .map(|layout_config| layout_config.use_rounding) @@ -224,10 +230,11 @@ with UI components as a child of an entity without UI components, your UI layout let layout_size = Vec2::new(layout.size.width, layout.size.height); + // Taffy layout position of the top-left corner of the node, relative to its parent. let layout_location = Vec2::new(layout.location.x, layout.location.y); - // The position of the center of the node, stored in the node's transform - let node_center = + // The position of the center of the node relative to its top-left corner. + let local_center = layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size); // only trigger change detection when the new values are different @@ -253,6 +260,16 @@ with UI components as a child of an entity without UI components, your UI layout node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); + // Computer the node's new global transform + let mut local_transform = + transform.compute_affine(inverse_target_scale_factor, layout_size, target_size); + local_transform.translation += local_center; + inherited_transform *= local_transform; + + if inherited_transform != **global_transform { + *global_transform = inherited_transform.into(); + } + if let Some(border_radius) = maybe_border_radius { // We don't trigger change detection for changes to border radius node.bypass_change_detection().border_radius = border_radius.resolve( @@ -290,10 +307,6 @@ with UI components as a child of an entity without UI components, your UI layout .max(0.); } - if transform.translation.truncate() != node_center { - transform.translation = node_center.extend(0.); - } - let scroll_position: Vec2 = maybe_scroll_position .map(|scroll_pos| { Vec2::new( @@ -333,7 +346,8 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface, use_rounding, target_size, - node_transform_query, + inherited_transform, + node_update_query, ui_children, inverse_target_scale_factor, layout_size, @@ -356,10 +370,7 @@ mod tests { use bevy_platform::collections::HashMap; use bevy_render::{camera::ManualTextureViews, prelude::Camera}; use bevy_transform::systems::mark_dirty_trees; - use bevy_transform::{ - prelude::GlobalTransform, - systems::{propagate_parent_transforms, sync_simple_transforms}, - }; + use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms}; use bevy_utils::prelude::default; use bevy_window::{ PrimaryWindow, Window, WindowCreated, WindowResized, WindowResolution, @@ -684,23 +695,20 @@ mod tests { ui_schedule.run(&mut world); let overlap_check = world - .query_filtered::<(Entity, &ComputedNode, &GlobalTransform), Without>() + .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without>() .iter(&world) .fold( Option::<(Rect, bool)>::None, - |option_rect, (entity, node, global_transform)| { - let current_rect = Rect::from_center_size( - global_transform.translation().truncate(), - node.size(), - ); + |option_rect, (entity, node, transform)| { + let current_rect = Rect::from_center_size(transform.translation, node.size()); assert!( current_rect.height().abs() + current_rect.width().abs() > 0., "root ui node {entity} doesn't have a logical size" ); assert_ne!( - global_transform.affine(), - GlobalTransform::default().affine(), - "root ui node {entity} global transform is not populated" + *transform, + UiGlobalTransform::default(), + "root ui node {entity} transform is not populated" ); let Some((rect, is_overlapping)) = option_rect else { return Some((current_rect, false)); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index a216c4220b..ac70897d06 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -1,8 +1,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" )] //! This crate contains Bevy's UI system, which can be used to create UI for both 2D and 3D games @@ -10,6 +10,7 @@ //! Spawn UI elements with [`widget::Button`], [`ImageNode`], [`Text`](prelude::Text) and [`Node`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) +pub mod interaction_states; pub mod measurement; pub mod ui_material; pub mod update; @@ -18,6 +19,7 @@ pub mod widget; pub mod gradients; #[cfg(feature = "bevy_ui_picking_backend")] pub mod picking_backend; +pub mod ui_transform; use bevy_derive::{Deref, DerefMut}; #[cfg(feature = "bevy_ui_picking_backend")] @@ -37,11 +39,13 @@ mod ui_node; pub use focus::*; pub use geometry::*; pub use gradients::*; +pub use interaction_states::{Checked, InteractionDisabled, Pressed}; pub use layout::*; pub use measurement::*; pub use render::*; pub use ui_material::*; pub use ui_node::*; +pub use ui_transform::*; use widget::{ImageNode, ImageNodeSize, ViewportNode}; @@ -64,6 +68,7 @@ pub mod prelude { gradients::*, ui_material::*, ui_node::*, + ui_transform::*, widget::{Button, ImageNode, Label, NodeImageMode, ViewportNode}, Interaction, MaterialNode, UiMaterialPlugin, UiScale, }, @@ -316,6 +321,11 @@ fn build_text_interop(app: &mut App) { app.add_plugins(accessibility::AccessibilityPlugin); + app.add_observer(interaction_states::on_add_disabled) + .add_observer(interaction_states::on_remove_disabled) + .add_observer(interaction_states::on_insert_is_checked) + .add_observer(interaction_states::on_remove_is_checked); + app.configure_sets( PostUpdate, AmbiguousWithText.ambiguous_with(widget::text_system), diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 26b84c6005..5647baee12 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,14 +24,13 @@ #![deny(missing_docs)] -use crate::{focus::pick_rounded_rect, prelude::*, UiStack}; +use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::prelude::*; -use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; use bevy_picking::backend::prelude::*; @@ -91,9 +90,8 @@ impl Plugin for UiPickingPlugin { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, + transform: &'static UiGlobalTransform, pickable: Option<&'static Pickable>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -110,6 +108,8 @@ pub fn ui_picking( ui_stack: Res, node_query: Query, mut output: EventWriter, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: Query<&ChildOf>, ) { // For each camera, the pointer and its position let mut pointer_pos_by_camera = HashMap::>::default(); @@ -181,43 +181,33 @@ pub fn ui_picking( continue; }; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); - // Nodes with Display::None have a (0., 0.) logical rect and can be ignored - if node_rect.size() == Vec2::ZERO { + if node.node.size() == Vec2::ZERO { continue; } - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); - let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); - // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // Find the normalized cursor position relative to the node. + // (±0., 0.) is the center with the corners at points (±0.5, ±0.5). // Coordinates are relative to the entire node, not just the visible region. for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { - let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size(); - - if visible_rect - .normalize(node_rect) - .contains(relative_cursor_position) - && pick_rounded_rect( - *cursor_position - node_rect.center(), - node_rect.size(), - node.node.border_radius, + if node.node.contains_point(*node.transform, *cursor_position) + && clip_check_recursive( + *cursor_position, + *node_entity, + &clipping_query, + &child_of_query, ) { hit_nodes .entry((camera_entity, *pointer_id)) .or_default() - .push((*node_entity, relative_cursor_position)); + .push(( + *node_entity, + node.transform.inverse().transform_point2(*cursor_position) + / node.node.size(), + )); } } } @@ -262,3 +252,27 @@ pub fn ui_picking( output.write(PointerHits::new(*pointer, picks, order)); } } + +/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. +pub fn clip_check_recursive( + point: Vec2, + entity: Entity, + clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: &Query<&ChildOf>, +) -> bool { + if let Ok(child_of) = child_of_query.get(entity) { + let parent = child_of.0; + if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { + if !computed_node + .resolve_clip_rect(node.overflow, node.overflow_clip_margin) + .contains(transform.inverse().transform_point2(point)) + { + // The point is clipped and should be ignored by picking + return false; + } + } + return clip_check_recursive(point, parent, clipping_query, child_of_query); + } + // Reached root, point unclipped by all ancestors + true +} diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 1c2b2c7d0a..b6f3f3501e 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -2,6 +2,7 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::{ BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems, ResolvedBorderRadius, TransparentUi, Val, @@ -18,7 +19,7 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2}; use bevy_render::sync_world::MainEntity; use bevy_render::RenderApp; use bevy_render::{ @@ -29,7 +30,6 @@ use bevy_render::{ view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; @@ -211,7 +211,7 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { /// Description of a shadow to be sorted and queued for rendering pub struct ExtractedBoxShadow { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub bounds: Vec2, pub clip: Option, pub extracted_camera_entity: Entity, @@ -236,7 +236,7 @@ pub fn extract_shadows( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, &BoxShadow, Option<&CalculatedClip>, @@ -302,7 +302,7 @@ pub fn extract_shadows( extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)), + transform: Affine2::from(transform) * Affine2::from_translation(offset), color: drop_shadow.color.into(), bounds: shadow_size + 6. * blur_radius, clip: clip.map(|clip| clip.clip), @@ -405,11 +405,15 @@ pub fn prepare_shadows( .get(item.index) .filter(|n| item.entity() == n.render_entity) { - let rect_size = box_shadow.bounds.extend(1.0); + let rect_size = box_shadow.bounds; // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + box_shadow + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -443,7 +447,7 @@ pub fn prepare_shadows( positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = box_shadow.transform.transform_vector3(rect_size); + let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -492,7 +496,7 @@ pub fn prepare_shadows( size: box_shadow.size.into(), radius, blur: box_shadow.blur_radius, - bounds: rect_size.xy().into(), + bounds: rect_size.into(), }); } diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index aa9440b8d8..c3ada22c2e 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -1,5 +1,6 @@ use crate::shader_flags; use crate::ui_node::ComputedNodeTarget; +use crate::ui_transform::UiGlobalTransform; use crate::CalculatedClip; use crate::ComputedNode; use bevy_asset::AssetId; @@ -16,7 +17,6 @@ use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::view::InheritedVisibility; use bevy_render::Extract; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; use super::ExtractedUiItem; use super::ExtractedUiNode; @@ -62,9 +62,9 @@ pub fn extract_debug_overlay( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - &GlobalTransform, &ComputedNodeTarget, )>, >, @@ -76,7 +76,7 @@ pub fn extract_debug_overlay( let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, visibility, maybe_clip, transform, computed_target) in &uinode_query { + for (entity, uinode, transform, visibility, maybe_clip, computed_target) in &uinode_query { if !debug_options.show_hidden && !visibility.get() { continue; } @@ -102,7 +102,7 @@ pub fn extract_debug_overlay( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index 31369899f7..bd818c7d5b 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -17,8 +17,9 @@ use bevy_ecs::{ use bevy_image::prelude::*; use bevy_math::{ ops::{cos, sin}, - FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles, + FloatOrd, Rect, Vec2, }; +use bevy_math::{Affine2, Vec2Swizzles}; use bevy_render::sync_world::MainEntity; use bevy_render::{ render_phase::*, @@ -29,7 +30,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::shader_flags::BORDER_ALL; @@ -238,7 +238,7 @@ pub enum ResolvedGradient { pub struct ExtractedGradient { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub clip: Option, pub extracted_camera_entity: Entity, @@ -354,7 +354,7 @@ pub fn extract_gradients( Entity, &ComputedNode, &ComputedNodeTarget, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, AnyOf<(&BackgroundGradient, &BorderGradient)>, @@ -414,7 +414,7 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, node_type, - transform: transform.compute_matrix(), + transform: transform.into(), }, main_entity: entity.into(), render_entity: commands.spawn(TemporaryRenderEntity).id(), @@ -439,7 +439,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -487,7 +487,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -541,7 +541,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -675,12 +675,16 @@ pub fn prepare_gradient( *item.batch_range_mut() = item_index as u32..item_index as u32 + 1; let uinode_rect = gradient.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (gradient.transform * (pos * rect_size).extend(1.)).xyz()); - let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + gradient + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); + let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -721,7 +725,7 @@ pub fn prepare_gradient( corner_points[3] + positions_diff[3], ]; - let transformed_rect_size = gradient.transform.transform_vector3(rect_size); + let transformed_rect_size = gradient.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 83140d0f3b..61319eda9b 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -8,7 +8,9 @@ pub mod ui_texture_slice_pipeline; mod debug_overlay; mod gradient; +use crate::prelude::UiGlobalTransform; use crate::widget::{ImageNode, ViewportNode}; + use crate::{ BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, @@ -22,7 +24,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::prelude::*; use bevy_ecs::system::SystemParam; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2}; use bevy_render::load_shader_library; use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_phase::ViewSortedRenderPhases; @@ -243,7 +245,7 @@ pub enum ExtractedUiItem { /// Ordering: left, top, right, bottom. border: BorderRect, node_type: NodeType, - transform: Mat4, + transform: Affine2, }, /// A contiguous sequence of text glyphs from the same section Glyphs { @@ -253,7 +255,7 @@ pub enum ExtractedUiItem { } pub struct ExtractedGlyph { - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, } @@ -344,7 +346,7 @@ pub fn extract_uinode_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -383,7 +385,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -403,7 +405,7 @@ pub fn extract_uinode_images( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -467,7 +469,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -487,7 +489,7 @@ pub fn extract_uinode_borders( Entity, &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -503,7 +505,7 @@ pub fn extract_uinode_borders( entity, node, computed_node, - global_transform, + transform, inherited_visibility, maybe_clip, camera, @@ -567,7 +569,7 @@ pub fn extract_uinode_borders( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: global_transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: computed_node.border(), @@ -600,7 +602,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: global_transform.compute_matrix(), + transform: transform.into(), atlas_scaling: None, flip_x: false, flip_y: false, @@ -749,7 +751,7 @@ pub fn extract_viewport_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -792,7 +794,7 @@ pub fn extract_viewport_nodes( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -812,7 +814,7 @@ pub fn extract_text_sections( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -830,7 +832,7 @@ pub fn extract_text_sections( for ( entity, uinode, - global_transform, + transform, inherited_visibility, clip, camera, @@ -847,8 +849,7 @@ pub fn extract_text_sections( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.)); + let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -866,7 +867,7 @@ pub fn extract_text_sections( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: transform * Affine2::from_translation(*position), rect, }); @@ -910,8 +911,8 @@ pub fn extract_text_shadows( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &ComputedNodeTarget, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &TextLayoutInfo, @@ -924,16 +925,8 @@ pub fn extract_text_shadows( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - target, - global_transform, - inherited_visibility, - clip, - text_layout_info, - shadow, - ) in &uinode_query + for (entity, uinode, transform, target, inherited_visibility, clip, text_layout_info, shadow) in + &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -944,9 +937,9 @@ pub fn extract_text_shadows( continue; }; - let transform = global_transform.affine() - * Mat4::from_translation( - (-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.), + let node_transform = Affine2::from(*transform) + * Affine2::from_translation( + -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); for ( @@ -965,7 +958,7 @@ pub fn extract_text_shadows( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: node_transform * Affine2::from_translation(*position), rect, }); @@ -998,7 +991,7 @@ pub fn extract_text_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -1021,8 +1014,8 @@ pub fn extract_text_background_colors( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); for &(section_entity, rect) in text_layout_info.section_rects.iter() { let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { @@ -1042,7 +1035,7 @@ pub fn extract_text_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform * Mat4::from_translation(rect.center().extend(0.)), + transform: transform * Affine2::from_translation(rect.center()), flip_x: false, flip_y: false, border: uinode.border(), @@ -1093,11 +1086,11 @@ impl Default for UiMeta { } } -pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ - Vec3::new(-0.5, -0.5, 0.0), - Vec3::new(0.5, -0.5, 0.0), - Vec3::new(0.5, 0.5, 0.0), - Vec3::new(-0.5, 0.5, 0.0), +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + Vec2::new(-0.5, -0.5), + Vec2::new(0.5, -0.5), + Vec2::new(0.5, 0.5), + Vec2::new(-0.5, 0.5), ]; pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; @@ -1321,12 +1314,12 @@ pub fn prepare_uinodes( let mut uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); - let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -1367,7 +1360,7 @@ pub fn prepare_uinodes( points[3] + positions_diff[3], ]; - let transformed_rect_size = transform.transform_vector3(rect_size); + let transformed_rect_size = transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -1448,7 +1441,7 @@ pub fn prepare_uinodes( border_radius.bottom_left, ], border: [border.left, border.top, border.right, border.bottom], - size: rect_size.xy().into(), + size: rect_size.into(), point: points[i].into(), }); } @@ -1470,13 +1463,14 @@ pub fn prepare_uinodes( let color = extracted_uinode.color.to_f32_array(); for glyph in &extracted_uinodes.glyphs[range.clone()] { let glyph_rect = glyph.rect; - let size = glyph.rect.size(); - - let rect_size = glyph_rect.size().extend(1.0); + let rect_size = glyph_rect.size(); // Specify the corners of the glyph let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (glyph.transform * (pos * rect_size).extend(1.)).xyz() + glyph + .transform + .transform_point2(pos * glyph_rect.size()) + .extend(0.) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -1511,7 +1505,7 @@ pub fn prepare_uinodes( // cull nodes that are completely clipped let transformed_rect_size = - glyph.transform.transform_vector3(rect_size); + glyph.transform.transform_vector2(rect_size); if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x.abs() || positions_diff[1].y - positions_diff[2].y @@ -1548,7 +1542,7 @@ pub fn prepare_uinodes( flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], radius: [0.0; 4], border: [0.0; 4], - size: size.into(), + size: rect_size.into(), point: [0.0; 2], }); } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 02fab4fdee..ebdeacccf9 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -1,9 +1,7 @@ -use core::{hash::Hash, marker::PhantomData, ops::Range}; - use crate::*; use bevy_asset::*; use bevy_ecs::{ - prelude::Component, + prelude::{Component, With}, query::ROQueryItem, system::{ lifetimeless::{Read, SRes}, @@ -11,24 +9,22 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_render::{ extract_component::ExtractComponentPlugin, globals::{GlobalsBuffer, GlobalsUniform}, + load_shader_library, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, TemporaryRenderEntity}, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_render::{ - load_shader_library, - sync_world::{MainEntity, TemporaryRenderEntity}, -}; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; +use core::{hash::Hash, marker::PhantomData, ops::Range}; /// Adds the necessary ECS resources and render logic to enable rendering entities using the given /// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). @@ -321,7 +317,7 @@ impl RenderCommand

for DrawUiMaterialNode { pub struct ExtractedUiMaterialNode { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub border: BorderRect, pub border_radius: ResolvedBorderRadius, @@ -356,7 +352,7 @@ pub fn extract_ui_material_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, @@ -387,7 +383,7 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), material: handle.id(), rect: Rect { min: Vec2::ZERO, @@ -459,10 +455,13 @@ pub fn prepare_uimaterial_nodes( let uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + extracted_uinode + .transform + .transform_point2(pos * rect_size) + .extend(1.0) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -496,7 +495,7 @@ pub fn prepare_uimaterial_nodes( ]; let transformed_rect_size = - extracted_uinode.transform.transform_vector3(rect_size); + extracted_uinode.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 80a55bbcd4..0e232ab1cc 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -1,5 +1,6 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::*; use bevy_asset::*; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; @@ -11,7 +12,7 @@ use bevy_ecs::{ }, }; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -25,7 +26,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; -use bevy_transform::prelude::GlobalTransform; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; use widget::ImageNode; @@ -218,7 +218,7 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { pub struct ExtractedUiTextureSlice { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub atlas_rect: Option, pub image: AssetId, @@ -246,7 +246,7 @@ pub fn extract_ui_texture_slices( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -306,7 +306,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), color: image.color.into(), rect: Rect { min: Vec2::ZERO, @@ -497,11 +497,12 @@ pub fn prepare_ui_slices( let uinode_rect = texture_slices.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (texture_slices.transform.transform_point2(pos * rect_size)).extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -536,7 +537,7 @@ pub fn prepare_ui_slices( ]; let transformed_rect_size = - texture_slices.transform.transform_vector3(rect_size); + texture_slices.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f5f914bdc0..1c5ed364b9 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1,4 +1,7 @@ -use crate::{FocusPolicy, UiRect, Val}; +use crate::{ + ui_transform::{UiGlobalTransform, UiTransform}, + FocusPolicy, UiRect, Val, +}; use bevy_color::{Alpha, Color}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; @@ -9,7 +12,6 @@ use bevy_render::{ view::Visibility, }; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use bevy_utils::once; use bevy_window::{PrimaryWindow, WindowRef}; use core::{f32, num::NonZero}; @@ -229,6 +231,73 @@ impl ComputedNode { pub const fn inverse_scale_factor(&self) -> f32 { self.inverse_scale_factor } + + // Returns true if `point` within the node. + // + // Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. + pub fn contains_point(&self, transform: UiGlobalTransform, point: Vec2) -> bool { + let Some(local_point) = transform + .try_inverse() + .map(|transform| transform.transform_point2(point)) + else { + return false; + }; + let [top, bottom] = if local_point.x < 0. { + [self.border_radius.top_left, self.border_radius.bottom_left] + } else { + [ + self.border_radius.top_right, + self.border_radius.bottom_right, + ] + }; + let r = if local_point.y < 0. { top } else { bottom }; + let corner_to_point = local_point.abs() - 0.5 * self.size; + let q = corner_to_point + r; + let l = q.max(Vec2::ZERO).length(); + let m = q.max_element().min(0.); + l + m - r < 0. + } + + /// Transform a point to normalized node space with the center of the node at the origin and the corners at [+/-0.5, +/-0.5] + pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option { + self.size + .cmpgt(Vec2::ZERO) + .all() + .then(|| transform.try_inverse()) + .flatten() + .map(|transform| transform.transform_point2(point) / self.size) + } + + /// Resolve the node's clipping rect in local space + pub fn resolve_clip_rect( + &self, + overflow: Overflow, + overflow_clip_margin: OverflowClipMargin, + ) -> Rect { + let mut clip_rect = Rect::from_center_size(Vec2::ZERO, self.size); + + let clip_inset = match overflow_clip_margin.visual_box { + OverflowClipBox::BorderBox => BorderRect::ZERO, + OverflowClipBox::ContentBox => self.content_inset(), + OverflowClipBox::PaddingBox => self.border(), + }; + + clip_rect.min.x += clip_inset.left; + clip_rect.min.y += clip_inset.top; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; + + if overflow.x == OverflowAxis::Visible { + clip_rect.min.x = -f32::INFINITY; + clip_rect.max.x = f32::INFINITY; + } + if overflow.y == OverflowAxis::Visible { + clip_rect.min.y = -f32::INFINITY; + clip_rect.max.y = f32::INFINITY; + } + + clip_rect + } } impl ComputedNode { @@ -323,12 +392,12 @@ impl From for ScrollPosition { #[require( ComputedNode, ComputedNodeTarget, + UiTransform, BackgroundColor, BorderColor, BorderRadius, FocusPolicy, ScrollPosition, - Transform, Visibility, ZIndex )] @@ -2061,6 +2130,16 @@ impl BorderColor { } } + /// Helper to set all border colors to a given color. + pub fn set_all(&mut self, color: impl Into) -> &mut Self { + let color: Color = color.into(); + self.top = color; + self.bottom = color; + self.left = color; + self.right = color; + self + } + /// Check if all contained border colors are transparent pub fn is_fully_transparent(&self) -> bool { self.top.is_fully_transparent() @@ -2796,8 +2875,8 @@ impl ComputedNodeTarget { } /// Adds a shadow behind text -#[derive(Component, Copy, Clone, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(Component, Default, Debug, Clone, PartialEq)] pub struct TextShadow { /// Shadow displacement in logical pixels /// With a value of zero the shadow will be hidden directly behind the text diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs new file mode 100644 index 0000000000..47f8484e54 --- /dev/null +++ b/crates/bevy_ui/src/ui_transform.rs @@ -0,0 +1,191 @@ +use crate::Val; +use bevy_derive::Deref; +use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::Affine2; +use bevy_math::Rot2; +use bevy_math::Vec2; +use bevy_reflect::prelude::*; + +/// A pair of [`Val`]s used to represent a 2-dimensional size or offset. +#[derive(Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct Val2 { + /// Translate the node along the x-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + pub x: Val, + /// Translate the node along the y-axis. + /// `Val::Percent` values are resolved based on the computed height of the UI Node. + /// `Val::Auto` is resolved to `0.`. + pub y: Val, +} + +impl Val2 { + pub const ZERO: Self = Self { + x: Val::ZERO, + y: Val::ZERO, + }; + + /// Creates a new [`Val2`] where both components are in logical pixels + pub const fn px(x: f32, y: f32) -> Self { + Self { + x: Val::Px(x), + y: Val::Px(y), + } + } + + /// Creates a new [`Val2`] where both components are percentage values + pub const fn percent(x: f32, y: f32) -> Self { + Self { + x: Val::Percent(x), + y: Val::Percent(y), + } + } + + /// Creates a new [`Val2`] + pub const fn new(x: Val, y: Val) -> Self { + Self { x, y } + } + + /// Resolves this [`Val2`] from the given `scale_factor`, `parent_size`, + /// and `viewport_size`. + /// + /// Component values of [`Val::Auto`] are resolved to 0. + pub fn resolve(&self, scale_factor: f32, base_size: Vec2, viewport_size: Vec2) -> Vec2 { + Vec2::new( + self.x + .resolve(scale_factor, base_size.x, viewport_size) + .unwrap_or(0.), + self.y + .resolve(scale_factor, base_size.y, viewport_size) + .unwrap_or(0.), + ) + } +} + +impl Default for Val2 { + fn default() -> Self { + Self::ZERO + } +} + +/// Relative 2D transform for UI nodes +/// +/// [`UiGlobalTransform`] is automatically inserted whenever [`UiTransform`] is inserted. +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[require(UiGlobalTransform)] +pub struct UiTransform { + /// Translate the node. + pub translation: Val2, + /// Scale the node. A negative value reflects the node in that axis. + pub scale: Vec2, + /// Rotate the node clockwise. + pub rotation: Rot2, +} + +impl UiTransform { + pub const IDENTITY: Self = Self { + translation: Val2::ZERO, + scale: Vec2::ONE, + rotation: Rot2::IDENTITY, + }; + + /// Creates a UI transform representing a rotation. + pub fn from_rotation(rotation: Rot2) -> Self { + Self { + rotation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a responsive translation. + pub fn from_translation(translation: Val2) -> Self { + Self { + translation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a scaling. + pub fn from_scale(scale: Vec2) -> Self { + Self { + scale, + ..Self::IDENTITY + } + } + + /// Resolves the translation from the given `scale_factor`, `base_value`, and `target_size` + /// and returns a 2d affine transform from the resolved translation, and the `UiTransform`'s rotation, and scale. + pub fn compute_affine(&self, scale_factor: f32, base_size: Vec2, target_size: Vec2) -> Affine2 { + Affine2::from_scale_angle_translation( + self.scale, + self.rotation.as_radians(), + self.translation + .resolve(scale_factor, base_size, target_size), + ) + } +} + +impl Default for UiTransform { + fn default() -> Self { + Self::IDENTITY + } +} + +/// Absolute 2D transform for UI nodes +/// +/// [`UiGlobalTransform`]s are updated from [`UiTransform`] and [`Node`](crate::ui_node::Node) +/// in [`ui_layout_system`](crate::layout::ui_layout_system) +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct UiGlobalTransform(Affine2); + +impl Default for UiGlobalTransform { + fn default() -> Self { + Self(Affine2::IDENTITY) + } +} + +impl UiGlobalTransform { + /// If the transform is invertible returns its inverse. + /// Otherwise returns `None`. + #[inline] + pub fn try_inverse(&self) -> Option { + (self.matrix2.determinant() != 0.).then_some(self.inverse()) + } +} + +impl From for UiGlobalTransform { + fn from(value: Affine2) -> Self { + Self(value) + } +} + +impl From for Affine2 { + fn from(value: UiGlobalTransform) -> Self { + value.0 + } +} + +impl From<&UiGlobalTransform> for Affine2 { + fn from(value: &UiGlobalTransform) -> Self { + value.0 + } +} diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 7e27c4abdd..c0e9d09d7b 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -2,6 +2,7 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::UiGlobalTransform, CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, UiTargetCamera, }; @@ -17,7 +18,6 @@ use bevy_ecs::{ use bevy_math::{Rect, UVec2}; use bevy_render::camera::Camera; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; /// Updates clipping for all nodes pub fn update_clipping_system( @@ -26,7 +26,7 @@ pub fn update_clipping_system( mut node_query: Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, ui_children: UiChildren, @@ -48,14 +48,13 @@ fn update_clipping( node_query: &mut Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, entity: Entity, mut maybe_inherited_clip: Option, ) { - let Ok((node, computed_node, global_transform, maybe_calculated_clip)) = - node_query.get_mut(entity) + let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity) else { return; }; @@ -91,10 +90,7 @@ fn update_clipping( maybe_inherited_clip } else { // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - let mut clip_rect = Rect::from_center_size( - global_transform.translation().truncate(), - computed_node.size(), - ); + let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size()); // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`]. // diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index c65c4df354..9a743595b8 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -138,8 +138,8 @@ impl From> for ImageNode { } /// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space in the layout for the image -#[derive(Default, Debug, Clone, Reflect)] -#[reflect(Clone, Default)] +#[derive(Default, Debug, Clone, PartialEq, Reflect)] +#[reflect(Clone, Default, PartialEq)] pub enum NodeImageMode { /// The image will be sized automatically by taking the size of the source image and applying any layout constraints. #[default] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 785040c1e9..d7f8e243a4 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -61,7 +61,7 @@ impl Default for TextNodeFlags { /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, TextLayout, TextFont, TextColor, TextSpan}; /// # use bevy_ui::prelude::Text; /// # /// # let font_handle: Handle = Default::default(); @@ -84,7 +84,7 @@ impl Default for TextNodeFlags { /// // With text justification. /// world.spawn(( /// Text::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans diff --git a/crates/bevy_ui/src/widget/viewport.rs b/crates/bevy_ui/src/widget/viewport.rs index f68033ea7f..9cdc348da5 100644 --- a/crates/bevy_ui/src/widget/viewport.rs +++ b/crates/bevy_ui/src/widget/viewport.rs @@ -171,11 +171,6 @@ pub fn update_viewport_render_target_size( height: u32::max(1, size.y as u32), ..default() }; - let image = images.get_mut(image_handle).unwrap(); - if image.data.is_some() { - image.resize(size); - } else { - image.texture_descriptor.size = size; - } + images.get_mut(image_handle).unwrap().resize(size); } } diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index ad3e1ae9c7..53eda0d358 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_utils" version = "0.16.0-dev" edition = "2024" description = "A collection of utils 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_utils/README.md b/crates/bevy_utils/README.md index 341755babc..4515209dbe 100644 --- a/crates/bevy_utils/README.md +++ b/crates/bevy_utils/README.md @@ -6,4 +6,4 @@ [![Docs](https://docs.rs/bevy_utils/badge.svg)](https://docs.rs/bevy_utils/latest/bevy_utils/) [![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) -A Collection of Utilities for the [Bevy Engine](https://bevyengine.org/). +A Collection of Utilities for the [Bevy Engine](https://bevy.org/). diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 164610eb9b..e3bb07a512 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -1,13 +1,13 @@ #![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] //! General utilities for first-party [Bevy] engine crates. //! -//! [Bevy]: https://bevyengine.org/ +//! [Bevy]: https://bevy.org/ /// Configuration information for this crate. pub mod cfg { diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index b2b6d730fe..2e3e7edf18 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_window" version = "0.16.0-dev" edition = "2024" description = "Provides windowing 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_window/src/lib.rs b/crates/bevy_window/src/lib.rs index 21ee8d64c9..fb8f1fb18f 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/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" )] #![no_std] diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 341afa4f60..43db87a1d2 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -3,7 +3,7 @@ name = "bevy_winit" version = "0.16.0-dev" edition = "2024" description = "A winit window and input backend 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_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index bdca3f8585..e3e4cb9f87 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -22,11 +22,12 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, + lifecycle::OnRemove, observer::Trigger, query::With, reflect::ReflectComponent, system::{Commands, Local, Query}, - world::{OnRemove, Ref}, + world::Ref, }; #[cfg(feature = "custom_cursor")] use bevy_image::{Image, TextureAtlasLayout}; @@ -194,7 +195,7 @@ fn update_cursors( fn on_remove_cursor_icon(trigger: Trigger, mut commands: Commands) { // Use `try_insert` to avoid panic if the window is being destroyed. commands - .entity(trigger.target()) + .entity(trigger.target().unwrap()) .try_insert(PendingCursor(Some(CursorSource::System( convert_system_cursor_icon(SystemCursorIcon::Default), )))); diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index d7c880a9b9..968386bc02 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/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" )] //! `bevy_winit` provides utilities to handle window creation and the eventloop through [`winit`] diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 97483c7358..873949ea89 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -3,9 +3,9 @@ use std::collections::HashMap; use bevy_ecs::{ entity::Entity, event::EventWriter, + lifecycle::RemovedComponents, prelude::{Changed, Component}, query::QueryFilter, - removal_detection::RemovedComponents, system::{Local, NonSendMarker, Query, SystemParamItem}, }; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; diff --git a/docs-template/EXAMPLE_README.md.tpl b/docs-template/EXAMPLE_README.md.tpl index 9cde78c3b9..dfd516303b 100644 --- a/docs-template/EXAMPLE_README.md.tpl +++ b/docs-template/EXAMPLE_README.md.tpl @@ -283,7 +283,7 @@ In browsers, audio is not authorized to start without being triggered by an user On the web, it's useful to reduce the size of the files that are distributed. With rust, there are many ways to improve your executable sizes, starting with -the steps described in [the quick-start guide](https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations). +the steps described in [the quick-start guide](https://bevy.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations). Now, when building the executable, use `--profile wasm-release` instead of `--release`: diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 1a1cb68fda..8784d5e725 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -21,6 +21,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_audio|Provides audio functionality| |bevy_color|Provides shared color types and operations| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| +|bevy_core_widgets|Headless widget collection for Bevy UI.| |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| @@ -85,6 +86,7 @@ The default feature set enables most of the expected features of a game engine, |ghost_nodes|Experimental support for nodes that are ignored for UI layouting| |gif|GIF image format support| |glam_assert|Enable assertions to check the validity of parameters passed to glam| +|hotpatching|Enable hotpatching of Bevy systems| |ico|ICO image format support| |jpeg|JPEG image format support| |libm|Uses the `libm` maths library instead of the one provided in `std` and `core`.| diff --git a/docs/linux_dependencies.md b/docs/linux_dependencies.md index a09bd629ff..a5a43c3d5f 100644 --- a/docs/linux_dependencies.md +++ b/docs/linux_dependencies.md @@ -91,7 +91,7 @@ Set the `PKG_CONFIG_PATH` env var to `/usr/lib//pkgconfig/`. For example export PKG_CONFIG_PATH="/usr/lib/x86_64-linux-gnu/pkgconfig/" ``` -## Arch / Manjaro +## [Arch](https://archlinux.org/) / [Manjaro](https://manjaro.org/) ```bash sudo pacman -S libx11 pkgconf alsa-lib libxcursor libxrandr libxi @@ -102,7 +102,7 @@ Install `pipewire-alsa` or `pulseaudio-alsa` depending on the sound server you a Depending on your graphics card, you may have to install one of the following: `vulkan-radeon`, `vulkan-intel`, or `mesa-vulkan-drivers` -## Void +## [Void](https://voidlinux.org/) ```bash sudo xbps-install -S pkgconf alsa-lib-devel libX11-devel eudev-libudev-devel @@ -110,6 +110,80 @@ sudo xbps-install -S pkgconf alsa-lib-devel libX11-devel eudev-libudev-devel ## [Nix](https://nixos.org) +### flake.nix + +Add a `flake.nix` file to the root of your GitHub repository containing: + +```nix +{ + description = "bevy flake"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + nixpkgs, + rust-overlay, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { + inherit system overlays; + }; + in + { + devShells.default = + with pkgs; + mkShell { + buildInputs = + [ + # Rust dependencies + (rust-bin.stable.latest.default.override { extensions = [ "rust-src" ]; }) + pkg-config + ] + ++ lib.optionals (lib.strings.hasInfix "linux" system) [ + # for Linux + # Audio (Linux only) + alsa-lib + # Cross Platform 3D Graphics API + vulkan-loader + # For debugging around vulkan + vulkan-tools + # Other dependencies + libudev-zero + xorg.libX11 + xorg.libXcursor + xorg.libXi + xorg.libXrandr + libxkbcommon + ]; + RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}"; + LD_LIBRARY_PATH = lib.makeLibraryPath [ + vulkan-loader + xorg.libX11 + xorg.libXi + xorg.libXcursor + libxkbcommon + ]; + }; + } + ); +} +``` + +> [!TIP] +> We have confirmed that this flake.nix can be used successfully on NixOS and MacOS with Rust's edition set to 2021. + +### shell.nix + Add a `shell.nix` file to the root of the project containing: ```nix @@ -138,8 +212,8 @@ If running nix on a non NixOS system (such as ubuntu, arch etc.), [NixGL](https: to link graphics drivers into the context of software installed by nix: 1. Install a system specific nixGL wrapper ([docs](https://github.com/nix-community/nixGL)). - * If you're running a nvidia GPU choose `nixVulkanNvidia`. - * Otherwise, choose another wrapper appropriate for your system. + - If you're running a nvidia GPU choose `nixVulkanNvidia`. + - Otherwise, choose another wrapper appropriate for your system. 2. Run `nixVulkanNvidia-xxx.xxx.xx cargo run` to compile a bevy program, where `xxx-xxx-xx` denotes the graphics driver version `nixVulkanNvidia` was compiled with. This is also possible with [Nix flakes](https://nixos.org/manual/nix/unstable/command-ref/new-cli/nix3-flake.html). @@ -152,7 +226,7 @@ for more information about `devShells`. Note that this template does not add Rust to the environment because there are many ways to do it. For example, to use stable Rust from nixpkgs, you can add `cargo` and `rustc` to `nativeBuildInputs`. -[Here]([https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/jumpy/package.nix](https://github.com/NixOS/nixpkgs/blob/0da3c44a9460a26d2025ec3ed2ec60a895eb1114/pkgs/games/jumpy/default.nix)) +[Here](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/jumpy/package.nix) is an example of packaging a Bevy program in nix. ## [OpenSUSE](https://www.opensuse.org/) @@ -161,7 +235,7 @@ is an example of packaging a Bevy program in nix. sudo zypper install libudev-devel gcc-c++ alsa-lib-devel ``` -## Gentoo +## [Gentoo](https://www.gentoo.org/) ```bash sudo emerge --ask libX11 pkgconf alsa-lib diff --git a/errors/README.md b/errors/README.md index be2adb2b00..fe25e78400 100644 --- a/errors/README.md +++ b/errors/README.md @@ -3,6 +3,6 @@ This crate lists and tests explanations and examples of Bevy's error codes. For the latest Bevy release, you can find a rendered version of the error code descriptions at -[bevyengine.org/learn/errors]. +[bevy.org/learn/errors]. -[bevyengine.org/learn/errors]: https://bevyengine.org/learn/errors +[bevy.org/learn/errors]: https://bevy.org/learn/errors diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index c549134419..9cffb8e00c 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -129,7 +129,7 @@ fn setup_sprites(mut commands: Commands, asset_server: Res) { cmd.with_children(|builder| { builder.spawn(( Text2d::new(rect.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, @@ -275,7 +275,7 @@ fn setup_texture_atlas( cmd.with_children(|builder| { builder.spawn(( Text2d::new(sprite_sheet.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index 94f4fe809f..91918b1d66 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -94,7 +94,7 @@ fn spawn_sprites( children![( Text2d::new(label), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), bevy::sprite::Anchor::TOP_CENTER, )], diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 7b1abfd8da..3123e8d891 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 50.0, ..default() }; - let text_justification = JustifyText::Center; + let text_justification = Justify::Center; commands.spawn(Camera2d); // Demonstrate changing translation commands.spawn(( @@ -78,7 +78,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(Unicode linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::WordBoundary), + TextLayout::new(Justify::Left, LineBreak::WordBoundary), // Wrap text in the rectangle TextBounds::from(box_size), // Ensure the text is drawn on top of the box @@ -94,7 +94,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(AnyCharacter linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::AnyCharacter), + TextLayout::new(Justify::Left, LineBreak::AnyCharacter), // Wrap text in the rectangle TextBounds::from(other_box_size), // Ensure the text is drawn on top of the box @@ -104,11 +104,11 @@ fn setup(mut commands: Commands, asset_server: Res) { // Demonstrate font smoothing off commands.spawn(( - Text2d::new("This text has\nFontSmoothing::None\nAnd JustifyText::Center"), + Text2d::new("This text has\nFontSmoothing::None\nAnd Justify::Center"), slightly_smaller_text_font .clone() .with_font_smoothing(FontSmoothing::None), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)), )); diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 7510afbcee..25106adcfb 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -279,7 +279,7 @@ fn create_label( commands.spawn(( Text2d::new(text), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform { translation: Vec3::new(translation.0, translation.1, translation.2), ..default() diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index 1f693ce238..fd93625c0e 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -5,16 +5,16 @@ use std::{f32::consts::PI, fmt::Write}; use bevy::{ anti_aliasing::{ contrast_adaptive_sharpening::ContrastAdaptiveSharpening, - experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing}, fxaa::{Fxaa, Sensitivity}, smaa::{Smaa, SmaaPreset}, + taa::TemporalAntiAliasing, }, core_pipeline::prepass::{DepthPrepass, MotionVectorPrepass}, image::{ImageSampler, ImageSamplerDescriptor}, pbr::CascadeShadowConfigBuilder, prelude::*, render::{ - camera::TemporalJitter, + camera::{MipBias, TemporalJitter}, render_asset::RenderAssetUsages, render_resource::{Extent3d, TextureDimension, TextureFormat}, view::Hdr, @@ -23,7 +23,7 @@ use bevy::{ fn main() { App::new() - .add_plugins((DefaultPlugins, TemporalAntiAliasPlugin)) + .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, (modify_aa, modify_sharpening, update_ui)) .run(); @@ -32,6 +32,7 @@ fn main() { type TaaComponents = ( TemporalAntiAliasing, TemporalJitter, + MipBias, DepthPrepass, MotionVectorPrepass, ); diff --git a/examples/3d/edit_material_on_gltf.rs b/examples/3d/edit_material_on_gltf.rs index f9de5842a9..029ec6bf1e 100644 --- a/examples/3d/edit_material_on_gltf.rs +++ b/examples/3d/edit_material_on_gltf.rs @@ -65,12 +65,12 @@ fn change_material( mut asset_materials: ResMut>, ) { // Get the `ColorOverride` of the entity, if it does not have a color override, skip - let Ok(color_override) = color_override.get(trigger.target()) else { + let Ok(color_override) = color_override.get(trigger.target().unwrap()) else { return; }; // Iterate over all children recursively - for descendants in children.iter_descendants(trigger.target()) { + for descendants in children.iter_descendants(trigger.target().unwrap()) { // Get the material of the descendant if let Some(material) = mesh_materials .get(descendants) diff --git a/examples/3d/pbr.rs b/examples/3d/pbr.rs index 12922db03b..da654ce1f3 100644 --- a/examples/3d/pbr.rs +++ b/examples/3d/pbr.rs @@ -85,8 +85,8 @@ fn setup( right: Val::ZERO, ..default() }, - Transform { - rotation: Quat::from_rotation_z(std::f32::consts::PI / 2.0), + UiTransform { + rotation: Rot2::degrees(90.), ..default() }, )); diff --git a/examples/3d/pcss.rs b/examples/3d/pcss.rs index 922781829c..b2715f0b57 100644 --- a/examples/3d/pcss.rs +++ b/examples/3d/pcss.rs @@ -3,7 +3,7 @@ use std::f32::consts::PI; use bevy::{ - anti_aliasing::experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing}, + anti_aliasing::taa::TemporalAntiAliasing, core_pipeline::{ prepass::{DepthPrepass, MotionVectorPrepass}, Skybox, @@ -120,7 +120,6 @@ fn main() { }), ..default() })) - .add_plugins(TemporalAntiAliasPlugin) .add_event::>() .add_systems(Startup, setup) .add_systems(Update, widgets::handle_ui_interactions::) diff --git a/examples/3d/scrolling_fog.rs b/examples/3d/scrolling_fog.rs index 3438417fb4..7ec53e1fad 100644 --- a/examples/3d/scrolling_fog.rs +++ b/examples/3d/scrolling_fog.rs @@ -11,7 +11,7 @@ //! interactions change based on the density of the fog. use bevy::{ - anti_aliasing::experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing}, + anti_aliasing::taa::TemporalAntiAliasing, core_pipeline::bloom::Bloom, image::{ ImageAddressMode, ImageFilterMode, ImageLoaderSettings, ImageSampler, @@ -32,7 +32,6 @@ fn main() { ..default() })) .insert_resource(DirectionalLightShadowMap { size: 4096 }) - .add_plugins(TemporalAntiAliasPlugin) .add_systems(Startup, setup) .add_systems(Update, scroll_fog) .run(); diff --git a/examples/3d/ssao.rs b/examples/3d/ssao.rs index b33ab42090..687288b1f5 100644 --- a/examples/3d/ssao.rs +++ b/examples/3d/ssao.rs @@ -1,7 +1,7 @@ //! A scene showcasing screen space ambient occlusion. use bevy::{ - anti_aliasing::experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing}, + anti_aliasing::taa::TemporalAntiAliasing, math::ops, pbr::{ScreenSpaceAmbientOcclusion, ScreenSpaceAmbientOcclusionQualityLevel}, prelude::*, @@ -15,7 +15,7 @@ fn main() { brightness: 1000., ..default() }) - .add_plugins((DefaultPlugins, TemporalAntiAliasPlugin)) + .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, update) .run(); diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 0808776e2b..987ceda70d 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -180,7 +180,7 @@ fn setup_image_viewer_scene( ..default() }, TextColor(Color::BLACK), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Node { align_self: AlignSelf::Center, margin: UiRect::all(Val::Auto), diff --git a/examples/3d/transmission.rs b/examples/3d/transmission.rs index 36a78b1a62..ee62654ea6 100644 --- a/examples/3d/transmission.rs +++ b/examples/3d/transmission.rs @@ -35,14 +35,17 @@ use bevy::{ }, }; +// *Note:* TAA is not _required_ for specular transmission, but +// it _greatly enhances_ the look of the resulting blur effects. +// Sadly, it's not available under WebGL. #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))] -use bevy::anti_aliasing::experimental::taa::{TemporalAntiAliasPlugin, TemporalAntiAliasing}; +use bevy::anti_aliasing::taa::TemporalAntiAliasing; + use rand::random; fn main() { - let mut app = App::new(); - - app.add_plugins(DefaultPlugins) + App::new() + .add_plugins(DefaultPlugins) .insert_resource(ClearColor(Color::BLACK)) .insert_resource(PointLightShadowMap { size: 2048 }) .insert_resource(AmbientLight { @@ -50,15 +53,8 @@ fn main() { ..default() }) .add_systems(Startup, setup) - .add_systems(Update, (example_control_system, flicker_system)); - - // *Note:* TAA is not _required_ for specular transmission, but - // it _greatly enhances_ the look of the resulting blur effects. - // Sadly, it's not available under WebGL. - #[cfg(any(feature = "webgpu", not(target_arch = "wasm32")))] - app.add_plugins(TemporalAntiAliasPlugin); - - app.run(); + .add_systems(Update, (example_control_system, flicker_system)) + .run(); } /// set up a simple 3D scene diff --git a/examples/README.md b/examples/README.md index 4f5030563a..4e679d0d7a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -280,6 +280,7 @@ Example | Description Example | Description --- | --- +[2D on Bevy UI](../examples/camera/2d_on_ui.rs) | Shows how to render 2D objects on top of Bevy UI [2D top-down camera](../examples/camera/2d_top_down_camera.rs) | A 2D top-down camera smoothly following player movements [Camera Orbit](../examples/camera/camera_orbit.rs) | Shows how to orbit a static scene using pitch, yaw, and roll. [Custom Projection](../examples/camera/custom_projection.rs) | Shows how to create custom camera projections. @@ -318,6 +319,7 @@ Example | Description [Fixed Timestep](../examples/ecs/fixed_timestep.rs) | Shows how to create systems that run every fixed timestep, rather than every tick [Generic System](../examples/ecs/generic_system.rs) | Shows how to create systems that can be reused with different types [Hierarchy](../examples/ecs/hierarchy.rs) | Creates a hierarchy of parents and children entities +[Hotpatching Systems](../examples/ecs/hotpatching_systems.rs) | Demonstrates how to hotpatch systems [Immutable Components](../examples/ecs/immutable_components.rs) | Demonstrates the creation and utility of immutable components [Iter Combinations](../examples/ecs/iter_combinations.rs) | Shows how to iterate over combinations of query results [Nondeterministic System Order](../examples/ecs/nondeterministic_system_order.rs) | Systems run in parallel, but their order isn't always deterministic. Here's how to detect and fix this. @@ -543,6 +545,8 @@ Example | Description [Box Shadow](../examples/ui/box_shadow.rs) | Demonstrates how to create a node with a shadow [Button](../examples/ui/button.rs) | Illustrates creating and updating a button [CSS Grid](../examples/ui/grid.rs) | An example for CSS Grid layout +[Core Widgets](../examples/ui/core_widgets.rs) | Demonstrates use of core (headless) widgets in Bevy UI +[Core Widgets (w/Observers)](../examples/ui/core_widgets_observers.rs) | Demonstrates use of core (headless) widgets in Bevy UI, with Observers [Directional Navigation](../examples/ui/directional_navigation.rs) | Demonstration of Directional Navigation between UI elements [Display and Visibility](../examples/ui/display_and_visibility.rs) | Demonstrates how Display and Visibility work in the UI. [Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text @@ -569,6 +573,7 @@ Example | Description [UI Texture Atlas Slice](../examples/ui/ui_texture_atlas_slice.rs) | Illustrates how to use 9 Slicing for TextureAtlases in UI [UI Texture Slice](../examples/ui/ui_texture_slice.rs) | Illustrates how to use 9 Slicing in UI [UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI +[UI Transform](../examples/ui/ui_transform.rs) | An example demonstrating how to translate, rotate and scale UI elements. [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements [Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates [Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support @@ -803,7 +808,7 @@ In browsers, audio is not authorized to start without being triggered by an user On the web, it's useful to reduce the size of the files that are distributed. With rust, there are many ways to improve your executable sizes, starting with -the steps described in [the quick-start guide](https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations). +the steps described in [the quick-start guide](https://bevy.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations). Now, when building the executable, use `--profile wasm-release` instead of `--release`: diff --git a/examples/animation/animated_mesh.rs b/examples/animation/animated_mesh.rs index ecea86fb17..d2c4fd8443 100644 --- a/examples/animation/animated_mesh.rs +++ b/examples/animation/animated_mesh.rs @@ -70,12 +70,12 @@ fn play_animation_when_ready( ) { // The entity we spawned in `setup_mesh_and_animation` is the trigger's target. // Start by finding the AnimationToPlay component we added to that entity. - if let Ok(animation_to_play) = animations_to_play.get(trigger.target()) { + if let Ok(animation_to_play) = animations_to_play.get(trigger.target().unwrap()) { // The SceneRoot component will have spawned the scene as a hierarchy // of entities parented to our entity. Since the asset contained a skinned // mesh and animations, it will also have spawned an animation player // component. Search our entity's descendants to find the animation player. - for child in children.iter_descendants(trigger.target()) { + for child in children.iter_descendants(trigger.target().unwrap()) { if let Ok(mut player) = players.get_mut(child) { // Tell the animation player to start the animation and keep // repeating it. diff --git a/examples/animation/animated_mesh_events.rs b/examples/animation/animated_mesh_events.rs index 2048f573fd..c0f261752e 100644 --- a/examples/animation/animated_mesh_events.rs +++ b/examples/animation/animated_mesh_events.rs @@ -47,7 +47,10 @@ fn observe_on_step( transforms: Query<&GlobalTransform>, mut seeded_rng: ResMut, ) { - let translation = transforms.get(trigger.target()).unwrap().translation(); + let translation = transforms + .get(trigger.target().unwrap()) + .unwrap() + .translation(); // Spawn a bunch of particles. for _ in 0..14 { let horizontal = seeded_rng.0.r#gen::() * seeded_rng.0.gen_range(8.0..12.0); diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index f31b2ccd5e..68b0eb7a8f 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -151,7 +151,7 @@ fn setup( ..default() }, TextColor(Color::Srgba(Srgba::RED)), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )) // Mark as an animation target. .insert(AnimationTarget { diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 610074744f..884ec1a2af 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -277,7 +277,7 @@ fn setup_node_rects(commands: &mut Commands) { ..default() }, TextColor(ANTIQUE_WHITE.into()), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), )) .id(); diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs index 07261b40df..05b50711fe 100644 --- a/examples/animation/animation_masks.rs +++ b/examples/animation/animation_masks.rs @@ -334,7 +334,7 @@ fn add_mask_group_control( } else { selected_button_text_style.clone() }, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Node { flex_grow: 1.0, margin: UiRect::vertical(Val::Px(3.0)), diff --git a/examples/animation/color_animation.rs b/examples/animation/color_animation.rs index 2b72c44bff..df7a764bbf 100644 --- a/examples/animation/color_animation.rs +++ b/examples/animation/color_animation.rs @@ -3,8 +3,8 @@ use bevy::{math::VectorSpace, prelude::*}; // We define this trait so we can reuse the same code for multiple color types that may be implemented using curves. -trait CurveColor: VectorSpace + Into + Send + Sync + 'static {} -impl + Send + Sync + 'static> CurveColor for T {} +trait CurveColor: VectorSpace + Into + Send + Sync + 'static {} +impl + Into + Send + Sync + 'static> CurveColor for T {} // We define this trait so we can reuse the same code for multiple color types that may be implemented using mixing. trait MixedColor: Mix + Into + Send + Sync + 'static {} diff --git a/examples/asset/asset_settings.rs b/examples/asset/asset_settings.rs index cfc76d774a..9f4c1fe507 100644 --- a/examples/asset/asset_settings.rs +++ b/examples/asset/asset_settings.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { // filtering. This tends to work much better for pixel art assets. // A good reference when filling this out is to check out [ImageLoaderSettings::default()] // and follow to the default implementation of each fields type. - // https://docs.rs/bevy/latest/bevy/render/texture/struct.ImageLoaderSettings.html# + // https://docs.rs/bevy/latest/bevy/image/struct.ImageLoaderSettings.html commands.spawn(( Sprite { image: asset_server.load("bevy_pixel_dark_with_meta.png"), diff --git a/examples/async_tasks/external_source_external_thread.rs b/examples/async_tasks/external_source_external_thread.rs index 1b7bb27b16..1b437ed76c 100644 --- a/examples/async_tasks/external_source_external_thread.rs +++ b/examples/async_tasks/external_source_external_thread.rs @@ -54,7 +54,7 @@ fn spawn_text(mut commands: Commands, mut reader: EventReader) { for (per_frame, event) in reader.read().enumerate() { commands.spawn(( Text2d::new(event.0.to_string()), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(per_frame as f32 * 100.0, 300.0, 0.0), )); } diff --git a/examples/audio/audio_control.rs b/examples/audio/audio_control.rs index 19bb8c807a..3a6e4a609a 100644 --- a/examples/audio/audio_control.rs +++ b/examples/audio/audio_control.rs @@ -1,6 +1,6 @@ //! This example illustrates how to load and play an audio file, and control how it's played. -use bevy::{audio::Volume, math::ops, prelude::*}; +use bevy::{math::ops, prelude::*}; fn main() { App::new() @@ -105,9 +105,9 @@ fn volume( if keyboard_input.just_pressed(KeyCode::Equal) { let current_volume = sink.volume(); - sink.set_volume(current_volume + Volume::Linear(0.1)); + sink.set_volume(current_volume.increase_by_percentage(10.0)); } else if keyboard_input.just_pressed(KeyCode::Minus) { let current_volume = sink.volume(); - sink.set_volume(current_volume - Volume::Linear(0.1)); + sink.set_volume(current_volume.increase_by_percentage(-10.0)); } } diff --git a/examples/audio/soundtrack.rs b/examples/audio/soundtrack.rs index 8a6a0dfb9a..27163b90d0 100644 --- a/examples/audio/soundtrack.rs +++ b/examples/audio/soundtrack.rs @@ -115,7 +115,9 @@ fn fade_in( ) { for (mut audio, entity) in audio_sink.iter_mut() { let current_volume = audio.volume(); - audio.set_volume(current_volume + Volume::Linear(time.delta_secs() / FADE_TIME)); + audio.set_volume( + current_volume.fade_towards(Volume::Linear(1.0), time.delta_secs() / FADE_TIME), + ); if audio.volume().to_linear() >= 1.0 { audio.set_volume(Volume::Linear(1.0)); commands.entity(entity).remove::(); @@ -132,7 +134,9 @@ fn fade_out( ) { for (mut audio, entity) in audio_sink.iter_mut() { let current_volume = audio.volume(); - audio.set_volume(current_volume - Volume::Linear(time.delta_secs() / FADE_TIME)); + audio.set_volume( + current_volume.fade_towards(Volume::Linear(0.0), time.delta_secs() / FADE_TIME), + ); if audio.volume().to_linear() <= 0.0 { commands.entity(entity).despawn(); } diff --git a/examples/camera/2d_on_ui.rs b/examples/camera/2d_on_ui.rs new file mode 100644 index 0000000000..df54da98b9 --- /dev/null +++ b/examples/camera/2d_on_ui.rs @@ -0,0 +1,70 @@ +//! This example shows how to render 2D objects on top of Bevy UI, by using a second camera with a higher `order` than the UI camera. + +use bevy::{color::palettes::tailwind, prelude::*, render::view::RenderLayers}; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, rotate_sprite) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + // The default camera. `IsDefaultUiCamera` makes this the default camera to render UI elements to. Alternatively, you can add the `UiTargetCamera` component to root UI nodes to define which camera they should be rendered to. + commands.spawn((Camera2d, IsDefaultUiCamera)); + + // The second camera. The higher order means that this camera will be rendered after the first camera. We will render to this camera to draw on top of the UI. + commands.spawn(( + Camera2d, + Camera { + order: 1, + // Don't draw anything in the background, to see the previous camera. + clear_color: ClearColorConfig::None, + ..default() + }, + // This camera will only render entities which are on the same render layer. + RenderLayers::layer(1), + )); + + commands.spawn(( + // We could also use a `UiTargetCamera` component here instead of the general `IsDefaultUiCamera`. + Node { + width: Val::Percent(100.), + height: Val::Percent(100.), + display: Display::Flex, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + ..default() + }, + BackgroundColor(tailwind::ROSE_400.into()), + children![( + Node { + height: Val::Percent(30.), + width: Val::Percent(20.), + min_height: Val::Px(150.), + min_width: Val::Px(150.), + border: UiRect::all(Val::Px(2.)), + ..default() + }, + BorderRadius::all(Val::Percent(25.0)), + BorderColor::all(Color::WHITE), + )], + )); + + // This 2D object will be rendered on the second camera, on top of the default camera where the UI is rendered. + commands.spawn(( + Sprite { + image: asset_server.load("textures/rpg/chars/sensei/sensei.png"), + custom_size: Some(Vec2::new(100., 100.)), + ..default() + }, + RenderLayers::layer(1), + )); +} + +fn rotate_sprite(time: Res