Yyatmita

【第14回】Bevy 入門:AI が書けない「今の書き方」で最小ゲームを動かす

AI に Bevy を頼むと SpriteBundle など古い API を吐く——Bevy 0.19 で実際に動く最小 ECS 構成と、AI が間違える「今の書き方」早見表。Rust ゲームエンジン入門。

Claude Codeでサイトをつくってみた#bevy#rust#ecs#game-dev#claude-code#llm
← 前の記事: 【第13回】AI にゲームは作れる。でも「手触り」は作れない — LLM に欠けた game feel を Bevy 製大富豪で埋めた記録

この記事は何か

Rust 製ゲームエンジン Bevy の入門です。ただし普通の入門とは狙いが違います。

Bevy に「四角を動かして」と AI に頼むと、動かないコードが返ってくることがよくあります。 Camera2dBundleSpriteBundleadd_startup_system——どれも昔の Bevy の書き方で、今の バージョンでは消えています。Bevy は 0.x のうちに破壊的変更を高頻度で重ねていて、LLM の学習データは古い Bevy に偏っているからです。

この記事は、今(0.19)動く最小構成を、実際に動いているゲームのコードから示します。 基礎(ECS)を掴みつつ、「AI が間違える所」を先に知っておくのが狙いです。

  • 対象: これから Bevy を始める人/AI に Bevy を書かせて詰まった人
  • シリーズ: 動かせたら次は 配信編(ブラウザ公開)と 手触り編game feel の実装)へ
  • 完成形デモ: /games/grimoire-royale/

1. Bevy = ECS(考え方だけ先に)

Bevy は ECS(Entity Component System) です。オブジェクト指向の「オブジェクトにメソッドが 生えている」とは発想が逆で、データと処理を分けます

  • Entity:ただの ID(「何か」の実体)
  • Component:Entity に貼るデータ(位置、色、「これはプレイヤー」という印)
  • System:毎フレーム走る関数。条件に合う Entity を拾って処理する
  • Resource:ゲーム全体で1つのグローバル状態(スコア、設定など)

「プレイヤーというオブジェクト」ではなく「Player という印が付いた Entity を、move システムが 毎フレーム動かす」と考えます。

2. 最小構成:四角を動かす(0.19 で動く)

Cargo.toml:

[dependencies]
bevy = "0.19"
# ブラウザ配信するときの wasm 設定は「配信編」で。まずはネイティブで動かす

main.rs(これで、黄色い四角が右へ流れていきます):

use bevy::prelude::*;
 
#[derive(Component)]
struct Player;
 
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)          // ウィンドウ・描画・入力などの標準一式
        .add_systems(Startup, setup)          // 起動時に1回
        .add_systems(Update, move_player)     // 毎フレーム
        .run();
}
 
fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);                 // 2D カメラ(※ Camera2dBundle ではない)
    commands.spawn((
        Sprite::from_color(Color::srgb(0.9, 0.7, 0.3), Vec2::new(80.0, 80.0)),
        Transform::from_xyz(0.0, 0.0, 0.0),
        Player,                                // 「これはプレイヤー」という印
    ));
}
 
fn move_player(time: Res<Time>, mut query: Query<&mut Transform, With<Player>>) {
    for mut transform in &mut query {
        // delta_secs() ぶんだけ動かす = フレームレート非依存
        transform.translation.x += 150.0 * time.delta_secs();
    }
}

読み解き:

  • spawn((A, B, C)) で「A・B・C というデータを持つ Entity」を1つ作る。Bundle は要らない (後述)。四角は「Sprite(見た目)+ Transform(位置)+ Player(印)」の3データの集合
  • Query<&mut Transform, With<Player>> = 「Player の印が付いた Entity の Transform を全部よこせ」。 System はこう宣言するだけで、Bevy が該当 Entity を渡してくる
  • Res<Time> = グローバルな時間リソース。delta_secs() で「前フレームからの経過秒」

3. AI が吐きがちな「古い書き方」早見表

ここが本題。同じことを AI に頼むと、次の左側(古い)が返りがちです。0.19 では右側です。

AI が吐きがち(古い)0.19 の今何が起きたか
Camera2dBundle::default()Camera2dBundle 廃止(0.15)→ required components
SpriteBundle { texture, transform, ..default() }(Sprite::from_image(h), Transform::…) のタプル同上
commands.spawn_bundle((…))commands.spawn((…))spawn_bundle は削除済み
.add_startup_system(setup).add_systems(Startup, setup)スケジュール API 刷新(0.10〜11)
.add_system(move_player).add_systems(Update, move_player)同上
time.delta_seconds()time.delta_secs()メソッド改名(0.14)

要点は Bundle 廃止です。以前は SpriteBundleCamera2dBundle のような「よく使う Component の詰め合わせ」がありましたが、0.15 で required components(ある Component を spawn すると必要な相棒が自動で付く)に置き換わりました。AI は古い詰め合わせ前提のコードを 書くので、そのままだとコンパイルが通りません

4. なぜ AI は古い Bevy を書くのか

Bevy は人気があって・破壊的変更が高頻度という、LLM にとって最悪の組み合わせです。

  • 破壊的変更が多い → 「正しい書き方」が半年で変わる
  • 人気で記事が多い → 古い書き方の記事が大量に学習データに残る

結果、LLM の分布は古い Bevy に引っ張られ、最新版で動かないコードを自信満々で出す。 だから Bevy こそ「動く最小構成を人間が確認して与える」必要があります。

これは姉妹記事(手触り編)の主張と同じ構造です。あちらは「LLM は game feel(手触り)を 知らない/気にしない」、こちらは「LLM は最新 API を追えない」。API も手触りも、最後は人間が知識として持ち込む——AI とゲームを作るとは、そういう分業です。

つぎは

四角が動かせたら、次の2本へ:


参考

  • Bevy 公式サイト / Bevy の Migration Guides(各 0.x の破壊的変更。ここを見れば「古いコード」が分かる)
  • Unofficial Bevy Cheat Book
  • 本記事の実 API 裏取り: grimoire-royale の crates/client/src/main.rs / view.rs(Bevy 0.19)