From 30cda2351f6bb7857562d80297e5112a095f723a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 21:12:41 +0200 Subject: [PATCH 01/26] Bump crate-ci/typos from 1.20.10 to 1.21.0 (#13256) Bumps [crate-ci/typos](https://github.com/crate-ci/typos) from 1.20.10 to 1.21.0.
Release notes

Sourced from crate-ci/typos's releases.

v1.21.0

[1.21.0] - 2024-04-30

Fixes

Changelog

Sourced from crate-ci/typos's changelog.

[1.21.0] - 2024-04-30

Fixes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.20.10&new-version=1.21.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bc582b9118ce..f7425545af51a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.20.10 + uses: crate-ci/typos@v1.21.0 - name: Typos info if: failure() run: | From 59b52fc94ec6aed72081856d3606e6754fbf503c Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 6 May 2024 15:06:10 -0500 Subject: [PATCH 02/26] Modulate the emissive texture by the emissive color again. (#13251) Fixes a regression introduced by #13031. --- crates/bevy_pbr/src/render/pbr_fragment.wgsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 7a900799949c6..6ff31260f7133 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -153,7 +153,7 @@ fn pbr_input_from_standard_material( var emissive: vec4 = pbr_bindings::material.emissive; #ifdef VERTEX_UVS if ((pbr_bindings::material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_EMISSIVE_TEXTURE_BIT) != 0u) { - emissive = vec4(pbr_functions::sample_texture( + emissive = vec4(emissive.rgb * pbr_functions::sample_texture( pbr_bindings::emissive_texture, pbr_bindings::emissive_sampler, uv, From bb76a2c69cb4a7fee87dbaef4e7463af8ed4cbb8 Mon Sep 17 00:00:00 2001 From: andristarr Date: Mon, 6 May 2024 22:49:32 +0200 Subject: [PATCH 03/26] multi_threaded feature rename (#12997) # Objective Fixes #12966 ## Solution Renaming multi_threaded feature to match snake case ## Migration Guide Bevy feature multi-threaded should be refered to multi_threaded from now on. --- Cargo.toml | 4 ++-- benches/Cargo.toml | 2 +- crates/bevy_asset/Cargo.toml | 2 +- crates/bevy_asset/src/io/file/mod.rs | 4 ++-- crates/bevy_asset/src/lib.rs | 22 +++++++++---------- crates/bevy_asset/src/processor/mod.rs | 12 +++++----- crates/bevy_ecs/Cargo.toml | 2 +- crates/bevy_ecs/src/event.rs | 4 ++-- crates/bevy_ecs/src/query/par_iter.rs | 6 ++--- crates/bevy_ecs/src/query/state.rs | 2 +- crates/bevy_ecs/src/schedule/executor/mod.rs | 6 ++--- .../src/schedule/executor/multi_threaded.rs | 2 +- crates/bevy_ecs/src/schedule/schedule.rs | 2 +- crates/bevy_internal/Cargo.toml | 10 ++++----- crates/bevy_internal/src/default_plugins.rs | 2 +- crates/bevy_render/Cargo.toml | 2 +- crates/bevy_render/src/lib.rs | 2 +- .../src/render_resource/pipeline_cache.rs | 4 ++-- crates/bevy_tasks/Cargo.toml | 2 +- crates/bevy_tasks/src/lib.rs | 12 +++++----- docs/cargo_features.md | 2 +- 21 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 593a73af98a2a..50b686ce00e06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,7 +67,7 @@ default = [ "bevy_sprite", "bevy_text", "bevy_ui", - "multi-threaded", + "multi_threaded", "png", "hdr", "vorbis", @@ -252,7 +252,7 @@ symphonia-wav = ["bevy_internal/symphonia-wav"] serialize = ["bevy_internal/serialize"] # Enables multithreaded parallelism in the engine. Disabling it forces all engine tasks to run on a single thread. -multi-threaded = ["bevy_internal/multi-threaded"] +multi_threaded = ["bevy_internal/multi_threaded"] # Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io. async-io = ["bevy_internal/async-io"] diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 417b3a225d243..3df074a75c045 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -11,7 +11,7 @@ rand = "0.8" rand_chacha = "0.3" criterion = { version = "0.3", features = ["html_reports"] } bevy_app = { path = "../crates/bevy_app" } -bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi-threaded"] } +bevy_ecs = { path = "../crates/bevy_ecs", features = ["multi_threaded"] } bevy_reflect = { path = "../crates/bevy_reflect" } bevy_tasks = { path = "../crates/bevy_tasks" } bevy_utils = { path = "../crates/bevy_utils" } diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 6d5e3f5fcd56b..c0ce99bacd9af 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -13,7 +13,7 @@ keywords = ["bevy"] [features] file_watcher = ["notify-debouncer-full", "watch"] embedded_watcher = ["file_watcher"] -multi-threaded = ["bevy_tasks/multi-threaded"] +multi_threaded = ["bevy_tasks/multi_threaded"] asset_processor = [] watch = [] trace = [] diff --git a/crates/bevy_asset/src/io/file/mod.rs b/crates/bevy_asset/src/io/file/mod.rs index 19a9ffb0cdae8..3bbc5074fbf2b 100644 --- a/crates/bevy_asset/src/io/file/mod.rs +++ b/crates/bevy_asset/src/io/file/mod.rs @@ -1,9 +1,9 @@ #[cfg(feature = "file_watcher")] mod file_watcher; -#[cfg(feature = "multi-threaded")] +#[cfg(feature = "multi_threaded")] mod file_asset; -#[cfg(not(feature = "multi-threaded"))] +#[cfg(not(feature = "multi_threaded"))] mod sync_file_asset; use bevy_utils::tracing::error; diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 4b3be75ecd80c..3b56b9cd55cef 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -62,11 +62,11 @@ use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}; use bevy_utils::{tracing::error, HashSet}; use std::{any::TypeId, sync::Arc}; -#[cfg(all(feature = "file_watcher", not(feature = "multi-threaded")))] +#[cfg(all(feature = "file_watcher", not(feature = "multi_threaded")))] compile_error!( "The \"file_watcher\" feature for hot reloading requires the \ - \"multi-threaded\" feature to be functional.\n\ - Consider either disabling the \"file_watcher\" feature or enabling \"multi-threaded\"" + \"multi_threaded\" feature to be functional.\n\ + Consider either disabling the \"file_watcher\" feature or enabling \"multi_threaded\"" ); /// Provides "asset" loading and processing functionality. An [`Asset`] is a "runtime value" that is loaded from an [`AssetSource`], @@ -659,8 +659,8 @@ mod tests { #[test] fn load_dependencies() { // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi-threaded"))] - panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + #[cfg(not(feature = "multi_threaded"))] + panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); let dir = Dir::default(); @@ -980,8 +980,8 @@ mod tests { #[test] fn failure_load_states() { // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi-threaded"))] - panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + #[cfg(not(feature = "multi_threaded"))] + panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); let dir = Dir::default(); @@ -1145,8 +1145,8 @@ mod tests { #[test] fn manual_asset_management() { // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi-threaded"))] - panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + #[cfg(not(feature = "multi_threaded"))] + panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); let dir = Dir::default(); let dep_path = "dep.cool.ron"; @@ -1246,8 +1246,8 @@ mod tests { #[test] fn load_folder() { // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded - #[cfg(not(feature = "multi-threaded"))] - panic!("This test requires the \"multi-threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi-threaded"); + #[cfg(not(feature = "multi_threaded"))] + panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded"); let dir = Dir::default(); diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index 31452a0f85840..bd33f9bb15796 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -152,9 +152,9 @@ impl AssetProcessor { /// Starts the processor in a background thread. pub fn start(_processor: Res) { - #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] error!("Cannot run AssetProcessor in single threaded mode (or WASM) yet."); - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { let processor = _processor.clone(); std::thread::spawn(move || { @@ -171,7 +171,7 @@ impl AssetProcessor { /// * Scan the unprocessed [`AssetReader`] and remove any final processed assets that are invalid or no longer exist. /// * For each asset in the unprocessed [`AssetReader`], kick off a new "process job", which will process the asset /// (if the latest version of the asset has not been processed). - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] pub fn process_assets(&self) { let start_time = std::time::Instant::now(); debug!("Processing Assets"); @@ -322,9 +322,9 @@ impl AssetProcessor { "Folder {} was added. Attempting to re-process", AssetPath::from_path(&path).with_source(source.id()) ); - #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] error!("AddFolder event cannot be handled in single threaded mode (or WASM) yet."); - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] IoTaskPool::get().scope(|scope| { scope.spawn(async move { self.process_assets_internal(scope, source, path) @@ -439,7 +439,7 @@ impl AssetProcessor { } #[allow(unused)] - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] async fn process_assets_internal<'scope>( &'scope self, scope: &'scope bevy_tasks::Scope<'scope, '_, ()>, diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 62a7fce6551c9..3b7c1640a31af 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -11,7 +11,7 @@ categories = ["game-engines", "data-structures"] [features] trace = [] -multi-threaded = ["bevy_tasks/multi-threaded", "arrayvec"] +multi_threaded = ["bevy_tasks/multi_threaded", "arrayvec"] bevy_debug_stepping = [] default = ["bevy_reflect"] diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index d4fada1139298..30835e62dd32a 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -928,12 +928,12 @@ impl<'a, E: Event> EventParIter<'a, E> { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool pub fn for_each_with_id) + Send + Sync + Clone>(self, func: FN) { - #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] { self.into_iter().for_each(|(e, i)| func(e, i)); } - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { let pool = bevy_tasks::ComputeTaskPool::get(); let thread_count = pool.thread_num(); diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index f433c31014a5b..7889228ba7af6 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -78,7 +78,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { func(&mut init, item); init }; - #[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] { let init = init(); // SAFETY: @@ -93,7 +93,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { .fold(init, func); } } - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { let thread_count = bevy_tasks::ComputeTaskPool::get().thread_num(); if thread_count <= 1 { @@ -122,7 +122,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { } } - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] fn get_batch_size(&self, thread_count: usize) -> usize { let max_items = || { let id_iter = self.state.matched_storage_ids.iter(); diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 5e52178837072..6cbae544f929d 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1393,7 +1393,7 @@ impl QueryState { /// with a mismatched [`WorldId`] is unsound. /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, T, FN, INIT>( &self, init_accum: INIT, diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 3bf2f7edeb454..3d76fdb184231 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -38,18 +38,18 @@ pub enum ExecutorKind { /// /// Useful if you're dealing with a single-threaded environment, saving your threads for /// other things, or just trying minimize overhead. - #[cfg_attr(any(target_arch = "wasm32", not(feature = "multi-threaded")), default)] + #[cfg_attr(any(target_arch = "wasm32", not(feature = "multi_threaded")), default)] SingleThreaded, /// Like [`SingleThreaded`](ExecutorKind::SingleThreaded) but calls [`apply_deferred`](crate::system::System::apply_deferred) /// immediately after running each system. Simple, /// Runs the schedule using a thread pool. Non-conflicting systems can run in parallel. - #[cfg_attr(all(not(target_arch = "wasm32"), feature = "multi-threaded"), default)] + #[cfg_attr(all(not(target_arch = "wasm32"), feature = "multi_threaded"), default)] MultiThreaded, } /// Holds systems and conditions of a [`Schedule`](super::Schedule) sorted in topological order -/// (along with dependency information for multi-threaded execution). +/// (along with dependency information for `multi_threaded` execution). /// /// Since the arrays are sorted in the same order, elements are referenced by their index. /// [`FixedBitSet`] is used as a smaller, more efficient substitute of `HashSet`. diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index fa9a19058081e..39606d998e3a2 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -317,7 +317,7 @@ impl<'scope, 'env: 'scope, 'sys> Context<'scope, 'env, 'sys> { } impl MultiThreadedExecutor { - /// Creates a new multi-threaded executor for use with a [`Schedule`]. + /// Creates a new `multi_threaded` executor for use with a [`Schedule`]. /// /// [`Schedule`]: crate::schedule::Schedule pub fn new() -> Self { diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 89a65ecd3708f..c9dc06438f0cf 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -1419,7 +1419,7 @@ impl ScheduleGraph { let hg_node_count = self.hierarchy.graph.node_count(); // get the number of dependencies and the immediate dependents of each system - // (needed by multi-threaded executor to run systems in the correct order) + // (needed by multi_threaded executor to run systems in the correct order) let mut system_dependencies = Vec::with_capacity(sys_count); let mut system_dependents = Vec::with_capacity(sys_count); for &sys_id in &dg_system_ids { diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 36b63ff19361c..0e4713bba94ed 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -77,11 +77,11 @@ serialize = [ "bevy_ui?/serialize", "bevy_color?/serialize", ] -multi-threaded = [ - "bevy_asset?/multi-threaded", - "bevy_ecs/multi-threaded", - "bevy_render?/multi-threaded", - "bevy_tasks/multi-threaded", +multi_threaded = [ + "bevy_asset?/multi_threaded", + "bevy_ecs/multi_threaded", + "bevy_render?/multi_threaded", + "bevy_tasks/multi_threaded", ] async-io = ["bevy_tasks/async-io"] diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index acfa89d0b659a..10d595df6ea91 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -79,7 +79,7 @@ impl PluginGroup for DefaultPlugins { // compressed texture formats .add(bevy_render::texture::ImagePlugin::default()); - #[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] + #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] { group = group.add(bevy_render::pipelined_rendering::PipelinedRenderingPlugin); } diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 82d16c933733a..7b0511dbd2c56 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -18,7 +18,7 @@ bmp = ["image/bmp"] webp = ["image/webp"] dds = ["ddsfile"] pnm = ["image/pnm"] -multi-threaded = ["bevy_tasks/multi-threaded"] +multi_threaded = ["bevy_tasks/multi_threaded"] shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out", "naga_oil/glsl"] shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"] diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 108adbabb99dd..dea5f7bb7d38f 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -96,7 +96,7 @@ use std::{ pub struct RenderPlugin { pub render_creation: RenderCreation, /// If `true`, disables asynchronous pipeline compilation. - /// This has no effect on macOS, Wasm, iOS, or without the `multi-threaded` feature. + /// This has no effect on macOS, Wasm, iOS, or without the `multi_threaded` feature. pub synchronous_pipeline_compilation: bool, } diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 377e24ce7ec35..678be9b0bd76e 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -982,7 +982,7 @@ impl PipelineCache { #[cfg(all( not(target_arch = "wasm32"), not(target_os = "macos"), - feature = "multi-threaded" + feature = "multi_threaded" ))] fn create_pipeline_task( task: impl Future> + Send + 'static, @@ -1001,7 +1001,7 @@ fn create_pipeline_task( #[cfg(any( target_arch = "wasm32", target_os = "macos", - not(feature = "multi-threaded") + not(feature = "multi_threaded") ))] fn create_pipeline_task( task: impl Future> + Send + 'static, diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index eb9cc232e5bf1..c7db7eeae7aec 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -multi-threaded = ["dep:async-channel", "dep:concurrent-queue"] +multi_threaded = ["dep:async-channel", "dep:concurrent-queue"] [dependencies] futures-lite = "2.0.1" diff --git a/crates/bevy_tasks/src/lib.rs b/crates/bevy_tasks/src/lib.rs index 34011532d6b96..17cfb348ef2c5 100644 --- a/crates/bevy_tasks/src/lib.rs +++ b/crates/bevy_tasks/src/lib.rs @@ -11,14 +11,14 @@ pub use slice::{ParallelSlice, ParallelSliceMut}; mod task; pub use task::Task; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] mod task_pool; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] pub use task_pool::{Scope, TaskPool, TaskPoolBuilder}; -#[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] +#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] mod single_threaded_task_pool; -#[cfg(any(target_arch = "wasm32", not(feature = "multi-threaded")))] +#[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] pub use single_threaded_task_pool::{FakeTask, Scope, TaskPool, TaskPoolBuilder, ThreadExecutor}; mod usages; @@ -26,9 +26,9 @@ mod usages; pub use usages::tick_global_task_pools_on_main_thread; pub use usages::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool}; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] mod thread_executor; -#[cfg(all(not(target_arch = "wasm32"), feature = "multi-threaded"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] pub use thread_executor::{ThreadExecutor, ThreadExecutorTicker}; #[cfg(feature = "async-io")] diff --git a/docs/cargo_features.md b/docs/cargo_features.md index a4ea90d371c3d..8a154ca433176 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -31,7 +31,7 @@ The default feature set enables most of the expected features of a game engine, |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |hdr|HDR image format support| |ktx2|KTX2 compressed texture support| -|multi-threaded|Enables multithreaded parallelism in the engine. Disabling it forces all engine tasks to run on a single thread.| +|multi_threaded|Enables multithreaded parallelism in the engine. Disabling it forces all engine tasks to run on a single thread.| |png|PNG image format support| |sysinfo_plugin|Enables system information diagnostic plugin| |tonemapping_luts|Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method on your `Camera2dBundle` or `Camera3dBundle`.| From 60a73fa60b5175039f54c8d5ec4543af99b636a9 Mon Sep 17 00:00:00 2001 From: Fpgu <35809384+341101@users.noreply.github.com> Date: Tue, 7 May 2024 04:52:05 +0800 Subject: [PATCH 04/26] Use `Dir3` for local axis methods in `GlobalTransform` (#13264) Switched the return type from `Vec3` to `Dir3` for directional axis methods within the `GlobalTransform` component. ## Migration Guide The `GlobalTransform` component's directional axis methods (e.g., `right()`, `left()`, `up()`, `down()`, `back()`, `forward()`) have been updated from returning `Vec3` to `Dir3`. --- crates/bevy_pbr/src/render/light.rs | 2 +- crates/bevy_transform/src/components/global_transform.rs | 8 ++++---- examples/3d/3d_viewport_to_world.rs | 7 +------ 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 74e340c01a33e..8248b93dc7eca 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -910,7 +910,7 @@ pub fn prepare_lights( // we don't use the alpha at all, so no reason to multiply only [0..3] color: Vec4::from_slice(&light.color.to_f32_array()) * light.illuminance, // direction is negated to be ready for N.L - dir_to_light: light.transform.back(), + dir_to_light: light.transform.back().into(), flags: flags.bits(), shadow_depth_bias: light.shadow_depth_bias, shadow_normal_bias: light.shadow_normal_bias, diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index e81cd32f6828e..5ce11f45fbb92 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -2,7 +2,7 @@ use std::ops::Mul; use super::Transform; use bevy_ecs::{component::Component, reflect::ReflectComponent}; -use bevy_math::{Affine3A, Mat4, Quat, Vec3, Vec3A}; +use bevy_math::{Affine3A, Dir3, Mat4, Quat, Vec3, Vec3A}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// Describe the position of an entity relative to the reference frame. @@ -42,13 +42,13 @@ macro_rules! impl_local_axis { ($pos_name: ident, $neg_name: ident, $axis: ident) => { #[doc=std::concat!("Return the local ", std::stringify!($pos_name), " vector (", std::stringify!($axis) ,").")] #[inline] - pub fn $pos_name(&self) -> Vec3 { - (self.0.matrix3 * Vec3::$axis).normalize() + pub fn $pos_name(&self) -> Dir3 { + Dir3::new_unchecked((self.0.matrix3 * Vec3::$axis).normalize()) } #[doc=std::concat!("Return the local ", std::stringify!($neg_name), " vector (-", std::stringify!($axis) ,").")] #[inline] - pub fn $neg_name(&self) -> Vec3 { + pub fn $neg_name(&self) -> Dir3 { -self.$pos_name() } }; diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 0347bc86b737d..9e4e4f73da3ed 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -37,12 +37,7 @@ fn draw_cursor( let point = ray.get_point(distance); // Draw a circle just above the ground plane at that position. - gizmos.circle( - point + ground.up() * 0.01, - Dir3::new_unchecked(ground.up()), // Up vector is already normalized. - 0.2, - Color::WHITE, - ); + gizmos.circle(point + ground.up() * 0.01, ground.up(), 0.2, Color::WHITE); } #[derive(Component)] From 6c78c7b4343c706978c9af525bb88b4d05230599 Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 6 May 2024 16:53:02 -0400 Subject: [PATCH 05/26] Refactor align example to use `Dir3` random sampling (#13259) # Objective Since `align` was introduced, it has been reworked to allow the input of `Dir3` instead of `Vec3`, and we also introduced random sampling for points on a sphere and then for `Dir3`. Previously, this example rolled its own random generation, but it doesn't need to any more. ## Solution Refactor the 'align' example to use `Dir3` instead of `Vec3`, using the `bevy_math` API for random directions. --- examples/transforms/align.rs | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs index f5b152775f050..acfe4a8d814d7 100644 --- a/examples/transforms/align.rs +++ b/examples/transforms/align.rs @@ -5,7 +5,6 @@ use bevy::input::mouse::{MouseButtonInput, MouseMotion}; use bevy::prelude::*; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; -use std::f32::consts::PI; fn main() { App::new() @@ -33,7 +32,7 @@ struct Cube { } #[derive(Component)] -struct RandomAxes(Vec3, Vec3); +struct RandomAxes(Dir3, Dir3); #[derive(Component)] struct Instructions; @@ -80,8 +79,8 @@ fn setup( }); // Initialize random axes - let first = random_direction(&mut seeded_rng); - let second = random_direction(&mut seeded_rng); + let first = seeded_rng.gen(); + let second = seeded_rng.gen(); commands.spawn(RandomAxes(first, second)); // Finally, our cube that is going to rotate @@ -185,8 +184,8 @@ fn handle_keypress( if keyboard.just_pressed(KeyCode::KeyR) { // Randomize the target axes - let first = random_direction(&mut seeded_rng.0); - let second = random_direction(&mut seeded_rng.0); + let first = seeded_rng.0.gen(); + let second = seeded_rng.0.gen(); *random_axes = RandomAxes(first, second); // Stop the cube and set it up to transform from its present orientation to the new one @@ -243,22 +242,6 @@ fn arrow_ends(transform: &Transform, axis: Vec3, length: f32) -> (Vec3, Vec3) { (transform.translation, transform.translation + local_vector) } -fn random_direction(rng: &mut impl Rng) -> Vec3 { - let height = rng.gen::() * 2. - 1.; - let theta = rng.gen::() * 2. * PI; - - build_direction(height, theta) -} - -fn build_direction(height: f32, theta: f32) -> Vec3 { - let z = height; - let m = f32::acos(z).sin(); - let x = theta.cos() * m; - let y = theta.sin() * m; - - Vec3::new(x, y, z) -} - // This is where `Transform::align` is actually used! // Note that the choice of `Vec3::X` and `Vec3::Y` here matches the use of those in `draw_cube_axes`. fn random_axes_target_alignment(random_axes: &RandomAxes) -> Transform { From fa0745fdd050be731605b53be97b143a346014e9 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 6 May 2024 14:15:10 -0700 Subject: [PATCH 06/26] Remove bevy log's usage of non send resource (#13252) # Objective I'm adopting #9122 and pulling some of the non controversial changes out to make the final pr easier to review. This pr removes the NonSend resource usage from `bevy_log`. ## Solution `tracing-chrome` uses a guard that is stored in the world, so that when it is dropped the json log file is written out. The guard is Send + !Sync, so we can store it in a SyncCell to hold it in a regular resource instead of using a non send resource. ## Testing Tested by running an example with `-F tracing chrome` and making sure there weren't any errors and the json file was created. --- ## Changelog - replaced `bevy_log`'s usage of a non send resource. --- crates/bevy_log/src/lib.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index f37f0729a847e..7a9a8492870f0 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -54,6 +54,15 @@ use tracing_log::LogTracer; #[cfg(feature = "tracing-chrome")] use tracing_subscriber::fmt::{format::DefaultFields, FormattedFields}; use tracing_subscriber::{prelude::*, registry::Registry, EnvFilter}; +#[cfg(feature = "tracing-chrome")] +use {bevy_ecs::system::Resource, bevy_utils::synccell::SyncCell}; + +/// Wrapper resource for `tracing-chrome`'s flush guard. +/// When the guard is dropped the chrome log is written to file. +#[cfg(feature = "tracing-chrome")] +#[allow(dead_code)] +#[derive(Resource)] +pub(crate) struct FlushGuard(SyncCell); /// Adds logging to Apps. This plugin is part of the `DefaultPlugins`. Adding /// this plugin will setup a collector appropriate to your target platform: @@ -177,7 +186,7 @@ impl Plugin for LogPlugin { } })) .build(); - app.insert_non_send_resource(guard); + app.insert_resource(FlushGuard(SyncCell::new(guard))); chrome_layer }; From 4f9f987099224ca41aab91a582b80fca8eb969f8 Mon Sep 17 00:00:00 2001 From: Lynn <62256001+solis-lumine-vorago@users.noreply.github.com> Date: Mon, 6 May 2024 23:31:51 +0200 Subject: [PATCH 07/26] Ellipse functions (#13025) # Objective - Add some useful methods to `Ellipse` ## Solution - Added `Ellipse::perimeter()` and `::focal_length()` --------- Co-authored-by: IQuick 143 --- crates/bevy_math/src/primitives/dim2.rs | 82 +++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 14660edc5b03b..e3c613a9fae5c 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -118,6 +118,73 @@ impl Ellipse { (a * a - b * b).sqrt() / a } + #[inline(always)] + /// Get the focal length of the ellipse. This corresponds to the distance between one of the foci and the center of the ellipse. + /// + /// The focal length of an ellipse is related to its eccentricity by `eccentricity = focal_length / semi_major` + pub fn focal_length(&self) -> f32 { + let a = self.semi_major(); + let b = self.semi_minor(); + + (a * a - b * b).sqrt() + } + + #[inline(always)] + /// Get an approximation for the perimeter or circumference of the ellipse. + /// + /// The approximation is reasonably precise with a relative error less than 0.007%, getting more precise as the eccentricity of the ellipse decreases. + pub fn perimeter(&self) -> f32 { + let a = self.semi_major(); + let b = self.semi_minor(); + + // In the case that `a == b`, the ellipse is a circle + if a / b - 1. < 1e-5 { + return PI * (a + b); + }; + + // In the case that `a` is much larger than `b`, the ellipse is a line + if a / b > 1e4 { + return 4. * a; + }; + + // These values are the result of (0.5 choose n)^2 where n is the index in the array + // They could be calculated on the fly but hardcoding them yields more accurate and faster results + // because the actual calculation for these values involves factorials and numbers > 10^23 + const BINOMIAL_COEFFICIENTS: [f32; 21] = [ + 1., + 0.25, + 0.015625, + 0.00390625, + 0.0015258789, + 0.00074768066, + 0.00042057037, + 0.00025963783, + 0.00017140154, + 0.000119028846, + 0.00008599834, + 0.00006414339, + 0.000049109784, + 0.000038430585, + 0.000030636627, + 0.000024815668, + 0.000020380836, + 0.000016942893, + 0.000014236736, + 0.000012077564, + 0.000010333865, + ]; + + // The algorithm used here is the Gauss-Kummer infinite series expansion of the elliptic integral expression for the perimeter of ellipses + // For more information see https://www.wolframalpha.com/input/?i=gauss-kummer+series + // We only use the terms up to `i == 20` for this approximation + let h = ((a - b) / (a + b)).powi(2); + + PI * (a + b) + * (0..=20) + .map(|i| BINOMIAL_COEFFICIENTS[i] * h.powi(i as i32)) + .sum::() + } + /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. #[inline(always)] pub fn semi_major(&self) -> f32 { @@ -861,6 +928,21 @@ mod tests { assert_eq!(circle.eccentricity(), 0., "incorrect circle eccentricity"); } + #[test] + fn ellipse_perimeter() { + let circle = Ellipse::new(1., 1.); + assert_relative_eq!(circle.perimeter(), 6.2831855); + + let line = Ellipse::new(75_000., 0.5); + assert_relative_eq!(line.perimeter(), 300_000.); + + let ellipse = Ellipse::new(0.5, 2.); + assert_relative_eq!(ellipse.perimeter(), 8.578423); + + let ellipse = Ellipse::new(5., 3.); + assert_relative_eq!(ellipse.perimeter(), 25.526999); + } + #[test] fn triangle_math() { let triangle = Triangle2d::new( From 1126b5a3d6fa58c0daaa11583abc90b152fceb98 Mon Sep 17 00:00:00 2001 From: moonlightaria <72459063+moonlightaria@users.noreply.github.com> Date: Tue, 7 May 2024 01:23:53 -0400 Subject: [PATCH 08/26] replace std::f32::EPSILON with f32::EPSILON (#13267) # Objective fixes clippy warning related to using a std::f32::EPSILON which is planned to be depreciated for f32::EPSILON --- .../bevy_math/src/bounding/bounded2d/mod.rs | 36 +++++++++---------- .../bevy_math/src/bounding/bounded3d/mod.rs | 36 +++++++++---------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index 471e7487dbeb6..b97da70e3d17a 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -271,12 +271,12 @@ mod aabb2d_tests { min: Vec2::new(-0.5, -1.), max: Vec2::new(1., 1.), }; - assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < std::f32::EPSILON); + assert!((aabb.center() - Vec2::new(0.25, 0.)).length() < f32::EPSILON); let aabb = Aabb2d { min: Vec2::new(5., -10.), max: Vec2::new(10., -5.), }; - assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < std::f32::EPSILON); + assert!((aabb.center() - Vec2::new(7.5, -7.5)).length() < f32::EPSILON); } #[test] @@ -286,7 +286,7 @@ mod aabb2d_tests { max: Vec2::new(1., 1.), }; let half_size = aabb.half_size(); - assert!((half_size - Vec2::new(0.75, 1.)).length() < std::f32::EPSILON); + assert!((half_size - Vec2::new(0.75, 1.)).length() < f32::EPSILON); } #[test] @@ -295,12 +295,12 @@ mod aabb2d_tests { min: Vec2::new(-1., -1.), max: Vec2::new(1., 1.), }; - assert!((aabb.visible_area() - 4.).abs() < std::f32::EPSILON); + assert!((aabb.visible_area() - 4.).abs() < f32::EPSILON); let aabb = Aabb2d { min: Vec2::new(0., 0.), max: Vec2::new(1., 0.5), }; - assert!((aabb.visible_area() - 0.5).abs() < std::f32::EPSILON); + assert!((aabb.visible_area() - 0.5).abs() < f32::EPSILON); } #[test] @@ -332,8 +332,8 @@ mod aabb2d_tests { max: Vec2::new(0.75, 1.), }; let merged = a.merge(&b); - assert!((merged.min - Vec2::new(-2., -1.)).length() < std::f32::EPSILON); - assert!((merged.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON); + assert!((merged.min - Vec2::new(-2., -1.)).length() < f32::EPSILON); + assert!((merged.max - Vec2::new(1., 1.)).length() < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -347,8 +347,8 @@ mod aabb2d_tests { max: Vec2::new(1., 1.), }; let padded = a.grow(Vec2::ONE); - assert!((padded.min - Vec2::new(-2., -2.)).length() < std::f32::EPSILON); - assert!((padded.max - Vec2::new(2., 2.)).length() < std::f32::EPSILON); + assert!((padded.min - Vec2::new(-2., -2.)).length() < f32::EPSILON); + assert!((padded.max - Vec2::new(2., 2.)).length() < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -360,8 +360,8 @@ mod aabb2d_tests { max: Vec2::new(2., 2.), }; let shrunk = a.shrink(Vec2::ONE); - assert!((shrunk.min - Vec2::new(-1., -1.)).length() < std::f32::EPSILON); - assert!((shrunk.max - Vec2::new(1., 1.)).length() < std::f32::EPSILON); + assert!((shrunk.min - Vec2::new(-1., -1.)).length() < f32::EPSILON); + assert!((shrunk.max - Vec2::new(1., 1.)).length() < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -373,8 +373,8 @@ mod aabb2d_tests { max: Vec2::ONE, }; let scaled = a.scale_around_center(Vec2::splat(2.)); - assert!((scaled.min - Vec2::splat(-2.)).length() < std::f32::EPSILON); - assert!((scaled.max - Vec2::splat(2.)).length() < std::f32::EPSILON); + assert!((scaled.min - Vec2::splat(-2.)).length() < f32::EPSILON); + assert!((scaled.max - Vec2::splat(2.)).length() < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } @@ -647,8 +647,8 @@ mod bounding_circle_tests { let a = BoundingCircle::new(Vec2::ONE, 5.); let b = BoundingCircle::new(Vec2::new(1., -4.), 1.); let merged = a.merge(&b); - assert!((merged.center - Vec2::new(1., 0.5)).length() < std::f32::EPSILON); - assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON); + assert!((merged.center - Vec2::new(1., 0.5)).length() < f32::EPSILON); + assert!((merged.radius() - 5.5).abs() < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -680,7 +680,7 @@ mod bounding_circle_tests { fn grow() { let a = BoundingCircle::new(Vec2::ONE, 5.); let padded = a.grow(1.25); - assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON); + assert!((padded.radius() - 6.25).abs() < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -689,7 +689,7 @@ mod bounding_circle_tests { fn shrink() { let a = BoundingCircle::new(Vec2::ONE, 5.); let shrunk = a.shrink(0.5); - assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON); + assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -698,7 +698,7 @@ mod bounding_circle_tests { fn scale_around_center() { let a = BoundingCircle::new(Vec2::ONE, 5.); let scaled = a.scale_around_center(2.); - assert!((scaled.radius() - 10.).abs() < std::f32::EPSILON); + assert!((scaled.radius() - 10.).abs() < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index c069975dfa933..4c4ad16749e3d 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -268,12 +268,12 @@ mod aabb3d_tests { min: Vec3A::new(-0.5, -1., -0.5), max: Vec3A::new(1., 1., 2.), }; - assert!((aabb.center() - Vec3A::new(0.25, 0., 0.75)).length() < std::f32::EPSILON); + assert!((aabb.center() - Vec3A::new(0.25, 0., 0.75)).length() < f32::EPSILON); let aabb = Aabb3d { min: Vec3A::new(5., 5., -10.), max: Vec3A::new(10., 10., -5.), }; - assert!((aabb.center() - Vec3A::new(7.5, 7.5, -7.5)).length() < std::f32::EPSILON); + assert!((aabb.center() - Vec3A::new(7.5, 7.5, -7.5)).length() < f32::EPSILON); } #[test] @@ -282,7 +282,7 @@ mod aabb3d_tests { min: Vec3A::new(-0.5, -1., -0.5), max: Vec3A::new(1., 1., 2.), }; - assert!((aabb.half_size() - Vec3A::new(0.75, 1., 1.25)).length() < std::f32::EPSILON); + assert!((aabb.half_size() - Vec3A::new(0.75, 1., 1.25)).length() < f32::EPSILON); } #[test] @@ -291,12 +291,12 @@ mod aabb3d_tests { min: Vec3A::new(-1., -1., -1.), max: Vec3A::new(1., 1., 1.), }; - assert!((aabb.visible_area() - 12.).abs() < std::f32::EPSILON); + assert!((aabb.visible_area() - 12.).abs() < f32::EPSILON); let aabb = Aabb3d { min: Vec3A::new(0., 0., 0.), max: Vec3A::new(1., 0.5, 0.25), }; - assert!((aabb.visible_area() - 0.875).abs() < std::f32::EPSILON); + assert!((aabb.visible_area() - 0.875).abs() < f32::EPSILON); } #[test] @@ -328,8 +328,8 @@ mod aabb3d_tests { max: Vec3A::new(0.75, 1., 2.), }; let merged = a.merge(&b); - assert!((merged.min - Vec3A::new(-2., -1., -1.)).length() < std::f32::EPSILON); - assert!((merged.max - Vec3A::new(1., 1., 2.)).length() < std::f32::EPSILON); + assert!((merged.min - Vec3A::new(-2., -1., -1.)).length() < f32::EPSILON); + assert!((merged.max - Vec3A::new(1., 1., 2.)).length() < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -343,8 +343,8 @@ mod aabb3d_tests { max: Vec3A::new(1., 1., 1.), }; let padded = a.grow(Vec3A::ONE); - assert!((padded.min - Vec3A::new(-2., -2., -2.)).length() < std::f32::EPSILON); - assert!((padded.max - Vec3A::new(2., 2., 2.)).length() < std::f32::EPSILON); + assert!((padded.min - Vec3A::new(-2., -2., -2.)).length() < f32::EPSILON); + assert!((padded.max - Vec3A::new(2., 2., 2.)).length() < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -356,8 +356,8 @@ mod aabb3d_tests { max: Vec3A::new(2., 2., 2.), }; let shrunk = a.shrink(Vec3A::ONE); - assert!((shrunk.min - Vec3A::new(-1., -1., -1.)).length() < std::f32::EPSILON); - assert!((shrunk.max - Vec3A::new(1., 1., 1.)).length() < std::f32::EPSILON); + assert!((shrunk.min - Vec3A::new(-1., -1., -1.)).length() < f32::EPSILON); + assert!((shrunk.max - Vec3A::new(1., 1., 1.)).length() < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -369,8 +369,8 @@ mod aabb3d_tests { max: Vec3A::ONE, }; let scaled = a.scale_around_center(Vec3A::splat(2.)); - assert!((scaled.min - Vec3A::splat(-2.)).length() < std::f32::EPSILON); - assert!((scaled.max - Vec3A::splat(2.)).length() < std::f32::EPSILON); + assert!((scaled.min - Vec3A::splat(-2.)).length() < f32::EPSILON); + assert!((scaled.max - Vec3A::splat(2.)).length() < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } @@ -671,8 +671,8 @@ mod bounding_sphere_tests { let a = BoundingSphere::new(Vec3::ONE, 5.); let b = BoundingSphere::new(Vec3::new(1., 1., -4.), 1.); let merged = a.merge(&b); - assert!((merged.center - Vec3A::new(1., 1., 0.5)).length() < std::f32::EPSILON); - assert!((merged.radius() - 5.5).abs() < std::f32::EPSILON); + assert!((merged.center - Vec3A::new(1., 1., 0.5)).length() < f32::EPSILON); + assert!((merged.radius() - 5.5).abs() < f32::EPSILON); assert!(merged.contains(&a)); assert!(merged.contains(&b)); assert!(!a.contains(&merged)); @@ -704,7 +704,7 @@ mod bounding_sphere_tests { fn grow() { let a = BoundingSphere::new(Vec3::ONE, 5.); let padded = a.grow(1.25); - assert!((padded.radius() - 6.25).abs() < std::f32::EPSILON); + assert!((padded.radius() - 6.25).abs() < f32::EPSILON); assert!(padded.contains(&a)); assert!(!a.contains(&padded)); } @@ -713,7 +713,7 @@ mod bounding_sphere_tests { fn shrink() { let a = BoundingSphere::new(Vec3::ONE, 5.); let shrunk = a.shrink(0.5); - assert!((shrunk.radius() - 4.5).abs() < std::f32::EPSILON); + assert!((shrunk.radius() - 4.5).abs() < f32::EPSILON); assert!(a.contains(&shrunk)); assert!(!shrunk.contains(&a)); } @@ -722,7 +722,7 @@ mod bounding_sphere_tests { fn scale_around_center() { let a = BoundingSphere::new(Vec3::ONE, 5.); let scaled = a.scale_around_center(2.); - assert!((scaled.radius() - 10.).abs() < std::f32::EPSILON); + assert!((scaled.radius() - 10.).abs() < f32::EPSILON); assert!(!a.contains(&scaled)); assert!(scaled.contains(&a)); } From 4737106bdd2f2087675981fcf82abad1f887caa1 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Tue, 7 May 2024 02:46:41 -0400 Subject: [PATCH 09/26] Extract mesh view layouts logic (#13266) Copied almost verbatim from the volumetric fog PR # Objective - Managing mesh view layouts is complicated ## Solution - Extract it to it's own struct - This was done as part of #13057 and is copied almost verbatim. I wanted to keep this part of the PR it's own atomic commit in case we ever have to revert fog or run a bisect. This change is good whether or not we have volumetric fog. Co-Authored-By: @pcwalton --- crates/bevy_pbr/src/render/mesh.rs | 33 +++------ .../bevy_pbr/src/render/mesh_view_bindings.rs | 74 ++++++++++++++++++- 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 3c499be4410c3..aee311f082355 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -33,15 +33,13 @@ use bevy_render::{ texture::{BevyDefault, DefaultImageSampler, ImageSampler, TextureFormatPixelInfo}, view::{ prepare_view_targets, GpuCulling, RenderVisibilityRanges, ViewTarget, ViewUniformOffset, - ViewVisibility, VisibilityRange, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + ViewVisibility, VisibilityRange, }, Extract, }; use bevy_transform::components::GlobalTransform; use bevy_utils::{tracing::error, tracing::warn, Entry, HashMap, Parallel}; -#[cfg(debug_assertions)] -use bevy_utils::warn_once; use bytemuck::{Pod, Zeroable}; use nonmax::{NonMaxU16, NonMaxU32}; use static_assertions::const_assert_eq; @@ -234,6 +232,7 @@ impl Plugin for MeshRenderPlugin { render_app .insert_resource(indirect_parameters_buffer) + .init_resource::() .init_resource::(); } @@ -1034,7 +1033,8 @@ fn collect_meshes_for_gpu_building( #[derive(Resource, Clone)] pub struct MeshPipeline { - view_layouts: [MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT], + /// A reference to all the mesh pipeline view layouts. + pub view_layouts: MeshPipelineViewLayouts, // This dummy white texture is to be used in place of optional StandardMaterial textures pub dummy_white_gpu_image: GpuImage, pub clustered_forward_buffer_binding_type: BufferBindingType, @@ -1065,18 +1065,12 @@ impl FromWorld for MeshPipeline { Res, Res, Res, + Res, )> = SystemState::new(world); - let (render_device, default_sampler, render_queue) = system_state.get_mut(world); + let (render_device, default_sampler, render_queue, view_layouts) = + system_state.get_mut(world); let clustered_forward_buffer_binding_type = render_device .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); - let visibility_ranges_buffer_binding_type = render_device - .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); - - let view_layouts = generate_view_layouts( - &render_device, - clustered_forward_buffer_binding_type, - visibility_ranges_buffer_binding_type, - ); // A 1x1x1 'all 1.0' texture to use as a dummy texture to use in place of optional StandardMaterial textures let dummy_white_gpu_image = { @@ -1113,7 +1107,7 @@ impl FromWorld for MeshPipeline { }; MeshPipeline { - view_layouts, + view_layouts: view_layouts.clone(), clustered_forward_buffer_binding_type, dummy_white_gpu_image, mesh_layouts: MeshLayouts::new(&render_device), @@ -1141,16 +1135,7 @@ impl MeshPipeline { } pub fn get_view_layout(&self, layout_key: MeshPipelineViewLayoutKey) -> &BindGroupLayout { - let index = layout_key.bits() as usize; - let layout = &self.view_layouts[index]; - - #[cfg(debug_assertions)] - if layout.texture_count > MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES { - // Issue our own warning here because Naga's error message is a bit cryptic in this situation - warn_once!("Too many textures in mesh pipeline view layout, this might cause us to hit `wgpu::Limits::max_sampled_textures_per_shader_stage` in some environments."); - } - - &layout.bind_group_layout + self.view_layouts.get_view_layout(layout_key) } } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index db2aa1831a6fa..e1f9ee573f1e6 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -1,4 +1,4 @@ -use std::{array, num::NonZeroU64}; +use std::{array, num::NonZeroU64, sync::Arc}; use bevy_core_pipeline::{ core_3d::ViewTransmissionTexture, @@ -7,10 +7,12 @@ use bevy_core_pipeline::{ get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, }, }; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, - system::{Commands, Query, Res}, + system::{Commands, Query, Res, Resource}, + world::{FromWorld, World}, }; use bevy_math::Vec4; use bevy_render::{ @@ -19,13 +21,19 @@ use bevy_render::{ render_resource::{binding_types::*, *}, renderer::RenderDevice, texture::{BevyDefault, FallbackImage, FallbackImageMsaa, FallbackImageZero, GpuImage}, - view::{Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms}, + view::{ + Msaa, RenderVisibilityRanges, ViewUniform, ViewUniforms, + VISIBILITY_RANGES_STORAGE_BUFFER_COUNT, + }, }; #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] use bevy_render::render_resource::binding_types::texture_cube; +use bevy_utils::warn_once; use environment_map::EnvironmentMapLight; +#[cfg(debug_assertions)] +use crate::MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES; use crate::{ environment_map::{self, RenderViewEnvironmentMapBindGroupEntries}, irradiance_volume::{ @@ -35,6 +43,7 @@ use crate::{ prepass, FogMeta, GlobalLightMeta, GpuFog, GpuLights, GpuPointLights, LightMeta, LightProbesBuffer, LightProbesUniform, MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionTextures, ShadowSamplers, ViewClusterBindings, ViewShadowBindings, + CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, }; #[derive(Clone)] @@ -330,6 +339,65 @@ fn layout_entries( entries.to_vec() } +/// Stores the view layouts for every combination of pipeline keys. +/// +/// This is wrapped in an [`Arc`] so that it can be efficiently cloned and +/// placed inside specializable pipeline types. +#[derive(Resource, Clone, Deref, DerefMut)] +pub struct MeshPipelineViewLayouts( + pub Arc<[MeshPipelineViewLayout; MeshPipelineViewLayoutKey::COUNT]>, +); + +impl FromWorld for MeshPipelineViewLayouts { + fn from_world(world: &mut World) -> Self { + // Generates all possible view layouts for the mesh pipeline, based on all combinations of + // [`MeshPipelineViewLayoutKey`] flags. + + let render_device = world.resource::(); + + let clustered_forward_buffer_binding_type = render_device + .get_supported_read_only_binding_type(CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT); + let visibility_ranges_buffer_binding_type = render_device + .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); + + Self(Arc::new(array::from_fn(|i| { + let key = MeshPipelineViewLayoutKey::from_bits_truncate(i as u32); + let entries = layout_entries( + clustered_forward_buffer_binding_type, + visibility_ranges_buffer_binding_type, + key, + render_device, + ); + let texture_count: usize = entries + .iter() + .filter(|entry| matches!(entry.ty, BindingType::Texture { .. })) + .count(); + + MeshPipelineViewLayout { + bind_group_layout: render_device + .create_bind_group_layout(key.label().as_str(), &entries), + #[cfg(debug_assertions)] + texture_count, + } + }))) + } +} + +impl MeshPipelineViewLayouts { + pub fn get_view_layout(&self, layout_key: MeshPipelineViewLayoutKey) -> &BindGroupLayout { + let index = layout_key.bits() as usize; + let layout = &self[index]; + + #[cfg(debug_assertions)] + if layout.texture_count > MESH_PIPELINE_VIEW_LAYOUT_SAFE_MAX_TEXTURES { + // Issue our own warning here because Naga's error message is a bit cryptic in this situation + warn_once!("Too many textures in mesh pipeline view layout, this might cause us to hit `wgpu::Limits::max_sampled_textures_per_shader_stage` in some environments."); + } + + &layout.bind_group_layout + } +} + /// Generates all possible view layouts for the mesh pipeline, based on all combinations of /// [`MeshPipelineViewLayoutKey`] flags. pub fn generate_view_layouts( From 22305acf660486554fe6276e580e65a910b9e2df Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Tue, 7 May 2024 03:55:32 -0400 Subject: [PATCH 10/26] Rename `bevy_reflect_derive` folder to `derive` (#13269) # Objective - Some of the "large" crates have sub-crates, usually for things such as macros. - For an example, see [`bevy_ecs_macros` at `bevy_ecs/macros`](https://github.com/bevyengine/bevy/tree/4f9f987099224ca41aab91a582b80fca8eb969f8/crates/bevy_ecs/macros). - The one crate that does not follow this convention is [`bevy_reflect_derive`](https://github.com/bevyengine/bevy/tree/4f9f987099224ca41aab91a582b80fca8eb969f8/crates/bevy_reflect/bevy_reflect_derive), which is in the `bevy_reflect/bevy_reflect_derive` folder and not `bevy_reflect/derive` or `bevy_reflect/macros`. ## Solution - Rename folder `bevy_reflect_derive` to `derive`. - I chose to use `derive` instead of `macros` because the crate name itself ends in `_derive`. (One of only two crates to actually use this convention, funnily enough.) ## Testing - Build and test `bevy_reflect` and `bevy_reflect_derive`. - Apply the following patch to `publish.sh` to run it in `--dry-run` mode, to test that the path has been successfully updated: - If you have any security concerns about applying random diffs, feel free to skip this step. Worst case scenario it fails and Cart has to manually publish a few crates. ```bash # Apply patch to make `publish.sh` *not* actually publish anything. git apply path/to/foo.patch # Make `publish.sh` executable. chmod +x tools/publish.sh # Execute `publish.sh`. ./tools/publish.sh ``` ```patch diff --git a/tools/publish.sh b/tools/publish.sh index b020bad28..fbcc09281 100644 --- a/tools/publish.sh +++ b/tools/publish.sh @@ -49,7 +49,7 @@ crates=( if [ -n "$(git status --porcelain)" ]; then echo "You have local changes!" - exit 1 + # exit 1 fi pushd crates @@ -61,15 +61,15 @@ do cp ../LICENSE-APACHE "$crate" pushd "$crate" git add LICENSE-MIT LICENSE-APACHE - cargo publish --no-verify --allow-dirty + cargo publish --no-verify --allow-dirty --dry-run popd - sleep 20 + # sleep 20 done popd echo "Publishing root crate" -cargo publish --allow-dirty +cargo publish --allow-dirty --dry-run echo "Cleaning local state" git reset HEAD --hard ``` --- ## Changelog - Moved `bevy_reflect_derive` from `crates/bevy_reflect/bevy_reflect_derive` to `crates/bevy_reflect/derive`. --- crates/bevy_reflect/Cargo.toml | 2 +- crates/bevy_reflect/{bevy_reflect_derive => derive}/Cargo.toml | 0 .../{bevy_reflect_derive => derive}/src/container_attributes.rs | 0 .../{bevy_reflect_derive => derive}/src/derive_data.rs | 0 .../{bevy_reflect_derive => derive}/src/documentation.rs | 0 .../{bevy_reflect_derive => derive}/src/enum_utility.rs | 0 .../{bevy_reflect_derive => derive}/src/field_attributes.rs | 0 .../{bevy_reflect_derive => derive}/src/from_reflect.rs | 0 .../{bevy_reflect_derive => derive}/src/impls/enums.rs | 0 .../{bevy_reflect_derive => derive}/src/impls/mod.rs | 0 .../{bevy_reflect_derive => derive}/src/impls/structs.rs | 0 .../{bevy_reflect_derive => derive}/src/impls/tuple_structs.rs | 0 .../{bevy_reflect_derive => derive}/src/impls/typed.rs | 0 .../{bevy_reflect_derive => derive}/src/impls/values.rs | 0 crates/bevy_reflect/{bevy_reflect_derive => derive}/src/lib.rs | 0 .../{bevy_reflect_derive => derive}/src/reflect_value.rs | 0 .../{bevy_reflect_derive => derive}/src/registration.rs | 0 .../{bevy_reflect_derive => derive}/src/serialization.rs | 0 .../{bevy_reflect_derive => derive}/src/trait_reflection.rs | 0 .../{bevy_reflect_derive => derive}/src/type_path.rs | 0 .../bevy_reflect/{bevy_reflect_derive => derive}/src/utility.rs | 0 tools/publish.sh | 2 +- 22 files changed, 2 insertions(+), 2 deletions(-) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/Cargo.toml (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/container_attributes.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/derive_data.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/documentation.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/enum_utility.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/field_attributes.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/from_reflect.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/impls/enums.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/impls/mod.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/impls/structs.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/impls/tuple_structs.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/impls/typed.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/impls/values.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/lib.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/reflect_value.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/registration.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/serialization.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/trait_reflection.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/type_path.rs (100%) rename crates/bevy_reflect/{bevy_reflect_derive => derive}/src/utility.rs (100%) diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 212463d3e3b45..7bce663922621 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -25,7 +25,7 @@ documentation = ["bevy_reflect_derive/documentation"] bevy_math = { path = "../bevy_math", version = "0.14.0-dev", features = [ "serialize", ], optional = true } -bevy_reflect_derive = { path = "bevy_reflect_derive", version = "0.14.0-dev" } +bevy_reflect_derive = { path = "derive", version = "0.14.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_ptr = { path = "../bevy_ptr", version = "0.14.0-dev" } diff --git a/crates/bevy_reflect/bevy_reflect_derive/Cargo.toml b/crates/bevy_reflect/derive/Cargo.toml similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/Cargo.toml rename to crates/bevy_reflect/derive/Cargo.toml diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs b/crates/bevy_reflect/derive/src/container_attributes.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/container_attributes.rs rename to crates/bevy_reflect/derive/src/container_attributes.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs b/crates/bevy_reflect/derive/src/derive_data.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs rename to crates/bevy_reflect/derive/src/derive_data.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/documentation.rs b/crates/bevy_reflect/derive/src/documentation.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/documentation.rs rename to crates/bevy_reflect/derive/src/documentation.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/enum_utility.rs b/crates/bevy_reflect/derive/src/enum_utility.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/enum_utility.rs rename to crates/bevy_reflect/derive/src/enum_utility.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/field_attributes.rs b/crates/bevy_reflect/derive/src/field_attributes.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/field_attributes.rs rename to crates/bevy_reflect/derive/src/field_attributes.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/from_reflect.rs b/crates/bevy_reflect/derive/src/from_reflect.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/from_reflect.rs rename to crates/bevy_reflect/derive/src/from_reflect.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs b/crates/bevy_reflect/derive/src/impls/enums.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/enums.rs rename to crates/bevy_reflect/derive/src/impls/enums.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/mod.rs b/crates/bevy_reflect/derive/src/impls/mod.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/mod.rs rename to crates/bevy_reflect/derive/src/impls/mod.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs b/crates/bevy_reflect/derive/src/impls/structs.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/structs.rs rename to crates/bevy_reflect/derive/src/impls/structs.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/tuple_structs.rs rename to crates/bevy_reflect/derive/src/impls/tuple_structs.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/typed.rs b/crates/bevy_reflect/derive/src/impls/typed.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/typed.rs rename to crates/bevy_reflect/derive/src/impls/typed.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs b/crates/bevy_reflect/derive/src/impls/values.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/impls/values.rs rename to crates/bevy_reflect/derive/src/impls/values.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/lib.rs b/crates/bevy_reflect/derive/src/lib.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/lib.rs rename to crates/bevy_reflect/derive/src/lib.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs b/crates/bevy_reflect/derive/src/reflect_value.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/reflect_value.rs rename to crates/bevy_reflect/derive/src/reflect_value.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/registration.rs b/crates/bevy_reflect/derive/src/registration.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/registration.rs rename to crates/bevy_reflect/derive/src/registration.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/serialization.rs b/crates/bevy_reflect/derive/src/serialization.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/serialization.rs rename to crates/bevy_reflect/derive/src/serialization.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/trait_reflection.rs b/crates/bevy_reflect/derive/src/trait_reflection.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/trait_reflection.rs rename to crates/bevy_reflect/derive/src/trait_reflection.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/type_path.rs b/crates/bevy_reflect/derive/src/type_path.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/type_path.rs rename to crates/bevy_reflect/derive/src/type_path.rs diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs b/crates/bevy_reflect/derive/src/utility.rs similarity index 100% rename from crates/bevy_reflect/bevy_reflect_derive/src/utility.rs rename to crates/bevy_reflect/derive/src/utility.rs diff --git a/tools/publish.sh b/tools/publish.sh index d9b63ddde835b..b020bad286e3f 100644 --- a/tools/publish.sh +++ b/tools/publish.sh @@ -7,7 +7,7 @@ crates=( bevy_derive bevy_math bevy_tasks - bevy_reflect/bevy_reflect_derive + bevy_reflect/derive bevy_reflect bevy_ecs/macros bevy_ecs From 03f4cc5dde5e35798b7d3912cd3133696f471085 Mon Sep 17 00:00:00 2001 From: Lynn <62256001+solis-lumine-vorago@users.noreply.github.com> Date: Tue, 7 May 2024 16:41:55 +0200 Subject: [PATCH 11/26] Extrusion (#13270) # Objective - Adds a basic `Extrusion` shape, suggestion of #10572 ## Solution - Adds `Measured2d` and `Measured3d` traits for getting the perimeter/area or area/volume of shapes. This allows implementing `.volume()` and `.area()` for all extrusions `Extrusion` within `bevy_math` - All existing perimeter, area and volume implementations for primitves have been moved into implementations of `Measured2d` and `Measured3d` - Shapes should be extruded along the Z-axis since an extrusion of depth `0.` should be equivalent in everything but name to the base shape ## Caviats - I am not sure about the naming. `Extrusion` could also be `Prism` and the `MeasuredNd` could also be something like `MeasuredPrimitiveNd`. If you have any other suggestions, please fell free to share them :) ## Future work This PR adds a basic `Extrusion` shape and does not implement a lot of things you might want it to. Some of the future possibilities include: - [ ] bounding for extrusions - [ ] making extrusions work with gizmos - [ ] meshing --------- Co-authored-by: Alice Cecile --- crates/bevy_math/src/primitives/dim2.rs | 196 +++++++++++---------- crates/bevy_math/src/primitives/dim3.rs | 216 ++++++++++++++++-------- crates/bevy_math/src/primitives/mod.rs | 18 ++ 3 files changed, 267 insertions(+), 163 deletions(-) diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index e3c613a9fae5c..eaedb9c099c50 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1,6 +1,6 @@ use std::f32::consts::PI; -use super::{Primitive2d, WindingOrder}; +use super::{Measured2d, Primitive2d, WindingOrder}; use crate::{Dir2, Vec2}; /// A circle primitive @@ -32,19 +32,6 @@ impl Circle { 2.0 * self.radius } - /// Get the area of the circle - #[inline(always)] - pub fn area(&self) -> f32 { - PI * self.radius.powi(2) - } - - /// Get the perimeter or circumference of the circle - #[inline(always)] - #[doc(alias = "circumference")] - pub fn perimeter(&self) -> f32 { - 2.0 * PI * self.radius - } - /// Finds the point on the circle that is closest to the given `point`. /// /// If the point is outside the circle, the returned point will be on the perimeter of the circle. @@ -65,6 +52,21 @@ impl Circle { } } +impl Measured2d for Circle { + /// Get the area of the circle + #[inline(always)] + fn area(&self) -> f32 { + PI * self.radius.powi(2) + } + + /// Get the perimeter or circumference of the circle + #[inline(always)] + #[doc(alias = "circumference")] + fn perimeter(&self) -> f32 { + 2.0 * PI * self.radius + } +} + /// An ellipse primitive #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -129,11 +131,31 @@ impl Ellipse { (a * a - b * b).sqrt() } + /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. + #[inline(always)] + pub fn semi_major(&self) -> f32 { + self.half_size.max_element() + } + + /// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse. + #[inline(always)] + pub fn semi_minor(&self) -> f32 { + self.half_size.min_element() + } +} + +impl Measured2d for Ellipse { + /// Get the area of the ellipse + #[inline(always)] + fn area(&self) -> f32 { + PI * self.half_size.x * self.half_size.y + } + #[inline(always)] /// Get an approximation for the perimeter or circumference of the ellipse. /// /// The approximation is reasonably precise with a relative error less than 0.007%, getting more precise as the eccentricity of the ellipse decreases. - pub fn perimeter(&self) -> f32 { + fn perimeter(&self) -> f32 { let a = self.semi_major(); let b = self.semi_minor(); @@ -184,24 +206,6 @@ impl Ellipse { .map(|i| BINOMIAL_COEFFICIENTS[i] * h.powi(i as i32)) .sum::() } - - /// Returns the length of the semi-major axis. This corresponds to the longest radius of the ellipse. - #[inline(always)] - pub fn semi_major(&self) -> f32 { - self.half_size.max_element() - } - - /// Returns the length of the semi-minor axis. This corresponds to the shortest radius of the ellipse. - #[inline(always)] - pub fn semi_minor(&self) -> f32 { - self.half_size.min_element() - } - - /// Get the area of the ellipse - #[inline(always)] - pub fn area(&self) -> f32 { - PI * self.half_size.x * self.half_size.y - } } /// A primitive shape formed by the region between two circles, also known as a ring. @@ -248,20 +252,6 @@ impl Annulus { self.outer_circle.radius - self.inner_circle.radius } - /// Get the area of the annulus - #[inline(always)] - pub fn area(&self) -> f32 { - PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2)) - } - - /// Get the perimeter or circumference of the annulus, - /// which is the sum of the perimeters of the inner and outer circles. - #[inline(always)] - #[doc(alias = "circumference")] - pub fn perimeter(&self) -> f32 { - 2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius) - } - /// Finds the point on the annulus that is closest to the given `point`: /// /// - If the point is outside of the annulus completely, the returned point will be on the outer perimeter. @@ -290,6 +280,22 @@ impl Annulus { } } +impl Measured2d for Annulus { + /// Get the area of the annulus + #[inline(always)] + fn area(&self) -> f32 { + PI * (self.outer_circle.radius.powi(2) - self.inner_circle.radius.powi(2)) + } + + /// Get the perimeter or circumference of the annulus, + /// which is the sum of the perimeters of the inner and outer circles. + #[inline(always)] + #[doc(alias = "circumference")] + fn perimeter(&self) -> f32 { + 2.0 * PI * (self.outer_circle.radius + self.inner_circle.radius) + } +} + /// An unbounded plane in 2D space. It forms a separating surface through the origin, /// stretching infinitely far #[derive(Clone, Copy, Debug, PartialEq)] @@ -471,25 +477,6 @@ impl Triangle2d { } } - /// Get the area of the triangle - #[inline(always)] - pub fn area(&self) -> f32 { - let [a, b, c] = self.vertices; - (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)).abs() / 2.0 - } - - /// Get the perimeter of the triangle - #[inline(always)] - pub fn perimeter(&self) -> f32 { - let [a, b, c] = self.vertices; - - let ab = a.distance(b); - let bc = b.distance(c); - let ca = c.distance(a); - - ab + bc + ca - } - /// Get the [`WindingOrder`] of the triangle #[inline(always)] #[doc(alias = "orientation")] @@ -548,6 +535,27 @@ impl Triangle2d { } } +impl Measured2d for Triangle2d { + /// Get the area of the triangle + #[inline(always)] + fn area(&self) -> f32 { + let [a, b, c] = self.vertices; + (a.x * (b.y - c.y) + b.x * (c.y - a.y) + c.x * (a.y - b.y)).abs() / 2.0 + } + + /// Get the perimeter of the triangle + #[inline(always)] + fn perimeter(&self) -> f32 { + let [a, b, c] = self.vertices; + + let ab = a.distance(b); + let bc = b.distance(c); + let ca = c.distance(a); + + ab + bc + ca + } +} + /// A rectangle primitive #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -605,18 +613,6 @@ impl Rectangle { 2.0 * self.half_size } - /// Get the area of the rectangle - #[inline(always)] - pub fn area(&self) -> f32 { - 4.0 * self.half_size.x * self.half_size.y - } - - /// Get the perimeter of the rectangle - #[inline(always)] - pub fn perimeter(&self) -> f32 { - 4.0 * (self.half_size.x + self.half_size.y) - } - /// Finds the point on the rectangle that is closest to the given `point`. /// /// If the point is outside the rectangle, the returned point will be on the perimeter of the rectangle. @@ -628,6 +624,20 @@ impl Rectangle { } } +impl Measured2d for Rectangle { + /// Get the area of the rectangle + #[inline(always)] + fn area(&self) -> f32 { + 4.0 * self.half_size.x * self.half_size.y + } + + /// Get the perimeter of the rectangle + #[inline(always)] + fn perimeter(&self) -> f32 { + 4.0 * (self.half_size.x + self.half_size.y) + } +} + /// A polygon with N vertices. /// /// For a version without generics: [`BoxedPolygon`] @@ -749,20 +759,6 @@ impl RegularPolygon { 2.0 * self.circumradius() * (PI / self.sides as f32).sin() } - /// Get the area of the regular polygon - #[inline(always)] - pub fn area(&self) -> f32 { - let angle: f32 = 2.0 * PI / (self.sides as f32); - (self.sides as f32) * self.circumradius().powi(2) * angle.sin() / 2.0 - } - - /// Get the perimeter of the regular polygon. - /// This is the sum of its sides - #[inline(always)] - pub fn perimeter(&self) -> f32 { - self.sides as f32 * self.side_length() - } - /// Get the internal angle of the regular polygon in degrees. /// /// This is the angle formed by two adjacent sides with points @@ -816,6 +812,22 @@ impl RegularPolygon { } } +impl Measured2d for RegularPolygon { + /// Get the area of the regular polygon + #[inline(always)] + fn area(&self) -> f32 { + let angle: f32 = 2.0 * PI / (self.sides as f32); + (self.sides as f32) * self.circumradius().powi(2) * angle.sin() / 2.0 + } + + /// Get the perimeter of the regular polygon. + /// This is the sum of its sides + #[inline(always)] + fn perimeter(&self) -> f32 { + self.sides as f32 * self.side_length() + } +} + /// A 2D capsule primitive, also known as a stadium or pill shape. /// /// A two-dimensional capsule is defined as a neighborhood of points at a distance (radius) from a line diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index b3308acbf8089..2906492917387 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -1,6 +1,6 @@ use std::f32::consts::{FRAC_PI_3, PI}; -use super::{Circle, Primitive3d}; +use super::{Circle, Measured2d, Measured3d, Primitive2d, Primitive3d}; use crate::{Dir3, InvalidDirectionError, Mat3, Vec2, Vec3}; /// A sphere primitive @@ -32,18 +32,6 @@ impl Sphere { 2.0 * self.radius } - /// Get the surface area of the sphere - #[inline(always)] - pub fn area(&self) -> f32 { - 4.0 * PI * self.radius.powi(2) - } - - /// Get the volume of the sphere - #[inline(always)] - pub fn volume(&self) -> f32 { - 4.0 * FRAC_PI_3 * self.radius.powi(3) - } - /// Finds the point on the sphere that is closest to the given `point`. /// /// If the point is outside the sphere, the returned point will be on the surface of the sphere. @@ -64,6 +52,20 @@ impl Sphere { } } +impl Measured3d for Sphere { + /// Get the surface area of the sphere + #[inline(always)] + fn area(&self) -> f32 { + 4.0 * PI * self.radius.powi(2) + } + + /// Get the volume of the sphere + #[inline(always)] + fn volume(&self) -> f32 { + 4.0 * FRAC_PI_3 * self.radius.powi(3) + } +} + /// A bounded plane in 3D space. It forms a surface starting from the origin with a defined height and width. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -360,9 +362,21 @@ impl Cuboid { 2.0 * self.half_size } + /// Finds the point on the cuboid that is closest to the given `point`. + /// + /// If the point is outside the cuboid, the returned point will be on the surface of the cuboid. + /// Otherwise, it will be inside the cuboid and returned as is. + #[inline(always)] + pub fn closest_point(&self, point: Vec3) -> Vec3 { + // Clamp point coordinates to the cuboid + point.clamp(-self.half_size, self.half_size) + } +} + +impl Measured3d for Cuboid { /// Get the surface area of the cuboid #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { 8.0 * (self.half_size.x * self.half_size.y + self.half_size.y * self.half_size.z + self.half_size.x * self.half_size.z) @@ -370,19 +384,9 @@ impl Cuboid { /// Get the volume of the cuboid #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { 8.0 * self.half_size.x * self.half_size.y * self.half_size.z } - - /// Finds the point on the cuboid that is closest to the given `point`. - /// - /// If the point is outside the cuboid, the returned point will be on the surface of the cuboid. - /// Otherwise, it will be inside the cuboid and returned as is. - #[inline(always)] - pub fn closest_point(&self, point: Vec3) -> Vec3 { - // Clamp point coordinates to the cuboid - point.clamp(-self.half_size, self.half_size) - } } /// A cylinder primitive @@ -437,16 +441,18 @@ impl Cylinder { pub fn base_area(&self) -> f32 { PI * self.radius.powi(2) } +} +impl Measured3d for Cylinder { /// Get the total surface area of the cylinder #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { 2.0 * PI * self.radius * (self.radius + 2.0 * self.half_height) } /// Get the volume of the cylinder #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { self.base_area() * 2.0 * self.half_height } } @@ -492,17 +498,19 @@ impl Capsule3d { half_height: self.half_length, } } +} +impl Measured3d for Capsule3d { /// Get the surface area of the capsule #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { // Modified version of 2pi * r * (2r + h) 4.0 * PI * self.radius * (self.radius + self.half_length) } /// Get the volume of the capsule #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { // Modified version of pi * r^2 * (4/3 * r + a) let diameter = self.radius * 2.0; PI * self.radius * diameter * (diameter / 3.0 + self.half_length) @@ -550,16 +558,18 @@ impl Cone { pub fn base_area(&self) -> f32 { PI * self.radius.powi(2) } +} +impl Measured3d for Cone { /// Get the total surface area of the cone #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { self.base_area() + self.lateral_area() } /// Get the volume of the cone #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { (self.base_area() * self.height) / 3.0 } } @@ -681,18 +691,20 @@ impl Torus { std::cmp::Ordering::Less => TorusKind::Spindle, } } +} +impl Measured3d for Torus { /// Get the surface area of the torus. Note that this only produces /// the expected result when the torus has a ring and isn't self-intersecting #[inline(always)] - pub fn area(&self) -> f32 { + fn area(&self) -> f32 { 4.0 * PI.powi(2) * self.major_radius * self.minor_radius } /// Get the volume of the torus. Note that this only produces /// the expected result when the torus has a ring and isn't self-intersecting #[inline(always)] - pub fn volume(&self) -> f32 { + fn volume(&self) -> f32 { 2.0 * PI.powi(2) * self.major_radius * self.minor_radius.powi(2) } } @@ -729,22 +741,6 @@ impl Triangle3d { } } - /// Get the area of the triangle. - #[inline(always)] - pub fn area(&self) -> f32 { - let [a, b, c] = self.vertices; - let ab = b - a; - let ac = c - a; - ab.cross(ac).length() / 2.0 - } - - /// Get the perimeter of the triangle. - #[inline(always)] - pub fn perimeter(&self) -> f32 { - let [a, b, c] = self.vertices; - a.distance(b) + b.distance(c) + c.distance(a) - } - /// Get the normal of the triangle in the direction of the right-hand rule, assuming /// the vertices are ordered in a counter-clockwise direction. /// @@ -835,6 +831,24 @@ impl Triangle3d { } } +impl Measured2d for Triangle3d { + /// Get the area of the triangle. + #[inline(always)] + fn area(&self) -> f32 { + let [a, b, c] = self.vertices; + let ab = b - a; + let ac = c - a; + ab.cross(ac).length() / 2.0 + } + + /// Get the perimeter of the triangle. + #[inline(always)] + fn perimeter(&self) -> f32 { + let [a, b, c] = self.vertices; + a.distance(b) + b.distance(c) + c.distance(a) + } +} + /// A tetrahedron primitive. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -868,28 +882,6 @@ impl Tetrahedron { } } - /// Get the surface area of the tetrahedron. - #[inline(always)] - pub fn area(&self) -> f32 { - let [a, b, c, d] = self.vertices; - let ab = b - a; - let ac = c - a; - let ad = d - a; - let bc = c - b; - let bd = d - b; - (ab.cross(ac).length() - + ab.cross(ad).length() - + ac.cross(ad).length() - + bc.cross(bd).length()) - / 2.0 - } - - /// Get the volume of the tetrahedron. - #[inline(always)] - pub fn volume(&self) -> f32 { - self.signed_volume().abs() - } - /// Get the signed volume of the tetrahedron. /// /// If it's negative, the normal vector of the face defined by @@ -915,6 +907,70 @@ impl Tetrahedron { } } +impl Measured3d for Tetrahedron { + /// Get the surface area of the tetrahedron. + #[inline(always)] + fn area(&self) -> f32 { + let [a, b, c, d] = self.vertices; + let ab = b - a; + let ac = c - a; + let ad = d - a; + let bc = c - b; + let bd = d - b; + (ab.cross(ac).length() + + ab.cross(ad).length() + + ac.cross(ad).length() + + bc.cross(bd).length()) + / 2.0 + } + + /// Get the volume of the tetrahedron. + #[inline(always)] + fn volume(&self) -> f32 { + self.signed_volume().abs() + } +} + +/// A 3D shape representing an extruded 2D `base_shape`. +/// +/// Extruding a shape effectively "thickens" a 2D shapes, +/// creating a shape with the same cross-section over the entire depth. +/// +/// The resulting volumes are prisms. +/// For example, a triangle becomes a triangular prism, while a circle becomes a cylinder. +#[doc(alias = "Prism")] +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Extrusion { + /// The base shape of the extrusion + pub base_shape: T, + /// Half of the depth of the extrusion + pub half_depth: f32, +} +impl Primitive3d for Extrusion {} + +impl Extrusion { + /// Create a new `Extrusion` from a given `base_shape` and `depth` + pub fn new(base_shape: T, depth: f32) -> Self { + Self { + base_shape, + half_depth: depth / 2., + } + } +} + +impl Measured3d for Extrusion { + /// Get the surface area of the extrusion + fn area(&self) -> f32 { + 2. * (self.base_shape.area() + self.half_depth * self.base_shape.perimeter()) + } + + /// Get the volume of the extrusion + fn volume(&self) -> f32 { + 2. * self.base_shape.area() * self.half_depth + } +} + #[cfg(test)] mod tests { // Reference values were computed by hand and/or with external tools @@ -1146,4 +1202,22 @@ mod tests { let degenerate = Triangle3d::new(Vec3::NEG_ONE, Vec3::ZERO, Vec3::ONE); assert!(degenerate.is_degenerate(), "did not find degenerate"); } + + #[test] + fn extrusion_math() { + let circle = Circle::new(0.75); + let cylinder = Extrusion::new(circle, 2.5); + assert_eq!(cylinder.area(), 15.315264, "incorrect surface area"); + assert_eq!(cylinder.volume(), 4.417865, "incorrect volume"); + + let annulus = crate::primitives::Annulus::new(0.25, 1.375); + let tube = Extrusion::new(annulus, 0.333); + assert_eq!(tube.area(), 14.886437, "incorrect surface area"); + assert_eq!(tube.volume(), 1.9124937, "incorrect volume"); + + let polygon = crate::primitives::RegularPolygon::new(3.8, 7); + let regular_prism = Extrusion::new(polygon, 1.25); + assert_eq!(regular_prism.area(), 107.8808, "incorrect surface area"); + assert_eq!(regular_prism.volume(), 49.392204, "incorrect volume"); + } } diff --git a/crates/bevy_math/src/primitives/mod.rs b/crates/bevy_math/src/primitives/mod.rs index 8fda6924ec5e8..460e635867ecb 100644 --- a/crates/bevy_math/src/primitives/mod.rs +++ b/crates/bevy_math/src/primitives/mod.rs @@ -29,3 +29,21 @@ pub enum WindingOrder { #[doc(alias("Degenerate", "Collinear"))] Invalid, } + +/// A trait for getting measurements of 2D shapes +pub trait Measured2d { + /// Get the perimeter of the shape + fn perimeter(&self) -> f32; + + /// Get the area of the shape + fn area(&self) -> f32; +} + +/// A trait for getting measurements of 3D shapes +pub trait Measured3d { + /// Get the surface area of the shape + fn area(&self) -> f32; + + /// Get the volume of the shape + fn volume(&self) -> f32; +} From 4350ad0bd184dc94bc7f2d0d076a3482950dfe8c Mon Sep 17 00:00:00 2001 From: Brezak Date: Wed, 8 May 2024 01:52:30 +0200 Subject: [PATCH 12/26] Make `AssetMetaCheck` a field on the asset plugin (#13177) # Objective There's a TODO comment above the `AssetMetaCheck` enum mentioning this should have been done in 0.13 ## Solution Do it in 0.14 ## Testing I've checked that all the asset tests compile. I've also run the asset_processing and asset_settings tests and they both work. --- ## Changelog ### Changed - [`AssetMetaCheck`](https://docs.rs/bevy/latest/bevy/asset/enum.AssetMetaCheck.html) is no longer a resource and is now a field on the [`AssetPlugin`](https://docs.rs/bevy/latest/bevy/asset/struct.AssetPlugin.html). ## Migration Guide Changes to how bevy handles asset meta files now need to be specified when inserting the `AssetPlugin`. --- crates/bevy_asset/src/lib.rs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 3b56b9cd55cef..ea8caf003a1dd 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -55,7 +55,6 @@ use bevy_app::{App, Last, Plugin, PreUpdate}; use bevy_ecs::{ reflect::AppTypeRegistry, schedule::{IntoSystemConfigs, IntoSystemSetConfigs, SystemSet}, - system::Resource, world::FromWorld, }; use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}; @@ -90,6 +89,8 @@ pub struct AssetPlugin { pub watch_for_changes_override: Option, /// The [`AssetMode`] to use for this server. pub mode: AssetMode, + /// How/If asset meta files should be checked. + pub meta_check: AssetMetaCheck, } #[derive(Debug)] @@ -118,8 +119,7 @@ pub enum AssetMode { /// Configures how / if meta files will be checked. If an asset's meta file is not checked, the default meta for the asset /// will be used. -// TODO: To avoid breaking Bevy 0.12 users in 0.12.1, this is a Resource. In Bevy 0.13 this should be changed to a field on AssetPlugin (if it is still needed). -#[derive(Debug, Default, Clone, Resource)] +#[derive(Debug, Default, Clone)] pub enum AssetMetaCheck { /// Always check if assets have meta files. If the meta does not exist, the default meta will be used. #[default] @@ -137,6 +137,7 @@ impl Default for AssetPlugin { file_path: Self::DEFAULT_UNPROCESSED_FILE_PATH.to_string(), processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(), watch_for_changes_override: None, + meta_check: AssetMetaCheck::default(), } } } @@ -171,16 +172,11 @@ impl Plugin for AssetPlugin { AssetMode::Unprocessed => { let mut builders = app.world_mut().resource_mut::(); let sources = builders.build_sources(watch, false); - let meta_check = app - .world() - .get_resource::() - .cloned() - .unwrap_or_else(AssetMetaCheck::default); app.insert_resource(AssetServer::new_with_meta_check( sources, AssetServerMode::Unprocessed, - meta_check, + self.meta_check.clone(), watch, )); } From 0dddfa07ab5bdb95b0dcc1041e423678f92e83f7 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 8 May 2024 02:34:59 -0500 Subject: [PATCH 13/26] Fix the WebGL 2 backend by giving the `visibility_ranges` array a fixed length. (#13210) WebGL 2 doesn't support variable-length uniform buffer arrays. So we arbitrarily set the length of the visibility ranges field to 64 on that platform. --------- Co-authored-by: IceSentry --- .../bevy_pbr/src/render/mesh_functions.wgsl | 16 +++++++- .../src/render/mesh_view_bindings.wgsl | 3 +- .../bevy_render/src/view/visibility/range.rs | 37 +++++++++++++++++-- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/crates/bevy_pbr/src/render/mesh_functions.wgsl b/crates/bevy_pbr/src/render/mesh_functions.wgsl index c100f16f6ffe9..ce8e9701271a5 100644 --- a/crates/bevy_pbr/src/render/mesh_functions.wgsl +++ b/crates/bevy_pbr/src/render/mesh_functions.wgsl @@ -1,7 +1,11 @@ #define_import_path bevy_pbr::mesh_functions #import bevy_pbr::{ - mesh_view_bindings::{view, visibility_ranges}, + mesh_view_bindings::{ + view, + visibility_ranges, + VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE + }, mesh_bindings::mesh, mesh_types::MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT, view_transformations::position_world_to_clip, @@ -90,8 +94,16 @@ fn mesh_tangent_local_to_world(model: mat4x4, vertex_tangent: vec4, in // camera distance to determine the dithering level. #ifdef VISIBILITY_RANGE_DITHER fn get_visibility_range_dither_level(instance_index: u32, world_position: vec4) -> i32 { +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + // If we're using a storage buffer, then the length is variable. + let visibility_buffer_array_len = arrayLength(&visibility_ranges); +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + // If we're using a uniform buffer, then the length is constant + let visibility_buffer_array_len = VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE; +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 + let visibility_buffer_index = mesh[instance_index].flags & 0xffffu; - if (visibility_buffer_index > arrayLength(&visibility_ranges)) { + if (visibility_buffer_index > visibility_buffer_array_len) { return -16; } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index 4fd1f2327d024..b8e74c60b8b43 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -35,10 +35,11 @@ @group(0) @binding(10) var fog: types::Fog; @group(0) @binding(11) var light_probes: types::LightProbes; +const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 6 @group(0) @binding(12) var visibility_ranges: array>; #else -@group(0) @binding(12) var visibility_ranges: array>; +@group(0) @binding(12) var visibility_ranges: array, VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE>; #endif @group(0) @binding(13) var screen_space_ambient_occlusion_texture: texture_2d; diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 62485e42bd8d0..d1e1d6546fa14 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -19,7 +19,7 @@ use bevy_reflect::Reflect; use bevy_transform::components::GlobalTransform; use bevy_utils::{prelude::default, EntityHashMap, HashMap}; use nonmax::NonMaxU16; -use wgpu::BufferUsages; +use wgpu::{BufferBindingType, BufferUsages}; use crate::{ camera::Camera, @@ -38,6 +38,11 @@ use super::{check_visibility, VisibilitySystems, WithMesh}; /// buffer slot. pub const VISIBILITY_RANGES_STORAGE_BUFFER_COUNT: u32 = 4; +/// The size of the visibility ranges buffer in elements (not bytes) when fewer +/// than 6 storage buffers are available and we're forced to use a uniform +/// buffer instead (most notably, on WebGL 2). +const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: usize = 64; + /// A plugin that enables [`VisibilityRange`]s, which allow entities to be /// hidden or shown based on distance to the camera. pub struct VisibilityRangePlugin; @@ -424,9 +429,33 @@ pub fn write_render_visibility_ranges( return; } - // If the buffer is empty, push *something* so that we allocate it. - if render_visibility_ranges.buffer.is_empty() { - render_visibility_ranges.buffer.push(default()); + // Mess with the length of the buffer to meet API requirements if necessary. + match render_device.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT) + { + // If we're using a uniform buffer, we must have *exactly* + // `VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE` elements. + BufferBindingType::Uniform + if render_visibility_ranges.buffer.len() > VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE => + { + render_visibility_ranges + .buffer + .truncate(VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE); + } + BufferBindingType::Uniform + if render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE => + { + while render_visibility_ranges.buffer.len() < VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE { + render_visibility_ranges.buffer.push(default()); + } + } + + // Otherwise, if we're using a storage buffer, just ensure there's + // something in the buffer, or else it won't get allocated. + BufferBindingType::Storage { .. } if render_visibility_ranges.buffer.is_empty() => { + render_visibility_ranges.buffer.push(default()); + } + + _ => {} } // Schedule the write. From 64e1a7835a7461924a94bda3e920596b7ce439d9 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Wed, 8 May 2024 04:13:39 -0400 Subject: [PATCH 14/26] Clean up 2d render phases (#12982) # Objective Currently, the 2d pipeline only has a transparent pass that is used for everything. I want to have separate passes for opaque/alpha mask/transparent meshes just like in 3d. This PR does the basic work to start adding new phases to the 2d pipeline and get the current setup a bit closer to 3d. ## Solution - Use `ViewNode` for `MainTransparentPass2dNode` - Added `Node2d::StartMainPass`, `Node2d::EndMainPass` - Rename everything to clarify that the main pass is currently the transparent pass --- ## Changelog - Added `Node2d::StartMainPass`, `Node2d::EndMainPass` ## Migration Guide If you were using `Node2d::MainPass` to order your own custom render node. You now need to order it relative to `Node2d::StartMainPass` or `Node2d::EndMainPass`. --- crates/bevy_core_pipeline/src/bloom/mod.rs | 2 +- ...de.rs => main_transparent_pass_2d_node.rs} | 54 +++++++------------ crates/bevy_core_pipeline/src/core_2d/mod.rs | 19 +++++-- .../bevy_core_pipeline/src/msaa_writeback.rs | 2 +- crates/bevy_sprite/src/mesh2d/material.rs | 12 ++--- crates/bevy_ui/src/render/mod.rs | 2 +- 6 files changed, 41 insertions(+), 50 deletions(-) rename crates/bevy_core_pipeline/src/core_2d/{main_pass_2d_node.rs => main_transparent_pass_2d_node.rs} (67%) diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 419a82ff6013f..1dc253c758461 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -81,7 +81,7 @@ impl Plugin for BloomPlugin { .add_render_graph_node::>(Core2d, Node2d::Bloom) .add_render_graph_edges( Core2d, - (Node2d::MainPass, Node2d::Bloom, Node2d::Tonemapping), + (Node2d::EndMainPass, Node2d::Bloom, Node2d::Tonemapping), ); } diff --git a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs similarity index 67% rename from crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs rename to crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs index 320141818af16..7ee48bd44ce75 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs @@ -3,67 +3,49 @@ use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, diagnostic::RecordDiagnostics, - render_graph::{Node, NodeRunError, RenderGraphContext}, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_phase::SortedRenderPhase, render_resource::RenderPassDescriptor, renderer::RenderContext, - view::{ExtractedView, ViewTarget}, + view::ViewTarget, }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; -pub struct MainPass2dNode { - query: QueryState< - ( - &'static ExtractedCamera, - &'static SortedRenderPhase, - &'static ViewTarget, - ), - With, - >, -} - -impl FromWorld for MainPass2dNode { - fn from_world(world: &mut World) -> Self { - Self { - query: world.query_filtered(), - } - } -} +#[derive(Default)] +pub struct MainTransparentPass2dNode {} -impl Node for MainPass2dNode { - fn update(&mut self, world: &mut World) { - self.query.update_archetypes(world); - } +impl ViewNode for MainTransparentPass2dNode { + type ViewQuery = ( + &'static ExtractedCamera, + &'static SortedRenderPhase, + &'static ViewTarget, + ); - fn run( + fn run<'w>( &self, graph: &mut RenderGraphContext, - render_context: &mut RenderContext, - world: &World, + render_context: &mut RenderContext<'w>, + (camera, transparent_phase, target): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, + world: &'w World, ) -> Result<(), NodeRunError> { let view_entity = graph.view_entity(); - let Ok((camera, transparent_phase, target)) = self.query.get_manual(world, view_entity) - else { - // no target - return Ok(()); - }; - { + if !transparent_phase.items.is_empty() { #[cfg(feature = "trace")] - let _main_pass_2d = info_span!("main_pass_2d").entered(); + let _main_pass_2d = info_span!("main_transparent_pass_2d").entered(); let diagnostics = render_context.diagnostic_recorder(); let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { - label: Some("main_pass_2d"), + label: Some("main_transparent_pass_2d"), color_attachments: &[Some(target.get_color_attachment())], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); - let pass_span = diagnostics.pass_span(&mut render_pass, "main_pass_2d"); + let pass_span = diagnostics.pass_span(&mut render_pass, "main_transparent_pass_2d"); if let Some(viewport) = camera.viewport.as_ref() { render_pass.set_camera_viewport(viewport); diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 85f986c111dd4..cf48b4030f5b5 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -1,5 +1,5 @@ mod camera_2d; -mod main_pass_2d_node; +mod main_transparent_pass_2d_node; pub mod graph { use bevy_render::render_graph::{RenderLabel, RenderSubGraph}; @@ -14,7 +14,9 @@ pub mod graph { #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] pub enum Node2d { MsaaWriteback, - MainPass, + StartMainPass, + MainTransparentPass, + EndMainPass, Bloom, Tonemapping, Fxaa, @@ -27,7 +29,7 @@ pub mod graph { use std::ops::Range; pub use camera_2d::*; -pub use main_pass_2d_node::*; +pub use main_transparent_pass_2d_node::*; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; @@ -68,14 +70,21 @@ impl Plugin for Core2dPlugin { render_app .add_render_sub_graph(Core2d) - .add_render_graph_node::(Core2d, Node2d::MainPass) + .add_render_graph_node::(Core2d, Node2d::StartMainPass) + .add_render_graph_node::>( + Core2d, + Node2d::MainTransparentPass, + ) + .add_render_graph_node::(Core2d, Node2d::EndMainPass) .add_render_graph_node::>(Core2d, Node2d::Tonemapping) .add_render_graph_node::(Core2d, Node2d::EndMainPassPostProcessing) .add_render_graph_node::>(Core2d, Node2d::Upscaling) .add_render_graph_edges( Core2d, ( - Node2d::MainPass, + Node2d::StartMainPass, + Node2d::MainTransparentPass, + Node2d::EndMainPass, Node2d::Tonemapping, Node2d::EndMainPassPostProcessing, Node2d::Upscaling, diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 8d19a6df136f0..5165f51ab08da 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -31,7 +31,7 @@ impl Plugin for MsaaWritebackPlugin { { render_app .add_render_graph_node::(Core2d, Node2d::MsaaWriteback) - .add_render_graph_edge(Core2d, Node2d::MsaaWriteback, Node2d::MainPass); + .add_render_graph_edge(Core2d, Node2d::MsaaWriteback, Node2d::StartMainPass); } { render_app diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 2805840a67694..d58083763aeaa 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -389,7 +389,7 @@ pub fn queue_material2d_meshes( } for (view, visible_entities, tonemapping, dither, mut transparent_phase) in &mut views { - let draw_transparent_pbr = transparent_draw_functions.read().id::>(); + let draw_transparent_2d = transparent_draw_functions.read().id::>(); let mut view_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()) | Mesh2dPipelineKey::from_hdr(view.hdr); @@ -410,7 +410,7 @@ pub fn queue_material2d_meshes( let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else { continue; }; - let Some(material2d) = render_materials.get(*material_asset_id) else { + let Some(material_2d) = render_materials.get(*material_asset_id) else { continue; }; let Some(mesh) = render_meshes.get(mesh_instance.mesh_asset_id) else { @@ -424,7 +424,7 @@ pub fn queue_material2d_meshes( &material2d_pipeline, Material2dKey { mesh_key, - bind_group_data: material2d.key.clone(), + bind_group_data: material_2d.key.clone(), }, &mesh.layout, ); @@ -437,18 +437,18 @@ pub fn queue_material2d_meshes( } }; - mesh_instance.material_bind_group_id = material2d.get_bind_group_id(); + mesh_instance.material_bind_group_id = material_2d.get_bind_group_id(); let mesh_z = mesh_instance.transforms.transform.translation.z; transparent_phase.add(Transparent2d { entity: *visible_entity, - draw_function: draw_transparent_pbr, + draw_function: draw_transparent_2d, pipeline: pipeline_id, // NOTE: Back-to-front ordering for transparent with ascending sort means far should have the // lowest sort key and getting closer should increase. As we have // -z in front of the camera, the largest distance is -far with values increasing toward the // camera. As such we can just use mesh_z as the distance - sort_key: FloatOrd(mesh_z + material2d.depth_bias), + sort_key: FloatOrd(mesh_z + material_2d.depth_bias), // Batching is done in batch_and_prepare_render_phase batch_range: 0..1, extra_index: PhaseItemExtraIndex::NONE, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 4ff38dc4db09f..3aaca1004ac21 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -126,7 +126,7 @@ pub fn build_ui_render(app: &mut App) { if let Some(graph_2d) = graph.get_sub_graph_mut(Core2d) { graph_2d.add_sub_graph(SubGraphUi, ui_graph_2d); graph_2d.add_node(NodeUi::UiPass, RunGraphOnViewNode::new(SubGraphUi)); - graph_2d.add_node_edge(Node2d::MainPass, NodeUi::UiPass); + graph_2d.add_node_edge(Node2d::EndMainPass, NodeUi::UiPass); graph_2d.add_node_edge(Node2d::EndMainPassPostProcessing, NodeUi::UiPass); graph_2d.add_node_edge(NodeUi::UiPass, Node2d::Upscaling); } From 2f87bb8c1ff721e75e8a40aec1a181ee725b7cd8 Mon Sep 17 00:00:00 2001 From: BD103 <59022059+BD103@users.noreply.github.com> Date: Wed, 8 May 2024 10:15:21 -0400 Subject: [PATCH 15/26] Don't deploy docs when working on a fork (#13278) # Objective - Some developers enable Github Actions for their fork and commit directly to main. This triggers the `docs.yml` action, which attempts to deploy the documentation even if Github Pages is not enabled. (It also creates a `CNAME` file specific to Bevy and should not be used in forks, even for testing.) - For an example, see [this run](https://github.com/tychedelia/bevy/actions/runs/8978912060/job/24660082729). ## Solution - Only attempt to deploy docs when running from the main Bevy repository. - This does not affect us checking `cargo doc` on pull requests, since that it done in `ci.yml`. ## Testing It's difficult to test this, but you'd probably: 1. Fork Bevy 2. Cherry pick this PR's commits onto the main branch of your fork. 3. Push another commit to the main branch, triggering Github Actions. 4. Check the Github Actions job summary to ensure that the `build-and-deploy` job is skipped. --- .github/workflows/docs.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e4002371d665b..49e6b2cd497c5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,6 +27,9 @@ concurrency: jobs: build-and-deploy: runs-on: ubuntu-latest + # Only run this job when on the main Bevy repository. Without this, it would also run on forks + # where developers work on the main branch but have not enabled Github Pages. + if: ${{ github.repository == 'bevyengine/bevy' }} environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} From 9c4ac7c29712ca6335f41bdc2ef68c6675ed8a9f Mon Sep 17 00:00:00 2001 From: Brezak Date: Wed, 8 May 2024 16:26:01 +0200 Subject: [PATCH 16/26] Finish the work on `try_apply` (#12646) # Objective Finish the `try_apply` implementation started in #6770 by @feyokorenhof. Supersedes and closes #6770. Closes #6182 ## Solution Add `try_apply` to `Reflect` and implement it in all the places that implement `Reflect`. --- ## Changelog Added `try_apply` to `Reflect`. --------- Co-authored-by: Feyo Korenhof Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com> --- .../bevy_reflect/derive/src/enum_utility.rs | 42 +++++---- crates/bevy_reflect/derive/src/impls/enums.rs | 27 ++++-- .../bevy_reflect/derive/src/impls/structs.rs | 22 +++-- .../derive/src/impls/tuple_structs.rs | 26 ++++-- .../bevy_reflect/derive/src/impls/values.rs | 20 +++-- crates/bevy_reflect/src/array.rs | 40 ++++++++- crates/bevy_reflect/src/enums/dynamic_enum.rs | 18 ++-- crates/bevy_reflect/src/enums/enum_trait.rs | 4 +- crates/bevy_reflect/src/enums/mod.rs | 36 +++++++- crates/bevy_reflect/src/impls/smallvec.rs | 10 ++- crates/bevy_reflect/src/impls/std.rs | 90 +++++++++++++------ crates/bevy_reflect/src/lib.rs | 50 ++++++++++- crates/bevy_reflect/src/list.rs | 35 ++++++-- crates/bevy_reflect/src/map.rs | 32 ++++++- crates/bevy_reflect/src/reflect.rs | 53 ++++++++++- crates/bevy_reflect/src/struct_trait.rs | 15 ++-- crates/bevy_reflect/src/tuple.rs | 36 ++++++-- crates/bevy_reflect/src/tuple_struct.rs | 16 ++-- crates/bevy_reflect/src/type_info.rs | 4 +- crates/bevy_reflect/src/type_path.rs | 5 ++ crates/bevy_reflect/src/utility.rs | 8 +- 21 files changed, 474 insertions(+), 115 deletions(-) diff --git a/crates/bevy_reflect/derive/src/enum_utility.rs b/crates/bevy_reflect/derive/src/enum_utility.rs index 36109bbd30c3f..c69a4ec4e245a 100644 --- a/crates/bevy_reflect/derive/src/enum_utility.rs +++ b/crates/bevy_reflect/derive/src/enum_utility.rs @@ -21,7 +21,7 @@ pub(crate) struct EnumVariantConstructors { pub(crate) fn get_variant_constructors( reflect_enum: &ReflectEnum, ref_value: &Ident, - can_panic: bool, + return_apply_error: bool, ) -> EnumVariantConstructors { let bevy_reflect_path = reflect_enum.meta().bevy_reflect_path(); let variant_count = reflect_enum.variants().len(); @@ -50,21 +50,6 @@ pub(crate) fn get_variant_constructors( _ => quote! { #FQDefault::default() } } } else { - let (resolve_error, resolve_missing) = if can_panic { - let field_ref_str = match &field_ident { - Member::Named(ident) => format!("the field `{ident}`"), - Member::Unnamed(index) => format!("the field at index {}", index.index) - }; - let ty = field.data.ty.to_token_stream(); - - let on_error = format!("{field_ref_str} should be of type `{ty}`"); - let on_missing = format!("{field_ref_str} is required but could not be found"); - - (quote!(.expect(#on_error)), quote!(.expect(#on_missing))) - } else { - (quote!(?), quote!(?)) - }; - let field_accessor = match &field.data.ident { Some(ident) => { let name = ident.to_string(); @@ -74,6 +59,31 @@ pub(crate) fn get_variant_constructors( }; reflect_index += 1; + let (resolve_error, resolve_missing) = if return_apply_error { + let field_ref_str = match &field_ident { + Member::Named(ident) => format!("{ident}"), + Member::Unnamed(index) => format!(".{}", index.index) + }; + let ty = field.data.ty.to_token_stream(); + + ( + quote!(.ok_or(#bevy_reflect_path::ApplyError::MismatchedTypes { + // The unwrap won't panic. By this point the #field_accessor would have been invoked once and any failure to + // access the given field handled by the `resolve_missing` code bellow. + from_type: ::core::convert::Into::into( + #bevy_reflect_path::DynamicTypePath::reflect_type_path(#FQOption::unwrap(#field_accessor)) + ), + to_type: ::core::convert::Into::into(<#ty as #bevy_reflect_path::TypePath>::type_path()) + })?), + quote!(.ok_or(#bevy_reflect_path::ApplyError::MissingEnumField { + variant_name: ::core::convert::Into::into(#name), + field_name: ::core::convert::Into::into(#field_ref_str) + })?) + ) + } else { + (quote!(?), quote!(?)) + }; + match &field.attrs.default { DefaultBehavior::Func(path) => quote! { if let #FQOption::Some(field) = #field_accessor { diff --git a/crates/bevy_reflect/derive/src/impls/enums.rs b/crates/bevy_reflect/derive/src/impls/enums.rs index 056a293ec91be..8293da7157785 100644 --- a/crates/bevy_reflect/derive/src/impls/enums.rs +++ b/crates/bevy_reflect/derive/src/impls/enums.rs @@ -230,7 +230,7 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream } #[inline] - fn apply(&mut self, #ref_value: &dyn #bevy_reflect_path::Reflect) { + fn try_apply(&mut self, #ref_value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { if let #bevy_reflect_path::ReflectRef::Enum(#ref_value) = #bevy_reflect_path::Reflect::reflect_ref(#ref_value) { if #bevy_reflect_path::Enum::variant_name(self) == #bevy_reflect_path::Enum::variant_name(#ref_value) { // Same variant -> just update fields @@ -238,12 +238,16 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #bevy_reflect_path::VariantType::Struct => { for field in #bevy_reflect_path::Enum::iter_fields(#ref_value) { let name = field.name().unwrap(); - #bevy_reflect_path::Enum::field_mut(self, name).map(|v| v.apply(field.value())); + if let #FQOption::Some(v) = #bevy_reflect_path::Enum::field_mut(self, name) { + #bevy_reflect_path::Reflect::try_apply(v, field.value())?; + } } } #bevy_reflect_path::VariantType::Tuple => { for (index, field) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::Enum::iter_fields(#ref_value)) { - #bevy_reflect_path::Enum::field_at_mut(self, index).map(|v| v.apply(field.value())); + if let #FQOption::Some(v) = #bevy_reflect_path::Enum::field_at_mut(self, index) { + #bevy_reflect_path::Reflect::try_apply(v, field.value())?; + } } } _ => {} @@ -254,12 +258,25 @@ pub(crate) fn impl_enum(reflect_enum: &ReflectEnum) -> proc_macro2::TokenStream #(#variant_names => { *self = #variant_constructors })* - name => panic!("variant with name `{}` does not exist on enum `{}`", name, ::type_path()), + name => { + return #FQResult::Err( + #bevy_reflect_path::ApplyError::UnknownVariant { + enum_name: ::core::convert::Into::into(#bevy_reflect_path::DynamicTypePath::reflect_type_path(self)), + variant_name: ::core::convert::Into::into(name), + } + ); + } } } } else { - panic!("`{}` is not an enum", #bevy_reflect_path::DynamicTypePath::reflect_type_path(#ref_value)); + return #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedKinds { + from_kind: #bevy_reflect_path::Reflect::reflect_kind(#ref_value), + to_kind: #bevy_reflect_path::ReflectKind::Enum, + } + ); } + #FQResult::Ok(()) } fn reflect_kind(&self) -> #bevy_reflect_path::ReflectKind { diff --git a/crates/bevy_reflect/derive/src/impls/structs.rs b/crates/bevy_reflect/derive/src/impls/structs.rs index 40441aafbcd7e..f51f0b2de4d23 100644 --- a/crates/bevy_reflect/derive/src/impls/structs.rs +++ b/crates/bevy_reflect/derive/src/impls/structs.rs @@ -208,29 +208,37 @@ pub(crate) fn impl_struct(reflect_struct: &ReflectStruct) -> proc_macro2::TokenS } #[inline] - fn apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) { + fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { if let #bevy_reflect_path::ReflectRef::Struct(struct_value) = #bevy_reflect_path::Reflect::reflect_ref(value) { for (i, value) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::Struct::iter_fields(struct_value)) { let name = #bevy_reflect_path::Struct::name_at(struct_value, i).unwrap(); - #bevy_reflect_path::Struct::field_mut(self, name).map(|v| v.apply(value)); + if let #FQOption::Some(v) = #bevy_reflect_path::Struct::field_mut(self, name) { + #bevy_reflect_path::Reflect::try_apply(v, value)?; + } } } else { - panic!("Attempted to apply non-struct type to struct type."); + return #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedKinds { + from_kind: #bevy_reflect_path::Reflect::reflect_kind(value), + to_kind: #bevy_reflect_path::ReflectKind::Struct + } + ); } + #FQResult::Ok(()) } - + #[inline] fn reflect_kind(&self) -> #bevy_reflect_path::ReflectKind { #bevy_reflect_path::ReflectKind::Struct } - + #[inline] fn reflect_ref(&self) -> #bevy_reflect_path::ReflectRef { #bevy_reflect_path::ReflectRef::Struct(self) } - + #[inline] fn reflect_mut(&mut self) -> #bevy_reflect_path::ReflectMut { #bevy_reflect_path::ReflectMut::Struct(self) } - + #[inline] fn reflect_owned(self: #FQBox) -> #bevy_reflect_path::ReflectOwned { #bevy_reflect_path::ReflectOwned::Struct(self) } diff --git a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs index 659261466cff5..255928cf97c6e 100644 --- a/crates/bevy_reflect/derive/src/impls/tuple_structs.rs +++ b/crates/bevy_reflect/derive/src/impls/tuple_structs.rs @@ -112,11 +112,11 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: _ => #FQOption::None, } } - + #[inline] fn field_len(&self) -> usize { #field_count } - + #[inline] fn iter_fields(&self) -> #bevy_reflect_path::TupleStructFieldIter { #bevy_reflect_path::TupleStructFieldIter::new(self) } @@ -177,28 +177,36 @@ pub(crate) fn impl_tuple_struct(reflect_struct: &ReflectStruct) -> proc_macro2:: } #[inline] - fn apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) { + fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { if let #bevy_reflect_path::ReflectRef::TupleStruct(struct_value) = #bevy_reflect_path::Reflect::reflect_ref(value) { for (i, value) in ::core::iter::Iterator::enumerate(#bevy_reflect_path::TupleStruct::iter_fields(struct_value)) { - #bevy_reflect_path::TupleStruct::field_mut(self, i).map(|v| v.apply(value)); + if let #FQOption::Some(v) = #bevy_reflect_path::TupleStruct::field_mut(self, i) { + #bevy_reflect_path::Reflect::try_apply(v, value)?; + } } } else { - panic!("Attempted to apply non-TupleStruct type to TupleStruct type."); + return #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedKinds { + from_kind: #bevy_reflect_path::Reflect::reflect_kind(value), + to_kind: #bevy_reflect_path::ReflectKind::TupleStruct, + } + ); } + #FQResult::Ok(()) } - + #[inline] fn reflect_kind(&self) -> #bevy_reflect_path::ReflectKind { #bevy_reflect_path::ReflectKind::TupleStruct } - + #[inline] fn reflect_ref(&self) -> #bevy_reflect_path::ReflectRef { #bevy_reflect_path::ReflectRef::TupleStruct(self) } - + #[inline] fn reflect_mut(&mut self) -> #bevy_reflect_path::ReflectMut { #bevy_reflect_path::ReflectMut::TupleStruct(self) } - + #[inline] fn reflect_owned(self: #FQBox) -> #bevy_reflect_path::ReflectOwned { #bevy_reflect_path::ReflectOwned::TupleStruct(self) } diff --git a/crates/bevy_reflect/derive/src/impls/values.rs b/crates/bevy_reflect/derive/src/impls/values.rs index cb9162cc4a734..c0e7b2d4fee44 100644 --- a/crates/bevy_reflect/derive/src/impls/values.rs +++ b/crates/bevy_reflect/derive/src/impls/values.rs @@ -85,14 +85,20 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { #FQBox::new(#FQClone::clone(self)) } - #[inline] - fn apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) { - let value = #bevy_reflect_path::Reflect::as_any(value); - if let #FQOption::Some(value) = ::downcast_ref::(value) { + #[inline] + fn try_apply(&mut self, value: &dyn #bevy_reflect_path::Reflect) -> #FQResult<(), #bevy_reflect_path::ApplyError> { + let any = #bevy_reflect_path::Reflect::as_any(value); + if let #FQOption::Some(value) = ::downcast_ref::(any) { *self = #FQClone::clone(value); } else { - panic!("Value is not {}.", ::type_path()); + return #FQResult::Err( + #bevy_reflect_path::ApplyError::MismatchedTypes { + from_type: ::core::convert::Into::into(#bevy_reflect_path::DynamicTypePath::reflect_type_path(value)), + to_type: ::core::convert::Into::into(::type_path()), + } + ); } + #FQResult::Ok(()) } #[inline] @@ -101,18 +107,22 @@ pub(crate) fn impl_value(meta: &ReflectMeta) -> proc_macro2::TokenStream { #FQResult::Ok(()) } + #[inline] fn reflect_kind(&self) -> #bevy_reflect_path::ReflectKind { #bevy_reflect_path::ReflectKind::Value } + #[inline] fn reflect_ref(&self) -> #bevy_reflect_path::ReflectRef { #bevy_reflect_path::ReflectRef::Value(self) } + #[inline] fn reflect_mut(&mut self) -> #bevy_reflect_path::ReflectMut { #bevy_reflect_path::ReflectMut::Value(self) } + #[inline] fn reflect_owned(self: #FQBox) -> #bevy_reflect_path::ReflectOwned { #bevy_reflect_path::ReflectOwned::Value(self) } diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 0b704a21f77e9..c49fddc2204d4 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -1,6 +1,6 @@ use crate::{ - self as bevy_reflect, utility::reflect_hasher, Reflect, ReflectKind, ReflectMut, ReflectOwned, - ReflectRef, TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, utility::reflect_hasher, ApplyError, Reflect, ReflectKind, ReflectMut, + ReflectOwned, ReflectRef, TypeInfo, TypePath, TypePathTable, }; use bevy_reflect_derive::impl_type_path; use std::{ @@ -262,6 +262,10 @@ impl Reflect for DynamicArray { array_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + array_try_apply(self, value) + } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; @@ -421,6 +425,38 @@ pub fn array_apply(array: &mut A, reflect: &dyn Reflect) { } } +/// Tries to apply the reflected [array](Array) data to the given [array](Array) and +/// returns a Result. +/// +/// # Errors +/// +/// * Returns an [`ApplyError::DifferentSize`] if the two arrays have differing lengths. +/// * Returns an [`ApplyError::MismatchedKinds`] if the reflected value is not a +/// [valid array](ReflectRef::Array). +/// * Returns any error that is generated while applying elements to each other. +/// +#[inline] +pub fn array_try_apply(array: &mut A, reflect: &dyn Reflect) -> Result<(), ApplyError> { + if let ReflectRef::Array(reflect_array) = reflect.reflect_ref() { + if array.len() != reflect_array.len() { + return Err(ApplyError::DifferentSize { + from_size: reflect_array.len(), + to_size: array.len(), + }); + } + for (i, value) in reflect_array.iter().enumerate() { + let v = array.get_mut(i).unwrap(); + v.try_apply(value)?; + } + } else { + return Err(ApplyError::MismatchedKinds { + from_kind: reflect.reflect_kind(), + to_kind: ReflectKind::Array, + }); + } + Ok(()) +} + /// Compares two [arrays](Array) (one concrete and one reflected) to see if they /// are equal. /// diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index d01c351448ae0..7d0bdccc1289b 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -1,9 +1,9 @@ use bevy_reflect_derive::impl_type_path; use crate::{ - self as bevy_reflect, enum_debug, enum_hash, enum_partial_eq, DynamicStruct, DynamicTuple, - Enum, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, Tuple, TypeInfo, - VariantFieldIter, VariantType, + self as bevy_reflect, enum_debug, enum_hash, enum_partial_eq, ApplyError, DynamicStruct, + DynamicTuple, Enum, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, Tuple, + TypeInfo, VariantFieldIter, VariantType, }; use std::any::Any; use std::fmt::Formatter; @@ -324,7 +324,7 @@ impl Reflect for DynamicEnum { } #[inline] - fn apply(&mut self, value: &dyn Reflect) { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::Enum(value) = value.reflect_ref() { if Enum::variant_name(self) == value.variant_name() { // Same variant -> just update fields @@ -333,14 +333,14 @@ impl Reflect for DynamicEnum { for field in value.iter_fields() { let name = field.name().unwrap(); if let Some(v) = Enum::field_mut(self, name) { - v.apply(field.value()); + v.try_apply(field.value())?; } } } VariantType::Tuple => { for (index, field) in value.iter_fields().enumerate() { if let Some(v) = Enum::field_at_mut(self, index) { - v.apply(field.value()); + v.try_apply(field.value())?; } } } @@ -369,8 +369,12 @@ impl Reflect for DynamicEnum { self.set_variant(value.variant_name(), dyn_variant); } } else { - panic!("`{}` is not an enum", value.reflect_type_path()); + return Err(ApplyError::MismatchedKinds { + from_kind: value.reflect_kind(), + to_kind: ReflectKind::Enum, + }); } + Ok(()) } #[inline] diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index e6cb09e83ab9c..a74194505181e 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -307,8 +307,8 @@ impl<'a> VariantField<'a> { } pub fn value(&self) -> &'a dyn Reflect { - match self { - Self::Struct(.., value) | Self::Tuple(value) => *value, + match *self { + Self::Struct(_, value) | Self::Tuple(value) => value, } } } diff --git a/crates/bevy_reflect/src/enums/mod.rs b/crates/bevy_reflect/src/enums/mod.rs index b78af06f86729..2cb77af571fcd 100644 --- a/crates/bevy_reflect/src/enums/mod.rs +++ b/crates/bevy_reflect/src/enums/mod.rs @@ -283,7 +283,9 @@ mod tests { } #[test] - #[should_panic(expected = "`bevy_reflect::DynamicTuple` is not an enum")] + #[should_panic( + expected = "called `Result::unwrap()` on an `Err` value: MismatchedKinds { from_kind: Tuple, to_kind: Enum }" + )] fn applying_non_enum_should_panic() { let mut value = MyEnum::B(0, 0); let mut dyn_tuple = DynamicTuple::default(); @@ -291,6 +293,38 @@ mod tests { value.apply(&dyn_tuple); } + #[test] + fn enum_try_apply_should_detect_type_mismatch() { + #[derive(Reflect, Debug, PartialEq)] + enum MyEnumAnalogue { + A(u32), + B(usize, usize), + C { foo: f32, bar: u8 }, + } + + let mut target = MyEnumAnalogue::A(0); + + // === Tuple === // + let result = target.try_apply(&MyEnum::B(0, 1)); + assert!( + matches!(result, Err(ApplyError::MismatchedTypes { .. })), + "`result` was {result:?}" + ); + + // === Struct === // + target = MyEnumAnalogue::C { foo: 0.0, bar: 1 }; + let result = target.try_apply(&MyEnum::C { + foo: 1.0, + bar: true, + }); + assert!( + matches!(result, Err(ApplyError::MismatchedTypes { .. })), + "`result` was {result:?}" + ); + // Type mismatch should occur after partial application. + assert_eq!(target, MyEnumAnalogue::C { foo: 1.0, bar: 1 }); + } + #[test] fn should_skip_ignored_fields() { #[derive(Reflect, Debug, PartialEq)] diff --git a/crates/bevy_reflect/src/impls/smallvec.rs b/crates/bevy_reflect/src/impls/smallvec.rs index b27c7a1e1ae9f..3943ac269bb86 100644 --- a/crates/bevy_reflect/src/impls/smallvec.rs +++ b/crates/bevy_reflect/src/impls/smallvec.rs @@ -5,9 +5,9 @@ use std::any::Any; use crate::utility::GenericTypeInfoCell; use crate::{ - self as bevy_reflect, FromReflect, FromType, GetTypeRegistration, List, ListInfo, ListIter, - Reflect, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, - TypeRegistration, Typed, + self as bevy_reflect, ApplyError, FromReflect, FromType, GetTypeRegistration, List, ListInfo, + ListIter, Reflect, ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, + TypePath, TypeRegistration, Typed, }; impl List for SmallVec @@ -113,6 +113,10 @@ where crate::list_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 55bdd75e36fd9..bcee1b51ee6e8 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -1,15 +1,15 @@ use crate::std_traits::ReflectDefault; -use crate::{self as bevy_reflect, ReflectFromPtr, ReflectFromReflect, ReflectOwned, TypeRegistry}; -use crate::{ - impl_type_path, map_apply, map_partial_eq, Array, ArrayInfo, ArrayIter, DynamicMap, - FromReflect, FromType, GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, - Reflect, ReflectDeserialize, ReflectKind, ReflectMut, ReflectRef, ReflectSerialize, TypeInfo, - TypePath, TypeRegistration, Typed, ValueInfo, -}; - use crate::utility::{ reflect_hasher, GenericTypeInfoCell, GenericTypePathCell, NonGenericTypeInfoCell, }; +use crate::{ + self as bevy_reflect, impl_type_path, map_apply, map_partial_eq, map_try_apply, ApplyError, + Array, ArrayInfo, ArrayIter, DynamicMap, DynamicTypePath, FromReflect, FromType, + GetTypeRegistration, List, ListInfo, ListIter, Map, MapInfo, MapIter, Reflect, + ReflectDeserialize, ReflectFromPtr, ReflectFromReflect, ReflectKind, ReflectMut, ReflectOwned, + ReflectRef, ReflectSerialize, TypeInfo, TypePath, TypeRegistration, TypeRegistry, Typed, + ValueInfo, +}; use bevy_reflect_derive::{impl_reflect, impl_reflect_value}; use std::fmt; use std::{ @@ -314,6 +314,10 @@ macro_rules! impl_reflect_for_veclike { crate::list_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -540,6 +544,10 @@ macro_rules! impl_reflect_for_hashmap { map_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -765,6 +773,10 @@ where map_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -909,6 +921,11 @@ impl Reflect for [T crate::array_apply(self, value); } + #[inline] + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::array_try_apply(self, value) + } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; @@ -1063,13 +1080,18 @@ impl Reflect for Cow<'static, str> { self } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + let any = value.as_any(); + if let Some(value) = any.downcast_ref::() { self.clone_from(value); } else { - panic!("Value is not a {}.", Self::type_path()); + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + // If we invoke the reflect_type_path on self directly the borrow checker complains that the lifetime of self must outlive 'static + to_type: Self::type_path().into(), + }); } + Ok(()) } fn set(&mut self, value: Box) -> Result<(), Box> { @@ -1253,6 +1275,10 @@ impl Reflect for Cow<'s crate::list_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::list_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -1349,13 +1375,17 @@ impl Reflect for &'static str { self } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(&value) = value.downcast_ref::() { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + let any = value.as_any(); + if let Some(&value) = any.downcast_ref::() { *self = value; } else { - panic!("Value is not a {}.", Self::type_path()); + return Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: Self::type_path().into(), + }); } + Ok(()) } fn set(&mut self, value: Box) -> Result<(), Box> { @@ -1451,12 +1481,16 @@ impl Reflect for &'static Path { self } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(&value) = value.downcast_ref::() { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + let any = value.as_any(); + if let Some(&value) = any.downcast_ref::() { *self = value; + Ok(()) } else { - panic!("Value is not a {}.", Self::type_path()); + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) } } @@ -1552,12 +1586,16 @@ impl Reflect for Cow<'static, Path> { self } - fn apply(&mut self, value: &dyn Reflect) { - let value = value.as_any(); - if let Some(value) = value.downcast_ref::() { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + let any = value.as_any(); + if let Some(value) = any.downcast_ref::() { self.clone_from(value); + Ok(()) } else { - panic!("Value is not a {}.", Self::type_path()); + Err(ApplyError::MismatchedTypes { + from_type: value.reflect_type_path().into(), + to_type: ::reflect_type_path(self).into(), + }) } } @@ -1789,14 +1827,14 @@ mod tests { // === None on None === // let patch = None::; - let mut value = None; + let mut value = None::; Reflect::apply(&mut value, &patch); assert_eq!(patch, value, "None apply onto None"); // === Some on None === // let patch = Some(Foo(123)); - let mut value = None; + let mut value = None::; Reflect::apply(&mut value, &patch); assert_eq!(patch, value, "Some apply onto None"); diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index 073fb5000bfee..b95c997906223 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -172,7 +172,7 @@ //! ## Patching //! //! These dynamic types come in handy when needing to apply multiple changes to another type. -//! This is known as "patching" and is done using the [`Reflect::apply`] method. +//! This is known as "patching" and is done using the [`Reflect::apply`] and [`Reflect::try_apply`] methods. //! //! ``` //! # use bevy_reflect::{DynamicEnum, Reflect}; @@ -613,6 +613,54 @@ mod tests { use crate::serde::{ReflectDeserializer, ReflectSerializer}; use crate::utility::GenericTypePathCell; + #[test] + fn try_apply_should_detect_kinds() { + #[derive(Reflect, Debug)] + struct Struct { + a: u32, + b: f32, + } + + #[derive(Reflect, Debug)] + enum Enum { + A, + B(u32), + } + + let mut struct_target = Struct { + a: 0xDEADBEEF, + b: 3.14, + }; + + let mut enum_target = Enum::A; + + let array_src = [8, 0, 8]; + + let result = struct_target.try_apply(&enum_target); + assert!( + matches!( + result, + Err(ApplyError::MismatchedKinds { + from_kind: ReflectKind::Enum, + to_kind: ReflectKind::Struct + }) + ), + "result was {result:?}" + ); + + let result = enum_target.try_apply(&array_src); + assert!( + matches!( + result, + Err(ApplyError::MismatchedKinds { + from_kind: ReflectKind::Array, + to_kind: ReflectKind::Enum + }) + ), + "result was {result:?}" + ); + } + #[test] fn reflect_struct() { #[derive(Reflect)] diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index bc423132c02ea..e01709b30db25 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -6,8 +6,8 @@ use bevy_reflect_derive::impl_type_path; use crate::utility::reflect_hasher; use crate::{ - self as bevy_reflect, FromReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, ApplyError, FromReflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, + ReflectRef, TypeInfo, TypePath, TypePathTable, }; /// A trait used to power [list-like] operations via [reflection]. @@ -312,6 +312,10 @@ impl Reflect for DynamicList { list_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + list_try_apply(self, value) + } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; @@ -436,19 +440,40 @@ pub fn list_hash(list: &L) -> Option { /// This function panics if `b` is not a list. #[inline] pub fn list_apply(a: &mut L, b: &dyn Reflect) { + if let Err(err) = list_try_apply(a, b) { + panic!("{err}"); + } +} + +/// Tries to apply the elements of `b` to the corresponding elements of `a` and +/// returns a Result. +/// +/// If the length of `b` is greater than that of `a`, the excess elements of `b` +/// are cloned and appended to `a`. +/// +/// # Errors +/// +/// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a list or if +/// applying elements to each other fails. +#[inline] +pub fn list_try_apply(a: &mut L, b: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::List(list_value) = b.reflect_ref() { for (i, value) in list_value.iter().enumerate() { if i < a.len() { if let Some(v) = a.get_mut(i) { - v.apply(value); + v.try_apply(value)?; } } else { - a.push(value.clone_value()); + List::push(a, value.clone_value()); } } } else { - panic!("Attempted to apply a non-list type to a list type."); + return Err(ApplyError::MismatchedKinds { + from_kind: b.reflect_kind(), + to_kind: ReflectKind::List, + }); } + Ok(()) } /// Compares a [`List`] with a [`Reflect`] value. diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 41ef9e575bb4f..085eda219e844 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -5,8 +5,8 @@ use bevy_reflect_derive::impl_type_path; use bevy_utils::{Entry, HashMap}; use crate::{ - self as bevy_reflect, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, - TypePath, TypePathTable, + self as bevy_reflect, ApplyError, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, + TypeInfo, TypePath, TypePathTable, }; /// A trait used to power [map-like] operations via [reflection]. @@ -345,6 +345,10 @@ impl Reflect for DynamicMap { map_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + map_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -502,17 +506,37 @@ pub fn map_debug(dyn_map: &dyn Map, f: &mut Formatter<'_>) -> std::fmt::Result { /// This function panics if `b` is not a reflected map. #[inline] pub fn map_apply(a: &mut M, b: &dyn Reflect) { + if let Err(err) = map_try_apply(a, b) { + panic!("{err}"); + } +} + +/// Tries to apply the elements of reflected map `b` to the corresponding elements of map `a` +/// and returns a Result. +/// +/// If a key from `b` does not exist in `a`, the value is cloned and inserted. +/// +/// # Errors +/// +/// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a reflected map or if +/// applying elements to each other fails. +#[inline] +pub fn map_try_apply(a: &mut M, b: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::Map(map_value) = b.reflect_ref() { for (key, b_value) in map_value.iter() { if let Some(a_value) = a.get_mut(key) { - a_value.apply(b_value); + a_value.try_apply(b_value)?; } else { a.insert_boxed(key.clone_value(), b_value.clone_value()); } } } else { - panic!("Attempted to apply a non-map type to a map type."); + return Err(ApplyError::MismatchedKinds { + from_kind: b.reflect_kind(), + to_kind: ReflectKind::Map, + }); } + Ok(()) } #[cfg(test)] diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 9a50decb7240b..43108e61a4278 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -8,6 +8,8 @@ use std::{ fmt::Debug, }; +use thiserror::Error; + use crate::utility::NonGenericTypeInfoCell; macro_rules! impl_reflect_enum { @@ -99,6 +101,42 @@ pub enum ReflectOwned { } impl_reflect_enum!(ReflectOwned); +/// A enumeration of all error outcomes that might happen when running [`try_apply`](Reflect::try_apply). +#[derive(Error, Debug)] +pub enum ApplyError { + #[error("attempted to apply `{from_kind}` to `{to_kind}`")] + /// Attempted to apply the wrong [kind](ReflectKind) to a type, e.g. a struct to a enum. + MismatchedKinds { + from_kind: ReflectKind, + to_kind: ReflectKind, + }, + + #[error("enum variant `{variant_name}` doesn't have a field named `{field_name}`")] + /// Enum variant that we tried to apply to was missing a field. + MissingEnumField { + variant_name: Box, + field_name: Box, + }, + + #[error("`{from_type}` is not `{to_type}`")] + /// Tried to apply incompatible types. + MismatchedTypes { + from_type: Box, + to_type: Box, + }, + + #[error("attempted to apply type with {from_size} size to a type with {to_size} size")] + /// Attempted to apply to types with mismatched sizez, e.g. a [u8; 4] to [u8; 3]. + DifferentSize { from_size: usize, to_size: usize }, + + #[error("variant with name `{variant_name}` does not exist on enum `{enum_name}`")] + /// The enum we tried to apply to didn't contain a variant with the give name. + UnknownVariant { + enum_name: Box, + variant_name: Box, + }, +} + /// A zero-sized enumuration of the "kinds" of a reflected type. /// /// A [`ReflectKind`] is obtained via [`Reflect::reflect_kind`], @@ -217,7 +255,20 @@ pub trait Reflect: DynamicTypePath + Any + Send + Sync { /// - If `T` is any complex type and the corresponding fields or elements of /// `self` and `value` are not of the same type. /// - If `T` is a value type and `self` cannot be downcast to `T` - fn apply(&mut self, value: &dyn Reflect); + fn apply(&mut self, value: &dyn Reflect) { + Reflect::try_apply(self, value).unwrap(); + } + + /// Tries to [`apply`](Reflect::apply) a reflected value to this value. + /// + /// Functions the same as the [`apply`](Reflect::apply) function but returns an error instead of + /// panicking. + /// + /// # Handling Errors + /// + /// This function may leave `self` in a partially mutated state if a error was encountered on the way. + /// consider maintaining a cloned instance of this data you can switch to if a error is encountered. + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError>; /// Performs a type-checked assignment of a reflected value to this value. /// diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index d26ea6adb5f42..b342ea92a6152 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -1,6 +1,6 @@ use crate::{ - self as bevy_reflect, NamedField, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - TypeInfo, TypePath, TypePathTable, + self as bevy_reflect, ApplyError, NamedField, Reflect, ReflectKind, ReflectMut, ReflectOwned, + ReflectRef, TypeInfo, TypePath, TypePathTable, }; use bevy_reflect_derive::impl_type_path; use bevy_utils::HashMap; @@ -420,19 +420,24 @@ impl Reflect for DynamicStruct { self } - fn apply(&mut self, value: &dyn Reflect) { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::Struct(struct_value) = value.reflect_ref() { for (i, value) in struct_value.iter_fields().enumerate() { let name = struct_value.name_at(i).unwrap(); if let Some(v) = self.field_mut(name) { - v.apply(value); + v.try_apply(value)?; } } } else { - panic!("Attempted to apply non-struct type to struct type."); + return Err(ApplyError::MismatchedKinds { + from_kind: value.reflect_kind(), + to_kind: ReflectKind::Struct, + }); } + Ok(()) } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index de6af437df4bb..50eb9512f96ce 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -2,9 +2,9 @@ use bevy_reflect_derive::impl_type_path; use bevy_utils::all_tuples; use crate::{ - self as bevy_reflect, utility::GenericTypePathCell, FromReflect, GetTypeRegistration, Reflect, - ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, TypeRegistration, TypeRegistry, - Typed, UnnamedField, + self as bevy_reflect, utility::GenericTypePathCell, ApplyError, FromReflect, + GetTypeRegistration, Reflect, ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypePath, + TypeRegistration, TypeRegistry, Typed, UnnamedField, }; use crate::{ReflectKind, TypePathTable}; use std::any::{Any, TypeId}; @@ -369,6 +369,10 @@ impl Reflect for DynamicTuple { Box::new(self.clone_dynamic()) } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + tuple_try_apply(self, value) + } + fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { tuple_partial_eq(self, value) } @@ -394,15 +398,33 @@ impl_type_path!((in bevy_reflect) DynamicTuple); /// This function panics if `b` is not a tuple. #[inline] pub fn tuple_apply(a: &mut T, b: &dyn Reflect) { + if let Err(err) = tuple_try_apply(a, b) { + panic!("{err}"); + } +} + +/// Tries to apply the elements of `b` to the corresponding elements of `a` and +/// returns a Result. +/// +/// # Errors +/// +/// This function returns an [`ApplyError::MismatchedKinds`] if `b` is not a tuple or if +/// applying elements to each other fails. +#[inline] +pub fn tuple_try_apply(a: &mut T, b: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::Tuple(tuple) = b.reflect_ref() { for (i, value) in tuple.iter_fields().enumerate() { if let Some(v) = a.field_mut(i) { - v.apply(value); + v.try_apply(value)?; } } } else { - panic!("Attempted to apply non-Tuple type to Tuple type."); + return Err(ApplyError::MismatchedKinds { + from_kind: b.reflect_kind(), + to_kind: ReflectKind::Tuple, + }); } + Ok(()) } /// Compares a [`Tuple`] with a [`Reflect`] value. @@ -545,6 +567,10 @@ macro_rules! impl_reflect_tuple { crate::tuple_apply(self, value); } + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { + crate::tuple_try_apply(self, value) + } + fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index c8699b506e040..b6cb0b720cc0b 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -1,8 +1,8 @@ use bevy_reflect_derive::impl_type_path; use crate::{ - self as bevy_reflect, DynamicTuple, Reflect, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, - Tuple, TypeInfo, TypePath, TypePathTable, UnnamedField, + self as bevy_reflect, ApplyError, DynamicTuple, Reflect, ReflectKind, ReflectMut, ReflectOwned, + ReflectRef, Tuple, TypeInfo, TypePath, TypePathTable, UnnamedField, }; use std::any::{Any, TypeId}; use std::fmt::{Debug, Formatter}; @@ -329,18 +329,23 @@ impl Reflect for DynamicTupleStruct { self } - fn apply(&mut self, value: &dyn Reflect) { + fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { if let ReflectRef::TupleStruct(tuple_struct) = value.reflect_ref() { for (i, value) in tuple_struct.iter_fields().enumerate() { if let Some(v) = self.field_mut(i) { - v.apply(value); + v.try_apply(value)?; } } } else { - panic!("Attempted to apply non-TupleStruct type to TupleStruct type."); + return Err(ApplyError::MismatchedKinds { + from_kind: value.reflect_kind(), + to_kind: ReflectKind::TupleStruct, + }); } + Ok(()) } + #[inline] fn set(&mut self, value: Box) -> Result<(), Box> { *self = value.take()?; Ok(()) @@ -371,6 +376,7 @@ impl Reflect for DynamicTupleStruct { Box::new(self.clone_dynamic()) } + #[inline] fn reflect_partial_eq(&self, value: &dyn Reflect) -> Option { tuple_struct_partial_eq(self, value) } diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index 6b61b71fdf14e..c64131a7b825f 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -25,7 +25,7 @@ use std::fmt::Debug; /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, TypeInfo, TypePath, ValueInfo}; +/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, TypeInfo, TypePath, ValueInfo, ApplyError}; /// # use bevy_reflect::utility::NonGenericTypeInfoCell; /// use bevy_reflect::Typed; /// @@ -60,7 +60,7 @@ use std::fmt::Debug; /// # fn into_reflect(self: Box) -> Box { todo!() } /// # fn as_reflect(&self) -> &dyn Reflect { todo!() } /// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn apply(&mut self, value: &dyn Reflect) { todo!() } +/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } /// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } /// # fn reflect_ref(&self) -> ReflectRef { todo!() } /// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } diff --git a/crates/bevy_reflect/src/type_path.rs b/crates/bevy_reflect/src/type_path.rs index 9d446cc81088e..d6e6a4ad44ef4 100644 --- a/crates/bevy_reflect/src/type_path.rs +++ b/crates/bevy_reflect/src/type_path.rs @@ -147,22 +147,27 @@ pub trait DynamicTypePath { } impl DynamicTypePath for T { + #[inline] fn reflect_type_path(&self) -> &str { Self::type_path() } + #[inline] fn reflect_short_type_path(&self) -> &str { Self::short_type_path() } + #[inline] fn reflect_type_ident(&self) -> Option<&str> { Self::type_ident() } + #[inline] fn reflect_crate_name(&self) -> Option<&str> { Self::crate_name() } + #[inline] fn reflect_module_path(&self) -> Option<&str> { Self::module_path() } diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index df860d194c373..86dcbbc175462 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -49,7 +49,7 @@ mod sealed { /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, Typed, TypeInfo, TypePath}; +/// # use bevy_reflect::{DynamicTypePath, NamedField, Reflect, ReflectMut, ReflectOwned, ReflectRef, StructInfo, Typed, TypeInfo, TypePath, ApplyError}; /// use bevy_reflect::utility::NonGenericTypeInfoCell; /// /// struct Foo { @@ -78,7 +78,7 @@ mod sealed { /// # fn into_reflect(self: Box) -> Box { todo!() } /// # fn as_reflect(&self) -> &dyn Reflect { todo!() } /// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn apply(&mut self, value: &dyn Reflect) { todo!() } +/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } /// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } /// # fn reflect_ref(&self) -> ReflectRef { todo!() } /// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } @@ -130,7 +130,7 @@ impl Default for NonGenericTypeCell { /// /// ``` /// # use std::any::Any; -/// # use bevy_reflect::{DynamicTypePath, Reflect, ReflectMut, ReflectOwned, ReflectRef, TupleStructInfo, Typed, TypeInfo, TypePath, UnnamedField}; +/// # use bevy_reflect::{DynamicTypePath, Reflect, ReflectMut, ReflectOwned, ReflectRef, TupleStructInfo, Typed, TypeInfo, TypePath, UnnamedField, ApplyError}; /// use bevy_reflect::utility::GenericTypeInfoCell; /// /// struct Foo(T); @@ -157,7 +157,7 @@ impl Default for NonGenericTypeCell { /// # fn into_reflect(self: Box) -> Box { todo!() } /// # fn as_reflect(&self) -> &dyn Reflect { todo!() } /// # fn as_reflect_mut(&mut self) -> &mut dyn Reflect { todo!() } -/// # fn apply(&mut self, value: &dyn Reflect) { todo!() } +/// # fn try_apply(&mut self, value: &dyn Reflect) -> Result<(), ApplyError> { todo!() } /// # fn set(&mut self, value: Box) -> Result<(), Box> { todo!() } /// # fn reflect_ref(&self) -> ReflectRef { todo!() } /// # fn reflect_mut(&mut self) -> ReflectMut { todo!() } From d9d305dab5b3b7a12a634ea7adb5c5a4c6406ec1 Mon Sep 17 00:00:00 2001 From: Vitaliy Sapronenko Date: Wed, 8 May 2024 17:26:26 +0300 Subject: [PATCH 17/26] Headless renderer example has been added (#13006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Fixes #11457. Fixes #22. ## Solution Based on [another headless application](https://github.com/richardanaya/headless/) --- ## Changelog - Adopted to bevy 0.14 --------- Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com> Co-authored-by: François Mockers --- Cargo.toml | 11 + examples/README.md | 1 + examples/app/headless_renderer.rs | 529 ++++++++++++++++++++++++++++++ 3 files changed, 541 insertions(+) create mode 100644 examples/app/headless_renderer.rs diff --git a/Cargo.toml b/Cargo.toml index 50b686ce00e06..6166492496727 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1274,6 +1274,17 @@ description = "An application that runs with default plugins and displays an emp category = "Application" wasm = false +[[example]] +name = "headless_renderer" +path = "examples/app/headless_renderer.rs" +doc-scrape-examples = true + +[package.metadata.example.headless_renderer] +name = "Headless Renderer" +description = "An application that runs with no window, but renders into image file" +category = "Application" +wasm = false + [[example]] name = "without_winit" path = "examples/app/without_winit.rs" diff --git a/examples/README.md b/examples/README.md index 6534b266f431b..4dd859c6b7a1f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -188,6 +188,7 @@ Example | Description [Empty](../examples/app/empty.rs) | An empty application (does nothing) [Empty with Defaults](../examples/app/empty_defaults.rs) | An empty application with default plugins [Headless](../examples/app/headless.rs) | An application that runs without default plugins +[Headless Renderer](../examples/app/headless_renderer.rs) | An application that runs with no window, but renders into image file [Log layers](../examples/app/log_layers.rs) | Illustrate how to add custom log layers [Logs](../examples/app/logs.rs) | Illustrate how to use generate log output [No Renderer](../examples/app/no_renderer.rs) | An application that runs with default plugins and displays an empty window, but without an actual renderer diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs new file mode 100644 index 0000000000000..e4b634f31130b --- /dev/null +++ b/examples/app/headless_renderer.rs @@ -0,0 +1,529 @@ +//! This example illustrates how to make headless renderer +//! derived from: +//! It follows this steps: +//! 1. Render from camera to gpu-image render target +//! 2. Copy form gpu image to buffer using `ImageCopyDriver` node in `RenderGraph` +//! 3. Copy from buffer to channel using `receive_image_from_buffer` after `RenderSet::Render` +//! 4. Save from channel to random named file using `scene::update` at `PostUpdate` in `MainWorld` +//! 5. Exit if `single_image` setting is set + +use bevy::{ + app::{AppExit, ScheduleRunnerPlugin}, + core_pipeline::tonemapping::Tonemapping, + prelude::*, + render::{ + camera::RenderTarget, + render_asset::RenderAssetUsages, + render_asset::RenderAssets, + render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, RenderLabel}, + render_resource::{ + Buffer, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, Extent3d, + ImageCopyBuffer, ImageDataLayout, Maintain, MapMode, TextureDimension, TextureFormat, + TextureUsages, + }, + renderer::{RenderContext, RenderDevice, RenderQueue}, + texture::BevyDefault, + Extract, Render, RenderApp, RenderSet, + }, +}; +use crossbeam_channel::{Receiver, Sender}; +use std::{ + ops::{Deref, DerefMut}, + path::PathBuf, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; + +// To communicate between the main world and the render world we need a channel. +// Since the main world and render world run in parallel, there will always be a frame of latency +// between the data sent from the render world and the data received in the main world +// +// frame n => render world sends data through the channel at the end of the frame +// frame n + 1 => main world receives the data +// +// Receiver and Sender are kept in resources because there is single camera and single target +// That's why there is single images role, if you want to differentiate images +// from different cameras, you should keep Receiver in ImageCopier and Sender in ImageToSave +// or send some id with data + +/// This will receive asynchronously any data sent from the render world +#[derive(Resource, Deref)] +struct MainWorldReceiver(Receiver>); + +/// This will send asynchronously any data to the main world +#[derive(Resource, Deref)] +struct RenderWorldSender(Sender>); + +// Parameters of resulting image +struct AppConfig { + width: u32, + height: u32, + single_image: bool, +} + +fn main() { + let config = AppConfig { + width: 1920, + height: 1080, + single_image: true, + }; + + // setup frame capture + App::new() + .insert_resource(SceneController::new( + config.width, + config.height, + config.single_image, + )) + .insert_resource(ClearColor(Color::srgb_u8(0, 0, 0))) + .add_plugins( + DefaultPlugins + .set(ImagePlugin::default_nearest()) + // Do not create a window on startup. + .set(WindowPlugin { + primary_window: None, + exit_condition: bevy::window::ExitCondition::DontExit, + close_when_requested: false, + }), + ) + .add_plugins(ImageCopyPlugin) + // headless frame capture + .add_plugins(CaptureFramePlugin) + .add_plugins(ScheduleRunnerPlugin::run_loop( + // Run 60 times per second. + Duration::from_secs_f64(1.0 / 60.0), + )) + .init_resource::() + .add_systems(Startup, setup) + .run(); +} + +/// Capture image settings and state +#[derive(Debug, Default, Resource)] +struct SceneController { + state: SceneState, + name: String, + width: u32, + height: u32, + single_image: bool, +} + +impl SceneController { + pub fn new(width: u32, height: u32, single_image: bool) -> SceneController { + SceneController { + state: SceneState::BuildScene, + name: String::from(""), + width, + height, + single_image, + } + } +} + +/// Capture image state +#[derive(Debug, Default)] +enum SceneState { + #[default] + // State before any rendering + BuildScene, + // Rendering state, stores the number of frames remaining before saving the image + Render(u32), +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut images: ResMut>, + mut scene_controller: ResMut, + render_device: Res, +) { + let render_target = setup_render_target( + &mut commands, + &mut images, + &render_device, + &mut scene_controller, + // pre_roll_frames should be big enough for full scene render, + // but the bigger it is, the longer example will run. + // To visualize stages of scene rendering change this param to 0 + // and change AppConfig::single_image to false in main + // Stages are: + // 1. Transparent image + // 2. Few black box images + // 3. Fully rendered scene images + // Exact number depends on device speed, device load and scene size + 40, + "main_scene".into(), + ); + + // Scene example for non black box picture + // circular base + commands.spawn(PbrBundle { + mesh: meshes.add(Circle::new(4.0)), + material: materials.add(Color::WHITE), + transform: Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + ..default() + }); + // cube + commands.spawn(PbrBundle { + mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), + material: materials.add(Color::srgb_u8(124, 144, 255)), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..default() + }); + // light + commands.spawn(PointLightBundle { + point_light: PointLight { + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..default() + }); + + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + tonemapping: Tonemapping::None, + camera: Camera { + // render to image + target: render_target, + ..default() + }, + ..default() + }); +} + +/// Plugin for Render world part of work +pub struct ImageCopyPlugin; +impl Plugin for ImageCopyPlugin { + fn build(&self, app: &mut App) { + let (s, r) = crossbeam_channel::unbounded(); + + let render_app = app + .insert_resource(MainWorldReceiver(r)) + .sub_app_mut(RenderApp); + + let mut graph = render_app.world_mut().resource_mut::(); + graph.add_node(ImageCopy, ImageCopyDriver); + graph.add_node_edge(bevy::render::graph::CameraDriverLabel, ImageCopy); + + render_app + .insert_resource(RenderWorldSender(s)) + // Make ImageCopiers accessible in RenderWorld system and plugin + .add_systems(ExtractSchedule, image_copy_extract) + // Receives image data from buffer to channel + // so we need to run it after the render graph is done + .add_systems(Render, receive_image_from_buffer.after(RenderSet::Render)); + } +} + +/// Setups render target and cpu image for saving, changes scene state into render mode +fn setup_render_target( + commands: &mut Commands, + images: &mut ResMut>, + render_device: &Res, + scene_controller: &mut ResMut, + pre_roll_frames: u32, + scene_name: String, +) -> RenderTarget { + let size = Extent3d { + width: scene_controller.width, + height: scene_controller.height, + ..Default::default() + }; + + // This is the texture that will be rendered to. + let mut render_target_image = Image::new_fill( + size, + TextureDimension::D2, + &[0; 4], + TextureFormat::bevy_default(), + RenderAssetUsages::default(), + ); + render_target_image.texture_descriptor.usage |= + TextureUsages::COPY_SRC | TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING; + let render_target_image_handle = images.add(render_target_image); + + // This is the texture that will be copied to. + let cpu_image = Image::new_fill( + size, + TextureDimension::D2, + &[0; 4], + TextureFormat::bevy_default(), + RenderAssetUsages::default(), + ); + let cpu_image_handle = images.add(cpu_image); + + commands.spawn(ImageCopier::new( + render_target_image_handle.clone(), + size, + render_device, + )); + + commands.spawn(ImageToSave(cpu_image_handle)); + + scene_controller.state = SceneState::Render(pre_roll_frames); + scene_controller.name = scene_name; + RenderTarget::Image(render_target_image_handle) +} + +/// Setups image saver +pub struct CaptureFramePlugin; +impl Plugin for CaptureFramePlugin { + fn build(&self, app: &mut App) { + info!("Adding CaptureFramePlugin"); + app.add_systems(PostUpdate, update); + } +} + +/// `ImageCopier` aggregator in `RenderWorld` +#[derive(Clone, Default, Resource, Deref, DerefMut)] +struct ImageCopiers(pub Vec); + +/// Used by `ImageCopyDriver` for copying from render target to buffer +#[derive(Clone, Component)] +struct ImageCopier { + buffer: Buffer, + enabled: Arc, + src_image: Handle, +} + +impl ImageCopier { + pub fn new( + src_image: Handle, + size: Extent3d, + render_device: &RenderDevice, + ) -> ImageCopier { + let padded_bytes_per_row = + RenderDevice::align_copy_bytes_per_row((size.width) as usize) * 4; + + let cpu_buffer = render_device.create_buffer(&BufferDescriptor { + label: None, + size: padded_bytes_per_row as u64 * size.height as u64, + usage: BufferUsages::MAP_READ | BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + ImageCopier { + buffer: cpu_buffer, + src_image, + enabled: Arc::new(AtomicBool::new(true)), + } + } + + pub fn enabled(&self) -> bool { + self.enabled.load(Ordering::Relaxed) + } +} + +/// Extracting `ImageCopier`s into render world, because `ImageCopyDriver` accesses them +fn image_copy_extract(mut commands: Commands, image_copiers: Extract>) { + commands.insert_resource(ImageCopiers( + image_copiers.iter().cloned().collect::>(), + )); +} + +/// `RenderGraph` label for `ImageCopyDriver` +#[derive(Debug, PartialEq, Eq, Clone, Hash, RenderLabel)] +struct ImageCopy; + +/// `RenderGraph` node +#[derive(Default)] +struct ImageCopyDriver; + +// Copies image content from render target to buffer +impl render_graph::Node for ImageCopyDriver { + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let image_copiers = world.get_resource::().unwrap(); + let gpu_images = world + .get_resource::>() + .unwrap(); + + for image_copier in image_copiers.iter() { + if !image_copier.enabled() { + continue; + } + + let src_image = gpu_images.get(&image_copier.src_image).unwrap(); + + let mut encoder = render_context + .render_device() + .create_command_encoder(&CommandEncoderDescriptor::default()); + + let block_dimensions = src_image.texture_format.block_dimensions(); + let block_size = src_image.texture_format.block_copy_size(None).unwrap(); + + let padded_bytes_per_row = RenderDevice::align_copy_bytes_per_row( + (src_image.size.x as usize / block_dimensions.0 as usize) * block_size as usize, + ); + + let texture_extent = Extent3d { + width: src_image.size.x, + height: src_image.size.y, + depth_or_array_layers: 1, + }; + + encoder.copy_texture_to_buffer( + src_image.texture.as_image_copy(), + ImageCopyBuffer { + buffer: &image_copier.buffer, + layout: ImageDataLayout { + offset: 0, + bytes_per_row: Some( + std::num::NonZeroU32::new(padded_bytes_per_row as u32) + .unwrap() + .into(), + ), + rows_per_image: None, + }, + }, + texture_extent, + ); + + let render_queue = world.get_resource::().unwrap(); + render_queue.submit(std::iter::once(encoder.finish())); + } + + Ok(()) + } +} + +/// runs in render world after Render stage to send image from buffer via channel (receiver is in main world) +fn receive_image_from_buffer( + image_copiers: Res, + render_device: Res, + sender: Res, +) { + for image_copier in image_copiers.0.iter() { + if !image_copier.enabled() { + continue; + } + + // Finally time to get our data back from the gpu. + // First we get a buffer slice which represents a chunk of the buffer (which we + // can't access yet). + // We want the whole thing so use unbounded range. + let buffer_slice = image_copier.buffer.slice(..); + + // Now things get complicated. WebGPU, for safety reasons, only allows either the GPU + // or CPU to access a buffer's contents at a time. We need to "map" the buffer which means + // flipping ownership of the buffer over to the CPU and making access legal. We do this + // with `BufferSlice::map_async`. + // + // The problem is that map_async is not an async function so we can't await it. What + // we need to do instead is pass in a closure that will be executed when the slice is + // either mapped or the mapping has failed. + // + // The problem with this is that we don't have a reliable way to wait in the main + // code for the buffer to be mapped and even worse, calling get_mapped_range or + // get_mapped_range_mut prematurely will cause a panic, not return an error. + // + // Using channels solves this as awaiting the receiving of a message from + // the passed closure will force the outside code to wait. It also doesn't hurt + // if the closure finishes before the outside code catches up as the message is + // buffered and receiving will just pick that up. + // + // It may also be worth noting that although on native, the usage of asynchronous + // channels is wholly unnecessary, for the sake of portability to WASM + // we'll use async channels that work on both native and WASM. + + let (s, r) = crossbeam_channel::bounded(1); + + // Maps the buffer so it can be read on the cpu + buffer_slice.map_async(MapMode::Read, move |r| match r { + // This will execute once the gpu is ready, so after the call to poll() + Ok(r) => s.send(r).expect("Failed to send map update"), + Err(err) => panic!("Failed to map buffer {err}"), + }); + + // In order for the mapping to be completed, one of three things must happen. + // One of those can be calling `Device::poll`. This isn't necessary on the web as devices + // are polled automatically but natively, we need to make sure this happens manually. + // `Maintain::Wait` will cause the thread to wait on native but not on WebGpu. + + // This blocks until the gpu is done executing everything + render_device.poll(Maintain::wait()).panic_on_timeout(); + + // This blocks until the buffer is mapped + r.recv().expect("Failed to receive the map_async message"); + + // This could fail on app exit, if Main world clears resources (including receiver) while Render world still renders + let _ = sender.send(buffer_slice.get_mapped_range().to_vec()); + + // We need to make sure all `BufferView`'s are dropped before we do what we're about + // to do. + // Unmap so that we can copy to the staging buffer in the next iteration. + image_copier.buffer.unmap(); + } +} + +/// CPU-side image for saving +#[derive(Component, Deref, DerefMut)] +struct ImageToSave(Handle); + +// Takes from channel image content sent from render world and saves it to disk +fn update( + images_to_save: Query<&ImageToSave>, + receiver: Res, + mut images: ResMut>, + mut scene_controller: ResMut, + mut app_exit_writer: EventWriter, + mut file_number: Local, +) { + if let SceneState::Render(n) = scene_controller.state { + if n < 1 { + // We don't want to block the main world on this, + // so we use try_recv which attempts to receive without blocking + let mut image_data = Vec::new(); + while let Ok(data) = receiver.try_recv() { + // image generation could be faster than saving to fs, + // that's why use only last of them + image_data = data; + } + if !image_data.is_empty() { + for image in images_to_save.iter() { + // Fill correct data from channel to image + let img_bytes = images.get_mut(image.id()).unwrap(); + img_bytes.data.clone_from(&image_data); + + // Create RGBA Image Buffer + let img = match img_bytes.clone().try_into_dynamic() { + Ok(img) => img.to_rgba8(), + Err(e) => panic!("Failed to create image buffer {e:?}"), + }; + + // Prepare directory for images, test_images in bevy folder is used here for example + // You should choose the path depending on your needs + let images_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test_images"); + info!("Saving image to: {images_dir:?}"); + std::fs::create_dir_all(&images_dir).unwrap(); + + // Choose filename starting from 000.png + let image_path = images_dir.join(format!("{:03}.png", file_number.deref())); + *file_number.deref_mut() += 1; + + // Finally saving image to file, this heavy blocking operation is kept here + // for example simplicity, but in real app you should move it to a separate task + if let Err(e) = img.save(image_path) { + panic!("Failed to save image: {}", e); + }; + } + if scene_controller.single_image { + app_exit_writer.send(AppExit::Success); + } + } + } else { + // clears channel for skipped frames + while receiver.try_recv().is_ok() {} + scene_controller.state = SceneState::Render(n - 1); + } + } +} From 3f2cc244d76eab97a3ef9fccc0d99b0eb3bbc783 Mon Sep 17 00:00:00 2001 From: moonlightaria <72459063+moonlightaria@users.noreply.github.com> Date: Thu, 9 May 2024 14:01:52 -0400 Subject: [PATCH 18/26] Add color conversions #13224 (#13276) # Objective fixes #13224 adds conversions for Vec3 and Vec4 since these appear so often ## Solution added Covert trait (couldn't think of good name) for [f32; 4], [f32, 3], Vec4, and Vec3 along with the symmetric implementation ## Changelog added conversions between arrays and vector to colors and vice versa #migration LinearRgba appears to have already had implicit conversions for [f32;4] and Vec4 --- crates/bevy_color/src/color_ops.rs | 22 +++++++++ crates/bevy_color/src/hsla.rs | 59 ++++++++++++++++++++++- crates/bevy_color/src/hsva.rs | 60 ++++++++++++++++++++++- crates/bevy_color/src/hwba.rs | 59 ++++++++++++++++++++++- crates/bevy_color/src/laba.rs | 59 ++++++++++++++++++++++- crates/bevy_color/src/lcha.rs | 60 ++++++++++++++++++++++- crates/bevy_color/src/linear_rgba.rs | 60 +++++++++++++++++++---- crates/bevy_color/src/oklaba.rs | 60 ++++++++++++++++++++++- crates/bevy_color/src/oklcha.rs | 59 ++++++++++++++++++++++- crates/bevy_color/src/srgba.rs | 72 ++++++++++++++++++++++------ crates/bevy_color/src/xyza.rs | 58 +++++++++++++++++++++- crates/bevy_ecs/src/event.rs | 2 +- crates/bevy_pbr/src/render/fog.rs | 22 +++++---- 13 files changed, 606 insertions(+), 46 deletions(-) diff --git a/crates/bevy_color/src/color_ops.rs b/crates/bevy_color/src/color_ops.rs index 1f39f3254c489..0441d11e9c0e3 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -1,3 +1,5 @@ +use bevy_math::{Vec3, Vec4}; + /// Methods for changing the luminance of a color. Note that these methods are not /// guaranteed to produce consistent results across color spaces, /// but will be within a given space. @@ -97,6 +99,26 @@ pub trait ClampColor: Sized { fn is_within_bounds(&self) -> bool; } +/// Trait with methods for converting colors to non-color types +pub trait ColorToComponents { + /// Convert to an f32 array + fn to_f32_array(self) -> [f32; 4]; + /// Convert to an f32 array without the alpha value + fn to_f32_array_no_alpha(self) -> [f32; 3]; + /// Convert to a Vec4 + fn to_vec4(self) -> Vec4; + /// Convert to a Vec3 + fn to_vec3(self) -> Vec3; + /// Convert from an f32 array + fn from_f32_array(color: [f32; 4]) -> Self; + /// Convert from an f32 array without the alpha value + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self; + /// Convert from a Vec4 + fn from_vec4(color: Vec4) -> Self; + /// Convert from a Vec3 + fn from_vec3(color: Vec3) -> Self; +} + /// Utility function for interpolating hue values. This ensures that the interpolation /// takes the shortest path around the color wheel, and that the result is always between /// 0 and 360. diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index 3c225b369ddab..0d0531750a4d0 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -1,7 +1,8 @@ use crate::{ - Alpha, ClampColor, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, - Xyza, + Alpha, ClampColor, ColorToComponents, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, + StandardColor, Xyza, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Hue-Saturation-Lightness (HSL) color space with alpha. @@ -195,6 +196,60 @@ impl ClampColor for Hsla { } } +impl ColorToComponents for Hsla { + fn to_f32_array(self) -> [f32; 4] { + [self.hue, self.saturation, self.lightness, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.hue, self.saturation, self.lightness] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.hue, self.saturation, self.lightness, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.hue, self.saturation, self.lightness) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + hue: color[0], + saturation: color[1], + lightness: color[2], + alpha: 1.0, + } + } +} + impl From for Hsva { fn from( Hsla { diff --git a/crates/bevy_color/src/hsva.rs b/crates/bevy_color/src/hsva.rs index f58cdefac5659..760cad64192cc 100644 --- a/crates/bevy_color/src/hsva.rs +++ b/crates/bevy_color/src/hsva.rs @@ -1,4 +1,8 @@ -use crate::{Alpha, ClampColor, Hue, Hwba, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza}; +use crate::{ + Alpha, ClampColor, ColorToComponents, Hue, Hwba, Lcha, LinearRgba, Mix, Srgba, StandardColor, + Xyza, +}; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Hue-Saturation-Value (HSV) color space with alpha. @@ -172,6 +176,60 @@ impl From for Hsva { } } +impl ColorToComponents for Hsva { + fn to_f32_array(self) -> [f32; 4] { + [self.hue, self.saturation, self.value, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.hue, self.saturation, self.value] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.hue, self.saturation, self.value, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.hue, self.saturation, self.value) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + hue: color[0], + saturation: color[1], + value: color[2], + alpha: 1.0, + } + } +} + // Derived Conversions impl From for Hsva { diff --git a/crates/bevy_color/src/hwba.rs b/crates/bevy_color/src/hwba.rs index f5e7cf3a93c38..65b6b67e09a40 100644 --- a/crates/bevy_color/src/hwba.rs +++ b/crates/bevy_color/src/hwba.rs @@ -2,7 +2,10 @@ //! in [_HWB - A More Intuitive Hue-Based Color Model_] by _Smith et al_. //! //! [_HWB - A More Intuitive Hue-Based Color Model_]: https://web.archive.org/web/20240226005220/http://alvyray.com/Papers/CG/HWB_JGTv208.pdf -use crate::{Alpha, ClampColor, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza}; +use crate::{ + Alpha, ClampColor, ColorToComponents, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza, +}; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Hue-Whiteness-Blackness (HWB) color space with alpha. @@ -142,6 +145,60 @@ impl ClampColor for Hwba { } } +impl ColorToComponents for Hwba { + fn to_f32_array(self) -> [f32; 4] { + [self.hue, self.whiteness, self.blackness, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.hue, self.whiteness, self.blackness] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.hue, self.whiteness, self.blackness, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.hue, self.whiteness, self.blackness) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + hue: color[0], + whiteness: color[1], + blackness: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + hue: color[0], + whiteness: color[1], + blackness: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + hue: color[0], + whiteness: color[1], + blackness: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + hue: color[0], + whiteness: color[1], + blackness: color[2], + alpha: 1.0, + } + } +} + impl From for Hwba { fn from( Srgba { diff --git a/crates/bevy_color/src/laba.rs b/crates/bevy_color/src/laba.rs index 2ce9b55b72393..cd0684e16f5ce 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -1,7 +1,8 @@ use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, Hsla, Hsva, Hwba, LinearRgba, Luminance, - Mix, Oklaba, Srgba, StandardColor, Xyza, + impl_componentwise_vector_space, Alpha, ClampColor, ColorToComponents, Hsla, Hsva, Hwba, + LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in LAB color space, with alpha @@ -164,6 +165,60 @@ impl Luminance for Laba { } } +impl ColorToComponents for Laba { + fn to_f32_array(self) -> [f32; 4] { + [self.lightness, self.a, self.b, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.lightness, self.a, self.b] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.lightness, self.a, self.b, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.lightness, self.a, self.b) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: 1.0, + } + } +} + impl From for Xyza { fn from( Laba { diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index b33236d6f10ed..437ca5d6d7d47 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -1,4 +1,8 @@ -use crate::{Alpha, ClampColor, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza}; +use crate::{ + Alpha, ClampColor, ColorToComponents, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, + StandardColor, Xyza, +}; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in LCH color space, with alpha @@ -200,6 +204,60 @@ impl ClampColor for Lcha { } } +impl ColorToComponents for Lcha { + fn to_f32_array(self) -> [f32; 4] { + [self.lightness, self.chroma, self.hue, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.lightness, self.chroma, self.hue] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.lightness, self.chroma, self.hue, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.lightness, self.chroma, self.hue) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: 1.0, + } + } +} + impl From for Laba { fn from( Lcha { diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index 3eb0646ff0979..0ef33ce37eedc 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -1,8 +1,8 @@ use crate::{ color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, - Luminance, Mix, StandardColor, + ColorToComponents, Luminance, Mix, StandardColor, }; -use bevy_math::Vec4; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; use bytemuck::{Pod, Zeroable}; @@ -281,15 +281,57 @@ impl ClampColor for LinearRgba { } } -impl From for [f32; 4] { - fn from(color: LinearRgba) -> Self { - [color.red, color.green, color.blue, color.alpha] +impl ColorToComponents for LinearRgba { + fn to_f32_array(self) -> [f32; 4] { + [self.red, self.green, self.blue, self.alpha] } -} -impl From for Vec4 { - fn from(color: LinearRgba) -> Self { - Vec4::new(color.red, color.green, color.blue, color.alpha) + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.red, self.green, self.blue] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.red, self.green, self.blue, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.red, self.green, self.blue) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: 1.0, + } } } diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 858091a719de6..46e23ff08795b 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,7 +1,9 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, Hsla, - Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, + ColorToComponents, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, + Xyza, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Oklab color space, with alpha @@ -173,6 +175,60 @@ impl ClampColor for Oklaba { } } +impl ColorToComponents for Oklaba { + fn to_f32_array(self) -> [f32; 4] { + [self.lightness, self.a, self.b, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.lightness, self.a, self.b] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.lightness, self.a, self.b, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.lightness, self.a, self.b) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + lightness: color[0], + a: color[1], + b: color[2], + alpha: 1.0, + } + } +} + #[allow(clippy::excessive_precision)] impl From for Oklaba { fn from(value: LinearRgba) -> Self { diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 01b21b6780560..3e748a1c945a6 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -1,7 +1,8 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, ClampColor, Hsla, Hsva, Hue, Hwba, Laba, Lcha, - LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, ClampColor, ColorToComponents, Hsla, Hsva, Hue, + Hwba, Laba, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// Color in Oklch color space, with alpha @@ -190,6 +191,60 @@ impl EuclideanDistance for Oklcha { } } +impl ColorToComponents for Oklcha { + fn to_f32_array(self) -> [f32; 4] { + [self.lightness, self.chroma, self.hue, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.lightness, self.chroma, self.hue] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.lightness, self.chroma, self.hue, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.lightness, self.chroma, self.hue) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + lightness: color[0], + chroma: color[1], + hue: color[2], + alpha: 1.0, + } + } +} + impl From for Oklcha { fn from( Oklaba { diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index 43c82fda25d68..def49cfca31d4 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -1,9 +1,9 @@ use crate::color_difference::EuclideanDistance; use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, - Xyza, + impl_componentwise_vector_space, Alpha, ClampColor, ColorToComponents, LinearRgba, Luminance, + Mix, StandardColor, Xyza, }; -use bevy_math::Vec4; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; use thiserror::Error; @@ -332,6 +332,60 @@ impl ClampColor for Srgba { } } +impl ColorToComponents for Srgba { + fn to_f32_array(self) -> [f32; 4] { + [self.red, self.green, self.blue, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.red, self.green, self.blue] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.red, self.green, self.blue, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.red, self.green, self.blue) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + red: color[0], + green: color[1], + blue: color[2], + alpha: 1.0, + } + } +} + impl From for Srgba { #[inline] fn from(value: LinearRgba) -> Self { @@ -356,18 +410,6 @@ impl From for LinearRgba { } } -impl From for [f32; 4] { - fn from(color: Srgba) -> Self { - [color.red, color.green, color.blue, color.alpha] - } -} - -impl From for Vec4 { - fn from(color: Srgba) -> Self { - Vec4::new(color.red, color.green, color.blue, color.alpha) - } -} - // Derived Conversions impl From for Srgba { diff --git a/crates/bevy_color/src/xyza.rs b/crates/bevy_color/src/xyza.rs index d3baf464f472e..6d46c85c92c45 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -1,6 +1,8 @@ use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, LinearRgba, Luminance, Mix, StandardColor, + impl_componentwise_vector_space, Alpha, ClampColor, ColorToComponents, LinearRgba, Luminance, + Mix, StandardColor, }; +use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; /// [CIE 1931](https://en.wikipedia.org/wiki/CIE_1931_color_space) color space, also known as XYZ, with an alpha channel. @@ -160,6 +162,60 @@ impl ClampColor for Xyza { } } +impl ColorToComponents for Xyza { + fn to_f32_array(self) -> [f32; 4] { + [self.x, self.y, self.z, self.alpha] + } + + fn to_f32_array_no_alpha(self) -> [f32; 3] { + [self.x, self.y, self.z] + } + + fn to_vec4(self) -> Vec4 { + Vec4::new(self.x, self.y, self.z, self.alpha) + } + + fn to_vec3(self) -> Vec3 { + Vec3::new(self.x, self.y, self.z) + } + + fn from_f32_array(color: [f32; 4]) -> Self { + Self { + x: color[0], + y: color[1], + z: color[2], + alpha: color[3], + } + } + + fn from_f32_array_no_alpha(color: [f32; 3]) -> Self { + Self { + x: color[0], + y: color[1], + z: color[2], + alpha: 1.0, + } + } + + fn from_vec4(color: Vec4) -> Self { + Self { + x: color[0], + y: color[1], + z: color[2], + alpha: color[3], + } + } + + fn from_vec3(color: Vec3) -> Self { + Self { + x: color[0], + y: color[1], + z: color[2], + alpha: 1.0, + } + } +} + impl From for Xyza { fn from( LinearRgba { diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 30835e62dd32a..5f5b7092f047e 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -1509,7 +1509,7 @@ mod tests { ); } - #[cfg(feature = "multi-threaded")] + #[cfg(feature = "multi_threaded")] #[test] fn test_events_par_iter() { use std::{collections::HashSet, sync::mpsc}; diff --git a/crates/bevy_pbr/src/render/fog.rs b/crates/bevy_pbr/src/render/fog.rs index 16ae9a1007d98..1421ddcd6daab 100644 --- a/crates/bevy_pbr/src/render/fog.rs +++ b/crates/bevy_pbr/src/render/fog.rs @@ -1,6 +1,6 @@ use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; -use bevy_color::LinearRgba; +use bevy_color::{ColorToComponents, LinearRgba}; use bevy_ecs::prelude::*; use bevy_math::{Vec3, Vec4}; use bevy_render::{ @@ -66,24 +66,27 @@ pub fn prepare_fog( match &fog.falloff { FogFalloff::Linear { start, end } => GpuFog { mode: GPU_FOG_MODE_LINEAR, - base_color: LinearRgba::from(fog.color).into(), - directional_light_color: LinearRgba::from(fog.directional_light_color).into(), + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), directional_light_exponent: fog.directional_light_exponent, be: Vec3::new(*start, *end, 0.0), ..Default::default() }, FogFalloff::Exponential { density } => GpuFog { mode: GPU_FOG_MODE_EXPONENTIAL, - base_color: LinearRgba::from(fog.color).into(), - directional_light_color: LinearRgba::from(fog.directional_light_color).into(), + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), directional_light_exponent: fog.directional_light_exponent, be: Vec3::new(*density, 0.0, 0.0), ..Default::default() }, FogFalloff::ExponentialSquared { density } => GpuFog { mode: GPU_FOG_MODE_EXPONENTIAL_SQUARED, - base_color: LinearRgba::from(fog.color).into(), - directional_light_color: LinearRgba::from(fog.directional_light_color).into(), + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), directional_light_exponent: fog.directional_light_exponent, be: Vec3::new(*density, 0.0, 0.0), ..Default::default() @@ -93,8 +96,9 @@ pub fn prepare_fog( inscattering, } => GpuFog { mode: GPU_FOG_MODE_ATMOSPHERIC, - base_color: LinearRgba::from(fog.color).into(), - directional_light_color: LinearRgba::from(fog.directional_light_color).into(), + base_color: LinearRgba::from(fog.color).to_vec4(), + directional_light_color: LinearRgba::from(fog.directional_light_color) + .to_vec4(), directional_light_exponent: fog.directional_light_exponent, be: *extinction, bi: *inscattering, From 42ba9dfaea6c2abe49ccbbc8c5d0cdb9bb9f265e Mon Sep 17 00:00:00 2001 From: Lee-Orr Date: Thu, 9 May 2024 14:06:05 -0400 Subject: [PATCH 19/26] Separate state crate (#13216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Extracts the state mechanisms into a new crate called "bevy_state". This comes with a few goals: - state wasn't really an inherent machinery of the ecs system, and so keeping it within bevy_ecs felt forced - by mixing it in with bevy_ecs, the maintainability of our more robust state system was significantly compromised moving state into a new crate makes it easier to encapsulate as it's own feature, and easier to read and understand since it's no longer a single, massive file. ## Solution move the state-related elements from bevy_ecs to a new crate ## Testing - Did you test these changes? If so, how? all the automated tests migrated and passed, ran the pre-existing examples without changes to validate. --- ## Migration Guide Since bevy_state is now gated behind the `bevy_state` feature, projects that use state but don't use the `default-features` will need to add that feature flag. Since it is no longer part of bevy_ecs, projects that use bevy_ecs directly will need to manually pull in `bevy_state`, trigger the StateTransition schedule, and handle any of the elements that bevy_app currently sets up. --------- Co-authored-by: Kristoffer Søholm --- Cargo.toml | 16 +- crates/bevy_app/Cargo.toml | 4 +- crates/bevy_app/src/app.rs | 42 +- crates/bevy_app/src/main_schedule.rs | 4 +- crates/bevy_app/src/sub_app.rs | 15 +- crates/bevy_ecs/src/lib.rs | 6 +- crates/bevy_ecs/src/schedule/condition.rs | 197 +-- crates/bevy_ecs/src/schedule/mod.rs | 2 - crates/bevy_ecs/src/schedule/state.rs | 1504 ----------------- crates/bevy_input/src/button_input.rs | 8 +- crates/bevy_internal/Cargo.toml | 4 + crates/bevy_internal/src/lib.rs | 2 + crates/bevy_internal/src/prelude.rs | 4 + crates/bevy_state/Cargo.toml | 29 + crates/bevy_state/macros/Cargo.toml | 23 + crates/bevy_state/macros/src/lib.rs | 24 + crates/bevy_state/macros/src/states.rs | 140 ++ crates/bevy_state/src/condition.rs | 204 +++ crates/bevy_state/src/lib.rs | 44 + .../bevy_state/src/state/computed_states.rs | 97 ++ .../src/state/freely_mutable_state.rs | 39 + crates/bevy_state/src/state/mod.rs | 502 ++++++ crates/bevy_state/src/state/resources.rs | 132 ++ crates/bevy_state/src/state/state_set.rs | 287 ++++ crates/bevy_state/src/state/states.rs | 41 + crates/bevy_state/src/state/sub_states.rs | 167 ++ crates/bevy_state/src/state/transitions.rs | 276 +++ docs/cargo_features.md | 1 + examples/README.md | 12 +- examples/{ecs => state}/computed_states.rs | 0 examples/{ecs => state}/state.rs | 0 examples/{ecs => state}/sub_states.rs | 0 tools/publish.sh | 2 + 33 files changed, 2080 insertions(+), 1748 deletions(-) delete mode 100644 crates/bevy_ecs/src/schedule/state.rs create mode 100644 crates/bevy_state/Cargo.toml create mode 100644 crates/bevy_state/macros/Cargo.toml create mode 100644 crates/bevy_state/macros/src/lib.rs create mode 100644 crates/bevy_state/macros/src/states.rs create mode 100644 crates/bevy_state/src/condition.rs create mode 100644 crates/bevy_state/src/lib.rs create mode 100644 crates/bevy_state/src/state/computed_states.rs create mode 100644 crates/bevy_state/src/state/freely_mutable_state.rs create mode 100644 crates/bevy_state/src/state/mod.rs create mode 100644 crates/bevy_state/src/state/resources.rs create mode 100644 crates/bevy_state/src/state/state_set.rs create mode 100644 crates/bevy_state/src/state/states.rs create mode 100644 crates/bevy_state/src/state/sub_states.rs create mode 100644 crates/bevy_state/src/state/transitions.rs rename examples/{ecs => state}/computed_states.rs (100%) rename examples/{ecs => state}/state.rs (100%) rename examples/{ecs => state}/sub_states.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 6166492496727..14a405ce4dd53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ workspace = true default = [ "animation", "bevy_asset", + "bevy_state", "bevy_audio", "bevy_color", "bevy_gilrs", @@ -334,6 +335,9 @@ meshlet_processor = ["bevy_internal/meshlet_processor"] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_internal/ios_simulator"] +# Enable built in global state machines +bevy_state = ["bevy_internal/bevy_state"] + [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.14.0-dev", default-features = false } @@ -1729,35 +1733,35 @@ wasm = false [[example]] name = "state" -path = "examples/ecs/state.rs" +path = "examples/state/state.rs" doc-scrape-examples = true [package.metadata.example.state] name = "State" description = "Illustrates how to use States to control transitioning from a Menu state to an InGame state" -category = "ECS (Entity Component System)" +category = "State" wasm = false [[example]] name = "sub_states" -path = "examples/ecs/sub_states.rs" +path = "examples/state/sub_states.rs" doc-scrape-examples = true [package.metadata.example.sub_states] name = "Sub States" description = "Using Sub States for hierarchical state handling." -category = "ECS (Entity Component System)" +category = "State" wasm = false [[example]] name = "computed_states" -path = "examples/ecs/computed_states.rs" +path = "examples/state/computed_states.rs" doc-scrape-examples = true [package.metadata.example.computed_states] name = "Computed States" description = "Advanced state patterns using Computed States" -category = "ECS (Entity Component System)" +category = "State" wasm = false [[example]] diff --git a/crates/bevy_app/Cargo.toml b/crates/bevy_app/Cargo.toml index 64333bfc9846c..1b4cfdb791cd9 100644 --- a/crates/bevy_app/Cargo.toml +++ b/crates/bevy_app/Cargo.toml @@ -11,9 +11,10 @@ keywords = ["bevy"] [features] trace = [] bevy_debug_stepping = [] -default = ["bevy_reflect"] +default = ["bevy_reflect", "bevy_state"] bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"] serialize = ["bevy_ecs/serde"] +bevy_state = ["dep:bevy_state"] [dependencies] # bevy @@ -22,6 +23,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev", default-features = fa bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true } bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.14.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 80fb05f2b9ac2..176c5caf60a56 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -7,9 +7,11 @@ use bevy_ecs::{ event::{event_update_system, ManualEventReader}, intern::Interned, prelude::*, - schedule::{FreelyMutableState, ScheduleBuildSettings, ScheduleLabel}, + schedule::{ScheduleBuildSettings, ScheduleLabel}, system::SystemId, }; +#[cfg(feature = "bevy_state")] +use bevy_state::{prelude::*, state::FreelyMutableState}; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::{tracing::debug, HashMap}; @@ -264,6 +266,7 @@ impl App { self.sub_apps.iter().any(|s| s.is_building_plugins()) } + #[cfg(feature = "bevy_state")] /// Initializes a [`State`] with standard starting values. /// /// This method is idempotent: it has no effect when called again using the same generic type. @@ -281,6 +284,7 @@ impl App { self } + #[cfg(feature = "bevy_state")] /// Inserts a specific [`State`] to the current [`App`] and overrides any [`State`] previously /// added of the same type. /// @@ -297,23 +301,19 @@ impl App { self } + #[cfg(feature = "bevy_state")] /// Sets up a type implementing [`ComputedStates`]. /// /// This method is idempotent: it has no effect when called again using the same generic type. - /// - /// For each source state the derived state depends on, it adds this state's derivation - /// to it's [`ComputeDependantStates`](bevy_ecs::schedule::ComputeDependantStates) schedule. pub fn add_computed_state(&mut self) -> &mut Self { self.main_mut().add_computed_state::(); self } + #[cfg(feature = "bevy_state")] /// Sets up a type implementing [`SubStates`]. /// /// This method is idempotent: it has no effect when called again using the same generic type. - /// - /// For each source state the derived state depends on, it adds this state's existence check - /// to it's [`ComputeDependantStates`](bevy_ecs::schedule::ComputeDependantStates) schedule. pub fn add_sub_state(&mut self) -> &mut Self { self.main_mut().add_sub_state::(); self @@ -983,10 +983,7 @@ impl Termination for AppExit { mod tests { use std::{marker::PhantomData, mem}; - use bevy_ecs::{ - schedule::{OnEnter, States}, - system::Commands, - }; + use bevy_ecs::{schedule::ScheduleLabel, system::Commands}; use crate::{App, AppExit, Plugin}; @@ -1059,11 +1056,9 @@ mod tests { App::new().add_plugins(PluginRun); } - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum AppState { - #[default] - MainMenu, - } + #[derive(ScheduleLabel, Hash, Clone, PartialEq, Eq, Debug)] + struct EnterMainMenu; + fn bar(mut commands: Commands) { commands.spawn_empty(); } @@ -1075,20 +1070,9 @@ mod tests { #[test] fn add_systems_should_create_schedule_if_it_does_not_exist() { let mut app = App::new(); - app.init_state::() - .add_systems(OnEnter(AppState::MainMenu), (foo, bar)); - - app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); - assert_eq!(app.world().entities().len(), 2); - } - - #[test] - fn add_systems_should_create_schedule_if_it_does_not_exist2() { - let mut app = App::new(); - app.add_systems(OnEnter(AppState::MainMenu), (foo, bar)) - .init_state::(); + app.add_systems(EnterMainMenu, (foo, bar)); - app.world_mut().run_schedule(OnEnter(AppState::MainMenu)); + app.world_mut().run_schedule(EnterMainMenu); assert_eq!(app.world().entities().len(), 2); } diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index 2399cf01c1216..16e5871e41704 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -1,9 +1,11 @@ use crate::{App, Plugin}; use bevy_ecs::{ - schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel, StateTransition}, + schedule::{ExecutorKind, InternedScheduleLabel, Schedule, ScheduleLabel}, system::{Local, Resource}, world::{Mut, World}, }; +#[cfg(feature = "bevy_state")] +use bevy_state::state::StateTransition; /// The schedule that contains the app logic that is evaluated each tick of [`App::update()`]. /// diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 0aa2e60338988..b6974cc60f7e0 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -2,12 +2,15 @@ use crate::{App, InternedAppLabel, Plugin, Plugins, PluginsState, Startup}; use bevy_ecs::{ event::EventRegistry, prelude::*, - schedule::{ - setup_state_transitions_in_world, FreelyMutableState, InternedScheduleLabel, - ScheduleBuildSettings, ScheduleLabel, - }, + schedule::{InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel}, system::SystemId, }; +#[cfg(feature = "bevy_state")] +use bevy_state::{ + prelude::*, + state::{setup_state_transitions_in_world, FreelyMutableState}, +}; + #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::{HashMap, HashSet}; @@ -295,6 +298,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::init_state`]. pub fn init_state(&mut self) -> &mut Self { if !self.world.contains_resource::>() { @@ -309,6 +313,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::insert_state`]. pub fn insert_state(&mut self, state: S) -> &mut Self { if !self.world.contains_resource::>() { @@ -324,6 +329,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::add_computed_state`]. pub fn add_computed_state(&mut self) -> &mut Self { if !self @@ -339,6 +345,7 @@ impl SubApp { self } + #[cfg(feature = "bevy_state")] /// See [`App::add_sub_state`]. pub fn add_sub_state(&mut self) -> &mut Self { if !self diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 89ea9f8fb915a..ca6e42f3c938c 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -49,10 +49,8 @@ pub mod prelude { query::{Added, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, removal_detection::RemovedComponents, schedule::{ - apply_deferred, apply_state_transition, common_conditions::*, ComputedStates, - Condition, IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfigs, NextState, OnEnter, - OnExit, OnTransition, Schedule, Schedules, State, StateSet, StateTransition, - StateTransitionEvent, States, SubStates, SystemSet, + apply_deferred, common_conditions::*, Condition, IntoSystemConfigs, IntoSystemSet, + IntoSystemSetConfigs, Schedule, Schedules, SystemSet, }, system::{ Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 4997bfd1103da..7d2f1ab6173ea 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -194,15 +194,12 @@ mod sealed { /// A collection of [run conditions](Condition) that may be useful in any bevy app. pub mod common_conditions { - use bevy_utils::warn_once; - use super::NotSystem; use crate::{ change_detection::DetectChanges, event::{Event, EventReader}, prelude::{Component, Query, With}, removal_detection::RemovedComponents, - schedule::{State, States}, system::{IntoSystem, Res, Resource, System}, }; @@ -657,173 +654,6 @@ pub mod common_conditions { } } - /// A [`Condition`](super::Condition)-satisfying system that returns `true` - /// if the state machine exists. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// app.add_systems( - /// // `state_exists` will only return true if the - /// // given state exists - /// my_system.run_if(state_exists::), - /// ); - /// - /// fn my_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// // `GameState` does not yet exist `my_system` won't run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 0); - /// - /// world.init_resource::>(); - /// - /// // `GameState` now exists so `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// ``` - pub fn state_exists(current_state: Option>>) -> bool { - current_state.is_some() - } - - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` - /// if the state machine is currently in `state`. - /// - /// Will return `false` if the state does not exist or if not in `state`. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// world.init_resource::>(); - /// - /// app.add_systems(( - /// // `in_state` will only return true if the - /// // given state equals the given value - /// play_system.run_if(in_state(GameState::Playing)), - /// pause_system.run_if(in_state(GameState::Paused)), - /// )); - /// - /// fn play_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// fn pause_system(mut counter: ResMut) { - /// counter.0 -= 1; - /// } - /// - /// // We default to `GameState::Playing` so `play_system` runs - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// *world.resource_mut::>() = State::new(GameState::Paused); - /// - /// // Now that we are in `GameState::Pause`, `pause_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 0); - /// ``` - pub fn in_state(state: S) -> impl FnMut(Option>>) -> bool + Clone { - move |current_state: Option>>| match current_state { - Some(current_state) => *current_state == state, - None => { - warn_once!("No state matching the type for {} exists - did you forget to `init_state` when initializing the app?", { - let debug_state = format!("{state:?}"); - let result = debug_state - .split("::") - .next() - .unwrap_or("Unknown State Type"); - result.to_string() - }); - - false - } - } - } - - /// A [`Condition`](super::Condition)-satisfying system that returns `true` - /// if the state machine changed state. - /// - /// To do things on transitions to/from specific states, use their respective OnEnter/OnExit - /// schedules. Use this run condition if you want to detect any change, regardless of the value. - /// - /// Returns false if the state does not exist or the state has not changed. - /// - /// # Example - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # #[derive(Resource, Default)] - /// # struct Counter(u8); - /// # let mut app = Schedule::default(); - /// # let mut world = World::new(); - /// # world.init_resource::(); - /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] - /// enum GameState { - /// #[default] - /// Playing, - /// Paused, - /// } - /// - /// world.init_resource::>(); - /// - /// app.add_systems( - /// // `state_changed` will only return true if the - /// // given states value has just been updated or - /// // the state has just been added - /// my_system.run_if(state_changed::), - /// ); - /// - /// fn my_system(mut counter: ResMut) { - /// counter.0 += 1; - /// } - /// - /// // `GameState` has just been added so `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// // `GameState` has not been updated so `my_system` will not run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 1); - /// - /// *world.resource_mut::>() = State::new(GameState::Paused); - /// - /// // Now that `GameState` has been updated `my_system` will run - /// app.run(&mut world); - /// assert_eq!(world.resource::().0, 2); - /// ``` - pub fn state_changed(current_state: Option>>) -> bool { - let Some(current_state) = current_state else { - return false; - }; - current_state.is_changed() - } - /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if there are any new events of the given type since it was last called. /// @@ -1032,7 +862,6 @@ mod tests { use crate as bevy_ecs; use crate::component::Component; use crate::schedule::IntoSystemConfigs; - use crate::schedule::{State, States}; use crate::system::Local; use crate::{change_detection::ResMut, schedule::Schedule, world::World}; use bevy_ecs_macros::Event; @@ -1131,20 +960,15 @@ mod tests { schedule.run(&mut world); assert_eq!(world.resource::().0, 0); } - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum TestState { - #[default] - A, - B, - } - #[derive(Component)] struct TestComponent; #[derive(Event)] struct TestEvent; + #[derive(Resource)] + struct TestResource(()); + fn test_system() {} // Ensure distributive_run_if compiles with the common conditions. @@ -1153,15 +977,12 @@ mod tests { Schedule::default().add_systems( (test_system, test_system) .distributive_run_if(run_once()) - .distributive_run_if(resource_exists::>) - .distributive_run_if(resource_added::>) - .distributive_run_if(resource_changed::>) - .distributive_run_if(resource_exists_and_changed::>) - .distributive_run_if(resource_changed_or_removed::>()) - .distributive_run_if(resource_removed::>()) - .distributive_run_if(state_exists::) - .distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B))) - .distributive_run_if(state_changed::) + .distributive_run_if(resource_exists::) + .distributive_run_if(resource_added::) + .distributive_run_if(resource_changed::) + .distributive_run_if(resource_exists_and_changed::) + .distributive_run_if(resource_changed_or_removed::()) + .distributive_run_if(resource_removed::()) .distributive_run_if(on_event::()) .distributive_run_if(any_with_component::) .distributive_run_if(not(run_once())), diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index b38f7adb67923..184bef250a6b5 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -7,7 +7,6 @@ mod graph_utils; #[allow(clippy::module_inception)] mod schedule; mod set; -mod state; mod stepping; pub use self::condition::*; @@ -16,7 +15,6 @@ pub use self::executor::*; use self::graph_utils::*; pub use self::schedule::*; pub use self::set::*; -pub use self::state::*; pub use self::graph_utils::NodeId; diff --git a/crates/bevy_ecs/src/schedule/state.rs b/crates/bevy_ecs/src/schedule/state.rs deleted file mode 100644 index 0cec393d6fd9a..0000000000000 --- a/crates/bevy_ecs/src/schedule/state.rs +++ /dev/null @@ -1,1504 +0,0 @@ -//! In Bevy, states are app-wide interdependent, finite state machines that are generally used to model the large scale structure of your program: whether a game is paused, if the player is in combat, if assets are loaded and so on. -//! -//! This module provides 3 distinct types of state, all of which implement the [`States`] trait: -//! -//! - Standard [`States`] can only be changed by manually setting the [`NextState`] resource. -//! These states are the baseline on which the other state types are built, and can be used on -//! their own for many simple patterns. See the [state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/state.rs) -//! for a simple use case. -//! - [`SubStates`] are children of other states - they can be changed manually using [`NextState`], -//! but are removed from the [`World`] if the source states aren't in the right state. See the [sub_states example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/sub_states.rs) -//! for a simple use case based on the derive macro, or read the trait docs for more complex scenarios. -//! - [`ComputedStates`] are fully derived from other states - they provide a [`compute`](ComputedStates::compute) method -//! that takes in the source states and returns their derived value. They are particularly useful for situations -//! where a simplified view of the source states is necessary - such as having an `InAMenu` computed state, derived -//! from a source state that defines multiple distinct menus. See the [computed state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/computed_states.rs) -//! to see usage samples for these states. -//! -//! Most of the utilities around state involve running systems during transitions between states, or -//! determining whether to run certain systems, though they can be used more directly as well. This -//! makes it easier to transition between menus, add loading screens, pause games, and the more. -//! -//! Specifically, Bevy provides the following utilities: -//! -//! - 3 Transition Schedules - [`OnEnter`], [`OnExit`] and [`OnTransition`] - which are used -//! to trigger systems specifically during matching transitions. -//! - A [`StateTransitionEvent`] that gets fired when a given state changes. -//! - The [`in_state`](crate::schedule::condition::in_state) and [`state_changed`](crate::schedule::condition:state_changed) run conditions - which are used -//! to determine whether a system should run based on the current state. - -use std::fmt::Debug; -use std::hash::Hash; -use std::marker::PhantomData; -use std::mem; -use std::ops::Deref; - -use crate as bevy_ecs; -use crate::event::{Event, EventReader, EventWriter}; -use crate::prelude::{FromWorld, Local, Res, ResMut}; -#[cfg(feature = "bevy_reflect")] -use crate::reflect::ReflectResource; -use crate::schedule::ScheduleLabel; -use crate::system::{Commands, In, IntoSystem, Resource}; -use crate::world::World; - -use bevy_ecs_macros::SystemSet; -pub use bevy_ecs_macros::{States, SubStates}; -use bevy_utils::all_tuples; - -use self::sealed::StateSetSealed; - -use super::{InternedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs, Schedule, Schedules}; - -/// Types that can define world-wide states in a finite-state machine. -/// -/// The [`Default`] trait defines the starting state. -/// Multiple states can be defined for the same world, -/// allowing you to classify the state of the world across orthogonal dimensions. -/// You can access the current state of type `T` with the [`State`] resource, -/// and the queued state with the [`NextState`] resource. -/// -/// State transitions typically occur in the [`OnEnter`] and [`OnExit`] schedules, -/// which can be run by triggering the [`StateTransition`] schedule. -/// -/// Types used as [`ComputedStates`] do not need to and should not derive [`States`]. -/// [`ComputedStates`] should not be manually mutated: functionality provided -/// by the [`States`] derive and the associated [`FreelyMutableState`] trait. -/// -/// # Example -/// -/// ``` -/// use bevy_ecs::prelude::States; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// ``` -pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { - /// How many other states this state depends on. - /// Used to help order transitions and de-duplicate [`ComputedStates`], as well as prevent cyclical - /// `ComputedState` dependencies. - const DEPENDENCY_DEPTH: usize = 1; -} - -/// This trait allows a state to be mutated directly using the [`NextState`] resource. -/// -/// While ordinary states are freely mutable (and implement this trait as part of their derive macro), -/// computed states are not: instead, they can *only* change when the states that drive them do. -pub trait FreelyMutableState: States { - /// This function registers all the necessary systems to apply state changes and run transition schedules - fn register_state(schedule: &mut Schedule) { - schedule - .add_systems( - apply_state_transition::.in_set(ApplyStateTransition::::apply()), - ) - .add_systems( - should_run_transition::> - .pipe(run_enter::) - .in_set(StateTransitionSteps::EnterSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_exit::) - .in_set(StateTransitionSteps::ExitSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_transition::) - .in_set(StateTransitionSteps::TransitionSchedules), - ) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::ManualTransitions), - ); - } -} - -/// The label of a [`Schedule`] that runs whenever [`State`] -/// enters this state. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct OnEnter(pub S); - -/// The label of a [`Schedule`] that runs whenever [`State`] -/// exits this state. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct OnExit(pub S); - -/// The label of a [`Schedule`] that **only** runs whenever [`State`] -/// exits the `from` state, AND enters the `to` state. -/// -/// Systems added to this schedule are always ran *after* [`OnExit`], and *before* [`OnEnter`]. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct OnTransition { - /// The state being exited. - pub from: S, - /// The state being entered. - pub to: S, -} - -/// A finite-state machine whose transitions have associated schedules -/// ([`OnEnter(state)`] and [`OnExit(state)`]). -/// -/// The current state value can be accessed through this resource. To *change* the state, -/// queue a transition in the [`NextState`] resource, and it will be applied by the next -/// [`apply_state_transition::`] system. -/// -/// The starting state is defined via the [`Default`] implementation for `S`. -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// fn game_logic(game_state: Res>) { -/// match game_state.get() { -/// GameState::InGame => { -/// // Run game logic here... -/// }, -/// _ => {}, -/// } -/// } -/// ``` -#[derive(Resource, Debug)] -#[cfg_attr( - feature = "bevy_reflect", - derive(bevy_reflect::Reflect), - reflect(Resource) -)] -pub struct State(S); - -impl State { - /// Creates a new state with a specific value. - /// - /// To change the state use [`NextState`] rather than using this to modify the `State`. - pub fn new(state: S) -> Self { - Self(state) - } - - /// Get the current state. - pub fn get(&self) -> &S { - &self.0 - } -} - -impl FromWorld for State { - fn from_world(world: &mut World) -> Self { - Self(S::from_world(world)) - } -} - -impl PartialEq for State { - fn eq(&self, other: &S) -> bool { - self.get() == other - } -} - -impl Deref for State { - type Target = S; - - fn deref(&self) -> &Self::Target { - self.get() - } -} - -/// The next state of [`State`]. -/// -/// To queue a transition, just set the contained value to `Some(next_state)`. -/// Note that these transitions can be overridden by other systems: -/// only the actual value of this resource at the time of [`apply_state_transition`] matters. -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// -/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] -/// enum GameState { -/// #[default] -/// MainMenu, -/// SettingsMenu, -/// InGame, -/// } -/// -/// fn start_game(mut next_game_state: ResMut>) { -/// next_game_state.set(GameState::InGame); -/// } -/// ``` -#[derive(Resource, Debug, Default)] -#[cfg_attr( - feature = "bevy_reflect", - derive(bevy_reflect::Reflect), - reflect(Resource) -)] -pub enum NextState { - /// No state transition is pending - #[default] - Unchanged, - /// There is a pending transition for state `S` - Pending(S), -} - -impl NextState { - /// Tentatively set a pending state transition to `Some(state)`. - pub fn set(&mut self, state: S) { - *self = Self::Pending(state); - } - - /// Remove any pending changes to [`State`] - pub fn reset(&mut self) { - *self = Self::Unchanged; - } -} - -/// Event sent when any state transition of `S` happens. -/// -/// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] -pub struct StateTransitionEvent { - /// the state we were in before - pub before: Option, - /// the state we're in now - pub after: Option, -} - -/// Runs [state transitions](States). -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub struct StateTransition; - -/// Applies manual state transitions using [`NextState`]. -/// -/// These system sets are run sequentially, in the order of the enum variants. -#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] -enum StateTransitionSteps { - ManualTransitions, - DependentTransitions, - ExitSchedules, - TransitionSchedules, - EnterSchedules, -} - -/// Defines a system set to aid with dependent state ordering -#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] -pub struct ApplyStateTransition(PhantomData); - -impl ApplyStateTransition { - fn apply() -> Self { - Self(PhantomData) - } -} - -/// This function actually applies a state change, and registers the required -/// schedules for downstream computed states and transition schedules. -/// -/// The `new_state` is an option to allow for removal - `None` will trigger the -/// removal of the `State` resource from the [`World`]. -fn internal_apply_state_transition( - mut event: EventWriter>, - mut commands: Commands, - current_state: Option>>, - new_state: Option, -) { - match new_state { - Some(entered) => { - match current_state { - // If the [`State`] resource exists, and the state is not the one we are - // entering - we need to set the new value, compute dependant states, send transition events - // and register transition schedules. - Some(mut state_resource) => { - if *state_resource != entered { - let exited = mem::replace(&mut state_resource.0, entered.clone()); - - event.send(StateTransitionEvent { - before: Some(exited.clone()), - after: Some(entered.clone()), - }); - } - } - None => { - // If the [`State`] resource does not exist, we create it, compute dependant states, send a transition event and register the `OnEnter` schedule. - commands.insert_resource(State(entered.clone())); - - event.send(StateTransitionEvent { - before: None, - after: Some(entered.clone()), - }); - } - }; - } - None => { - // We first remove the [`State`] resource, and if one existed we compute dependant states, send a transition event and run the `OnExit` schedule. - if let Some(resource) = current_state { - commands.remove_resource::>(); - - event.send(StateTransitionEvent { - before: Some(resource.get().clone()), - after: None, - }); - } - } - } -} - -/// Sets up the schedules and systems for handling state transitions -/// within a [`World`]. -/// -/// Runs automatically when using `App` to insert states, but needs to -/// be added manually in other situations. -pub fn setup_state_transitions_in_world( - world: &mut World, - startup_label: Option, -) { - let mut schedules = world.get_resource_or_insert_with(Schedules::default); - if schedules.contains(StateTransition) { - return; - } - let mut schedule = Schedule::new(StateTransition); - schedule.configure_sets( - ( - StateTransitionSteps::ManualTransitions, - StateTransitionSteps::DependentTransitions, - StateTransitionSteps::ExitSchedules, - StateTransitionSteps::TransitionSchedules, - StateTransitionSteps::EnterSchedules, - ) - .chain(), - ); - schedules.insert(schedule); - - if let Some(startup) = startup_label { - schedules.add_systems(startup, |world: &mut World| { - let _ = world.try_run_schedule(StateTransition); - }); - } -} - -/// If a new state is queued in [`NextState`], this system: -/// - Takes the new state value from [`NextState`] and updates [`State`]. -/// - Sends a relevant [`StateTransitionEvent`] -/// - Runs the [`OnExit(exited_state)`] schedule, if it exists. -/// - Runs the [`OnTransition { from: exited_state, to: entered_state }`](OnTransition), if it exists. -/// - Derive any dependent states through the [`ComputeDependantStates::`] schedule, if it exists. -/// - Runs the [`OnEnter(entered_state)`] schedule, if it exists. -/// -/// If the [`State`] resource does not exist, it does nothing. Removing or adding states -/// should be done at App creation or at your own risk. -/// -/// For [`SubStates`] - it only applies the state if the `SubState` currently exists. Otherwise, it is wiped. -/// When a `SubState` is re-created, it will use the result of it's `should_exist` method. -pub fn apply_state_transition( - event: EventWriter>, - commands: Commands, - current_state: Option>>, - next_state: Option>>, -) { - // We want to check if the State and NextState resources exist - let Some(next_state_resource) = next_state else { - return; - }; - - match next_state_resource.as_ref() { - NextState::Pending(new_state) => { - if let Some(current_state) = current_state { - if new_state != current_state.get() { - let new_state = new_state.clone(); - internal_apply_state_transition( - event, - commands, - Some(current_state), - Some(new_state), - ); - } - } - } - NextState::Unchanged => { - // This is the default value, so we don't need to re-insert the resource - return; - } - } - - *next_state_resource.value = NextState::::Unchanged; -} - -fn should_run_transition( - first: Local, - res: Option>>, - mut event: EventReader>, -) -> (Option>, PhantomData) { - if !*first.0 { - *first.0 = true; - if let Some(res) = res { - event.clear(); - - return ( - Some(StateTransitionEvent { - before: None, - after: Some(res.get().clone()), - }), - PhantomData, - ); - } - } - (event.read().last().cloned(), PhantomData) -} - -fn run_enter( - In((transition, _)): In<(Option>, PhantomData>)>, - world: &mut World, -) { - let Some(transition) = transition else { - return; - }; - - let Some(after) = transition.after else { - return; - }; - - let _ = world.try_run_schedule(OnEnter(after)); -} - -fn run_exit( - In((transition, _)): In<(Option>, PhantomData>)>, - world: &mut World, -) { - let Some(transition) = transition else { - return; - }; - - let Some(before) = transition.before else { - return; - }; - - let _ = world.try_run_schedule(OnExit(before)); -} - -fn run_transition( - In((transition, _)): In<( - Option>, - PhantomData>, - )>, - world: &mut World, -) { - let Some(transition) = transition else { - return; - }; - let Some(from) = transition.before else { - return; - }; - let Some(to) = transition.after else { - return; - }; - - let _ = world.try_run_schedule(OnTransition { from, to }); -} - -/// A state whose value is automatically computed based on the values of other [`States`]. -/// -/// A **computed state** is a state that is deterministically derived from a set of `SourceStates`. -/// The [`StateSet`] is passed into the `compute` method whenever one of them changes, and the -/// result becomes the state's value. -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// /// Computed States require some state to derive from -/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// enum AppState { -/// #[default] -/// Menu, -/// InGame { paused: bool } -/// } -/// -/// -/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] -/// struct InGame; -/// -/// impl ComputedStates for InGame { -/// /// We set the source state to be the state, or a tuple of states, -/// /// we want to depend on. You can also wrap each state in an Option, -/// /// if you want the computed state to execute even if the state doesn't -/// /// currently exist in the world. -/// type SourceStates = AppState; -/// -/// /// We then define the compute function, which takes in -/// /// your SourceStates -/// fn compute(sources: AppState) -> Option { -/// match sources { -/// /// When we are in game, we want to return the InGame state -/// AppState::InGame { .. } => Some(InGame), -/// /// Otherwise, we don't want the `State` resource to exist, -/// /// so we return None. -/// _ => None -/// } -/// } -/// } -/// ``` -/// -/// you can then add it to an App, and from there you use the state as normal -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// # struct App; -/// # impl App { -/// # fn new() -> Self { App } -/// # fn init_state(&mut self) -> &mut Self {self} -/// # fn add_computed_state(&mut self) -> &mut Self {self} -/// # } -/// # struct AppState; -/// # struct InGame; -/// -/// App::new() -/// .init_state::() -/// .add_computed_state::(); -/// ``` -pub trait ComputedStates: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { - /// The set of states from which the [`Self`] is derived. - /// - /// This can either be a single type that implements [`States`], an Option of a type - /// that implements [`States`], or a tuple - /// containing multiple types that implement [`States`] or Optional versions of them. - /// - /// For example, `(MapState, EnemyState)` is valid, as is `(MapState, Option)` - type SourceStates: StateSet; - - /// Computes the next value of [`State`]. - /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. - /// - /// If the result is [`None`], the [`State`] resource will be removed from the world. - fn compute(sources: Self::SourceStates) -> Option; - - /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) - /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not - /// used. - fn register_computed_state_systems(schedule: &mut Schedule) { - Self::SourceStates::register_computed_state_systems_in_schedule::(schedule); - } -} - -impl States for S { - const DEPENDENCY_DEPTH: usize = S::SourceStates::SET_DEPENDENCY_DEPTH + 1; -} - -mod sealed { - /// Sealed trait used to prevent external implementations of [`StateSet`](super::StateSet). - pub trait StateSetSealed {} -} - -/// A [`States`] type or tuple of types which implement [`States`]. -/// -/// This trait is used allow implementors of [`States`], as well -/// as tuples containing exclusively implementors of [`States`], to -/// be used as [`ComputedStates::SourceStates`]. -/// -/// It is sealed, and auto implemented for all [`States`] types and -/// tuples containing them. -pub trait StateSet: sealed::StateSetSealed { - /// The total [`DEPENDENCY_DEPTH`](`States::DEPENDENCY_DEPTH`) of all - /// the states that are part of this [`StateSet`], added together. - /// - /// Used to de-duplicate computed state executions and prevent cyclic - /// computed states. - const SET_DEPENDENCY_DEPTH: usize; - - /// Sets up the systems needed to compute `T` whenever any `State` in this - /// `StateSet` is changed. - fn register_computed_state_systems_in_schedule>( - schedule: &mut Schedule, - ); - - /// Sets up the systems needed to compute whether `T` exists whenever any `State` in this - /// `StateSet` is changed. - fn register_sub_state_systems_in_schedule>( - schedule: &mut Schedule, - ); -} - -/// The `InnerStateSet` trait is used to isolate [`ComputedStates`] & [`SubStates`] from -/// needing to wrap all state dependencies in an [`Option`]. -/// -/// Some [`ComputedStates`]'s might need to exist in different states based on the existence -/// of other states. So we needed the ability to use[`Option`] when appropriate. -/// -/// The isolation works because it is implemented for both S & [`Option`], and has the `RawState` associated type -/// that allows it to know what the resource in the world should be. We can then essentially "unwrap" it in our -/// `StateSet` implementation - and the behaviour of that unwrapping will depend on the arguments expected by the -/// the [`ComputedStates`] & [`SubStates]`. -trait InnerStateSet: Sized { - type RawState: States; - - const DEPENDENCY_DEPTH: usize; - - fn convert_to_usable_state(wrapped: Option<&State>) -> Option; -} - -impl InnerStateSet for S { - type RawState = Self; - - const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; - - fn convert_to_usable_state(wrapped: Option<&State>) -> Option { - wrapped.map(|v| v.0.clone()) - } -} - -impl InnerStateSet for Option { - type RawState = S; - - const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; - - fn convert_to_usable_state(wrapped: Option<&State>) -> Option { - Some(wrapped.map(|v| v.0.clone())) - } -} - -impl StateSetSealed for S {} - -impl StateSet for S { - const SET_DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; - - fn register_computed_state_systems_in_schedule>( - schedule: &mut Schedule, - ) { - let system = |mut parent_changed: EventReader>, - event: EventWriter>, - commands: Commands, - current_state: Option>>, - state_set: Option>>| { - if parent_changed.is_empty() { - return; - } - parent_changed.clear(); - - let new_state = - if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { - T::compute(state_set) - } else { - None - }; - - internal_apply_state_transition(event, commands, current_state, new_state); - }; - - schedule - .add_systems(system.in_set(ApplyStateTransition::::apply())) - .add_systems( - should_run_transition::> - .pipe(run_enter::) - .in_set(StateTransitionSteps::EnterSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_exit::) - .in_set(StateTransitionSteps::ExitSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_transition::) - .in_set(StateTransitionSteps::TransitionSchedules), - ) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::DependentTransitions) - .after(ApplyStateTransition::::apply()), - ); - } - - fn register_sub_state_systems_in_schedule>( - schedule: &mut Schedule, - ) { - let system = |mut parent_changed: EventReader>, - event: EventWriter>, - commands: Commands, - current_state: Option>>, - state_set: Option>>| { - if parent_changed.is_empty() { - return; - } - parent_changed.clear(); - - let new_state = - if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { - T::should_exist(state_set) - } else { - None - }; - - match new_state { - Some(value) => { - if current_state.is_none() { - internal_apply_state_transition( - event, - commands, - current_state, - Some(value), - ); - } - } - None => { - internal_apply_state_transition(event, commands, current_state, None); - } - }; - }; - - schedule - .add_systems(system.in_set(ApplyStateTransition::::apply())) - .add_systems( - apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions), - ) - .add_systems( - should_run_transition::> - .pipe(run_enter::) - .in_set(StateTransitionSteps::EnterSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_exit::) - .in_set(StateTransitionSteps::ExitSchedules), - ) - .add_systems( - should_run_transition::> - .pipe(run_transition::) - .in_set(StateTransitionSteps::TransitionSchedules), - ) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::DependentTransitions) - .after(ApplyStateTransition::::apply()), - ); - } -} - -/// A sub-state is a state that exists only when the source state meet certain conditions, -/// but unlike [`ComputedStates`] - while they exist they can be manually modified. -/// -/// The default approach to creating [`SubStates`] is using the derive macro, and defining a single source state -/// and value to determine it's existence. -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// enum AppState { -/// #[default] -/// Menu, -/// InGame -/// } -/// -/// -/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// #[source(AppState = AppState::InGame)] -/// enum GamePhase { -/// #[default] -/// Setup, -/// Battle, -/// Conclusion -/// } -/// ``` -/// -/// you can then add it to an App, and from there you use the state as normal: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// # struct App; -/// # impl App { -/// # fn new() -> Self { App } -/// # fn init_state(&mut self) -> &mut Self {self} -/// # fn add_sub_state(&mut self) -> &mut Self {self} -/// # } -/// # struct AppState; -/// # struct GamePhase; -/// -/// App::new() -/// .init_state::() -/// .add_sub_state::(); -/// ``` -/// -/// In more complex situations, the recommendation is to use an intermediary computed state, like so: -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// -/// /// Computed States require some state to derive from -/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// enum AppState { -/// #[default] -/// Menu, -/// InGame { paused: bool } -/// } -/// -/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] -/// struct InGame; -/// -/// impl ComputedStates for InGame { -/// /// We set the source state to be the state, or set of states, -/// /// we want to depend on. Any of the states can be wrapped in an Option. -/// type SourceStates = Option; -/// -/// /// We then define the compute function, which takes in the AppState -/// fn compute(sources: Option) -> Option { -/// match sources { -/// /// When we are in game, we want to return the InGame state -/// Some(AppState::InGame { .. }) => Some(InGame), -/// /// Otherwise, we don't want the `State` resource to exist, -/// /// so we return None. -/// _ => None -/// } -/// } -/// } -/// -/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// #[source(InGame = InGame)] -/// enum GamePhase { -/// #[default] -/// Setup, -/// Battle, -/// Conclusion -/// } -/// ``` -/// -/// However, you can also manually implement them. If you do so, you'll also need to manually implement the `States` & `FreelyMutableState` traits. -/// Unlike the derive, this does not require an implementation of [`Default`], since you are providing the `exists` function -/// directly. -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # use bevy_ecs::schedule::FreelyMutableState; -/// -/// /// Computed States require some state to derive from -/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] -/// enum AppState { -/// #[default] -/// Menu, -/// InGame { paused: bool } -/// } -/// -/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] -/// enum GamePhase { -/// Setup, -/// Battle, -/// Conclusion -/// } -/// -/// impl SubStates for GamePhase { -/// /// We set the source state to be the state, or set of states, -/// /// we want to depend on. Any of the states can be wrapped in an Option. -/// type SourceStates = Option; -/// -/// /// We then define the compute function, which takes in the [`Self::SourceStates`] -/// fn should_exist(sources: Option) -> Option { -/// match sources { -/// /// When we are in game, so we want a GamePhase state to exist, and the default is -/// /// GamePhase::Setup -/// Some(AppState::InGame { .. }) => Some(GamePhase::Setup), -/// /// Otherwise, we don't want the `State` resource to exist, -/// /// so we return None. -/// _ => None -/// } -/// } -/// } -/// -/// impl States for GamePhase { -/// const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; -/// } -/// -/// impl FreelyMutableState for GamePhase {} -/// ``` -pub trait SubStates: States + FreelyMutableState { - /// The set of states from which the [`Self`] is derived. - /// - /// This can either be a single type that implements [`States`], or a tuple - /// containing multiple types that implement [`States`], or any combination of - /// types implementing [`States`] and Options of types implementing [`States`] - type SourceStates: StateSet; - - /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. - /// The result is used to determine the existence of [`State`]. - /// - /// If the result is [`None`], the [`State`] resource will be removed from the world, otherwise - /// if the [`State`] resource doesn't exist - it will be created with the [`Some`] value. - fn should_exist(sources: Self::SourceStates) -> Option; - - /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) - /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not - /// used. - fn register_sub_state_systems(schedule: &mut Schedule) { - Self::SourceStates::register_sub_state_systems_in_schedule::(schedule); - } -} - -macro_rules! impl_state_set_sealed_tuples { - ($(($param: ident, $val: ident, $evt: ident)), *) => { - impl<$($param: InnerStateSet),*> StateSetSealed for ($($param,)*) {} - - impl<$($param: InnerStateSet),*> StateSet for ($($param,)*) { - - const SET_DEPENDENCY_DEPTH : usize = $($param::DEPENDENCY_DEPTH +)* 0; - - - fn register_computed_state_systems_in_schedule>( - schedule: &mut Schedule, - ) { - let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { - if ($($evt.is_empty())&&*) { - return; - } - $($evt.clear();)* - - let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { - T::compute(($($val),*, )) - } else { - None - }; - - internal_apply_state_transition(event, commands, current_state, new_state); - }; - - schedule - .add_systems(system.in_set(ApplyStateTransition::::apply())) - .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) - .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) - .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::DependentTransitions) - $(.after(ApplyStateTransition::<$param::RawState>::apply()))* - ); - } - - fn register_sub_state_systems_in_schedule>( - schedule: &mut Schedule, - ) { - let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { - if ($($evt.is_empty())&&*) { - return; - } - $($evt.clear();)* - - let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { - T::should_exist(($($val),*, )) - } else { - None - }; - match new_state { - Some(value) => { - if current_state.is_none() { - internal_apply_state_transition(event, commands, current_state, Some(value)); - } - } - None => { - internal_apply_state_transition(event, commands, current_state, None); - }, - }; - }; - - schedule - .add_systems(system.in_set(ApplyStateTransition::::apply())) - .add_systems(apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions)) - .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) - .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) - .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) - .configure_sets( - ApplyStateTransition::::apply() - .in_set(StateTransitionSteps::DependentTransitions) - $(.after(ApplyStateTransition::<$param::RawState>::apply()))* - ); - } - } - }; -} - -all_tuples!(impl_state_set_sealed_tuples, 1, 15, S, s, ereader); - -#[cfg(test)] -mod tests { - use bevy_ecs_macros::SubStates; - - use super::*; - use crate as bevy_ecs; - - use crate::event::EventRegistry; - - use crate::prelude::ResMut; - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum SimpleState { - #[default] - A, - B(bool), - } - - #[derive(PartialEq, Eq, Debug, Hash, Clone)] - enum TestComputedState { - BisTrue, - BisFalse, - } - - impl ComputedStates for TestComputedState { - type SourceStates = Option; - - fn compute(sources: Option) -> Option { - sources.and_then(|source| match source { - SimpleState::A => None, - SimpleState::B(value) => Some(if value { Self::BisTrue } else { Self::BisFalse }), - }) - } - } - - #[test] - fn computed_state_with_a_single_source_is_correctly_derived() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - let mut schedules = Schedules::new(); - let mut apply_changes = Schedule::new(StateTransition); - TestComputedState::register_computed_state_systems(&mut apply_changes); - SimpleState::register_state(&mut apply_changes); - schedules.insert(apply_changes); - - world.insert_resource(schedules); - - setup_state_transitions_in_world(&mut world, None); - - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!( - world.resource::>().0, - TestComputedState::BisTrue - ); - - world.insert_resource(NextState::Pending(SimpleState::B(false))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(false) - ); - assert_eq!( - world.resource::>().0, - TestComputedState::BisFalse - ); - - world.insert_resource(NextState::Pending(SimpleState::A)); - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - } - - #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] - #[source(SimpleState = SimpleState::B(true))] - enum SubState { - #[default] - One, - Two, - } - - #[test] - fn sub_state_exists_only_when_allowed_but_can_be_modified_freely() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - let mut schedules = Schedules::new(); - let mut apply_changes = Schedule::new(StateTransition); - SubState::register_sub_state_systems(&mut apply_changes); - SimpleState::register_state(&mut apply_changes); - schedules.insert(apply_changes); - - world.insert_resource(schedules); - - setup_state_transitions_in_world(&mut world, None); - - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SubState::Two)); - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!(world.resource::>().0, SubState::One); - - world.insert_resource(NextState::Pending(SubState::Two)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!(world.resource::>().0, SubState::Two); - - world.insert_resource(NextState::Pending(SimpleState::B(false))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(false) - ); - assert!(!world.contains_resource::>()); - } - - #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] - #[source(TestComputedState = TestComputedState::BisTrue)] - enum SubStateOfComputed { - #[default] - One, - Two, - } - - #[test] - fn substate_of_computed_states_works_appropriately() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - let mut schedules = Schedules::new(); - let mut apply_changes = Schedule::new(StateTransition); - TestComputedState::register_computed_state_systems(&mut apply_changes); - SubStateOfComputed::register_sub_state_systems(&mut apply_changes); - SimpleState::register_state(&mut apply_changes); - schedules.insert(apply_changes); - - world.insert_resource(schedules); - - setup_state_transitions_in_world(&mut world, None); - - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!( - world.resource::>().0, - SubStateOfComputed::One - ); - - world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(true) - ); - assert_eq!( - world.resource::>().0, - SubStateOfComputed::Two - ); - - world.insert_resource(NextState::Pending(SimpleState::B(false))); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - SimpleState::B(false) - ); - assert!(!world.contains_resource::>()); - } - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - struct OtherState { - a_flexible_value: &'static str, - another_value: u8, - } - - #[derive(PartialEq, Eq, Debug, Hash, Clone)] - enum ComplexComputedState { - InAAndStrIsBobOrJane, - InTrueBAndUsizeAbove8, - } - - impl ComputedStates for ComplexComputedState { - type SourceStates = (Option, Option); - - fn compute(sources: (Option, Option)) -> Option { - match sources { - (Some(simple), Some(complex)) => { - if simple == SimpleState::A - && (complex.a_flexible_value == "bob" || complex.a_flexible_value == "jane") - { - Some(ComplexComputedState::InAAndStrIsBobOrJane) - } else if simple == SimpleState::B(true) && complex.another_value > 8 { - Some(ComplexComputedState::InTrueBAndUsizeAbove8) - } else { - None - } - } - _ => None, - } - } - } - - #[test] - fn complex_computed_state_gets_derived_correctly() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - world.init_resource::>(); - - let mut schedules = Schedules::new(); - let mut apply_changes = Schedule::new(StateTransition); - - ComplexComputedState::register_computed_state_systems(&mut apply_changes); - - SimpleState::register_state(&mut apply_changes); - OtherState::register_state(&mut apply_changes); - schedules.insert(apply_changes); - - world.insert_resource(schedules); - - setup_state_transitions_in_world(&mut world, None); - - world.run_schedule(StateTransition); - assert_eq!(world.resource::>().0, SimpleState::A); - assert_eq!( - world.resource::>().0, - OtherState::default() - ); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.run_schedule(StateTransition); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(OtherState { - a_flexible_value: "felix", - another_value: 13, - })); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - ComplexComputedState::InTrueBAndUsizeAbove8 - ); - - world.insert_resource(NextState::Pending(SimpleState::A)); - world.insert_resource(NextState::Pending(OtherState { - a_flexible_value: "jane", - another_value: 13, - })); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - ComplexComputedState::InAAndStrIsBobOrJane - ); - - world.insert_resource(NextState::Pending(SimpleState::B(false))); - world.insert_resource(NextState::Pending(OtherState { - a_flexible_value: "jane", - another_value: 13, - })); - world.run_schedule(StateTransition); - assert!(!world.contains_resource::>()); - } - - #[derive(Resource, Default)] - struct ComputedStateTransitionCounter { - enter: usize, - exit: usize, - } - - #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] - enum SimpleState2 { - #[default] - A1, - B2, - } - - #[derive(PartialEq, Eq, Debug, Hash, Clone)] - enum TestNewcomputedState { - A1, - B2, - B1, - } - - impl ComputedStates for TestNewcomputedState { - type SourceStates = (Option, Option); - - fn compute((s1, s2): (Option, Option)) -> Option { - match (s1, s2) { - (Some(SimpleState::A), Some(SimpleState2::A1)) => Some(TestNewcomputedState::A1), - (Some(SimpleState::B(true)), Some(SimpleState2::B2)) => { - Some(TestNewcomputedState::B2) - } - (Some(SimpleState::B(true)), _) => Some(TestNewcomputedState::B1), - _ => None, - } - } - } - - #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] - struct Startup; - - #[test] - fn computed_state_transitions_are_produced_correctly() { - let mut world = World::new(); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - EventRegistry::register_event::>(&mut world); - world.init_resource::>(); - world.init_resource::>(); - world.init_resource::(); - - setup_state_transitions_in_world(&mut world, Some(Startup.intern())); - - let mut schedules = world - .get_resource_mut::() - .expect("Schedules don't exist in world"); - let apply_changes = schedules - .get_mut(StateTransition) - .expect("State Transition Schedule Doesn't Exist"); - - TestNewcomputedState::register_computed_state_systems(apply_changes); - - SimpleState::register_state(apply_changes); - SimpleState2::register_state(apply_changes); - - schedules.insert({ - let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::A1)); - schedule.add_systems(|mut count: ResMut| { - count.enter += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnExit(TestNewcomputedState::A1)); - schedule.add_systems(|mut count: ResMut| { - count.exit += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B1)); - schedule.add_systems(|mut count: ResMut| { - count.enter += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B1)); - schedule.add_systems(|mut count: ResMut| { - count.exit += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B2)); - schedule.add_systems(|mut count: ResMut| { - count.enter += 1; - }); - schedule - }); - - schedules.insert({ - let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B2)); - schedule.add_systems(|mut count: ResMut| { - count.exit += 1; - }); - schedule - }); - - world.init_resource::(); - - setup_state_transitions_in_world(&mut world, None); - - assert_eq!(world.resource::>().0, SimpleState::A); - assert_eq!(world.resource::>().0, SimpleState2::A1); - assert!(!world.contains_resource::>()); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.insert_resource(NextState::Pending(SimpleState2::B2)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - TestNewcomputedState::B2 - ); - assert_eq!(world.resource::().enter, 1); - assert_eq!(world.resource::().exit, 0); - - world.insert_resource(NextState::Pending(SimpleState2::A1)); - world.insert_resource(NextState::Pending(SimpleState::A)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - TestNewcomputedState::A1 - ); - assert_eq!( - world.resource::().enter, - 2, - "Should Only Enter Twice" - ); - assert_eq!( - world.resource::().exit, - 1, - "Should Only Exit Once" - ); - - world.insert_resource(NextState::Pending(SimpleState::B(true))); - world.insert_resource(NextState::Pending(SimpleState2::B2)); - world.run_schedule(StateTransition); - assert_eq!( - world.resource::>().0, - TestNewcomputedState::B2 - ); - assert_eq!( - world.resource::().enter, - 3, - "Should Only Enter Three Times" - ); - assert_eq!( - world.resource::().exit, - 2, - "Should Only Exit Twice" - ); - - world.insert_resource(NextState::Pending(SimpleState::A)); - world.run_schedule(StateTransition); - assert!(!world.contains_resource::>()); - assert_eq!( - world.resource::().enter, - 3, - "Should Only Enter Three Times" - ); - assert_eq!( - world.resource::().exit, - 3, - "Should Only Exit Twice" - ); - } -} diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index 967bf850de0c1..3bb22f9409e1b 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -5,10 +5,6 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_utils::HashSet; use std::hash::Hash; -// unused import, but needed for intra doc link to work -#[allow(unused_imports)] -use bevy_ecs::schedule::State; - /// A "press-able" input of type `T`. /// /// ## Usage @@ -23,8 +19,8 @@ use bevy_ecs::schedule::State; /// ## Multiple systems /// /// In case multiple systems are checking for [`ButtonInput::just_pressed`] or [`ButtonInput::just_released`] -/// but only one should react, for example in the case of triggering -/// [`State`] change, you should consider clearing the input state, either by: +/// but only one should react, for example when modifying a +/// [`Resource`], you should consider clearing the input state, either by: /// /// * Using [`ButtonInput::clear_just_pressed`] or [`ButtonInput::clear_just_released`] instead. /// * Calling [`ButtonInput::clear`] or [`ButtonInput::reset`] immediately after the state change. diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 0e4713bba94ed..37038350386e2 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -179,6 +179,9 @@ bevy_dev_tools = ["dep:bevy_dev_tools"] # Enable support for the ios_simulator by downgrading some rendering capabilities ios_simulator = ["bevy_pbr?/ios_simulator", "bevy_render?/ios_simulator"] +# Enable built in global state machines +bevy_state = ["dep:bevy_state", "bevy_app/bevy_state"] + [dependencies] # bevy bevy_a11y = { path = "../bevy_a11y", version = "0.14.0-dev" } @@ -187,6 +190,7 @@ bevy_core = { path = "../bevy_core", version = "0.14.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.14.0-dev" } bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.14.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_state = { path = "../bevy_state", optional = true, version = "0.14.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.14.0-dev" } bevy_input = { path = "../bevy_input", version = "0.14.0-dev" } bevy_log = { path = "../bevy_log", version = "0.14.0-dev" } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 7eb982634e39c..f7f828c986bc4 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -52,6 +52,8 @@ pub use bevy_render as render; pub use bevy_scene as scene; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; +#[cfg(feature = "bevy_state")] +pub use bevy_state as state; pub use bevy_tasks as tasks; #[cfg(feature = "bevy_text")] pub use bevy_text as text; diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index aa256727810da..7566246296d36 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -62,3 +62,7 @@ pub use crate::gizmos::prelude::*; #[doc(hidden)] #[cfg(feature = "bevy_gilrs")] pub use crate::gilrs::*; + +#[doc(hidden)] +#[cfg(feature = "bevy_state")] +pub use crate::state::prelude::*; diff --git a/crates/bevy_state/Cargo.toml b/crates/bevy_state/Cargo.toml new file mode 100644 index 0000000000000..009d283eaea8a --- /dev/null +++ b/crates/bevy_state/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "bevy_state" +version = "0.14.0-dev" +edition = "2021" +description = "Bevy Engine's entity component system" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["ecs", "game", "bevy"] +categories = ["game-engines", "data-structures"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["bevy_reflect"] + + +[dependencies] +bevy_ecs = { path = "../bevy_ecs", version = "0.14.0-dev" } +bevy_state_macros = { path = "macros", version = "0.14.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.14.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.14.0-dev", optional = true } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_state/macros/Cargo.toml b/crates/bevy_state/macros/Cargo.toml new file mode 100644 index 0000000000000..70ff618e4749a --- /dev/null +++ b/crates/bevy_state/macros/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bevy_state_macros" +version = "0.14.0-dev" +description = "Bevy ECS Macros" +edition = "2021" +license = "MIT OR Apache-2.0" + +[lib] +proc-macro = true + +[dependencies] +bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.14.0-dev" } + +syn = { version = "2.0", features = ["full"] } +quote = "1.0" +proc-macro2 = "1.0" + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--cfg", "docsrs"] +all-features = true diff --git a/crates/bevy_state/macros/src/lib.rs b/crates/bevy_state/macros/src/lib.rs new file mode 100644 index 0000000000000..7d401a4793612 --- /dev/null +++ b/crates/bevy_state/macros/src/lib.rs @@ -0,0 +1,24 @@ +// FIXME(3492): remove once docs are ready +#![allow(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +extern crate proc_macro; + +mod states; + +use bevy_macro_utils::BevyManifest; +use proc_macro::TokenStream; + +#[proc_macro_derive(States)] +pub fn derive_states(input: TokenStream) -> TokenStream { + states::derive_states(input) +} + +#[proc_macro_derive(SubStates, attributes(source))] +pub fn derive_substates(input: TokenStream) -> TokenStream { + states::derive_substates(input) +} + +pub(crate) fn bevy_state_path() -> syn::Path { + BevyManifest::default().get_path("bevy_state") +} diff --git a/crates/bevy_state/macros/src/states.rs b/crates/bevy_state/macros/src/states.rs new file mode 100644 index 0000000000000..76a6cbcddf1e4 --- /dev/null +++ b/crates/bevy_state/macros/src/states.rs @@ -0,0 +1,140 @@ +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Pat, Path, Result}; + +use crate::bevy_state_path; + +pub fn derive_states(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + + let generics = ast.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let mut base_trait_path = bevy_state_path(); + base_trait_path.segments.push(format_ident!("state").into()); + + let mut trait_path = base_trait_path.clone(); + trait_path.segments.push(format_ident!("States").into()); + + let mut state_mutation_trait_path = base_trait_path.clone(); + state_mutation_trait_path + .segments + .push(format_ident!("FreelyMutableState").into()); + + let struct_name = &ast.ident; + + quote! { + impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause {} + + impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { + } + } + .into() +} + +struct Source { + source_type: Path, + source_value: Pat, +} + +fn parse_sources_attr(ast: &DeriveInput) -> Result { + let mut result = ast + .attrs + .iter() + .filter(|a| a.path().is_ident("source")) + .map(|meta| { + let mut source = None; + let value = meta.parse_nested_meta(|nested| { + let source_type = nested.path.clone(); + let source_value = Pat::parse_multi(nested.value()?)?; + source = Some(Source { + source_type, + source_value, + }); + Ok(()) + }); + match source { + Some(value) => Ok(value), + None => match value { + Ok(_) => Err(syn::Error::new( + ast.span(), + "Couldn't parse SubStates source", + )), + Err(e) => Err(e), + }, + } + }) + .collect::>>()?; + + if result.len() > 1 { + return Err(syn::Error::new( + ast.span(), + "Only one source is allowed for SubStates", + )); + } + + let Some(result) = result.pop() else { + return Err(syn::Error::new(ast.span(), "SubStates require a source")); + }; + + Ok(result) +} + +pub fn derive_substates(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let sources = parse_sources_attr(&ast).expect("Failed to parse substate sources"); + + let generics = ast.generics; + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + + let mut base_trait_path = bevy_state_path(); + base_trait_path.segments.push(format_ident!("state").into()); + + let mut trait_path = base_trait_path.clone(); + trait_path.segments.push(format_ident!("SubStates").into()); + + let mut state_set_trait_path = base_trait_path.clone(); + state_set_trait_path + .segments + .push(format_ident!("StateSet").into()); + + let mut state_trait_path = base_trait_path.clone(); + state_trait_path + .segments + .push(format_ident!("States").into()); + + let mut state_mutation_trait_path = base_trait_path.clone(); + state_mutation_trait_path + .segments + .push(format_ident!("FreelyMutableState").into()); + + let struct_name = &ast.ident; + + let source_state_type = sources.source_type; + let source_state_value = sources.source_value; + + let result = quote! { + impl #impl_generics #trait_path for #struct_name #ty_generics #where_clause { + type SourceStates = #source_state_type; + + fn should_exist(sources: #source_state_type) -> Option { + if matches!(sources, #source_state_value) { + Some(Self::default()) + } else { + None + } + } + } + + impl #impl_generics #state_trait_path for #struct_name #ty_generics #where_clause { + const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + } + + impl #impl_generics #state_mutation_trait_path for #struct_name #ty_generics #where_clause { + } + }; + + // panic!("Got Result\n{}", result.to_string()); + + result.into() +} diff --git a/crates/bevy_state/src/condition.rs b/crates/bevy_state/src/condition.rs new file mode 100644 index 0000000000000..c0ff5abe49dd1 --- /dev/null +++ b/crates/bevy_state/src/condition.rs @@ -0,0 +1,204 @@ +use bevy_ecs::{change_detection::DetectChanges, system::Res}; +use bevy_utils::warn_once; + +use crate::state::{State, States}; + +/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// if the state machine exists. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// app.add_systems( +/// // `state_exists` will only return true if the +/// // given state exists +/// my_system.run_if(state_exists::), +/// ); +/// +/// fn my_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// // `GameState` does not yet exist `my_system` won't run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 0); +/// +/// world.init_resource::>(); +/// +/// // `GameState` now exists so `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// ``` +pub fn state_exists(current_state: Option>>) -> bool { + current_state.is_some() +} + +/// Generates a [`Condition`](bevy_ecs::prelude::Condition)-satisfying closure that returns `true` +/// if the state machine is currently in `state`. +/// +/// Will return `false` if the state does not exist or if not in `state`. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// world.init_resource::>(); +/// +/// app.add_systems(( +/// // `in_state` will only return true if the +/// // given state equals the given value +/// play_system.run_if(in_state(GameState::Playing)), +/// pause_system.run_if(in_state(GameState::Paused)), +/// )); +/// +/// fn play_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// fn pause_system(mut counter: ResMut) { +/// counter.0 -= 1; +/// } +/// +/// // We default to `GameState::Playing` so `play_system` runs +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// *world.resource_mut::>() = State::new(GameState::Paused); +/// +/// // Now that we are in `GameState::Pause`, `pause_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 0); +/// ``` +pub fn in_state(state: S) -> impl FnMut(Option>>) -> bool + Clone { + move |current_state: Option>>| match current_state { + Some(current_state) => *current_state == state, + None => { + warn_once!("No state matching the type for {} exists - did you forget to `init_state` when initializing the app?", { + let debug_state = format!("{state:?}"); + let result = debug_state + .split("::") + .next() + .unwrap_or("Unknown State Type"); + result.to_string() + }); + + false + } + } +} + +/// A [`Condition`](bevy_ecs::prelude::Condition)-satisfying system that returns `true` +/// if the state machine changed state. +/// +/// To do things on transitions to/from specific states, use their respective OnEnter/OnExit +/// schedules. Use this run condition if you want to detect any change, regardless of the value. +/// +/// Returns false if the state does not exist or the state has not changed. +/// +/// # Example +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # #[derive(Resource, Default)] +/// # struct Counter(u8); +/// # let mut app = Schedule::default(); +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] +/// enum GameState { +/// #[default] +/// Playing, +/// Paused, +/// } +/// +/// world.init_resource::>(); +/// +/// app.add_systems( +/// // `state_changed` will only return true if the +/// // given states value has just been updated or +/// // the state has just been added +/// my_system.run_if(state_changed::), +/// ); +/// +/// fn my_system(mut counter: ResMut) { +/// counter.0 += 1; +/// } +/// +/// // `GameState` has just been added so `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// // `GameState` has not been updated so `my_system` will not run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 1); +/// +/// *world.resource_mut::>() = State::new(GameState::Paused); +/// +/// // Now that `GameState` has been updated `my_system` will run +/// app.run(&mut world); +/// assert_eq!(world.resource::().0, 2); +/// ``` +pub fn state_changed(current_state: Option>>) -> bool { + let Some(current_state) = current_state else { + return false; + }; + current_state.is_changed() +} + +#[cfg(test)] +mod tests { + use crate as bevy_state; + + use bevy_ecs::schedule::{Condition, IntoSystemConfigs, Schedule}; + + use crate::prelude::*; + use bevy_state_macros::States; + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum TestState { + #[default] + A, + B, + } + + fn test_system() {} + + // Ensure distributive_run_if compiles with the common conditions. + #[test] + fn distributive_run_if_compiles() { + Schedule::default().add_systems( + (test_system, test_system) + .distributive_run_if(state_exists::) + .distributive_run_if(in_state(TestState::A).or_else(in_state(TestState::B))) + .distributive_run_if(state_changed::), + ); + } +} diff --git a/crates/bevy_state/src/lib.rs b/crates/bevy_state/src/lib.rs new file mode 100644 index 0000000000000..2104dcedcd897 --- /dev/null +++ b/crates/bevy_state/src/lib.rs @@ -0,0 +1,44 @@ +//! In Bevy, states are app-wide interdependent, finite state machines that are generally used to model the large scale structure of your program: whether a game is paused, if the player is in combat, if assets are loaded and so on. +//! +//! This module provides 3 distinct types of state, all of which implement the [`States`](state::States) trait: +//! +//! - Standard [`States`](state::States) can only be changed by manually setting the [`NextState`](state::NextState) resource. +//! These states are the baseline on which the other state types are built, and can be used on +//! their own for many simple patterns. See the [state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/state.rs) +//! for a simple use case. +//! - [`SubStates`](state::SubStates) are children of other states - they can be changed manually using [`NextState`](state::NextState), +//! but are removed from the [`World`](bevy_ecs::prelude::World) if the source states aren't in the right state. See the [sub_states example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/sub_states.rs) +//! for a simple use case based on the derive macro, or read the trait docs for more complex scenarios. +//! - [`ComputedStates`](state::ComputedStates) are fully derived from other states - they provide a [`compute`](state::ComputedStates::compute) method +//! that takes in the source states and returns their derived value. They are particularly useful for situations +//! where a simplified view of the source states is necessary - such as having an `InAMenu` computed state, derived +//! from a source state that defines multiple distinct menus. See the [computed state example](https://github.com/bevyengine/bevy/blob/latest/examples/ecs/computed_states.rs) +//! to see usage samples for these states. +//! +//! Most of the utilities around state involve running systems during transitions between states, or +//! determining whether to run certain systems, though they can be used more directly as well. This +//! makes it easier to transition between menus, add loading screens, pause games, and the more. +//! +//! Specifically, Bevy provides the following utilities: +//! +//! - 3 Transition Schedules - [`OnEnter`](crate::state::OnEnter), [`OnExit`](crate::state::OnExit) and [`OnTransition`](crate::state::OnTransition) - which are used +//! to trigger systems specifically during matching transitions. +//! - A [`StateTransitionEvent`](crate::state::StateTransitionEvent) that gets fired when a given state changes. +//! - The [`in_state`](crate::condition::in_state) and [`state_changed`](crate::condition::state_changed) run conditions - which are used +//! to determine whether a system should run based on the current state. + +/// Provides definitions for the runtime conditions that interact with the state system +pub mod condition; +/// Provides definitions for the basic traits required by the state system +pub mod state; + +/// Most commonly used re-exported types. +pub mod prelude { + #[doc(hidden)] + pub use crate::condition::*; + #[doc(hidden)] + pub use crate::state::{ + apply_state_transition, ComputedStates, NextState, OnEnter, OnExit, OnTransition, State, + StateSet, StateTransition, StateTransitionEvent, States, SubStates, + }; +} diff --git a/crates/bevy_state/src/state/computed_states.rs b/crates/bevy_state/src/state/computed_states.rs new file mode 100644 index 0000000000000..fda0f99d8c821 --- /dev/null +++ b/crates/bevy_state/src/state/computed_states.rs @@ -0,0 +1,97 @@ +use std::fmt::Debug; +use std::hash::Hash; + +use bevy_ecs::schedule::Schedule; + +use super::state_set::StateSet; +use super::states::States; + +/// A state whose value is automatically computed based on the values of other [`States`]. +/// +/// A **computed state** is a state that is deterministically derived from a set of `SourceStates`. +/// The [`StateSet`] is passed into the `compute` method whenever one of them changes, and the +/// result becomes the state's value. +/// +/// ``` +/// # use bevy_state::prelude::*; +/// # use bevy_ecs::prelude::*; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// struct InGame; +/// +/// impl ComputedStates for InGame { +/// /// We set the source state to be the state, or a tuple of states, +/// /// we want to depend on. You can also wrap each state in an Option, +/// /// if you want the computed state to execute even if the state doesn't +/// /// currently exist in the world. +/// type SourceStates = AppState; +/// +/// /// We then define the compute function, which takes in +/// /// your SourceStates +/// fn compute(sources: AppState) -> Option { +/// match sources { +/// /// When we are in game, we want to return the InGame state +/// AppState::InGame { .. } => Some(InGame), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// ``` +/// +/// you can then add it to an App, and from there you use the state as normal +/// +/// ``` +/// # use bevy_state::prelude::*; +/// # use bevy_ecs::prelude::*; +/// +/// # struct App; +/// # impl App { +/// # fn new() -> Self { App } +/// # fn init_state(&mut self) -> &mut Self {self} +/// # fn add_computed_state(&mut self) -> &mut Self {self} +/// # } +/// # struct AppState; +/// # struct InGame; +/// +/// App::new() +/// .init_state::() +/// .add_computed_state::(); +/// ``` +pub trait ComputedStates: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { + /// The set of states from which the [`Self`] is derived. + /// + /// This can either be a single type that implements [`States`], an Option of a type + /// that implements [`States`], or a tuple + /// containing multiple types that implement [`States`] or Optional versions of them. + /// + /// For example, `(MapState, EnemyState)` is valid, as is `(MapState, Option)` + type SourceStates: StateSet; + + /// Computes the next value of [`State`](crate::state::State). + /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. + /// + /// If the result is [`None`], the [`State`](crate::state::State) resource will be removed from the world. + fn compute(sources: Self::SourceStates) -> Option; + + /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) + /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not + /// used. + fn register_computed_state_systems(schedule: &mut Schedule) { + Self::SourceStates::register_computed_state_systems_in_schedule::(schedule); + } +} + +impl States for S { + const DEPENDENCY_DEPTH: usize = S::SourceStates::SET_DEPENDENCY_DEPTH + 1; +} diff --git a/crates/bevy_state/src/state/freely_mutable_state.rs b/crates/bevy_state/src/state/freely_mutable_state.rs new file mode 100644 index 0000000000000..1fc809f9e5d1f --- /dev/null +++ b/crates/bevy_state/src/state/freely_mutable_state.rs @@ -0,0 +1,39 @@ +use bevy_ecs::prelude::Schedule; +use bevy_ecs::schedule::{IntoSystemConfigs, IntoSystemSetConfigs}; +use bevy_ecs::system::IntoSystem; + +use super::states::States; +use super::transitions::*; + +/// This trait allows a state to be mutated directly using the [`NextState`](crate::state::NextState) resource. +/// +/// While ordinary states are freely mutable (and implement this trait as part of their derive macro), +/// computed states are not: instead, they can *only* change when the states that drive them do. +pub trait FreelyMutableState: States { + /// This function registers all the necessary systems to apply state changes and run transition schedules + fn register_state(schedule: &mut Schedule) { + schedule + .add_systems( + apply_state_transition::.in_set(ApplyStateTransition::::apply()), + ) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::ManualTransitions), + ); + } +} diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs new file mode 100644 index 0000000000000..d434b8cca470b --- /dev/null +++ b/crates/bevy_state/src/state/mod.rs @@ -0,0 +1,502 @@ +mod computed_states; +mod freely_mutable_state; +mod resources; +mod state_set; +mod states; +mod sub_states; +mod transitions; + +pub use computed_states::*; +pub use freely_mutable_state::*; +pub use resources::*; +pub use state_set::*; +pub use states::*; +pub use sub_states::*; +pub use transitions::*; + +#[cfg(test)] +mod tests { + use bevy_ecs::prelude::*; + use bevy_ecs::schedule::ScheduleLabel; + use bevy_state_macros::SubStates; + + use super::*; + + use bevy_ecs::event::EventRegistry; + + use bevy_ecs::prelude::ResMut; + + use crate as bevy_state; + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum SimpleState { + #[default] + A, + B(bool), + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum TestComputedState { + BisTrue, + BisFalse, + } + + impl ComputedStates for TestComputedState { + type SourceStates = Option; + + fn compute(sources: Option) -> Option { + sources.and_then(|source| match source { + SimpleState::A => None, + SimpleState::B(value) => Some(if value { Self::BisTrue } else { Self::BisFalse }), + }) + } + } + + #[test] + fn computed_state_with_a_single_source_is_correctly_derived() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + TestComputedState::register_computed_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + TestComputedState::BisTrue + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert_eq!( + world.resource::>().0, + TestComputedState::BisFalse + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + } + + #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] + #[source(SimpleState = SimpleState::B(true))] + enum SubState { + #[default] + One, + Two, + } + + #[test] + fn sub_state_exists_only_when_allowed_but_can_be_modified_freely() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SubState::register_sub_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SubState::Two)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SubState::One); + + world.insert_resource(NextState::Pending(SubState::Two)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SubState::Two); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert!(!world.contains_resource::>()); + } + + #[derive(SubStates, PartialEq, Eq, Debug, Default, Hash, Clone)] + #[source(TestComputedState = TestComputedState::BisTrue)] + enum SubStateOfComputed { + #[default] + One, + Two, + } + + #[test] + fn substate_of_computed_states_works_appropriately() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + TestComputedState::register_computed_state_systems(&mut apply_changes); + SubStateOfComputed::register_sub_state_systems(&mut apply_changes); + SimpleState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + SubStateOfComputed::One + ); + + world.insert_resource(NextState::Pending(SubStateOfComputed::Two)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!( + world.resource::>().0, + SubStateOfComputed::Two + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(false) + ); + assert!(!world.contains_resource::>()); + } + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + struct OtherState { + a_flexible_value: &'static str, + another_value: u8, + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum ComplexComputedState { + InAAndStrIsBobOrJane, + InTrueBAndUsizeAbove8, + } + + impl ComputedStates for ComplexComputedState { + type SourceStates = (Option, Option); + + fn compute(sources: (Option, Option)) -> Option { + match sources { + (Some(simple), Some(complex)) => { + if simple == SimpleState::A + && (complex.a_flexible_value == "bob" || complex.a_flexible_value == "jane") + { + Some(ComplexComputedState::InAAndStrIsBobOrJane) + } else if simple == SimpleState::B(true) && complex.another_value > 8 { + Some(ComplexComputedState::InTrueBAndUsizeAbove8) + } else { + None + } + } + _ => None, + } + } + } + + #[test] + fn complex_computed_state_gets_derived_correctly() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + + ComplexComputedState::register_computed_state_systems(&mut apply_changes); + + SimpleState::register_state(&mut apply_changes); + OtherState::register_state(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + + setup_state_transitions_in_world(&mut world, None); + + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!( + world.resource::>().0, + OtherState::default() + ); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "felix", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + ComplexComputedState::InTrueBAndUsizeAbove8 + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "jane", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + ComplexComputedState::InAAndStrIsBobOrJane + ); + + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.insert_resource(NextState::Pending(OtherState { + a_flexible_value: "jane", + another_value: 13, + })); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + } + + #[derive(Resource, Default)] + struct ComputedStateTransitionCounter { + enter: usize, + exit: usize, + } + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum SimpleState2 { + #[default] + A1, + B2, + } + + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum TestNewcomputedState { + A1, + B2, + B1, + } + + impl ComputedStates for TestNewcomputedState { + type SourceStates = (Option, Option); + + fn compute((s1, s2): (Option, Option)) -> Option { + match (s1, s2) { + (Some(SimpleState::A), Some(SimpleState2::A1)) => Some(TestNewcomputedState::A1), + (Some(SimpleState::B(true)), Some(SimpleState2::B2)) => { + Some(TestNewcomputedState::B2) + } + (Some(SimpleState::B(true)), _) => Some(TestNewcomputedState::B1), + _ => None, + } + } + } + + #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] + struct Startup; + + #[test] + fn computed_state_transitions_are_produced_correctly() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + world.init_resource::>(); + world.init_resource::>(); + world.init_resource::(); + + setup_state_transitions_in_world(&mut world, Some(Startup.intern())); + + let mut schedules = world + .get_resource_mut::() + .expect("Schedules don't exist in world"); + let apply_changes = schedules + .get_mut(StateTransition) + .expect("State Transition Schedule Doesn't Exist"); + + TestNewcomputedState::register_computed_state_systems(apply_changes); + + SimpleState::register_state(apply_changes); + SimpleState2::register_state(apply_changes); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::A1)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::A1)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B1)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B1)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnEnter(TestNewcomputedState::B2)); + schedule.add_systems(|mut count: ResMut| { + count.enter += 1; + }); + schedule + }); + + schedules.insert({ + let mut schedule = Schedule::new(OnExit(TestNewcomputedState::B2)); + schedule.add_systems(|mut count: ResMut| { + count.exit += 1; + }); + schedule + }); + + world.init_resource::(); + + setup_state_transitions_in_world(&mut world, None); + + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::B2 + ); + assert_eq!(world.resource::().enter, 1); + assert_eq!(world.resource::().exit, 0); + + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::A1 + ); + assert_eq!( + world.resource::().enter, + 2, + "Should Only Enter Twice" + ); + assert_eq!( + world.resource::().exit, + 1, + "Should Only Exit Once" + ); + + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + TestNewcomputedState::B2 + ); + assert_eq!( + world.resource::().enter, + 3, + "Should Only Enter Three Times" + ); + assert_eq!( + world.resource::().exit, + 2, + "Should Only Exit Twice" + ); + + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + assert_eq!( + world.resource::().enter, + 3, + "Should Only Enter Three Times" + ); + assert_eq!( + world.resource::().exit, + 3, + "Should Only Exit Twice" + ); + } +} diff --git a/crates/bevy_state/src/state/resources.rs b/crates/bevy_state/src/state/resources.rs new file mode 100644 index 0000000000000..3d98c56367913 --- /dev/null +++ b/crates/bevy_state/src/state/resources.rs @@ -0,0 +1,132 @@ +use std::ops::Deref; + +use bevy_ecs::{ + system::Resource, + world::{FromWorld, World}, +}; + +use super::{freely_mutable_state::FreelyMutableState, states::States}; + +#[cfg(feature = "bevy_reflect")] +use bevy_ecs::prelude::ReflectResource; + +/// A finite-state machine whose transitions have associated schedules +/// ([`OnEnter(state)`](crate::state::OnEnter) and [`OnExit(state)`](crate::state::OnExit)). +/// +/// The current state value can be accessed through this resource. To *change* the state, +/// queue a transition in the [`NextState`] resource, and it will be applied by the next +/// [`apply_state_transition::`](crate::state::apply_state_transition) system. +/// +/// The starting state is defined via the [`Default`] implementation for `S`. +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn game_logic(game_state: Res>) { +/// match game_state.get() { +/// GameState::InGame => { +/// // Run game logic here... +/// }, +/// _ => {}, +/// } +/// } +/// ``` +#[derive(Resource, Debug)] +#[cfg_attr( + feature = "bevy_reflect", + derive(bevy_reflect::Reflect), + reflect(Resource) +)] +pub struct State(pub(crate) S); + +impl State { + /// Creates a new state with a specific value. + /// + /// To change the state use [`NextState`] rather than using this to modify the `State`. + pub fn new(state: S) -> Self { + Self(state) + } + + /// Get the current state. + pub fn get(&self) -> &S { + &self.0 + } +} + +impl FromWorld for State { + fn from_world(world: &mut World) -> Self { + Self(S::from_world(world)) + } +} + +impl PartialEq for State { + fn eq(&self, other: &S) -> bool { + self.get() == other + } +} + +impl Deref for State { + type Target = S; + + fn deref(&self) -> &Self::Target { + self.get() + } +} + +/// The next state of [`State`]. +/// +/// To queue a transition, just set the contained value to `Some(next_state)`. +/// +/// Note that these transitions can be overridden by other systems: +/// only the actual value of this resource at the time of [`apply_state_transition`](crate::state::apply_state_transition) matters. +/// +/// ``` +/// use bevy_state::prelude::*; +/// use bevy_ecs::prelude::*; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// fn start_game(mut next_game_state: ResMut>) { +/// next_game_state.set(GameState::InGame); +/// } +/// ``` +#[derive(Resource, Debug, Default)] +#[cfg_attr( + feature = "bevy_reflect", + derive(bevy_reflect::Reflect), + reflect(Resource) +)] +pub enum NextState { + /// No state transition is pending + #[default] + Unchanged, + /// There is a pending transition for state `S` + Pending(S), +} + +impl NextState { + /// Tentatively set a pending state transition to `Some(state)`. + pub fn set(&mut self, state: S) { + *self = Self::Pending(state); + } + + /// Remove any pending changes to [`State`] + pub fn reset(&mut self) { + *self = Self::Unchanged; + } +} diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs new file mode 100644 index 0000000000000..067711829dbf0 --- /dev/null +++ b/crates/bevy_state/src/state/state_set.rs @@ -0,0 +1,287 @@ +use bevy_ecs::{ + event::{EventReader, EventWriter}, + schedule::{IntoSystemConfigs, IntoSystemSetConfigs, Schedule}, + system::{Commands, IntoSystem, Res, ResMut}, +}; +use bevy_utils::all_tuples; + +use self::sealed::StateSetSealed; + +use super::{ + apply_state_transition, computed_states::ComputedStates, internal_apply_state_transition, + run_enter, run_exit, run_transition, should_run_transition, sub_states::SubStates, + ApplyStateTransition, OnEnter, OnExit, OnTransition, State, StateTransitionEvent, + StateTransitionSteps, States, +}; + +mod sealed { + /// Sealed trait used to prevent external implementations of [`StateSet`](super::StateSet). + pub trait StateSetSealed {} +} + +/// A [`States`] type or tuple of types which implement [`States`]. +/// +/// This trait is used allow implementors of [`States`], as well +/// as tuples containing exclusively implementors of [`States`], to +/// be used as [`ComputedStates::SourceStates`]. +/// +/// It is sealed, and auto implemented for all [`States`] types and +/// tuples containing them. +pub trait StateSet: sealed::StateSetSealed { + /// The total [`DEPENDENCY_DEPTH`](`States::DEPENDENCY_DEPTH`) of all + /// the states that are part of this [`StateSet`], added together. + /// + /// Used to de-duplicate computed state executions and prevent cyclic + /// computed states. + const SET_DEPENDENCY_DEPTH: usize; + + /// Sets up the systems needed to compute `T` whenever any `State` in this + /// `StateSet` is changed. + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ); + + /// Sets up the systems needed to compute whether `T` exists whenever any `State` in this + /// `StateSet` is changed. + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ); +} + +/// The `InnerStateSet` trait is used to isolate [`ComputedStates`] & [`SubStates`] from +/// needing to wrap all state dependencies in an [`Option`]. +/// +/// Some [`ComputedStates`]'s might need to exist in different states based on the existence +/// of other states. So we needed the ability to use[`Option`] when appropriate. +/// +/// The isolation works because it is implemented for both S & [`Option`], and has the `RawState` associated type +/// that allows it to know what the resource in the world should be. We can then essentially "unwrap" it in our +/// `StateSet` implementation - and the behaviour of that unwrapping will depend on the arguments expected by the +/// the [`ComputedStates`] & [`SubStates]`. +trait InnerStateSet: Sized { + type RawState: States; + + const DEPENDENCY_DEPTH: usize; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option; +} + +impl InnerStateSet for S { + type RawState = Self; + + const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option { + wrapped.map(|v| v.0.clone()) + } +} + +impl InnerStateSet for Option { + type RawState = S; + + const DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn convert_to_usable_state(wrapped: Option<&State>) -> Option { + Some(wrapped.map(|v| v.0.clone())) + } +} + +impl StateSetSealed for S {} + +impl StateSet for S { + const SET_DEPENDENCY_DEPTH: usize = S::DEPENDENCY_DEPTH; + + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |mut parent_changed: EventReader>, + event: EventWriter>, + commands: Commands, + current_state: Option>>, + state_set: Option>>| { + if parent_changed.is_empty() { + return; + } + parent_changed.clear(); + + let new_state = + if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { + T::compute(state_set) + } else { + None + }; + + internal_apply_state_transition(event, commands, current_state, new_state); + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + .after(ApplyStateTransition::::apply()), + ); + } + + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |mut parent_changed: EventReader>, + event: EventWriter>, + commands: Commands, + current_state: Option>>, + state_set: Option>>| { + if parent_changed.is_empty() { + return; + } + parent_changed.clear(); + + let new_state = + if let Some(state_set) = S::convert_to_usable_state(state_set.as_deref()) { + T::should_exist(state_set) + } else { + None + }; + + match new_state { + Some(value) => { + if current_state.is_none() { + internal_apply_state_transition( + event, + commands, + current_state, + Some(value), + ); + } + } + None => { + internal_apply_state_transition(event, commands, current_state, None); + } + }; + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems( + apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions), + ) + .add_systems( + should_run_transition::> + .pipe(run_enter::) + .in_set(StateTransitionSteps::EnterSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_exit::) + .in_set(StateTransitionSteps::ExitSchedules), + ) + .add_systems( + should_run_transition::> + .pipe(run_transition::) + .in_set(StateTransitionSteps::TransitionSchedules), + ) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + .after(ApplyStateTransition::::apply()), + ); + } +} + +macro_rules! impl_state_set_sealed_tuples { + ($(($param: ident, $val: ident, $evt: ident)), *) => { + impl<$($param: InnerStateSet),*> StateSetSealed for ($($param,)*) {} + + impl<$($param: InnerStateSet),*> StateSet for ($($param,)*) { + + const SET_DEPENDENCY_DEPTH : usize = $($param::DEPENDENCY_DEPTH +)* 0; + + + fn register_computed_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { + if ($($evt.is_empty())&&*) { + return; + } + $($evt.clear();)* + + let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { + T::compute(($($val),*, )) + } else { + None + }; + + internal_apply_state_transition(event, commands, current_state, new_state); + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) + .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) + .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + $(.after(ApplyStateTransition::<$param::RawState>::apply()))* + ); + } + + fn register_sub_state_systems_in_schedule>( + schedule: &mut Schedule, + ) { + let system = |($(mut $evt),*,): ($(EventReader>),*,), event: EventWriter>, commands: Commands, current_state: Option>>, ($($val),*,): ($(Option>>),*,)| { + if ($($evt.is_empty())&&*) { + return; + } + $($evt.clear();)* + + let new_state = if let ($(Some($val)),*,) = ($($param::convert_to_usable_state($val.as_deref())),*,) { + T::should_exist(($($val),*, )) + } else { + None + }; + match new_state { + Some(value) => { + if current_state.is_none() { + internal_apply_state_transition(event, commands, current_state, Some(value)); + } + } + None => { + internal_apply_state_transition(event, commands, current_state, None); + }, + }; + }; + + schedule + .add_systems(system.in_set(ApplyStateTransition::::apply())) + .add_systems(apply_state_transition::.in_set(StateTransitionSteps::ManualTransitions)) + .add_systems(should_run_transition::>.pipe(run_enter::).in_set(StateTransitionSteps::EnterSchedules)) + .add_systems(should_run_transition::>.pipe(run_exit::).in_set(StateTransitionSteps::ExitSchedules)) + .add_systems(should_run_transition::>.pipe(run_transition::).in_set(StateTransitionSteps::TransitionSchedules)) + .configure_sets( + ApplyStateTransition::::apply() + .in_set(StateTransitionSteps::DependentTransitions) + $(.after(ApplyStateTransition::<$param::RawState>::apply()))* + ); + } + } + }; +} + +all_tuples!(impl_state_set_sealed_tuples, 1, 15, S, s, ereader); diff --git a/crates/bevy_state/src/state/states.rs b/crates/bevy_state/src/state/states.rs new file mode 100644 index 0000000000000..90c28cd93a2c7 --- /dev/null +++ b/crates/bevy_state/src/state/states.rs @@ -0,0 +1,41 @@ +use std::fmt::Debug; + +use std::hash::Hash; + +pub use bevy_state_macros::States; + +/// Types that can define world-wide states in a finite-state machine. +/// +/// The [`Default`] trait defines the starting state. +/// Multiple states can be defined for the same world, +/// allowing you to classify the state of the world across orthogonal dimensions. +/// You can access the current state of type `T` with the [`State`](crate::state::State) resource, +/// and the queued state with the [`NextState`](crate::state::NextState) resource. +/// +/// State transitions typically occur in the [`OnEnter`](crate::state::OnEnter) and [`OnExit`](crate::state::OnExit) schedules, +/// which can be run by triggering the [`StateTransition`](crate::state::StateTransition) schedule. +/// +/// Types used as [`ComputedStates`](crate::state::ComputedStates) do not need to and should not derive [`States`]. +/// [`ComputedStates`](crate::state::ComputedStates) should not be manually mutated: functionality provided +/// by the [`States`] derive and the associated [`FreelyMutableState`](crate::state::FreelyMutableState) trait. +/// +/// # Example +/// +/// ``` +/// use bevy_state::prelude::States; +/// +/// #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] +/// enum GameState { +/// #[default] +/// MainMenu, +/// SettingsMenu, +/// InGame, +/// } +/// +/// ``` +pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug { + /// How many other states this state depends on. + /// Used to help order transitions and de-duplicate [`ComputedStates`](crate::state::ComputedStates), as well as prevent cyclical + /// `ComputedState` dependencies. + const DEPENDENCY_DEPTH: usize = 1; +} diff --git a/crates/bevy_state/src/state/sub_states.rs b/crates/bevy_state/src/state/sub_states.rs new file mode 100644 index 0000000000000..8046a059b9652 --- /dev/null +++ b/crates/bevy_state/src/state/sub_states.rs @@ -0,0 +1,167 @@ +use bevy_ecs::schedule::Schedule; + +use super::{freely_mutable_state::FreelyMutableState, state_set::StateSet, states::States}; +pub use bevy_state_macros::SubStates; + +/// A sub-state is a state that exists only when the source state meet certain conditions, +/// but unlike [`ComputedStates`](crate::state::ComputedStates) - while they exist they can be manually modified. +/// +/// The default approach to creating [`SubStates`] is using the derive macro, and defining a single source state +/// and value to determine it's existence. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame +/// } +/// +/// +/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// #[source(AppState = AppState::InGame)] +/// enum GamePhase { +/// #[default] +/// Setup, +/// Battle, +/// Conclusion +/// } +/// ``` +/// +/// you can then add it to an App, and from there you use the state as normal: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// +/// # struct App; +/// # impl App { +/// # fn new() -> Self { App } +/// # fn init_state(&mut self) -> &mut Self {self} +/// # fn add_sub_state(&mut self) -> &mut Self {self} +/// # } +/// # struct AppState; +/// # struct GamePhase; +/// +/// App::new() +/// .init_state::() +/// .add_sub_state::(); +/// ``` +/// +/// In more complex situations, the recommendation is to use an intermediary computed state, like so: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// struct InGame; +/// +/// impl ComputedStates for InGame { +/// /// We set the source state to be the state, or set of states, +/// /// we want to depend on. Any of the states can be wrapped in an Option. +/// type SourceStates = Option; +/// +/// /// We then define the compute function, which takes in the AppState +/// fn compute(sources: Option) -> Option { +/// match sources { +/// /// When we are in game, we want to return the InGame state +/// Some(AppState::InGame { .. }) => Some(InGame), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// +/// #[derive(SubStates, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// #[source(InGame = InGame)] +/// enum GamePhase { +/// #[default] +/// Setup, +/// Battle, +/// Conclusion +/// } +/// ``` +/// +/// However, you can also manually implement them. If you do so, you'll also need to manually implement the `States` & `FreelyMutableState` traits. +/// Unlike the derive, this does not require an implementation of [`Default`], since you are providing the `exists` function +/// directly. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_state::prelude::*; +/// # use bevy_state::state::FreelyMutableState; +/// +/// /// Computed States require some state to derive from +/// #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] +/// enum AppState { +/// #[default] +/// Menu, +/// InGame { paused: bool } +/// } +/// +/// #[derive(Clone, PartialEq, Eq, Hash, Debug)] +/// enum GamePhase { +/// Setup, +/// Battle, +/// Conclusion +/// } +/// +/// impl SubStates for GamePhase { +/// /// We set the source state to be the state, or set of states, +/// /// we want to depend on. Any of the states can be wrapped in an Option. +/// type SourceStates = Option; +/// +/// /// We then define the compute function, which takes in the [`Self::SourceStates`] +/// fn should_exist(sources: Option) -> Option { +/// match sources { +/// /// When we are in game, so we want a GamePhase state to exist, and the default is +/// /// GamePhase::Setup +/// Some(AppState::InGame { .. }) => Some(GamePhase::Setup), +/// /// Otherwise, we don't want the `State` resource to exist, +/// /// so we return None. +/// _ => None +/// } +/// } +/// } +/// +/// impl States for GamePhase { +/// const DEPENDENCY_DEPTH : usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; +/// } +/// +/// impl FreelyMutableState for GamePhase {} +/// ``` +pub trait SubStates: States + FreelyMutableState { + /// The set of states from which the [`Self`] is derived. + /// + /// This can either be a single type that implements [`States`], or a tuple + /// containing multiple types that implement [`States`], or any combination of + /// types implementing [`States`] and Options of types implementing [`States`] + type SourceStates: StateSet; + + /// This function gets called whenever one of the [`SourceStates`](Self::SourceStates) changes. + /// The result is used to determine the existence of [`State`](crate::state::State). + /// + /// If the result is [`None`], the [`State`](crate::state::State) resource will be removed from the world, otherwise + /// if the [`State`](crate::state::State) resource doesn't exist - it will be created with the [`Some`] value. + fn should_exist(sources: Self::SourceStates) -> Option; + + /// This function sets up systems that compute the state whenever one of the [`SourceStates`](Self::SourceStates) + /// change. It is called by `App::add_computed_state`, but can be called manually if `App` is not + /// used. + fn register_sub_state_systems(schedule: &mut Schedule) { + Self::SourceStates::register_sub_state_systems_in_schedule::(schedule); + } +} diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs new file mode 100644 index 0000000000000..5e15ae6293f69 --- /dev/null +++ b/crates/bevy_state/src/state/transitions.rs @@ -0,0 +1,276 @@ +use std::{marker::PhantomData, mem, ops::DerefMut}; + +use bevy_ecs::{ + event::{Event, EventReader, EventWriter}, + schedule::{ + InternedScheduleLabel, IntoSystemSetConfigs, Schedule, ScheduleLabel, Schedules, SystemSet, + }, + system::{Commands, In, Local, Res, ResMut}, + world::World, +}; + +use super::{ + freely_mutable_state::FreelyMutableState, + resources::{NextState, State}, + states::States, +}; + +/// The label of a [`Schedule`] that runs whenever [`State`] +/// enters this state. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnEnter(pub S); + +/// The label of a [`Schedule`] that runs whenever [`State`] +/// exits this state. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnExit(pub S); + +/// The label of a [`Schedule`] that **only** runs whenever [`State`] +/// exits the `from` state, AND enters the `to` state. +/// +/// Systems added to this schedule are always ran *after* [`OnExit`], and *before* [`OnEnter`]. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnTransition { + /// The state being exited. + pub from: S, + /// The state being entered. + pub to: S, +} + +/// Runs [state transitions](States). +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct StateTransition; + +/// Event sent when any state transition of `S` happens. +/// +/// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] +pub struct StateTransitionEvent { + /// the state we were in before + pub before: Option, + /// the state we're in now + pub after: Option, +} + +/// Applies manual state transitions using [`NextState`]. +/// +/// These system sets are run sequentially, in the order of the enum variants. +#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) enum StateTransitionSteps { + ManualTransitions, + DependentTransitions, + ExitSchedules, + TransitionSchedules, + EnterSchedules, +} + +/// Defines a system set to aid with dependent state ordering +#[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ApplyStateTransition(PhantomData); + +impl ApplyStateTransition { + pub(crate) fn apply() -> Self { + Self(PhantomData) + } +} + +/// This function actually applies a state change, and registers the required +/// schedules for downstream computed states and transition schedules. +/// +/// The `new_state` is an option to allow for removal - `None` will trigger the +/// removal of the `State` resource from the [`World`]. +pub(crate) fn internal_apply_state_transition( + mut event: EventWriter>, + mut commands: Commands, + current_state: Option>>, + new_state: Option, +) { + match new_state { + Some(entered) => { + match current_state { + // If the [`State`] resource exists, and the state is not the one we are + // entering - we need to set the new value, compute dependant states, send transition events + // and register transition schedules. + Some(mut state_resource) => { + if *state_resource != entered { + let exited = mem::replace(&mut state_resource.0, entered.clone()); + + event.send(StateTransitionEvent { + before: Some(exited.clone()), + after: Some(entered.clone()), + }); + } + } + None => { + // If the [`State`] resource does not exist, we create it, compute dependant states, send a transition event and register the `OnEnter` schedule. + commands.insert_resource(State(entered.clone())); + + event.send(StateTransitionEvent { + before: None, + after: Some(entered.clone()), + }); + } + }; + } + None => { + // We first remove the [`State`] resource, and if one existed we compute dependant states, send a transition event and run the `OnExit` schedule. + if let Some(resource) = current_state { + commands.remove_resource::>(); + + event.send(StateTransitionEvent { + before: Some(resource.get().clone()), + after: None, + }); + } + } + } +} + +/// Sets up the schedules and systems for handling state transitions +/// within a [`World`]. +/// +/// Runs automatically when using `App` to insert states, but needs to +/// be added manually in other situations. +pub fn setup_state_transitions_in_world( + world: &mut World, + startup_label: Option, +) { + let mut schedules = world.get_resource_or_insert_with(Schedules::default); + if schedules.contains(StateTransition) { + return; + } + let mut schedule = Schedule::new(StateTransition); + schedule.configure_sets( + ( + StateTransitionSteps::ManualTransitions, + StateTransitionSteps::DependentTransitions, + StateTransitionSteps::ExitSchedules, + StateTransitionSteps::TransitionSchedules, + StateTransitionSteps::EnterSchedules, + ) + .chain(), + ); + schedules.insert(schedule); + + if let Some(startup) = startup_label { + schedules.add_systems(startup, |world: &mut World| { + let _ = world.try_run_schedule(StateTransition); + }); + } +} + +/// If a new state is queued in [`NextState`], this system +/// takes the new state value from [`NextState`] and updates [`State`], as well as +/// sending the relevant [`StateTransitionEvent`]. +/// +/// If the [`State`] resource does not exist, it does nothing. Removing or adding states +/// should be done at App creation or at your own risk. +/// +/// For [`SubStates`](crate::state::SubStates) - it only applies the state if the `SubState` currently exists. Otherwise, it is wiped. +/// When a `SubState` is re-created, it will use the result of it's `should_exist` method. +pub fn apply_state_transition( + event: EventWriter>, + commands: Commands, + current_state: Option>>, + next_state: Option>>, +) { + // We want to check if the State and NextState resources exist + let Some(mut next_state_resource) = next_state else { + return; + }; + + match next_state_resource.as_ref() { + NextState::Pending(new_state) => { + if let Some(current_state) = current_state { + if new_state != current_state.get() { + let new_state = new_state.clone(); + internal_apply_state_transition( + event, + commands, + Some(current_state), + Some(new_state), + ); + } + } + } + NextState::Unchanged => { + // This is the default value, so we don't need to re-insert the resource + return; + } + } + + *next_state_resource.as_mut() = NextState::::Unchanged; +} + +pub(crate) fn should_run_transition( + mut first: Local, + res: Option>>, + mut event: EventReader>, +) -> (Option>, PhantomData) { + let first_mut = first.deref_mut(); + if !*first_mut { + *first_mut = true; + if let Some(res) = res { + event.clear(); + + return ( + Some(StateTransitionEvent { + before: None, + after: Some(res.get().clone()), + }), + PhantomData, + ); + } + } + (event.read().last().cloned(), PhantomData) +} + +pub(crate) fn run_enter( + In((transition, _)): In<(Option>, PhantomData>)>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + + let Some(after) = transition.after else { + return; + }; + + let _ = world.try_run_schedule(OnEnter(after)); +} + +pub(crate) fn run_exit( + In((transition, _)): In<(Option>, PhantomData>)>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + + let Some(before) = transition.before else { + return; + }; + + let _ = world.try_run_schedule(OnExit(before)); +} + +pub(crate) fn run_transition( + In((transition, _)): In<( + Option>, + PhantomData>, + )>, + world: &mut World, +) { + let Some(transition) = transition else { + return; + }; + let Some(from) = transition.before else { + return; + }; + let Some(to) = transition.after else { + return; + }; + + let _ = world.try_run_schedule(OnTransition { from, to }); +} diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 8a154ca433176..8ccd806a6431d 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -25,6 +25,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_render|Provides rendering functionality| |bevy_scene|Provides scene functionality| |bevy_sprite|Provides sprite functionality| +|bevy_state|Enable built in global state machines| |bevy_text|Provides text functionality| |bevy_ui|A custom ECS-driven UI framework| |bevy_winit|winit window and input backend| diff --git a/examples/README.md b/examples/README.md index 4dd859c6b7a1f..e441faa246e79 100644 --- a/examples/README.md +++ b/examples/README.md @@ -55,6 +55,7 @@ git checkout v0.4.0 - [Reflection](#reflection) - [Scene](#scene) - [Shaders](#shaders) + - [State](#state) - [Stress Tests](#stress-tests) - [Time](#time) - [Tools](#tools) @@ -251,7 +252,6 @@ Example | Description --- | --- [Component Change Detection](../examples/ecs/component_change_detection.rs) | Change detection on components [Component Hooks](../examples/ecs/component_hooks.rs) | Define component hooks to manage component lifecycle events -[Computed States](../examples/ecs/computed_states.rs) | Advanced state patterns using Computed States [Custom Query Parameters](../examples/ecs/custom_query_param.rs) | Groups commonly used compound queries and query filters into a single type [Custom Schedule](../examples/ecs/custom_schedule.rs) | Demonstrates how to add custom schedules [Dynamic ECS](../examples/ecs/dynamic.rs) | Dynamically create components, spawn entities with those components and query those components @@ -268,8 +268,6 @@ Example | Description [Run Conditions](../examples/ecs/run_conditions.rs) | Run systems only when one or multiple conditions are met [Send and receive events](../examples/ecs/send_and_receive_events.rs) | Demonstrates how to send and receive events of the same type in a single system [Startup System](../examples/ecs/startup_system.rs) | Demonstrates a startup system (one that runs once when the app starts up) -[State](../examples/ecs/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state -[Sub States](../examples/ecs/sub_states.rs) | Using Sub States for hierarchical state handling. [System Closure](../examples/ecs/system_closure.rs) | Show how to use closures as systems, and how to configure `Local` variables by capturing external state [System Parameter](../examples/ecs/system_param.rs) | Illustrates creating custom system parameters with `SystemParam` [System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully @@ -361,6 +359,14 @@ Example | Description [Shader Defs](../examples/shader/shader_defs.rs) | A shader that uses "shaders defs" (a bevy tool to selectively toggle parts of a shader) [Texture Binding Array (Bindless Textures)](../examples/shader/texture_binding_array.rs) | A shader that shows how to bind and sample multiple textures as a binding array (a.k.a. bindless textures). +## State + +Example | Description +--- | --- +[Computed States](../examples/state/computed_states.rs) | Advanced state patterns using Computed States +[State](../examples/state/state.rs) | Illustrates how to use States to control transitioning from a Menu state to an InGame state +[Sub States](../examples/state/sub_states.rs) | Using Sub States for hierarchical state handling. + ## Stress Tests These examples are used to test the performance and stability of various parts of the engine in an isolated way. diff --git a/examples/ecs/computed_states.rs b/examples/state/computed_states.rs similarity index 100% rename from examples/ecs/computed_states.rs rename to examples/state/computed_states.rs diff --git a/examples/ecs/state.rs b/examples/state/state.rs similarity index 100% rename from examples/ecs/state.rs rename to examples/state/state.rs diff --git a/examples/ecs/sub_states.rs b/examples/state/sub_states.rs similarity index 100% rename from examples/ecs/sub_states.rs rename to examples/state/sub_states.rs diff --git a/tools/publish.sh b/tools/publish.sh index b020bad286e3f..99dcf1e27522c 100644 --- a/tools/publish.sh +++ b/tools/publish.sh @@ -11,6 +11,8 @@ crates=( bevy_reflect bevy_ecs/macros bevy_ecs + bevy_state/macros + bevy_state bevy_app bevy_time bevy_log From 705c144259aecfd0399a6b22445468e853130a12 Mon Sep 17 00:00:00 2001 From: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Date: Thu, 9 May 2024 11:17:54 -0700 Subject: [PATCH 20/26] bevy_reflect: Remove `ContainerAttributes::merge` (#13303) # Objective Unblocks #11659. Currently the `Reflect` derive macro has to go through a merge process for each `#[reflect]`/`#[reflet_value]` attribute encountered on a container type. Not only is this a bit inefficient, but it also has a soft requirement that we can compare attributes such that an error can be thrown on duplicates, invalid states, etc. While working on #11659 this proved to be challenging due to the fact that `syn` types don't implement `PartialEq` or `Hash` without enabling the `extra-traits` feature. Ideally, we wouldn't have to enable another feature just to accommodate this one use case. ## Solution Removed `ContainerAttributes::merge`. This was a fairly simple change as we could just have the parsing functions take `&mut self` instead of returning `Self`. ## Testing CI should build as there should be no user-facing change. --- .../derive/src/container_attributes.rs | 126 +++++------------- crates/bevy_reflect/derive/src/derive_data.rs | 14 +- .../bevy_reflect/derive/src/reflect_value.rs | 6 +- 3 files changed, 45 insertions(+), 101 deletions(-) diff --git a/crates/bevy_reflect/derive/src/container_attributes.rs b/crates/bevy_reflect/derive/src/container_attributes.rs index 9d236d9b5b8a8..e87e565940074 100644 --- a/crates/bevy_reflect/derive/src/container_attributes.rs +++ b/crates/bevy_reflect/derive/src/container_attributes.rs @@ -92,24 +92,6 @@ impl FromReflectAttrs { .map(|lit| lit.value()) .unwrap_or(true) } - - /// Merges this [`FromReflectAttrs`] with another. - pub fn merge(&mut self, other: FromReflectAttrs) -> Result<(), syn::Error> { - if let Some(new) = other.auto_derive { - if let Some(existing) = &self.auto_derive { - if existing.value() != new.value() { - return Err(syn::Error::new( - new.span(), - format!("`{FROM_REFLECT_ATTR}` already set to {}", existing.value()), - )); - } - } else { - self.auto_derive = Some(new); - } - } - - Ok(()) - } } /// A collection of attributes used for deriving `TypePath` via the `Reflect` derive. @@ -133,24 +115,6 @@ impl TypePathAttrs { .map(|lit| lit.value()) .unwrap_or(true) } - - /// Merges this [`TypePathAttrs`] with another. - pub fn merge(&mut self, other: TypePathAttrs) -> Result<(), syn::Error> { - if let Some(new) = other.auto_derive { - if let Some(existing) = &self.auto_derive { - if existing.value() != new.value() { - return Err(syn::Error::new( - new.span(), - format!("`{TYPE_PATH_ATTR}` already set to {}", existing.value()), - )); - } - } else { - self.auto_derive = Some(new); - } - } - - Ok(()) - } } /// A collection of traits that have been registered for a reflected type. @@ -231,14 +195,16 @@ impl ContainerAttributes { /// /// # Example /// - `Hash, Debug(custom_debug), MyTrait` - pub fn parse_terminated(input: ParseStream, trait_: ReflectTraitToImpl) -> syn::Result { - let mut this = Self::default(); - + pub fn parse_terminated( + &mut self, + input: ParseStream, + trait_: ReflectTraitToImpl, + ) -> syn::Result<()> { terminated_parser(Token![,], |stream| { - this.parse_container_attribute(stream, trait_) + self.parse_container_attribute(stream, trait_) })(input)?; - Ok(this) + Ok(()) } /// Parse the contents of a `#[reflect(...)]` attribute into a [`ContainerAttributes`] instance. @@ -246,8 +212,12 @@ impl ContainerAttributes { /// # Example /// - `#[reflect(Hash, Debug(custom_debug), MyTrait)]` /// - `#[reflect(no_field_bounds)]` - pub fn parse_meta_list(meta: &MetaList, trait_: ReflectTraitToImpl) -> syn::Result { - meta.parse_args_with(|stream: ParseStream| Self::parse_terminated(stream, trait_)) + pub fn parse_meta_list( + &mut self, + meta: &MetaList, + trait_: ReflectTraitToImpl, + ) -> syn::Result<()> { + meta.parse_args_with(|stream: ParseStream| self.parse_terminated(stream, trait_)) } /// Parse a single container attribute. @@ -392,7 +362,7 @@ impl ContainerAttributes { trait_: ReflectTraitToImpl, ) -> syn::Result<()> { let pair = input.parse::()?; - let value = extract_bool(&pair.value, |lit| { + let extracted_bool = extract_bool(&pair.value, |lit| { // Override `lit` if this is a `FromReflect` derive. // This typically means a user is opting out of the default implementation // from the `Reflect` derive and using the `FromReflect` derive directly instead. @@ -401,7 +371,16 @@ impl ContainerAttributes { .unwrap_or_else(|| lit.clone()) })?; - self.from_reflect_attrs.auto_derive = Some(value); + if let Some(existing) = &self.from_reflect_attrs.auto_derive { + if existing.value() != extracted_bool.value() { + return Err(syn::Error::new( + extracted_bool.span(), + format!("`{FROM_REFLECT_ATTR}` already set to {}", existing.value()), + )); + } + } else { + self.from_reflect_attrs.auto_derive = Some(extracted_bool); + } Ok(()) } @@ -416,7 +395,7 @@ impl ContainerAttributes { trait_: ReflectTraitToImpl, ) -> syn::Result<()> { let pair = input.parse::()?; - let value = extract_bool(&pair.value, |lit| { + let extracted_bool = extract_bool(&pair.value, |lit| { // Override `lit` if this is a `FromReflect` derive. // This typically means a user is opting out of the default implementation // from the `Reflect` derive and using the `FromReflect` derive directly instead. @@ -425,7 +404,16 @@ impl ContainerAttributes { .unwrap_or_else(|| lit.clone()) })?; - self.type_path_attrs.auto_derive = Some(value); + if let Some(existing) = &self.type_path_attrs.auto_derive { + if existing.value() != extracted_bool.value() { + return Err(syn::Error::new( + extracted_bool.span(), + format!("`{TYPE_PATH_ATTR}` already set to {}", existing.value()), + )); + } + } else { + self.type_path_attrs.auto_derive = Some(extracted_bool); + } Ok(()) } @@ -530,50 +518,6 @@ impl ContainerAttributes { pub fn no_field_bounds(&self) -> bool { self.no_field_bounds } - - /// Merges the trait implementations of this [`ContainerAttributes`] with another one. - /// - /// An error is returned if the two [`ContainerAttributes`] have conflicting implementations. - pub fn merge(&mut self, other: ContainerAttributes) -> Result<(), syn::Error> { - // Destructuring is used to help ensure that all fields are merged - let Self { - debug, - hash, - partial_eq, - from_reflect_attrs, - type_path_attrs, - custom_where, - no_field_bounds, - idents, - } = self; - - debug.merge(other.debug)?; - hash.merge(other.hash)?; - partial_eq.merge(other.partial_eq)?; - from_reflect_attrs.merge(other.from_reflect_attrs)?; - type_path_attrs.merge(other.type_path_attrs)?; - - Self::merge_custom_where(custom_where, other.custom_where); - - *no_field_bounds |= other.no_field_bounds; - - for ident in other.idents { - add_unique_ident(idents, ident)?; - } - Ok(()) - } - - fn merge_custom_where(this: &mut Option, other: Option) { - match (this, other) { - (Some(this), Some(other)) => { - this.predicates.extend(other.predicates); - } - (this @ None, Some(other)) => { - *this = Some(other); - } - _ => {} - } - } } /// Adds an identifier to a vector of identifiers if it is not already present. diff --git a/crates/bevy_reflect/derive/src/derive_data.rs b/crates/bevy_reflect/derive/src/derive_data.rs index 48625ec3ca283..3c45b5ee3be15 100644 --- a/crates/bevy_reflect/derive/src/derive_data.rs +++ b/crates/bevy_reflect/derive/src/derive_data.rs @@ -183,7 +183,7 @@ impl<'a> ReflectDerive<'a> { input: &'a DeriveInput, provenance: ReflectProvenance, ) -> Result { - let mut traits = ContainerAttributes::default(); + let mut container_attributes = ContainerAttributes::default(); // Should indicate whether `#[reflect_value]` was used. let mut reflect_mode = None; // Should indicate whether `#[type_path = "..."]` was used. @@ -205,9 +205,7 @@ impl<'a> ReflectDerive<'a> { } reflect_mode = Some(ReflectMode::Normal); - let new_traits = - ContainerAttributes::parse_meta_list(meta_list, provenance.trait_)?; - traits.merge(new_traits)?; + container_attributes.parse_meta_list(meta_list, provenance.trait_)?; } Meta::List(meta_list) if meta_list.path.is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) => { if !matches!(reflect_mode, None | Some(ReflectMode::Value)) { @@ -218,9 +216,7 @@ impl<'a> ReflectDerive<'a> { } reflect_mode = Some(ReflectMode::Value); - let new_traits = - ContainerAttributes::parse_meta_list(meta_list, provenance.trait_)?; - traits.merge(new_traits)?; + container_attributes.parse_meta_list(meta_list, provenance.trait_)?; } Meta::Path(path) if path.is_ident(REFLECT_VALUE_ATTRIBUTE_NAME) => { if !matches!(reflect_mode, None | Some(ReflectMode::Value)) { @@ -296,7 +292,7 @@ impl<'a> ReflectDerive<'a> { generics: &input.generics, }; - let meta = ReflectMeta::new(type_path, traits); + let meta = ReflectMeta::new(type_path, container_attributes); if provenance.source == ReflectImplSource::ImplRemoteType && meta.type_path_attrs().should_auto_derive() @@ -439,7 +435,7 @@ impl<'a> ReflectMeta<'a> { Self { docs, ..self } } - /// The registered reflect traits on this struct. + /// The registered reflect attributes on this struct. pub fn attrs(&self) -> &ContainerAttributes { &self.attrs } diff --git a/crates/bevy_reflect/derive/src/reflect_value.rs b/crates/bevy_reflect/derive/src/reflect_value.rs index e924401a93351..6faa0e8752d45 100644 --- a/crates/bevy_reflect/derive/src/reflect_value.rs +++ b/crates/bevy_reflect/derive/src/reflect_value.rs @@ -55,7 +55,11 @@ impl ReflectValueDef { if input.peek(Paren) { let content; parenthesized!(content in input); - traits = Some(ContainerAttributes::parse_terminated(&content, trait_)?); + traits = Some({ + let mut attrs = ContainerAttributes::default(); + attrs.parse_terminated(&content, trait_)?; + attrs + }); } Ok(ReflectValueDef { attrs, From 4da11fda77f58b9ebe126c92747a6666e530d7d5 Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 9 May 2024 19:30:44 -0400 Subject: [PATCH 21/26] Add `AXES` iterators for `Dir` types (#13305) # Objective Sometimes it's nice to iterate over all the coordinate axes using something like `Vec3::AXES`. This was not available for the corresponding `Dir` types and now it is. ## Solution We already have things like `Dir2::X`, `Dir3::Z` and so on, so I just threw them in an array like the vector types do it. I also slightly refactored the sphere gizmo code to use `Dir3::AXES` and operate on directions instead of using `Dir3::new_unchecked`. ## Testing I looked at the sphere in the `3d_gizmos` example and it seems to work, so I assume I didn't break anything. --- crates/bevy_gizmos/src/gizmos.rs | 9 ++------- crates/bevy_math/src/direction.rs | 6 ++++++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index c62e8b25d891f..f9fc93fd5ad1d 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -825,14 +825,9 @@ where if !self.gizmos.enabled { return; } - for axis in Vec3::AXES { + for axis in Dir3::AXES { self.gizmos - .circle( - self.position, - Dir3::new_unchecked(self.rotation * axis), - self.radius, - self.color, - ) + .circle(self.position, self.rotation * axis, self.radius, self.color) .segments(self.circle_segments); } } diff --git a/crates/bevy_math/src/direction.rs b/crates/bevy_math/src/direction.rs index d44c54a39938e..dbaf7fd671136 100644 --- a/crates/bevy_math/src/direction.rs +++ b/crates/bevy_math/src/direction.rs @@ -92,6 +92,8 @@ impl Dir2 { pub const NEG_X: Self = Self(Vec2::NEG_X); /// A unit vector pointing along the negative Y axis. pub const NEG_Y: Self = Self(Vec2::NEG_Y); + /// The directional axes. + pub const AXES: [Self; 2] = [Self::X, Self::Y]; /// Create a direction from a finite, nonzero [`Vec2`]. /// @@ -254,6 +256,8 @@ impl Dir3 { pub const NEG_Y: Self = Self(Vec3::NEG_Y); /// A unit vector pointing along the negative Z axis. pub const NEG_Z: Self = Self(Vec3::NEG_Z); + /// The directional axes. + pub const AXES: [Self; 3] = [Self::X, Self::Y, Self::Z]; /// Create a direction from a finite, nonzero [`Vec3`]. /// @@ -419,6 +423,8 @@ impl Dir3A { pub const NEG_Y: Self = Self(Vec3A::NEG_Y); /// A unit vector pointing along the negative Z axis. pub const NEG_Z: Self = Self(Vec3A::NEG_Z); + /// The directional axes. + pub const AXES: [Self; 3] = [Self::X, Self::Y, Self::Z]; /// Create a direction from a finite, nonzero [`Vec3A`]. /// From 4b61bbe4e1b364a74e706c3dd4ee06659301211d Mon Sep 17 00:00:00 2001 From: Sean Sullivan Date: Fri, 10 May 2024 06:00:08 -0400 Subject: [PATCH 22/26] bevy_core: Derive useful traits on FrameCount (#13291) # Objective I am emboldened by my last small PR and am here with another. - **Describe the objective or issue this PR addresses.** It would be nice if `FrameCount` could be used by downstream plugins that want to use frame data. The example that I have in mind is [`leafwing_input_playback`](https://github.com/Leafwing-Studios/leafwing_input_playback/issues/29) which has a [duplicate implementation of `FrameCount`](https://github.com/Leafwing-Studios/leafwing_input_playback/blob/main/src/frame_counting.rs#L9-L37) used in several structs which rely on those derives (or otherwise the higher-level structs would have to implement these traits manually). That crate, using `FrameCount`, tracks input frames and timestamps and enables various playback modes. I am aware that bevy org refrains from deriving lots of unnecessary stuff on bevy types to avoid compile time creep. It is worth mentioning the (equally reasonable) alternative that downstream crates _should_ implement some `FrameCount` themselves if they want special behavior from it. ## Solution - **Describe the solution used to achieve the objective above.** I added derives for `PartialEq, Eq, PartialOrd, Ord` and implementations for `serde::{Deserialize, Serialize}` to `FrameCount`. ## Testing Manually confirmed that the serde implementation works, but that's all. Let me know if I should do more here. --------- Co-authored-by: Alice Cecile --- crates/bevy_core/Cargo.toml | 1 + crates/bevy_core/src/lib.rs | 2 +- crates/bevy_core/src/serde.rs | 50 +++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/crates/bevy_core/Cargo.toml b/crates/bevy_core/Cargo.toml index d149581ebb93e..bbf51137c7671 100644 --- a/crates/bevy_core/Cargo.toml +++ b/crates/bevy_core/Cargo.toml @@ -32,6 +32,7 @@ serialize = ["dep:serde"] [dev-dependencies] crossbeam-channel = "0.5.0" +serde_test = "1.0" [lints] workspace = true diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index 073460b3b3796..dc93ddf2c0bef 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -80,7 +80,7 @@ fn tick_global_task_pools(_main_thread_marker: Option>) { /// [`FrameCount`] will wrap to 0 after exceeding [`u32::MAX`]. Within reasonable /// assumptions, one may exploit wrapping arithmetic to determine the number of frames /// that have elapsed between two observations – see [`u32::wrapping_sub()`]. -#[derive(Debug, Default, Resource, Clone, Copy)] +#[derive(Debug, Default, Resource, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct FrameCount(pub u32); /// Adds frame counting functionality to Apps. diff --git a/crates/bevy_core/src/serde.rs b/crates/bevy_core/src/serde.rs index f8835e9a5f8d2..fc4d81b4bd055 100644 --- a/crates/bevy_core/src/serde.rs +++ b/crates/bevy_core/src/serde.rs @@ -9,6 +9,7 @@ use serde::{ }; use super::name::Name; +use super::FrameCount; impl Serialize for Name { fn serialize(&self, serializer: S) -> Result { @@ -39,3 +40,52 @@ impl<'de> Visitor<'de> for EntityVisitor { Ok(Name::new(v)) } } + +// Manually implementing serialize/deserialize allows us to use a more compact representation as simple integers +impl Serialize for FrameCount { + fn serialize(&self, serializer: S) -> Result { + serializer.serialize_u32(self.0) + } +} + +impl<'de> Deserialize<'de> for FrameCount { + fn deserialize>(deserializer: D) -> Result { + deserializer.deserialize_u32(FrameVisitor) + } +} + +struct FrameVisitor; + +impl<'de> Visitor<'de> for FrameVisitor { + type Value = FrameCount; + + fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.write_str(any::type_name::()) + } + + fn visit_u32(self, v: u32) -> Result + where + E: Error, + { + Ok(FrameCount(v)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use serde_test::{assert_tokens, Token}; + + #[test] + fn test_serde_name() { + let name = Name::new("MyComponent"); + assert_tokens(&name, &[Token::String("MyComponent")]); + } + + #[test] + fn test_serde_frame_count() { + let frame_count = FrameCount(100); + assert_tokens(&frame_count, &[Token::U32(100)]); + } +} From dcb8a13b223fbc9425f7af01b3941dd80a229384 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Fri, 10 May 2024 23:15:56 +1000 Subject: [PATCH 23/26] Remove `ClampColor` (#13307) # Objective - Fixes #12543 ## Solution - Removed `ClampColor` ## Testing - CI Passed --- ## Migration Guide Manually clamp the various colour components yourself if this behaviour is still required. ```rust fn clamped_srgba(color: Srgba) -> Srgba { Srgba { red: color.red.clamp(0., 1.), green: color.green.clamp(0., 1.), blue: color.blue.clamp(0., 1.), alpha: color.alpha.clamp(0., 1.), } } ``` --- crates/bevy_animation/src/animatable.rs | 6 ++-- crates/bevy_color/src/color_ops.rs | 17 ----------- crates/bevy_color/src/hsla.rs | 37 +---------------------- crates/bevy_color/src/hsva.rs | 38 +---------------------- crates/bevy_color/src/hwba.rs | 39 +----------------------- crates/bevy_color/src/laba.rs | 39 ++---------------------- crates/bevy_color/src/lcha.rs | 38 +---------------------- crates/bevy_color/src/linear_rgba.rs | 39 ++---------------------- crates/bevy_color/src/oklaba.rs | 40 ++----------------------- crates/bevy_color/src/oklcha.rs | 39 ++---------------------- crates/bevy_color/src/srgba.rs | 39 ++---------------------- crates/bevy_color/src/xyza.rs | 39 ++---------------------- 12 files changed, 19 insertions(+), 391 deletions(-) diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 666485a305d30..4e59ccc8b2875 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,5 +1,5 @@ use crate::util; -use bevy_color::{ClampColor, Laba, LinearRgba, Oklaba, Srgba, Xyza}; +use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza}; use bevy_ecs::world::World; use bevy_math::*; use bevy_reflect::Reflect; @@ -63,7 +63,7 @@ macro_rules! impl_color_animatable { #[inline] fn interpolate(a: &Self, b: &Self, t: f32) -> Self { let value = *a * (1. - t) + *b * t; - value.clamped() + value } #[inline] @@ -76,7 +76,7 @@ macro_rules! impl_color_animatable { value = Self::interpolate(&value, &input.value, input.weight); } } - value.clamped() + value } } }; diff --git a/crates/bevy_color/src/color_ops.rs b/crates/bevy_color/src/color_ops.rs index 0441d11e9c0e3..e37592bdd4dba 100644 --- a/crates/bevy_color/src/color_ops.rs +++ b/crates/bevy_color/src/color_ops.rs @@ -82,23 +82,6 @@ pub trait Hue: Sized { } } -/// Trait with methods for asserting a colorspace is within bounds. -/// -/// During ordinary usage (e.g. reading images from disk, rendering images, picking colors for UI), colors should always be within their ordinary bounds (such as 0 to 1 for RGB colors). -/// However, some applications, such as high dynamic range rendering or bloom rely on unbounded colors to naturally represent a wider array of choices. -pub trait ClampColor: Sized { - /// Return a new version of this color clamped, with all fields in bounds. - fn clamped(&self) -> Self; - - /// Changes all the fields of this color to ensure they are within bounds. - fn clamp(&mut self) { - *self = self.clamped(); - } - - /// Are all the fields of this color in bounds? - fn is_within_bounds(&self) -> bool; -} - /// Trait with methods for converting colors to non-color types pub trait ColorToComponents { /// Convert to an f32 array diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index 0d0531750a4d0..66aface9b4408 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -1,5 +1,5 @@ use crate::{ - Alpha, ClampColor, ColorToComponents, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, + Alpha, ColorToComponents, Hsva, Hue, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, }; use bevy_math::{Vec3, Vec4}; @@ -178,24 +178,6 @@ impl Luminance for Hsla { } } -impl ClampColor for Hsla { - fn clamped(&self) -> Self { - Self { - hue: self.hue.rem_euclid(360.), - saturation: self.saturation.clamp(0., 1.), - lightness: self.lightness.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.saturation) - && (0. ..=1.).contains(&self.lightness) - && (0. ..=1.).contains(&self.alpha) - } -} - impl ColorToComponents for Hsla { fn to_f32_array(self) -> [f32; 4] { [self.hue, self.saturation, self.lightness, self.alpha] @@ -440,21 +422,4 @@ mod tests { assert_approx_eq!(color.hue, reference.hue, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Hsla::hsl(361., 2., -1.); - let color_2 = Hsla::hsl(250.2762, 1., 0.67); - let mut color_3 = Hsla::hsl(-50., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Hsla::hsl(1., 1., 0.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Hsla::hsl(310., 1., 1.)); - } } diff --git a/crates/bevy_color/src/hsva.rs b/crates/bevy_color/src/hsva.rs index 760cad64192cc..a66ca4c43bc91 100644 --- a/crates/bevy_color/src/hsva.rs +++ b/crates/bevy_color/src/hsva.rs @@ -1,6 +1,5 @@ use crate::{ - Alpha, ClampColor, ColorToComponents, Hue, Hwba, Lcha, LinearRgba, Mix, Srgba, StandardColor, - Xyza, + Alpha, ColorToComponents, Hue, Hwba, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza, }; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -124,24 +123,6 @@ impl Hue for Hsva { } } -impl ClampColor for Hsva { - fn clamped(&self) -> Self { - Self { - hue: self.hue.rem_euclid(360.), - saturation: self.saturation.clamp(0., 1.), - value: self.value.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.saturation) - && (0. ..=1.).contains(&self.value) - && (0. ..=1.).contains(&self.alpha) - } -} - impl From for Hwba { fn from( Hsva { @@ -316,21 +297,4 @@ mod tests { assert_approx_eq!(color.hsv.alpha, hsv2.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Hsva::hsv(361., 2., -1.); - let color_2 = Hsva::hsv(250.2762, 1., 0.67); - let mut color_3 = Hsva::hsv(-50., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Hsva::hsv(1., 1., 0.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Hsva::hsv(310., 1., 1.)); - } } diff --git a/crates/bevy_color/src/hwba.rs b/crates/bevy_color/src/hwba.rs index 65b6b67e09a40..0f6f9a6b568b1 100644 --- a/crates/bevy_color/src/hwba.rs +++ b/crates/bevy_color/src/hwba.rs @@ -2,9 +2,7 @@ //! in [_HWB - A More Intuitive Hue-Based Color Model_] by _Smith et al_. //! //! [_HWB - A More Intuitive Hue-Based Color Model_]: https://web.archive.org/web/20240226005220/http://alvyray.com/Papers/CG/HWB_JGTv208.pdf -use crate::{ - Alpha, ClampColor, ColorToComponents, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza, -}; +use crate::{Alpha, ColorToComponents, Hue, Lcha, LinearRgba, Mix, Srgba, StandardColor, Xyza}; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -127,24 +125,6 @@ impl Hue for Hwba { } } -impl ClampColor for Hwba { - fn clamped(&self) -> Self { - Self { - hue: self.hue.rem_euclid(360.), - whiteness: self.whiteness.clamp(0., 1.), - blackness: self.blackness.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.whiteness) - && (0. ..=1.).contains(&self.blackness) - && (0. ..=1.).contains(&self.alpha) - } -} - impl ColorToComponents for Hwba { fn to_f32_array(self) -> [f32; 4] { [self.hue, self.whiteness, self.blackness, self.alpha] @@ -348,21 +328,4 @@ mod tests { assert_approx_eq!(color.hwb.alpha, hwb2.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Hwba::hwb(361., 2., -1.); - let color_2 = Hwba::hwb(250.2762, 1., 0.67); - let mut color_3 = Hwba::hwb(-50., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Hwba::hwb(1., 1., 0.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Hwba::hwb(310., 1., 1.)); - } } diff --git a/crates/bevy_color/src/laba.rs b/crates/bevy_color/src/laba.rs index cd0684e16f5ce..8e35336335370 100644 --- a/crates/bevy_color/src/laba.rs +++ b/crates/bevy_color/src/laba.rs @@ -1,6 +1,6 @@ use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, ColorToComponents, Hsla, Hsva, Hwba, - LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + impl_componentwise_vector_space, Alpha, ColorToComponents, Hsla, Hsva, Hwba, LinearRgba, + Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, }; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -118,24 +118,6 @@ impl Alpha for Laba { } } -impl ClampColor for Laba { - fn clamped(&self) -> Self { - Self { - lightness: self.lightness.clamp(0., 1.5), - a: self.a.clamp(-1.5, 1.5), - b: self.b.clamp(-1.5, 1.5), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.5).contains(&self.lightness) - && (-1.5..=1.5).contains(&self.a) - && (-1.5..=1.5).contains(&self.b) - && (0. ..=1.).contains(&self.alpha) - } -} - impl Luminance for Laba { #[inline] fn with_luminance(&self, lightness: f32) -> Self { @@ -432,21 +414,4 @@ mod tests { assert_approx_eq!(color.lab.alpha, laba.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Laba::lab(-1., 2., -2.); - let color_2 = Laba::lab(1., 1.5, -1.2); - let mut color_3 = Laba::lab(-0.4, 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Laba::lab(0., 1.5, -1.5)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Laba::lab(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index 437ca5d6d7d47..4058ef4d4be63 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -1,6 +1,5 @@ use crate::{ - Alpha, ClampColor, ColorToComponents, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, - StandardColor, Xyza, + Alpha, ColorToComponents, Hue, Laba, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, }; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -186,24 +185,6 @@ impl Luminance for Lcha { } } -impl ClampColor for Lcha { - fn clamped(&self) -> Self { - Self { - lightness: self.lightness.clamp(0., 1.5), - chroma: self.chroma.clamp(0., 1.5), - hue: self.hue.rem_euclid(360.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.5).contains(&self.lightness) - && (0. ..=1.5).contains(&self.chroma) - && (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.alpha) - } -} - impl ColorToComponents for Lcha { fn to_f32_array(self) -> [f32; 4] { [self.lightness, self.chroma, self.hue, self.alpha] @@ -404,21 +385,4 @@ mod tests { assert_approx_eq!(color.lch.alpha, lcha.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Lcha::lch(-1., 2., 400.); - let color_2 = Lcha::lch(1., 1.5, 249.54); - let mut color_3 = Lcha::lch(-0.4, 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Lcha::lch(0., 1.5, 40.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Lcha::lch(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/linear_rgba.rs b/crates/bevy_color/src/linear_rgba.rs index 0ef33ce37eedc..9306b8927b352 100644 --- a/crates/bevy_color/src/linear_rgba.rs +++ b/crates/bevy_color/src/linear_rgba.rs @@ -1,6 +1,6 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, - ColorToComponents, Luminance, Mix, StandardColor, + color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents, + Luminance, Mix, StandardColor, }; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -263,24 +263,6 @@ impl EuclideanDistance for LinearRgba { } } -impl ClampColor for LinearRgba { - fn clamped(&self) -> Self { - Self { - red: self.red.clamp(0., 1.), - green: self.green.clamp(0., 1.), - blue: self.blue.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.red) - && (0. ..=1.).contains(&self.green) - && (0. ..=1.).contains(&self.blue) - && (0. ..=1.).contains(&self.alpha) - } -} - impl ColorToComponents for LinearRgba { fn to_f32_array(self) -> [f32; 4] { [self.red, self.green, self.blue, self.alpha] @@ -455,21 +437,4 @@ mod tests { let twice_as_light = color.lighter(0.2); assert!(lighter2.distance_squared(&twice_as_light) < 0.0001); } - - #[test] - fn test_clamp() { - let color_1 = LinearRgba::rgb(2., -1., 0.4); - let color_2 = LinearRgba::rgb(0.031, 0.749, 1.); - let mut color_3 = LinearRgba::rgb(-1., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), LinearRgba::rgb(1., 0., 0.4)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, LinearRgba::rgb(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/oklaba.rs b/crates/bevy_color/src/oklaba.rs index 46e23ff08795b..1f4785d070c02 100644 --- a/crates/bevy_color/src/oklaba.rs +++ b/crates/bevy_color/src/oklaba.rs @@ -1,7 +1,6 @@ use crate::{ - color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ClampColor, - ColorToComponents, Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, - Xyza, + color_difference::EuclideanDistance, impl_componentwise_vector_space, Alpha, ColorToComponents, + Hsla, Hsva, Hwba, Lcha, LinearRgba, Luminance, Mix, Srgba, StandardColor, Xyza, }; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -157,24 +156,6 @@ impl EuclideanDistance for Oklaba { } } -impl ClampColor for Oklaba { - fn clamped(&self) -> Self { - Self { - lightness: self.lightness.clamp(0., 1.), - a: self.a.clamp(-1., 1.), - b: self.b.clamp(-1., 1.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.lightness) - && (-1. ..=1.).contains(&self.a) - && (-1. ..=1.).contains(&self.b) - && (0. ..=1.).contains(&self.alpha) - } -} - impl ColorToComponents for Oklaba { fn to_f32_array(self) -> [f32; 4] { [self.lightness, self.a, self.b, self.alpha] @@ -406,21 +387,4 @@ mod tests { assert_approx_eq!(oklaba.b, oklaba2.b, 0.001); assert_approx_eq!(oklaba.alpha, oklaba2.alpha, 0.001); } - - #[test] - fn test_clamp() { - let color_1 = Oklaba::lab(-1., 2., -2.); - let color_2 = Oklaba::lab(1., 0.42, -0.4); - let mut color_3 = Oklaba::lab(-0.4, 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Oklaba::lab(0., 1., -1.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Oklaba::lab(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 3e748a1c945a6..18e6f532887ff 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -1,6 +1,6 @@ use crate::{ - color_difference::EuclideanDistance, Alpha, ClampColor, ColorToComponents, Hsla, Hsva, Hue, - Hwba, Laba, Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, + color_difference::EuclideanDistance, Alpha, ColorToComponents, Hsla, Hsva, Hue, Hwba, Laba, + Lcha, LinearRgba, Luminance, Mix, Oklaba, Srgba, StandardColor, Xyza, }; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -280,24 +280,6 @@ impl From for Oklaba { } } -impl ClampColor for Oklcha { - fn clamped(&self) -> Self { - Self { - lightness: self.lightness.clamp(0., 1.), - chroma: self.chroma.clamp(0., 1.), - hue: self.hue.rem_euclid(360.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.lightness) - && (0. ..=1.).contains(&self.chroma) - && (0. ..=360.).contains(&self.hue) - && (0. ..=1.).contains(&self.alpha) - } -} - // Derived Conversions impl From for Oklcha { @@ -444,21 +426,4 @@ mod tests { assert_approx_eq!(oklcha.hue, oklcha2.hue, 0.001); assert_approx_eq!(oklcha.alpha, oklcha2.alpha, 0.001); } - - #[test] - fn test_clamp() { - let color_1 = Oklcha::lch(-1., 2., 400.); - let color_2 = Oklcha::lch(1., 1., 249.54); - let mut color_3 = Oklcha::lch(-0.4, 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Oklcha::lch(0., 1., 40.)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Oklcha::lch(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/srgba.rs b/crates/bevy_color/src/srgba.rs index def49cfca31d4..0a4411aa6bceb 100644 --- a/crates/bevy_color/src/srgba.rs +++ b/crates/bevy_color/src/srgba.rs @@ -1,7 +1,7 @@ use crate::color_difference::EuclideanDistance; use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, ColorToComponents, LinearRgba, Luminance, - Mix, StandardColor, Xyza, + impl_componentwise_vector_space, Alpha, ColorToComponents, LinearRgba, Luminance, Mix, + StandardColor, Xyza, }; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -314,24 +314,6 @@ impl EuclideanDistance for Srgba { } } -impl ClampColor for Srgba { - fn clamped(&self) -> Self { - Self { - red: self.red.clamp(0., 1.), - green: self.green.clamp(0., 1.), - blue: self.blue.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.red) - && (0. ..=1.).contains(&self.green) - && (0. ..=1.).contains(&self.blue) - && (0. ..=1.).contains(&self.alpha) - } -} - impl ColorToComponents for Srgba { fn to_f32_array(self) -> [f32; 4] { [self.red, self.green, self.blue, self.alpha] @@ -515,21 +497,4 @@ mod tests { assert!(matches!(Srgba::hex("yyy"), Err(HexColorError::Parse(_)))); assert!(matches!(Srgba::hex("##fff"), Err(HexColorError::Parse(_)))); } - - #[test] - fn test_clamp() { - let color_1 = Srgba::rgb(2., -1., 0.4); - let color_2 = Srgba::rgb(0.031, 0.749, 1.); - let mut color_3 = Srgba::rgb(-1., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Srgba::rgb(1., 0., 0.4)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Srgba::rgb(0., 1., 1.)); - } } diff --git a/crates/bevy_color/src/xyza.rs b/crates/bevy_color/src/xyza.rs index 6d46c85c92c45..6929ca5ca5799 100644 --- a/crates/bevy_color/src/xyza.rs +++ b/crates/bevy_color/src/xyza.rs @@ -1,6 +1,6 @@ use crate::{ - impl_componentwise_vector_space, Alpha, ClampColor, ColorToComponents, LinearRgba, Luminance, - Mix, StandardColor, + impl_componentwise_vector_space, Alpha, ColorToComponents, LinearRgba, Luminance, Mix, + StandardColor, }; use bevy_math::{Vec3, Vec4}; use bevy_reflect::prelude::*; @@ -144,24 +144,6 @@ impl Mix for Xyza { } } -impl ClampColor for Xyza { - fn clamped(&self) -> Self { - Self { - x: self.x.clamp(0., 1.), - y: self.y.clamp(0., 1.), - z: self.z.clamp(0., 1.), - alpha: self.alpha.clamp(0., 1.), - } - } - - fn is_within_bounds(&self) -> bool { - (0. ..=1.).contains(&self.x) - && (0. ..=1.).contains(&self.y) - && (0. ..=1.).contains(&self.z) - && (0. ..=1.).contains(&self.alpha) - } -} - impl ColorToComponents for Xyza { fn to_f32_array(self) -> [f32; 4] { [self.x, self.y, self.z, self.alpha] @@ -290,21 +272,4 @@ mod tests { assert_approx_eq!(color.xyz.alpha, xyz2.alpha, 0.001); } } - - #[test] - fn test_clamp() { - let color_1 = Xyza::xyz(2., -1., 0.4); - let color_2 = Xyza::xyz(0.031, 0.749, 1.); - let mut color_3 = Xyza::xyz(-1., 1., 1.); - - assert!(!color_1.is_within_bounds()); - assert_eq!(color_1.clamped(), Xyza::xyz(1., 0., 0.4)); - - assert!(color_2.is_within_bounds()); - assert_eq!(color_2, color_2.clamped()); - - color_3.clamp(); - assert!(color_3.is_within_bounds()); - assert_eq!(color_3, Xyza::xyz(0., 1., 1.)); - } } From 443ce9a62b6bb0136c192a6fe46e4c254a895e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Sat, 11 May 2024 21:32:31 +0200 Subject: [PATCH 24/26] gizmos: take normal of normal on plane 3d before rotation (#13326) # Objective - Example `render_primitives` is painful to look at for the plane in 3d, the gizmo seems to have extra rotations https://github.com/bevyengine/bevy/assets/8672791/08509624-14ac-4f00-a758-9a14233ef1a9 ## Solution - Take the normal of the normal before rotation, then rotate it ## Testing - Run the example, rotations are more in sync https://github.com/bevyengine/bevy/assets/8672791/91c26ce4-4b7b-4575-ba32-7c32026e4596 --- crates/bevy_gizmos/src/primitives/dim3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index 66924fbb3f007..8af28f396a675 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -245,7 +245,7 @@ where let normal = self.rotation * *self.normal; self.gizmos .primitive_3d(self.normal, self.position, self.rotation, self.color); - let normals_normal = normal.any_orthonormal_vector(); + let normals_normal = self.rotation * self.normal.any_orthonormal_vector(); // draws the axes // get rotation for each direction From 278380394f29332f4dee2dae4f1ad41500ccdee9 Mon Sep 17 00:00:00 2001 From: rmsthebest <11652273+rmsthebest@users.noreply.github.com> Date: Sun, 12 May 2024 17:01:05 +0200 Subject: [PATCH 25/26] Avoid bevy_reflect::List::iter wrapping in release mode (#13271) # Objective Fixes #13230 ## Solution Uses solution described in #13230 They mention a worry about adding a branch, but I'm not sure there is one. This code ```Rust #[no_mangle] pub fn next_if_some(num: i32, b: Option) -> i32 { num + b.is_some() as i32 } ``` produces this assembly with opt-level 3 ```asm next_if_some: xor eax, eax cmp sil, 2 setne al add eax, edi ret ``` ## Testing Added test from #13230, tagged it as ignore as it is only useful in release mode and very slow if you accidentally invoke it in debug mode. --- ## Changelog Iterationg of ListIter will no longer overflow and wrap around ## Migration Guide --- crates/bevy_reflect/src/array.rs | 31 ++++++++++- crates/bevy_reflect/src/enums/enum_trait.rs | 57 ++++++++++++++++++++- crates/bevy_reflect/src/list.rs | 28 +++++++++- crates/bevy_reflect/src/map.rs | 25 ++++++++- crates/bevy_reflect/src/struct_trait.rs | 30 ++++++++++- crates/bevy_reflect/src/tuple.rs | 23 ++++++++- crates/bevy_reflect/src/tuple_struct.rs | 25 ++++++++- 7 files changed, 212 insertions(+), 7 deletions(-) diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index c49fddc2204d4..f5f1158c20f11 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -378,7 +378,7 @@ impl<'a> Iterator for ArrayIter<'a> { #[inline] fn next(&mut self) -> Option { let value = self.array.get(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -503,3 +503,32 @@ pub fn array_debug(dyn_array: &dyn Array, f: &mut std::fmt::Formatter<'_>) -> st } debug.finish() } +#[cfg(test)] +mod tests { + use crate::{Reflect, ReflectRef}; + #[test] + fn next_index_increment() { + const SIZE: usize = if cfg!(debug_assertions) { + 4 + } else { + // If compiled in release mode, verify we dont overflow + usize::MAX + }; + + let b = Box::new([(); SIZE]).into_reflect(); + + let ReflectRef::Array(array) = b.reflect_ref() else { + panic!("Not an array..."); + }; + + let mut iter = array.iter(); + iter.index = SIZE - 1; + assert!(iter.next().is_some()); + + // When None we should no longer increase index + assert!(iter.next().is_none()); + assert!(iter.index == SIZE); + assert!(iter.next().is_none()); + assert!(iter.index == SIZE); + } +} diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index a74194505181e..66029923da25b 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -280,7 +280,7 @@ impl<'a> Iterator for VariantFieldIter<'a> { Some(VariantField::Struct(name, self.container.field(name)?)) } }; - self.index += 1; + self.index += value.is_some() as usize; value } @@ -312,3 +312,58 @@ impl<'a> VariantField<'a> { } } } + +// Tests that need access to internal fields have to go here rather than in mod.rs +#[cfg(test)] +mod tests { + use crate as bevy_reflect; + use crate::*; + + #[derive(Reflect, Debug, PartialEq)] + enum MyEnum { + A, + B(usize, i32), + C { foo: f32, bar: bool }, + } + #[test] + fn next_index_increment() { + // unit enums always return none, so index should stay at 0 + let unit_enum = MyEnum::A; + let mut iter = unit_enum.iter_fields(); + let size = iter.len(); + for _ in 0..2 { + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } + // tuple enums we iter over each value (unnamed fields), stop after that + let tuple_enum = MyEnum::B(0, 1); + let mut iter = tuple_enum.iter_fields(); + let size = iter.len(); + for _ in 0..2 { + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + } + for _ in 0..2 { + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } + + // struct enums, we iterate over each field in the struct + let struct_enum = MyEnum::C { + foo: 0., + bar: false, + }; + let mut iter = struct_enum.iter_fields(); + let size = iter.len(); + for _ in 0..2 { + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + } + for _ in 0..2 { + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } + } +} diff --git a/crates/bevy_reflect/src/list.rs b/crates/bevy_reflect/src/list.rs index e01709b30db25..5b786ee76a991 100644 --- a/crates/bevy_reflect/src/list.rs +++ b/crates/bevy_reflect/src/list.rs @@ -405,7 +405,7 @@ impl<'a> Iterator for ListIter<'a> { #[inline] fn next(&mut self) -> Option { let value = self.list.get(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -533,6 +533,7 @@ pub fn list_debug(dyn_list: &dyn List, f: &mut Formatter<'_>) -> std::fmt::Resul #[cfg(test)] mod tests { use super::DynamicList; + use crate::{Reflect, ReflectRef}; use std::assert_eq; #[test] @@ -547,4 +548,29 @@ mod tests { assert_eq!(index, value); } } + + #[test] + fn next_index_increment() { + const SIZE: usize = if cfg!(debug_assertions) { + 4 + } else { + // If compiled in release mode, verify we dont overflow + usize::MAX + }; + let b = Box::new(vec![(); SIZE]).into_reflect(); + + let ReflectRef::List(list) = b.reflect_ref() else { + panic!("Not a list..."); + }; + + let mut iter = list.iter(); + iter.index = SIZE - 1; + assert!(iter.next().is_some()); + + // When None we should no longer increase index + assert!(iter.next().is_none()); + assert!(iter.index == SIZE); + assert!(iter.next().is_none()); + assert!(iter.index == SIZE); + } } diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 085eda219e844..5b182c68a973a 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -417,7 +417,7 @@ impl<'a> Iterator for MapIter<'a> { fn next(&mut self) -> Option { let value = self.map.get_at(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -618,4 +618,27 @@ mod tests { assert!(map.get_at(2).is_none()); } + + #[test] + fn next_index_increment() { + let values = ["first", "last"]; + let mut map = DynamicMap::default(); + map.insert(0usize, values[0]); + map.insert(1usize, values[1]); + + let mut iter = map.iter(); + let size = iter.len(); + + for _ in 0..2 { + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + } + + // When None we should no longer increase index + for _ in 0..2 { + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } + } } diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index b342ea92a6152..5d134034691d6 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -204,7 +204,7 @@ impl<'a> Iterator for FieldIter<'a> { fn next(&mut self) -> Option { let value = self.struct_val.field_at(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -562,3 +562,31 @@ pub fn struct_debug(dyn_struct: &dyn Struct, f: &mut Formatter<'_>) -> std::fmt: } debug.finish() } + +#[cfg(test)] +mod tests { + use crate as bevy_reflect; + use crate::*; + #[derive(Reflect, Default)] + struct MyStruct { + a: (), + b: (), + c: (), + } + #[test] + fn next_index_increment() { + let my_struct = MyStruct::default(); + let mut iter = my_struct.iter_fields(); + iter.index = iter.len() - 1; + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + + // When None we should no longer increase index + let prev_index = iter.index; + assert!(iter.next().is_none()); + assert_eq!(prev_index, iter.index); + assert!(iter.next().is_none()); + assert_eq!(prev_index, iter.index); + } +} diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 50eb9512f96ce..cf111edcdfb99 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -75,7 +75,7 @@ impl<'a> Iterator for TupleFieldIter<'a> { fn next(&mut self) -> Option { let value = self.tuple.field(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -709,3 +709,24 @@ macro_rules! impl_type_path_tuple { } all_tuples!(impl_type_path_tuple, 0, 12, P); + +#[cfg(test)] +mod tests { + use super::Tuple; + + #[test] + fn next_index_increment() { + let mut iter = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).iter_fields(); + let size = iter.len(); + iter.index = size - 1; + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + + // When None we should no longer increase index + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } +} diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index b6cb0b720cc0b..8aeb103984029 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -155,7 +155,7 @@ impl<'a> Iterator for TupleStructFieldIter<'a> { fn next(&mut self) -> Option { let value = self.tuple_struct.field(self.index); - self.index += 1; + self.index += value.is_some() as usize; value } @@ -475,3 +475,26 @@ pub fn tuple_struct_debug( } debug.finish() } + +#[cfg(test)] +mod tests { + use crate as bevy_reflect; + use crate::*; + #[derive(Reflect)] + struct Ts(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8); + #[test] + fn next_index_increment() { + let mut iter = Ts(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11).iter_fields(); + let size = iter.len(); + iter.index = size - 1; + let prev_index = iter.index; + assert!(iter.next().is_some()); + assert_eq!(prev_index, iter.index - 1); + + // When None we should no longer increase index + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + assert!(iter.next().is_none()); + assert_eq!(size, iter.index); + } +} From dc0fdd6ad980191645acd182efc18661cec5911d Mon Sep 17 00:00:00 2001 From: charlotte Date: Sun, 12 May 2024 08:56:01 -0700 Subject: [PATCH 26/26] Ensure clean exit (#13236) # Objective Fixes two issues related to #13208. First, we ensure render resources for a window are always dropped first to ensure that the `winit::Window` always drops on the main thread when it is removed from `WinitWindows`. Previously, changes in #12978 caused the window to drop in the render world, causing issues. We accomplish this by delaying despawning the window by a frame by inserting a marker component `ClosingWindow` that indicates the window has been requested to close and is in the process of closing. The render world now responds to the equivalent `WindowClosing` event rather than `WindowCloseed` which now fires after the render resources are guarunteed to be cleaned up. Secondly, fixing the above caused (revealed?) that additional events were being delivered to the the event loop handler after exit had already been requested: in my testing `RedrawRequested` and `LoopExiting`. This caused errors to be reported try to send an exit event on the close channel. There are two options here: - Guard the handler so no additional events are delivered once the app is exiting. I ~considered this but worried it might be confusing or bug prone if in the future someone wants to handle `LoopExiting` or some other event to clean-up while exiting.~ We are now taking this approach. - Only send an exit signal if we are not already exiting. ~It doesn't appear to cause any problems to handle the extra events so this seems safer.~ Fixing this also appears to have fixed #13231. Fixes #10260. ## Testing Tested on mac only. --- ## Changelog ### Added - A `WindowClosing` event has been added that indicates the window will be despawned on the next frame. ### Changed - Windows now close a frame after their exit has been requested. ## Migration Guide - Ensure custom exit logic does not rely on the app exiting the same frame as a window is closed. --- crates/bevy_render/src/view/window/mod.rs | 10 +++++----- crates/bevy_window/src/event.rs | 14 ++++++++++++++ crates/bevy_window/src/lib.rs | 2 ++ crates/bevy_window/src/system.rs | 15 ++++++++++++--- crates/bevy_window/src/window.rs | 5 +++++ crates/bevy_winit/src/lib.rs | 5 +++++ crates/bevy_winit/src/system.rs | 16 ++++++++++++---- 7 files changed, 55 insertions(+), 12 deletions(-) diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 762771a41edd5..cf347586e6429 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -12,7 +12,7 @@ use bevy_ecs::{entity::EntityHashMap, prelude::*}; use bevy_utils::warn_once; use bevy_utils::{default, tracing::debug, HashSet}; use bevy_window::{ - CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed, + CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosing, }; use std::{ num::NonZeroU32, @@ -117,7 +117,7 @@ impl DerefMut for ExtractedWindows { fn extract_windows( mut extracted_windows: ResMut, screenshot_manager: Extract>, - mut closed: Extract>, + mut closing: Extract>, windows: Extract)>>, mut removed: Extract>, mut window_surfaces: ResMut, @@ -177,9 +177,9 @@ fn extract_windows( } } - for closed_window in closed.read() { - extracted_windows.remove(&closed_window.window); - window_surfaces.remove(&closed_window.window); + for closing_window in closing.read() { + extracted_windows.remove(&closing_window.window); + window_surfaces.remove(&closing_window.window); } for removed_window in removed.read() { extracted_windows.remove(&removed_window); diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 039b646b8f94a..9bc698acaed72 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -94,6 +94,20 @@ pub struct WindowClosed { pub window: Entity, } +/// An event that is sent whenever a window is closing. This will be sent when +/// after a [`WindowCloseRequested`] event is received and the window is in the process of closing. +#[derive(Event, Debug, Clone, PartialEq, Eq, Reflect)] +#[reflect(Debug, PartialEq)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct WindowClosing { + /// Window that has been requested to close and is the process of closing. + pub window: Entity, +} + /// An event that is sent whenever a window is destroyed by the underlying window system. /// /// Note that if your application only has a single window, this event may be your last chance to diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index cabdb04d1818d..7ecf8d21c0e02 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -89,6 +89,7 @@ impl Plugin for WindowPlugin { #[allow(deprecated)] app.add_event::() .add_event::() + .add_event::() .add_event::() .add_event::() .add_event::() @@ -139,6 +140,7 @@ impl Plugin for WindowPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_window/src/system.rs b/crates/bevy_window/src/system.rs index ac92c77408e71..d6bb40bfefdc6 100644 --- a/crates/bevy_window/src/system.rs +++ b/crates/bevy_window/src/system.rs @@ -1,4 +1,4 @@ -use crate::{PrimaryWindow, Window, WindowCloseRequested}; +use crate::{ClosingWindow, PrimaryWindow, Window, WindowCloseRequested}; use bevy_app::AppExit; use bevy_ecs::prelude::*; @@ -39,8 +39,17 @@ pub fn exit_on_primary_closed( /// Ensure that you read the caveats documented on that field if doing so. /// /// [`WindowPlugin`]: crate::WindowPlugin -pub fn close_when_requested(mut commands: Commands, mut closed: EventReader) { +pub fn close_when_requested( + mut commands: Commands, + mut closed: EventReader, + closing: Query>, +) { + // This was inserted by us on the last frame so now we can despawn the window + for window in closing.iter() { + commands.entity(window).despawn(); + } + // Mark the window as closing so we can despawn it on the next frame for event in closed.read() { - commands.entity(event.window).despawn(); + commands.entity(event.window).insert(ClosingWindow); } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index d80f070a1657b..453c9245b58d8 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -1171,6 +1171,11 @@ impl Default for EnabledButtons { } } +/// Marker component for a [`Window`] that has been requested to close and +/// is in the process of closing (on the next frame). +#[derive(Component)] +pub struct ClosingWindow; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 69008613259d7..60794135e39aa 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -302,6 +302,11 @@ pub fn winit_runner(mut app: App) -> AppExit { let mut winit_events = Vec::default(); // set up the event loop let event_handler = move |event, event_loop: &EventLoopWindowTarget| { + // The event loop is in the process of exiting, so don't deliver any new events + if event_loop.exiting() { + return; + } + handle_winit_event( &mut app, &mut runner_state, diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 82327aaf6a60f..d871c0fb6e207 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -8,7 +8,8 @@ use bevy_ecs::{ }; use bevy_utils::tracing::{error, info, warn}; use bevy_window::{ - RawHandleWrapper, Window, WindowClosed, WindowCreated, WindowMode, WindowResized, + ClosingWindow, RawHandleWrapper, Window, WindowClosed, WindowClosing, WindowCreated, + WindowMode, WindowResized, }; use winit::{ @@ -16,6 +17,7 @@ use winit::{ event_loop::EventLoopWindowTarget, }; +use bevy_ecs::query::With; #[cfg(target_arch = "wasm32")] use winit::platform::web::WindowExtWebSys; @@ -97,18 +99,24 @@ pub fn create_windows( } pub(crate) fn despawn_windows( + closing: Query>, mut closed: RemovedComponents, window_entities: Query<&Window>, - mut close_events: EventWriter, + mut closing_events: EventWriter, + mut closed_events: EventWriter, mut winit_windows: NonSendMut, ) { + for window in closing.iter() { + closing_events.send(WindowClosing { window }); + } for window in closed.read() { info!("Closing window {:?}", window); // Guard to verify that the window is in fact actually gone, - // rather than having the component added and removed in the same frame. + // rather than having the component added + // and removed in the same frame. if !window_entities.contains(window) { winit_windows.remove_window(window); - close_events.send(WindowClosed { window }); + closed_events.send(WindowClosed { window }); } } }