Bevy 0.19 新クエリシステムでECS検索速度45%向上|破壊的変更と移行ガイド【2026年5月最新】
Bevy 0.19の新クエリシステム実装を徹底解説。ECS検索速度45%向上を実現したアーキテクチャ変更、破壊的変更への対応、実装パターンを実例付きで完全ガイド。
約8分で読めますBevy 0.19が2026年5月3日にリリースされました。最大の変更点はクエリシステムの完全な再設計です。従来のクエリAPIは柔軟性に欠け、特に大規模なゲーム世界でのEntity検索において深刻なパフォーマンスボトルネックとなっていました。新しいクエリシステムでは、検索速度が平均45%向上し、メモリフットプリントも30%削減されています。
この記事では、Bevy 0.19の新クエリシステムの技術的詳細、破壊的変更への移行方法、そして実際のゲーム開発での実装パターンを実例付きで解説します。
Bevy 0.19クエリシステムの技術的変更点
アーキタイプベースクエリからスパーステーブルへの移行
Bevy 0.18以前のクエリシステムはアーキタイプベースで実装されていました。これはComponentの組み合わせごとにEntityを分類する手法で、同じComponent構成のEntityをまとめて処理する際には高速ですが、動的なComponent追加・削除が頻繁に発生するゲームでは非効率でした。
Bevy 0.19ではスパーステーブル(Sparse Table)ベースのクエリに移行しました。各ComponentをビットマスクとEntityIDのペアで管理することで、Component構成の変更によるEntityの再分類コストを削減しています。
以下のダイアグラムは、新しいクエリシステムのアーキテクチャを示しています。
flowchart TD
A["Query<br/>(Position, Velocity)"] --> B["Component Bitset<br/>解析"]
B --> C["Sparse Table<br/>インデックス検索"]
C --> D["Entity Storage<br/>直接アクセス"]
D --> E["結果セット<br/>取得"]
F["Component追加/削除"] --> G["Bitset更新<br/>O(1)"]
G --> H["再分類不要"]
style C fill:#4CAF50
style G fill:#2196F3
style H fill:#FF9800
このアーキテクチャ変更により、以下のパフォーマンス改善が実現されています。
ベンチマーク結果(公式発表より):
- 100万Entity規模での
Query<(&Position, &Velocity)>の実行時間: 18.3ms → 10.1ms(45%削減) - Component追加・削除時の再分類コスト: 平均2.4ms → 0.3ms(87%削減)
- メモリフットプリント: 1GB → 720MB(28%削減)
新しいクエリAPI: QueryState と QueryBuilder
従来のQueryはシステム引数としてのみ使用でき、動的なクエリ構築ができませんでした。Bevy 0.19ではQueryStateとQueryBuilderが導入され、ランタイムでのクエリ組み立てが可能になりました。
use bevy::prelude::*;
use bevy::ecs::query::{QueryState, QueryBuilder};
// 従来の静的クエリ(0.18以前)
fn old_system(query: Query<(&Position, &Velocity)>) {
for (pos, vel) in query.iter() {
// 処理
}
}
// 新しい動的クエリ(0.19)
fn new_dynamic_system(world: &mut World) {
let mut query_state = QueryState::<(&Position, &Velocity)>::new(world);
for (pos, vel) in query_state.iter(world) {
// 処理
}
}
// QueryBuilderによる動的構築
fn builder_system(world: &mut World) {
let mut builder = QueryBuilder::<Entity>::new(world);
// 条件に応じてComponentを追加
builder
.with::<Position>()
.with::<Velocity>()
.without::<Destroyed>();
let query = builder.build();
for entity in query.iter(world) {
// 処理
}
}
QueryBuilderを使うことで、ゲームの状態に応じて動的にクエリ条件を変更できるため、複雑なAIシステムやイベント駆動処理での実装が大幅に簡潔になります。
破壊的変更と移行パターン
Bevy 0.19のクエリシステム変更は破壊的変更を伴います。以下の主要な変更点を把握し、既存コードを移行する必要があります。
1. Query::get_mut の戻り値型変更
Bevy 0.18ではQuery::get_mutはResult<Mut<T>, QueryEntityError>を返していましたが、0.19ではOption<Mut<T>>に変更されました。
// 0.18以前
fn old_code(mut query: Query<&mut Health>, entity: Entity) {
if let Ok(mut health) = query.get_mut(entity) {
health.value -= 10;
}
}
// 0.19
fn new_code(mut query: Query<&mut Health>, entity: Entity) {
if let Some(mut health) = query.get_mut(entity) {
health.value -= 10;
}
}
この変更により、エラーハンドリングがシンプルになり、コードの可読性が向上します。
2. Query::iter_combinations の削除
Query::iter_combinationsは削除され、Query::iter_manyに置き換えられました。
// 0.18以前
fn old_collision_check(query: Query<(Entity, &Position, &Collider)>) {
for [(e1, p1, c1), (e2, p2, c2)] in query.iter_combinations() {
// 衝突判定
}
}
// 0.19
fn new_collision_check(query: Query<(Entity, &Position, &Collider)>) {
let entities: Vec<_> = query.iter().map(|(e, _, _)| e).collect();
for pairs in entities.windows(2) {
if let [e1, e2] = pairs {
if let (Some((_, p1, c1)), Some((_, p2, c2))) =
(query.get(*e1).ok(), query.get(*e2).ok()) {
// 衝突判定
}
}
}
}
以下のダイアグラムは、新しい衝突判定パターンの処理フローを示しています。
sequenceDiagram
participant S as System
participant Q as Query
participant W as World
S->>Q: iter()でEntity一覧取得
Q->>W: Component読み取り
W-->>Q: Entity + Component
Q-->>S: Vec<Entity>
S->>S: windows(2)でペア生成
loop 各ペア
S->>Q: get(e1), get(e2)
Q->>W: Component読み取り
W-->>Q: Component参照
Q-->>S: Option<Component>
S->>S: 衝突判定実行
end
3. Added と Changed のフィルタ挙動変更
Added<T>とChanged<T>は内部実装が変更され、検出精度が向上しました。従来は同一フレーム内での複数回変更を検出できませんでしたが、0.19ではタイムスタンプベースの変更追跡により正確に検出できます。
// 0.19での正確な変更検出
fn change_detection_system(
query: Query<(Entity, &Health), Changed<Health>>,
) {
for (entity, health) in query.iter() {
println!("Entity {:?} の Health が変更されました: {}", entity, health.value);
}
}
大規模ゲーム開発での実装パターン
パターン1: 動的クエリによる条件分岐最適化
従来は複数のシステムを用意していたような条件分岐処理を、単一の動的クエリで実装できます。
use bevy::prelude::*;
use bevy::ecs::query::QueryBuilder;
#[derive(Component)]
struct Enemy;
#[derive(Component)]
struct Boss;
#[derive(Component)]
struct Position(Vec3);
#[derive(Component)]
struct Aggro(f32);
fn dynamic_ai_system(
world: &mut World,
difficulty: Res<GameDifficulty>,
) {
let mut builder = QueryBuilder::<(Entity, &Position)>::new(world);
// 難易度に応じてクエリ条件を変更
match difficulty.level {
1..=3 => {
builder.with::<Enemy>().without::<Boss>();
},
4..=7 => {
builder.with::<Enemy>();
},
8..=10 => {
builder.with::<Enemy>().with::<Aggro>();
},
_ => {}
}
let query = builder.build();
for (entity, pos) in query.iter(world) {
// AI処理
}
}
パターン2: QueryState によるクエリキャッシング
頻繁に実行されるクエリはQueryStateとしてキャッシュすることで、毎フレームのクエリ構築コストを削減できます。
use bevy::prelude::*;
use bevy::ecs::query::QueryState;
#[derive(Resource)]
struct CachedQueries {
enemy_query: QueryState<(Entity, &Position, &Health)>,
projectile_query: QueryState<(Entity, &Position, &Velocity)>,
}
fn setup_queries(world: &mut World) {
let enemy_query = QueryState::<(Entity, &Position, &Health)>::new(world);
let projectile_query = QueryState::<(Entity, &Position, &Velocity)>::new(world);
world.insert_resource(CachedQueries {
enemy_query,
projectile_query,
});
}
fn collision_system(
world: &mut World,
mut cached: ResMut<CachedQueries>,
) {
// キャッシュされたクエリを使用
let enemies: Vec<_> = cached.enemy_query.iter(world)
.map(|(e, p, h)| (e, p.clone(), h.clone()))
.collect();
let projectiles: Vec<_> = cached.projectile_query.iter(world)
.map(|(e, p, v)| (e, p.clone(), v.clone()))
.collect();
// 衝突判定処理
for (proj_entity, proj_pos, _) in &projectiles {
for (enemy_entity, enemy_pos, enemy_health) in &enemies {
if proj_pos.0.distance(enemy_pos.0) < 1.0 {
// ダメージ処理
}
}
}
}
以下のダイアグラムは、クエリキャッシングによるパフォーマンス改善の仕組みを示しています。
flowchart LR
A["毎フレーム実行"] --> B{QueryStateキャッシュ}
B -->|キャッシュあり| C["既存QueryState使用<br/>構築コスト: 0μs"]
B -->|キャッシュなし| D["新規Query構築<br/>構築コスト: 150μs"]
C --> E["クエリ実行<br/>実行コスト: 200μs"]
D --> E
E --> F["合計: 200μs<br/>(キャッシュあり)"]
E --> G["合計: 350μs<br/>(キャッシュなし)"]
style C fill:#4CAF50
style F fill:#4CAF50
style D fill:#FF5722
style G fill:#FF5722
パターン3: 並列クエリ処理
Bevy 0.19ではQuery::par_iterの内部実装が改善され、マルチコアCPUでのスケーリング性能が向上しました。
use bevy::prelude::*;
use bevy::tasks::ComputeTaskPool;
fn parallel_physics_system(
mut query: Query<(&mut Position, &Velocity)>,
task_pool: Res<ComputeTaskPool>,
) {
query.par_iter_mut().for_each(|(mut pos, vel)| {
pos.0 += vel.0 * 0.016; // 60FPS想定
});
}
並列処理ベンチマーク(4コアCPU):
- Bevy 0.18: 100万Entity処理 = 42ms
- Bevy 0.19: 100万Entity処理 = 23ms(45%高速化)
移行チェックリスト
既存のBevy 0.18プロジェクトを0.19に移行する際は、以下の項目を確認してください。
-
Query::get_mut の戻り値型変更
Result<Mut<T>, QueryEntityError>→Option<Mut<T>>に修正if let Ok(...)→if let Some(...)に置き換え
-
Query::iter_combinations の削除
iter_combinations()をiter_many()または手動ループに置き換え- 既存のコンビネーション処理をリファクタリング
-
Added
/ Changed の精度向上 - 同一フレーム内での複数回変更を前提とした処理を再検証
- デバッグログで変更検出タイミングを確認
-
QueryState の活用
- 頻繁に実行されるクエリを
QueryStateでキャッシュ - リソースとして保持し、システム間で再利用
- 頻繁に実行されるクエリを
-
QueryBuilder の導入
- 動的なクエリ条件が必要な処理を
QueryBuilderで実装 - 条件分岐による複数システムを単一システムに統合
- 動的なクエリ条件が必要な処理を
まとめ
Bevy 0.19の新クエリシステムは、以下の点で大幅な改善をもたらします。
- パフォーマンス: ECS検索速度45%向上、メモリ使用量30%削減
- 柔軟性:
QueryBuilderによる動的クエリ構築が可能に - 保守性:
QueryStateキャッシングで重複クエリを削減 - 並列性: マルチコア環境でのスケーリング性能向上
破壊的変更は限定的で、主に戻り値型の変更と廃止されたAPIの置き換えが中心です。移行コストは小さく、パフォーマンス改善の恩恵が大きいため、早期の移行を推奨します。
大規模なゲーム開発では、QueryStateによるクエリキャッシングとQueryBuilderによる動的クエリ構築を組み合わせることで、複雑なゲームロジックを効率的に実装できます。