-
Notifications
You must be signed in to change notification settings - Fork 41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor into Modular Plugins #76
Conversation
Thanks for working on this! I really like the modularity of this approach. Feels elegant that you could just write your own plugin if the ones we provide aren't a good fit. Relatively easy to understand how it works.
Yes, would probably make sense to expose a (sane) entity map as a resource... Would probably need to be two steps? One for adding mappings, and one for applying them... Not sure how that would work with our hack?
Maybe another approach is to provide access to the latest confirmed frame, and just clear frames older than that? Knowing latest confirmed frame could be really useful anyway. |
The goal is to split the responsibilities of `WorldSnapshot` into modular plugins.
Ok so this is turning into a pretty major re-factor of
Using these plugins, the app.add_plugins((
GgrsChecksumPlugin,
GgrsResourceSnapshotClonePlugin::<Checksum>::default(),
GgrsEntitySnapshotPlugin,
GgrsComponentSnapshotReflectPlugin::<Parent>::default(),
GgrsComponentMapEntitiesPlugin::<Parent>::default(),
GgrsComponentSnapshotReflectPlugin::<Children>::default(),
GgrsComponentMapEntitiesPlugin::<Children>::default(),
)); While this is a lot of plugins (and I've placed each into their own file which creates a lot of verbosity compared to impl<C> GgrsComponentChecksumHashPlugin<C>
where
C: Component + Hash,
{
pub fn update(
mut commands: Commands,
components: Query<&C, (With<Rollback>, Without<ChecksumFlag<C>>)>,
mut checksum: Query<&mut ChecksumPart, (Without<Rollback>, With<ChecksumFlag<C>>)>,
) {
let mut hasher = DefaultHasher::new();
let Ok(mut checksum) = checksum.get_single_mut() else {
commands.spawn((ChecksumPart::default(), ChecksumFlag::<C>::default()));
return;
};
for component in components.iter() {
component.hash(&mut hasher);
}
*checksum = ChecksumPart(hasher.finish());
}
}
impl<C> Plugin for GgrsComponentChecksumHashPlugin<C>
where
C: Component + Hash,
{
fn build(&self, app: &mut App) {
app.add_systems(SaveWorld, Self::update);
}
} Because of how they're segregated along |
Love it!
I wonder if this affects performance?
This seems super-clean to me :) |
Had it backwards. The kind of bug that only early morning coffee can find.
Thanks!
Honestly unsure. I think it would help with compiler optimisations, since the type information will be held onto all the way til it's taken by Bevy itself. What I am confident in saying is this framework would allow for the creation of the optimal solution. For example, it may be much better to create a custom plugin for adding/removing
Again, thanks! |
I'm marking this PR as ready for review because at this point I'd really appreciate some more specific feedback on possible changes. Currently, I believe this PR "just works", and doesn't even require any updates to existing games, since it mostly reuses existing API. A key exception is rollback resources/components added with I've made lots of very opinionated changes, but I'm completely open to revisiting any of my choices if they're a poor fit for the project. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the general direction and API changes.
I really wanted to finish reviewing this today, but I noticed I got a bit tired half-way through and my comments were getting sloppier.
Not sure when I will have time to continue, so posting what I've got so far, even though there most likely are some stupid questions in there :P
What I've read so far is great, though :) Also love that you documented things fairly well.
IMO it would be better to break backwards source-compatibility and just make what we think is the best API going forwards |
…napshotSetPlugin`
Sorting by `Rollback` where possible, and moved to `wrapping_add` otherwise
I do agree with this sentiment, especially considering Bevy itself is still breaking its API at a somewhat regular pace (not a complaint, there's amazing progress being made!). I would like to have a crack at making a little plugin builder to facilitate the syntax we've discussed previously: app.add_plugins((
Rollback::for_resource<Foo>().with_clone().with_mapping(),
Rollback::for_component<Bar>().with_copy(),
)); |
src/lib.rs
Outdated
/// | ||
/// NOTE: Unlike previous versions of `bevy_ggrs`, this will no longer automatically | ||
/// apply entity mapping through the [`MapEntities`](`bevy::ecs::entity::MapEntities`) trait. | ||
/// If you require this behavior, see [`GgrsComponentMapEntitiesPlugin`]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You hint towards the documentation of this plugin, but I cannot find any helpful text there that would further educate the user how to deal with this.
In addition to extra documentation, we probably need a parented example (e.g. cube with smaller child cube on top) that showcases usage of this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, I've gone over all the plugins and added some extra documentation and at least 1 example demonstrating how to use the plugin. As a benefit, I've ensured every example properly compiles as a doc-test, so this has also increased test coverage.
As for the example, I completely agree. I think it would be good to maybe work on a stress-test example, as a benchmark. Since, as far as I can tell, there currently isn't really a benchmark in this crate for testing performance regression against.
…ulation Should mildly improve performance and reduce noise in tracing when accessing schedules
Removed Redundant `apply_deferred` steps; rollback tracking of Checksum
To spur discussion, I've started profiling this PR versus the current main branch, just to see what (if any) performance changes this would introduce. Running the current sync test example on my laptop, this PR increases running time for the My hypothesis is that for a more realistic application, the added overhead will be outweighed by the ability to run systems in parallel. Regardless of whether that theory holds or not, I will also be spending the next couple of days seeing if I can tune the performance. |
Once I got home I did some more testing, this appears to have a massive performance boost for large quantities of rollback components like I suspected. On my laptop I ran the following: cargo run --example box_game_synctest --release --features bevy/trace_chrome -- --num-players 1000 --check-distance 5
On the main branch, I can't even run above 1,000 players, it just crashes on startup. If I instead run this PR with "only" 1,000 players, loading and saving both drop down to 0.26ms. Failure to run doesn't occur until above 15,000 players. Tomorrow I'm going to try producing some charts to visualise the scaling difference. Based on this rudimentary testing tho, it appears my hunch was correct in that the overhead of additional save/load systems is totally negligible once a significant number of rollback entities and data is at play. |
What exactly do you mean by player? Cubes per player? In any case, very cool and promising results :) |
Leaving the box game test unchanged and setting the command line |
Performance TestingEquipment
Procedure
Results
InterpretationThis PR improves performance for the box game sync-test example where there are more than 10 players, by an order of magnitude. Since 10 rollback entities is (in my opinion) an expected number of entities to include in a typical multiplayer game, it is reasonable to state that this PR improves performance in all expected cases. |
I had a go at making a stress test example #78. Can be cherry-picked on top of this PR. An interesting finding is that I seem to be dropping a lot of frames when spawning a bunch of new particles, but then it runs smoothly afterwards. I also made branch to run on main for comparison: https://github.com/gschup/bevy_ggrs/compare/main...johanhelsing:bevy_ggrs:particles-stress-test-old?expand=1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is already a huge improvement both in terms of extensibility and performance.
As far as I'm concerned, this is good to go :)
We can always pursue further performance improvements in other PRs.
Since `Rollback`'s should be added to `RollbackOrdered` in an already ordered manner, back-first sorting should only reach depth 1 the vast majority of the time, effectively avoiding sorting entirely.
Ok I'm done tinkering with this PR now. I made one last change where I've created a app.add_plugins((
ComponentSnapshotPlugin::<ReflectStrategy<Parent>>::default(),
ComponentSnapshotPlugin::<CloneStrategy<Foo>>::default(),
ResourceSnapshotPlugin::<CopyStrategy<Bar>>::default(),
)); A /// A [`Strategy`] based on [`Clone`]
pub struct CloneStrategy<T: Clone>(PhantomData<T>);
impl<T: Clone> Strategy for CloneStrategy<T> {
type Target = T;
type Stored = T;
#[inline(always)]
fn store(target: &Self::Target) -> Self::Stored {
target.clone()
}
#[inline(always)]
fn load(stored: &Self::Stored) -> Self::Target {
stored.clone()
}
} This changes nothing about the behaviour of this PR so none of the reviews up until this point should be affected. |
Whoops! I tidied it to death, I'll fix that import issue once I'm back home! |
Really nice idea with the Strategy! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After previous reviewers did such a great job, it's hard to find any glaring issues! This really feels like a giant leap forward from the ugly chunk of code that was world_snapshot.rs
. Thank you so much for the work!
ComponentSnapshotPlugin::<ReflectStrategy<Parent>>::default(), | ||
ComponentMapEntitiesPlugin::<Parent>::default(), | ||
ComponentSnapshotPlugin::<ReflectStrategy<Children>>::default(), | ||
ComponentMapEntitiesPlugin::<Children>::default(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this reads much cleaner now, nice!
Thank you very much for saying that, it really means a lot!
No trouble at all, it was my pleasure! And thanks to yourself, @johanhelsing, and @nezuo for the help along the way! |
Objective
For a long time now, there's been a desire to avoid the requirement for
Reflection
in order to rollback components and resources. Since #66 has been merged, we now have the option of adding completely custom rollback behaviour either as an end user, or withinbevy_ggrs
.Solution
I have created a selection of plugins, which (with some helper data structures) demonstrate how we could add completely custom rollback behaviour. Because these plugins don't overlap on any mutable parameters (with some reasonable exceptions), their systems can run entirely in parallel, as scheduled by Bevy. This has the potential to provide a massive performance improvement, even without smarter implementations. For example, the Bevy smart points (
Res
,ResMut
,Mut
,Ref
, etc.) all contain aTicks
item, which records when items were last added/changed. It would be pretty trivial to store that information along side the snapshot, avoiding rollback entirely for unchanged items, and thus avoiding noisy change detection (Changed
,Added
, etc.) even withoutbypass_change_detection
.Usage
When setting up an app, end users can either use extension methods provided by
GgrsApp
:Or by adding the underlying plugins directly:
Plugins
GgrsPlugin
The main entry point for
bevy_ggrs
, providing a sensible default starting point.EntitySnapshotPlugin
A [
Plugin
] which manages the rollback forEntities
. This will ensure allEntities
match the state of the desired frame, or can be mapped using a [RollbackEntityMap
], which this [Plugin
] will also manage.This can be considered one of the core plugins, since it is responsible for ensuring the entity graph itself is ready, allowing other systems to manage resources and components.
ComponentSnapshotPlugin<S>
A [
Plugin
] which manages snapshots for a [Component
] a provided [Strategy
]S
.The three built-in strategies provided are:
CloneStrategy
: Clones data and stores as the original type.CopyStrategy
: Copies data and stores as the original type.ReflectStrategy
: Reflects the data, clones it, and stores asBox<dyn Reflect>
.ResourceSnapshotPlugin<S>
A [
Plugin
] which manages snapshots for a [Resource
] a provided [Strategy
]S
.The three built-in strategies provided are:
CloneStrategy
: Clones data and stores as the original type.CopyStrategy
: Copies data and stores as the original type.ReflectStrategy
: Reflects the data, clones it, and stores asBox<dyn Reflect>
.ChecksumPlugin
A [
Plugin
] which creates a [Checksum
] resource which can be read after the [SaveWorldSet::Snapshot
] set in the [SaveWorld
] schedule has been run. To add you own data to this [Checksum
], create an [Entity
] with a [ChecksumPart
] [Component
]. Every [Entity
] with this [Component
] will participate in the creation of a [Checksum
].Note that this plugin does not store [
Checksums
] for older frames.ComponentChecksumHashPlugin<C>
A [
Plugin
] which will track the [Component
]C
onRollback Entities
and ensure a [ChecksumPart
] is available and updated. This can be used to generate aChecksum
.ComponentMapEntitiesPlugin<C>
A [
Plugin
] which updates the state of a post-rollback [Component
]C
using [MapEntities
].SnapshotSetPlugin
Sets up the [
LoadWorldSet
] and [SaveWorldSet
] sets, allowing for explicit ordering of rollback systems across plugins.ResourceChecksumHashPlugin<R>
Plugin which will track the [
Resource
]R
and ensure a [ChecksumPart
] is available and updated. This can be used to generate aChecksum
.ResourceMapEntitiesPlugin<R>
A [
Plugin
] which updates the state of a post-rollback [Resource
]R
using [MapEntities
].Outstanding Issues
Components and resources which require mapping don't have an easy way to gain access to theGot a map resource available now,EntityMap
generated duringload_world
. I think this should be added as a resource to theLoadWorld
schedule as a simple fix.with some bugs. Bugs are now gone!but it should be set automatically based on the session (I believe GGRS provides a "max rollback distance" parameter we could use). I'm now asking theSession
for the currentconfirmed_frame
, and anything older than that is discarded.I've simply added these plugins and not integrated them withAll plugins are accessible throughGgrsPlugin
orGgrsApp
. I think it might be desirable to have a clean way to add rollback functionality likeregister_rollback_component
.GgrsApp
methods.I believe it is possible to create a more genericImplemented as described.RollbackResourcePlugin
/RollbackComponentPlugin
that can accept aStrategy
, which would be an object describing how to store, load, and update a rollback item.