-
Notifications
You must be signed in to change notification settings - Fork 66
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
Bevy Subworld RFC #16
base: main
Are you sure you want to change the base?
Conversation
After a first skim, I have three other use cases in mind for this:
Do those sound feasible to you with this design? |
- Running systems on specific disjoint groups of entities based on some criteria. | ||
- Running systems on subworld `A` every frame and on subworld `B` every 10 seconds. | ||
- Helps systems stay clean and not require overuse of _marker-components_ (and possibly _relations_) which can lead to excessive archetypal fracturing which will end up hindering performance. | ||
- Give developers the choice to use either a _single_, _global_, world that contains every entity (_note_: for some games, that might makes the most sense and be the most performing), or multiple `Subworlds` if they so desire. |
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 less of a benefit and more of a literal description? I'm not sure it fits well here :)
|
||
### Terms & Definitions 📚 | ||
|
||
- `Subworld`: This refers to the original definition of a `World` in bevy 0.5. (essentially a collection of components which belong to a set of entities). |
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'm not sure I think "Subworld" is a useful term. What is it part of, as implied by sub-?
Would "multiple Worlds" capture the same idea in a clearer way?
fn setup(mut commands: Commands) { | ||
commands | ||
.spawn() // this will spawn the entity into the default world | ||
.create_world("World2") |
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.
We'll want to steal the Labels API from SystemLabels / StageLabel etc. for 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.
Yea, this is what I assumed we should use :)
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.
Excellent. As you polish this up you should call this out explicitly to make the design as clear as possible.
// This system will operate on *all* worlds by default | ||
.add_system(general_system.system()) | ||
// add this system which only operates on world 2 | ||
.add_world_system(["World2"], world2_system.system()) |
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 think that we should try to get better composable system insertion syntax before moving forward with this proposal. Swapping to the builder pattern shouldn't be hard, but needs its own RFC.
We're already in a rough place in terms of combinatorial API explosion, and this adds another layer.
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.
Yea that's a fair point. Would you have a link/reference to an issue/PR associated with this task?
I could potentially help move it forward as well 🥳
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.
Apparently I'd only discussed it informally! Here's a fresh one for you: bevyengine/bevy#1978
Should be a fairly straightforward change.
|
||
## Rationale and alternatives 📜 | ||
|
||
- Using _marker-components_ to distinguish entities that reside in disjoint sets. |
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.
Marker components don't work well for disjoint sets where you don't know the size at compile time. This is the "dynamic component problem".
Relations can get around this to some degree but have different semantics, and "tag components" or the like are not yet implemented or designed at all.
|
||
## Unresolved questions ❓ | ||
|
||
- How do resources behave in respect to each `Subworld`? Are they shared between them or made exclusive to each. (or both?? E.g. `Res<T>`/`ResMut<T>` vs `Local<T>`). |
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.
Given that Resources are now part of World
, the default behavior would likely be to be isolated to each Subworld. I'm not sure that's the desired behavior though.
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.
Yes I agree that it's not the desired behavior. I'll update this to better reflect the desire/intention of Resources.
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 would think the asset server at the very least needs to be shared between worlds. Though storing handles to resources should be per world, so that when the world is dropped the handle can be freed.
- Should/Would it be possible for `Subworlds` to communicate? Or more specifically, the entities inside them to communicate? | ||
- What would the API look like for creating/modifying/removing `Subworld`s and how would you prescribe systems to run on specific sets of `Subworld`. E.g. a `SubworldRunCriteria`? | ||
- We should aim to **not** actually change the name `World` to `Subworld`. And instead introduce some new type `Worlds` (which is a collection of the sub-worlds). | ||
- How does this interact with `Stages` and `RunCriteria`? |
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.
Being able to desynchronize schedules across worlds would be very interesting, but may result in serious scope creep.
|
||
- How do resources behave in respect to each `Subworld`? Are they shared between them or made exclusive to each. (or both?? E.g. `Res<T>`/`ResMut<T>` vs `Local<T>`). | ||
- How do we support moving/copying entities and their component from one `Subworld` to another? | ||
- Should/Would it be possible for `Subworlds` to communicate? Or more specifically, the entities inside them to communicate? |
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.
Perhaps via shared resources? This is particularly compelling since Events are stored as resources.
// add this system which only operates on world 2 | ||
.add_world_system(["World2"], world2_system.system()) | ||
// add this system which runs on the Default world and World2 | ||
.add_world_system(["Default", "World2"], two_world_system.system()) |
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 would love to see a "for each world, run this system" API as well. I expect this will be the common case.
Perhaps with added filtering?
How will subworlds interact with the scheduler? Specifically stages, states, and exclusive systems. |
I'm wondering if this needs to be an engine level thing or if it can just be its own library with the subworlds as resources. Having it as a library pretty much clears up all the questions about how the resources are shared and also allows for the consumers to set it up as needed for their use case. |
I would be very interested to see if we could use this idea to temporarily store removed entities / components / relations in a purgatory world. I think that would clean up both the API and the impl in a satisfying way. |
`@sircarter had an excellent comment that I don't want to get lost:
|
This has some overlap with my pipelined rendering experiments:
My current experimental solution is pub struct App {
pub world: World,
pub runner: Box<dyn Fn(App)>,
pub schedule: Schedule,
sub_apps: Vec<SubApp>,
}
struct SubApp {
app: App,
runner: Box<dyn Fn(&mut World, &mut App)>,
}
impl Plugin for PipelinedRenderPlugin {
fn build(&self, app: &mut App) {
/* more render stuff here */
let mut render_app = App::empty();
let mut extract_stage = SystemStage::parallel();
// don't apply buffers when the stage finishes running
// extract stage runs on the app world, but the buffers are applied to the render world
extract_stage.set_apply_buffers(false);
render_app
.add_stage(RenderStage::Extract, extract_stage)
.add_stage(RenderStage::Prepare, SystemStage::parallel())
.add_stage(RenderStage::Render, SystemStage::parallel());
app.add_sub_app(render_app, |app_world, render_app| {
// extract
extract(app_world, render_app);
// prepare
let prepare = render_app
.schedule
.get_stage_mut::<SystemStage>(&RenderStage::Prepare)
.unwrap();
prepare.run(&mut render_app.world);
// render
let render = render_app
.schedule
.get_stage_mut::<SystemStage>(&RenderStage::Render)
.unwrap();
render.run(&mut render_app.world);
});
}
impl Plugin for PipelinedSpritePlugin {
fn build(&self, app: &mut App) {
// 0 is currently implicitly the "render app". A more complete solution would use typed labels for each app.
app.sub_app_mut(0)
.add_system_to_stage(RenderStage::Extract, extract_sprites.system())
.add_system_to_stage(RenderStage::Prepare, prepare_sprites.system()));
/* more render stuff here */
}
} |
I'm definitely not advocating for SubApps as the solution at this point. Just sharing where my head is at / what my requirements currently are. |
From Discord: we could use this as a tool to get closer to "Instant" command processing. Have three default worlds (or more likely two shadow worlds for "normal" world):
This would be fantastic for prototyping and ergonomics, and has a nice clear mental model. |
|
||
```rust | ||
struct Worlds { | ||
subworlds: HashMap<WorldId, World>, |
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.
If we use types for World identifiers (ex: MainWorld
, RenderWorld
, etc) and include those in Query types (which has been discussed elsewhere in this RFC / on discord), then we could remove the current runtime Query WorldId checks (that ensure the query matches the world it was created for) in favor of compile time type safety.
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.
Eh actually maybe not, unless we make it impossible to create two worlds with the same type identifier.
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.
That seems like a perfectly sensible restriction to have.
I would like to say that having separate worlds alone may be not enough. Imagine that you have some simulation - not only you want to isolate it entities and systems/queries, it is also desirable to tick/update it at specific rate. You may want to have that simulation real-time synced, or may want to jump forward in time, by simulating hundreds of world frames at once (probably while not locking main world). In other words, tick world manually may be necessary, fixed step is not always an option. So I would vote for something close to separate apps... That would also make development of certain game aspects (like mini-games, smart 3D HUD/interface, etc...) more isolated/modular. You can make your mini game as separate project, then just attach it to main one. (This could be achieved with plugins as well - but you can touch something in main world you want/should not) |
@tower120 Subworlds may not be enough for ever use case, but they are still a useful additional and one that can be added independent of multiple executors/synced apps. Although for your examples I can imagine some good solutions that don't require multiple apps. |
Agreed. I think we'll want "multiple ECS schedules" and "multiple worlds" with the ability to pass an arbitrary number of worlds into a given schedule. I think "multiple apps" currently implies a tight [Schedule, World] coupling, which I think isn't what we want. Instead, I think we want to allow "arbitrary orchestrator logic" to determine how Schedules execute on Worlds. That way you don't need to create a bunch of empty Schedules when all you need is "staging worlds" for your app logic. |
@cart Ok, that sounds even nicer. But what about Time resource? Doesn't schedulers coupled with time? And I think I want to have some kind of "manual" scheduler, where you can do |
For this, the current custom runner abstraction works pretty well; I think that we could integrate that nicely into what Cart's proposing to get a really lovely abstraction. |
Lots of things rely on specific System/World pairs. As long as the Time used by app logic is stored in the "app world" and it is ticked using the "app schedule", things will work as expected. |
@alice-i-cecile Looks like that "runner" blocking.... It would be much nicer to BTW, why do you want "subworlds" (like hierarchical?), not just "worlds"(plain) ? |
The initial implementation was easier if you stored them in Resources :P From my perspective, I'd enjoy a bit of hierarchy to have nice "purgatory" or "command staging" worlds associated with each "proper" world. |
We can always have freestanding Worlds that live inside of components / resources. But imo if we're integrating with the scheduler it should only care about "global top level worlds" |
BTW, Unity ECS has interesting feature of world merging. In Unity ECS archetype's storage consist of 16Kb chunk linked list. So merge is close to just changing pointers in list. Maybe bevy could utilize something like that with new multi-world system... |
Ooh thats very clever. We'd need to do that at the table level, not the archetype level (because archetypes are just metadata in Bevy), but its definitely possible. I'm starting to think we need both: Sub WorldsShared entity id space, shared (or easily mergable) metadata ids like ComponentId/ArchetypeId/BundleId, etc. Separate (but easily mergable) Component/Resource storages. These would be extremely useful for a number of scenarios:
The major missing pieces here:
Multiple WorldsCompletely separate Worlds without any shared state. Useful for running parallel tasks/contexts (ex: AppWorld, RenderWorld, NetworkWorld, OtherAppWorld, EditorWorld, etc). The major missing pieces here:
SummaryI think "sub worlds" could yield some pretty significant performance and functionality wins that would impact all Bevy apps, but they are a more complicated endeavor. "Multiple worlds" are only useful in a few specific contexts, but the implementation scope is much smaller. I don't think theres a good way to unify these two scenarios (and I don't think theres much value in doing so). We should probably start two separate efforts to design them. |
I completely agree with your analysis above @cart. Both are very useful, but have entirely distinct use cases and design challenges. Getting the high-level API right is about the only bit that I think should be done in unison. |
I also completely agree with this. |
@cart What about "Universe" concept? Universe is basically EntitySpace (virtually, EntityId generator). Each world constructed_within/constructed_with universe and cannot be moved between universes. P.S. What kind of ECS storage bevy use? (I thought it is archetype-centic...) |
Bevy supports both the table layout, which is often called archetype in other ECS'es and the sparse layout. The default is table, but you can override it to sparse on a per-component basis. |
Just being able to run the same Or heck, even being able to clone a |
My current gut feeling is that the basic design should be:
|
Any updates on this? Is it still planned to implement this? |
A solution for something in this space is still desired, but the ECS crew largely have their hands full pushing towards relations at the current time. |
I've briefly read through this and I'm not sure this solution has been proposed: #[derive(Component)]
struct SomeComponent;
#[derive(World, Invisible)]
struct AlternateInvisibleWorld;
#[derive(World, Visible)]
struct AlternateVisibleWorld;
fn some_system(mut commands: Commands){
// Default is MainWorld
for _ in (0..3) {
commands.spawn(SomeComponent).with_children(|parent| parent.spawn(...));
}
commands.spawn(SomeComponent).in_world(AlternateVisibleWorld).with_children(|parent| parent.spawn(...));
for _ in (0..2) {
commands.spawn(SomeComponent).in_world(AlternateInvisibleWorld).with_children(|parent| parent.spawn(...));
}
}
fn query_all_worlds(
some_main_world_components: Query<SomeComponent, In<MainWorld> >,
some_visible_components: Query<SomeComponent, In<AlternateVisibleWorld> >,
some_invisible_components: Query<SomeComponent, In<AlternateInvisibleWorld> >,
){
assert!(some_components.iter().count() == 3);
assert!(some_visible_components().count() == 1);
assert!(some_invisible_components().count() == 2);
}
// Queries by default use MainWorld
fn query_only_main_world(
some_main_world_components: Query<SomeComponent>,
){
assert!(some_components.iter().count() == 3);
}
fn also_query_all_worlds(
some_main_world_components: Query<SomeComponent, In<AllWorlds> >,
){
assert!(some_components.iter().count() == 6);
}
fn query_union_alternate_worlds(
some_main_world_components: Query<SomeComponent, (In< AlternateVisibleWorld >, In<AlternateInvisibleWorld>) >,
){
assert!(some_components.iter().count() == 3);
} So, ultimately, the design would utilize one |
Rendered
This is based on the discord discussion starting here.
This is a proposal for a
Subworlds
concept: disjoint sets of entities & their components.