Expressively define plugins using functions (#11080)
# Objective Plugins are an incredible tool for encapsulating functionality. They are low-key one of Bevy's best features. Combined with rust's module and privacy system, it's a match made in heaven. The one downside is that they can be a little too verbose to define. 90% of all plugin definitions look something like this: ```rust pub struct MyPlugin; impl Plugin for MyPlugin { fn build(&self, app: &mut App) { app.init_resource::<CameraAssets>() .add_event::<SetCamera>() .add_systems(Update, (collect_set_camera_events, drive_camera).chain()); } } ``` Every so often it gets a little spicier: ```rust pub struct MyGenericPlugin<T>(PhantomData<T>); impl<T> Default for MyGenericPlugin<T> { fn default() -> Self { ... } } impl<T> Plugin for MyGenericPlugin<T> { ... } ``` This is an annoying amount of boilerplate. Ideally, plugins should be focused and small in scope, which means any app is going to have a *lot* of them. Writing a plugin should be as easy as possible, and the *only* part of this process that carries any meaning is the body of `fn build`. ## Solution Implement `Plugin` for functions that take `&mut App` as a parameter. The two examples above now look like this: ```rust pub fn my_plugin(app: &mut App) { app.init_resource::<CameraAssets>() .add_event::<SetCamera>() .add_systems(Update, (collect_set_camera_events, drive_camera).chain()); } pub fn my_generic_plugin<T>(app: &mut App) { // No need for PhantomData, it just works. } ``` Almost all plugins can be written this way, which I believe will make bevy code much more attractive. Less boilerplate and less meaningless indentation. More plugins with smaller scopes. --- ## Changelog The `Plugin` trait is now implemented for all functions that take `&mut App` as their only parameter. This is an abbreviated way of defining plugins with less boilerplate than manually implementing the trait. --------- Co-authored-by: Federico Rinaldi <gisquerin@gmail.com>
This commit is contained in:
parent
8f25805b66
commit
d66c868e6f
@ -21,6 +21,40 @@ use std::any::Any;
|
||||
/// * once the app started, it will wait for all registered [`Plugin::ready`] to return `true`
|
||||
/// * it will then call all registered [`Plugin::finish`]
|
||||
/// * and call all registered [`Plugin::cleanup`]
|
||||
///
|
||||
/// ## Defining a plugin.
|
||||
///
|
||||
/// Most plugins are simply functions that add configuration to an [`App`].
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_app::{App, Update};
|
||||
/// App::new().add_plugins(my_plugin).run();
|
||||
///
|
||||
/// // This function implements `Plugin`, along with every other `fn(&mut App)`.
|
||||
/// pub fn my_plugin(app: &mut App) {
|
||||
/// app.add_systems(Update, hello_world);
|
||||
/// }
|
||||
/// # fn hello_world() {}
|
||||
/// ```
|
||||
///
|
||||
/// For more advanced use cases, the `Plugin` trait can be implemented manually for a type.
|
||||
///
|
||||
/// ```
|
||||
/// # use bevy_app::*;
|
||||
/// pub struct AccessibilityPlugin {
|
||||
/// pub flicker_damping: bool,
|
||||
/// // ...
|
||||
/// }
|
||||
///
|
||||
/// impl Plugin for AccessibilityPlugin {
|
||||
/// fn build(&self, app: &mut App) {
|
||||
/// if self.flicker_damping {
|
||||
/// app.add_systems(PostUpdate, damp_flickering);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// # fn damp_flickering() {}
|
||||
/// ````
|
||||
pub trait Plugin: Downcast + Any + Send + Sync {
|
||||
/// Configures the [`App`] to which this plugin is added.
|
||||
fn build(&self, app: &mut App);
|
||||
@ -60,6 +94,12 @@ pub trait Plugin: Downcast + Any + Send + Sync {
|
||||
|
||||
impl_downcast!(Plugin);
|
||||
|
||||
impl<T: Fn(&mut App) + Send + Sync + 'static> Plugin for T {
|
||||
fn build(&self, app: &mut App) {
|
||||
self(app);
|
||||
}
|
||||
}
|
||||
|
||||
/// A type representing an unsafe function that returns a mutable pointer to a [`Plugin`].
|
||||
/// It is used for dynamically loading plugins.
|
||||
///
|
||||
|
@ -37,7 +37,7 @@ fn main() {
|
||||
.init_state::<GameState>()
|
||||
.add_systems(Startup, setup)
|
||||
// Adds the plugins for each state
|
||||
.add_plugins((splash::SplashPlugin, menu::MenuPlugin, game::GamePlugin))
|
||||
.add_plugins((splash::splash_plugin, menu::menu_plugin, game::game_plugin))
|
||||
.run();
|
||||
}
|
||||
|
||||
@ -51,19 +51,15 @@ mod splash {
|
||||
use super::{despawn_screen, GameState};
|
||||
|
||||
// This plugin will display a splash screen with Bevy logo for 1 second before switching to the menu
|
||||
pub struct SplashPlugin;
|
||||
|
||||
impl Plugin for SplashPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
// As this plugin is managing the splash screen, it will focus on the state `GameState::Splash`
|
||||
app
|
||||
// When entering the state, spawn everything needed for this screen
|
||||
.add_systems(OnEnter(GameState::Splash), splash_setup)
|
||||
// While in this state, run the `countdown` system
|
||||
.add_systems(Update, countdown.run_if(in_state(GameState::Splash)))
|
||||
// When exiting the state, despawn everything that was spawned for this screen
|
||||
.add_systems(OnExit(GameState::Splash), despawn_screen::<OnSplashScreen>);
|
||||
}
|
||||
pub fn splash_plugin(app: &mut App) {
|
||||
// As this plugin is managing the splash screen, it will focus on the state `GameState::Splash`
|
||||
app
|
||||
// When entering the state, spawn everything needed for this screen
|
||||
.add_systems(OnEnter(GameState::Splash), splash_setup)
|
||||
// While in this state, run the `countdown` system
|
||||
.add_systems(Update, countdown.run_if(in_state(GameState::Splash)))
|
||||
// When exiting the state, despawn everything that was spawned for this screen
|
||||
.add_systems(OnExit(GameState::Splash), despawn_screen::<OnSplashScreen>);
|
||||
}
|
||||
|
||||
// Tag component used to tag entities added on the splash screen
|
||||
@ -125,14 +121,10 @@ mod game {
|
||||
|
||||
// This plugin will contain the game. In this case, it's just be a screen that will
|
||||
// display the current settings for 5 seconds before returning to the menu
|
||||
pub struct GamePlugin;
|
||||
|
||||
impl Plugin for GamePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::Game), game_setup)
|
||||
.add_systems(Update, game.run_if(in_state(GameState::Game)))
|
||||
.add_systems(OnExit(GameState::Game), despawn_screen::<OnGameScreen>);
|
||||
}
|
||||
pub fn game_plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::Game), game_setup)
|
||||
.add_systems(Update, game.run_if(in_state(GameState::Game)))
|
||||
.add_systems(OnExit(GameState::Game), despawn_screen::<OnGameScreen>);
|
||||
}
|
||||
|
||||
// Tag component used to tag entities added on the game screen
|
||||
@ -253,57 +245,50 @@ mod menu {
|
||||
// - a main menu with "New Game", "Settings", "Quit"
|
||||
// - a settings menu with two submenus and a back button
|
||||
// - two settings screen with a setting that can be set and a back button
|
||||
pub struct MenuPlugin;
|
||||
|
||||
impl Plugin for MenuPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app
|
||||
// At start, the menu is not enabled. This will be changed in `menu_setup` when
|
||||
// entering the `GameState::Menu` state.
|
||||
// Current screen in the menu is handled by an independent state from `GameState`
|
||||
.init_state::<MenuState>()
|
||||
.add_systems(OnEnter(GameState::Menu), menu_setup)
|
||||
// Systems to handle the main menu screen
|
||||
.add_systems(OnEnter(MenuState::Main), main_menu_setup)
|
||||
.add_systems(OnExit(MenuState::Main), despawn_screen::<OnMainMenuScreen>)
|
||||
// Systems to handle the settings menu screen
|
||||
.add_systems(OnEnter(MenuState::Settings), settings_menu_setup)
|
||||
.add_systems(
|
||||
OnExit(MenuState::Settings),
|
||||
despawn_screen::<OnSettingsMenuScreen>,
|
||||
)
|
||||
// Systems to handle the display settings screen
|
||||
.add_systems(
|
||||
OnEnter(MenuState::SettingsDisplay),
|
||||
display_settings_menu_setup,
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(
|
||||
setting_button::<DisplayQuality>
|
||||
.run_if(in_state(MenuState::SettingsDisplay)),
|
||||
),
|
||||
)
|
||||
.add_systems(
|
||||
OnExit(MenuState::SettingsDisplay),
|
||||
despawn_screen::<OnDisplaySettingsMenuScreen>,
|
||||
)
|
||||
// Systems to handle the sound settings screen
|
||||
.add_systems(OnEnter(MenuState::SettingsSound), sound_settings_menu_setup)
|
||||
.add_systems(
|
||||
Update,
|
||||
setting_button::<Volume>.run_if(in_state(MenuState::SettingsSound)),
|
||||
)
|
||||
.add_systems(
|
||||
OnExit(MenuState::SettingsSound),
|
||||
despawn_screen::<OnSoundSettingsMenuScreen>,
|
||||
)
|
||||
// Common systems to all screens that handles buttons behavior
|
||||
.add_systems(
|
||||
Update,
|
||||
(menu_action, button_system).run_if(in_state(GameState::Menu)),
|
||||
);
|
||||
}
|
||||
pub fn menu_plugin(app: &mut App) {
|
||||
app
|
||||
// At start, the menu is not enabled. This will be changed in `menu_setup` when
|
||||
// entering the `GameState::Menu` state.
|
||||
// Current screen in the menu is handled by an independent state from `GameState`
|
||||
.init_state::<MenuState>()
|
||||
.add_systems(OnEnter(GameState::Menu), menu_setup)
|
||||
// Systems to handle the main menu screen
|
||||
.add_systems(OnEnter(MenuState::Main), main_menu_setup)
|
||||
.add_systems(OnExit(MenuState::Main), despawn_screen::<OnMainMenuScreen>)
|
||||
// Systems to handle the settings menu screen
|
||||
.add_systems(OnEnter(MenuState::Settings), settings_menu_setup)
|
||||
.add_systems(
|
||||
OnExit(MenuState::Settings),
|
||||
despawn_screen::<OnSettingsMenuScreen>,
|
||||
)
|
||||
// Systems to handle the display settings screen
|
||||
.add_systems(
|
||||
OnEnter(MenuState::SettingsDisplay),
|
||||
display_settings_menu_setup,
|
||||
)
|
||||
.add_systems(
|
||||
Update,
|
||||
(setting_button::<DisplayQuality>.run_if(in_state(MenuState::SettingsDisplay)),),
|
||||
)
|
||||
.add_systems(
|
||||
OnExit(MenuState::SettingsDisplay),
|
||||
despawn_screen::<OnDisplaySettingsMenuScreen>,
|
||||
)
|
||||
// Systems to handle the sound settings screen
|
||||
.add_systems(OnEnter(MenuState::SettingsSound), sound_settings_menu_setup)
|
||||
.add_systems(
|
||||
Update,
|
||||
setting_button::<Volume>.run_if(in_state(MenuState::SettingsSound)),
|
||||
)
|
||||
.add_systems(
|
||||
OnExit(MenuState::SettingsSound),
|
||||
despawn_screen::<OnSoundSettingsMenuScreen>,
|
||||
)
|
||||
// Common systems to all screens that handles buttons behavior
|
||||
.add_systems(
|
||||
Update,
|
||||
(menu_action, button_system).run_if(in_state(GameState::Menu)),
|
||||
);
|
||||
}
|
||||
|
||||
// State used for the current menu screen
|
||||
|
Loading…
Reference in New Issue
Block a user