From 1294b71e355750fe5c660580304700c8cbea1b77 Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Mon, 9 Jun 2025 22:44:49 +0200 Subject: [PATCH] Introduce `CheckChangeTicks` event that is triggered by `World::check_change_ticks` (#19274) # Objective In the past I had custom data structures containing `Tick`s. I learned that these need to be regularly checked to clamp them. But there was no way to hook into that logic so I abandoned storing ticks since then. Another motivation to open this up some more is to be more able to do a correct implementation of `System::check_ticks`. ## Solution Add `CheckChangeTicks` and trigger it in `World::check_change_ticks`. Make `Tick::check_tick` public. This event makes it possible to store ticks in components or resources and have them checked. I also made `Schedules::check_change_ticks` public so users can store schedules in custom resources/components for whatever reasons. ## Testing The logic boils down to a single `World::trigger` call and I don't think this needs more tests. ## Alternatives Making this obsolete like with #15683. --- ## Showcase From the added docs: ```rs use bevy_ecs::prelude::*; use bevy_ecs::component::CheckChangeTicks; #[derive(Resource)] struct CustomSchedule(Schedule); let mut world = World::new(); world.add_observer(|tick: Trigger, mut schedule: ResMut| { schedule.0.check_change_ticks(tick.get()); }); ``` --------- Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/component.rs | 38 +++++++++++++++++++++++- crates/bevy_ecs/src/schedule/schedule.rs | 2 +- crates/bevy_ecs/src/world/mod.rs | 8 +++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index d083901ccc..c4b8eec31d 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -15,6 +15,7 @@ use crate::{ use alloc::boxed::Box; use alloc::{borrow::Cow, format, vec::Vec}; pub use bevy_ecs_macros::Component; +use bevy_ecs_macros::Event; use bevy_platform::sync::Arc; use bevy_platform::{ collections::{HashMap, HashSet}, @@ -2616,7 +2617,7 @@ impl Tick { /// /// Returns `true` if wrapping was performed. Otherwise, returns `false`. #[inline] - pub(crate) fn check_tick(&mut self, tick: Tick) -> bool { + pub fn check_tick(&mut self, tick: Tick) -> bool { let age = tick.relative_to(*self); // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true // so long as this check always runs before that can happen. @@ -2629,6 +2630,41 @@ impl Tick { } } +/// An observer [`Event`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make +/// use of bevy's periodic checks that clamps ticks to a certain range, preventing overflows and thus +/// keeping methods like [`Tick::is_newer_than`] reliably return `false` for ticks that got too old. +/// +/// # Example +/// +/// Here a schedule is stored in a custom resource. This way the systems in it would not have their change +/// ticks automatically updated via [`World::check_change_ticks`], possibly causing `Tick`-related bugs on +/// long-running apps. +/// +/// To fix that, add an observer for this event that calls the schedule's +/// [`Schedule::check_change_ticks`](bevy_ecs::schedule::Schedule::check_change_ticks). +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_ecs::component::CheckChangeTicks; +/// +/// #[derive(Resource)] +/// struct CustomSchedule(Schedule); +/// +/// # let mut world = World::new(); +/// world.add_observer(|tick: Trigger, mut schedule: ResMut| { +/// schedule.0.check_change_ticks(tick.get()); +/// }); +/// ``` +#[derive(Debug, Clone, Copy, Event)] +pub struct CheckChangeTicks(pub(crate) Tick); + +impl CheckChangeTicks { + /// Get the `Tick` that can be used as the parameter of [`Tick::check_tick`]. + pub fn get(self) -> Tick { + self.0 + } +} + /// Interior-mutable access to the [`Tick`]s for a single component or resource. #[derive(Copy, Clone, Debug)] pub struct TickCells<'a> { diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 2d559db1dd..a4bc0d3533 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -558,7 +558,7 @@ impl Schedule { /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub fn check_change_ticks(&mut self, change_tick: Tick) { for system in &mut self.executable.systems { if !is_apply_deferred(system) { system.check_change_tick(change_tick); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 4172f0b31d..d84717755b 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -39,9 +39,9 @@ use crate::{ }, change_detection::{MaybeLocation, MutUntyped, TicksMut}, component::{ - Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentIds, ComponentInfo, - ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, - RequiredComponents, RequiredComponentsError, Tick, + CheckChangeTicks, Component, ComponentDescriptor, ComponentHooks, ComponentId, + ComponentIds, ComponentInfo, ComponentTicks, Components, ComponentsQueuedRegistrator, + ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, entity::{Entities, Entity, EntityDoesNotExistError}, entity_disabling::DefaultQueryFilters, @@ -2964,6 +2964,8 @@ impl World { schedules.check_change_ticks(change_tick); } + self.trigger(CheckChangeTicks(change_tick)); + self.last_check_tick = change_tick; }