Skip to content

Commit 1294b71

Browse files
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 <[email protected]>
1 parent 2768af5 commit 1294b71

File tree

3 files changed

+43
-5
lines changed

3 files changed

+43
-5
lines changed

crates/bevy_ecs/src/component.rs

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::{
1515
use alloc::boxed::Box;
1616
use alloc::{borrow::Cow, format, vec::Vec};
1717
pub use bevy_ecs_macros::Component;
18+
use bevy_ecs_macros::Event;
1819
use bevy_platform::sync::Arc;
1920
use bevy_platform::{
2021
collections::{HashMap, HashSet},
@@ -2616,7 +2617,7 @@ impl Tick {
26162617
///
26172618
/// Returns `true` if wrapping was performed. Otherwise, returns `false`.
26182619
#[inline]
2619-
pub(crate) fn check_tick(&mut self, tick: Tick) -> bool {
2620+
pub fn check_tick(&mut self, tick: Tick) -> bool {
26202621
let age = tick.relative_to(*self);
26212622
// This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true
26222623
// so long as this check always runs before that can happen.
@@ -2629,6 +2630,41 @@ impl Tick {
26292630
}
26302631
}
26312632

2633+
/// An observer [`Event`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make
2634+
/// use of bevy's periodic checks that clamps ticks to a certain range, preventing overflows and thus
2635+
/// keeping methods like [`Tick::is_newer_than`] reliably return `false` for ticks that got too old.
2636+
///
2637+
/// # Example
2638+
///
2639+
/// Here a schedule is stored in a custom resource. This way the systems in it would not have their change
2640+
/// ticks automatically updated via [`World::check_change_ticks`], possibly causing `Tick`-related bugs on
2641+
/// long-running apps.
2642+
///
2643+
/// To fix that, add an observer for this event that calls the schedule's
2644+
/// [`Schedule::check_change_ticks`](bevy_ecs::schedule::Schedule::check_change_ticks).
2645+
///
2646+
/// ```
2647+
/// use bevy_ecs::prelude::*;
2648+
/// use bevy_ecs::component::CheckChangeTicks;
2649+
///
2650+
/// #[derive(Resource)]
2651+
/// struct CustomSchedule(Schedule);
2652+
///
2653+
/// # let mut world = World::new();
2654+
/// world.add_observer(|tick: Trigger<CheckChangeTicks>, mut schedule: ResMut<CustomSchedule>| {
2655+
/// schedule.0.check_change_ticks(tick.get());
2656+
/// });
2657+
/// ```
2658+
#[derive(Debug, Clone, Copy, Event)]
2659+
pub struct CheckChangeTicks(pub(crate) Tick);
2660+
2661+
impl CheckChangeTicks {
2662+
/// Get the `Tick` that can be used as the parameter of [`Tick::check_tick`].
2663+
pub fn get(self) -> Tick {
2664+
self.0
2665+
}
2666+
}
2667+
26322668
/// Interior-mutable access to the [`Tick`]s for a single component or resource.
26332669
#[derive(Copy, Clone, Debug)]
26342670
pub struct TickCells<'a> {

crates/bevy_ecs/src/schedule/schedule.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ impl Schedule {
558558
/// Iterates the change ticks of all systems in the schedule and clamps any older than
559559
/// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE).
560560
/// This prevents overflow and thus prevents false positives.
561-
pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) {
561+
pub fn check_change_ticks(&mut self, change_tick: Tick) {
562562
for system in &mut self.executable.systems {
563563
if !is_apply_deferred(system) {
564564
system.check_change_tick(change_tick);

crates/bevy_ecs/src/world/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ use crate::{
3939
},
4040
change_detection::{MaybeLocation, MutUntyped, TicksMut},
4141
component::{
42-
Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentIds, ComponentInfo,
43-
ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable,
44-
RequiredComponents, RequiredComponentsError, Tick,
42+
CheckChangeTicks, Component, ComponentDescriptor, ComponentHooks, ComponentId,
43+
ComponentIds, ComponentInfo, ComponentTicks, Components, ComponentsQueuedRegistrator,
44+
ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick,
4545
},
4646
entity::{Entities, Entity, EntityDoesNotExistError},
4747
entity_disabling::DefaultQueryFilters,
@@ -2964,6 +2964,8 @@ impl World {
29642964
schedules.check_change_ticks(change_tick);
29652965
}
29662966

2967+
self.trigger(CheckChangeTicks(change_tick));
2968+
29672969
self.last_check_tick = change_tick;
29682970
}
29692971

0 commit comments

Comments
 (0)