-
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
Many Worlds #43
Many Worlds #43
Conversation
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.
Did a pass on typos :)
rfcs/43-many-worlds.md
Outdated
### Why are schedules associated with worlds? | ||
|
||
Each system must be initialized from the world it belongs to, in a reasonably expensive fashion. | ||
Free-floating schedules, |
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.
Seems you have half a sentence here 😄
Would this help making a future bevy editor a reality? Having an |
Co-authored-by: Andreas Weibye <[email protected]>
Oh, that's a neat architecture. I haven't dug deep enough into that problem space to say for sure, but I suspect it's worth exploring. |
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 a fan of the multiple-schedule parts, but I feel like a combination of better control over task scheduling and dedicated thin api over spawning tasks for this use case could work better.
|
||
During each pass of the game loop, each schedule runs in parallel, modifying the world it is assigned to. | ||
These worlds are fully isolated: they cannot view or modify any other world, and stages progress independently across schedules. | ||
Then, the **global world** (which stores common, read-only data) is processed, and any systems in its schedule are run. |
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 worried about the implications of having a lot of centrally stored immutable data, and unsure about how much use it could have. Something that is truly immutable is best left to a rust global, and stuff that needs to be mutated seems like it could cause bottlenecks.
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.
Hmm. Right: if we want to go this route, we should also have GlobalQuery
, to dramatically increase the utility of the Global
world.
You may be correct though, and it would be wonderful to be able to simplify the design so much. I'll chew on use cases some more.
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 global resources are needed. Something like AssetServer
or WgpuSettings
would belong there. Global entities are not a good idea I think, so maybe it doesn't need to be a World
. A global schedule would be good tho. This would be the place to put systems that are exclusive over all worlds.
.set_world(SimulationWorld(i)) | ||
// This ensures basic functionality like `Time` functions properly | ||
// for each of our worlds | ||
.add_plugins(MinimalPlugins) |
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.
Would this even be necessary? I'm assuming something like Time
would be in a global in such an architecture.
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.
Hmm. I think you're right.
|
||
Schedules must be cloneable in order to reasonably spin up new worlds. | ||
|
||
When schedules are cloned, they must become uninitialized again. |
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 doesn't quite feel like "cloning" to me. Perhaps it'd be better described as "apps contain schedule initializers"? Either way, an easy to make multiples of schedules seems like a great idea.
I have a basic proof of concept for 0.5 for the scientific simulation case here. It's very limited in features, but should work well for that narrow use case. |
Here's a task-pool based design for multi-world I had, which can be implemented fully user-side fn add_world_to_app<P, F>(app: &mut App, mut schedule: Schedule, mut sync_fn: F)
where
P: SystemParam,
F: for<'w, 's> FnMut(&mut World, <P::Fetch as SystemParamFetch<'w, 's>>::Item),
for<'w, 's> <P::Fetch as SystemParamFetch<'w, 's>>::Item: SystemParam<Fetch = P::Fetch>,
F: Send + Sync + 'static,
{
let (task_to_world_tx, task_to_world_rx) = async_channel::unbounded();
let (world_to_task_tx, world_to_task_rx) = async_channel::unbounded();
let task = async move {
let mut world = Some(World::new());
let tx = task_to_world_tx;
let rx = world_to_task_rx;
loop {
schedule.run(world.as_mut().unwrap());
tx.send(world.take().unwrap()).await.unwrap();
world = Some(rx.recv().await.unwrap());
}
};
let system = move |system_param: <P::Fetch as SystemParamFetch<'_, '_>>::Item| {
let tx = &world_to_task_tx;
let rx = &task_to_world_rx;
if let Ok(mut world) = rx.try_recv() {
sync_fn(&mut world, system_param);
tx.try_send(world).unwrap();
}
};
app.add_system(system);
app.world
.get_resource::<AsyncComputeTaskPool>()
.unwrap()
.spawn(task)
.detach();
}
fn main() {
let mut app = App::new();
// the turbofish shouldn't be necessary, but it is. blame rustc
add_world_to_app::<Res<u32>, _>(&mut app, Schedule::default(), |world, res| {
// this closure is ran in a system in the main world.
// and here you have access to the entire subworld and any resources / queries you want from the main world.
// this function runs once per subworld tick for synchronization
// and a subworld tick does not block main world progress
})
} |
Hey sorry I left a comment and then deleted it because I realized that I hadn't fully grokked the RFC when I wrote it, and I want to leave more coherent feedback. I like the general direction of the RFC. I'm thinking about how it will address my particular use case of running a discrete time simulation where I can manually call Then, for example, let's say I want to run my schedule every time a specific key is pressed. Doing this from a custom runner might work, although I don't think the RFC proposed a specific interface for custom runners. If we just have exclusive access to the But I also think it might be nicer if I could run my schedule within another system via For example, I could have this: fn update_simulation(
mut sim: ResMut<Simulation>,
mut tick_signal: EventReader<TickEvent>,
) {
let Simulation { world, schedule } = &mut *sim;
for _ in tick_signal.iter() {
// Presumably this runs on a global task pool, which can be accessed via any world. It would be nice if this could be
// async as well so long simulations don't block rendering.
schedule.run_once(world);
}
}
Could you expand on this? |
|
||
- `spawn_world(label: impl WorldLabel)` | ||
- `despawn_world(label: impl WorldLabel)` | ||
- `set_world(label: impl WorldLabel)` |
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 am wary of builder APIs which change the meaning of downstream methods in this way. usually i would go with something like
app.with_world(label, |&mut app| app.add_plugin(MyPlugin))
but i maybe there are backwards compat concerns?
There are two things that jump out to me as being desirable here, although I'm not sure whether either is feasible.
|
Does this RFC support simulating different worlds at different update rates? |
is this still relevant now that we have sub apps with their own world? |
Currently no. I'm not fully happy with the current state of this RFC; moving it to draft until I have some cycles to devote to it.
Very. This is / was an attempt to try and wrangle the API and complexities of the subapps to create a coherent design. |
|
||
Currently, this flavor of design requires abandoning `bevy_app` completely, carefully limiting *every* system used (including base / external systems) or possibly some very advanced shenanigans with exclusive systems. | ||
|
||
A more limited "sub-world" design (see #16 for further discussion) is more feasible, by storing a `World` struct inside a resource, but this design is harder to reason about, unnecessarily hierarchical and inherently limited. |
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.
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 the main point here is that the hierarchic structure of App -> Subapp -> World -> Subworld
is overly complicated. Also this expands a lot more than #16 and standardizes communication between worlds.
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.
Yep, exactly!
.spawn_world(GameRulesWorld) | ||
.set_world(GameRulesWorld) | ||
.add_plugin(GameLogicPlugin) | ||
// We don't need or want all of the player-facing systems in our simulations | ||
.set_world(CoreWorld::Main) | ||
.add_plugins(DefaultPlugins) | ||
.add_plugin(UiPlugin) | ||
.add_plugin(RenderingPlugin) | ||
.add_plugin(InputPlugin) |
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 I understand correctly this means "add GameLogicPlugin to GameRulesWorld" and "add DefaultPlugins, UiPlugin, ... to CoreWorld::Main". What is the reason to use this API instead of something like.add_world_plugin([GameRulesWorld], GameRulesWorld)
.
By default, three worlds are added to your app: `CoreWorld::Global`, `CoreWorld::Main` and `CoreWorld::Rendering`. | ||
By convention, the main world should contain the vast majority of your logic, while the global world should only contain global resources (and other read-only data) that need to be accessed across worlds. |
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.
What is the purpose of Global World
? I don't quite understand what's the reason to have it?
Is Global World only a container for global resources? Can it contain entities, systems, etc?
.clone_entities_to_world(sim_query.entities(), sim_world) | ||
// This system will evaluate the world state, send it back with an event reporting the values | ||
.add_system(evaluate_action_system.to_world(sim_world)) | ||
.send_event(proposed_action, sim_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.
Currently, we use EventWriter
to write events, here we can also you app_commands to send events, what will be the difference?
Also, it's possible that one system in the world wants to broadcast the event to all world since it does not know which world should respond to it. How this situation should be handled?
While working on bevyengine/bevy#3877 I noticed that a central hurdle when working with multiple worlds is the question of relating entities. Obviously if there is a correspondence between entities in two worlds there could just be a But it would be nice if worlds could just share the same "entity namespace", i.e. related entities just have the same id. This is how the relation between the main and render world works right now, which I think is a very valuable example for anything that works with multiple worlds. I'm not sure though if this implementable without changing very fundamental ECS code, the As a side node: The proposed |
This seems to be a quite hard problem even without sharing entity namespaces. See bevyengine/bevy#3096 for why
Personally I like 1. the most. With some effort we might even get around using |
Proposal what to add to the RFC:
For convenience a Basically anything that at that is now a Alongside a list of worlds All in all execution of worlds will not be impacted. During the "global step" entities are flushed. Pros:
Cons:
|
Is this overly restrictive in terms of blocking all worlds at once? I can imagine scenarios where one small world may want to exist with an exclusive system that doesn't block all of the other worlds. Would it still be feasible to have world-exclusive as well as universe-exclusive systems? It's been a while since I looked at this part of the code, so I don't have any technical feedback at this time. |
|
Closing for now: this needs more thought in the context of what we've learned about pipelined rendering and I want to let others try their hand at a design for this! I remain strongly in favor of an API to support this sort of design in the future though. |
**RENDERED
Building on the ideas introduced in #16 by @NathanSWard (and the associated thread), this RFC presents a general-purpose multiple worlds API.
The TL;DR:
WorldLabel
.WorldLabel
.AppCommands
are applied.AppCommands
can be used to send data between worlds, move entities, make new worlds, clone schedules and so on.This should be useful for: