How to Use the Bevy Game Engine in Rust

Install Bevy as a Cargo dependency and run a minimal App to start building games in Rust.

The Game Loop in Rust

You have a game idea. Maybe a roguelike with procedural dungeons, or a 2D platformer where physics feels crisp. You've played with Python scripts or JS canvas demos, but now you want the performance and safety of Rust without fighting a C++ legacy codebase. You need an engine that speaks Rust's language, not one that forces Rust to wear a C++ costume. Bevy fits that gap. It's a data-driven engine written in Rust, designed to leverage the borrow checker and the ECS pattern without making you feel like you're writing a compiler plugin.

Bevy is not part of the standard library. It lives on crates.io and installs as a dependency. You bring your own project structure; Bevy brings the renderer, the asset loader, the input system, and the execution model. The result is an engine that feels like a native Rust citizen, not a foreign import.

Entity-Component-System: Data over Objects

Bevy uses an Entity-Component-System architecture. If you're coming from Python or JS, you're probably used to classes. A Player class has health, position, and a move() method. In Bevy, you break that apart. An entity is just an ID, a ticket number. Components are plain data structs attached to that ID. Systems are functions that process entities based on the components they have.

Think of a spreadsheet. Rows are entities. Columns are components. A system is a formula that runs over specific columns. If a row has a "Position" column and a "Velocity" column, the "Physics" formula updates the position. You don't tell the row to move. The formula moves the data. This separation lets the engine parallelize work and keeps data cache-friendly. Components of the same type are stored contiguously in memory, so iterating over them is fast. The CPU prefetches data efficiently.

Stop thinking about objects. Start thinking about data columns.

Minimal Setup

Add Bevy to your Cargo.toml. The version number moves fast; check crates.io for the latest stable release. The example below uses 0.15.

[package]
name = "my_game"
version = "0.1.0"
edition = "2024"

[dependencies]
bevy = "0.15"

Convention aside: Bevy encourages use bevy::prelude::*;. This glob import is the one place in the Rust community where wildcards are standard. The prelude pulls in App, Commands, Query, Res, and the core traits. Fighting the glob import adds noise without value.

use bevy::prelude::*;

/// Entry point for the Bevy application.
fn main() {
    App::new()
        // DefaultPlugins sets up the window, renderer, and input handling.
        // Without this, you get a headless app that does nothing visible.
        .add_plugins(DefaultPlugins)
        // Systems run every frame. This one prints a message to verify the loop.
        .add_systems(Update, hello_world)
        .run();
}

/// A simple system that runs once per frame during the Update schedule.
fn hello_world() {
    println!("Bevy is running!");
}

Anatomy of a Bevy App

When you call App::new(), you get a builder that accumulates configuration. add_plugins(DefaultPlugins) is the heavy lifter. It wires up the window manager, the WGPU renderer, asset loading, and input polling. If you skip this, your app compiles but you won't see a window.

add_systems registers your logic. Bevy groups systems into schedules. Update is the main loop that runs every frame. Startup runs once at the beginning. FixedUpdate runs at a constant rate for physics. run() hands control to Bevy's main loop, which polls events, runs schedules, and renders frames until the window closes.

Bevy distinguishes between components and resources. Components are per-entity data. Resources are global singletons. Time, AssetServer, and Window are resources. You inject resources into systems using Res<T>. You access components using Query. Never query a resource; inject it as a system parameter.

Treat Commands as a todo list, not an action queue.

Realistic Example: Moving Sprite

This example spawns a red square and moves it across the screen. It demonstrates components, systems, queries, and resources.

use bevy::prelude::*;

/// Component to track movement speed.
/// Derive Component is required for Bevy to recognize this struct.
#[derive(Component)]
struct Velocity {
    x: f32,
    y: f32,
}

/// Entry point.
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // Startup systems run once at the beginning of the app.
        .add_systems(Startup, spawn_player)
        // Update systems run every frame.
        .add_systems(Update, move_player)
        .run();
}

/// Startup system to create the initial game state.
fn spawn_player(mut commands: Commands) {
    commands.spawn((
        // SpriteBundle contains Camera, Transform, and ImageNode components.
        SpriteBundle {
            // Position the sprite at the center of the screen.
            transform: Transform::from_xyz(0.0, 0.0, 0.0),
            // Give it a red color.
            color: Color::srgb(1.0, 0.0, 0.0),
            sprite: Sprite {
                // Set the size to 64x64 pixels.
                custom_size: bevy::math::Vec2::new(64.0, 64.0),
                ..default()
            },
            ..default()
        },
        // Attach our custom velocity component.
        Velocity { x: 100.0, y: 0.0 },
    ));
}

/// System that queries entities with Transform and Velocity.
/// Bevy ensures this runs after spawn_player because Update runs after Startup.
fn move_player(
    // Res<Time> gives access to global time resources.
    time: Res<Time>,
    // Query fetches mutable Transform and immutable Velocity for matching entities.
    mut query: Query<(&mut Transform, &Velocity)>,
) {
    // delta_seconds gives the time elapsed since the last frame.
    // This keeps movement consistent regardless of frame rate.
    let delta = time.delta_seconds();

    for (mut transform, velocity) in &mut query {
        transform.translation.x += velocity.x * delta;
        transform.translation.y += velocity.y * delta;
    }
}

Convention aside: default() is used heavily in Bevy structs. When you only need to override a few fields, use ..default() to fill the rest. It keeps code readable and future-proofs against new fields.

The borrow checker protects your queries. If it compiles, the data access is safe.

Pitfalls and Compiler Signals

Bevy's ECS model shifts where errors appear. You won't get null pointer exceptions, but you will encounter trait bound errors and query conflicts.

If you forget #[derive(Component)] on Velocity, the compiler rejects the spawn call with E0277 (trait bound not satisfied). Bevy requires components to implement the Component trait. The derive macro generates this implementation.

Query conflicts happen when you request conflicting access in the same system. If you have Query<&mut Transform> and Query<&Transform> without filters, Bevy detects a potential conflict. The compiler may reject this, or Bevy may panic at runtime depending on the configuration. Use With and Without filters to narrow queries.

// Safe: Only queries entities that have Velocity.
// Prevents conflict with systems that modify all Transforms.
fn safe_query(mut query: Query<(&mut Transform, &Velocity)>) { ... }

Commands are buffered. You cannot use the result of a command immediately. If you spawn an entity and try to query it in the same system, you won't find it. Commands execute between systems.

Filter your queries aggressively. Narrow queries run faster and avoid conflicts.

Decision Matrix

Use Bevy when you want a data-driven architecture that scales to many entities. Use Bevy when you need a modern renderer based on WGPU that targets desktop and web without code changes. Use Bevy when you prefer Rust idioms over C++-style object hierarchies. Reach for a minimal library like wgpu directly when you are building a custom engine and need total control over the rendering pipeline. Reach for macroquad when you are prototyping a simple 2D game and want a single-file setup with immediate mode graphics. Reach for godot-rust when you need a visual editor and prefer an entity-component model managed by a GUI tool.

Where to go next