Rust Bevy ECS 入門
はじめに
Rust 製のゲームエンジン bevy を使いこなすには、そのシステムの基礎をなす Bevy ECS の理解が必須になる。ここでは Bevy ECS の基本的な使い方をざっくりとまとめる。画面に何か描画するようなコードは一切出てこない。
Rust と Bevy のバージョンは下記を想定している。
- Rust: 1.57.0
- Bevy: 0.6.0
本稿で使用している機能の使用例はこちらにまとめてあるので、ご参考まで。
Bevy を使用するための環境構築については、下記を参照。
Bevy における基本的なプログラムの概要
まず、Bevy ECS を使用した App の全体像をざっくりと把握するため、Bevy Book の ECS ページの例を参考にしたものを掲載する。
use bevy::prelude::*;
/// Components
/// Component を derive した struct や enum が Component として使用できる
#[derive(Component)]
struct Person;
#[derive(Component)]
struct Name(String);
/// Startup System
/// Rust の通常の関数が System として使用できる
/// App の開始時に一度だけ呼ばれる
/// Commands を使い、Person と Name を持った Entity を生成する
fn add_people(mut commands: Commands) {
commands.spawn().insert(Person).insert(Name("Rust".to_string()));
commands.spawn().insert(Person).insert(Name("Bevy".to_string()));
commands.spawn().insert(Person).insert(Name("Ferris".to_string()));
}
/// System
/// Rust の通常の関数が System として使用できる
/// Query を使って Component を取得し、それに対して処理を行う
fn greet_people(query: Query<&Name, With<Person>>) {
for name in query.iter() {
println!("hello {}!", name.0);
}
}
/// App に System を登録し、Bevy の App を構築して Run する
fn main() {
App::new()
.add_startup_system(add_people)
.add_system(greet_people)
.run();
}
$ cargo run
hello Rust!
hello Bevy!
hello Ferris!
Bevy ECS を構成する基本要素
ECS では、Entity/Component/System をもとにシステム全体が構成される。
- Entity : ユニークな ID を持つ、Component の集合体
- Component : Entity に付与されるデータ
- System : 様々なデータにアクセスして処理を行う関数
Bevy Book の説明が端的でわかりやすかったため、日本語訳を引用する。
Bevy のすべてのアプリのロジックは、Entity Component System パラダイムを使用しており、しばしば ECS と略記されます。ECS は、プログラムをエンティティ、コンポーネント、システムに分割するソフトウェアパターンです。エンティティはユニークな「もの」で、コンポーネントのグループを割り当てられ、それをシステムを使って処理します。
例えば、あるエンティティは
Position
とVelocity
のコンポーネントを持ち、別のエンティティはPosition
とUI
のコンポーネントを持つかもしれません。システムは、特定のコンポーネントタイプのセットで実行されるロジックです。例えば、Position
とVelocity
コンポーネントを持つすべてのエンティティで実行される移動システムがあるとします。ECS パターンは、アプリのデータやロジックをコアコンポーネントに分割することで、クリーンでデカップリングされた設計を促進します。また、メモリアクセスのパターンを最適化し、並列処理を容易にすることで、コードの高速化にも役立ちます。
Bevy - ECS の序文 (DeepL による翻訳)
Bevy ECS では、Entity/Component/System に加えて、下記の機能が重要な役割を果たす。
- Resource : App の中でグローバルで唯一のデータ
- Query : System でアクセスする Entity を取得する
- Commands : Entity の生成・破棄、Component の追加・削除、Resource の管理等を行う
- Event : System 同士でデータを送受信する
Component や System は、できる限り小さな粒度で実装することが望ましい。 これにより、Bevy の並列処理のアクセス競合をできる限り小さくでき、処理の高速化が可能となる。次節から、Bevy ECS を構成する要素とその使い方を端的にまとめていく。
App
- Bevy を使用するために、必要な全要素を App へ登録する
- System, Plugin, Resource, Event 等を登録する必要がある
- Component は App に登録する必要はない
fn main() {
App::new()
.add_plugins(DefaultPlugins) // 標準的な Bevy の機能を追加
.insert_resource(MyResource) // Global で唯一な Resource を追加
.add_event::<MyEvent>() // Event を追加
.add_startup_system(setup) // 初期化時に一度だけ呼ばれる System
.add_system(my_system) // System を追加
.run();
}
Entity
- Entity はユニークな ID を持つオブジェクトで、複数の Component の集合体を表す
- Entity は後述の Command を使って生成し、複数の Component を追加できる
struct Entity(u64);
fn add_entity(mut commands: Commands) {
let entity = commands
.spawn() // Entity の生成
.insert(Person) // Person Component の追加
.insert(Name("Bevy".to_string())) // Name Component の追加
.id(); // Entity を取得
println!("Entity ID is {}", entity.id());
}
Component
- Component は Entity に付与されるプロパティを表す
-
Component
トレイトを実装したstruct
やenum
が使用できる - New Type Pattern でシンプルな型でも Component として使用することができる
- 空の
struct
を使えば Query Filter で利用可能な Marker Component として利用できる
// `Component` を実装した struct や enum が Component として使用可能
#[derive(Component)]
struct Position { x: f32, y: f32 }
// New Type を使って単純な String を Component として使える
#[derive(Component)]
struct PlayerName(String);
// 空の struct は Marker Component としても使える
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Enemy;
fn add_entities(mut commands: Commands) {
// Player Entity を生成する
commands
.spawn() // Entity を生成
.insert(Player) // Player の Marker を追加
.insert(Position::default()) // Position Component を追加
.insert(PlayerName("Ferris".to_string())); // PlayerName を追加
// Enemy Entity を生成する
commands
.spawn() // Entity を生成
.insert(Enemy) // Enemy の Marker を追加
.insert(Position::default()); // Position Component を追加
}
Bundle
- Bundle は複数の Component 持つ Entity のテンプレートである
- Bundle として使用する
struct
はBundle
をderive
する必要がある - Bundle に中を Bundle を含め、ネストさせることもできる (
#[bundle]
が必要) - 任意の Component のタプルも Bundle として解釈される
/// Component を Bundle としてまとめて定義する
/// Bundle を定義するには derive(Bundle) が必要
#[derive(Default, Bundle)]
struct PlayerStatus {
hp: PlayerHp,
mp: PlayerMp,
xp: PlayerXp,
}
/// Bundle は入れ子にすることもできる
#[derive(Default, Bundle)]
struct PlayerBundle {
name: PlayerName,
position: Position,
_marker: Player,
// Bundle を入れ子にするには #[bundle] が必要
#[bundle]
status: PlayerStatus,
}
fn add_entities(mut commands: Commands) {
// Player Bundle を持った Entity を生成する
commands.spawn_bundle(PlayerBundle::default());
// Enemy の Entity を生成する
// タプルで Component をまとめると Bundle と解釈される
commands.spawn_bundle((Enemy, Position { x: -1.0, y: -2.0 }));
}
System
- System は App の様々な要素にアクセスし、様々な処理を行う
- System は Rust の通常の関数として定義する
- System 関数を実行させるには、下記のように App に登録する必要がある
-
add_startup_system()
: 起動時に一度だけ呼ばれる処理を登録する -
add_system()
: 毎フレーム呼ばれる処理を登録する
-
fn setup () {
println!("Hello from setup");
}
fn hello_world () {
println!("Hello from system");
}
fn main() {
App::new()
.add_startup_system(setup) // 起動時に一度だけ呼ばれる
.add_system(hello_world) // 毎フレームよばれる
.run();
}
System から様々なデータへのアクセス
- System の引数として指定することで、様々なデータへアクセスし処理を行うことが可能
Res<T>
ResMut<T>
: Resource にアクセスするQuery
: フィルタリングされた Entity の Component にアクセスするCommands
: Entity/Component/Resource を生成・削除するEventWriter
EventReader
: Event を送受信する
- System の引数には、任意の種類を可変長で指定可能 (デフォルト 16 個まで)
- タプルで引数をグループ化/ネストすることで、この引数の数の制限を回避することも可能
fn my_system(
resource: Res<MyResource>, // ResourceA にイミュータブルアクセス
query: Query<&MyComponent>, // ComponentA をクエリしてアクセス
mut commands: Commands, // Commands を使って要素の生成・削除
event_writer: EventWriter<Data>, // Event を送信
(ra, mut rb): (Res<ResourceA>, ResMut<ResourceB>), // タプルでグループ化
) {
// do something
}
System Order
- Bevy はできる限り多くの System を並列して実行するように設計されている
- 複数の System からデータへのミュータブルアクセスが必要な場合、実行順は保証されない
- System Label を使用することで、明示的に System の実行順を制御することができる
App::new()
// second という label をつける
.add_system(second.label("second"))
// first は second より前に
.add_system(first.before("second"))
// fourth は second より後に
.add_system(fourth.label("fourth").after("second"))
// third は second より後 fourth より前に
.add_system(third.after("second").before("fourth"))
.run();
System Set
- System Set は複数の System にまとめて Label/Order/State 等を付与することができる
- System Set 内の System に、個別に Label や Ordering を設定することもできる
App::new()
// second と third をまとめて SystemSet にする
.add_system_set(
SystemSet::new()
// label をつけておく
.label("second and third")
// first よりも後に second と third を実行する
.after("first")
// ここにも label をつけておく
.with_system(second.label("second"))
// third は second より後に
.with_system(third.after("second")),
)
// first は second and third より前に
.add_system(first.label("first").before("second and third"))
// fourth は second and third より後に
.add_system(fourth.after("second and third"))
.run();
System Chaining
- 複数の System の入出力を連結し、一つの大きな System として扱うことができる
- これにより、System から
Result<T>
などのエラーを返すことができるようになる - Chain された System への入力は
In(result): In<Result<()>>
のように指定する - ただし Chain の最後の System は、出力 (返り値) を持つことはできない
use anyhow::Result;
use bevy::prelude::*;
/// 処理の結果を後段の System へ転送する
fn parse_number() -> Result<()> {
let s = "number".parse::<i32>()?; // Err
Ok(())
}
/// 前段の System から送られてきた Result が Err だったら処理する
fn handle_error(In(result): In<Result<()>>) {
if let Err(e) = result {
println!("parse error: {:?}", e);
}
}
fn main() {
App::new().add_system(
// chain() で後段の System を登録する
parse_number.chain(handle_error),
)
.run();
}
SystemParam
-
SystemParam
は System の引数をひとまとめにした型の定義 -
SystemParam
は System の引数にできるものは何でも入れることができる - 定義する型には
SystemParam
トレイトを derive する必要がある - Bevy 0.6.0 時点では Lifetime は下記のように指定する必要がある
-
SystemParam
を derive する際は<'w, 's>
と指定する必要がある-
'w
:World
の Lifetime -
's
:State
の Lifetime
-
- Query は
<'w, 's>
, Resource は<'w>
, Local Resource は<'s>
を要求する - Query 内の Component の Lifetime は
'static
を要求する - メンバが
'w
's
を要求しない場合はPhantomData
を導入する必要がある
-
use bevy::ecs::system::SystemParam; // 要インポート
/// SystemParam は System の引数にできるものは何でも持つことができる
#[derive(SystemParam)]
struct MySystemParam<'w, 's> {
query: Query<'w, 's, (Entity, &'static MyComponent)>,
resource: ResMut<'w, MyResource>,
local: Local<'s, usize>,
}
#[derive(SystemParam)]
struct MySystemParam2<'w, 's> {
resource: ResMut<'w, MyResource>,
// 's を満たすために PhantomData が必要
#[system_param(ignore)]
_secret: PhantomData<&'s ()>,
}
/// SystemParam は直接 System の引数にすることができる
fn query_system_param(mut my_system_param: MySystemParam) {
// ..
}
Query
- Query を使い、System から Entity の Component へアクセスすることができる
- Query 結果には、条件にマッチした複数の Entity の Component が含まれる
-
iter()
iter_mut()
で Component へのイテレータを取得できる -
get(entity)
get_mut(entity)
で指定した Entity の Component を取得できる - Entity がひとつだけなら、下記のように直接 Component を取得できる
-
single()
single_mut()
: ひとつだけでない場合はpanic!
する -
get_single()
get_single_mut()
: ひとつだけかどうかのResult
を返す
-
-
fn my_system(mut q: Query<&mut MyComponent>) {
// iter() iter_mut() でイテレートして処理
for c in q.iter_mut() {
// すべての MyComponent をミュータブルにイテレートして処理
}
// get() get_mut() で Entity を指定して取得
if let Ok(c) = q.get_mut(entity) {
// 指定した Entity が見つかれば処理
}
// Entity がひとつだけなのであれば、直接取得できる
let c = q.single();
}
Query の様々な指定方法
- Query に指定する Component の型は
&T
&mut T
やそのOption
である必要がある - Bundle は Query に指定できず、Bundle に含まれる個別の Component を使う必要がある
- タプルで複数の Component を指定すれば、そのすべてを持つ Entity を取得できる
-
Option<T>
を使うことで、その Component が存在する場合は取得できる -
Entity
を Query に指定することで、Entity ID を取得できる
fn my_system(
q_a: Query<&CompA>, // A を持つ Entity
mut q_b: Query<&mut CompB>, // B を持つ Entity (Mutable)
q_ac: Query<(&CompA, &CompC)>, // A と C を両方持つ Entity
q_ad: Query<(&CompA, Option<&CompD>)>, // A と持っていれば D
q_ae: Query<(&CompA, Entity)>, // A とその Entity ID)
{
for a in q_a.iter() {
println!("{:?}", a);
}
let mut b = q_b.single_mut().unwrap();
// do something with b
println!("{:?}", b);
for (a, c) in q_ac.iter() {
println!("{:?}, {:?}", a, c);
}
for (a, d) in q_ad.iter() {
println!("{:?}, {:?}", a, d);
}
for (a, e) in q_ae.iter() {
println!("{:?}, {:?}", a, e);
}
}
Query Filter
- Query の第二引数を指定することで、Query 結果にフィルタかけることができる
- Query Filter で指定した Component は Query の要素には含まれない
- 複数の Query Filter をタプルでくくることで、AND 条件でフィルタリングができる
- 複数の Query Filter を
Or<(...)>
でくくることで、OR 条件でフィルタリングができる - Query Filter には下記のような種類がある
-
With<T>
Without<T>
Or<(...)>
Added<T>
Changed<T>
-
fn my_system(
q_a_wc: Query<&CompA, With<CompC>>, // A with C
q_a_woc: Query<&CompA, Without<CompC>>, // A w/o C
q_ac_wd: Query<(&CompA, &CompC), With<CompD>>, // A + C with D
q_a_wc_wod: Query<&CompA, (With<CompC>, Without<CompD>)>, // A with C && w/o D
q_a_wc_wd: Query<&CompA, Or<(With<CompC>, With<CompD>)>>, // A with C || with D
) {
// ...
}
Query Sets
- 同じ Component を複数のフィルタでミュータブルに Query することは出来ない
-
QuerySet
とQueryState
を使えば、4 つまで同じ型をミュータブルに Query 可能になる -
q0()
q1()
のようにして、各 Query にアクセスすることができる
/// NG: ミュータブルな A の Query が複数存在する
fn my_system(
mut q_a_wb: Query<&mut CompA, With<CompB>>, // A with B (Mutable)
mut q_a_wc: Query<&mut CompA, With<CompC>>, // A with C (Mutable)
) {
// ...
}
/// OK
fn my_system(
mut q: QuerySet<(
QueryState<&mut CompA, With<CompB>>, // A with B (Mutable)
QueryState<&mut CompA, With<CompC>>, // A with C (Mutable)
)>,
) {
for mut a in q.q0().iter_mut() {
// QueryState<&mut CompA, With<CompB>> にアクセス
}
for mut a in q.q1().iter_mut() {
// QueryState<&mut CompA, With<CompC>> にアクセス
}
}
その他の Query の機能
Query 結果を総当たりでイテレートする
- 下記を使用すれば、その Query の複数個の組み合わせを総当たりできる
-
iter_combinations()
iter_combinations_mut()
でイテレータを生成 -
iter.fetch_next()
で次の組を取得する (Option
)
fn add_two_comp(query: Query<&MyComp>) {
let mut iter = query.iter_combinations();
while let Some([MyComp(c1), MyComp(c2)]) = iter.fetch_next() {
println!("{} + {} = {}", c1, c2, c1 + c2);
}
}
0 + 1 = 1
0 + 2 = 2
0 + 3 = 3
1 + 2 = 3
1 + 3 = 4
2 + 3 = 5
Parallel Iterator
-
ParallelIterator
は重い Query の処理を並列で実行するイテレータ -
ParallelIterator
を使用するには、ComputeTaskPool
が必要 -
par_for_each()
par_for_each_mut()
を Batch Size を指定して使用する -
ParallelIterator
は遅くなる可能性もあり、Profiling して採用を検討すべき-
ParallelIterator
はオーバーヘッドが大きい - 負荷の軽い処理や Batch Size が小さすぎる場合には遅くなる
-
use bevy::tasks::ComputeTaskPool; // 要 import
/// parallel_iterator を使用するには、ComputeTaskPool が必要
fn add_one(pool: Res<ComputeTaskPool>, mut query: Query<&mut MyComp>) {
const BATCH_SIZE: usize = 100;
query.par_for_each_mut(&pool, BATCH_SIZE, |mut my_comp| {
my_comp.0 += 1;
});
}
Resource
- Resource は App で Global に共有される、唯一な存在のデータである (シングルトン)
参考: DefaultPlugins を入れた状態で登録されている Resource 一覧 (Windows)
ActiveCameras
AmbientLight
Arc<Queue>
AssetServer
AssetServerSettings
Assets<AudioSource>
Assets<ColorMaterial>
Assets<DynamicScene>
Assets<Font>
Assets<FontAtlasSet>
Assets<Gltf>
Assets<GltfMesh>
Assets<GltfNode>
Assets<GltfPrimitive>
Assets<Image>
Assets<Mesh>
Assets<Scene>
Assets<Shader>
Assets<StandardMaterial>
Assets<TextureAtlas>
AsyncComputeTaskPool
Audio
AudioOutput
Axis<GamepadAxis>
Axis<GamepadButton>
ClearColor
ComputeTaskPool
Diagnostics
DirectionalLightShadowMap
EventLoop<()>
EventLoopProxy<()>
Events<AppExit>
Events<AssetEvent<AudioSource>>
Events<AssetEvent<ColorMaterial>>
Events<AssetEvent<DynamicScene>>
Events<AssetEvent<Font>>
Events<AssetEvent<FontAtlasSet>>
Events<AssetEvent<Gltf>>
Events<AssetEvent<GltfMesh>>
Events<AssetEvent<GltfNode>>
Events<AssetEvent<GltfPrimitive>>
Events<AssetEvent<Image>>
Events<AssetEvent<Mesh>>
Events<AssetEvent<Scene>>
Events<AssetEvent<Shader>>
Events<AssetEvent<StandardMaterial>>
Events<AssetEvent<TextureAtlas>>
Events<CloseWindow>
Events<CreateWindow>
Events<CursorEntered>
Events<CursorLeft>
Events<CursorMoved>
Events<FileDragAndDrop>
Events<GamepadEvent>
Events<GamepadEventRaw>
Events<KeyboardInput>
Events<MouseButtonInput>
Events<MouseMotion>
Events<MouseWheel>
Events<ReceivedCharacter>
Events<TouchInput>
Events<WindowBackendScaleFactorChanged>
Events<WindowCloseRequested>
Events<WindowCreated>
Events<WindowFocused>
Events<WindowMoved>
Events<WindowResized>
Events<WindowScaleFactorChanged>
FixedTimesteps
FlexSurface
GamepadSettings
Gamepads
Gilrs
Input<GamepadButton>
Input<KeyCode>
Input<MouseButton>
IoTaskPool
LogSettings
Msaa
PointLightShadowMap
RenderDevice
SceneSpawner
ScratchRenderWorld
TextPipeline<Entity>
Time
Touches
TypeRegistryArc
VisiblePointLights
WgpuOptions
Windows
WinitWindows
Resource の追加と削除
- Resource の追加には 3 通りの方法がある
- App 初期化時に
insert_resource()
でインスタンスを追加 - App 初期化時に
init_resource::<T>()
で初期化して追加 (要Default
orFromWorld
) - System 内で Command を使って追加 (
insert_resource()
のみ)
- App 初期化時に
- Resource を削除するには System 内で Command を使い
remove_resource::<T>()
する - 既存の Resource をもう一度追加しようとすると、新たな値によって上書きされる
fn main() {
App::new()
// ...
.init_resource::<MyResourceA>() // Default or FromWorld で初期化されて追加
.insert_resource(MyResourceB(5)) // 手動で初期化して追加
// ...
.run();
}
fn my_system(mut commands: Commands) {
commands.insert_resource(MyResource(3));
commands.remove_resource::<MyResource>();
}
init_resource::<T>()
)
Resource の初期化 (- シンプルな初期化であれば
Default
を実装するだけで十分 -
FromWorld
を実装すれば、World の全要素にアクセスしつつ複雑な初期化をすることが可能
/// Default で初期化される Resource
#[derive(Default)]
struct MyFirstCounter(usize);
/// FromWorld で初期化される Resource
struct MySecondCounter(usize);
impl FromWorld for MySecondCounter {
// ここでは ECS World のすべての要素にアクセスすることが可能
fn from_world(world: &mut World) -> Self {
let count = world.get_resource::<MyFirstCounter>().unwrap();
MySecondCounter(count.0) // MyFirstCounter の値で初期化
}
}
/// 初期化できない Resource
struct MyThreshold(usize);
System から Resource へのアクセス
- System からは
Res<T>
ResMut<T>
を使って Resource にアクセスできる -
Option
でくるめば、存在しないかもしれない (今後追加/削除される) Resource を扱える
/// System
fn count_up(
thresh: Res<MyThreshold>, // イミュータブルに参照
mut first_counter: ResMut<MyFirstCounter>, // ミュータブルに参照
second_counter: Option<ResMut<MySecondCounter>>, // 存在するかわからない Resource
) {
if let Some(mut second_counter) = second_counter {
// MySecondCounter が存在したら何かする
}
}
Time
Resource
- Bevy が提供する
Time
Resource を 使うと、ゲーム内の時間を追跡できる - Bevy が提供する
Timer
型を使えば、特定の時間が経過したかを検出できる
use bevy::prelude::*;
#[derive(Component)]
struct MyTimer(Timer);
fn setup(mut commands: Commands) {
// MyTimer を 2.0 秒ごとに繰り返すように設定
commands.spawn().insert(MyTimer(Timer::from_seconds(2.0, true)));
}
// MyTimer に経過時間を Time Resource を使って適用し、指定時間が経過したかをチェックする
fn print_timer(time: Res<Time>, mut query: Query<&mut MyTimer>) {
let mut my_timer = query.single_mut();
if my_timer.0.tick(time.delta()).just_finished() {
println!("Tick");
}
}
fn main() {
App::new()
.add_plugins(MinimalPlugins)
.add_startup_system(setup)
.add_system(print_timer)
.run();
}
Local Resource
-
Local<T>
は各 System 内でのみ使用できる、可変な Local Resource - 同じ型
Local<T>
でも複数の System で共有されず、各 System で別のインスタンスとなる - 自動的に初期化されるため、
Default
かFromWorld
を実装している必要がある - System を App に追加する際に、個別に初期化することも可能
use bevy::prelude::*;
/// Local<T> には Default の実装が必要
#[derive(Default)]
struct MyCounter(usize);
fn count_up_1(mut counter: Local<MyCounter>) {
// ここの counter と
}
fn count_up_2(mut counter: Local<MyCounter>) {
// ここの counter は別物
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_system(count_up_1)
// .config() を使って手動で Local<MyCounter> を初期化する
.add_system(count_up_2.config(|params| {
params.0 = Some(MyCounter(0)); // 0 番目の引数を初期化したいので params.0
}))
.run();
}
Commands
- Commands を使うことで、下記のようなことができる
- Resource の追加・削除 (
insert_resource()
,remove_resource::<T>()
) - Entity (
EntityCommands
) の追加 (spawn()
,spawn_bundle()
,spawn_batch()
) - Entity (
EntityCommands
) の取得 (entity()
)
- Resource の追加・削除 (
- Entity (
EntityCommands
) を上記で取得し、Component/Bundle 等の追加・削除を行う-
id()
で Entity を取得 -
insert()
insert_bundle()
で Entity へ Component を追加 -
remove::<T>()
remove_bundle::<T>()
で Entity から Component を削除 -
despawn()
despawn_recursive()
で Entity を削除
-
- Commands は即時に適用はされずキューに積まれ、次の Stage への遷移直前に実行される
fn setup(mut commands: Commands) {
// Entity の生成と Component/Bundle の追加
let a = commands
.spawn() // Entity (EntityCommands) を生成
.insert(CompA) // Component を追加
.id(); // id() で Entity を取得できる
let ab = commands
.spawn_bundle((CompA, CompB)) // Bundle を持った Entity を生成
.id();
let abc = commands
.spawn()
.insert(CompA)
.insert_bundle((CompB, CompC)) // Bundle を追加 (タプルは Bundle)
.id();
// spawn_batch() で Bundle へのイテレータから複数の Entity を一度に生成できる
commands.spawn_batch(vec![
(CompA, CompB, CompC),
(CompA, CompB, CompC),
(CompA, CompB, CompC),
]);
// Entity の Component を削除
commands.entity(abc).remove::<CompB>();
// Entity の削除
commands.entity(a).despawn();
commands.entity(ab).despawn();
commands.entity(abc).despawn();
// Resource の追加と削除
commands.insert_resource(MyResource);
commands.remove_resource::<MyResource>();
}
Event
EventWriter<T>
EventReader<T>
を介して、任意のデータを System 間で送受信できる- Event はブロードキャストされ、受信側 System は独立して同じ Event を同時に処理する
- Event は App の作成時に
add_event::<T>()
で追加する必要がある
use bevy::prelude::*;
/// 送受信する Event
/// 内部に送受信したい様々なデータをもたせる
struct MyEvent(String);
/// Event を送信する System
/// EventWriter を使って MyEvent を送信する
fn event_write(mut event_writer: EventWriter<MyEvent>) {
event_writer.send(MyEvent("Hello, event!".to_string()))
}
/// Event を受信する System
/// EventReader のイテレータを使って受信 Event を処理する
fn event_read(mut event_reader: EventReader<MyEvent>) {
for e in event_reader.iter() {
let msg = &e.0;
println!("received event: {}", msg);
}
}
fn main() {
App::new()
.add_event::<MyEvent>() // Event を App に登録する必要がある
.add_system(event_write)
.add_system(event_read)
.run();
}
Plugin
- Plugin は App をモジュラーにリファクタリングする際に有用
- Plugin は App に追加する要素をひとまとまりにすることができる
- App を
module
に分割して Plugin を適用すれば、module
内で設計を完結できる- App に必要なすべての型を
pub
として外部に公開する必要がなくなる
- App に必要なすべての型を
- Plugin の有用な使用例としては、下記のような例が挙げられる
- Physics や外部入力など、様々なサブシステムを Plugin としてまとめる
- Bevy の追加機能を自作して crate として公開する
- 異なる State に対してそれぞれ別の Plugin を実装する
- Plugin を使用するには、
Plugin
トレイトを実装する必要がある
use bevy::prelude::*;
/// Plugin にまとめたい様々な要素
struct MyResource;
struct MyEvent;
/// 自作の Plugin
struct MyPlugin;
/// 自作の Plugin に Plugin トレイトを実装すれば、Plugin として使用できる
/// Plugin トレイトでは App Builder に必要な要素を追加するだけで良い
impl Plugin for MyPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(MyResource)
.add_event::<MyEvent>()
.add_startup_system(plugin_setup)
.add_system(plugin_system);
}
}
/// Plugin にまとめたい startup_system
fn plugin_setup() {
println!("MyPlugin's setup");
}
/// Plugin にまとめたい system
fn plugin_system() {
println!("MyPlugin's system");
}
fn main() {
App::new()
.add_plugin(MyPlugin) // Plugin を追加する
.run();
}
Plugin Group
- Plugin Group は複数の Plugin を一度に登録することができる
-
DefaultPlugins
MinimalPlugins
なども Plugin Group として定義されている - Plugin Group を使用するには、
PluginGroup
トレイトを実装する必要がある
use bevy::app::PluginGroupBuilder; // PluginGroup トレイトを実装するには追加が必要
use bevy::prelude::*;
/// 自作の Plugin
struct FooPlugin;
struct BarPlugin;
impl Plugin for FooPlugin {}
impl Plugin for BarPlugin {}
/// 自作の Plugin Group
struct MyPluginGroup;
impl PluginGroup for MyPluginGroup {
fn build(&mut self, group: &mut PluginGroupBuilder) {
group.add(FooPlugin).add(BarPlugin); // Plugin を group に追加
}
}
fn main() {
App::new()
.add_plugins(MyPluginGroup) // PluginGroup を追加
.run();
}
-
DefaultPlugins
MinimalPlugins
では、下記の詳細に示すような様々な Plugin が導入される -
DefaultPlugins
はゲーム制作に必要な一通りの Plugin が導入される -
MinimalPlugins
は Window のない Headless App を構成する最低限の Plugin が導入される-
DefaultPlugins
内のWinitPlugin
がないと App は一度きりしか動かない -
MinimalPlugins
ではScheduleRunnerPlugin
が代わりに Game Loop を構成する
-
`DefaultPlugins` `MinimalPlugins` で導入される Plugin の一覧
pub struct DefaultPlugins;
impl PluginGroup for DefaultPlugins {
fn build(&mut self, group: &mut PluginGroupBuilder) {
group.add(bevy_log::LogPlugin::default());
group.add(bevy_core::CorePlugin::default());
group.add(bevy_transform::TransformPlugin::default());
group.add(bevy_diagnostic::DiagnosticsPlugin::default());
group.add(bevy_input::InputPlugin::default());
group.add(bevy_window::WindowPlugin::default());
group.add(bevy_asset::AssetPlugin::default());
group.add(bevy_scene::ScenePlugin::default());
#[cfg(feature = "bevy_render")]
group.add(bevy_render::RenderPlugin::default());
#[cfg(feature = "bevy_sprite")]
group.add(bevy_sprite::SpritePlugin::default());
#[cfg(feature = "bevy_pbr")]
group.add(bevy_pbr::PbrPlugin::default());
#[cfg(feature = "bevy_ui")]
group.add(bevy_ui::UiPlugin::default());
#[cfg(feature = "bevy_text")]
group.add(bevy_text::TextPlugin::default());
#[cfg(feature = "bevy_audio")]
group.add(bevy_audio::AudioPlugin::default());
#[cfg(feature = "bevy_gilrs")]
group.add(bevy_gilrs::GilrsPlugin::default());
#[cfg(feature = "bevy_gltf")]
group.add(bevy_gltf::GltfPlugin::default());
#[cfg(feature = "bevy_winit")]
group.add(bevy_winit::WinitPlugin::default());
#[cfg(feature = "bevy_wgpu")]
group.add(bevy_wgpu::WgpuPlugin::default());
}
}
pub struct MinimalPlugins;
impl PluginGroup for MinimalPlugins {
fn build(&mut self, group: &mut PluginGroupBuilder) {
group.add(bevy_core::CorePlugin::default());
group.add(bevy_app::ScheduleRunnerPlugin::default());
}
}
Plugin Group の中の Plugin を一部無効化する
- Plugin Group を使用する場合は、その中の一部の Plugin を無効化できる
- 無効化されていてる Plugin でも、ビルドはされる
App::new()
.add_plugins_with(DefaultPlugins, |plugins| {
plugins
.disable::<AudioPlugin>() // AudioPlugin を無効化
.disable::<LogPlugin>() // LogPlugin を無効化
})
.run();
Bevy の Plugin を crate.io に公開する
- Bevy の crate を公開する際にも Plugin/Plugin Group として公開するのが良い
- Plugin を公開する際には、下記のような情報を参考に
State
- State は App の実行時の流れを構造化し、制御することができる機能
- メニュースクリーンやロードスクリーン
- ゲームの Pause/Resume
- 異なるゲームモード 等
State と System Set の登録
- State を使用するには下記を実施する必要がある
- State を定義する
enum
にはDebug
Clone
PartialEq
Eq
Hash
の実装が必要 -
add_state()
で初期値の State を App Builder へ登録 - State 専用の System Set を登録 (各 State に対して複数追加できる)
- State を定義する
- State の様々なタイミングのみで動作する System を追加することができる
- 通常の State で使用するもの
-
on_enter
: State 開始時に一度だけ呼ばれる -
on_update
: State の動作中に毎フレーム呼ばれる -
on_exit
: State の終了時 (遷移時) に一度だけ呼ばれる
-
- State Stack に関連するもの (後述)
-
on_pause
: State が Inactive になる際に一度だけ呼ばれる -
on_in_stack_update
: State が Inactive でも Active でも毎フレーム呼ばれる -
on_inactive_update
: State が Inactive なときだけ毎フレーム呼ばれる -
on_resume
: State が Active に戻る際に一度だけ呼ばれる
-
- 通常の State で使用するもの
/// State は enum として定義する (Debug, Clone, PartialEq, Eq, Hash が必要)
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum AppState { Menu, Game, End }
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// State の初期値を登録して State を有効化する
.add_state(AppState::Menu)
// State に関係なく動作する System
.add_system(count_frame)
// AppState::Menu に入ったときのみ動作する System Set
.add_system_set(
SystemSet::on_enter(AppState::Menu).with_system(menu_on_enter)
)
// AppState::Game で毎フレーム動作する System Set
.add_system_set(
SystemSet::on_update(AppState::Game).with_system(game_on_update)
)
// AppState::End を終了するときのみ動作する System Set
.add_system_set(
SystemSet::on_exit(AppState::End).with_system(end_on_exit)
)
.run();
}
System から State へのアクセス
- System 内で State へアクセスする場合
Res<State<T>>
ResMut<State<T>>
を使う-
current()
で現在の State を取得できる -
set()
で State を変更できるが、下記の場合は失敗する- 既にその State になっている場合
- 他の State への変更が既にキューされている場合
-
/// System 内で State にアクセスするためには `State<T>` Resource を使う
fn count_frame(app_state: Res<State<AppState>>) {
// current() で現在の State が取得できる
match app_state.current() {
// ...
}
}
/// System 内で State にアクセスするためには `State<T>` Resource を使う
fn menu_on_enter(mut app_state: ResMut<State<AppState>>) {
app_state.set(AppState::Game).unwrap(); // Menu から Game へ Statを を変更
}
State Stack
- State を完全に移行せずにスタックさせることも可能 (Pause・メニュー画面などに使える)
-
State<T>
のpush()
/pop()
メソッドを使って State Stack を利用できる - State がバックグラウンドになった Inactive 時に動作する System を登録することもできる
fn push_to_state_stack(mut app_state: ResMut<State<AppState>>) {
// push() で State Stack に State を積む
app_state.push(AppState::Paused).unwrap();
}
fn pop_from_state_stack(mut app_state: ResMut<State<AppState>>) {
// pop() で State Stack から以前の状態に戻す
app_state.pop().unwrap();
}
// ...
fn main() {
App::new()
// ...
// AppState::Game が Inactive になる際に一度だけ呼ばれる
.add_system_set(
SystemSet::on_pause(AppState::Game)
.with_system(game_on_pause)
)
// AppState::Game が Inactive でも Active でも毎フレーム呼ばれる
// ただし Bevy 0.6.0 にはバグがあり、on_update() と同じ動作しかしてくれない
// https://github.com/bevyengine/bevy/issues/3179
.add_system_set(
SystemSet::on_in_stack_update(AppState::Game)
.with_system(game_on_in_stack_update),
)
// AppState::Game が Inactive なときだけ毎フレーム呼ばれる
.add_system_set(
SystemSet::on_inactive_update(AppState::Game)
.with_system(game_on_inactive_update),
)
// AppState::Game が Active に戻る際に一度だけ呼ばれる
.add_system_set(
SystemSet::on_resume(AppState::Game)
.with_system(game_on_resume)
)
// ...
.run();
}
-
Input<T>
をトリガとして State を変更する場合、input は自分でクリアする必要がある - 詳細はこちらの issue を参照
fn esc_to_menu(
mut keys: ResMut<Input<KeyCode>>,
mut app_state: ResMut<State<AppState>>,
) {
if keys.just_pressed(KeyCode::Escape) {
app_state.set(AppState::MainMenu).unwrap();
keys.reset(KeyCode::Escape); // you should clear input by yourself
}
}
Run Criteria
- 特定の System を実行するか否かを Run Criteria と呼ばれる関数で実行時に判定する機能
- 任意の条件が満たされた場合のみ動作する System, System Set, Stage を追加できる
- Run Criteria は低レベルな機能で、State 等で制御しきれない場合に使用する
Run Criteria を適用した System
- Run Criteria は
enum ShouldRun
を返す System として定義する - Run Criteria は通常の System と同様、任意のパラメータを引数にできる
use bevy::ecs::schedule::ShouldRun; // Run Criteria を使用するには追加が必要
use bevy::prelude::*;
/// count が 100 より大きくなったときのみ ShouldRun::Yes を返す Run Criteria
fn my_run_criteria(mut count: Local<usize>) -> ShouldRun {
if *count > 100 {
*count = 0;
ShouldRun::Yes
} else {
*count += 1;
ShouldRun::No
}
}
/// my_run_criteria を適用する System
fn my_system() {
println!("Hello, run criteria!");
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// with_run_criteria() で Run Criteria を System に適用する
.add_system(my_system.with_run_criteria(my_run_criteria))
.run();
}
Run Criteria Label
- 同じ Run Criteria を共有する System/System Set がある場合、Label の使用が推奨される
- Label 付き Run Criteria は各フレームで一度だけ実行され、結果が全 System で共有される
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_system(
// my_run_criteria に Label をつけておく
my_system.with_run_criteria(my_run_criteria.label("MyRunCriteria")),
)
// with_run_criteria に Label を指定すれば、その結果が使い回される
.add_system_set(
SystemSet::new()
.with_run_criteria("MyRunCriteria")
.with_system(my_system_a)
.with_system(my_system_b),
)
.run();
}
Label
- Label は System, Run Criteria, Stage, Ambiguity Sets に名前をつけるために使用する
- Label には文字列はもちろん、独自の型も使用可能
- 文字列ではなく独自の型を Label とすることで、型チェックや補完などの恩恵が得られる
- 独自の Label 型には
Clone + Eq + Hash + Debug (+ Send + Sync + 'static)
の実装が必要 - 独自の Label 型には、用途に応じて下記のいずれかのトレイトの実装が必要
StageLabel
SystemLabel
RunCriteriaLabel
AmbiguitySetLabel
/// 独自の Label 型を定義する
#[derive(Debug, Clone, PartialEq, Eq, Hash, SystemLabel)]
struct MySecondSystemLabel;
// ...
fn main() {
App::new()
// 独自の Label 型は文字列の Label と同様に使用できる
.add_system(second.label(MySecondSystemLabel))
.add_system(first.before(MySecondSystemLabel))
.add_system(third.after(MySecondSystemLabel))
.run();
}
Stage
- Bevy の 1 フレームは、デフォルトで 5 つの Stage (
CoreStage
) から構成されている - 下記の Stage が上から順に実行され、各 Stage で各々に登録された System が実行される
CoreStage::First
CoreStage::PreUpdate
-
CoreStage::Update
: ユーザが追加した System はデフォルトでここに登録される CoreStage::PostUpdate
CoreStage::Last
- 次の Stage への移行時には、その Stage の全 System が実行済みであることが保証される
- 各 Stage の System 内での Command は、全 System の実行完了後に実行される
- 各 Stage 内の System は、デフォルトで可能な限り並列で実行される
独自 Stage を利用すべき場合
- 各 Stage のすべての System が実行された後、同じフレームで System を実行したい
- 各 Stage のすべての Command が実行された後、同じフレームで System を実行したい
独自の Stage の使用方法
- 独自の Stage とその Stage での System は下記のように追加することができる
- 独自の Stage に Label をつけて登録する (Stage の場所は様々ある)
- Stage の動作は
single_threaded()
かparallel()
かが指定できる -
add_system_to_stage
で Stage Label を指定して System を追加する
use bevy::prelude::*;
/// 独自の Stage Label を定義する
#[derive(Debug, Clone, PartialEq, Eq, Hash, StageLabel)]
struct MyStage;
// ...
fn main() {
App::new()
// 独自の Stage を Label をつけて登録する (SystemStage::parallel() も使用可能)
.add_stage_after(CoreStage::Update, MyStage, SystemStage::single_threaded())
// CoreStage::Update で実行される System を登録
.add_system(first)
// MyStage に System を登録
.add_system_to_stage(MyStage, second)
.add_system_to_stage(MyStage, third)
.run();
}
Change Detection
Component Change Detection
- Query Filter を使うことで、Component データの追加・変更を簡単に検知することができる
-
Added<T>
: Component の新しいインスタンスの生成を検知する- Component を持った新しい Entity が生成されたとき
- 既存の Entity に Component が追加されたとき
-
Changed<T>
: Component のインスタンスに変更を検知する- Component を持った新しい Entity が生成されたとき (
Added<T>
と同じ) - Component がミュータブルアクセスされた場合
-
DerefMut
によるため、ミュータブルに Query しただけでは起きない -
DerefMut
によるため、実際に値が変更されなくても検知される
-
- Component を持った新しい Entity が生成されたとき (
-
-
ChangeTrackers<T>
Component を Query することで、フィルタリングせずに検知できる
/// Added<T> を Query Filter として、追加された Component を Query
fn print_added(query: Query<(Entity, &Counter), Added<Counter>>) {
for (entity, counter) in query.iter() {
println!("Added counter {}, count = {}", entity.id(), counter.0);
}
}
/// Changed<T> を Query Filter として、変更された Component を Query
fn print_changed(query: Query<(Entity, &Counter), Changed<Counter>>) {
for (entity, counter) in query.iter() {
println!("Changed counter entity {} to {}", entity.id(), counter.0);
}
}
/// ChangeTrackers<T> を Query すれば、フィルタリングせずに追加・変更を検知できる
fn print_tracker(query: Query<(Entity, &Counter, ChangeTrackers<Counter>)>) {
for (entity, counter, trackers) in query.iter() {
if trackers.is_added() {
println!("Tracker detected addition {}, {}", entity.id(), counter.0);
}
if trackers.is_changed() {
println!("Tracker detected change {} to {}", entity.id(), counter.0);
}
}
}
Resource Change Detection
- Resource の追加・変更の検知は、
Res
/ResMut
のメソッドによって検知できる-
is_added()
: Resource が追加されたかどうか -
is_changed()
: Resource が変更されたかどうか
-
/// 存在するかわからない Resource の追加と変更を検知する
fn print_added_changed(counter: Option<Res<Counter>>) {
if let Some(counter) = counter {
// Counter が追加されたときに実行される
if counter.is_added() {
println!("Counter has added");
}
// Counter が変更されたときに実行される
if counter.is_changed() {
println!("Counter has change to {}", counter.0);
}
} else {
println!("Counter has not found");
}
}
Removal Detection
Component Removal Detection
- Component の削除の検知は、Stage をまたいだ処理をすることで可能
- Command を使用した Component の削除は各 Stage の最後に行われるため
-
RemovedComponents<T>
が引数の System を後段の Stage に登録し、イテレートすれば良い
/// CoreStage::PostUpdate で MyComponent の削除を検知する
/// RemovedComponents<T> でこれまでに削除された Component を検出できる
fn detect_removals(removals: RemovedComponents<MyComponent>) {
for entity in removals.iter() {
println!("MyComponent Entity {} has removed", entity.id());
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
// Component を追加する
.add_startup_system(add_components)
// CoreStage::Update で MyComponent が存在するときは削除する
.add_system(remove_components_if_exist)
// CoreStage::PostUpdate で MyComponent の削除を検知する
.add_system_to_stage(CoreStage::PostUpdate, detect_removals)
.run();
}
Resource Removal Detection
- Bevy から Resource の削除を検知するための API は提供されない
-
Option
で Resource を包み、Local<T>
で前フレームでの有無を保持すれば検知できる - この仕組み上、Resource の削除の検知は必ず 1 フレーム遅れる
/// 前フレームで Resource が存在したかを保存することで、次のフレームで削除を検知する
fn detect_removals(
my_resource: Option<Res<MyResource>>, // 存在するかわからないので Option
mut my_resource_existed: Local<bool>, // 前フレームでの有無をローカルに保存
) {
if let Some(_) = my_resource {
println!("MyResource exists");
*my_resource_existed = true;
} else if *my_resource_existed {
println!("MyResource has removed");
*my_resource_existed = false;
}
}
Hierarchical Entity (Parent/Child)
- Command を使用することで、Entity の親子関係をつくることができる (子は 8 個まで)
- 主な利用方法は、ゲーム内の
Transform
に親子関係を作ること (ここでは言及しない)- 自作オブジェクトに親子関係を持たせても、そのままでは移動に親子関係を持たない
-
GlobalTransform
Transform
の両方を親子に追加すれば、移動に親子関係ができる
Parent/Child Entity の構築
- Entity に親子関係を作る方法は、下記の 2 通りがある
-
spawn()
時にwith_children()
で Child をつくる - Entity を取得して
push_children()
する
-
#[derive(Component)]
struct MyParent(String);
#[derive(Component)]
struct MyChild(String);
fn setup(mut commands: Commands) {
let parent = commands
.spawn()
.insert(MyParent("MyParent".to_string())) // Entity を MyParent とし、
.with_children(|parent| {
// Child として MyChild を生成
parent.spawn().insert(MyChild("MyChild1".to_string()));
})
.id();
// 別途 MyChild Entity を生成し
let child = commands
.spawn()
.insert(MyChild("MyChild2".to_string()))
.id();
// MyParent の Entity ID を使って MyChild を Child として追加
commands.entity(parent).push_children(&[child]);
}
Parent/Child Entity へのアクセス
非常に紛らわしいが、親子関係を作ることで下記のように Component が付与される。
- Child となった Entity には
Parent
Component が付与される - Parent となった Entity には
Children
Component が付与される
そのため、それぞれを Query するためには下記のようにする。
- Parent Entity を Query するには
Query<&Children>
を使う - Child Entity を Query するには
Query<&Parent>
を使う
また、Parent
Children
Component には、それぞれの子・親を特定するため、
- 子が持つ
Parent
Component は親の Entity ID を.0
で参照できる - 親が持つ
Children
Component は子の Entity ID の集合をイテレートできる
これらを用いて、実際の Entity (下記の例では MyParent
と MyChild
) を取得するには、
- 親は
Parent
の Entity ID を使い、親MyParent
の Query から取得する - 子は
Children
内の Entity ID を使い、子MyChild
の Query から特定の子を取得する
/// Child Entity の Query から、それぞれの Parent Entity を取得する
fn find_parent_from_children(
q_child: Query<&Parent>, // Parent Component を持つ Child Entity を Query
q_my_parent: Query<&MyParent>, // MyParent を Query
) {
// Child Entity が持っている Parent Component をイテレート
for parent in q_child.iter() {
// Parent Component は Entity (ID) を 0 番目の要素として持つので、
// それを使って q_my_parent から MyParent を取得する
let p = q_my_parent.get(parent.0).unwrap();
println!("{}", p.0);
}
}
/// Parent Entity の Query から、Children Entity を取得する
fn find_children_from_parent(
q_parent: Query<&Children>, // Children Component を持つ Parent を Query
q_my_child: Query<&MyChild>, // MyChild を Query
) {
// Parent Entity が持っている Children Component をイテレート
for children in q_parent.iter() {
// Children Component は Child Entity ID の集合を持っているのでイテレート
for &child in children.iter() {
// Child Entity ID を使って、q_my_child から MyChild を取得する
let my_child = q_my_child.get(child).unwrap();
println!("{}", my_child.0);
}
}
}
Parent/Child Entity の破棄
- Command で親の Entity ID を指定し、子を含めて再帰的に despawn することができる
/// MyParent の Entity ID を Query し、それで MyParent を MyChild 含めて再帰的に破棄する
/// MyParent は Children を持っているので、それを Query Filter として使う
fn clean_up(mut commands: Commands, query: Query<Entity, With<Children>>) {
let e = query.single().unwrap(); // MyParent は唯一のはず
commands.entity(e).despawn_recursive(); // MyParent を MyChild 含めて再帰的に破棄
}
Custom Runner
- App を Update する Runner を独自に定義することができる
-
DefaultPlugins
MinimalPlugins
の Runner は上書きされるので注意
use bevy::prelude::*;
/// App を手動で Update する Runner をカスタマイズできる
fn my_runner(mut app: App) {
println!("my_runner!");
app.update();
}
fn hello_world() {
println!("Hello, world!");
}
fn main() {
App::new()
// Custom Runner を適用する
.set_runner(my_runner)
.add_system(hello_world)
.run();
}
System を指定した間隔で実行する
- System を指定した間隔で実行するには、いくつかの方法がある
-
FixedTimestep
の Run Criteria を使う (System ごとに指定) -
ScheduleRunnerPlugin
を使う (全 System)
-
Fixed Timesetp (Run Criteria)
- Bevy 組み込みの Run Criteria の一つに
FixedTimestep
がある - 指定した Time Step が経過した場合のみ System が実行される
use bevy::core::FixedTimestep; // 要 import
use bevy::prelude::*;
fn hello_world() {
println!("hello world");
}
fn main() {
App::new()
.add_plugins(MinimalPlugins)
.add_system_set(
SystemSet::new()
// FixedTimestep を Run Criteria として設定する
// Time Step (== Frame Duration) を 0.5 に設定 (2 FPS)
.with_run_criteria(FixedTimestep::step(0.5))
.with_system(hello_world),
)
.run();
}
ScheduleRunnerPlugin
(Runner)
-
ScheduledRunnerPlugin
で全 System を一定周期で実行できる -
MinimalPlugins
には同梱されている (winit
の代わりの Runner) - Runner がコンフリクトするため
DefaultPlugin
(winit
) とは共存できない
/// 要 import
use bevy::app::{ScheduleRunnerPlugin, ScheduleRunnerSettings};
fn main() {
App::new()
// 1 秒ごとに System が実行されるように設定し、Plugin を導入
.insert_resource(ScheduleRunnerSettings::run_loop(Duration::from_secs_f64(
1.0,
)))
.add_plugin(ScheduleRunnerPlugin::default())
.add_system(hello_world)
.run();
}
Exclusive System (Direct World/ECS Access)
-
exclusive_system()
を使用することで、System から直接 World にアクセスが可能 -
&mut world: World
を引数とする System を作り、exclusive_system()
として登録する - Exclusive System は App のスレッドで動作するスレッドローカルな System となる
- Exclusive System 実行中は他の System の並行動作が止まるので使用を控えるのが望ましい
-
World
を使えばあらゆることが可能だが、詳細は docs を参照
/// Exclusive System を使うことで、App 内の World のすべての要素にアクセスできる
/// https://docs.rs/bevy/latest/bevy/ecs/world/struct.World.html
fn my_exclusive_system(world: &mut World) {
println!("Here is my exclusive system");
world.insert_resource(MyResource);
world.spawn().insert(MyComponent);
}
fn main() {
App::new()
.add_system(my_exclusive_system.exclusive_system())
.run();
}
Thread Pool Resource
-
DefaultTaskPoolOptions::with_num_threads(n)
でスレッドプールの数を変更できる - 上記データを Resource として追加する
/// スレッドプールの数を指定することができる
fn main() {
App::new()
.insert_resource(DefaultTaskPoolOptions::with_num_threads(4))
.add_plugins(DefaultPlugins)
.run();
}
Ambiguity Detection of System Execution Order
- System は並列で実行されるため、登録順と実行順が前後する可能性がある
- 同じデータを扱う System を順序指定なしで実行すれば、出力は状況によって変わり得る
- 下記 Plugin を使用 System 順と実行順の入れ替りを検知してログを出力できる
ReportExecutionOrderAmbiguities
-
LogPlugin
(DefaultPlugin
に同梱)
- これは確実な検知ではないため、あくまで参考情報として使用する
App.new()
// ...
.add_plugin(LogPlugin::default()) // DefaultPlugin には同梱
.insert_resource(ReportExecutionOrderAmbiguities)
.run();
Execution order ambiguities detected, you might want to add an explicit dependency relation between some of these systems:
* Parallel systems:
-- "&app::increment_counter" and "&app::print_counter"
conflicts: ["Counter"]
Writing Tests for Systems
- System のテストは Rust 標準の
cargo test
を使用して実施できる - テスト内で World を自分で作り、様々な要素を追加し、実行すれば良い
- Entity や Resource を生成する
- Stage をつくって System を登録する
- System を実行し、結果を取得して確認する
//! cargo test を使って、System のテストを実行することが可能
//! cargo test --test test_system
use bevy::prelude::*;
struct Counter(usize);
/// テストされる System
fn count_up(mut query: Query<&mut Counter>) {
for mut counter in query.iter_mut() {
counter.0 += 1;
}
}
#[test]
fn has_counted_up() {
// World を自分で作り、Counter を持った Entity を生成する
let mut world = World::default();
let entity = world.spawn().insert(Counter(0)).id();
// Stage を自分で作り、そこにテストすべき System を追加して実行する
let mut update_stage = SystemStage::parallel();
update_stage.add_system(count_up);
update_stage.run(&mut world);
// Component を取得して結果をテストする
let count = world.get::<Counter>(entity).unwrap();
assert_eq!(count.0, 1);
}
References
Discussion
いい記事ありがとうございます!!!
bevyいじっているときに、このあたりの基本的なコンポーネントの説明がなくて困っていたので助かりました!