Rustの習作としてゲームを作ろうとしていて、その際に状態をどうやって管理したらいいかにつまづいている。 なんかうまい方法や決まったイディオム・知見などがあるんじゃないかと思うんだけどうまく探せてないのか、把握してないのでどうしたらいいのかを考えていた。
例えば敵がプレイヤーの位置に向かってくる、とかいった場合にプレイヤーの座標を取得する必要があるんだけどどうするか。 問題としては
- 敵からプレイヤー情報をどうやって参照するか
に加えて、Rustでは
- 敵の動作のためミュータブル参照を取っている状態で、プレイヤー情報の参照が取れるのか
ということがあると思う。
敵からプレイヤー情報をどうやって参照するか
例えばC++やC#ではゲーム全体を管理する GameManager みたいなクラスをシングルトンにして、そいつに問い合わせる形にでもするかと思う。
あまり行儀がよいとはいえないが、まあ望みは実現できる。
Rustだとシングルトンをグローバル変数で実現することになるのではないかと思うけど、そうなるとシングルトンは扱いにくいんじゃないかと思う。
ということで、ひとまずまっとうに更新関数に引数として渡すことにしてみる。
GameManager がゲームの情報として Player と Enemy を保持しているとして:
pub struct GameManager { |
GameManager の update で Enemy の update に Player の参照を渡す:
impl GameManager { |
RefCellを使用する
一応上の方法で動かせるけど、敵の更新時にプレイヤーの位置だけじゃなく、
ステージ数によってとか残り時間によってとかいろいろ参照したい状態が追加・変更されたりした場合に
引数を変更するとなると呼び出し元も変更する必要があって対応が辛い。
じゃあってんで、ゲーム情報全体 &GameManager を受け渡してみる:
impl GameManager { |
しかしこれは self.enemy のミュータブル参照と self のイミュータブル参照が同時には取れないのでコンパイルエラーが発生する。
どう回避したらいいかというと、 RefCell を使用する。
RefCell はコンパイル時じゃなくて実行時に借用を判定するようにして、制限を緩めることができる:
use std::cell::RefCell; |
これによって GameManager が管理する Enemy のミュータブル参照に自分を渡せるようになる。
疎結合にする(トレイト・ジェネリクスを使用する)
Enemy に GameManager を渡してしまうのはガチガチの密結合でよくない。
これを回避するにはトレイトを使用する:
pub trait GameInfoHandler { |
GameManager で上記のトレイトを実装し:
impl GameInfoHandler for GameManager { |
Enemy 側ではジェネリクスで受け取る:
impl Enemy { |
こうすることで Enemy は GameManager に依存しなくなり、柔軟さが増す。
自分で責任を取る
RefCell を使うのがまっとうな方法だとは思うんだけど、実行時に借用チェックが行われることになる。
大したコストではないことはわかってるんだがどうしても気になるし、毎度 borrow_mut() とかするのもメンドイし、あらゆるものを RefCell にしなきゃならなくなってしまう。
シングルスレッドだし同時にいじらないことは自分で責任持つよ!
ということで unsafe に手を出してみる。
生ポインタを直接使用する
生ポインタを取得し、参照に変換することで借用チェッカから逃れることができる。
Enemy のミュータブル参照を生ポインタ経由で取得:
pub struct GameManager { |
すると self.enemy のミュータブル参照は raw_ptr の取得時だけとなり、 self のイミュータブル参照はコンパイルエラーにならずに動かすことができる。
Enemy のミュータブル参照ではなく逆に GameManager のイミュータブル参照を生ポインタ経由させても動く:
impl GameManager { |
ミュータブルは正規に借用してイミュータブルを自己責任で取得するこちらのほうが心理的には安心かもしれない。
生ポインタのキャストを毎度書くのも手間なので、 peep などという便利関数を作ってやるといいかもしれない:
pub unsafe fn peep<'a, T>(t: &T) -> &'a mut T { |
追記: 【Rust】不変参照から可変参照に変換する(未定義動作ではない(けど安全でもない))方法
UnsafeCell
生ポインタを直接触ってもいいんだけど、 UnsafeCell というものがある。
get メソッドでミュータブルな生ポインタが取得できる:
use std::cell::UnsafeCell; |
やってることは生ポインタを自分で触るのと変わりはない。
欲をいえば開発時は RefCell を使用して、リリース時は UnsafeCell に切り替えるようなことができればいいんだけど、実装方法が分からなかった…。
締め
unsafe といっても「安全を保証しない」というだけで、Rustではマルチスレッドで動作させる場合も安全であることを保証できない、というだけで一概に危険というわけではないのではないか。
などと自分を納得させて、とりあえず unsafe を使ってでも望みのプログラムを書けるようになるのが先決だな、と思った。
- 実際のところ、ミュータブル1つとイミュータブル複数の借用を使用する場合にシングルスレッドで問題が起こることってあるんだろうか…?
リンク
- 保証を選ぶ
RefCellなどの説明 - RefCell
と内部可変性パターン - The Rust Programming Language - std::cell::UnsafeCell - Rust
- Unsafe Rust - The Rust Programming Language
また、unsafeは、そのブロックが必ずしも危険だったり、絶対メモリ安全上の問題を抱えていることを意味するものではありません: その意図は、プログラマとしてunsafeブロック内のコードがメモリに合法的にアクセスすることを保証することです。
- 危険な型変換:&T→&mut T - yohhoyの日記