bevy-ecs について
bevy-ecs は、Bevy の ECS でゲームを構成する Rust 開発者向けのスキルです。Bevy 0.20 の例を使い、App、プラグイン、コンポーネント、リソース、クエリ、スケジューリングに加え、バージョン管理や一般的な問題を説明します。
主な機能
- 01App とプラグインの構築
- 02コンポーネントとリソースの定義
- 03システムとエンティティクエリの作成
- 04スケジュールと実行条件の設定
- 05クエリ競合と遅延コマンドへの対応
活用シーン
- 01Bevy ゲームの ECS ロジック構成
- 02フレームレートに依存しない移動の実装
- 03依存関係のあるシステムの順序付け
- 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 を組み合わせてください。
基本的なワークフロー
- バージョンを確認して固定します。 まず
Cargo.tomlとCargo.lockを確認します。新規プロジェクトではbevy = "0.20"を使用します。既存のプロジェクトを、Bevy のマイナーリリースをまたいで黙って移行してはいけません。対応するドキュメントと移行ガイドを正とします。 Appを構築します。App::new().add_plugins(DefaultPlugins)により、ウィンドウ管理、入力、レンダリング、時間などが利用できるようになります。システムをスケジュールに登録します。Startupは一度、Updateは毎フレーム実行されます。- データをコンポーネント、グローバルな状態をリソースとしてモデル化します。 エンティティごとのデータには
#[derive(Component)]、一つだけ存在するデータ(スコア、設定、Timeクロックなど)には#[derive(Resource)]を使用します。0.19 以降、ResourceはComponentを拡張し、各リソースはそれぞれ独自のエンティティに格納されるため、両方を derive しないでください。 - システムは通常の関数として記述します。 パラメーターでデータへのアクセスを宣言します。エンティティには
Query<...>、リソースにはRes<T>/ResMut<T>、遅延スポーン/デスポーンにはCommandsを使用します。アクセスが競合しない場合、システムは並列実行されます。 time.delta_secs()に基づいて移動させ、フレームレートに依存しないようにします。- 順序が必要なものだけを
.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
Componentis not implemented」 →#[derive(Component)]を付け忘れています (リソースの場合は#[derive(Resource)])。 - スポーンしたエンティティが同じフレーム内の後続のクエリに表示されない →
Commandsは 遅延され、次の同期ポイントで適用されます。スポーンしたシステムではなく、後続のシステムで エンティティを読み取ってください。 - システムの実行順序を想定しているが、強制していない → システムはデフォルトで並列実行されます。
BをAの後に実行する必要がある場合は、(A, B).chain()または明示的な順序制約を追加してください。 偶然の順序に依存している箇所を見つけるには、テストでScheduleBuildSettings::shuffle_seedを使って スケジュールをシャッフルしてください(0.20、debug機能。有効な参照を参照してください)。 ResourceとComponentの両方を derive した後に「traitComponentの実装が競合しています」 → 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— 小規模なプロジェクト向けの軽量エンジン。
コメント
Markdown 対応