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-devdevelopment line of Bevy (themainbranch). 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 yourCargo.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_linkingenabled — 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
buildis 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>andAdded<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
Commandsare deferred. If a later system in the same schedule run needs to see a freshly spawned entity, insert anApplyDeferredstep 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
MinimalPluginsdrives the app with aScheduleRunnerPlugin. By default it loops as fast as possible (which pins a CPU core). For a headless server, set a fixed wait between iterations withScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(1.0 / 60.0)), or run a single iteration withrun_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
FixedUpdatezero 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 fromTime<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_lutsCargo feature (or to change the camera'sTonemappingmethod). 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 intodocs/cargo_features.mdin 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
webgpuoverrideswebgl2when 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.