Swap an app's World
with another World
at runtime. Useful for having separate menu and game worlds, for separate loading and main worlds, etc.
Most Bevy games have this common pattern:
- Press play button on the main menu.
- Change state to
GameState::Play
. - Initialize game state.
- Run the game.
- The game ends.
- Cleanup game data.
- Change state to
GameState::Menu
. - Show the menu.
This definitely works, and has worked for many games for years.
It's clunky. Instead, what if we could:
- Press play button on the main menu.
- Initialize game state in a fresh world.
- Run the game.
- The game ends.
- Discard the game world.
- Show the menu.
This way the game world is isolated from the menu world, and you don't need to manage GameState
just for accessing the main menu.
A bevy_worldswap
app can hold two worlds. The foreground world is just a normal world that renders to the window. The background world is stored internally and doesn't render anywhere, but you can choose to update it in the background alongside the foreground world (see BackgroundTickRate
).
Controlling foreground/background worlds is done with SwapCommands
. Swap commands can be sent from both foreground and background worlds using the SwapCommandSender
resource.
The following commands are provided:
- SwapCommand::Pass: Pass control of the foreground to a new
WorldSwapApp
and drop (or recover) the world currently in the foreground. - SwapCommand::Fork: Pass control of the foreground to a new
WorldSwapApp
and put the world currently in the foreground into the background. - SwapCommand::Swap: Switch the foreground and background worlds.
- SwapCommand::Join: Pass control of the foreground to the background world, and drop (or recover) the previous foreground world.
You can use the WorldSwapStatus
resource to detect whether a world is in the foreground or background, or if it's suspended. There are also several run conditions: suspended
, in_background
, in_foreground
, entered_foreground
, entered_background
.
Your main app needs to use WorldSwapPlugin
, which must be added after DefaultPlugins
if you use it.
use bevy::prelude::*;
use bevy_worldswap::prelude::*;
fn main()
{
App::new()
.add_plugins(DefaultPlugins) // Must go before WorldSwapPlugin if you add this.
// ...
.add_plugins(WorldSwapPlugin::default())
// ...
.run();
}
The initial app you set up will contain the first foreground world, which can be sent to the background or passed to other worlds with SwapCommands
.
To make a new app that should run in the foreground, there are two options depending if your app is headless or not.
Once your new app is made, pass it to WorldSwapApp::new
. WorldSwapApp
holds your app while suspended or in the background.
A headless app is one that doesn't use windows. Typically a headless app will use Bevy's MinimalPlugins
, and if it uses assets it will include Bevy's AssetPlugin
.
If your child app will read assets, it is recommended to re-use the AssetServer
from the original app (this will allow the child app to read Assets
loaded in other worlds). To do that, just clone the AssetServer
resource into your new child app.
use bevy::prelude::*;
use bevy_worldswap::prelude::*;
fn pass_control_to_headless_app(
asset_server: Res<AssetServer>,
swap_commands: Res<SwapCommandSender>
) {
let mut my_headless_app = App::new();
my_headless_app.add_plugins(MinimalPlugins)
.insert_resource(asset_server.clone()) // Reuse the original app's AssetServer.
.add_plugins(AssetPlugin::default()) // This should go *after* inserting an AssetServer clone.
// ...
;
swap_commands.send(SwapCommand::Pass(WorldSwapApp::new(my_headless_app)));
}
A windowed app needs to use ChildDefaultPlugins
instead of DefaultPlugins
. In order to link your new app with existing windows, a number of rendering resources need to be cloned.
use bevy::prelude::*;
use bevy::render::renderer::{
RenderAdapter, RenderAdapterInfo, RenderDevice, RenderInstance, RenderQueue
};
use bevy_worldswap::prelude::*;
fn pass_control_to_windowed_app(
asset_server: Res<AssetServer>,
devices: Res<RenderDevice>,
queue: Res<RenderQueue>,
adapter_info: Res<RenderAdapterInfo>,
adapter: Res<RenderAdapter>,
instance: Res<RenderInstance>,
target: Res<RenderWorkerTarget>,
swap_commands: Res<SwapCommandSender>,
)
{
let mut my_headless_app = App::new();
my_headless_app.add_plugins(ChildDefaultPlugins{
asset_server: asset_server.clone(),
devices: devices.clone(),
queue: queue.clone(),
adapter_info: adapter_info.clone(),
adapter: adapter.clone(),
instance: instance.clone(),
synchronous_pipeline_compilation: false, // This is forwarded to RenderPlugin.
target: target.clone(),
})
// ...
;
swap_commands.send(SwapCommand::Pass(WorldSwapApp::new(my_headless_app)));
}
If a Pass
command is detected, then the passed world will enter the foreground. The previous foreground world will either be dropped or recovered, depending on if the WorldSwapPlugin::swap_pass_recovery
callback is set.
For example:
use bevy::prelude::*;
use bevy_worldswap::prelude::*;
fn main()
{
App::new()
// ...
.add_plugins(WorldSwapPlugin{
swap_pass_recovery: Some(
|foreground_world: &mut World, prev_app: WorldSwapApp|
{
// Extract data from the previous app, or cache it for sending
// into the foreground again.
}
),
..Default::default()
})
// ...
.run();
}
WorldSwapApps
passed to the recovery callback will have WorldSwapStatus::Suspended
.
A similar pattern holds for Join
commands, with the WorldSwapPlugin::swap_join_recovery
callback.
Note: When a foreground world sends AppExit
and there is a world in the background, then the AppExit
will be intercepted and transformed into a Join
command (after the Main
schedule is done). Otherwise the AppExit
will be allowed to pass through and the entire app will shut down.
This project has a couple caveats to keep in mind.
- Logging: Foreground and background worlds log to the same output stream.
- SubApps:
SubApps
in secondary apps you construct will be discarded, other thanRenderApp
/RenderExtractApp
, which we extract and manage internally. - Assets
- Constructing new secondary apps will cause
Duplicate AssetLoader registered for Asset type ...
warnings to be printed. There is no solution right now, but the warnings are harmless. - If assets become entities then it will no longer be possible to share assets or
AssetServer
between apps. Apps should be designed with the assumptionAssetServer
can't be shared.
- Constructing new secondary apps will cause
- Accessibility: Accessibility is untested and may not work at all in swapped worlds. The problem is accessibility node ids equal window entity ids, which are different for the same window when swapping worlds, and node construction is one-and-done when creating a new window so the internal IDs can't be mapped. A Bevy refactor for node ID assignment/management may be required. There is also a potential nasty edge condition where a window spawned in one world has the same entity id as a window spawned in a previous world.
- Bevy PR to fix this.
This project has a custom rustfmt.toml
file. To run it you can use cargo +nightly fmt --all
. Nightly is not required for using this crate, only for running rustfmt
.
bevy |
bevy_worldswap |
---|---|
main | main |