simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

AIに身体性を与えるためのマイクロワールドの構築12:ワーカースレッドの仕組み

前置き

ゲームの描画ループを止めることなく、バックグラウンドで処理を行う作りにすることは、ゲーム体験を向上させる点で重要なことです。
マイクロワールドの表示において、ブロックのメッシュの更新がバックグラウンドで実行できないという Unity の厳しい制約がありました。

そのため、メイン描画スレッドの他にバックグラウンド処理用のスレッドを立て、可能な限り裏で処理をして、Unityがどうしてもメインスレッドで処理してほしいと宣言している処理のみをメインスレッドに渡し、さらにフレームレートが落ちないよう重い処理を分割して消化する仕組みを作る必要があります。

成果物

こちら、上から見た図です。
マウスで動かしているのがプレイヤーのカメラの位置、周囲のチャンクが近い順に作成され、遠いチャンクは削除されていることがわかります。

f:id:simplestar_tech:20180108192448g:plain

目に見えませんが、定期的に走査する処理が走り、部分的にチャンクを更新しながら、不要となったチャンクを開放する処理が走ります。

実装メモ

私の考えは次の通りです。
ゲームが始まると同時に一つのメインビュースレッドと二つのモデル側のスレッドが走ります。
具体的にはメインビュースレッドの描画準備が整い次第、メインスレッドが二つのモデルワーカースレッドを走らせます。

実は UniRx を使って次のコードで別スレッド処理を起動できるのですが…

Observable.Start(() => {
// ここに別スレッドで行う処理
})
.ObserveOnMainThread()
.Subscribe(_ => { });

開発中のコードにてクラッシュすると、どこで例外が発生したのか Unity からうまく追えなくなるという問題があります。
絶対にエラーが出ない単純な処理なら UniRx を用いるのもコーディング面で便利ですけど…今回はそのケースにあてはまりません。

やっぱりスレッドは以前書いた以下の記事を参考にして実行することにします。
simplestar-tech.hatenablog.com

Mixed Reality Tool Kit の導入を以前、書きましたが、この時点でビルド設定は Experimental だったので、簡単に sync / async キーワードが使えました。
まずはワーカースレッドをメインスレッドが生きている間中走らせるようにしてみました。
作られる二つのスレッドにはそれぞれ次の名前が付いています。
1. LatticeScanner
2. SpiralScanner
1. はその名の通り格子状に走査するスレッドで、具体的には連番でチャンクを 3 x 3 で復元し、チャンクの全てのブロックの処理を行った後、オブザーバーまでの距離と共にビューに通知します。
2. はその名の通り螺旋状に走査するスレッドで、具体的にはオブザーバーがいるチャンクを中心として螺旋状にループしながら、3 x 3 でチャンクを復元し、チャンクの全てのブロックの処理を行った後、ビューに通知します。

落ち着いてきたころの実装コードはこちら

        public void StartWorkerThreads()
        {
            _workThread = true;
            _mainThread = SynchronizationContext.Current;

            Task.Run(_ScannLattice);
            Task.Run(_ScanSpiral);
        }

       private Action _ScannLattice = () =>
        {
            var threadId = Thread.CurrentThread.ManagedThreadId;

        private Action _ScanSpiral = () =>
        {
            var threadId = Thread.CurrentThread.ManagedThreadId;

LatticeScanner の場合
Observer から遠く離れている可能性があります。
その場合、計算で不要となった3つのチャンクを開放しなければなりません。
いつチャンクの情報を開放するのか?→これはブロックの処理が終わったタイミングで開放することにしました。
どうやって3つのチャンクを特定するのか?→ループの書式上、最も若いオフセットでアクセスしたチャンクで特定できました。
開放したは良いですが、デバッグ表示として、ビューで結果を可視化したい。→可視化できるようにしました。
そこで、緯度経度が変更される瞬間も不要なチャンクが開放されることを可視化により確認。

確認した時の動画
www.youtube.com

ここでもう一方の SpiralScanner の場合
Latticeの方は世界一周して、オブザーバーの近くにくるまで時間がかかりますので、常にオブザーバーを中心として螺旋状にスキャンする仕組みも必要です。
こちらも可視化してみます。
www.youtube.com

これは、消してほしくないものまで消していますね。
オブザーバーの周囲にあるチャンクは消さないようにしたいのですが、どうすればその判定ができるでしょうか?

オフセット計算できる関数→ありませんので作ります。

循環の部分はどうすればよいのでしょうか?→考えます。。。

なるほど!遠すぎるということも近いということに気付きました。
これでインデックスが折り返すような状況でも残したいチャンクだけ残るように修正できそうです。

www.youtube.com

できました。

次に、オブジェクトが残り続けるという問題を解決します。
先ほどのオブジェクトが近い判定が循環を含めて行えるようになりましたので、こちらを利用して
ビュー側の Update 処理で、オブザーバーから遠すぎる場合はオブジェクトを削除するようにしてみました。

また、同時に同じチャンクに対して処理を行うとブロックデータを破壊してしまうので、チャンクごとにロックオブジェクトを作成し同じチャンクに同時にアクセスできないようにしました。

www.youtube.com

スレッドの仕組みについては、おおむね片付きました。
次は地形生成のプラグイン機構を作っていきます。