mcpField
B
bevy-ecs

bevy-ecs

github.com/gamedev-skills開発5.4k5.7k 回インストール1 日前に更新
リポジトリ

bevy-ecs について

bevy-ecs は、Bevy の ECS でゲームを構成する Rust 開発者向けのスキルです。Bevy 0.20 の例を使い、App、プラグイン、コンポーネント、リソース、クエリ、スケジューリングに加え、バージョン管理や一般的な問題を説明します。

主な機能

  1. 01App とプラグインの構築
  2. 02コンポーネントとリソースの定義
  3. 03システムとエンティティクエリの作成
  4. 04スケジュールと実行条件の設定
  5. 05クエリ競合と遅延コマンドへの対応

活用シーン

  1. 01Bevy ゲームの ECS ロジック構成
  2. 02フレームレートに依存しない移動の実装
  3. 03依存関係のあるシステムの順序付け
  4. 04アクセス競合やバージョン固有 API エラーの調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill bevy-ecs

インストール後、Claude Code で/bevy-ecsを入力して読み込まれたか確認します。

同梱ファイルreferences/queries-and-scheduling.md

詳細

Bevy ECS

Rust で Bevy ゲームを Entity Component System を中心に構成します。App とプラグイン、コンポーネントとリソース、クエリを使うシステム、スケジューリング、フレームレートに依存しない更新を扱います。新しい例は Bevy 0.20 を対象としています。プロジェクトですでに別のリリースが固定されている場合は、そのリリースを維持し、対応する移行ガイドを使用してください。

使用する場面

  • Bevy の App の構成、Component/Resource 型の定義、エンティティをクエリするシステムの作成、システムの順序付け/フィルタリング、または借用競合によるパニックやフレームレート依存の移動の修正を行う場合。
  • Cargo.toml が bevy に依存し、コードで App::new()、add_systems、Query、または Commands を呼び出す場合。

使用しない場面: これは ECS のコアです。高度なレンダリング、カスタムシェーダー/パイプライン、UI レイアウト、オーディオは別の領域です。エンジンに依存しない AI や手続き型アルゴリズムには、game-ai/procedural-gen を組み合わせてください。

基本的なワークフロー

  1. バージョンを確認して固定します。 まず Cargo.toml と Cargo.lock を確認します。新規プロジェクトでは bevy = "0.20" を使用します。既存のプロジェクトを、Bevy のマイナーリリースをまたいで黙って移行してはいけません。対応するドキュメントと移行ガイドを正とします。
  2. App を構築します。 App::new().add_plugins(DefaultPlugins) により、ウィンドウ管理、入力、レンダリング、時間などが利用できるようになります。システムをスケジュールに登録します。Startup は一度、Update は毎フレーム実行されます。
  3. データをコンポーネント、グローバルな状態をリソースとしてモデル化します。 エンティティごとのデータには #[derive(Component)]、一つだけ存在するデータ(スコア、設定、Time クロックなど)には #[derive(Resource)] を使用します。0.19 以降、Resource は Component を拡張し、各リソースはそれぞれ独自のエンティティに格納されるため、両方を derive しないでください。
  4. システムは通常の関数として記述します。 パラメーターでデータへのアクセスを宣言します。エンティティには Query<...>、リソースには Res<T>/ResMut<T>、遅延スポーン/デスポーンには Commands を使用します。アクセスが競合しない場合、システムは並列実行されます。
  5. time.delta_secs() に基づいて移動させ、フレームレートに依存しないようにします。
  6. 順序が必要なものだけを .chain() または明示的な制約で順序付けし、run_if でシステムを実行条件に従わせます。 関連するセットアップは Plugin にまとめます。cargo run でビルドしてパニックを確認してください。Bevy は起動時にクエリの競合を報告します。

パターン

1. Cargo.toml + 最小限の App
# Cargo.toml — pin the version; the API differs across minor releases.
[dependencies]
bevy = "0.20"
// main.rs
use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)            // window, input, render, time, ...
        .add_systems(Startup, setup)            // runs once at startup
        .add_systems(Update, move_players)      // runs every frame
        .run();
}
2. コンポーネント、リソース、スポーン
#[derive(Component)]
struct Player;

#[derive(Component)]
struct Velocity(Vec2);

#[derive(Resource)]
struct Score(u32);

fn setup(mut commands: Commands) {
    commands.insert_resource(Score(0));

    // Camera2d is a component with required components (bundles removed in 0.16);
    // spawning it pulls in Transform, Camera, etc. automatically.
    commands.spawn(Camera2d);

    // Spawn an entity as a tuple of components.
    commands.spawn((
        Player,
        Velocity(Vec2::new(150.0, 0.0)),
        Transform::from_xyz(0.0, 0.0, 0.0),
    ));
}
3. クエリと Time リソースを使うシステム
// Iterate every entity that has BOTH Velocity and Transform; mutate Transform.
fn move_players(time: Res<Time>, mut query: Query<(&Velocity, &mut Transform)>) {
    for (velocity, mut transform) in &mut query {
        // delta_secs() is f32 seconds (renamed from delta_seconds() in 0.16).
        transform.translation += velocity.0.extend(0.0) * time.delta_secs();
    }
}
4. クエリフィルター(With / Without / Changed)
// Only entities tagged Player (the Player component itself isn't read).
fn aim_player(mut q: Query<&mut Transform, With<Player>>) { /* ... */ }

// Disjoint two mutable Transform queries so they don't conflict at runtime.
fn separate(
    mut players: Query<&mut Transform, With<Player>>,
    mut enemies: Query<&mut Transform, Without<Player>>,
) { /* ... */ }

// React only when Health changed since last run (change detection).
fn on_health_change(q: Query<&Health, Changed<Health>>) {
    for health in &q { /* update the HUD, etc. */ }
}
5. リソースの読み取りと書き込み
fn add_points(mut score: ResMut<Score>) {
    score.0 += 10;                 // ResMut = write access
}

fn show_score(score: Res<Score>) {
    info!("score: {}", score.0);   // Res = read access
}
6. 順序付け、実行条件、プラグイン
fn main() {
    App::new()
        .add_plugins((DefaultPlugins, GameplayPlugin))
        // .chain() forces order: damage resolves before death is checked.
        .add_systems(Update, (apply_damage, check_deaths).chain())
        // run_if gates a system on a condition each frame.
        .add_systems(Update, spawn_wave.run_if(wave_timer_finished))
        .run();
}

struct GameplayPlugin;
impl Plugin for GameplayPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(Score(0))
           .add_systems(Startup, setup)
           .add_systems(Update, (move_players, add_points));
    }
}

よくある落とし穴

  • delta_seconds() が見つからない → 0.16 で time.delta_secs()(および elapsed_secs())に改名されました。古い名前を使うとコンパイルに失敗します。
  • 移動速度がフレームレートに応じて変わる → フレームごとの変更量に time.delta_secs() を掛けてください。フレーム時間が一定だと想定しないでください。
  • パニック error[B0001]: 「... accesses component(s) Transform in a way that conflicts with a previous system parameter」 → 1 つのシステム内の 2 つの Query が同じ コンポーネントに書き込んでいるか、一方が読み込み、もう一方が重複するエンティティに書き込んでいます。 With/Without を使って対象を分離するか、ParamSet を使ってください。
  • Camera2dBundle/SpriteBundle が見つからない → バンドルは 0.15 で非推奨になり、 0.16 で削除されました。 コンポーネント(Camera2d、Sprite、Transform)を直接スポーンしてください。残りは必須 コンポーネントによって補完されます。
  • 「trait Component is not implemented」 → #[derive(Component)] を付け忘れています (リソースの場合は #[derive(Resource)])。
  • スポーンしたエンティティが同じフレーム内の後続のクエリに表示されない → Commands は 遅延され、次の同期ポイントで適用されます。スポーンしたシステムではなく、後続のシステムで エンティティを読み取ってください。
  • システムの実行順序を想定しているが、強制していない → システムはデフォルトで並列実行されます。 B を A の後に実行する必要がある場合は、(A, B).chain() または明示的な順序制約を追加してください。 偶然の順序に依存している箇所を見つけるには、テストで ScheduleBuildSettings::shuffle_seed を使って スケジュールをシャッフルしてください(0.20、debug 機能。有効な参照を参照してください)。
  • Resource と Component の両方を derive した後に「trait Component の実装が競合しています」 → 0.19 以降、 Resource は Component を拡張しているため、Resource だけを derive してください。
  • 「すべて Despawn」がパニックする、または Res<T> の隣にある Query<EntityMut> が error[B0002] でパニックする → 0.19 以降、すべてのリソースは IsResource が付いたエンティティ上に存在するため、 Query<Entity>/EntityRef/EntityMut もそれらのエンティティにマッチします。独自の マーカーコンポーネントをクエリするか、Without<IsResource> を追加してください(use bevy::ecs::resource::IsResource)。
  • On<Add, Player> が「構造体は 1 個のジェネリック引数を取りますが、2 個のジェネリック引数が指定されました」で失敗する → 0.20 ではコンポーネントがライフサイクルイベントに移動しました。 On<Add<Player>> と記述してください(同様に Insert<T>、Discard<T>、Remove<T>、Despawn<T>)。
  • 現在の状態に「切り替える」と OnExit/OnEnter が再度実行される → NextState::set は常に遷移スケジュールを実行します。同じ状態への遷移をスキップするには set_if_different を使ってください(0.20 で set_if_neq から改名されました。ResMut<NextState<_>> で set_if_neq を呼び出すと、無関係な変更検出メソッドが選ばれ、「NextState を比較できません」というエラーになります)。
  • Name::new(text.as_str()) で「text の有効期間が十分ではありません」 → 0.20 以降、 Name が受け取るのは &'static str リテラルまたは所有権を持つ String であり、借用した &str ではありません。 Name::new(text.clone()) または Name::new(format!("Enemy {id}")) を渡してください。
  • 0.20 にアップグレードした後、重なったスプライトの描画順が入れ替わる → スプライトは Mesh2d バックエンド経由で描画されるようになり、同じ z での描画順は以前から保証されていませんでした。 重なる可能性のあるスプライトには、それぞれ異なる Transform の z 値を設定してください。
  • 古い Bevy のコード例をコピーして貼り付ける → API はマイナーバージョン間で変わります。バッファリングされたイベントシステムはメッセージシステムに変わりました。EventReader/EventWriter/ add_event はなくなったため、#[derive(Message)]、MessageReader/ MessageWriter、app.add_message::<T>() を使ってください。固定したバージョンに対応するドキュメントと移行ガイドを確認し、異なるバージョンを混在させないでください。

参考資料

  • スケジュールと SystemSet の順序付け(0.20 の弱い順序付けとスケジュールの シャッフルを含む)、States/OnEnter/OnExit と状態スコープのデスポーン、変更 検出、Commands のライフサイクルと同期ポイント、ParamSet とリソースエンティティの 競合、iter_many の結果、0.20 構文でのメッセージ/オブザーバーについては、 references/queries-and-scheduling.md をお読みください。

関連スキル

  • game-ai — ECS で実装するための、ポータブルな概念としての FSM/ビヘイビアツリー/ステアリング。
  • procedural-gen — システムから駆動するノイズ/RNG/生成アルゴリズム。
  • pygame-core / love2d-core — 小規模なプロジェクト向けの軽量エンジン。

コメント