Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions rfcs/Subworlds.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# Feature Name: `Subworlds`

## Summary

Currently, `bevy` contains a single _global_ `World` which holds all entities/components, which are operated on via `Queries` and `Systems`. However, there are uses for disjoint sets of entities that can operate independently from one another. This RFC proposes a concept of `Subworlds`, which are individually a distinct collection of entities and their components. In this model, `Queries` and `Systems` can operate on these `Subworlds` independently and concurrently. This provides the ability to encapsulate sets of entities into `Subworlds`, reducing some of the complexity and overhead that came with grouping entities in the _global_ `World` (via _marker-components_ for example). By excluding large sets of entities that live in other `Subworlds`, `Systems` and `Queries` can more efficiently perform their logic on a smaller, more concise set of components.

## Motivation 🏃

The primary motivation for using `Subworlds` to allows systems and queries to operate on disjoint sets of entities and their components.

## Guide-level explanation 📝

### 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).
Copy link
Member

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?

- _Note_: I may use `World` and `Subworld` interchangeably.
- `Worlds`: This can be thought of as a collection of `Subworlds`.

### Example ⭐
- Your game is composed of levels.
- The player is currently on level 1 and nearing the end, so you would like to begin loading level 2.
- Level 1 and 2 are completely disjoint and do not share any data (except perhaps the player themselves).
- The difficulty resides in the fact that entities from levels 1 and 2 contain similar components, but the entities themselves are disjoint.
- In order to begin populating level 2's entities, something must exist to distinguish the two sets of entities, so that systems operating for level 1 (e.g. _physics_) do not operate on level 2's entities, even though they may contain physics components.
- Only having a single `World` requires that systems filter the entities based on what level they belong to, adding overhead and complexity to either the `Query`, the system, the components describing the entities, or a combination.
- `Subworlds` would provide a clean and efficient solution to this problem.
- The primary systems `[physics, render, etc...]` would only operate on Level 1's `Subworld`.
- A `load_next_level` system could be running concurrently on Level 2's `Subworld`.
- Then when Level 2 begins, the primary systems operate the on later `Subworld` while the former one gets saved/dropped.

### Things `Subworlds` would allow 🙂
- Running systems on subworld `A` on thread `A`, while running the same systems on subworld `B` on thread `B`.
- 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.
Copy link
Member

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 :)


## Reference-level explanation 🧑‍🏫

TODO: expand upon this

```rust
struct Worlds {
subworlds: HashMap<WorldId, World>,
Copy link
Member

@cart cart May 21, 2021

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.

Copy link
Member

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.

Copy link
Member

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.

}

struct World { // aka `Subworld`
entities: Entities,
components: Components,
// more stuff
}
```

### Example API (This is not final!) 💻
```rust
fn setup(mut commands: Commands) {
commands
.spawn() // this will spawn the entity into the default world
.create_world("World2")
Copy link
Member

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.

Copy link
Author

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 :)

Copy link
Member

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.

.create_world("World3")
.set_world("World2") // sets the current world being operated on
.spawn(); // will spawn this entity into "World2"
}

fn on_level_change(mut commands: Commands) {
commands
.remove_world("World2") // removes the world and all of it's components
.add_world("AnotherWorld")
.with_system(some_system.system())
.unwrap();
}

fn main() {
App::build()
// 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())
Copy link
Member

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.

Copy link
Author

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 🥳

Copy link
Member

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.

// add this system which runs on the Default world and World2
.add_world_system(["Default", "World2"], two_world_system.system())
Copy link
Member

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?

// provide a RunCriteria
.add_world_set(
WorldSet::new()
.with_run_criteria(some_criteria.system())
.with_system(another_system.system())
.with_worlds(["World2"])
)
.run();
}
```

- By default, there is a single `World` contained within the `Worlds` (this mirrors the previous behavior of `bevy`).
- Queries and systems still only operate entities and components from a single `World`.
- However, systems can be applied (concurrently) to multiple `Worlds` at a time.

## Drawbacks 🙁

In theory adding a dynamic number of `Subworlds` would add overhead since they must be kept in (something like) a `HashMap<Id, Subworld>`, where as having a single _global_ `World` does not incur this overhead.

## Rationale and alternatives 📜

- Using _marker-components_ to distinguish entities that reside in disjoint sets.
Copy link
Member

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.

- This works, however, if `EntitiesA` {E1, E2, E3} are completely disjoint from `EntitiesB` {E4, E5, E6}, you shouldn't have to keep them grouped into a single set (`World`).
- Also, when constructing queries, this adds the overhead of need to check all entities instead of the entities of a specific `Subworld` that you are interested in.

- _Relations_ could also be a potential solution to this problem.
- However the same problems described above apply.
- The overhead of needing to check entity relationships exceeds that of running a query on a specific `Subworld`.
- Overuse of _Relations_ and _marker-components_ could also lead to extreme archetypal fractaling, and worse case a single archetype-per-entities scenario.

## Prior art 🎨

- See [here](https://discord.com/channels/691052431525675048/749335865876021248/834310334264115210) for the initial discord discussion that sparked this RFC.

- The popular C++ ECS library [EnTT](https://github.com/skypjack/entt) supports this behavior, and even encourages it, via giving you direct access their `World` structure (`EnTT` calls these _registries_).
```cpp
auto level1 = entt::registry{}; // registry is analogous to bevy's World.
auto level2 = entt::registry{};

// depending on the current level, we can run the same system on a different sub-<worlds/registries>.
some_system(level1);
some_system(level2);
```
- Currently supporting this is `bevy` is not possible, as systems and the `World` they operate on are closely coupled.
- `EnTT` is able to support this since systems are nothing more than free functions.

## 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>`).
Copy link
Member

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.

Copy link
Author

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.

Copy link

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.

- 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?
Copy link
Member

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.

- 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`?
Copy link
Member

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.


## Future possibilities 🚀

- Entity move
- World-specific resources