From fb2bcbb097f7d017bd32d287d8a6bf474ac6ac69 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Thu, 22 Jan 2026 15:03:06 +0000 Subject: [PATCH 1/6] WIP Despawn using despawn frame marker component --- src/snapshot/despawn.rs | 169 ++++++++++++++++++++++++++++++++++++++++ src/snapshot/mod.rs | 3 + src/snapshot/set.rs | 15 +++- 3 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 src/snapshot/despawn.rs diff --git a/src/snapshot/despawn.rs b/src/snapshot/despawn.rs new file mode 100644 index 0000000..0117762 --- /dev/null +++ b/src/snapshot/despawn.rs @@ -0,0 +1,169 @@ +//! Rollback entity deferred despawning module. +//! +//! This module allows rollback entities to have non-rollback components (such as static collision +//! properties or mesh handles) by deferring the actual despawn until the requested frame has been +//! confirmed. Despawned entities are instead marked with the disabling component +//! [`RollbackDespawned`] so that they behave as though they are despawned by default. +//! +//! # Examples +//! ```rust +//! # use bevy::prelude::*; +//! # use bevy_ggrs::{prelude::*, ResourceChecksumPlugin}; +//! # +//! # const FPS: usize = 60; +//! # +//! # type MyInputType = u8; +//! # +//! # fn read_local_inputs() {} +//! # +//! # fn start(session: Session>) { +//! # let mut app = App::new(); +//! #[derive(Resource, Clone, Hash)] +//! struct BossHealth(u32); +//! +//! // To include something in the checksum, it should also be rolled back +//! app.rollback_resource_with_clone::(); +//! +//! // This will update the checksum every frame to include BossHealth +//! app.add_plugins(ResourceChecksumPlugin::::default()); +//! # } +//! ``` +//! +//! Entities which have been marked for despawn are disabled using the [`RollbackDespawned`] +//! component, so they will appear as though they are despawned to normal system queries. + +use std::cmp::Ordering; +use bevy::app::{App, Plugin}; +use bevy::prelude::{Children, Component, Entity, EntityCommands, EntityMut, EntityRef, EntityWorldMut, IntoScheduleConfigs, Local, Query, QueryState, Res, World}; +use ggrs::Frame; +use crate::{AdvanceWorld, AdvanceWorldSystems, ConfirmedFrameCount, LoadWorld, LoadWorldSystems, RollbackFrameCount, SaveWorld, SaveWorldSystems}; +use crate::snapshot::despawn::private::RollbackDespawnCommandExtensionSeal; + +/// Marks an entity as despawned, contains the frame that the entity was despawned on. +/// +/// When an entity is marked with this component, they MUST not be allowed to affect the simulation +/// in any way, as they may eventually be despawned on different frames for different peers. +/// This component is registered as "disabling" (see [ecs module docs](bevy_ecs::entity_disabling)) +/// so that the entity will not show up in queries by default. +#[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct RollbackDespawned(Frame); + +pub struct RollbackDespawnPlugin; + +impl Plugin for RollbackDespawnPlugin { + fn build(&self, app: &mut App) { + app.register_disabling_component::(); + + app.add_systems(LoadWorld, resurrect_entities.in_set(LoadWorldSystems::EntityResurrect)) + .add_systems(AdvanceWorld, despawn_confirmed_entities.in_set(AdvanceWorldSystems::DespawnConfirmed)); + } +} + +fn resurrect_entities( + world: &mut World, + despawn_query: &mut QueryState<(Entity, &RollbackDespawned)>, +) { + let rollback_frame = world.resource::(); + + despawn_query + .iter(world) + .filter_map(|(entity, despawned_frame)| { + // During a world load, entities marked with the current rollback frame were marked + // for despawn during that frame, so we only want to resurrect entities despawned after. + Some(entity).filter(|_e| despawned_frame > rollback_frame) + }) + .collect::>() + .into_iter() + .for_each(|entity| { world.entity_mut(entity).remove::(); }); +} + +fn despawn_confirmed_entities( + world: &mut World, + despawn_query: &mut QueryState<(Entity, &RollbackDespawned)>, + mut local: Local, +) { + let confirmed_frame = world.resource::(); + if *confirmed_frame == *local { + return; // No work necessary + } + *local = *confirmed_frame; + + despawn_query + .iter(world) + .filter_map(|(entity, despawned_frame)| { + // Entities marked as despawned on the confirmed frame or earlier can be immediately + // despawned. + Some(entity).filter(|_e| despawned_frame <= confirmed_frame) + }) + .collect::>() + .into_iter() + .for_each(|entity| { world.despawn(entity); }); +} + +mod private { + pub trait RollbackDespawnCommandExtensionSeal {} +} +pub trait RollbackDespawnCommandExtension: private::RollbackDespawnCommandExtensionSeal { + /// Despawns this entity and its children recursively using the [`RollbackDespawned`] + /// component, such that they can be resurrected following a rollback. + /// + /// NOTE: This does not yet support [`RelationshipTarget`] with linked spawn mode. + fn despawn_rollback(&mut self); + + /// NOTE: Not implemented yet. + fn despawn_children_rollback(&mut self) -> &mut Self; + + /// NOTE: Not implemented yet. + fn despawn_related_rollback(&mut self) -> &mut Self; +} + +impl RollbackDespawnCommandExtensionSeal for EntityCommands<'_> {} + +impl RollbackDespawnCommandExtension for EntityCommands<'_> { + fn despawn_rollback(&mut self) { + self.queue_silenced(despawn_rollback); + } + + fn despawn_children_rollback(&mut self) -> &mut Self { + todo!() + } + + fn despawn_related_rollback(&mut self) -> &mut Self { + todo!() + } +} + +fn despawn_rollback(mut entity: EntityWorldMut) { + if let Some(&RollbackFrameCount(frame)) = entity.get_resource::() { + // If we have RollbackFrameCount we should also have ConfirmedFrameCount + let &ConfirmedFrameCount(confirmed) = entity.get_resource::().unwrap(); + + // TODO handle wraparound + if confirmed < frame { + entity.insert_recursive::(RollbackDespawned(frame)); + return; + } + } + + // If current frame is confirmed or rollback sim is not present, we can simply despawn + entity.despawn(); +} + +macro_rules! newtype_partial_ord { + ($i:ident, $j:ident) => { + impl PartialEq<$j> for $i { + fn eq(&self, other: &$j) -> bool { + self.0 == other.0 + } + } + + impl PartialOrd<$j> for $i { + fn partial_cmp(&self, other: &$j) -> Option { + Some(self.0.cmp(&other.0)) + } + } + }; +} + +newtype_partial_ord!(RollbackDespawned, RollbackFrameCount); +newtype_partial_ord!(RollbackDespawned, ConfirmedFrameCount); diff --git a/src/snapshot/mod.rs b/src/snapshot/mod.rs index ac0f06e..9db196a 100644 --- a/src/snapshot/mod.rs +++ b/src/snapshot/mod.rs @@ -18,6 +18,7 @@ mod rollback_app; mod rollback_entity_map; mod set; mod strategy; +mod despawn; pub use checksum::*; pub use childof_snapshot::*; @@ -34,6 +35,7 @@ pub use rollback_app::*; pub use rollback_entity_map::*; pub use set::*; pub use strategy::*; +use crate::snapshot::despawn::RollbackDespawnPlugin; pub mod prelude { pub use super::{Checksum, LoadWorldSystems, SaveWorldSystems}; @@ -304,6 +306,7 @@ impl Plugin for SnapshotPlugin { EntitySnapshotPlugin, ResourceSnapshotPlugin::>::default(), ChildOfSnapshotPlugin, + RollbackDespawnPlugin, )); } } diff --git a/src/snapshot/set.rs b/src/snapshot/set.rs index e9974a9..0670b68 100644 --- a/src/snapshot/set.rs +++ b/src/snapshot/set.rs @@ -7,6 +7,9 @@ use crate::snapshot::{AdvanceWorld, LoadWorld, SaveWorld}; /// and [`Resource`] snapshots are loaded and applied to the [`World`]. #[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] pub enum LoadWorldSystems { + /// Removes any despawn markers if the loaded frame is before they were marked. + /// See [`despawn module docs`](`crate::snapshot::despawn`). + EntityResurrect, /// Recreate the [`Entity`] graph as it was during the frame to be rolled back to. /// When this set is complete, all entities that were alive during the snapshot /// frame have been recreated, and any that were not have been removed. If the @@ -19,7 +22,7 @@ pub enum LoadWorldSystems { /// When this set is complete, all [`Components`](`Component`) and [`Resources`](`Resource`) /// will be rolled back to their exact state during the snapshot. /// - /// NOTE: At this point, [`Entity`] relationships may be broken, see [`LoadWorldSet::Mapping`] + /// NOTE: At this point, [`Entity`] relationships may be broken, see [`LoadWorldSystems::Mapping`] /// for when those relationships are fixed. Data, /// Flush any deferred operations @@ -49,6 +52,9 @@ pub enum SaveWorldSystems { #[derive(SystemSet, Hash, Debug, PartialEq, Eq, Clone)] pub enum AdvanceWorldSystems { First, + /// Despawn any entities if their marked frame has been confirmed. + /// See [`despawn module docs`](`crate::snapshot::despawn`). + DespawnConfirmed, Main, Last, } @@ -62,6 +68,7 @@ impl Plugin for SnapshotSetPlugin { app.configure_sets( LoadWorld, ( + LoadWorldSystems::EntityResurrect, LoadWorldSystems::Entity, LoadWorldSystems::EntityFlush, LoadWorldSystems::Data, @@ -72,12 +79,16 @@ impl Plugin for SnapshotSetPlugin { ) .configure_sets( SaveWorld, - (SaveWorldSystems::Checksum, SaveWorldSystems::Snapshot).chain(), + ( + SaveWorldSystems::Checksum, + SaveWorldSystems::Snapshot + ).chain(), ) .configure_sets( AdvanceWorld, ( AdvanceWorldSystems::First, + AdvanceWorldSystems::DespawnConfirmed, AdvanceWorldSystems::Main, AdvanceWorldSystems::Last, ) From a74577b236923249a06984b37c6ba0f6f22fb187 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Thu, 22 Jan 2026 15:48:07 +0000 Subject: [PATCH 2/6] Cargo fmt --- src/snapshot/despawn.rs | 32 ++++++++++++++++++++++++-------- src/snapshot/mod.rs | 4 ++-- src/snapshot/set.rs | 5 +---- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/snapshot/despawn.rs b/src/snapshot/despawn.rs index 0117762..2598c96 100644 --- a/src/snapshot/despawn.rs +++ b/src/snapshot/despawn.rs @@ -32,12 +32,18 @@ //! Entities which have been marked for despawn are disabled using the [`RollbackDespawned`] //! component, so they will appear as though they are despawned to normal system queries. -use std::cmp::Ordering; +use crate::snapshot::despawn::private::RollbackDespawnCommandExtensionSeal; +use crate::{ + AdvanceWorld, AdvanceWorldSystems, ConfirmedFrameCount, LoadWorld, LoadWorldSystems, + RollbackFrameCount, SaveWorld, SaveWorldSystems, +}; use bevy::app::{App, Plugin}; -use bevy::prelude::{Children, Component, Entity, EntityCommands, EntityMut, EntityRef, EntityWorldMut, IntoScheduleConfigs, Local, Query, QueryState, Res, World}; +use bevy::prelude::{ + Children, Component, Entity, EntityCommands, EntityMut, EntityRef, EntityWorldMut, + IntoScheduleConfigs, Local, Query, QueryState, Res, World, +}; use ggrs::Frame; -use crate::{AdvanceWorld, AdvanceWorldSystems, ConfirmedFrameCount, LoadWorld, LoadWorldSystems, RollbackFrameCount, SaveWorld, SaveWorldSystems}; -use crate::snapshot::despawn::private::RollbackDespawnCommandExtensionSeal; +use std::cmp::Ordering; /// Marks an entity as despawned, contains the frame that the entity was despawned on. /// @@ -54,8 +60,14 @@ impl Plugin for RollbackDespawnPlugin { fn build(&self, app: &mut App) { app.register_disabling_component::(); - app.add_systems(LoadWorld, resurrect_entities.in_set(LoadWorldSystems::EntityResurrect)) - .add_systems(AdvanceWorld, despawn_confirmed_entities.in_set(AdvanceWorldSystems::DespawnConfirmed)); + app.add_systems( + LoadWorld, + resurrect_entities.in_set(LoadWorldSystems::EntityResurrect), + ) + .add_systems( + AdvanceWorld, + despawn_confirmed_entities.in_set(AdvanceWorldSystems::DespawnConfirmed), + ); } } @@ -74,7 +86,9 @@ fn resurrect_entities( }) .collect::>() .into_iter() - .for_each(|entity| { world.entity_mut(entity).remove::(); }); + .for_each(|entity| { + world.entity_mut(entity).remove::(); + }); } fn despawn_confirmed_entities( @@ -97,7 +111,9 @@ fn despawn_confirmed_entities( }) .collect::>() .into_iter() - .for_each(|entity| { world.despawn(entity); }); + .for_each(|entity| { + world.despawn(entity); + }); } mod private { diff --git a/src/snapshot/mod.rs b/src/snapshot/mod.rs index 9db196a..e374762 100644 --- a/src/snapshot/mod.rs +++ b/src/snapshot/mod.rs @@ -8,6 +8,7 @@ mod childof_snapshot; mod component_checksum; mod component_map; mod component_snapshot; +mod despawn; mod entity; mod entity_checksum; mod resource_checksum; @@ -18,8 +19,8 @@ mod rollback_app; mod rollback_entity_map; mod set; mod strategy; -mod despawn; +use crate::snapshot::despawn::RollbackDespawnPlugin; pub use checksum::*; pub use childof_snapshot::*; pub use component_checksum::*; @@ -35,7 +36,6 @@ pub use rollback_app::*; pub use rollback_entity_map::*; pub use set::*; pub use strategy::*; -use crate::snapshot::despawn::RollbackDespawnPlugin; pub mod prelude { pub use super::{Checksum, LoadWorldSystems, SaveWorldSystems}; diff --git a/src/snapshot/set.rs b/src/snapshot/set.rs index 0670b68..0d71972 100644 --- a/src/snapshot/set.rs +++ b/src/snapshot/set.rs @@ -79,10 +79,7 @@ impl Plugin for SnapshotSetPlugin { ) .configure_sets( SaveWorld, - ( - SaveWorldSystems::Checksum, - SaveWorldSystems::Snapshot - ).chain(), + (SaveWorldSystems::Checksum, SaveWorldSystems::Snapshot).chain(), ) .configure_sets( AdvanceWorld, From 3a51fc03c1615ad2114660a48596c0c30bdf6b7f Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Thu, 22 Jan 2026 16:29:33 +0000 Subject: [PATCH 3/6] Make component and commands available (not sure that the component needs to be public?) --- src/snapshot/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/snapshot/mod.rs b/src/snapshot/mod.rs index e374762..6d2c31d 100644 --- a/src/snapshot/mod.rs +++ b/src/snapshot/mod.rs @@ -39,6 +39,7 @@ pub use strategy::*; pub mod prelude { pub use super::{Checksum, LoadWorldSystems, SaveWorldSystems}; + pub use super::despawn::{RollbackDespawned, RollbackDespawnCommandExtension}; } /// Label for the schedule which loads and overwrites a snapshot of the world. From 02659a51a7c8a6bd4c8a10f7e20a1f344c7b27ad Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Thu, 22 Jan 2026 16:31:12 +0000 Subject: [PATCH 4/6] Pub use despawn --- src/snapshot/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snapshot/mod.rs b/src/snapshot/mod.rs index 6d2c31d..6821baf 100644 --- a/src/snapshot/mod.rs +++ b/src/snapshot/mod.rs @@ -20,7 +20,7 @@ mod rollback_entity_map; mod set; mod strategy; -use crate::snapshot::despawn::RollbackDespawnPlugin; +pub use despawn::*; pub use checksum::*; pub use childof_snapshot::*; pub use component_checksum::*; From 4b28ad04c95bc446c5f3ed91f5ece71ee2f18788 Mon Sep 17 00:00:00 2001 From: Adam Mitchell Date: Thu, 22 Jan 2026 16:58:24 +0000 Subject: [PATCH 5/6] Cleanup --- src/snapshot/despawn.rs | 7 ++----- src/snapshot/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/snapshot/despawn.rs b/src/snapshot/despawn.rs index 2598c96..63773fa 100644 --- a/src/snapshot/despawn.rs +++ b/src/snapshot/despawn.rs @@ -35,13 +35,10 @@ use crate::snapshot::despawn::private::RollbackDespawnCommandExtensionSeal; use crate::{ AdvanceWorld, AdvanceWorldSystems, ConfirmedFrameCount, LoadWorld, LoadWorldSystems, - RollbackFrameCount, SaveWorld, SaveWorldSystems, + RollbackFrameCount, }; use bevy::app::{App, Plugin}; -use bevy::prelude::{ - Children, Component, Entity, EntityCommands, EntityMut, EntityRef, EntityWorldMut, - IntoScheduleConfigs, Local, Query, QueryState, Res, World, -}; +use bevy::prelude::*; use ggrs::Frame; use std::cmp::Ordering; diff --git a/src/snapshot/mod.rs b/src/snapshot/mod.rs index 6821baf..a1c8750 100644 --- a/src/snapshot/mod.rs +++ b/src/snapshot/mod.rs @@ -20,12 +20,12 @@ mod rollback_entity_map; mod set; mod strategy; -pub use despawn::*; pub use checksum::*; pub use childof_snapshot::*; pub use component_checksum::*; pub use component_map::*; pub use component_snapshot::*; +pub use despawn::*; pub use entity::*; pub use entity_checksum::*; pub use resource_checksum::*; @@ -38,8 +38,8 @@ pub use set::*; pub use strategy::*; pub mod prelude { + pub use super::despawn::{RollbackDespawnCommandExtension, RollbackDespawned}; pub use super::{Checksum, LoadWorldSystems, SaveWorldSystems}; - pub use super::despawn::{RollbackDespawned, RollbackDespawnCommandExtension}; } /// Label for the schedule which loads and overwrites a snapshot of the world. From 5f361f15c77b9dce77e788186325192a98f0387d Mon Sep 17 00:00:00 2001 From: Georg Schuppe Date: Fri, 20 Mar 2026 21:04:05 +0100 Subject: [PATCH 6/6] fix: clean up RollbackDespawnPlugin for merge - Replace wrong doc example with a correct despawn_rollback usage example - Remove unimplemented todo!() methods (despawn_children_rollback, despawn_related_rollback) - Remove unnecessary sealed trait pattern (no other seals exist in codebase) - Rebase onto main --- src/snapshot/despawn.rs | 53 +++++++++++------------------------------ 1 file changed, 14 insertions(+), 39 deletions(-) diff --git a/src/snapshot/despawn.rs b/src/snapshot/despawn.rs index 63773fa..b9e8b4d 100644 --- a/src/snapshot/despawn.rs +++ b/src/snapshot/despawn.rs @@ -8,31 +8,25 @@ //! # Examples //! ```rust //! # use bevy::prelude::*; -//! # use bevy_ggrs::{prelude::*, ResourceChecksumPlugin}; +//! # use bevy_ggrs::prelude::*; //! # -//! # const FPS: usize = 60; -//! # -//! # type MyInputType = u8; -//! # -//! # fn read_local_inputs() {} -//! # -//! # fn start(session: Session>) { -//! # let mut app = App::new(); -//! #[derive(Resource, Clone, Hash)] -//! struct BossHealth(u32); -//! -//! // To include something in the checksum, it should also be rolled back -//! app.rollback_resource_with_clone::(); -//! -//! // This will update the checksum every frame to include BossHealth -//! app.add_plugins(ResourceChecksumPlugin::::default()); -//! # } +//! fn despawn_player( +//! mut commands: Commands, +//! players: Query>, +//! ) { +//! for entity in &players { +//! // Instead of commands.entity(entity).despawn(), use despawn_rollback() +//! // so the entity can be resurrected if a rollback happens. +//! commands.entity(entity).despawn_rollback(); +//! } +//! } +//! # #[derive(Component)] +//! # struct Player; //! ``` //! //! Entities which have been marked for despawn are disabled using the [`RollbackDespawned`] //! component, so they will appear as though they are despawned to normal system queries. -use crate::snapshot::despawn::private::RollbackDespawnCommandExtensionSeal; use crate::{ AdvanceWorld, AdvanceWorldSystems, ConfirmedFrameCount, LoadWorld, LoadWorldSystems, RollbackFrameCount, @@ -113,37 +107,18 @@ fn despawn_confirmed_entities( }); } -mod private { - pub trait RollbackDespawnCommandExtensionSeal {} -} -pub trait RollbackDespawnCommandExtension: private::RollbackDespawnCommandExtensionSeal { +pub trait RollbackDespawnCommandExtension { /// Despawns this entity and its children recursively using the [`RollbackDespawned`] /// component, such that they can be resurrected following a rollback. /// /// NOTE: This does not yet support [`RelationshipTarget`] with linked spawn mode. fn despawn_rollback(&mut self); - - /// NOTE: Not implemented yet. - fn despawn_children_rollback(&mut self) -> &mut Self; - - /// NOTE: Not implemented yet. - fn despawn_related_rollback(&mut self) -> &mut Self; } -impl RollbackDespawnCommandExtensionSeal for EntityCommands<'_> {} - impl RollbackDespawnCommandExtension for EntityCommands<'_> { fn despawn_rollback(&mut self) { self.queue_silenced(despawn_rollback); } - - fn despawn_children_rollback(&mut self) -> &mut Self { - todo!() - } - - fn despawn_related_rollback(&mut self) -> &mut Self { - todo!() - } } fn despawn_rollback(mut entity: EntityWorldMut) {