Bevyを楽しく書くために、最低限必要なRustのTips 3選

概要

Bevy Engineを初めて使う人にとって、Rust言語特有の壁があります。今回は特にハマりがちな点を3つ挙げてご紹介します。

前置き

Bevy Engineは、ゲームエンジンと紹介されることが多いですが、個人的にはopenFrameworksなどと同様に、汎用的な開発フレームワークと捉えています。

Bevy Engineはとてもよく設計されていて、幅広い用途に活用できるのですが、良くも悪くも現状はRust言語専用に作られていて、Rust言語以外でも使えるようにしようとする動きはなくはないものの、現状ではRustで書かざるをえません。

Rustは、C++やJavaと比べても難易度は高く、Bevyは難易度を緩和する仕組みを設けてくれてはいますが、BevyとRustにはいくつかのハマりポイントがあります。

当記事では、特にRust言語特有の事情に焦点をあて、Bevyに初めて触れる際に、特に抑えておくべき、Tipsともいえるコツをいくつか挙げていきます。

1. 可変参照はスコープで1つだけ

所有権は、RustをRustたらしめているものでありながら、プログラマを最も苦しめるものでもあります。

特に、他のプログラミング言語ではほとんど意識することがない事柄を意識せねばならず、意外な箇所でエラーに阻まれ、思ったようなコードが書けないことがあります。

今回は特に、 「可変参照はスコープで1つだけ」 というルールを挙げておきます。下記のSlackOverflowのやり取りがわかりやすいので、コードを参考にして少し改変しながらみていきます。

以下のコードは、do_stuffの2回目でエラーになり、コンパイルが通りません。

fn do_stuff(n: &mut usize) {
    *n += 1;
}

fn main() {
    let mut v = vec![1, 2, 3, 4];
    let current1 = &mut v[1];
    do_stuff(current1);
    
    let current2 = &mut v[0];
    do_stuff(current2);
    
    do_stuff(current1); // error[E0499]: cannot borrow `v` as mutable more than once at a time
    
    println!("{:?}", v);
}
error[E0499]: cannot borrow `v` as mutable more than once at a time
  --> src/main.rs:10:25
   |
7  |     let current1 = &mut v[1];
   |                         - first mutable borrow occurs here
...
10 |     let current2 = &mut v[0];
   |                         ^ second mutable borrow occurs here
...
13 |     do_stuff(current1);
   |              -------- first borrow later used here

このケースの場合は、1回目の借用と2回目の借用で、スコープが被ってしまっているのが原因です。以下のように修正すればコンパイルが通るようになります。

fn do_stuff(n: &mut usize) {
    *n += 1;
}

fn main() {
    let mut v = vec![1, 2, 3, 4];
    {
        let current1 = &mut v[1];
        do_stuff(current1);
    }
    {
        let current2 = &mut v[0];
        do_stuff(current2);
    }
    {
        let current3 = &mut v[1];
        do_stuff(current3);
    }
    
    println!("{:?}", v); // [2, 4, 3, 4]
}

この、&mutを使いたいのにコンパイルが通らないというのが、Bevyを使っていて一番遭遇率が高いケースだと思いますので、これを知っているか否かでハマり時間がだいぶ変わると思います。

ちなみにどうしてもこれによりコンパイルが通らない場合、Bevyではシステムを分割することで対応できることが多くあります。

なおBevy固有の事項としては クエリ (Query) でも、同様の問題が発生することがあります。こちらはWithoutを利用することで回避できます。詳しくは下記記事を参照してください。

[Rust] Bevyのはまりどころ - Componentの同時アクセス

2. 再借用トリック: 可変参照と不変参照の共存

再び所有権まわりですが、先ほどの問題と似ていて次に多いエラーが、一度immutable (不変) で借用するとmutable (可変) で借用できないというものです。逆パターンのこともあります。

コード例:

#[derive(Component)]
struct MyThing {
    a: Foo,
    b: Bar,
}

fn my_system(mut q: Query<&mut MyThing>) {
    for thing in q.iter_mut() {
        helper_func(&thing.a, &mut thing.b); // ERROR!
    }
}

fn helper_func(foo: &Foo, bar: &mut Bar) {
    // do something
}

エラー内容:

error[E0502]: cannot borrow `thing` as mutable because it is also borrowed as immutable
  --> src/main.rs:14:36
   |
14 |         helper_func(&thing.a, &mut thing.b); // ERROR!
   |         -----------  -----         ^^^^^ mutable borrow occurs here
   |         |            |
   |         |            immutable borrow occurs here
   |         immutable borrow later used by call

error[E0596]: cannot borrow `thing` as mutable, as it is not declared as mutable
  --> src/main.rs:14:36
   |
14 |         helper_func(&thing.a, &mut thing.b); // ERROR!
   |                                    ^^^^^ cannot borrow as mutable
   |
help: consider changing this to be mutable
   |
13 |     for mut thing in q.iter_mut() {
   |         +++

ご丁寧に解決策のヒントまで書かれているのですが、このヒントでは実は解決しません。(Rustはこういうことが多いのです…。)

本当の解決策は先程の記事にかかれていて、再借用トリックを使います。

for thing in q.iter_mut() {
    let thing = thing.into_inner();
    // または
    // let thing = &mut *thing;
    helper_func(&thing.a, &mut thing.b);
}

このトラブルの原因は、Bevyが提供するRes<T>ResMut<T>などのスマートポインタに起因するものです。詳細は元記事解説がありますので、脚注にその翻訳だけ載せておきます 1

3. システムがどうしてもコンパイルを通らない

次に多いトラブルが、自作の関数をBevyのシステムとして add_systems() したいのにコンパイルを通せない、あるいは .run_if() の前の部分でエラーが出るというものです。

これはBevy固有の内容が多いので、上記記事の翻訳をそのまま載せておきます。

初心者にありがちなミス

ただし、エンティティーは特別なものであり、コンポーネントではないので、Query<Entity>は正しい。

上記には含まれていませんが、システムの引数が16個を超えると謎のエラーが出たりします。これについては別記事で解説しています。

また、ResMutにはしているもののmutを付け忘れるなんてこともよくあります。

この他、Rust固有ではないBevy Engineで抑えておくべきポイントについては、下記記事を参照してください。

まとめ: Tipsを挙げるとキリがないけれど…

今回はあくまで最低限のTipsを挙げるならという観点で、3つだけ厳選してみました。

これだけで避けられるエラーは多いはずですが、他にもライフタイム周りなど、Rustはハマりどころが本当に多いです。

特にライフタイムについてはBevyの恩恵を一番受けられるところだと思いますが、ライフタイム周りのエラーについて挙げるとキリがなく、かつケースバイケースのことが多いので、それについてはまた別記事でまとめる機会があればと思います。

  1. 「再借用トリック」解説文の機械翻訳:

    Bevy は通常、特別なラッパー型 ( Res<T>, ResMut<T>Mut<T>(コンポーネントをミュータブルに問い合わせる場合) ) を使ってデータにアクセスできます。

    これにより、Bevyはデータへのアクセスを追跡することができます。これらはRustのDeref特性を使用する「スマート・ポインタ」型です。通常はシームレスに動作するので、気づかないことも多いです。

    しかし、ある意味、コンパイラにとっては不透明です。Rust言語では、構造体に直接アクセスできる場合は、構造体のフィールドを個別に借用できますが、構造体が別の型にラップされている場合は、これが機能しません。

    上に示した再借用トリックは、効果的にラッパーを通常のRust参照に変換します。*thingDerefMutでラッパーを参照解除し、&mut がそれをミュータブルに借用します。これで、Mut<MyStuff> の代わりに&mut MyStuff を持つことになります。