diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index 004aba0a6fc0a..38d0a07c3d8a3 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -74,6 +74,14 @@ pub struct RunFixedUpdateLoop; #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct FixedUpdate; +/// Indicates that fixed updates are currently running. +/// This is automatically set by `bevy_time`. +#[derive(Resource)] +pub struct FixedUpdateScheduleIsCurrentlyRunning { + /// Sequentially increasing with each fixed update. + pub update: u64, +} + /// The schedule that contains app logic. /// This is run by the [`Main`] schedule. #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml index 4a40902afae7b..39a71fdcc4ab4 100644 --- a/crates/bevy_gizmos/Cargo.toml +++ b/crates/bevy_gizmos/Cargo.toml @@ -16,6 +16,7 @@ webgl = [] bevy_pbr = { path = "../bevy_pbr", version = "0.12.0-dev", optional = true } bevy_sprite = { path = "../bevy_sprite", version = "0.12.0-dev", optional = true } bevy_app = { path = "../bevy_app", version = "0.12.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.12.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.12.0-dev" } bevy_math = { path = "../bevy_math", version = "0.12.0-dev" } bevy_asset = { path = "../bevy_asset", version = "0.12.0-dev" } diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index c2258bc0673f3..29175afa8f40a 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -2,13 +2,17 @@ use std::{f32::consts::TAU, iter}; +use bevy_app::FixedUpdateScheduleIsCurrentlyRunning; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - system::{Deferred, Resource, SystemBuffer, SystemMeta, SystemParam}, - world::World, + component::Tick, + system::{Resource, SystemMeta, SystemParam}, + world::{unsafe_world_cell::UnsafeWorldCell, World}, }; use bevy_math::{Mat2, Quat, Vec2, Vec3}; use bevy_render::color::Color; use bevy_transform::TransformPoint; +use bevy_utils::default; type PositionItem = [f32; 3]; type ColorItem = [f32; 4]; @@ -16,6 +20,13 @@ type ColorItem = [f32; 4]; const DEFAULT_CIRCLE_SEGMENTS: usize = 32; #[derive(Resource, Default)] +pub(crate) struct GizmoStorages { + pub frame: GizmoStorage, + pub fixed_update_tick: u64, + pub fixed_update: GizmoStorage, +} + +#[derive(Default)] pub(crate) struct GizmoStorage { pub list_positions: Vec, pub list_colors: Vec, @@ -28,30 +39,82 @@ pub(crate) struct GizmoStorage { /// They are drawn in immediate mode, which means they will be rendered only for /// the frames in which they are spawned. /// Gizmos should be spawned before the [`Last`](bevy_app::Last) schedule to ensure they are drawn. -#[derive(SystemParam)] +#[derive(Deref, DerefMut)] pub struct Gizmos<'s> { - buffer: Deferred<'s, GizmoBuffer>, + buffer: &'s mut GizmoBuffer, } +/// Buffer in which gizmos are recorded, usually accessed via the [`Gizmos SystemParam`](Gizmos). #[derive(Default)] -struct GizmoBuffer { +pub struct GizmoBuffer { list_positions: Vec, list_colors: Vec, strip_positions: Vec, strip_colors: Vec, } -impl SystemBuffer for GizmoBuffer { - fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) { - let mut storage = world.resource_mut::(); - storage.list_positions.append(&mut self.list_positions); - storage.list_colors.append(&mut self.list_colors); - storage.strip_positions.append(&mut self.strip_positions); - storage.strip_colors.append(&mut self.strip_colors); +// Wrap to keep State hidden +const _: () = { + #[derive(Default)] + pub struct State { + buffer: GizmoBuffer, + /// Which fixed update tick this belongs to, `None` if this isn't from a fixed update. + fixed_time_update: Option, } -} -impl<'s> Gizmos<'s> { + // SAFETY: Only local state is accessed. + unsafe impl SystemParam for Gizmos<'_> { + type State = State; + type Item<'w, 's> = Gizmos<'s>; + + fn init_state(_: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + default() + } + + fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { + let mut storages = world.resource_mut::(); + + let storage = if let Some(tick) = state.fixed_time_update { + // If a new fixed update has begun, clear gizmos from previous fixed update + if storages.fixed_update_tick < tick { + storages.fixed_update_tick = tick; + storages.fixed_update.list_positions.clear(); + storages.fixed_update.list_colors.clear(); + storages.fixed_update.strip_positions.clear(); + storages.fixed_update.strip_colors.clear(); + } + &mut storages.fixed_update + } else { + &mut storages.frame + }; + + storage + .list_positions + .append(&mut state.buffer.list_positions); + storage.list_colors.append(&mut state.buffer.list_colors); + storage + .strip_positions + .append(&mut state.buffer.strip_positions); + storage.strip_colors.append(&mut state.buffer.strip_colors); + } + + unsafe fn get_param<'w, 's>( + state: &'s mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + _change_tick: Tick, + ) -> Self::Item<'w, 's> { + state.fixed_time_update = world + .get_resource::() + .map(|current| current.update); + Gizmos { + buffer: &mut state.buffer, + } + } + } +}; + +impl GizmoBuffer { /// Draw a line in 3D from `start` to `end`. /// /// This should be called for each frame the line needs to be rendered. @@ -153,11 +216,10 @@ impl<'s> Gizmos<'s> { #[inline] pub fn linestrip(&mut self, positions: impl IntoIterator, color: Color) { self.extend_strip_positions(positions); - let len = self.buffer.strip_positions.len(); - self.buffer - .strip_colors + let len = self.strip_positions.len(); + self.strip_colors .resize(len - 1, color.as_linear_rgba_f32()); - self.buffer.strip_colors.push([f32::NAN; 4]); + self.strip_colors.push([f32::NAN; 4]); } /// Draw a line in 3D made of straight segments between the points, with a color gradient. @@ -182,11 +244,8 @@ impl<'s> Gizmos<'s> { pub fn linestrip_gradient(&mut self, points: impl IntoIterator) { let points = points.into_iter(); - let GizmoBuffer { - strip_positions, - strip_colors, - .. - } = &mut *self.buffer; + let strip_positions = &mut self.strip_positions; + let strip_colors = &mut self.strip_colors; let (min, _) = points.size_hint(); strip_positions.reserve(min); @@ -228,9 +287,9 @@ impl<'s> Gizmos<'s> { normal: Vec3, radius: f32, color: Color, - ) -> CircleBuilder<'_, 's> { + ) -> CircleBuilder<'_> { CircleBuilder { - gizmos: self, + buffer: self, position, normal, radius, @@ -266,9 +325,9 @@ impl<'s> Gizmos<'s> { rotation: Quat, radius: f32, color: Color, - ) -> SphereBuilder<'_, 's> { + ) -> SphereBuilder<'_> { SphereBuilder { - gizmos: self, + buffer: self, position, rotation, radius, @@ -487,12 +546,7 @@ impl<'s> Gizmos<'s> { /// # bevy_ecs::system::assert_is_system(system); /// ``` #[inline] - pub fn circle_2d( - &mut self, - position: Vec2, - radius: f32, - color: Color, - ) -> Circle2dBuilder<'_, 's> { + pub fn circle_2d(&mut self, position: Vec2, radius: f32, color: Color) -> Circle2dBuilder<'_> { Circle2dBuilder { gizmos: self, position, @@ -538,7 +592,7 @@ impl<'s> Gizmos<'s> { arc_angle: f32, radius: f32, color: Color, - ) -> Arc2dBuilder<'_, 's> { + ) -> Arc2dBuilder<'_> { Arc2dBuilder { gizmos: self, position, @@ -571,30 +625,50 @@ impl<'s> Gizmos<'s> { self.linestrip_2d([tl, tr, br, bl, tl], color); } + /// Draw all gizmos from another buffer. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::{gizmos::GizmoBuffer, prelude::*}; + /// # use bevy_ecs::prelude::*; + /// #[derive(Resource)] + /// struct Buffer(GizmoBuffer); + /// + /// fn system(mut gizmos: Gizmos, buffered: Res) { + /// gizmos.submit_buffer(&buffered.0); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn submit_buffer(&mut self, gizmos: &Self) { + self.list_positions + .extend_from_slice(&gizmos.list_positions); + self.list_colors.extend_from_slice(&gizmos.list_colors); + self.strip_positions + .extend_from_slice(&gizmos.strip_positions); + self.strip_colors.extend_from_slice(&gizmos.strip_colors); + } + #[inline] fn extend_list_positions(&mut self, positions: impl IntoIterator) { - self.buffer - .list_positions + self.list_positions .extend(positions.into_iter().map(|vec3| vec3.to_array())); } #[inline] fn extend_list_colors(&mut self, colors: impl IntoIterator) { - self.buffer - .list_colors + self.list_colors .extend(colors.into_iter().map(|color| color.as_linear_rgba_f32())); } #[inline] fn add_list_color(&mut self, color: Color, count: usize) { - self.buffer - .list_colors + self.list_colors .extend(iter::repeat(color.as_linear_rgba_f32()).take(count)); } #[inline] fn extend_strip_positions(&mut self, positions: impl IntoIterator) { - self.buffer.strip_positions.extend( + self.strip_positions.extend( positions .into_iter() .map(|vec3| vec3.to_array()) @@ -603,9 +677,9 @@ impl<'s> Gizmos<'s> { } } -/// A builder returned by [`Gizmos::circle`]. -pub struct CircleBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +/// A builder returned by [`GizmoBuffer::circle`]. +pub struct CircleBuilder<'a> { + buffer: &'a mut GizmoBuffer, position: Vec3, normal: Vec3, radius: f32, @@ -613,7 +687,7 @@ pub struct CircleBuilder<'a, 's> { segments: usize, } -impl CircleBuilder<'_, '_> { +impl CircleBuilder<'_> { /// Set the number of line-segments for this circle. pub fn segments(mut self, segments: usize) -> Self { self.segments = segments; @@ -621,18 +695,18 @@ impl CircleBuilder<'_, '_> { } } -impl Drop for CircleBuilder<'_, '_> { +impl Drop for CircleBuilder<'_> { fn drop(&mut self) { let rotation = Quat::from_rotation_arc(Vec3::Z, self.normal); let positions = circle_inner(self.radius, self.segments) .map(|vec2| (self.position + rotation * vec2.extend(0.))); - self.gizmos.linestrip(positions, self.color); + self.buffer.linestrip(positions, self.color); } } -/// A builder returned by [`Gizmos::sphere`]. -pub struct SphereBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +/// A builder returned by [`GizmoBuffer::sphere`]. +pub struct SphereBuilder<'a> { + buffer: &'a mut GizmoBuffer, position: Vec3, rotation: Quat, radius: f32, @@ -640,7 +714,7 @@ pub struct SphereBuilder<'a, 's> { circle_segments: usize, } -impl SphereBuilder<'_, '_> { +impl SphereBuilder<'_> { /// Set the number of line-segments per circle for this sphere. pub fn circle_segments(mut self, segments: usize) -> Self { self.circle_segments = segments; @@ -648,26 +722,26 @@ impl SphereBuilder<'_, '_> { } } -impl Drop for SphereBuilder<'_, '_> { +impl Drop for SphereBuilder<'_> { fn drop(&mut self) { for axis in Vec3::AXES { - self.gizmos + self.buffer .circle(self.position, self.rotation * axis, self.radius, self.color) .segments(self.circle_segments); } } } -/// A builder returned by [`Gizmos::circle_2d`]. -pub struct Circle2dBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +/// A builder returned by [`GizmoBuffer::circle_2d`]. +pub struct Circle2dBuilder<'a> { + gizmos: &'a mut GizmoBuffer, position: Vec2, radius: f32, color: Color, segments: usize, } -impl Circle2dBuilder<'_, '_> { +impl Circle2dBuilder<'_> { /// Set the number of line-segments for this circle. pub fn segments(mut self, segments: usize) -> Self { self.segments = segments; @@ -675,16 +749,16 @@ impl Circle2dBuilder<'_, '_> { } } -impl Drop for Circle2dBuilder<'_, '_> { +impl Drop for Circle2dBuilder<'_> { fn drop(&mut self) { let positions = circle_inner(self.radius, self.segments).map(|vec2| (vec2 + self.position)); self.gizmos.linestrip_2d(positions, self.color); } } -/// A builder returned by [`Gizmos::arc_2d`]. -pub struct Arc2dBuilder<'a, 's> { - gizmos: &'a mut Gizmos<'s>, +/// A builder returned by [`GizmoBuffer::arc_2d`]. +pub struct Arc2dBuilder<'a> { + gizmos: &'a mut GizmoBuffer, position: Vec2, direction_angle: f32, arc_angle: f32, @@ -693,7 +767,7 @@ pub struct Arc2dBuilder<'a, 's> { segments: Option, } -impl Arc2dBuilder<'_, '_> { +impl Arc2dBuilder<'_> { /// Set the number of line-segments for this arc. pub fn segments(mut self, segments: usize) -> Self { self.segments = Some(segments); @@ -701,7 +775,7 @@ impl Arc2dBuilder<'_, '_> { } } -impl Drop for Arc2dBuilder<'_, '_> { +impl Drop for Arc2dBuilder<'_> { fn drop(&mut self) { let segments = match self.segments { Some(segments) => segments, diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 79f696e1f063e..2ed46f22bd65f 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -15,6 +15,12 @@ //! ``` //! //! See the documentation on [`Gizmos`](crate::gizmos::Gizmos) for more examples. +//! +//! Gizmos will only be visible for the upcoming frame, or until the next fixed +//! update if drawn during [`FixedUpdate`] as indicated by [`FixedUpdateScheduleIsCurrentlyRunning`]. +//! +//! [`FixedUpdate`]: ::bevy_app::FixedUpdate +//! [`FixedUpdateScheduleIsCurrentlyRunning`]: ::bevy_app::FixedUpdateScheduleIsCurrentlyRunning pub mod gizmos; @@ -29,7 +35,7 @@ pub mod prelude { pub use crate::{gizmos::Gizmos, AabbGizmo, AabbGizmoConfig, GizmoConfig}; } -use bevy_app::{Last, Plugin, PostUpdate}; +use bevy_app::{FixedUpdate, Last, Plugin, PostUpdate}; use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle}; use bevy_core::cast_slice; use bevy_ecs::{ @@ -65,7 +71,8 @@ use bevy_transform::{ components::{GlobalTransform, Transform}, TransformSystem, }; -use gizmos::{GizmoStorage, Gizmos}; + +use gizmos::{GizmoStorages, Gizmos}; use std::mem; const LINE_SHADER_HANDLE: Handle = Handle::weak_from_u128(7414812689238026784); @@ -82,7 +89,7 @@ impl Plugin for GizmoPlugin { .add_plugins(RenderAssetPlugin::::default()) .init_resource::() .init_resource::() - .init_resource::() + .init_resource::() .add_systems(Last, update_gizmo_meshes) .add_systems( PostUpdate, @@ -91,7 +98,10 @@ impl Plugin for GizmoPlugin { draw_all_aabbs.run_if(|config: Res| config.aabb.draw_all), ) .after(TransformSystem::TransformPropagate), - ); + ) + // Ensure gizmos from previous fixed update are cleaned up if no other system + // accesses `Gizmos` during fixed update any more + .add_systems(FixedUpdate, |_: Gizmos| ()); let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; @@ -272,8 +282,23 @@ struct LineGizmoHandles { fn update_gizmo_meshes( mut line_gizmos: ResMut>, mut handles: ResMut, - mut storage: ResMut, + mut storages: ResMut, ) { + // Combine gizmos for this frame (which get cleared here) with the ones from the last fixed update (which get cleared during system buffer application) + let mut storage = mem::take(&mut storages.frame); + storage + .list_positions + .extend_from_slice(&storages.fixed_update.list_positions); + storage + .list_colors + .extend_from_slice(&storages.fixed_update.list_colors); + storage + .strip_positions + .extend_from_slice(&storages.fixed_update.strip_positions); + storage + .strip_colors + .extend_from_slice(&storages.fixed_update.strip_colors); + if storage.list_positions.is_empty() { handles.list = None; } else if let Some(handle) = handles.list.as_ref() { diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 723e65afd9c8f..13ada4f749c69 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -189,6 +189,12 @@ pub mod gizmos { //! ``` //! //! See the documentation on [`Gizmos`](gizmos::Gizmos) for more examples. + //! + //! Gizmos will only be visible for the upcoming frame, or until the next fixed + //! update if drawn during [`FixedUpdate`] as indicated by [`FixedUpdateScheduleIsCurrentlyRunning`]. + //! + //! [`FixedUpdate`]: ::bevy_app::FixedUpdate + //! [`FixedUpdateScheduleIsCurrentlyRunning`]: ::bevy_app::FixedUpdateScheduleIsCurrentlyRunning pub use bevy_gizmos::*; } diff --git a/crates/bevy_time/src/fixed_timestep.rs b/crates/bevy_time/src/fixed_timestep.rs index 655a19ba774d7..ea3355d9b14ba 100644 --- a/crates/bevy_time/src/fixed_timestep.rs +++ b/crates/bevy_time/src/fixed_timestep.rs @@ -22,7 +22,7 @@ //! variants for game simulation, but rather use the value of [`FixedTime`] instead. use crate::Time; -use bevy_app::FixedUpdate; +use bevy_app::{FixedUpdate, FixedUpdateScheduleIsCurrentlyRunning}; use bevy_ecs::{system::Resource, world::World}; use bevy_utils::Duration; use thiserror::Error; @@ -38,6 +38,7 @@ use thiserror::Error; #[derive(Resource, Debug)] pub struct FixedTime { accumulated: Duration, + total_ticks: u64, /// The amount of time spanned by each fixed update. /// Defaults to 1/60th of a second. /// @@ -51,6 +52,7 @@ impl FixedTime { FixedTime { accumulated: Duration::ZERO, period, + total_ticks: 0, } } @@ -59,6 +61,7 @@ impl FixedTime { FixedTime { accumulated: Duration::ZERO, period: Duration::from_secs_f32(period), + total_ticks: 0, } } @@ -77,6 +80,11 @@ impl FixedTime { self.accumulated } + /// Returns how often this has expended a period of time. + pub fn times_expended(&self) -> u64 { + self.total_ticks + } + /// Attempts to advance by a single period. This will return [`FixedUpdateError`] if there is not enough /// accumulated time -- in other words, if advancing time would put the fixed update schedule /// ahead of the main schedule. @@ -85,6 +93,7 @@ impl FixedTime { pub fn expend(&mut self) -> Result<(), FixedUpdateError> { if let Some(new_value) = self.accumulated.checked_sub(self.period) { self.accumulated = new_value; + self.total_ticks += 1; Ok(()) } else { Err(FixedUpdateError::NotEnoughTime { @@ -100,6 +109,7 @@ impl Default for FixedTime { FixedTime { accumulated: Duration::ZERO, period: Duration::from_secs_f32(1. / 60.), + total_ticks: 0, } } } @@ -121,6 +131,10 @@ pub enum FixedUpdateError { /// /// For more information, see the [module-level documentation](self). pub fn run_fixed_update_schedule(world: &mut World) { + world.insert_resource(FixedUpdateScheduleIsCurrentlyRunning { + update: world.resource::().times_expended(), + }); + // Tick the time let delta_time = world.resource::