Skip to content

Split View: Bevy 게임 엔진 — Rust로 다시 쓰는 ECS 패러다임의 게임 개발 (심층 핸즈온, 2026)

|

Bevy 게임 엔진 — Rust로 다시 쓰는 ECS 패러다임의 게임 개발 (심층 핸즈온, 2026)

프롤로그 — 왜 Rust와 ECS가 게임 개발에 어울리는가

게임 엔진의 역사는 두 줄로 압축할 수 있다.

  1. 상속 시대(1995~2015) — Unity의 MonoBehaviour, Unreal의 UObject, Godot의 Node. 모든 게임 오브젝트는 클래스를 상속받고, "GameObject is a Component" 식의 깊은 계층을 만들었다. C++ 또는 C# 같은 OOP 언어가 자연스러웠다.
  2. 데이터 지향 시대(2015~) — Naughty Dog, Insomniac, Epic의 Mass(Unreal 5)가 "캐시 미스가 곧 프레임 드랍"이라는 걸 인정했다. Data-Oriented Design(DOD)이 부상했고, Entity-Component-System(ECS)이 모범 답안으로 등장했다.

ECS는 한 줄로 이렇다.

  • Entity = 그냥 ID(64비트 정수). 데이터도 동작도 없다.
  • Component = 순수 데이터(Position, Velocity, Sprite). 행동은 없다.
  • System = 함수. "어떤 component 조합을 가진 모든 entity에 대해" 행동한다.

상속 트리가 없다. Player extends Character extends Pawn 같은 것 자체가 사라진다. "플레이어"는 그냥 Player, Position, Velocity, Health component를 같이 가진 entity일 뿐이다.

여기에 Rust를 얹으면 ECS의 약점이었던 두 가지가 즉시 해결된다. 첫째, 동시성. 빌림 검사기(borrow checker)가 "두 system이 같은 component를 동시에 mutate하지 못한다"를 컴파일 타임에 보장한다. Bevy의 스케줄러는 이걸 이용해 system을 자동 병렬화한다. 둘째, 안정성. 게임 코드는 늘 포인터·null·범위 외 접근 버그로 멍든다. Rust는 그 카테고리 자체를 없앤다.

문제는 "현실"이다. 2026년 5월, Unity는 여전히 모바일·인디·VR 1위, Unreal은 AAA·시네마틱 1위, Godot은 인디 2D 진영에서 큰 점유, Bevy는 오픈소스 진영의 별이지만 출시된 상업 게임은 손에 꼽는다. 그래서 이 글은 "Year of Bevy"를 외치지 않는다. 대신 정직하게 답한다 — Bevy는 무엇을 잘하고, 어디까지 와 있고, 어디서 막혀 있는가.

이 글의 구성.

  1. ECS 패러다임 이해 — MonoBehaviour와 무엇이 다른가
  2. Bevy의 구조 — App, Schedule, Plugin
  3. 플러그인 아키텍처 — 모든 것이 플러그인이다
  4. wgpu로 렌더링 — 브라우저·데스크톱·모바일을 한 번에
  5. 첫 Bevy 게임 30분 — 키 입력으로 움직이는 원
  6. Godot·Unity·Unreal·Macroquad와의 비교
  7. 실제로 출시된 Bevy 게임들 — Foresight, Roll It Up, 그리고 Tiny Glade의 진실
  8. Bevy가 아직 준비 안 된 곳

기억할 한 줄: "엔진은 도구지 신앙이 아니다. 하지만 ECS는 도구 이전에 패러다임이고, Bevy는 그 패러다임을 Rust로 가장 정직하게 구현한 답이다."


1장 · ECS 패러다임 — Unity·Unreal과 무엇이 다른가

1.1 상속 모델의 작동 방식

Unity에서 적 캐릭터를 만들면 보통 이렇다.

// Unity / MonoBehaviour 스타일
public class Enemy : MonoBehaviour {
    public int health = 100;
    public float speed = 2.0f;
    private Transform target;

    void Start() { target = GameObject.FindWithTag("Player").transform; }
    void Update() {
        transform.position = Vector3.MoveTowards(
            transform.position, target.position, speed * Time.deltaTime);
        if (health <= 0) Destroy(gameObject);
    }
}

EnemyMonoBehaviour를 상속받고, 상태(체력·속도)와 동작(Update)을 같은 클래스에 둔다. 객체 100개면 Update가 100번 호출된다. 가상 함수 호출 + 캐시 미스 + 분기 예측 실패가 누적된다.

Unreal의 AActor + UCharacterMovementComponent 조합도 본질은 같다. AEnemy : public ACharacter : public APawn : public AActor라는 깊은 상속 체인, 그리고 각 컴포넌트는 부모 actor에 포인터를 들고 다닌다.

1.2 ECS의 작동 방식

같은 적 캐릭터를 Bevy로 쓰면.

// Bevy / ECS 스타일
use bevy::prelude::*;

#[derive(Component)]
struct Enemy;

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

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

#[derive(Component)]
struct Target(Entity);

fn move_enemies(
    time: Res<Time>,
    mut enemies: Query<(&mut Transform, &Speed, &Target), With<Enemy>>,
    targets: Query<&Transform, Without<Enemy>>,
) {
    for (mut tf, speed, target) in &mut enemies {
        if let Ok(target_tf) = targets.get(target.0) {
            let dir = (target_tf.translation - tf.translation).normalize_or_zero();
            tf.translation += dir * speed.0 * time.delta_secs();
        }
    }
}

fn kill_dead_enemies(mut commands: Commands, q: Query<(Entity, &Health), With<Enemy>>) {
    for (e, hp) in &q {
        if hp.0 <= 0 { commands.entity(e).despawn(); }
    }
}

차이는 명확하다.

  • Enemy마커 component(빈 구조체). 데이터도 행동도 없다.
  • Health(i32), Speed(f32)순수 데이터. 메서드 없다.
  • move_enemiessystem. "Enemy 마커가 있는 entity"만 골라 한 번에 처리한다.
  • entity 100개의 Position·Speed·Target은 메모리에 연속 배치된다(archetype 기반 저장). CPU 캐시 히트율이 높다.
  • move_enemieskill_dead_enemies는 다른 component에 접근하므로 자동으로 병렬 실행된다.

1.3 Archetype과 데이터 배치

Bevy(그리고 Flecs, Unity DOTS, Unreal Mass)는 archetype 기반 ECS다. "어떤 component 집합을 가진 entity"별로 메모리 청크를 만든다.

archetype A: [Position, Velocity, Sprite] → entity 1000개의 Position이 연속, Velocity가 연속, Sprite가 연속. archetype B: [Position, Velocity, Sprite, Health, Enemy] → 또 다른 청크.

Query<(&Position, &Velocity)>는 A와 B 양쪽의 청크를 순회하면서, 각 청크 안에선 단순 배열 순회로 끝난다. 가상 함수 호출도 없고, 캐시 미스도 거의 없다. 이게 ECS가 빠른 이유다.

1.4 ECS의 단점도 정직하게

ECS는 만병통치약이 아니다.

  • 상태 머신·복잡한 동작 트리는 component로 모델링하면 component가 폭발한다. Idle, Walking, Attacking, Hurt, Dead를 각각 component로 두면 system이 모든 조합을 알아야 한다. 차라리 enum 한 개를 component 안에 두는 게 낫다.
  • 부모-자식 관계는 ECS의 원본에는 없는 개념이다. Bevy는 Parent/Children component로 트리를 흉내내지만, 본질적으로 ECS와는 결이 다르다.
  • 러닝 커브가 가파르다. "왜 메서드를 못 쓰지?", "이 query가 borrow를 어떻게 갈라?" 같은 질문을 한참 한다.

기억할 한 줄: "OOP는 동사를 명사 안에 가둔다. ECS는 명사(데이터)와 동사(system)를 끝까지 분리한다."


2장 · Bevy의 구조 — App, Schedule, System

2.1 App

Bevy 프로그램의 진입점은 App이다. main 함수는 보통 이렇게 생겼다.

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, spawn_camera)
        .add_systems(Update, (move_player, count_fps).chain())
        .run();
}
  • App::new() — 비어 있는 ECS 월드와 스케줄러를 만든다.
  • .add_plugins(DefaultPlugins) — 윈도우, 입력, 렌더, 시간, 자산 로딩 등 기본 기능을 통째로 묶은 플러그인 묶음을 추가한다.
  • .add_systems(Startup, ...) — 게임 시작 시 한 번만 실행될 system.
  • .add_systems(Update, ...) — 매 프레임 실행될 system.
  • .run() — 메인 루프를 돌린다.

코드 전체에서 class GameManager : MonoBehaviour { void Awake() } 같은 매니저 클래스가 없다는 점이 핵심이다. App 자체가 매니저고, 게임 로직은 자유 함수다.

2.2 Schedule — 언제 실행되나

Bevy 0.10에서 "스테이지"가 사라지고 스케줄이 들어왔다. 0.15 기준 주요 스케줄은 이렇다.

  • Startup — 앱 시작 시 한 번.
  • PreUpdate — 매 프레임 시작, 입력 수집 후.
  • Update — 매 프레임 메인 로직.
  • PostUpdate — 매 프레임, transform 전파·물리 후처리 등.
  • FixedUpdate — 고정 timestep(기본 64Hz). 물리·네트워크 락스텝.
  • Last — 프레임 막판.

system 사이 순서는 .before(), .after(), .chain(), SystemSet으로 제어한다.

app.add_systems(
    Update,
    (
        input_system,
        movement_system.after(input_system),
        collision_system.after(movement_system),
    ),
);

2.3 Component·Resource·Event

데이터를 ECS 월드에 넣는 세 가지 방법.

  • Component — entity에 붙는 데이터. #[derive(Component)].
  • Resource — 월드에 하나만 존재하는 글로벌 데이터. 점수, 설정, 자산 핸들. #[derive(Resource)].
  • Event — 한 프레임 단위 메시지. system 간 통신. #[derive(Event)].
#[derive(Resource)]
struct Score(u32);

#[derive(Event)]
struct EnemyKilled { entity: Entity, by: Entity }

fn award_score(mut score: ResMut<Score>, mut events: EventReader<EnemyKilled>) {
    for _ in events.read() {
        score.0 += 100;
    }
}

Res<T>는 읽기 전용, ResMut<T>는 쓰기 가능. 두 system이 같은 ResMut를 잡으면 자동으로 직렬 실행되고, 한쪽이 Res 한쪽이 Res면 병렬이다.

2.4 Query — system이 데이터를 가져오는 법

fn move_player(
    time: Res<Time>,
    mut q: Query<(&mut Transform, &Speed), With<Player>>,
) {
    for (mut tf, speed) in &mut q {
        tf.translation.x += speed.0 * time.delta_secs();
    }
}

Query 시그니처가 곧 system의 "데이터 요구사항"이다. With<Player>는 필터(component를 받지는 않지만 entity가 가지고 있어야 함). 두 system이 충돌하지 않는 query를 가지면 Bevy는 자동으로 두 개를 병렬 실행한다.

기억할 한 줄: "Bevy 코드를 읽는 법 = system 시그니처만 봐도 그 system이 무엇을 읽고 무엇을 쓰는지 다 보인다."


3장 · 플러그인 아키텍처 — 모든 것이 플러그인이다

Bevy의 가장 큰 미적 결정은 "엔진 자체가 플러그인 컬렉션"이라는 점이다.

DefaultPlugins를 풀어보면 약 20개의 플러그인이 들어 있다.

  • WindowPlugin — 창 생성
  • InputPlugin — 키보드·마우스·게임패드
  • RenderPlugin — wgpu 기반 렌더
  • SpritePlugin — 2D 스프라이트
  • PbrPlugin — 3D 물리 기반 렌더링
  • UiPlugin — bevy_ui
  • AudioPlugin — kira 기반 오디오
  • AssetPlugin — 비동기 자산 로딩
  • TimePlugin, TransformPlugin, LogPlugin, ...

원하지 않으면 통째로 뺄 수 있다.

// 헤드리스 서버 — 렌더·윈도우 없이 ECS만
App::new()
    .add_plugins(MinimalPlugins)
    .add_plugins(LogPlugin::default())
    .add_systems(Update, game_logic)
    .run();

3.1 자기 플러그인 만들기

내 게임 코드도 플러그인으로 묶으면 모듈화가 깔끔하다.

use bevy::prelude::*;

pub struct PlayerPlugin;

impl Plugin for PlayerPlugin {
    fn build(&self, app: &mut App) {
        app
            .add_event::<PlayerHurt>()
            .add_systems(Startup, spawn_player)
            .add_systems(Update, (
                player_input,
                player_movement.after(player_input),
                player_animation,
            ));
    }
}

#[derive(Event)]
pub struct PlayerHurt { pub amount: i32 }

#[derive(Component)]
pub struct Player;

fn spawn_player(mut commands: Commands) {
    commands.spawn((
        Player,
        Transform::default(),
        // ... sprite, collider, etc.
    ));
}

fn player_input(/* ... */) {}
fn player_movement(/* ... */) {}
fn player_animation(/* ... */) {}

main은 이렇게 짧아진다.

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins((PlayerPlugin, EnemyPlugin, AudioPlugin, UiPlugin))
        .run();
}

3.2 생태계의 플러그인들 (2026년 5월 기준)

  • Avian — 2D/3D 물리. 옛 bevy_xpbd의 후속. position-based dynamics, 0.15 호환.
  • bevy_egui — egui를 Bevy에 띄우는 디버그 UI 플러그인. 인스펙터·툴 제작 표준.
  • bevy-inspector-egui — 월드의 component·resource를 런타임에 즉시 편집.
  • Lightyear — 클라이언트-서버 네트워킹. replication, rollback, prediction.
  • bevy_rapier — Rapier 물리 엔진 바인딩(Avian 대신 선택).
  • bevy_tweening, bevy_easings — 애니메이션 보간.
  • bevy_kira_audio — 더 풍부한 오디오 제어.
  • Big Brain — utility AI 결정 트리.

bevy.org 또는 bevy_assets 깃허브 레포에 큐레이션된 목록이 있다.

기억할 한 줄: "플러그인은 단순한 모듈이 아니라 Bevy의 사상이다 — 엔진과 게임 코드 사이의 경계가 없다."


4장 · wgpu 렌더링 — 브라우저·데스크톱·모바일을 한 번에

Bevy의 렌더러는 wgpu 위에 얹혀 있다. wgpu는 Rust 진영의 WebGPU 구현으로, 한 코드로 Vulkan·Metal·DirectX 12·WebGPU·OpenGL ES를 모두 타겟한다.

4.1 무엇이 좋은가

  • 크로스 플랫폼이 진짜다. macOS에서 Metal, Linux에서 Vulkan, 윈도우에서 DX12, 모바일에서 Vulkan/Metal, 웹에서 WebGPU. 셰이더는 WGSL로 한 번 쓰면 다 돌아간다.
  • WebGPU 지원이 1급 시민이다. cargo build --target wasm32-unknown-unknownwasm-bindgen으로 묶으면 브라우저에서 실행된다. 0.15 기준 itch.io 같은 곳에 올리기 충분하다.
  • 셰이더가 모던하다. WGSL은 GLSL보다 안전(타입·바운드 체크), HLSL보다 단순.

4.2 무엇이 약한가

  • WebGPU 지원 브라우저가 아직 좁다. 2026년 5월 기준, Chrome·Edge는 안정, Safari는 18부터 지원, Firefox는 일부 platform에서 nightly. 게임을 웹에 올릴 때 폴백을 고민해야 한다.
  • 셰이더 컴파일 시간·드라이버 호환성은 wgpu의 책임이고, Vulkan SDK가 직접 주는 도구만큼 매끄럽지 않다.
  • PBR 파이프라인은 모던하지만 UE5만큼 풍부하지 않다. Lumen·Nanite 같은 첨단 기능은 없다(그게 필요하면 Bevy를 쓸 이유가 거의 없다).

4.3 커스텀 셰이더

WGSL로 셰이더를 짠 다음 Material trait을 구현해 Bevy material로 등록한다.

use bevy::{
    prelude::*,
    render::render_resource::{AsBindGroup, ShaderRef},
    sprite::Material2d,
};

#[derive(Asset, TypePath, AsBindGroup, Clone, Debug)]
struct WaveMaterial {
    #[uniform(0)] time: f32,
    #[uniform(0)] color: LinearRgba,
}

impl Material2d for WaveMaterial {
    fn fragment_shader() -> ShaderRef { "shaders/wave.wgsl".into() }
}

그리고 셰이더 파일.

struct Uniforms {
    time: f32,
    color: vec4<f32>,
};

@group(2) @binding(0) var<uniform> u: Uniforms;

@fragment
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    let wave = sin(uv.x * 10.0 + u.time) * 0.5 + 0.5;
    return vec4<f32>(u.color.rgb * wave, 1.0);
}

기억할 한 줄: "wgpu 덕에 Bevy는 '한 코드로 어디든 돌아가는 렌더러'를 거의 공짜로 가졌다 — 단, '어디든'이 '최적'은 아니다."


5장 · 첫 Bevy 게임 30분 — 키 입력으로 움직이는 원

이제 코드를 쓴다. "원이 화면에 떠 있고, 화살표 키로 움직인다." 30분 안에 끝낸다.

5.1 프로젝트 시작

# Rust가 없다면 rustup으로 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 새 프로젝트
cargo new moving_circle && cd moving_circle

Cargo.toml을 연다.

[package]
name = "moving_circle"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy = "0.15"

# 빠른 컴파일을 위한 dev 프로파일 — 권장
[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 3

5.2 첫 코드 — 빈 창 띄우기

src/main.rs.

use bevy::prelude::*;

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

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);
}
cargo run

검은 빈 창이 뜬다. 처음 빌드는 2~3분 걸린다(의존성 컴파일). 이후 increment 빌드는 수 초.

5.3 원 그리기

use bevy::prelude::*;

#[derive(Component)]
struct Player;

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

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

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn(Camera2d);

    commands.spawn((
        Player,
        Speed(300.0),
        Mesh2d(meshes.add(Circle::new(40.0))),
        MeshMaterial2d(materials.add(Color::srgb(0.2, 0.7, 1.0))),
        Transform::from_xyz(0.0, 0.0, 0.0),
    ));
}

다시 cargo run. 파란 원 하나가 가운데 떠 있다.

코드의 핵심.

  • commands.spawn((..., ..., ...)) — 튜플로 component 묶음을 한 번에 붙인다.
  • Mesh2d + MeshMaterial2d — Bevy 0.15의 새 2D 머티리얼 API. Mesh와 머티리얼이 각각 자기 component.
  • Transform — 위치·회전·스케일.

5.4 입력으로 움직이기

update 스케줄에 system을 추가한다.

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

fn move_player(
    time: Res<Time>,
    keyboard: Res<ButtonInput<KeyCode>>,
    mut q: Query<(&mut Transform, &Speed), With<Player>>,
) {
    let dt = time.delta_secs();
    for (mut tf, speed) in &mut q {
        let mut dir = Vec2::ZERO;
        if keyboard.pressed(KeyCode::ArrowLeft)  { dir.x -= 1.0; }
        if keyboard.pressed(KeyCode::ArrowRight) { dir.x += 1.0; }
        if keyboard.pressed(KeyCode::ArrowUp)    { dir.y += 1.0; }
        if keyboard.pressed(KeyCode::ArrowDown)  { dir.y -= 1.0; }
        if dir != Vec2::ZERO {
            tf.translation += (dir.normalize() * speed.0 * dt).extend(0.0);
        }
    }
}

cargo run. 화살표 키를 누르면 원이 움직인다. 끝.

핵심 패턴.

  • Res<Time> — Bevy가 자동으로 제공하는 시간 리소스.
  • Res<ButtonInput<KeyCode>> — 키보드 상태 리소스.
  • Query<(&mut Transform, &Speed), With<Player>> — Player 마커가 있는 entity의 Transform과 Speed만 가져온다.
  • time.delta_secs() — 프레임 독립적 이동.

5.5 적 한 마리 추가

#[derive(Component)]
struct Enemy;

fn setup(/* ... */) {
    // ... (위와 동일)

    commands.spawn((
        Enemy,
        Speed(150.0),
        Mesh2d(meshes.add(Circle::new(30.0))),
        MeshMaterial2d(materials.add(Color::srgb(1.0, 0.3, 0.3))),
        Transform::from_xyz(200.0, 200.0, 0.0),
    ));
}

fn chase_player(
    time: Res<Time>,
    player_q: Query<&Transform, With<Player>>,
    mut enemy_q: Query<(&mut Transform, &Speed), (With<Enemy>, Without<Player>)>,
) {
    let Ok(player_tf) = player_q.single() else { return };
    let dt = time.delta_secs();
    for (mut tf, speed) in &mut enemy_q {
        let dir = (player_tf.translation - tf.translation).normalize_or_zero();
        tf.translation += dir * speed.0 * dt;
    }
}

// main에 .add_systems(Update, (move_player, chase_player)) 추가

빨간 적이 플레이어를 따라온다. ECS의 진가 — 별도 클래스 정의 없이, 새 component와 system만 추가하면 새 행동이 생긴다.

기억할 한 줄: "30분 안에 '키로 움직이는 원'까지 가는 게 진짜 '엔진의 첫 인상' 테스트다. Bevy는 그 테스트를 통과한다."


6장 · Godot·Unity·Unreal·Macroquad와의 정직한 비교

엔진은 종교가 아니다. 표를 보자.

6.1 한 줄 요약

  • Unity (C#) — 산업 표준. 2D 인디·모바일·VR·간단한 3D의 압도적 1위. 에디터·자산 스토어가 무기.
  • Unreal Engine (C++/Blueprint) — AAA·시네마틱·고품질 3D의 1위. Nanite, Lumen, MetaHuman. 자체적으로 엔터프라이즈 솔루션.
  • Godot (GDScript/C#) — 인디 2D, 가벼운 3D. 오픈소스, MIT, ~50MB 에디터. 4.x 이후 3D가 많이 좋아졌다.
  • Bevy (Rust) — ECS 우선, 오픈소스, MIT/Apache. 에디터 없음(BSN 계획 중). 라이브러리에 가깝다.
  • Macroquad (Rust) — "그냥 2D 게임을 빨리 시작하고 싶다"에 답하는 미니멀 Rust 게임 라이브러리. ECS 없음.

6.2 항목별 비교

언어와 패러다임

  • Unity: C# + OOP/컴포넌트
  • Unreal: C++ + Blueprint(비주얼 스크립팅)
  • Godot: GDScript(Python-스러움) + C#
  • Bevy: Rust + ECS
  • Macroquad: Rust + immediate-mode 절차적

에디터

  • Unity·Unreal·Godot: 풍부한 GUI 에디터 내장.
  • Bevy: 공식 에디터 없음. 0.15 시점 BSN(Bevy Scene Notation) 신 포맷 RFC가 진행 중이고, 에디터는 그 위에 만들어질 예정이다. 그 전엔 bevy-inspector-egui로 런타임 인스펙터에 의존한다.
  • Macroquad: 에디터 없음.

컴파일 속도

  • Unity·Unreal: C++/C# 컴파일 + 핫 리로드 옵션.
  • Godot: GDScript는 즉시 실행, C#은 컴파일.
  • Bevy: Rust 컴파일이 느리다. dev 빌드 최적화(opt-level=1)와 cargo watch -x run, dynamic_linking 피처(0.x.x 한정)를 쓰면 수 초~십수 초.

런타임 성능

  • Unreal: AAA 게임에서 입증된 최고 성능, 다만 빌드가 무겁다.
  • Unity DOTS: ECS를 도입했지만 학습 곡선·생태계 분열.
  • Bevy: ECS + Rust가 메모리 안전·자동 병렬화로 작은 코드로 좋은 멀티스레드 성능.
  • Godot: 충분히 빠르나 AAA 타이틀에는 부족.

모바일/콘솔 빌드

  • Unity·Unreal: 1급, 콘솔 SDK 라이선스 보유.
  • Godot: iOS·Android 가능, 콘솔은 서드파티 포트.
  • Bevy: 데스크톱·웹은 잘 됨. iOS는 가능하나 까다로움. 안드로이드는 nightly 단계 작업이 많다. 콘솔은 사실상 불가(라이선스).

오픈소스 정책

  • Unity·Unreal: 매출 일정 이상 시 라이선스/로열티.
  • Godot·Bevy·Macroquad: MIT/Apache, 완전 자유.

6.3 언제 무엇을 쓰나

  • 모바일 인디 2D·간단한 3D → Unity
  • AAA 그래픽·시네마틱 → Unreal
  • 인디 2D, GDScript 친화 → Godot
  • Rust 좋아함, ECS 패러다임 학습, 도구·시뮬레이션 → Bevy
  • "2D 게임 잼 5시간 안에" → Macroquad

기억할 한 줄: "Bevy는 'Unity 대체재'가 아니다. 다른 도구다 — Rust·ECS·오픈소스·모듈성을 동시에 원할 때만 옳다."


7장 · 실제로 출시된 Bevy 게임들 — 무엇이 사실인가

여기는 정직해야 한다. 인터넷에 도는 "Bevy로 만든 게임" 리스트에는 가끔 거짓이 섞인다.

7.1 진짜 Bevy로 만들어진 상업 게임

  • Foresight — RTS 인디 게임. 개발사 Captured by Aliens가 명시적으로 Bevy 사용을 공개했다.
  • Roll It Up — 인디 캐주얼.
  • Tunnet — 네트워크 시뮬레이션 게임. Bevy 0.x 시절부터 개발됐고 출시.
  • Jarl — Steam 출시 그리드 기반 RTS, Bevy 기반.
  • 다수의 itch.io 게임 잼 작품 — Bevy Jam(연 1~2회)의 결과물들. bevyengine.orgitch.io/jam/bevy-jam 검색하면 수백 개.

7.2 종종 오해되는 케이스 — Tiny Glade

Pounce Light의 Tiny Glade는 2024년 9월 출시 직후 Steam 인기 1위까지 올라간 인디 명작 시뮬레이터다. 이 게임은 Bevy를 사용하지 않는다 — 자체 Rust 엔진을 만들었고, 일부 컴포넌트로 Rust 생태계의 라이브러리(wgpu 등)는 활용했지만 "Bevy 게임"으로 묶으면 잘못이다. 개발사가 여러 인터뷰와 GDC 토크에서 자체 엔진임을 분명히 했다.

"Tiny Glade가 Bevy로 만들어졌다"라는 글은 무시하라. Bevy 커뮤니티는 Tiny Glade의 성공으로 "Rust 게임 가능성"을 보여줬다는 점을 기뻐했지만, 엔진 자체는 다르다.

7.3 비게임 사용 사례

Bevy는 ECS + wgpu 조합 덕에 게임이 아닌 곳에서도 쓰인다.

  • Foresight Mining Software — 광산 시뮬레이션 도구.
  • Komodo — 일부 인터랙티브 시각화에 Bevy.
  • 다수의 데이터 시각화·로봇 시뮬레이션·교육용 데모.

엔진을 라이브러리처럼 임포트할 수 있다는 게 Bevy의 큰 특징이다 — Unity·Unreal로는 불가능에 가깝다.

기억할 한 줄: "Bevy는 'Tiny Glade 같은 게임을 만든다'고 거짓말하지 않는다. 'Foresight·Jarl·Tunnet 같은 게임을 만들었고, 데이터 시각화·시뮬레이션에서도 쓴다'고 정직하게 말한다."


8장 · Bevy가 아직 준비 안 된 곳 — 정직한 한계

"Year of Bevy"는 2024년에도, 2025년에도, 2026년에도 매년 외쳐졌다. 그 사이 진짜로 성숙한 부분과 여전히 거친 부분을 가른다.

8.1 성숙해진 것

  • 2D 렌더링·기본 UI — 0.15부터 Mesh2d/MeshMaterial2d로 일관성 정리.
  • 물리 — Avian이 안정. 2D/3D, 컨스트레인트, CCD까지.
  • 오디오bevy_audio(기본) + bevy_kira_audio(고급).
  • WebGPU 빌드 — 데모용으로 충분히 매끄럽다.
  • 플러그인 생태계bevy_assets 레포에 수백 개. 자주 쓰이는 건 메인테이너가 매 릴리스 따라간다.

8.2 여전히 거친 것

  • 공식 에디터 없음 — 0.15 시점 BSN(Bevy Scene Notation) RFC 진행 중. 이건 게임 오브젝트 계층·자산·머티리얼을 자료로 표현하는 신 포맷 제안인데, 받아들여지면 그 위에 에디터가 생긴다. 채택 일정은 2026~2027년 사이로 본다.
  • 핫 리로드 — 자산은 워치 모드로 어느 정도, 코드는 dynamic_linking 피처로 부분적. JIT 수준 핫 리로드는 아직 시기상조.
  • 버전 간 호환성 깨짐 — 0.10·0.11·0.12·0.13·0.14·0.15 사이 마이그레이션이 매번 적지 않다. 0.15 마이그레이션 가이드 한 번 읽어야 한다. 1.0이 나오기 전까지는 이 상황이 계속된다.
  • 콘솔 빌드 — Switch·PS5·Xbox는 사실상 불가. 라이선스 + 비공개 SDK 문제.
  • 모바일 빌드 — iOS는 동작하지만 도구가 거칠고, 안드로이드는 nightly·실험적. Unity·Unreal과는 비교 안 된다.
  • GUI 디자이너 부재 — bevy_ui가 코드 기반이라 디자이너가 마우스로 UI 짜는 워크플로가 없다(BSN으로 해결될 예정).
  • 러닝 커브 — Rust + ECS + Bevy API. 학교 후에 첫 게임 짜는 시간은 다른 엔진보다 길다.

8.3 0.15 → 1.0의 길

Bevy 공식 로드맵(2026년 5월 갱신 기준)이 1.0까지 미루는 큰 항목.

  1. BSN 정착과 공식 에디터의 최소 버전.
  2. 안정적 ABI(플러그인을 동적 로딩).
  3. 핫 리로드 시나리오 확립.
  4. 멀티 윈도우·고DPI·드래그 앤 드롭 같은 데스크톱 polish.
  5. 비주얼 스크립팅(필요하다면 외부 플러그인으로).

"1.0이 언제냐"고 묻지 마라. 답은 항상 "준비되면". 다만 0.15는 작은 인디 게임을 시작하기에 충분히 안정적이다.

기억할 한 줄: "Bevy 1.0을 기다리지 마라. 0.15로 만들 수 있는 게임을 지금 만들어라. 1.0 마이그레이션은 그때 한다."


에필로그 — 시작하는 사람을 위한 안내

30분 핸즈온 체크리스트

  • rustup으로 Rust 설치(1.81+)
  • cargo new + Cargo.tomlbevy = "0.15"
  • Camera2d 스폰 — 빈 창 확인
  • Mesh2d(Circle::new(40.0)) + MeshMaterial2d로 원 그리기
  • Update 스케줄에 move_player system, Res<ButtonInput<KeyCode>>로 키 입력
  • Enemy 마커 component + chase_player system 추가
  • cargo run --release로 최적화 빌드 한 번
  • cargo build --target wasm32-unknown-unknownwasm-bindgen으로 웹 빌드 시도
  • bevy-inspector-egui 플러그인 붙여 런타임 인스펙터 띄우기
  • Avian 추가해 두 원에 collider 붙이고 충돌 들어보기

Bevy 안티 패턴 7가지

  1. System 안에서 Vec<Entity>를 들고 다니며 매 프레임 lookup — query를 잘 짜면 안 해도 된다.
  2. 모든 걸 component로 — 상태 머신·간단한 enum은 component 안에 두는 게 낫다.
  3. Commands를 모든 곳에서 mut — 가능하지만 결과 반영은 다음 프레임이다. 즉시 반영이 필요하면 World를 직접 받는 exclusive system을 쓴다.
  4. Query::single()을 panic으로 풀기let Ok(...) = q.single() else { return }; 같은 형태가 안전하다.
  5. 이벤트 폭주EventReader를 매 프레임 들고 있지 않으면 이벤트가 사라진다. MyEvent를 두 system이 동시에 읽으려면 add_event + 일관된 system 순서.
  6. 렌더 system을 Update에 두기 — 렌더 system은 보통 Bevy 내부가 알아서 한다. 사용자 코드는 데이터만 바꾸면 된다.
  7. 0.x 마이그레이션 무시 — Bevy 0.x → 0.(x+1)는 매번 4~12시간 작업이 든다. 1.0 전에 큰 프로젝트면 마이그레이션 buffer를 일정에 넣어라.

다음 글 예고

  • Avian으로 2D 플랫포머 만들기 — 점프·코요테 타임·중력 조정·연속 충돌 감지.
  • Lightyear로 멀티플레이어 슈터 — replication·prediction·rollback의 클라이언트-서버 모델.
  • Bevy + wasm + itch.io 출시 — 웹 빌드, 키 캡처, 모바일 터치 매핑까지.
  • BSN 신 포맷 RFC 정독 — Bevy 1.0이 노리는 모양과 그게 게임 코드를 어떻게 바꾸는가.

기억할 한 줄: "Rust + ECS는 게임 엔진 패러다임의 답 중 가장 정직한 답이다. Bevy는 그 답을 직접 손으로 짜볼 수 있는 가장 빠른 길이다."


참고 / References

Bevy Game Engine — A Hands-On Rust ECS Tour of Modern Game Development (Deep Dive, 2026)

Prologue — Why Rust and ECS Fit Game Development

The history of game engines compresses into two lines.

  1. The inheritance era (1995–2015) — Unity's MonoBehaviour, Unreal's UObject, Godot's Node. Every game object descended from a class hierarchy, and "GameObject is a Component" built deep trees. C++ and C# fit naturally.
  2. The data-oriented era (2015–) — Naughty Dog, Insomniac, and Epic's Mass system in Unreal 5 conceded that "a cache miss is a frame drop." Data-Oriented Design rose, and Entity-Component-System emerged as the textbook answer.

ECS in one paragraph.

  • Entity = just an ID, a 64-bit integer. No data, no behavior.
  • Component = pure data (Position, Velocity, Sprite). No methods.
  • System = a function that runs over "every entity that has this set of components."

No inheritance tree. Player extends Character extends Pawn vanishes. A "player" is an entity that happens to carry Player, Position, Velocity, and Health components.

Layer Rust on top of ECS and two old weaknesses dissolve at once. First, concurrency. The borrow checker proves at compile time that two systems cannot mutate the same component simultaneously, and Bevy's scheduler exploits that to parallelize automatically. Second, stability. Game code historically bruises against pointer, null, and out-of-bounds bugs — Rust removes the whole category.

Now the honest part. In May 2026, Unity still leads on mobile, indie, and VR. Unreal leads AAA and cinematics. Godot owns a strong slice of indie 2D. Bevy is the open-source star but counts shipped commercial titles on two hands. So this piece will not chant "Year of Bevy." It will answer instead — what does Bevy do well, how far has it come, where is it still stuck?

The plan.

  1. Understanding the ECS paradigm — how it differs from MonoBehaviour
  2. Bevy's anatomy — App, Schedule, Plugin
  3. The plugin architecture — everything is a plugin
  4. Rendering through wgpu — browser, desktop, and mobile from one codebase
  5. Your first Bevy game in thirty minutes — a circle that moves with arrow keys
  6. Honest comparison against Godot, Unity, Unreal, Macroquad
  7. Games actually shipped on Bevy — Foresight, Tunnet, and the truth about Tiny Glade
  8. Where Bevy still is not ready

The keeper sentence: "An engine is a tool, not a faith. But ECS is a paradigm before it is a tool, and Bevy is the most honest implementation of that paradigm — on top of Rust."


1. The ECS Paradigm — How It Differs From Unity and Unreal

1.1 The inheritance model in practice

An enemy in Unity usually looks like this.

// Unity / MonoBehaviour style
public class Enemy : MonoBehaviour {
    public int health = 100;
    public float speed = 2.0f;
    private Transform target;

    void Start() { target = GameObject.FindWithTag("Player").transform; }
    void Update() {
        transform.position = Vector3.MoveTowards(
            transform.position, target.position, speed * Time.deltaTime);
        if (health <= 0) Destroy(gameObject);
    }
}

Enemy inherits from MonoBehaviour, holds state (health, speed), and the behavior (Update) all in one class. A hundred enemies mean Update is dispatched a hundred times — virtual calls, cache misses, and branch mispredictions accumulate.

Unreal's AActor + UCharacterMovementComponent combination is structurally identical: a deep inheritance chain like AEnemy : public ACharacter : public APawn : public AActor, and each component holds a pointer back to its actor.

1.2 The ECS model in practice

The same enemy in Bevy.

// Bevy / ECS style
use bevy::prelude::*;

#[derive(Component)]
struct Enemy;

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

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

#[derive(Component)]
struct Target(Entity);

fn move_enemies(
    time: Res<Time>,
    mut enemies: Query<(&mut Transform, &Speed, &Target), With<Enemy>>,
    targets: Query<&Transform, Without<Enemy>>,
) {
    for (mut tf, speed, target) in &mut enemies {
        if let Ok(target_tf) = targets.get(target.0) {
            let dir = (target_tf.translation - tf.translation).normalize_or_zero();
            tf.translation += dir * speed.0 * time.delta_secs();
        }
    }
}

fn kill_dead_enemies(mut commands: Commands, q: Query<(Entity, &Health), With<Enemy>>) {
    for (e, hp) in &q {
        if hp.0 <= 0 { commands.entity(e).despawn(); }
    }
}

The differences are sharp.

  • Enemy is a marker component — an empty struct. No data, no behavior.
  • Health(i32) and Speed(f32) are pure data. No methods.
  • move_enemies is a system that processes only entities carrying the Enemy marker, in bulk.
  • The Position, Speed, and Target of a hundred enemies are laid out contiguously in memory under archetype-based storage, so cache hit rates are high.
  • move_enemies and kill_dead_enemies touch different components, so Bevy runs them in parallel automatically.

1.3 Archetypes and memory layout

Bevy (and Flecs, Unity DOTS, Unreal Mass) is an archetype-based ECS. It groups entities into chunks by "which components they have."

Archetype A: [Position, Velocity, Sprite] — a thousand entities laid out with Position contiguous, then Velocity contiguous, then Sprite contiguous. Archetype B: [Position, Velocity, Sprite, Health, Enemy] — a separate chunk.

A Query of Position plus Velocity walks chunks from both A and B, and inside each chunk it is just an array walk. No virtual dispatch, almost no cache miss. That is why ECS is fast.

1.4 The honest downsides of ECS

ECS is not a silver bullet.

  • State machines and complex behavior trees explode when you model each state as a component. Idle, Walking, Attacking, Hurt, Dead as separate components forces every system to know every combination. Putting an enum inside a single component is usually saner.
  • Parent-child relationships are not native to vanilla ECS. Bevy fakes a tree with Parent and Children components, but the grain is different.
  • The learning curve is steep. "Why can't I just call a method?" and "How does this query split the borrow?" eat hours.

The keeper sentence: "OOP cages verbs inside nouns. ECS keeps nouns (data) and verbs (systems) separated all the way down."


2. Bevy's Anatomy — App, Schedule, System

2.1 App

Every Bevy program starts at an App. The main function usually looks like this.

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, spawn_camera)
        .add_systems(Update, (move_player, count_fps).chain())
        .run();
}
  • App::new() creates an empty ECS world and scheduler.
  • .add_plugins(DefaultPlugins) pulls in the standard bundle — windowing, input, rendering, time, asset loading.
  • .add_systems(Startup, ...) registers systems that run once at game start.
  • .add_systems(Update, ...) registers systems that run every frame.
  • .run() enters the main loop.

The crucial absence is the absence of a manager. There is no class GameManager : MonoBehaviour { void Awake() }. The App is the manager, and the game logic is free functions.

2.2 Schedule — When systems run

Bevy 0.10 retired stages and introduced schedules. As of 0.15 the main ones are.

  • Startup — once at app start.
  • PreUpdate — every frame, after input is gathered.
  • Update — every frame, main game logic.
  • PostUpdate — every frame, after transform propagation and physics.
  • FixedUpdate — fixed timestep (default 64 Hz). Physics and lockstep networking.
  • Last — end of frame.

Ordering between systems is controlled with .before(), .after(), .chain(), and named SystemSet groupings.

app.add_systems(
    Update,
    (
        input_system,
        movement_system.after(input_system),
        collision_system.after(movement_system),
    ),
);

2.3 Component, Resource, Event

Three ways to put data into the ECS world.

  • Component — data attached to an entity. #[derive(Component)].
  • Resource — a single global per world. Score, settings, asset handles. #[derive(Resource)].
  • Event — frame-scoped messages between systems. #[derive(Event)].
#[derive(Resource)]
struct Score(u32);

#[derive(Event)]
struct EnemyKilled { entity: Entity, by: Entity }

fn award_score(mut score: ResMut<Score>, mut events: EventReader<EnemyKilled>) {
    for _ in events.read() {
        score.0 += 100;
    }
}

Res is read-only access, ResMut is mutable. Two systems that both want ResMut of the same resource serialize automatically; a Res reader runs in parallel with another Res reader.

2.4 Query — How a system asks for data

fn move_player(
    time: Res<Time>,
    mut q: Query<(&mut Transform, &Speed), With<Player>>,
) {
    for (mut tf, speed) in &mut q {
        tf.translation.x += speed.0 * time.delta_secs();
    }
}

The Query signature is the system's data manifest. With<Player> is a filter — it does not pull the marker into the iteration, but it requires the entity to carry it. If two systems hold non-overlapping queries, Bevy can run them simultaneously.

The keeper sentence: "Reading Bevy code = the system signature already tells you what that system reads and what it writes."


3. Plugin Architecture — Everything Is a Plugin

Bevy's biggest aesthetic decision is that "the engine itself is a collection of plugins."

Unpack DefaultPlugins and roughly twenty plugins fall out.

  • WindowPlugin — window creation
  • InputPlugin — keyboard, mouse, gamepad
  • RenderPlugin — wgpu-backed rendering
  • SpritePlugin — 2D sprites
  • PbrPlugin — physically based 3D rendering
  • UiPlugin — bevy_ui
  • AudioPlugin — kira-backed audio
  • AssetPlugin — asynchronous asset loading
  • TimePlugin, TransformPlugin, LogPlugin, and so on.

Anything you do not want, you can drop.

// Headless server — ECS only, no window or renderer
App::new()
    .add_plugins(MinimalPlugins)
    .add_plugins(LogPlugin::default())
    .add_systems(Update, game_logic)
    .run();

3.1 Writing your own plugin

Bundling your own code into a plugin keeps modules clean.

use bevy::prelude::*;

pub struct PlayerPlugin;

impl Plugin for PlayerPlugin {
    fn build(&self, app: &mut App) {
        app
            .add_event::<PlayerHurt>()
            .add_systems(Startup, spawn_player)
            .add_systems(Update, (
                player_input,
                player_movement.after(player_input),
                player_animation,
            ));
    }
}

#[derive(Event)]
pub struct PlayerHurt { pub amount: i32 }

#[derive(Component)]
pub struct Player;

fn spawn_player(mut commands: Commands) {
    commands.spawn((
        Player,
        Transform::default(),
        // sprite, collider, etc.
    ));
}

fn player_input(/* ... */) {}
fn player_movement(/* ... */) {}
fn player_animation(/* ... */) {}

Then main becomes tiny.

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins((PlayerPlugin, EnemyPlugin, AudioPlugin, UiPlugin))
        .run();
}

3.2 The ecosystem (as of May 2026)

  • Avian — 2D and 3D physics. The successor to bevy_xpbd. Position-based dynamics, compatible with 0.15.
  • bevy_egui — Drops egui into Bevy for debug UI. The de facto standard for inspectors and tooling.
  • bevy-inspector-egui — Live-edit components and resources in the running world.
  • Lightyear — Client-server networking with replication, prediction, and rollback.
  • bevy_rapier — Rapier physics bindings (alternative to Avian).
  • bevy_tweening and bevy_easings — Animation interpolation.
  • bevy_kira_audio — Richer audio control.
  • Big Brain — Utility AI decision trees.

The bevy_assets repository on GitHub curates the canonical list.

The keeper sentence: "A plugin is not just a module — it is Bevy's philosophy. There is no border between engine and game code."


4. wgpu Rendering — One Codebase for Browser, Desktop, and Mobile

Bevy's renderer sits on wgpu, the Rust-side implementation of WebGPU. One codebase targets Vulkan, Metal, DirectX 12, WebGPU, and OpenGL ES.

4.1 What it gets right

  • Cross-platform is real. Metal on macOS, Vulkan on Linux, DX12 on Windows, Vulkan/Metal on mobile, WebGPU on the web. Shaders go through WGSL once and run everywhere.
  • WebGPU is a first-class target. cargo build --target wasm32-unknown-unknown plus wasm-bindgen produces a browser build. As of 0.15 it is good enough to ship on itch.io.
  • Modern shaders. WGSL is safer than GLSL (typed, bounds-checked) and simpler than HLSL.

4.2 What it does not get right

  • Browser support is still narrow. As of May 2026, WebGPU is stable in Chrome and Edge, supported in Safari 18, and only on nightly Firefox on some platforms. You still need a fallback plan for web shipping.
  • Shader compile times and driver compatibility sit with wgpu, and the tooling is not yet as smooth as Vulkan SDK natively gives you.
  • The PBR pipeline is modern but not UE5-rich. No Lumen, no Nanite. If you need those, Bevy is not the engine.

4.3 Custom shaders

Write WGSL, then implement the Material trait to register it as a Bevy material.

use bevy::{
    prelude::*,
    render::render_resource::{AsBindGroup, ShaderRef},
    sprite::Material2d,
};

#[derive(Asset, TypePath, AsBindGroup, Clone, Debug)]
struct WaveMaterial {
    #[uniform(0)] time: f32,
    #[uniform(0)] color: LinearRgba,
}

impl Material2d for WaveMaterial {
    fn fragment_shader() -> ShaderRef { "shaders/wave.wgsl".into() }
}

And the shader file.

struct Uniforms {
    time: f32,
    color: vec4<f32>,
};

@group(2) @binding(0) var<uniform> u: Uniforms;

@fragment
fn fragment(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
    let wave = sin(uv.x * 10.0 + u.time) * 0.5 + 0.5;
    return vec4<f32>(u.color.rgb * wave, 1.0);
}

The keeper sentence: "wgpu gave Bevy a 'one codebase, every platform' renderer almost for free — though 'every' does not yet mean 'optimal'."


5. Your First Bevy Game in Thirty Minutes — A Circle That Moves

Time to actually write code. "A circle on screen that moves with arrow keys." Done in thirty minutes.

5.1 Project setup

# If you do not have Rust, install via rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# New project
cargo new moving_circle && cd moving_circle

Open Cargo.toml.

[package]
name = "moving_circle"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy = "0.15"

# Faster dev compile — recommended
[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 3

5.2 First code — empty window

src/main.rs.

use bevy::prelude::*;

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

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);
}
cargo run

A black window appears. The first build takes two to three minutes (dependency compile). Incremental builds afterward take seconds.

5.3 Draw a circle

use bevy::prelude::*;

#[derive(Component)]
struct Player;

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

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

fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn(Camera2d);

    commands.spawn((
        Player,
        Speed(300.0),
        Mesh2d(meshes.add(Circle::new(40.0))),
        MeshMaterial2d(materials.add(Color::srgb(0.2, 0.7, 1.0))),
        Transform::from_xyz(0.0, 0.0, 0.0),
    ));
}

cargo run again. A blue circle sits in the middle of the window.

The shape of the code.

  • commands.spawn((..., ..., ...)) attaches a tuple of components at once.
  • Mesh2d + MeshMaterial2d are Bevy 0.15's refreshed 2D material API. Mesh and material each become their own component.
  • Transform carries position, rotation, and scale.

5.4 Move with input

Add a system to the Update schedule.

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

fn move_player(
    time: Res<Time>,
    keyboard: Res<ButtonInput<KeyCode>>,
    mut q: Query<(&mut Transform, &Speed), With<Player>>,
) {
    let dt = time.delta_secs();
    for (mut tf, speed) in &mut q {
        let mut dir = Vec2::ZERO;
        if keyboard.pressed(KeyCode::ArrowLeft)  { dir.x -= 1.0; }
        if keyboard.pressed(KeyCode::ArrowRight) { dir.x += 1.0; }
        if keyboard.pressed(KeyCode::ArrowUp)    { dir.y += 1.0; }
        if keyboard.pressed(KeyCode::ArrowDown)  { dir.y -= 1.0; }
        if dir != Vec2::ZERO {
            tf.translation += (dir.normalize() * speed.0 * dt).extend(0.0);
        }
    }
}

cargo run. Press arrow keys — the circle moves. Done.

The key patterns.

  • Res<Time> is the time resource Bevy provides automatically.
  • Res<ButtonInput<KeyCode>> is the keyboard state resource.
  • A Query of Transform and Speed, filtered with With<Player>, pulls only Player entities.
  • time.delta_secs() keeps movement frame-rate independent.

5.5 Add an enemy

#[derive(Component)]
struct Enemy;

fn setup(/* ... */) {
    // same as before

    commands.spawn((
        Enemy,
        Speed(150.0),
        Mesh2d(meshes.add(Circle::new(30.0))),
        MeshMaterial2d(materials.add(Color::srgb(1.0, 0.3, 0.3))),
        Transform::from_xyz(200.0, 200.0, 0.0),
    ));
}

fn chase_player(
    time: Res<Time>,
    player_q: Query<&Transform, With<Player>>,
    mut enemy_q: Query<(&mut Transform, &Speed), (With<Enemy>, Without<Player>)>,
) {
    let Ok(player_tf) = player_q.single() else { return };
    let dt = time.delta_secs();
    for (mut tf, speed) in &mut enemy_q {
        let dir = (player_tf.translation - tf.translation).normalize_or_zero();
        tf.translation += dir * speed.0 * dt;
    }
}

// in main: .add_systems(Update, (move_player, chase_player))

The red enemy chases the player. ECS shines here — no new class hierarchy, only a new component and a new system, and a new behavior appears.

The keeper sentence: "Reaching 'a circle that moves with arrow keys' in thirty minutes is the real first-impression test for an engine. Bevy passes."


6. Honest Comparison Against Godot, Unity, Unreal, Macroquad

An engine is not a religion. To the table.

6.1 One-line summary

  • Unity (C#) — Industry standard. Dominant for 2D indie, mobile, VR, and lightweight 3D. Editor and Asset Store are the moat.
  • Unreal Engine (C++/Blueprint) — King of AAA, cinematics, and high-end 3D. Nanite, Lumen, MetaHuman, plus enterprise-grade solutions.
  • Godot (GDScript/C#) — Indie 2D and lightweight 3D. Open source, MIT, around 50 MB editor. 3D improved a lot in the 4.x line.
  • Bevy (Rust) — ECS-first, open source, MIT/Apache. No editor (BSN proposal in progress). Closer to a library than a traditional engine.
  • Macroquad (Rust) — A minimal Rust game library that answers "I just want to start a 2D game quickly." No ECS.

6.2 Side-by-side

Language and paradigm

  • Unity: C# with OOP and component composition.
  • Unreal: C++ plus Blueprint visual scripting.
  • Godot: GDScript (Python-ish) with optional C#.
  • Bevy: Rust with ECS.
  • Macroquad: Rust, immediate-mode procedural.

Editor

  • Unity, Unreal, Godot: rich built-in GUI editors.
  • Bevy: no official editor. As of 0.15, the BSN (Bevy Scene Notation) RFC is open, and an editor will follow on top of it. Until then, runtime inspection through bevy-inspector-egui is the substitute.
  • Macroquad: no editor.

Compile speed

  • Unity, Unreal: C++/C# builds with hot-reload options.
  • Godot: GDScript runs instantly; C# compiles.
  • Bevy: Rust compiles slowly. Dev-build tweaks (opt-level=1), cargo watch -x run, and the dynamic_linking feature (on 0.x.x) bring iteration to seconds or low tens of seconds.

Runtime performance

  • Unreal: proven AAA performance, but heavy builds.
  • Unity DOTS: introduces ECS, but at the cost of a steeper curve and a split ecosystem.
  • Bevy: ECS plus Rust delivers solid multithreaded performance with safety, on relatively little code.
  • Godot: fast enough, not AAA territory.

Mobile/console builds

  • Unity, Unreal: first-class, console SDK licenses in place.
  • Godot: iOS and Android supported, consoles via third-party ports.
  • Bevy: desktop and web are smooth. iOS is possible but rough. Android is mostly nightly work. Consoles are practically impossible (licensing).

Open-source posture

  • Unity, Unreal: licensing or royalties above revenue thresholds.
  • Godot, Bevy, Macroquad: MIT/Apache, fully free.

6.3 When to choose what

  • Mobile indie 2D, simple 3D — Unity.
  • AAA graphics, cinematics — Unreal.
  • Indie 2D, GDScript-friendly team — Godot.
  • Rust lover, want ECS, building tools or simulations — Bevy.
  • "2D game jam in five hours" — Macroquad.

The keeper sentence: "Bevy is not a 'Unity replacement.' It is a different tool — correct only when you want Rust, ECS, open source, and modularity at the same time."


7. Games Actually Shipped on Bevy — What Is True

This is where honesty matters. Lists of "games made with Bevy" circulating on the internet sometimes lie.

7.1 Commercial games genuinely built on Bevy

  • Foresight — an indie RTS from Captured by Aliens. The studio publicly states they use Bevy.
  • Roll It Up — an indie casual title.
  • Tunnet — a network-simulation game developed against Bevy from early 0.x and shipped.
  • Jarl — a grid-based RTS released on Steam, Bevy-based.
  • Many itch.io jam entries — outputs of Bevy Jam (held one to two times a year). Searching bevyengine.org or itch.io/jam/bevy-jam surfaces hundreds.

7.2 The common misunderstanding — Tiny Glade

Pounce Light's Tiny Glade topped Steam Popular New Releases shortly after its September 2024 launch and became a celebrated indie simulator. It does not use Bevy — the studio built its own Rust engine. Some Rust ecosystem libraries (wgpu and others) are reused, but it is wrong to file Tiny Glade under "Bevy games." The developers have stated this in multiple interviews and GDC talks.

Ignore posts that claim "Tiny Glade was made with Bevy." The Bevy community welcomed Tiny Glade's success as evidence that "Rust games can ship," but the engine is distinct.

7.3 Non-game uses

The ECS plus wgpu combination is also used outside games.

  • Foresight Mining Software — mining simulation tooling.
  • Komodo — interactive visualization built partly on Bevy.
  • Many data-visualization, robotics-simulation, and educational demos.

The fact that the engine can be imported like a library is a real differentiator — Unity and Unreal cannot do this in practice.

The keeper sentence: "Bevy does not lie about making 'a Tiny Glade.' It tells the truth — it shipped Foresight, Jarl, and Tunnet, and it is used in data visualization and simulation as well."


8. Where Bevy Is Not Ready — Honest Limits

"Year of Bevy" has been declared in 2024, in 2025, and again in 2026. Let us split what is actually mature from what remains rough.

8.1 What matured

  • 2D rendering and basic UI — 0.15 consolidated the API around Mesh2d and MeshMaterial2d.
  • Physics — Avian is stable, covers 2D and 3D, constraints, and CCD.
  • Audiobevy_audio (basic) plus bevy_kira_audio (richer).
  • WebGPU builds — smooth enough for demos.
  • Plugin ecosystem — hundreds of entries in bevy_assets. The popular ones track each release.

8.2 What is still rough

  • No official editor. As of 0.15 the BSN (Bevy Scene Notation) RFC is in flight. It proposes a data format for scene hierarchies, assets, and materials, and an editor would follow. Adoption likely lands in 2026 to 2027.
  • Hot reload. Asset watching works; code hot reload via dynamic_linking is partial. JIT-level hot reload is still future.
  • Breaking changes across versions. Migration between 0.10 to 0.11 to 0.12 to 0.13 to 0.14 to 0.15 has been non-trivial every time. Reading the 0.15 migration guide is mandatory. This will continue until 1.0.
  • Console builds. Switch, PS5, and Xbox are practically unreachable due to licensing and closed SDKs.
  • Mobile builds. iOS works but tooling is rough; Android is nightly and experimental. Not in the same league as Unity or Unreal.
  • No GUI designer. bevy_ui is code-only; a designer-driven workflow is the BSN goal.
  • Steep learning curve. Rust plus ECS plus the Bevy API. Time-to-first-game is longer than other engines.

8.3 The road from 0.15 to 1.0

The official Bevy roadmap (as of May 2026) holds these items before 1.0.

  1. BSN landing and a minimum viable official editor.
  2. A stable ABI for dynamic plugin loading.
  3. A grounded story for hot reload.
  4. Desktop polish — multiple windows, high-DPI, drag-and-drop.
  5. Visual scripting (likely as a plugin, if at all).

Do not ask when 1.0 ships. The answer is always "when it is ready." But 0.15 is stable enough to start a small indie game today.

The keeper sentence: "Do not wait for Bevy 1.0. Build the game 0.15 lets you build now. Migrate at 1.0."


Epilogue — Guidance for People Starting Out

Thirty-minute hands-on checklist

  • Install Rust 1.81+ via rustup
  • cargo new and add bevy = "0.15" to Cargo.toml
  • Spawn Camera2d — confirm the empty window
  • Draw a circle with Mesh2d(Circle::new(40.0)) and MeshMaterial2d
  • Add a move_player system on Update reading Res<ButtonInput<KeyCode>>
  • Add an Enemy marker component plus a chase_player system
  • Run cargo run --release for an optimized build at least once
  • Try cargo build --target wasm32-unknown-unknown and wasm-bindgen for a web build
  • Attach bevy-inspector-egui to surface a runtime inspector
  • Add Avian, give the two circles colliders, and feel the collision

Seven Bevy anti-patterns

  1. Carrying a Vec<Entity> through systems and doing lookups every frame — a well-shaped query removes the need.
  2. Modeling everything as a component — state machines and small enums often belong inside a single component.
  3. Using Commands everywhere for mutation — fine, but the effect lands next frame. Use an exclusive system with &mut World when you need immediate effect.
  4. Unwrapping Query::single() with a panic — prefer let Ok(...) = q.single() else { return };.
  5. Event overflow — if no EventReader runs in a frame, the events vanish. Two systems reading the same event need consistent ordering plus an add_event registration.
  6. Putting render systems on Update — render systems are mostly internal. Your code should only write data.
  7. Ignoring 0.x migrations — each minor bump costs four to twelve hours. Bake a migration buffer into the schedule until 1.0 lands.

What is next

  • Build a 2D platformer with Avian — jumping, coyote time, gravity tuning, continuous collision detection.
  • Multiplayer shooter on Lightyear — replication, prediction, and rollback in the client-server model.
  • Bevy plus wasm plus itch.io shipping — web builds, key capture, mobile touch mapping.
  • Read the BSN RFC end to end — the shape Bevy 1.0 is aiming for and how it reshapes game code.

The keeper sentence: "Rust plus ECS is the most honest answer to the game-engine-paradigm question. Bevy is the quickest path to writing that answer with your own hands."


References