
# Objective Fixes a part of #14274. Bevy has an incredibly inconsistent naming convention for its system sets, both internally and across the ecosystem. <img alt="System sets in Bevy" src="https://github.com/user-attachments/assets/d16e2027-793f-4ba4-9cc9-e780b14a5a1b" width="450" /> *Names of public system set types in Bevy* Most Bevy types use a naming of `FooSystem` or just `Foo`, but there are also a few `FooSystems` and `FooSet` types. In ecosystem crates on the other hand, `FooSet` is perhaps the most commonly used name in general. Conventions being so wildly inconsistent can make it harder for users to pick names for their own types, to search for system sets on docs.rs, or to even discern which types *are* system sets. To reign in the inconsistency a bit and help unify the ecosystem, it would be good to establish a common recommended naming convention for system sets in Bevy itself, similar to how plugins are commonly suffixed with `Plugin` (ex: `TimePlugin`). By adopting a consistent naming convention in first-party Bevy, we can softly nudge ecosystem crates to follow suit (for types where it makes sense to do so). Choosing a naming convention is also relevant now, as the [`bevy_cli` recently adopted lints](https://github.com/TheBevyFlock/bevy_cli/pull/345) to enforce naming for plugins and system sets, and the recommended naming used for system sets is still a bit open. ## Which Name To Use? Now the contentious part: what naming convention should we actually adopt? This was discussed on the Bevy Discord at the end of last year, starting [here](<https://discord.com/channels/691052431525675048/692572690833473578/1310659954683936789>). `FooSet` and `FooSystems` were the clear favorites, with `FooSet` very narrowly winning an unofficial poll. However, it seems to me like the consensus was broadly moving towards `FooSystems` at the end and after the poll, with Cart ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311140204974706708)) and later Alice ([source](https://discord.com/channels/691052431525675048/692572690833473578/1311092530732859533)) and also me being in favor of it. Let's do a quick pros and cons list! Of course these are just what I thought of, so take it with a grain of salt. `FooSet`: - Pro: Nice and short! - Pro: Used by many ecosystem crates. - Pro: The `Set` suffix comes directly from the trait name `SystemSet`. - Pro: Pairs nicely with existing APIs like `in_set` and `configure_sets`. - Con: `Set` by itself doesn't actually indicate that it's related to systems *at all*, apart from the implemented trait. A set of what? - Con: Is `FooSet` a set of `Foo`s or a system set related to `Foo`? Ex: `ContactSet`, `MeshSet`, `EnemySet`... `FooSystems`: - Pro: Very clearly indicates that the type represents a collection of systems. The actual core concept, system(s), is in the name. - Pro: Parallels nicely with `FooPlugins` for plugin groups. - Pro: Low risk of conflicts with other names or misunderstandings about what the type is. - Pro: In most cases, reads *very* nicely and clearly. Ex: `PhysicsSystems` and `AnimationSystems` as opposed to `PhysicsSet` and `AnimationSet`. - Pro: Easy to search for on docs.rs. - Con: Usually results in longer names. - Con: Not yet as widely used. Really the big problem with `FooSet` is that it doesn't actually describe what it is. It describes what *kind of thing* it is (a set of something), but not *what it is a set of*, unless you know the type or check its docs or implemented traits. `FooSystems` on the other hand is much more self-descriptive in this regard, at the cost of being a bit longer to type. Ultimately, in some ways it comes down to preference and how you think of system sets. Personally, I was originally in favor of `FooSet`, but have been increasingly on the side of `FooSystems`, especially after seeing what the new names would actually look like in Avian and now Bevy. I prefer it because it usually reads better, is much more clearly related to groups of systems than `FooSet`, and overall *feels* more correct and natural to me in the long term. For these reasons, and because Alice and Cart also seemed to share a preference for it when it was previously being discussed, I propose that we adopt a `FooSystems` naming convention where applicable. ## Solution Rename Bevy's system set types to use a consistent `FooSet` naming where applicable. - `AccessibilitySystem` → `AccessibilitySystems` - `GizmoRenderSystem` → `GizmoRenderSystems` - `PickSet` → `PickingSystems` - `RunFixedMainLoopSystem` → `RunFixedMainLoopSystems` - `TransformSystem` → `TransformSystems` - `RemoteSet` → `RemoteSystems` - `RenderSet` → `RenderSystems` - `SpriteSystem` → `SpriteSystems` - `StateTransitionSteps` → `StateTransitionSystems` - `RenderUiSystem` → `RenderUiSystems` - `UiSystem` → `UiSystems` - `Animation` → `AnimationSystems` - `AssetEvents` → `AssetEventSystems` - `TrackAssets` → `AssetTrackingSystems` - `UpdateGizmoMeshes` → `GizmoMeshSystems` - `InputSystem` → `InputSystems` - `InputFocusSet` → `InputFocusSystems` - `ExtractMaterialsSet` → `MaterialExtractionSystems` - `ExtractMeshesSet` → `MeshExtractionSystems` - `RumbleSystem` → `RumbleSystems` - `CameraUpdateSystem` → `CameraUpdateSystems` - `ExtractAssetsSet` → `AssetExtractionSystems` - `Update2dText` → `Text2dUpdateSystems` - `TimeSystem` → `TimeSystems` - `AudioPlaySet` → `AudioPlaybackSystems` - `SendEvents` → `EventSenderSystems` - `EventUpdates` → `EventUpdateSystems` A lot of the names got slightly longer, but they are also a lot more consistent, and in my opinion the majority of them read much better. For a few of the names I took the liberty of rewording things a bit; definitely open to any further naming improvements. There are still also cases where the `FooSystems` naming doesn't really make sense, and those I left alone. This primarily includes system sets like `Interned<dyn SystemSet>`, `EnterSchedules<S>`, `ExitSchedules<S>`, or `TransitionSchedules<S>`, where the type has some special purpose and semantics. ## Todo - [x] Should I keep all the old names as deprecated type aliases? I can do this, but to avoid wasting work I'd prefer to first reach consensus on whether these renames are even desired. - [x] Migration guide - [x] Release notes
362 lines
13 KiB
Rust
362 lines
13 KiB
Rust
use bevy_app::FixedMain;
|
|
use bevy_ecs::world::World;
|
|
#[cfg(feature = "bevy_reflect")]
|
|
use bevy_reflect::Reflect;
|
|
use core::time::Duration;
|
|
|
|
use crate::{time::Time, virt::Virtual};
|
|
|
|
/// The fixed timestep game clock following virtual time.
|
|
///
|
|
/// A specialization of the [`Time`] structure. **For method documentation, see
|
|
/// [`Time<Fixed>#impl-Time<Fixed>`].**
|
|
///
|
|
/// It is automatically inserted as a resource by
|
|
/// [`TimePlugin`](crate::TimePlugin) and updated based on
|
|
/// [`Time<Virtual>`](Virtual). The fixed clock is automatically set as the
|
|
/// generic [`Time`] resource during [`FixedUpdate`](bevy_app::FixedUpdate)
|
|
/// schedule processing.
|
|
///
|
|
/// The fixed timestep clock advances in fixed-size increments, which is
|
|
/// extremely useful for writing logic (like physics) that should have
|
|
/// consistent behavior, regardless of framerate.
|
|
///
|
|
/// The default [`timestep()`](Time::timestep) is 64 hertz, or 15625
|
|
/// microseconds. This value was chosen because using 60 hertz has the potential
|
|
/// for a pathological interaction with the monitor refresh rate where the game
|
|
/// alternates between running two fixed timesteps and zero fixed timesteps per
|
|
/// frame (for example when running two fixed timesteps takes longer than a
|
|
/// frame). Additionally, the value is a power of two which losslessly converts
|
|
/// into [`f32`] and [`f64`].
|
|
///
|
|
/// To run a system on a fixed timestep, add it to one of the [`FixedMain`]
|
|
/// schedules, most commonly [`FixedUpdate`](bevy_app::FixedUpdate).
|
|
///
|
|
/// This schedule is run a number of times between
|
|
/// [`PreUpdate`](bevy_app::PreUpdate) and [`Update`](bevy_app::Update)
|
|
/// according to the accumulated [`overstep()`](Time::overstep) time divided by
|
|
/// the [`timestep()`](Time::timestep). This means the schedule may run 0, 1 or
|
|
/// more times during a single update (which typically corresponds to a rendered
|
|
/// frame).
|
|
///
|
|
/// `Time<Fixed>` and the generic [`Time`] resource will report a
|
|
/// [`delta()`](Time::delta) equal to [`timestep()`](Time::timestep) and always
|
|
/// grow [`elapsed()`](Time::elapsed) by one [`timestep()`](Time::timestep) per
|
|
/// iteration.
|
|
///
|
|
/// The fixed timestep clock follows the [`Time<Virtual>`](Virtual) clock, which
|
|
/// means it is affected by [`pause()`](Time::pause),
|
|
/// [`set_relative_speed()`](Time::set_relative_speed) and
|
|
/// [`set_max_delta()`](Time::set_max_delta) from virtual time. If the virtual
|
|
/// clock is paused, the [`FixedUpdate`](bevy_app::FixedUpdate) schedule will
|
|
/// not run. It is guaranteed that the [`elapsed()`](Time::elapsed) time in
|
|
/// `Time<Fixed>` is always between the previous `elapsed()` and the current
|
|
/// `elapsed()` value in `Time<Virtual>`, so the values are compatible.
|
|
///
|
|
/// Changing the timestep size while the game is running should not normally be
|
|
/// done, as having a regular interval is the point of this schedule, but it may
|
|
/// be necessary for effects like "bullet-time" if the normal granularity of the
|
|
/// fixed timestep is too big for the slowed down time. In this case,
|
|
/// [`set_timestep()`](Time::set_timestep) and be called to set a new value. The
|
|
/// new value will be used immediately for the next run of the
|
|
/// [`FixedUpdate`](bevy_app::FixedUpdate) schedule, meaning that it will affect
|
|
/// the [`delta()`](Time::delta) value for the very next
|
|
/// [`FixedUpdate`](bevy_app::FixedUpdate), even if it is still during the same
|
|
/// frame. Any [`overstep()`](Time::overstep) present in the accumulator will be
|
|
/// processed according to the new [`timestep()`](Time::timestep) value.
|
|
#[derive(Debug, Copy, Clone)]
|
|
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone))]
|
|
pub struct Fixed {
|
|
timestep: Duration,
|
|
overstep: Duration,
|
|
}
|
|
|
|
impl Time<Fixed> {
|
|
/// Corresponds to 64 Hz.
|
|
const DEFAULT_TIMESTEP: Duration = Duration::from_micros(15625);
|
|
|
|
/// Return new fixed time clock with given timestep as [`Duration`]
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `timestep` is zero.
|
|
pub fn from_duration(timestep: Duration) -> Self {
|
|
let mut ret = Self::default();
|
|
ret.set_timestep(timestep);
|
|
ret
|
|
}
|
|
|
|
/// Return new fixed time clock with given timestep seconds as `f64`
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `seconds` is zero, negative or not finite.
|
|
pub fn from_seconds(seconds: f64) -> Self {
|
|
let mut ret = Self::default();
|
|
ret.set_timestep_seconds(seconds);
|
|
ret
|
|
}
|
|
|
|
/// Return new fixed time clock with given timestep frequency in Hertz (1/seconds)
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `hz` is zero, negative or not finite.
|
|
pub fn from_hz(hz: f64) -> Self {
|
|
let mut ret = Self::default();
|
|
ret.set_timestep_hz(hz);
|
|
ret
|
|
}
|
|
|
|
/// Returns the amount of virtual time that must pass before the fixed
|
|
/// timestep schedule is run again.
|
|
#[inline]
|
|
pub fn timestep(&self) -> Duration {
|
|
self.context().timestep
|
|
}
|
|
|
|
/// Sets the amount of virtual time that must pass before the fixed timestep
|
|
/// schedule is run again, as [`Duration`].
|
|
///
|
|
/// Takes effect immediately on the next run of the schedule, respecting
|
|
/// what is currently in [`Self::overstep`].
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `timestep` is zero.
|
|
#[inline]
|
|
pub fn set_timestep(&mut self, timestep: Duration) {
|
|
assert_ne!(
|
|
timestep,
|
|
Duration::ZERO,
|
|
"attempted to set fixed timestep to zero"
|
|
);
|
|
self.context_mut().timestep = timestep;
|
|
}
|
|
|
|
/// Sets the amount of virtual time that must pass before the fixed timestep
|
|
/// schedule is run again, as seconds.
|
|
///
|
|
/// Timestep is stored as a [`Duration`], which has fixed nanosecond
|
|
/// resolution and will be converted from the floating point number.
|
|
///
|
|
/// Takes effect immediately on the next run of the schedule, respecting
|
|
/// what is currently in [`Self::overstep`].
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `seconds` is zero, negative or not finite.
|
|
#[inline]
|
|
pub fn set_timestep_seconds(&mut self, seconds: f64) {
|
|
assert!(
|
|
seconds.is_sign_positive(),
|
|
"seconds less than or equal to zero"
|
|
);
|
|
assert!(seconds.is_finite(), "seconds is infinite");
|
|
self.set_timestep(Duration::from_secs_f64(seconds));
|
|
}
|
|
|
|
/// Sets the amount of virtual time that must pass before the fixed timestep
|
|
/// schedule is run again, as frequency.
|
|
///
|
|
/// The timestep value is set to `1 / hz`, converted to a [`Duration`] which
|
|
/// has fixed nanosecond resolution.
|
|
///
|
|
/// Takes effect immediately on the next run of the schedule, respecting
|
|
/// what is currently in [`Self::overstep`].
|
|
///
|
|
/// # Panics
|
|
///
|
|
/// Panics if `hz` is zero, negative or not finite.
|
|
#[inline]
|
|
pub fn set_timestep_hz(&mut self, hz: f64) {
|
|
assert!(hz.is_sign_positive(), "Hz less than or equal to zero");
|
|
assert!(hz.is_finite(), "Hz is infinite");
|
|
self.set_timestep_seconds(1.0 / hz);
|
|
}
|
|
|
|
/// Returns the amount of overstep time accumulated toward new steps, as
|
|
/// [`Duration`].
|
|
#[inline]
|
|
pub fn overstep(&self) -> Duration {
|
|
self.context().overstep
|
|
}
|
|
|
|
/// Discard a part of the overstep amount.
|
|
///
|
|
/// If `discard` is higher than overstep, the overstep becomes zero.
|
|
#[inline]
|
|
pub fn discard_overstep(&mut self, discard: Duration) {
|
|
let context = self.context_mut();
|
|
context.overstep = context.overstep.saturating_sub(discard);
|
|
}
|
|
|
|
/// Returns the amount of overstep time accumulated toward new steps, as an
|
|
/// [`f32`] fraction of the timestep.
|
|
#[inline]
|
|
pub fn overstep_fraction(&self) -> f32 {
|
|
self.context().overstep.as_secs_f32() / self.context().timestep.as_secs_f32()
|
|
}
|
|
|
|
/// Returns the amount of overstep time accumulated toward new steps, as an
|
|
/// [`f64`] fraction of the timestep.
|
|
#[inline]
|
|
pub fn overstep_fraction_f64(&self) -> f64 {
|
|
self.context().overstep.as_secs_f64() / self.context().timestep.as_secs_f64()
|
|
}
|
|
|
|
fn accumulate(&mut self, delta: Duration) {
|
|
self.context_mut().overstep += delta;
|
|
}
|
|
|
|
fn expend(&mut self) -> bool {
|
|
let timestep = self.timestep();
|
|
if let Some(new_value) = self.context_mut().overstep.checked_sub(timestep) {
|
|
// reduce accumulated and increase elapsed by period
|
|
self.context_mut().overstep = new_value;
|
|
self.advance_by(timestep);
|
|
true
|
|
} else {
|
|
// no more periods left in accumulated
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for Fixed {
|
|
fn default() -> Self {
|
|
Self {
|
|
timestep: Time::<Fixed>::DEFAULT_TIMESTEP,
|
|
overstep: Duration::ZERO,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Runs [`FixedMain`] zero or more times based on delta of
|
|
/// [`Time<Virtual>`](Virtual) and [`Time::overstep`].
|
|
/// You can order your systems relative to this by using
|
|
/// [`RunFixedMainLoopSystems`](bevy_app::prelude::RunFixedMainLoopSystems).
|
|
pub(super) fn run_fixed_main_schedule(world: &mut World) {
|
|
let delta = world.resource::<Time<Virtual>>().delta();
|
|
world.resource_mut::<Time<Fixed>>().accumulate(delta);
|
|
|
|
// Run the schedule until we run out of accumulated time
|
|
let _ = world.try_schedule_scope(FixedMain, |world, schedule| {
|
|
while world.resource_mut::<Time<Fixed>>().expend() {
|
|
*world.resource_mut::<Time>() = world.resource::<Time<Fixed>>().as_generic();
|
|
schedule.run(world);
|
|
}
|
|
});
|
|
|
|
*world.resource_mut::<Time>() = world.resource::<Time<Virtual>>().as_generic();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_set_timestep() {
|
|
let mut time = Time::<Fixed>::default();
|
|
|
|
assert_eq!(time.timestep(), Time::<Fixed>::DEFAULT_TIMESTEP);
|
|
|
|
time.set_timestep(Duration::from_millis(500));
|
|
assert_eq!(time.timestep(), Duration::from_millis(500));
|
|
|
|
time.set_timestep_seconds(0.25);
|
|
assert_eq!(time.timestep(), Duration::from_millis(250));
|
|
|
|
time.set_timestep_hz(8.0);
|
|
assert_eq!(time.timestep(), Duration::from_millis(125));
|
|
}
|
|
|
|
#[test]
|
|
fn test_expend() {
|
|
let mut time = Time::<Fixed>::from_seconds(2.0);
|
|
|
|
assert_eq!(time.delta(), Duration::ZERO);
|
|
assert_eq!(time.elapsed(), Duration::ZERO);
|
|
|
|
time.accumulate(Duration::from_secs(1));
|
|
|
|
assert_eq!(time.delta(), Duration::ZERO);
|
|
assert_eq!(time.elapsed(), Duration::ZERO);
|
|
assert_eq!(time.overstep(), Duration::from_secs(1));
|
|
assert_eq!(time.overstep_fraction(), 0.5);
|
|
assert_eq!(time.overstep_fraction_f64(), 0.5);
|
|
|
|
assert!(!time.expend()); // false
|
|
|
|
assert_eq!(time.delta(), Duration::ZERO);
|
|
assert_eq!(time.elapsed(), Duration::ZERO);
|
|
assert_eq!(time.overstep(), Duration::from_secs(1));
|
|
assert_eq!(time.overstep_fraction(), 0.5);
|
|
assert_eq!(time.overstep_fraction_f64(), 0.5);
|
|
|
|
time.accumulate(Duration::from_secs(1));
|
|
|
|
assert_eq!(time.delta(), Duration::ZERO);
|
|
assert_eq!(time.elapsed(), Duration::ZERO);
|
|
assert_eq!(time.overstep(), Duration::from_secs(2));
|
|
assert_eq!(time.overstep_fraction(), 1.0);
|
|
assert_eq!(time.overstep_fraction_f64(), 1.0);
|
|
|
|
assert!(time.expend()); // true
|
|
|
|
assert_eq!(time.delta(), Duration::from_secs(2));
|
|
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
|
assert_eq!(time.overstep(), Duration::ZERO);
|
|
assert_eq!(time.overstep_fraction(), 0.0);
|
|
assert_eq!(time.overstep_fraction_f64(), 0.0);
|
|
|
|
assert!(!time.expend()); // false
|
|
|
|
assert_eq!(time.delta(), Duration::from_secs(2));
|
|
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
|
assert_eq!(time.overstep(), Duration::ZERO);
|
|
assert_eq!(time.overstep_fraction(), 0.0);
|
|
assert_eq!(time.overstep_fraction_f64(), 0.0);
|
|
|
|
time.accumulate(Duration::from_secs(1));
|
|
|
|
assert_eq!(time.delta(), Duration::from_secs(2));
|
|
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
|
assert_eq!(time.overstep(), Duration::from_secs(1));
|
|
assert_eq!(time.overstep_fraction(), 0.5);
|
|
assert_eq!(time.overstep_fraction_f64(), 0.5);
|
|
|
|
assert!(!time.expend()); // false
|
|
|
|
assert_eq!(time.delta(), Duration::from_secs(2));
|
|
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
|
assert_eq!(time.overstep(), Duration::from_secs(1));
|
|
assert_eq!(time.overstep_fraction(), 0.5);
|
|
assert_eq!(time.overstep_fraction_f64(), 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_expend_multiple() {
|
|
let mut time = Time::<Fixed>::from_seconds(2.0);
|
|
|
|
time.accumulate(Duration::from_secs(7));
|
|
assert_eq!(time.overstep(), Duration::from_secs(7));
|
|
|
|
assert!(time.expend()); // true
|
|
assert_eq!(time.elapsed(), Duration::from_secs(2));
|
|
assert_eq!(time.overstep(), Duration::from_secs(5));
|
|
|
|
assert!(time.expend()); // true
|
|
assert_eq!(time.elapsed(), Duration::from_secs(4));
|
|
assert_eq!(time.overstep(), Duration::from_secs(3));
|
|
|
|
assert!(time.expend()); // true
|
|
assert_eq!(time.elapsed(), Duration::from_secs(6));
|
|
assert_eq!(time.overstep(), Duration::from_secs(1));
|
|
|
|
assert!(!time.expend()); // false
|
|
assert_eq!(time.elapsed(), Duration::from_secs(6));
|
|
assert_eq!(time.overstep(), Duration::from_secs(1));
|
|
}
|
|
}
|