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<CheckChangeTicks>, mut schedule: ResMut<CustomSchedule>| {
    schedule.0.check_change_ticks(tick.get());
});
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
urben1680 2025-06-09 22:44:49 +02:00 committed by GitHub
parent 2768af5d2d
commit 1294b71e35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 43 additions and 5 deletions

View File

@ -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<CheckChangeTicks>, mut schedule: ResMut<CustomSchedule>| {
/// 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> {

View File

@ -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);

View File

@ -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;
}