Bevy is a code-first game engine in Rust. It utilizes Entity-Component-System (ECS) architecture. As of 2026-02, the engine is still being developed; the authors publish migration instructions with each new version.

This website focuses on showing the main constructs of Bevy and expects knowledge of Rust.

Setup

Running the following commands should result in a gray empty window. Requires Cargo ↗.

shell:

cargo init hello_world
cd hello_world
cargo add bevy
# change src/main.rs to the code below
cargo run

main.rs:

use bevy::prelude::*;

fn main() {
    let mut app = App::new();
    app.add_plugins(DefaultPlugins);
    app.run();
}

To develop consult the official documentation.

ECS and other engine components

  • Resources are data-holding singletons implemented as Rust structs.
  • Components are data-holding Rust structs.
  • Entities are sets of components.
  • Systems are functions that use resources to act on entities.
  • Plugins collect operations over app to make the program more readable.

Each of these parts need to be registered within the engine to work. We’ll see examples for each in two parts: its code, and part that needs to register it in the main() function.

Note that many arguments in Bevy accept either the structure itself or a tuple of such structures.

Plugins

Every other part can be registered to the app in fn main(), plugins serve only to group these parts in a logical manner to make the project easier to orient in.

Plugin is an empty struct that gets its functionality by implementing Plugin. It acts on app, the same way as we do in main.

pub struct ResearchPlugin;

impl Plugin for ResearchPlugin {
    fn build(&self, app: &mut App) {
        app
            .init_resource ...
            .add_systems ...
        );
    }
}

fn main() {
    ...
    app.add_plugins(
        (ResearchPlugin, GameControlsPlugin, MenuPlugin, UiPlugin, CameraPlugin)
    );
    ...
}

Bevy comes with many prepared resources of its own which we registered when we invoked app.add_plugins(DefaultPlugins);. Many of its parts can be activated/deactivated with features settings in Cargo.toml. The plugins within the DefaultPlugins can be modified as follows.

    app.add_plugins(DefaultPlugins.set(WindowPlugin{
        primary_window: Some(Window {
            title: String::from("Hello world!"),
            ..default()
        }),
        ..default()
    }));

Resources

Resources are data-holding singletons.

#[derive(Resource, Default)]
struct RoundInfo {
    level: u32,
    score: u32,
}

fn main() {
    ...
    app.init_resource::<RoundInfo>()
    ...
}

Component

Components hold some data for an entity. Its presence can be queried on later, so empty components can serve as a tag. Components can be struct or enum.

#[derive(Component)]
struct Player;

#[derive(Component)]
struct Health {
    maximum: u32,
    current: u32,
}

Entity

To design an Entity we first need to decompose into its constituent Components. Do not think of Entities as objects, they are entirely made of just components.

Once we have the necessary components we can spawn an entity. Canonically, this is done through Commands which puts entity’s creation to a command queue.

fn spawn_player(mut commands: Commands) {
    let player = (
        Player,
        Health,
        ... other components
    );
    commands.spawn(player);
}

fn main() {
    ...
    app.add_systems(Startup, (spawn_player, ...));
    ...
}

Systems

Systems are simple rust methods that have parameters injected by the engine. Arguments can be:

  • engine’s utilities like Commands
  • resources
    • Res<Resource> for immutable
    • ResMut<Resource> for mutable
  • queries pass in a collection of entities that satisfy the query – think of it as a database select
    • Query<&mut Position)> access positions of all entities that have a position
    • Query<(&Player, &mut Health), (With<Visible>, Without<Sleeping>)> to access Player and Health components of entities given some restrictions on existence of its other components
    • Query here can be replaced with Single, Option<Single>, or Populated if we want to filter based on assumptions on the result size
    • queries must be disjoint so that two queries cannot mutably access the same component

They get registered in the app as to run once with Startup or run every frame with Update.

fn spawn_default_map(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut turrets: Query<(&Transform, &mut TurretCombat), Without<UnderConstruction>>,
    enemies: Query<(Entity, &Transform), With<EnemyUnit>>,
) {
    ...
}

fn main() {
    ...
    app.add_systems(Startup, (setup, spawn_default_map));
    app.add_systems(Update,
        move_everything.run_if(in_state(AppState::GamePlaying))
    );
    ...
}

Further engine systems

Above, we saw Commands which gets injected due to it implementing SystemParam.