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:
Joseph 2024-01-26 18:40:15 -08:00 committed by GitHub
parent 8f25805b66
commit d66c868e6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 98 additions and 73 deletions

View File

@ -21,6 +21,40 @@ use std::any::Any;
/// * once the app started, it will wait for all registered [`Plugin::ready`] to return `true` /// * once the app started, it will wait for all registered [`Plugin::ready`] to return `true`
/// * it will then call all registered [`Plugin::finish`] /// * it will then call all registered [`Plugin::finish`]
/// * and call all registered [`Plugin::cleanup`] /// * 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 { pub trait Plugin: Downcast + Any + Send + Sync {
/// Configures the [`App`] to which this plugin is added. /// Configures the [`App`] to which this plugin is added.
fn build(&self, app: &mut App); fn build(&self, app: &mut App);
@ -60,6 +94,12 @@ pub trait Plugin: Downcast + Any + Send + Sync {
impl_downcast!(Plugin); 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`]. /// A type representing an unsafe function that returns a mutable pointer to a [`Plugin`].
/// It is used for dynamically loading plugins. /// It is used for dynamically loading plugins.
/// ///

View File

@ -37,7 +37,7 @@ fn main() {
.init_state::<GameState>() .init_state::<GameState>()
.add_systems(Startup, setup) .add_systems(Startup, setup)
// Adds the plugins for each state // 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(); .run();
} }
@ -51,19 +51,15 @@ mod splash {
use super::{despawn_screen, GameState}; use super::{despawn_screen, GameState};
// This plugin will display a splash screen with Bevy logo for 1 second before switching to the menu // This plugin will display a splash screen with Bevy logo for 1 second before switching to the menu
pub struct SplashPlugin; pub fn splash_plugin(app: &mut App) {
// As this plugin is managing the splash screen, it will focus on the state `GameState::Splash`
impl Plugin for SplashPlugin { app
fn build(&self, app: &mut App) { // When entering the state, spawn everything needed for this screen
// As this plugin is managing the splash screen, it will focus on the state `GameState::Splash` .add_systems(OnEnter(GameState::Splash), splash_setup)
app // While in this state, run the `countdown` system
// When entering the state, spawn everything needed for this screen .add_systems(Update, countdown.run_if(in_state(GameState::Splash)))
.add_systems(OnEnter(GameState::Splash), splash_setup) // When exiting the state, despawn everything that was spawned for this screen
// While in this state, run the `countdown` system .add_systems(OnExit(GameState::Splash), despawn_screen::<OnSplashScreen>);
.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 // 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 // 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 // display the current settings for 5 seconds before returning to the menu
pub struct GamePlugin; pub fn game_plugin(app: &mut App) {
app.add_systems(OnEnter(GameState::Game), game_setup)
impl Plugin for GamePlugin { .add_systems(Update, game.run_if(in_state(GameState::Game)))
fn build(&self, app: &mut App) { .add_systems(OnExit(GameState::Game), despawn_screen::<OnGameScreen>);
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 // 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 main menu with "New Game", "Settings", "Quit"
// - a settings menu with two submenus and a back button // - a settings menu with two submenus and a back button
// - two settings screen with a setting that can be set and a back button // - two settings screen with a setting that can be set and a back button
pub struct MenuPlugin; pub fn menu_plugin(app: &mut App) {
app
impl Plugin for MenuPlugin { // At start, the menu is not enabled. This will be changed in `menu_setup` when
fn build(&self, app: &mut App) { // entering the `GameState::Menu` state.
app // Current screen in the menu is handled by an independent state from `GameState`
// At start, the menu is not enabled. This will be changed in `menu_setup` when .init_state::<MenuState>()
// entering the `GameState::Menu` state. .add_systems(OnEnter(GameState::Menu), menu_setup)
// Current screen in the menu is handled by an independent state from `GameState` // Systems to handle the main menu screen
.init_state::<MenuState>() .add_systems(OnEnter(MenuState::Main), main_menu_setup)
.add_systems(OnEnter(GameState::Menu), menu_setup) .add_systems(OnExit(MenuState::Main), despawn_screen::<OnMainMenuScreen>)
// Systems to handle the main menu screen // Systems to handle the settings menu screen
.add_systems(OnEnter(MenuState::Main), main_menu_setup) .add_systems(OnEnter(MenuState::Settings), settings_menu_setup)
.add_systems(OnExit(MenuState::Main), despawn_screen::<OnMainMenuScreen>) .add_systems(
// Systems to handle the settings menu screen OnExit(MenuState::Settings),
.add_systems(OnEnter(MenuState::Settings), settings_menu_setup) despawn_screen::<OnSettingsMenuScreen>,
.add_systems( )
OnExit(MenuState::Settings), // Systems to handle the display settings screen
despawn_screen::<OnSettingsMenuScreen>, .add_systems(
) OnEnter(MenuState::SettingsDisplay),
// Systems to handle the display settings screen display_settings_menu_setup,
.add_systems( )
OnEnter(MenuState::SettingsDisplay), .add_systems(
display_settings_menu_setup, Update,
) (setting_button::<DisplayQuality>.run_if(in_state(MenuState::SettingsDisplay)),),
.add_systems( )
Update, .add_systems(
( OnExit(MenuState::SettingsDisplay),
setting_button::<DisplayQuality> despawn_screen::<OnDisplaySettingsMenuScreen>,
.run_if(in_state(MenuState::SettingsDisplay)), )
), // Systems to handle the sound settings screen
) .add_systems(OnEnter(MenuState::SettingsSound), sound_settings_menu_setup)
.add_systems( .add_systems(
OnExit(MenuState::SettingsDisplay), Update,
despawn_screen::<OnDisplaySettingsMenuScreen>, setting_button::<Volume>.run_if(in_state(MenuState::SettingsSound)),
) )
// Systems to handle the sound settings screen .add_systems(
.add_systems(OnEnter(MenuState::SettingsSound), sound_settings_menu_setup) OnExit(MenuState::SettingsSound),
.add_systems( despawn_screen::<OnSoundSettingsMenuScreen>,
Update, )
setting_button::<Volume>.run_if(in_state(MenuState::SettingsSound)), // Common systems to all screens that handles buttons behavior
) .add_systems(
.add_systems( Update,
OnExit(MenuState::SettingsSound), (menu_action, button_system).run_if(in_state(GameState::Menu)),
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 // State used for the current menu screen