Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Bevy User Guide

Generated June 2026 from bevyengine/bevy at 457b918

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Bevy is a refreshingly simple, data-driven game engine built in Rust. It is free and open source forever. This guide shows you how to install Bevy, build your first app, and use the engine's day-to-day features — the Entity Component System (ECS), 2D and 3D rendering, assets, input, audio, UI, and more.

Note

This guide documents the 0.19.0-dev development line of Bevy (the main branch). Bevy ships a new release with breaking API changes roughly once every three months, accompanied by official migration guides. When you depend on a published release (e.g. 0.18), the concepts below apply, but a few type names may differ. Always pin a specific version in your Cargo.toml.


1. Quick Start

The smallest possible Bevy program opens a window with the default engine features enabled.

use bevy::prelude::*;

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

A more useful "Hello, World" runs a system every frame:

use bevy::prelude::*;

fn main() {
    App::new()
        .add_systems(Update, hello_world_system)
        .run();
}

fn hello_world_system() {
    println!("hello world");
}

The three building blocks you just used are the heart of every Bevy app:

Concept What it is
App The container that wires together plugins, systems, schedules, and the world.
Plugins Self-contained bundles of engine functionality. DefaultPlugins gives you the "full engine".
Systems Plain Rust functions that read and write game state. Added to a schedule such as Update.

Tip

Bevy prioritizes modularity as a core principle. Every engine feature — even rendering — is implemented as a plugin. You add only what you need.


2. Installation

2.1 Requirements

Requirement Detail
Rust toolchain A recent stable Rust. Bevy's Minimum Supported Rust Version (MSRV) tracks close to "the latest stable release", because the engine relies heavily on new language and compiler features.
Edition Bevy crates use Rust edition 2024.
Platform Windows, macOS, Linux, Web (WebAssembly), Android, and iOS are supported.
GPU A GPU/driver supporting Vulkan, Metal, DirectX 12, or (on the web) WebGPU/WebGL2.

Important

Bevy is still in active development. Important features are still being added and documentation is sparse in places. Use it if you are comfortable working in a fast-moving environment.

2.2 Add Bevy to a project

Create a new Rust binary project and add the bevy crate:

cargo add

cargo new my_game
cd my_game
cargo add bevy

Cargo.toml

[dependencies]
bevy = "0.18"

Then build and run:

cargo run
   Compiling bevy v0.18.0
   ...
    Finished `dev` profile [unoptimized + debuginfo] target(s)
     Running `target/debug/my_game`

Warning

The first build compiles the entire engine and can take several minutes. Subsequent builds are incremental and much faster. See Fast Compiles below to speed up iteration.

2.3 Run the official examples

The Bevy repository ships hundreds of runnable examples. Clone the repo and run one by name:

git clone https://github.com/bevyengine/bevy
cd bevy
# Switch to the latest released version (the default branch is main/development)
git checkout latest
cargo run --example breakout
# A few examples worth starting with:
cargo run --example 3d_scene       # A lit 3D scene
cargo run --example 2d_shapes      # 2D primitive shapes
cargo run --example ecs_guide      # A guided tour of the ECS
cargo run --example input_keyboard # Reading keyboard input

Tip

Browse the full catalogue in the repository's examples/ directory. Each subfolder (2d, 3d, ui, audio, ecs, shader, animation, …) is grouped by topic.

2.4 Optimizing release builds

Debug builds of a Bevy game can run slowly because dependencies are unoptimized. A common pattern is to optimize dependencies even in dev builds:

# Cargo.toml — optimize dependencies in dev, keep fast incremental builds for your code
[profile.dev.package."*"]
opt-level = 3

[profile.dev]
opt-level = 1

For shipping, always build with --release:

cargo run --release

2.5 Choosing only the features you need

Bevy is highly modular. You can disable default features and enable only the subset your game uses, which cuts compile times and shrinks your binary. For example, to build a 2D-only game:

[dependencies]
bevy = { version = "0.18", default-features = false, features = ["2d"] }

See Section 12: Cargo Features for the full menu of profiles, collections, and individual feature flags.

2.6 Fast Compiles

Bevy builds fine on stable Rust with the default configuration. For really fast iterative compiles, enable the optional "fast compiles" setup (a faster linker plus the dynamic-linking feature). The canonical, up-to-date instructions live in the official Setup guide.

The single biggest win is dynamic linking during development:

cargo run --features bevy/dynamic_linking

Caution

Never ship a game with dynamic_linking enabled — it requires Bevy's shared library to be present at runtime. Use it only for local iteration. It is also unsupported on WebAssembly.


3. Your First Real App

Let's build something interactive: a 2D app that spawns a camera, draws a sprite, and moves it with the arrow keys. This touches the core workflow you'll use constantly.

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, move_player)
        .run();
}

#[derive(Component)]
struct Player;

fn setup(mut commands: Commands) {
    // Spawn a 2D camera so we can see anything at all.
    commands.spawn(Camera2d);

    // Spawn the player: a colored square with our Player marker.
    commands.spawn((
        Sprite::from_color(Color::srgb(0.25, 0.5, 1.0), Vec2::new(50.0, 50.0)),
        Transform::from_xyz(0.0, 0.0, 0.0),
        Player,
    ));
}

fn move_player(
    keyboard: Res<ButtonInput<KeyCode>>,
    time: Res<Time>,
    mut players: Query<&mut Transform, With<Player>>,
) {
    let speed = 300.0;
    for mut transform in &mut players {
        let mut direction = Vec2::ZERO;
        if keyboard.pressed(KeyCode::ArrowLeft) {
            direction.x -= 1.0;
        }
        if keyboard.pressed(KeyCode::ArrowRight) {
            direction.x += 1.0;
        }
        if keyboard.pressed(KeyCode::ArrowUp) {
            direction.y += 1.0;
        }
        if keyboard.pressed(KeyCode::ArrowDown) {
            direction.y -= 1.0;
        }
        transform.translation += (direction.normalize_or_zero() * speed * time.delta_secs()).extend(0.0);
    }
}

Everything you need to understand this program is covered next.

Note

Notice time.delta_secs() in the movement math. Multiplying movement by the frame's elapsed time makes motion frame-rate independent, so your player moves at the same real-world speed on a 30 FPS laptop and a 240 FPS desktop.


4. The ECS: Core Concepts

Bevy's data model is an Entity Component System. Instead of class hierarchies, you compose game objects out of plain data and operate on them with functions.

Term Definition
Entity A lightweight ID. It owns no data itself — it's a key that components attach to.
Component A piece of data attached to an entity (position, health, sprite, …). A Rust struct/enum with #[derive(Component)].
System A function that queries and mutates components and resources. Bevy runs systems for you, in parallel when safe.
Resource Globally-unique data not tied to any entity (the clock, asset stores, settings).
World The container that stores all entities, components, and resources.
Schedule An ordered collection of systems (e.g. Update, FixedUpdate).

4.1 Components

Define a component by deriving Component:

use bevy::prelude::*;

#[derive(Component)]
struct Health(f32);

#[derive(Component)]
struct Velocity {
    x: f32,
    y: f32,
}

// A "marker" component carries no data; it just tags entities.
#[derive(Component)]
struct Enemy;

4.2 Spawning entities

You create entities by spawning a bundle of components — any tuple of components is a bundle:

fn spawn_enemies(mut commands: Commands) {
    commands.spawn((
        Enemy,
        Health(100.0),
        Velocity { x: 1.0, y: 0.0 },
        Transform::from_xyz(10.0, 0.0, 0.0),
    ));
}

4.3 Required components

A component can declare other components it requires. When you add it, Bevy automatically inserts the required components (using their Default, or a custom constructor) if they aren't already present. Many engine components use this — for example, adding Sprite automatically brings in Transform.

use bevy::prelude::*;

#[derive(Component)]
#[require(Transform, Velocity)]
struct Projectile;

#[derive(Component, Default)]
struct Velocity(Vec2);

#[derive(Component, Default)]
struct Transform2;
# // (Transform is provided by the engine; shown here only for illustration)

You can also register requirements from a plugin or at app build time:

app.register_required_components::<Projectile, Velocity>();
// Or with a custom constructor:
app.register_required_components_with::<Projectile, Velocity>(|| Velocity(Vec2::X));

Note

Required components must be registered before the component is first added to any entity. Doing this in a plugin's build is the safe, conventional place.

4.4 Systems and queries

A system is a function whose parameters describe the data it wants. Bevy inspects those parameters, fetches the data, and calls your function. The most important parameter is Query, which iterates entities matching a set of components.

// Move every entity that has both Transform and Velocity.
fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res<Time>) {
    for (mut transform, velocity) in &mut query {
        transform.translation.x += velocity.x * time.delta_secs();
        transform.translation.y += velocity.y * time.delta_secs();
    }
}

Read access uses &Component; write access uses &mut Component. Bevy uses this information to run non-conflicting systems in parallel automatically.

4.5 Query filters

Filters narrow a query without fetching the component's data. Combine them in the second type parameter of Query.

Filter Meaning
With<T> Entity must have component T.
Without<T> Entity must not have component T.
Added<T> T was added to the entity since this system last ran.
Changed<T> T was added or mutated since this system last ran.
Or<(A, B)> Matches if either filter A or B matches.
AnyOf<(A, B)> Fetches whichever of the listed components are present (as Options).
Has<T> Fetches a bool indicating whether T is present (does not filter).
Allow<T> Opt a query back into seeing entities hidden by a disabling component.
// Only players, never enemies — and only react when their Health actually changed.
fn on_player_damaged(query: Query<&Health, (With<Player>, Without<Enemy>, Changed<Health>)>) {
    for health in &query {
        println!("player health is now {}", health.0);
    }
}
# #[derive(bevy::prelude::Component)] struct Player;
# #[derive(bevy::prelude::Component)] struct Enemy;
# #[derive(bevy::prelude::Component)] struct Health(f32);

Tip

Changed<T> and Added<T> are the foundation of efficient, reactive logic. Instead of recomputing everything every frame, only do work for entities whose data actually changed.

4.6 Accessing a single entity

When you expect exactly one matching entity, Single and Populated are cleaner than looping:

// Panics/skips gracefully if there isn't exactly one player.
fn read_player(player: Single<&Transform, With<Player>>) {
    println!("player at {:?}", player.translation);
}
# #[derive(bevy::prelude::Component)] struct Player;

Single<D> requires exactly one match; Option<Single<D>> tolerates zero or one; Populated<D> is a query guaranteed to be non-empty (the system is skipped when empty).

4.7 Resources

Resources are global singletons. Define one with #[derive(Resource)] and insert it into the app:

use bevy::prelude::*;

#[derive(Resource)]
struct Score(u32);

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(Score(0))
        .add_systems(Update, award_points)
        .run();
}

fn award_points(mut score: ResMut<Score>) {
    score.0 += 1;
}
Access Use in a system
Res<T> Shared, read-only access to resource T.
ResMut<T> Exclusive, mutable access to resource T.
Local<T> Per-system state that isn't shared with anyone else.
NonSend<T> / NonSendMut<T> Access to thread-local (!Send) data; forces the system onto the main thread.

Use init_resource::<T>() for resources that implement Default or FromWorld, and insert_resource(value) to provide an explicit value.

4.8 Commands: structural changes

Spawning, despawning, and adding/removing components are structural changes. They can't happen instantly mid-system (other systems might be reading the world in parallel), so you queue them with Commands. They are applied at the next synchronization point.

fn spawn_and_modify(mut commands: Commands, enemies: Query<Entity, With<Enemy>>) {
    // Spawn a new entity and keep its id.
    let id = commands.spawn((Player, Health(100.0))).id();

    // Add or remove components later.
    commands.entity(id).insert(Velocity { x: 0.0, y: 0.0 });
    commands.entity(id).remove::<Health>();

    // Despawn all enemies.
    for enemy in &enemies {
        commands.entity(enemy).despawn();
    }
}
# use bevy::prelude::*;
# #[derive(Component)] struct Player;
# #[derive(Component)] struct Enemy;
# #[derive(Component)] struct Health(f32);
# #[derive(Component)] struct Velocity { x: f32, y: f32 }

Important

Changes made through Commands are deferred. If a later system in the same schedule run needs to see a freshly spawned entity, insert an ApplyDeferred step between them, or do the read in a subsequent schedule.


5. Building the App: Plugins and Schedules

5.1 The App builder

App::new() creates an app with core engine scaffolding. You then chain builder methods:

Method Purpose
add_plugins(...) Install a plugin or group of plugins.
add_systems(schedule, systems) Add one or more systems to a schedule.
insert_resource(value) / init_resource::<T>() Provide a resource.
add_message::<M>() Enable a message (event) type M.
add_observer(observer) Register a global observer (see §7).
configure_sets(schedule, sets) Order and gate groups of systems.
run() Build everything and start the main loop.

5.2 Plugins

A plugin is any type implementing Plugin. Its build method receives &mut App, letting it register whatever it needs — systems, resources, types. This is how you organize a real game into modules.

use bevy::prelude::*;

pub struct CombatPlugin;

impl Plugin for CombatPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(Score(0))
            .add_systems(Update, (apply_damage, check_deaths));
    }
}
# #[derive(Resource)] struct Score(u32);
# fn apply_damage() {}
# fn check_deaths() {}

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

You can also pass a tuple of plugins: .add_plugins((CombatPlugin, AudioPlugin, UiPlugin)).

5.3 DefaultPlugins vs MinimalPlugins

Bevy ships two built-in plugin groups:

Group What it includes
DefaultPlugins The full windowed engine: logging, input, windowing, assets, rendering, audio, UI, scenes, gizmos, animation, gamepad support, picking, and more. This is what most games use.
MinimalPlugins The bare minimum: a task pool, frame counter, time, and a schedule runner loop. No window, no rendering. Ideal for headless apps, servers, and tests.

DefaultPlugins honors your Cargo feature flags — disabling a feature like bevy_audio removes the corresponding plugin automatically.

You can customize a group's plugins (reorder, replace settings, or disable one) with .set(...) and .build():

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "My Game".into(),
                resolution: (1280.0, 720.0).into(),
                ..default()
            }),
            ..default()
        }))
        .run();
}

Note

MinimalPlugins drives the app with a ScheduleRunnerPlugin. By default it loops as fast as possible (which pins a CPU core). For a headless server, set a fixed wait between iterations with ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0 / 60.0)), or run a single iteration with run_once.

5.4 The schedule pipeline

Each frame, Bevy runs the Main schedule, which runs a fixed sequence of sub-schedules. You add your systems to whichever one fits.

Startup phase (runs ONCE at launch):
    PreStartup  →  Startup  →  PostStartup

Every frame, in order:
    First
    PreUpdate
    RunFixedMainLoop   ← runs the fixed-timestep schedules 0..N times (see §8.2)
    Update             ← most of your game logic goes here
    SpawnScene
    PostUpdate
    Last
Schedule Typical use
PreStartup / Startup / PostStartup One-time setup: spawn the camera, load assets, build the initial scene.
First Very early per-frame bookkeeping (time is updated here).
PreUpdate Ingest raw OS input into engine resources, before your logic reads it.
Update Your per-frame gameplay logic. Most systems live here.
SpawnScene Scene spawning is processed here.
PostUpdate React to changes from Update (e.g. transform/hierarchy propagation, rendering extract).
Last End-of-frame cleanup.

The fixed-timestep schedules — FixedFirst, FixedPreUpdate, FixedUpdate, FixedPostUpdate, FixedLast — run inside RunFixedMainLoop (see §8.2).

5.5 Ordering systems

By default, systems in a schedule may run in any order (and in parallel). When order matters, declare it explicitly:

app.add_systems(Update, (
    read_input,
    apply_movement.after(read_input),
    detect_collisions.after(apply_movement),
));

// Or as an ordered chain:
app.add_systems(Update, (read_input, apply_movement, detect_collisions).chain());

Group related systems with system sets for coarse ordering and shared run conditions:

#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum GameSet {
    Input,
    Logic,
    Render,
}

app.configure_sets(Update, (GameSet::Input, GameSet::Logic, GameSet::Render).chain());
app.add_systems(Update, read_input.in_set(GameSet::Input));
# use bevy::prelude::*;
# fn read_input() {}

5.6 Run conditions

Attach a run_if condition to skip a system unless a predicate holds:

app.add_systems(Update, pause_menu.run_if(in_state(GameState::Paused)));
app.add_systems(Update, autosave.run_if(on_timer(std::time::Duration::from_secs(30))));
# use bevy::prelude::*;
# #[derive(States, Clone, PartialEq, Eq, Hash, Debug, Default)] enum GameState { #[default] Paused }
# fn pause_menu() {}
# fn autosave() {}

Bevy provides many built-in conditions in common_conditions (re-exported in the prelude), including resource_exists, resource_changed, any_with_component, on_event/on_message, and run_once.


6. Messages (Events)

Messages let systems communicate without direct coupling: one system writes, another reads. (Earlier Bevy versions called these "events"; the buffered API is now Message.)

use bevy::prelude::*;

#[derive(Message)]
struct PlayerDied {
    score: u32,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_message::<PlayerDied>()
        .add_systems(Update, (emit_death, on_death))
        .run();
}

fn emit_death(mut writer: MessageWriter<PlayerDied>) {
    writer.write(PlayerDied { score: 42 });
}

fn on_death(mut reader: MessageReader<PlayerDied>) {
    for ev in reader.read() {
        println!("player died with score {}", ev.score);
    }
}
Type Role
MessageWriter<M> Send messages of type M.
MessageReader<M> Read messages since this system last ran.
Messages<M> The underlying double-buffered queue resource.

Warning

Messages live for two frames by default (double-buffered). If a reader runs less often than messages are written, it can miss some. For guaranteed delivery, use observers (§7) or ensure your reader runs every frame.


7. Observers and Lifecycle Events

Observers react immediately to an event the moment it is triggered, rather than polling a queue. They're ideal for one-off reactions: an entity took damage, a button was clicked, a component was added.

use bevy::prelude::*;

#[derive(EntityEvent)]
struct Damage {
    entity: Entity,
    amount: f32,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_observer(on_damage)
        .run();
}

// `On<Damage>` gives you the triggered event.
fn on_damage(event: On<Damage>, mut healths: Query<&mut Health>) {
    if let Ok(mut health) = healths.get_mut(event.entity) {
        health.0 -= event.amount;
    }
}
# #[derive(Component)] struct Health(f32);

// Trigger an observer from anywhere you have Commands:
fn deal_damage(mut commands: Commands, target: Entity) {
    commands.trigger(Damage { entity: target, amount: 10.0 });
}

7.1 Lifecycle events

Bevy automatically fires lifecycle events you can observe: Add, Insert, Remove, and Despawn for components. You can also detect removed components in a regular system with RemovedComponents<T>.

// Run cleanup whenever an Enemy component is removed this frame.
fn cleanup_on_enemy_removed(mut removed: RemovedComponents<Enemy>) {
    for entity in removed.read() {
        println!("enemy {entity} was removed");
    }
}
# use bevy::prelude::*;
# #[derive(Component)] struct Enemy;

Tip

Reach for observers when you need an immediate, targeted reaction and messages when you want decoupled, batch-processed communication that several systems may consume.


8. Time and the Game Loop

8.1 The Time resource

The Time resource tracks elapsed time. Use it to make logic frame-rate independent.

fn tick(time: Res<Time>) {
    let dt: f32 = time.delta_secs();          // seconds since last frame
    let elapsed: f32 = time.elapsed_secs();   // seconds since startup
    println!("frame took {dt}s, total {elapsed}s");
}
# use bevy::prelude::*;

8.2 Fixed timestep

Physics and other simulations should advance in fixed-size steps for determinism, independent of render frame rate. Put that logic in FixedUpdate, which runs zero or more times per frame inside RunFixedMainLoop until the accumulated time is consumed.

app.add_systems(FixedUpdate, simulate_physics);

// Read fixed-step time inside FixedUpdate systems:
fn simulate_physics(time: Res<Time<Fixed>>) {
    let step = time.delta_secs(); // constant (default: 1/64th of a second)
    // ...advance the simulation by `step`...
}
# use bevy::prelude::*;
# fn _x() {}

You can change the fixed rate:

app.insert_resource(Time::<Fixed>::from_hz(60.0)); // 60 simulation steps per second
Schedule (inside RunFixedMainLoop) Use
FixedFirst Early fixed-step bookkeeping.
FixedPreUpdate Prepare state before fixed logic.
FixedUpdate Deterministic gameplay/physics.
FixedPostUpdate React to fixed-step changes.
FixedLast Fixed-step cleanup.

Note

A single render frame may run FixedUpdate zero times (if the frame was very fast) or several times (if it was slow). To render smoothly between fixed steps, interpolate using the "overstep" fraction available from Time<Fixed>.


9. Transforms and Hierarchy

9.1 Transform and GlobalTransform

Every visible entity has a Transform (its local position, rotation, and scale) and a GlobalTransform (its absolute world-space transform, computed by the engine each frame in PostUpdate).

use bevy::prelude::*;

fn spawn_object(mut commands: Commands) {
    commands.spawn((
        Transform::from_xyz(0.0, 1.0, 0.0)
            .with_rotation(Quat::from_rotation_y(std::f32::consts::FRAC_PI_4))
            .with_scale(Vec3::splat(2.0)),
        // ...mesh/sprite...
    ));
}

Transform is built on the glam math library (re-exported through bevy_math and the prelude), which Bevy uses with a right-handed, Y-up convention. The core math types you'll use constantly:

Type Use
Vec2 / Vec3 / Vec4 2D / 3D / 4D vectors. Constants: Vec3::X, Vec3::Y, Vec3::Z, Vec3::ZERO.
Quat A unit quaternion rotation. Build with Quat::from_rotation_y(angle), Quat::from_axis_angle(axis, angle), or Quat::from_euler(EulerRot::XYZ, x, y, z).
Mat3 / Mat4 3×3 / 4×4 matrices for projections and advanced transforms.
Dir2 / Dir3 Normalized direction wrappers.
// Common vector operations:
let a = Vec3::new(1.0, 2.0, 3.0);
let len = a.length();
let unit = a.normalize_or_zero();
let dot = a.dot(Vec3::Y);
let cross = Vec3::X.cross(Vec3::Y); // == Vec3::Z
let mid = Vec3::ZERO.lerp(a, 0.5);
# use bevy::prelude::*;

9.2 Parent–child relationships

Entities form hierarchies through the ChildOf / Children relationship. A child's GlobalTransform is its Transform combined with all of its ancestors' transforms.

use bevy::prelude::*;

fn spawn_hierarchy(mut commands: Commands) {
    commands
        .spawn(Transform::from_xyz(0.0, 0.0, 0.0)) // parent
        .with_children(|parent| {
            parent.spawn(Transform::from_xyz(1.0, 0.0, 0.0)); // child, offset from parent
            parent.spawn(Transform::from_xyz(-1.0, 0.0, 0.0));
        });
}

You can also set relationships explicitly with the ChildOf component, and query them with Children and ChildOf. Despawning a parent despawns its children.


10. Rendering

Bevy's renderer is built on wgpu, a cross-platform graphics API that runs on Vulkan, Metal, DirectX 12, OpenGL, and — on the web — WebGPU/WebGL2. For everyday game code you rarely touch wgpu directly; you spawn cameras and renderable entities and the engine does the rest.

10.1 Cameras

Nothing renders without a camera. Spawn Camera2d for 2D and Camera3d for 3D.

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

fn setup_3d(mut commands: Commands) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 5.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
}
# use bevy::prelude::*;

10.2 2D essentials

fn spawn_sprite(mut commands: Commands, assets: Res<AssetServer>) {
    // Image sprite loaded from assets/player.png
    commands.spawn(Sprite::from_image(assets.load("player.png")));

    // Solid-color rectangle
    commands.spawn(Sprite::from_color(Color::srgb(1.0, 0.0, 0.0), Vec2::new(64.0, 64.0)));
}
# use bevy::prelude::*;

10.3 3D essentials

A typical 3D scene needs a mesh, a material, a light, and a camera:

use bevy::prelude::*;

fn setup_scene(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // A cuboid mesh with a PBR material.
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
        Transform::from_xyz(0.0, 0.5, 0.0),
    ));

    // A directional "sun" light.
    commands.spawn((
        DirectionalLight { shadows_enabled: true, ..default() },
        Transform::from_xyz(4.0, 8.0, 4.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    // The camera.
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-3.0, 4.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));
}

Bevy provides physically-based rendering (bevy_pbr) with StandardMaterial, point/spot/directional lights, shadows, and built-in mesh primitives (Cuboid, Sphere, Plane3d, Cylinder, Capsule3d, and more).

10.4 Colors

Construct colors in whichever space suits you:

let red = Color::srgb(1.0, 0.0, 0.0);
let translucent = Color::srgba(0.0, 0.0, 1.0, 0.5);
let hsl = Color::hsl(120.0, 1.0, 0.5);
let named = bevy::color::palettes::css::TOMATO;
# use bevy::prelude::*;

Tip

If your 3D scene renders entirely pink, you most likely need the tonemapping_luts Cargo feature (or to change the camera's Tonemapping method). The look-up tables it bundles are required for the default tonemapper.


11. Common Tasks (Recipes)

11.1 Loading and using assets

The AssetServer loads files asynchronously from your assets/ directory and hands back a Handle<T>. Stored assets live in the Assets<T> resource.

use bevy::prelude::*;

#[derive(Resource)]
struct GameAssets {
    player: Handle<Image>,
    music: Handle<AudioSource>,
}

fn load_assets(mut commands: Commands, assets: Res<AssetServer>) {
    commands.insert_resource(GameAssets {
        player: assets.load("sprites/player.png"),
        music: assets.load("audio/theme.ogg"),
    });
}

Note

Handles are cheap, cloneable references. Loading is asynchronous: a freshly loaded asset may not be available for a frame or two. Query Assets<T> or use asset events/states to know when it's ready.

11.2 Playing audio

fn play_music(mut commands: Commands, assets: Res<AssetServer>) {
    commands.spawn((
        AudioPlayer::new(assets.load("audio/theme.ogg")),
        PlaybackSettings::LOOP,
    ));
}
# use bevy::prelude::*;

Audio requires the bevy_audio feature plus a format feature (vorbis for .ogg, flac, mp3, wav, …). The audio collection enables bevy_audio + vorbis; audio-all-formats enables every decoder.

11.3 Reading input

Input Resource / type
Keyboard Res<ButtonInput<KeyCode>>
Mouse buttons Res<ButtonInput<MouseButton>>
Mouse motion / wheel MessageReader<MouseMotion>, MessageReader<MouseWheel>
Gamepad Query<&Gamepad> (requires bevy_gilrs)
Touch Res<Touches>
fn input(keys: Res<ButtonInput<KeyCode>>, mouse: Res<ButtonInput<MouseButton>>) {
    if keys.just_pressed(KeyCode::Space) {
        println!("jump!");
    }
    if keys.pressed(KeyCode::KeyW) {
        println!("moving forward");
    }
    if mouse.just_pressed(MouseButton::Left) {
        println!("fire!");
    }
}
# use bevy::prelude::*;
Method Meaning
pressed(b) true every frame the input is held.
just_pressed(b) true only on the frame it went down.
just_released(b) true only on the frame it went up.

11.4 Game states

States model high-level modes — main menu, playing, paused, game over. Define an enum implementing States, register it, and gate systems on it.

use bevy::prelude::*;

#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
enum AppState {
    #[default]
    Menu,
    InGame,
    Paused,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_state::<AppState>()
        .add_systems(OnEnter(AppState::InGame), spawn_level)
        .add_systems(OnExit(AppState::InGame), despawn_level)
        .add_systems(Update, gameplay.run_if(in_state(AppState::InGame)))
        .run();
}
# fn spawn_level() {}
# fn despawn_level() {}
# fn gameplay() {}
Hook Fires when
OnEnter(S) You transition into state S.
OnExit(S) You transition out of state S.
in_state(S) Run condition: true while in state S.

Change state by setting the NextState<AppState> resource: next.set(AppState::Paused).

States require the bevy_state feature (enabled by default).

11.5 Drawing debug gizmos

Gizmos draw immediate-mode debug lines and shapes — great for visualizing colliders, rays, and grids. They don't persist; you redraw them every frame.

fn draw_debug(mut gizmos: Gizmos) {
    gizmos.line(Vec3::ZERO, Vec3::new(0.0, 5.0, 0.0), Color::srgb(0.0, 1.0, 0.0));
    gizmos.circle_2d(Vec2::ZERO, 50.0, Color::WHITE);
}
# use bevy::prelude::*;

11.6 Building a UI

Bevy includes its own ECS-driven UI framework (bevy_ui). UI elements are entities with Node components, laid out with a flexbox-style system.

use bevy::prelude::*;

fn spawn_ui(mut commands: Commands) {
    commands.spawn((
        Node {
            width: Val::Percent(100.0),
            height: Val::Percent(100.0),
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
            ..default()
        },
    )).with_children(|parent| {
        parent.spawn((
            Text::new("Hello, Bevy!"),
            TextColor(Color::WHITE),
        ));
    });
}

11.7 Spawning and despawning at runtime

fn spawn_bullet(mut commands: Commands, origin: Vec3) {
    commands.spawn((
        Bullet,
        Transform::from_translation(origin),
        Velocity(Vec3::Z * 20.0),
    ));
}

fn despawn_offscreen(mut commands: Commands, bullets: Query<(Entity, &Transform), With<Bullet>>) {
    for (entity, transform) in &bullets {
        if transform.translation.length() > 1000.0 {
            commands.entity(entity).despawn();
        }
    }
}
# use bevy::prelude::*;
# #[derive(Component)] struct Bullet;
# #[derive(Component)] struct Velocity(Vec3);

11.8 Exiting the app

Send the AppExit message to quit cleanly:

fn quit_on_escape(keys: Res<ButtonInput<KeyCode>>, mut exit: MessageWriter<AppExit>) {
    if keys.just_pressed(KeyCode::Escape) {
        exit.write(AppExit::Success);
    }
}
# use bevy::prelude::*;

12. Cargo Features Cheat Sheet

Bevy exposes a large set of Cargo features. Enabling more adds functionality at the cost of longer compile times and bigger binaries. Features are organized into three tiers: profiles (high-level), collections (mid-level), and individual features.

12.1 Profiles

Pair these with default-features = false to compile only the slice of Bevy you need.

Profile Description
default The full Bevy experience — a combination of 2d, 3d, ui, and audio.
2d Core framework + 2D functionality + scenes + picking.
3d Core framework + 3D functionality + scenes + picking.
ui Core framework + Bevy UI + scenes + picking.
# A lean 2D game:
bevy = { version = "0.18", default-features = false, features = ["2d"] }

12.2 Selected collections

Collection Enables
dev Dev-time conveniences: debug, bevy_dev_tools, file_watcher (asset hot-reloading). Do not ship with this.
audio bevy_audio + vorbis.
audio-all-formats All audio decoders (aac, flac, mp3, mp4, vorbis, wav).
scene bevy_world_serialization + bevy_scene.
picking All picking backends (mesh_picking, sprite_picking, ui_picking).
default_app Baseline for headless apps (no renderer).
default_no_std Recommended defaults for no_std targets.

12.3 Commonly toggled individual features

Feature What it adds
dynamic_linking Faster iterative compiles (dev only; never ship).
file_watcher Hot-reload assets when files change on disk.
multi_threaded Multithreaded engine parallelism (on by default).
serialize serde serialization support.
trace / trace_tracy / trace_chrome Performance tracing back-ends.
webgl2 / webgpu Web rendering back-ends (webgpu overrides webgl2).
wayland / x11 Linux display-server back-ends.
Image formats png, jpeg, hdr, ktx2, dds, webp, exr, bmp, gif, tga, tiff, qoi, pnm, ff, basis-universal.
Audio formats vorbis, flac, mp3, wav, aac, mp4.
meshlet Experimental high-poly meshlet renderer.
bevy_solari Experimental raytraced lighting.
dlss NVIDIA Deep Learning Super Sampling.

Tip

The canonical, always-current feature list — including which bevy_* sub-crate each feature toggles — is generated into docs/cargo_features.md in the repository and rendered in the Bevy API docs. The Admin Guide documents the full set.


13. Building and Shipping

13.1 Desktop

# Development
cargo run

# Release build (optimized)
cargo run --release
cargo build --release   # binary in target/release/

Remember to ship your assets/ directory alongside the executable — the AssetServer loads files relative to it.

13.2 Web (WebAssembly)

rustup target add wasm32-unknown-unknown
cargo build --release --target wasm32-unknown-unknown

Then use a tool like wasm-bindgen (or wasm-server-runner / trunk) to produce the JS glue and serve it. Choose a web rendering backend with features:

WebGL2 (broadest support)

bevy = { version = "0.18", features = ["webgl2"] }

WebGPU (best performance)

bevy = { version = "0.18", features = ["webgpu"] }

Note

webgpu overrides webgl2 when both are enabled — a single Wasm build targets one or the other, not both. WebGL2 lacks compute shaders, so some advanced rendering features are WebGPU-only.

13.3 Mobile

Bevy supports Android and iOS. The repository's examples/mobile shows a complete setup. For Android, the modern path uses GameActivity (android-game-activity, the default); the older android-native-activity is used by the deprecated cargo-apk tool.

# Android targets
rustup target add aarch64-linux-android armv7-linux-androideabi

Important

Bevy's example apps target Android API level 33 by default. If you use bevy_audio, the minimum supported Android API version is 26 (Android 8 / Oreo).


14. Troubleshooting

Error: cannot find a window / nothing appears on screen

Cause: No camera was spawned, or you used MinimalPlugins (which has no window). Solution: Spawn a Camera2d or Camera3d in a Startup system, and use DefaultPlugins. Prevention: Every visual app needs both a window-providing plugin group and a camera.

Error: everything in my 3D scene renders solid pink

Cause: The default tonemapper needs look-up tables that aren't bundled. Solution: Enable the tonemapping_luts feature, or set a different Tonemapping on your camera. Prevention: Keep tonemapping_luts enabled (it is part of the default 3D feature set).

Error: my sprite/model is invisible but no error is logged

Cause: The asset is still loading, the entity is outside the camera's view, or it has no Transform. Solution: Confirm the file path is correct and under assets/, check the camera position, and ensure a Transform is present (most renderable components require it automatically). Prevention: Use asset events or a loading state to wait until assets are ready before relying on them.

Error: Resource requested by ... does not exist

Cause: A system accessed Res<T>/ResMut<T> for a resource that was never inserted. Solution: Call insert_resource(T) or init_resource::<T>() during app setup before the system runs. Prevention: Initialize resources in the same plugin that adds the systems depending on them.

Error: my system never runs

Cause: It was added to the wrong schedule, gated behind a run_if that's always false, or filtered out by an empty Single/Populated. Solution: Verify the schedule (Update vs Startup), check the run condition, and confirm matching entities exist. Prevention: Start with Update and no run conditions, then add gating once the system works.

Error: a newly spawned entity isn't visible to another system this frame

Cause: Commands are deferred and applied at the next sync point. Solution: Read the entity in a later schedule, or insert an ApplyDeferred step between the spawning and reading systems. Prevention: Treat structural changes as "queued"; don't assume they're immediate within one schedule run.

Error: input feels laggy or movement speed varies by machine

Cause: Game logic isn't scaled by Time::delta_secs(), so it depends on frame rate. Solution: Multiply per-frame changes by time.delta_secs(), or move the logic into FixedUpdate. Prevention: Always scale continuous motion by delta time.

Error: my MessageReader misses some messages

Cause: Messages are double-buffered (live ~2 frames); a reader running too infrequently drops them. Solution: Ensure the reader runs every frame, or switch to an observer for guaranteed, immediate handling. Prevention: Use observers for must-handle events.

Error: compile times are painfully long

Cause: Compiling the whole engine, unoptimized dependencies, or no fast-linker setup. Solution: Enable dynamic_linking for dev, optimize dependencies via [profile.dev.package."*"], and follow the Fast Compiles setup. Prevention: Trim Cargo features to only what you use.

Error: failed to find an appropriate adapter / GPU initialization fails

Cause: No compatible GPU backend (Vulkan/Metal/DX12) or outdated drivers. Solution: Update graphics drivers; on Linux ensure Vulkan is installed. As a fallback you can force a backend (see the Admin Guide's WgpuSettings / WGPU_BACKEND coverage). Prevention: Test on target hardware early.

Error: audio doesn't play

Cause: The bevy_audio feature or the relevant format decoder isn't enabled. Solution: Enable bevy_audio plus the matching format feature (vorbis for .ogg, flac, mp3, wav). Prevention: Use the audio or audio-all-formats collection.


15. Frequently Asked Questions

Q: Is Bevy production-ready? Bevy is powerful and used for real games and tools, but it is still pre-1.0 and ships breaking changes roughly every three months. Pin a version and budget time for migrations between releases.

Q: What's the difference between a Component and a Resource? A component is data attached to a specific entity (you may have thousands). A resource is a single global value not tied to any entity (the clock, asset stores, settings).

Q: When should I use Update vs FixedUpdate? Use Update for anything tied to rendering and responsiveness (input handling, UI, camera). Use FixedUpdate for deterministic simulation (physics) that must advance at a constant rate regardless of frame rate.

Q: How do I run systems in a guaranteed order? Use .before(), .after(), or .chain() when adding them, and organize coarse phases with system sets via configure_sets. Systems with no declared order may run in parallel in any order.

Q: What replaced Events/EventReader/EventWriter? The buffered communication API is now Message / MessageReader / MessageWriter (register with add_message). For immediate, targeted reactions, use Event/EntityEvent with observers (On<...> + add_observer).

Q: How do I make one entity a child of another? Spawn children with .with_children(|parent| { ... }), or attach the ChildOf relationship. Children inherit their parent's transform, and despawning a parent despawns its children.

Q: How do I quit the game from code? Send AppExit::Success through a MessageWriter<AppExit>.

Q: Do I need to enable rendering features for a headless server? No. Use MinimalPlugins (or default_app without a renderer) and add a ScheduleRunnerPlugin loop. This is ideal for dedicated servers and CLI tools.

Q: Where do my asset files go? In an assets/ directory next to your project. AssetServer::load("sprites/player.png") resolves to assets/sprites/player.png. Ship the assets/ folder with your build.

Q: How do I hot-reload assets during development? Enable the file_watcher feature (or the dev collection). Edits to files on disk are reloaded live.

Q: Can I use only one bevy_* sub-crate instead of the umbrella bevy crate? Yes. Each module of the bevy crate (except the prelude) is published separately as bevy_app, bevy_ecs, bevy_render, and so on. Most users depend on the umbrella bevy crate for convenience.

Q: How do I target no_std (embedded / niche platforms)? Disable default features and enable the default_no_std collection (plus libm and critical-section as needed). Note that some features require std.

Q: Why is my whole CPU core pinned at 100% in a headless app? ScheduleRunnerPlugin loops as fast as possible by default. Configure it with run_loop(Duration) to cap the iteration rate, or run_once for a single tick.

Q: How do I detect when a component changed instead of polling every frame? Add the Changed<T> (or Added<T>) filter to your query. The query then only yields entities whose T changed since your system last ran.

Q: How do I customize the window title/size? Configure WindowPlugin via DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { .. }), .. }).

Q: Where can I learn more? Start with the official Quick Start Guide, browse the examples, and read the API docs. For deployment, configuration, and architecture, see the companion Bevy Admin Guide.