Tainted\\Coders

Bevy Plugins

Bevy version: 0.19Last updated:

Bevy enables us to break up our game into smaller features using plugins.

fn some_plugin_system() {
  println!("Hello from some plugin system!");
}

fn plugin(app: &mut App) {
  app.add_systems(Startup, some_plugin_system);
}

fn main() {
  App::new().add_plugins(plugin).run();
}

Plugins are useful for bundling up all the setup and runtime behavior of a feature into something we can toggle on and off. Plugins encapsulate the complexity of the feature into something we can "plug-into" our app.

By performing the necessary setup such as adding systems, resources and events to your game, each plugin is responsible for injecting its behavior into your game loop.

Plugins are how third party crates add functionality to Bevy. For example, avian has a plugin that adds physics to your games.

The Plugin trait

Usually we prefer using simple functions. As long as our function takes an &mut App, Bevy will automatically implement the Plugin trait for us.

However, if our plugin has additional life-cycle requirements we can implement the Plugin trait and define the life-cycle methods we are interested in.

struct CameraPlugin;

impl Plugin for CameraPlugin {
  fn finish(&self, _app: &mut App) {
    info!("Camera plugin finished setting up");
  }

  fn build(&self, app: &mut App) {
    app.add_systems(Startup, initialize_camera);
  }
}

Plugin life-cycle

Each Plugin goes through a life-cycle:

  1. We add our plugin to the App with add_plugins.
  2. The plugin's build method is called immediately and the plugin is registered.
  3. Once the app starts, Bevy polls each plugin's ready method until every plugin returns true.
  4. Bevy then calls finish on every plugin.
  5. Bevy calls cleanup on every plugin, and then the app's schedules start running.

build is where we add our systems, resources, events and other plugins.

ready lets a plugin wait for asynchronous setup (such as initializing a renderer) before the app starts.

finish runs once all plugins are ready and is useful when one plugin depends on another plugin's asynchronous setup.

cleanup runs after all plugins are built and finished, but before the app's schedules execute. It is not called again when the app exits.

Plugin ordering and dependencies

Plugins are built in the same order they are added to the App.

Plugins must be defined statically before the app starts running and cannot be modified at runtime.

Ideally a plugin would fully encapsulate its dependencies, but that's not always possible. Plugins that have dependencies do not currently have a good way to ensure those dependencies.

Plugins are unique by default: adding the same plugin to your app more than once will panic. Uniqueness is based on the plugin's name, which defaults to its type name. You can check is_plugin_added before adding a plugin, or override is_unique to return false for plugins that are meant to be added multiple times.

fn physics_plugin(app: &mut App) {
  if !app.is_plugin_added::<avian2d::debug_render::PhysicsDebugPlugin>() {
    app.add_plugins(DebugPhysicsPlugin);
  }

  if !app.is_plugin_added::<bevy::input::InputPlugin>() {
    app.add_plugins(bevy::input::InputPlugin);
  }
}

The is_plugin_added guard is also convenient in tests, where the app under test may already include a plugin that our plugin also adds.

Plugin configuration

If your plugin becomes complicated enough to demand configuration you can add fields to the struct that implements Plugin.

pub struct CameraPlugin {
  debug: bool,
}

fn initialize_camera(mut commands: Commands) {
  commands.spawn(Camera2d);
}

impl Plugin for CameraPlugin {
  fn build(&self, app: &mut App) {
    app.add_systems(Startup, initialize_camera);

    if self.debug {
      // Do something
    }
  }
}

fn main() {
  App::new()
    .add_plugins(CameraPlugin { debug: true })
    .run();
}

Plugin groups

Having plugins call other plugins is powerful and leads to a hierarchy of plugin dependencies. This can make it hard to configure.

Collections of plugins with a complicated setup can use the PluginGroup trait, which lets us group related plugins together and configure them later. Bevy also provides a plugin_group! macro for declaring a group from a list of plugins, which is how DefaultPlugins and MinimalPlugins are defined.

This can be a great choice for library authors writing plugins that others can add to their game.

pub struct GamePlugins;

impl PluginGroup for GamePlugins {
  fn build(self) -> PluginGroupBuilder {
    PluginGroupBuilder::start::<Self>()
      .add(CameraPlugin)
      .add(PhysicsPlugin)
  }
}

This will let us (or anyone consuming your plugins) configure exactly how the set of plugins runs in the context of our app:

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    .add_plugins(GamePlugins.build().disable::<PhysicsPlugin>())
    .run();
}

The builder also lets us replace a plugin with set, or insert one before or after an existing plugin with add_before and add_after, which is how we control ordering when it matters.

The plugin_group! macro

Writing a PluginGroup by hand takes a bit of boilerplate. For most groups we can use Bevy's plugin_group! macro, which generates the PluginGroup implementation for us. Every plugin in the group must implement Default:

use bevy::{app::plugin_group, prelude::*};

mod camera {
  use bevy::prelude::*;

  // Every plugin in a `plugin_group!` needs to implement `Default`.
  #[derive(Default)]
  pub struct CameraPlugin;

  impl Plugin for CameraPlugin {
    fn build(&self, app: &mut App) {
      app.add_systems(Startup, spawn_camera);
    }
  }

  fn spawn_camera(mut commands: Commands) {
    commands.spawn(Camera2d);
  }
}

#[derive(Default)]
struct PhysicsPlugin {
  gravity: f32,
}

impl Plugin for PhysicsPlugin {
  fn build(&self, app: &mut App) {
    app.insert_resource(Gravity(self.gravity));
  }
}

#[derive(Resource)]
struct Gravity(f32);

plugin_group! {
  /// The set of plugins that make up our game.
  pub struct GamePlugins {
    // Plugins from another module use three colons.
    camera:::CameraPlugin,
    // Plugins in this module use a single colon.
    :PhysicsPlugin,
  }
}

fn main() {
  App::new()
    .add_plugins(DefaultPlugins)
    // The macro builds each plugin with `Default`, so we can replace one
    // with `set` to configure it.
    .add_plugins(GamePlugins.set(PhysicsPlugin { gravity: 9.8 }))
    .run();
}

The macro is not in the prelude, so we import it from bevy::app. Plugins defined in the same module are referenced with a single colon (:PhysicsPlugin), while plugins from another module use three colons (camera:::CameraPlugin). Because the macro builds each plugin with Default, we configure one by replacing it with set after building the group.

The default plugins

Bevy includes DefaultPlugins in the bevy::prelude which gives us all of the obvious things our game would need:

PluginFeatureDescription
DiagnosticsPluginAdds core diagnostics
FrameCountPluginAdds frame counting functionality
InputPluginAdds keyboard and mouse input
PanicHandlerPluginAdds sensible panic handling
TaskPoolPluginSetup of default task pools for multi-threading
TimePluginAdds time functionality
TransformPluginHandles Transform components
AccessibilityPluginbevy_windowAdds non-GUI accessibility functionality
AnimationPluginbevy_animationAdds animation support
AntiAliasPluginbevy_anti_aliasAdds multi-sample anti-aliasing (MSAA)
AssetPluginbevy_assetAdds asset server and resources to load assets
AudioPluginbevy_audioAdds support for using sound assets
CameraPluginbevy_cameraAdds 2D and 3D camera components and systems
CiTestingPluginbevy_ci_testingHelps instrument continuous integration
ClipboardPluginbevy_clipboardAdds clipboard support to a Bevy app
CorePipelinePluginbevy_core_pipelineThe core rendering pipeline
DefaultPickingPluginsbevy_pickingAdds picking functionality
DlssInitPlugindlssInitializes DLSS support if available
GilrsPluginbevy_gilrsAdds support for gamepad inputs
GizmoPluginbevy_gizmosProvides an immediate mode drawing api for visual debugging
GizmoRenderPluginbevy_gizmos_renderSends gizmos to the renderer
GltfPluginbevy_gltfAdds support for loading gltf models
HotPatchPluginhotpatchingEnables hot-patching of assets
ImagePluginbevy_imageAdds the Image asset and prepares them to render on your GPU
InputDispatchPluginbevy_input_focusDispatches bubbling keyboard and gamepad button events to the focused entity
InputFocusPluginbevy_input_focusSets up the core input focus system
LightPluginbevy_lightAdds light components and systems
LogPluginbevy_logAdds logging to apps
MeshPluginbevy_meshAdds the Mesh asset and prepares them to render on the GPU
PbrPluginbevy_pbrAdds physical based rendering with StandardMaterial etc
PipelinedRenderingPluginbevy_renderAdds pipelined rendering
PostProcessPluginbevy_post_processAdds post processing effects
RenderDebugOverlayPluginbevy_dev_tools + bevy_pbrAdds a rendering debug overlay to visualize various renderer buffers
RenderPluginbevy_renderSets up rendering backend powered by wgpu crate
ScenePluginbevy_sceneAdds support for spawning Bevy Scenes
ScheduleRunnerPlugin!bevy_windowConfigures an App to run its Schedule according to a given RunMode (added when windowing is disabled)
SpritePluginbevy_spriteHandling of sprites (images on our entities)
SpriteRenderPluginbevy_sprite_renderAdds support for sending sprites to the renderer
StatesPluginbevy_stateAdds state management for Apps
TerminalCtrlCHandlerPluginstdHandles Ctrl-C signals in terminal applications
TextPluginbevy_textSupports loading fonts and rendering text
UiPluginbevy_uiAdds support for UI layouts (flex, grid, etc)
UiRenderPluginbevy_ui_renderAdds support for sending UI nodes to renderer
UiWidgetsPluginsbevy_ui_widgetsRegisters the observers for all of the widgets in this crate
WebAssetPluginbevy_assetAdds the http and https asset sources to an app
WindowPluginbevy_windowProvides an interface to create and manage Window components
WinitPluginbevy_winitInterface to create operating system windows (to actually display our game)
WorldSerializationPluginbevy_world_serializationLoading and saving collections of entities and components to files
You can toggle on and off features with cargo.

See cargo features for more details.

The minimal plugins

Instead of DefaultPlugins, Bevy also provides MinimalPlugins which only includes the bare essentials to get an app running:

This can be especially useful during tests to setup an app without any rendering concerns.