Claude Media
Claude CodeでBevyのRustゲームを開発する手順

Claude CodeでBevyのRustゲームを開発する手順

BevyでRustゲームを作る公式クイックスタートの手順と、Claude CodeのCargo権限・hooks設定によるビルド高速化をあわせて解説します。

BevyとはRust製のECSゲームエンジン

Bevyは、Rustだけで書かれたオープンソースのゲームエンジンです。ECS(Entity Component System)というデータ駆動の設計パターンを中核に据え、ゲームの状態を「エンティティ」「コンポーネント」「システム」の3つに分解して組み立てます。

エンジン本体を動かすだけならRustのインストールのみで完結します。ウィンドウ表示や音声・入力など環境固有の機能を使うときだけ、OS別の追加ライブラリが必要になります。Claude Codeはcargoコマンドを実行できるので、Bevy用の設定がなくても通常のRust開発フローでプロジェクトを進められます。

本記事は公式のQuick Start「Getting Started」の手順に沿って進めます。プロジェクト作成からECSの基礎、Claude Code側でビルドを速く回すための権限・hooks設定までを扱います。同じRustのプロジェクトでMCPサーバーを書く機会があるなら、RustでMCPサーバーを書く公式SDKの使い方も参考になります。Web APIを作るなら、AxumのRust API開発で型エラーの直し方を扱っています。

RustでBevyを始める最短手順とClaude Codeの使いどころ

最短の手順は、cargo newでプロジェクトを作り、cargo add bevyで依存を足し、App::new().add_plugins(DefaultPlugins).run()でウィンドウを出す3段階です。ここまでにClaude Code固有の設定は要りません。

Claude Codeの設定が効くのは、cargo runやcargo checkを何度も回す段階からです。確認プロンプトをpermissions.allowで減らし、.rsの編集後にcargo checkを走らせ、失敗時に終了コード2を返すフック(|| exit 2)にすると、コンパイルエラーの全文がClaudeに届きます。

初回ビルドの待ち時間と、依存クレートが多いことによる反復ビルドの遅さは別の問題です。前者は一度きりで、後者はCargo側の設定で縮めます。

開発を始める前に整えるもの

Bevyを動かすには、最新の安定版Rustツールチェーンと、OS別の追加ライブラリが必要です。BevyのMSRV(サポートする最小Rustバージョン)は「最新の安定版リリース」と定義されているため、rustup updateで常に最新の状態を保つ必要があります。

rustc --version
cargo --version

OS別の追加ライブラリも必要です。LinuxならVulkan関連パッケージ、WindowsならVisual Studio C++ Build Toolsの「Desktop development with C++」ワークロード、macOSならXcodeのコマンドラインツール(xcode-select --install)を入れておきます。

コードエディタは、Language Server Protocolに対応しrust-analyzerを使えるものが公式に推奨されています。CLIで使う場合、Claude Codeはエディタとは別のターミナルで動きます。VS Codeなど別ウィンドウでrust-analyzerの補完を見ながら、Claude Codeに実装を指示するという組み合わせが機能します。Rustの言語自体に不慣れな場合は、公式が案内するThe Rust Book・Rust by Example・Rustlingsで基礎を先に固めておくと、Claude Codeが生成したコードの意図を追いやすくなります。

手順 — Bevyプロジェクトを作って動かす

ステップ1: Bevyをプロジェクトに追加する

最初のステップは、通常のRust実行プロジェクトを作り、依存関係にBevyを加えることです。cargo newで新規プロジェクトを作成し、cargo addでBevyを追加すれば準備は完了します。

cargo new my_bevy_game
cd my_bevy_game
cargo add bevy

Cargo.tomlにはbevy = "0.19"のように依存関係が追記されます。公式の例ではeditionは"2024"です。公式のGitHubリポジトリをクローンしてgit checkout v0.19.0のようにバージョンを切り替えれば、同梱の実例も試せます。cargo run --example breakoutのように実行するだけです。

ステップ2: ウィンドウを表示する

Bevyのプログラムは、すべてAppという1つの構造体を中心に組み立てます。最小のBevyアプリはApp::new().run()のわずか1行で動きますが、これだけでは何も起こりません。

use bevy::prelude::*;
 
fn main() {
    App::new().run();
}

Appはゲームのデータを保持するWorldと、ループを制御する仕組みを持つだけの「空の器」です。ウィンドウ表示やレンダリングといった機能は、DefaultPluginsというプラグイン群を追加して初めて有効になります。

App::new()
    .add_plugins(DefaultPlugins)
    .run();

DefaultPluginsを追加すると、2D/3Dレンダラー・アセット読み込み・UI・ウィンドウ・入力といった、一般的なゲームエンジンに期待される機能がまとめて有効になります。

ステップ3: コンポーネントとシステムを書く

Bevyのゲームロジックは、すべてECSというパターンで記述します。エンティティは一意の「モノ」、コンポーネントはエンティティに付けるデータ、システムは特定のコンポーネントを持つエンティティに対して実行するロジックです。

他のRust製ECS実装は複雑なライフタイムやトレイト、ビルダーパターンを要求しがちです。Bevy ECSはコンポーネントを#[derive(Component)]を付けた普通の構造体として、システムを普通のRust関数として書けます。

#[derive(Component)]
struct Person;
 
#[derive(Component)]
struct Name(String);
 
fn add_people(mut commands: Commands) {
    commands.spawn((Person, Name("Elaina Proctor".to_string())));
    commands.spawn((Person, Name("Renzo Hume".to_string())));
}

add_peopleは起動時に1度だけ実行する「startup system」として登録します。エンティティを取得して処理するにはQueryを使います。

fn greet_people(query: Query<&Name, With<Person>>) {
    for name in &query {
        println!("hello {}!", name.0);
    }
}
 
fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, add_people)
        .add_systems(Update, greet_people)
        .run();
}

値を書き換えるシステムはQueryを可変にし、&mutで取得したコンポーネントを直接更新します。複数のシステムを決まった順序で実行したいときは.chain()を使います。

fn update_people(mut query: Query<&mut Name, With<Person>>) {
    for mut name in &mut query {
        if name.0 == "Elaina Proctor" {
            name.0 = "Elaina Hume".to_string();
        }
    }
}

update_peopleのあとにgreet_peopleを実行するよう.chain()で順序を固定すれば、名前を変更してから挨拶する、という依存関係を保証できます。デフォルトではシステムは可能な限り並列実行されるため、順序を保証したい処理だけを明示的に連結します。

ステップ4: PluginとResourceで整理する

Bevyのもう1つの中核原則がモジュール性です。レンダラーのような内蔵機能も自作のゲームロジックも、すべてPluginとして実装します。Pluginトレイトを実装した構造体をadd_pluginsで登録すれば、複数のシステムをひとまとまりにできます。

pub struct HelloPlugin;
 
impl Plugin for HelloPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, add_people);
        app.add_systems(Update, (update_people, greet_people).chain());
    }
}

エンティティやコンポーネントに紐づかない「グローバルに1つだけ」のデータはResourceトレイトで表現します。経過時間・アセットコレクション・レンダラーなどが典型例です。挨拶を2秒おきに1回へ間引くなら、Timer型のリソースを使います。

#[derive(Resource)]
struct GreetTimer(Timer);
 
fn greet_people(
    time: Res<Time>,
    mut timer: ResMut<GreetTimer>,
    query: Query<&Name, With<Person>>,
) {
    if timer.0.tick(time.delta()).just_finished() {
        for name in &query {
            println!("hello {}!", name.0);
        }
    }
}

insert_resourceでリソースの初期値をプラグインに登録すれば、以降はどのシステムからでもRes・ResMut経由でアクセスできます。

Claude Codeでビルドを速くする設定

Bevyは依存クレートが多く、素のデバッグビルドでは反復のたびに待ち時間が発生します。Claude Codeで開発するなら、確認プロンプトを減らす権限設定と、ビルド速度を上げるCargo設定の両方を先に整えておくと作業が滑らかになります。

Claude Codeは既定ではcargo runのようなBashコマンドの実行ごとに確認を求めます。頻繁に使うコマンドは.claude/settings.jsonのpermissions.allowにワイルドカード付きで登録しておくと、確認なしで実行できます。

{
  "permissions": {
    "allow": [
      "Bash(cargo run *)",
      "Bash(cargo check *)",
      "Bash(cargo test *)"
    ]
  }
}

末尾の*の前に置く半角スペースがルールの一部です。Bash(cargo run *)はcargo run単体にもcargo run --releaseにも一致し、スペースを省いたBash(cargo*)はcargo-watchのような別のコマンドにも一致します。Bash(cargo *)と書くとサブコマンドを問わないので、cargo installやcargo publishまで確認なしで通ります。設定ファイルを書かずに試すだけなら、claude --allowedTools "Bash(cargo check *)"のようにフラグでも渡せます(v2.1.287のclaude --helpに"Bash(git *) Edit"の書き方が載っています)。

Bash権限ルールが複合コマンドをどう評価するかはClaude CodeのBash権限ルールは複合コマンドをどう評価するかにまとめています。登録済みルールの一覧管理はClaude Codeの/permissionsコマンドで権限ルールとauto mode拒否を管理するで扱っています。

ファイル編集のたびにcargo checkを自動実行したい場合は、PostToolUseフックを使います。ifには権限ルールと同じ書き方でツール名と引数を書けるので、.rsファイルを編集したときだけ走らせる絞り込みができます。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "cargo check --message-format short || exit 2",
            "if": "Edit(*.rs)"
          }
        ]
      }
    ]
  }
}

コマンドの末尾に|| exit 2を付けているのが要点です。

くらべる

cargo checkのエラーがClaudeに届くかどうか

届かない

素の cargo check

コンパイルに失敗しても、終了コード2以外は「ブロックしないエラー」です。トランスクリプトにはFailed with non-blocking status code:に続けてstderrの1行目だけが出て、編集は通ったまま進みます。

届く

|| exit 2 を付ける

PostToolUseは終了コード2のときstderrをClaudeに見せます。ツールはすでに実行済みなので編集は取り消されませんが、エラー全文を読んだClaudeが次の修正に進めます。

Bevyは依存クレートが多く、フックの実行に時間がかかることがあります。既定のタイムアウトはcommandフックで600秒です。待たされたくないときは、フックに"asyncRewake": trueを足します。フックがバックグラウンドで走り、終了コード2のときだけstderrがシステムリマインダーとしてClaudeに渡ります。Hooksの全イベントと設定パターンはClaude Code Hooks完全ガイドにまとめています。

Cargo側にも、反復ビルドを速くする設定があります。

Cargo

反復ビルドを速くする4つの設定

  • dynamic_linking機能

    再リンクを省ける、最も効果の大きい設定です。Windowsではopt-levelの調整も併せて有効にしないと「too many exported symbols」エラーになります。

  • lld / moldリンカー

    Linuxではlld、さらに速いmold(lldの最大5倍)を選べます。moldはWindows非対応で、macOSは標準のld-primeがlldより速いため追加は不要です。

  • devプロファイルのopt-level

    自分のコードはopt-level = 1、依存クレートは3にします。公式は、リリースモードで開発すると再コンパイルが遅くなりデバッグ記号も失うので勧めないとしています。

  • Cranelift codegen

    LLVMより約30%速くコンパイルできるnightly限定のバックエンドです。生成物は最適化が弱く、macOSではBevyアプリがクラッシュすることがあり、Wasmビルドも未対応です。

dynamic_linkingはcargo add bevy -F dynamic_linking、または--features bevy/dynamic_linking付きのcargo runで有効にできます。出荷用ビルドではlibbevy_dylibの同梱が必要になるため、無効化してから配布するのが基本です。dynamic_linkingを有効にしたままcargo test --docを走らせるには、rustc --print target-libdirが返すパスをPATH環境変数に加える必要があります。権限例でcargo testを許可している場合は、この点も押さえておくと失敗の原因を追いやすくなります。

Cargo.tomlの最適化プロファイル例
[profile.dev]
opt-level = 1
 
[profile.dev.package."*"]
opt-level = 3
Linuxでlldを使う.cargo/config.toml
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]

事前にsudo apt-get install lld clang(Ubuntuの場合)でlldとclangを入れます。moldを使うときは、rustflagsの値を-C link-arg=-fuse-ld=/usr/bin/moldに差し替えます。

よくあるつまずき

公式のTroubleshootingページには、Bevy特有のエラーが3つ整理されています。

「Unable to find a GPU」というパニックは、Vulkan対応ドライバが入っていないか、GPUのないヘッドレス環境で実行したときに出ます。Linuxではvulkan-intelやvulkan-radeonなどVulkan関連パッケージの導入で解消します。

Windowsで動的リンクビルドをVS Codeのデバッガから起動すると、終了コード-1073741515で落ちることがあります。原因はデバッグ拡張のパス解決の癖で、launch.jsonのPATH環境変数にRustツールチェーンのディレクトリを追加すれば解決します。

Wasmターゲット向けにビルドすると、getrandomクレートのコンパイルエラーが出ることがあります。uuidなど複数の依存が別バージョンのgetrandomを要求している場合、package指定でクレート名を分けたうえでwasm_jsフィーチャーを個別に有効化する必要があります。

まとめ

Bevy側の作業は公式のGetting Startedに沿えば足り、Claude Code側で差が出るのは権限ルールのスペースとフックの終了コードの2点です。cargo checkを|| exit 2で包めば、コンパイルエラーがClaudeの次の編集に反映されます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. bevy.org/learn/quick-start/getting-started
  2. bevy.org/learn/quick-start/getting-started/setup
  3. bevy.org/learn/quick-start/getting-started/apps
  4. bevy.org/learn/quick-start/getting-started/ecs
  5. bevy.org/learn/quick-start/getting-started/plugins
  6. bevy.org/learn/quick-start/getting-started/resources
  7. bevy.org/learn/quick-start/troubleshooting
  8. code.claude.com/docs/en/permissions
  9. code.claude.com/docs/en/hooks
  10. code.claude.com/docs/en/interactive-mode
この記事を共有:XはてブLinkedIn