最初に
ちょっと前の記事で「STGだけど弾を撃たないゲームを作る」、とか言ったなぁ!
あれは嘘だ!!
バン!バン!バン!
というわけで
作業スケジュールが詰まっている割に何故か夏休みは1週間もらえたので、その期間を利用して制作中のゲームで弾が撃てるようになりました。どうもありがとう。
まぁ、上の画像でもわかるように弾丸が前に飛ぶだけの単純な動作なのですが、それだけではつまらいので、弾を撃つ処理にはJobSystemを使っています。
JobSystemについては丁度1年ぐらい前にも記事でまとめたのですが、実際のゲームに対して利用したものを紹介するのは今回が初めてになりますね。
JobSystemとは
JobSystemとは、ざっくり言うと「Unityで並列処理を可能とする機能」で、これにより大量のオブジェクトを動かすような場合に、高速で処理する事が可能になる・・・らしいので大量の弾丸が動き回るようなSTGではこのJobSystemはかなり有用です。
このJobSystemにはインターフェースが幾つか用意されているのですが、今回はIJobParallelForTransformを使っています。
IJobParallelForTransformはジョブ実行中にTransformを並列に処理できるインターフェースになっているので、JobSystemでObjectの動作を制御する場合はこのインターフェースを使用した方が有益です。
Transformを並列に処理できるとはいっても、ObjectのTransformを直接扱えるわけではなく、それとリンク付けされたTransformAccessという型をJobSystemへ渡して処理することになります。
このTransformAccessに用意されているプロパティは以下の5つ
- localPosition
- localRotation
- localScale
- position
- rotation
これらのプロパティをJobSystem内で操作するとObjectのTransformに反映される、という仕組みです。
事前準備
JobSystemは専用の入出力情報(NativeArray)を事前に用意して、Update()関数内でJobSystemにジョブを発行する流れとなりますが、IJobParallelForTransformを使う場合はこの入出力情報(NativeArray)以外に、前述のTransformAccessを配列化したTransformAccessArrayも用意する必要があります。
下のソースの場合、objList内のObjectからTransformAccessArrayを生成して、入出力情報(NativeArray)の領域確保も行っています。
/// <summary> /// 移動用構造体 /// </summary> struct MoveStruct { public int ActiveFLG; // 動作フラグ 0:OFF 1:ON public float Vo; // 速度 public float Amount_Dist; // 累積の移動距離 public MoveStruct(float argVo, Vector3 argPos) { this.ActiveFLG = 1; this.Vo = argVo; this.Amount_Dist = 0.0f; } } // 対象となるGameObjectリスト List<GameObject> objList = new List<GameObject>(); /// <summary> /// 初期処理 /// </summary> public void Init_Test() { // TransformAccessArrayの領域確保 Transform[] transforms = new Transform[objList.Count]; for(int iCnt = 0; iCnt < objList.Count; iCnt++) { transforms[iCnt] = objList[iCnt].transform; } TransformAccessArray transformAccessArray = new TransformAccessArray(transforms); // 入出力情報の領域確保 NativeArray<MoveStruct> moveStructs = new NativeArray<MoveStruct>(transformAccessArray.length, Allocator.Persistent); }
領域を用意したらJobSystem内で処理する為の入力バッファの中身を埋めます。
今回の場合は移動速度を設定しています。
for (int i = 0; i < transformAccessArray.length; i++) { MoveStruct setStruct = new MoveStruct(velocity); moveStructs[i] = setStruct; }
ここまで用意出来たらUpdate()関数でJobSystemにジョブを発行します。
Updateでの処理の流れ
- Jobのインスタンスを生成する
- Jobへパラメータを引き渡す
- Job処理を実行する
- Job処理の完了を待つ
ここで注意することは、前述のようにJobSystemで処理する為の情報はNativeArrayで渡されますが、対象となるObjectのTransformはTransformAccessArrayという型でジョブには渡ってくる、ということです。
このNativeArrayとTransformAccessArrayはリンク付けはされていないので、複数のObjectを扱う場合はこちら側で意識してIndexを合わせる必要があります。
例えばJobSystemでの処理結果をNativeArrayに保持して次のJob実行時に引き渡すような処理をする場合、TransformAccessArrayとのIndexが一致していないと、Objectは意図しない動きをすることになります
また、注意する事はもう一つあって、それは
- NativeArrayの変更はJob処理の完了を待つ必要がある
ということです。
NativeArrayの変更タイミング
実際のUpdate()関数のソースは以下のようになります。
// Jobの終了待ち等を行うHandle JobHandle jobMoveHandle; // 移動用 float Threshold; // 閾値距離 void Update() { // 移動用JOB領域設定 MotionUpdate obstractMoveJob = new MotionUpdate() { Accessor = this._moveStructs, DeltaTime = Time.deltaTime, }; // JobSystemの完了待ち this.jobMoveHandle.Complete(); // 移動後のチェック UpdatedCheck(); // Job処理実行 this.jobMoveHandle = obstractMoveJob.Schedule(this.transformAccessArray); JobHandle.ScheduleBatchedJobs(); } public void UpdatedCheck() { for (int iCnt = 0; iCnt < moveStructs.Length; iCnt++) { MoveStruct targetStruct = this.moveStructs[iCnt]; // 累計移動距離が閾値距離を超えている場合 if(targetStruct.Amount_Dist > Threshold) { // 動作フラグをOFFにする targetStruct.ActiveFLG = 0; this.moveStructs[iCnt] = targetStruct; } } }
ここでJob処理実行前に完了待ち(Complete)を行っているのが分かると思います。
これはJOB発行⇒完了待ち(同じフレーム)、よりも JOB発行⇒次フレーム⇒完了待ち、のほうがより高速化する、というTips以外にも、UpdatedCheck()でNativeArrayの情報を変更していますが、これをCompleteの後に実行しないとエラーが出る、という理由からです。
こんな感じでエラーが大量にでる。
ちなみにJobSystem側のソースはこんな感じ、移動距離の累積をNativeArrayに保持して、次フレームのUpdatedCheck()にて参照されるようにしています。
struct MotionUpdate : IJobParallelForTransform { public NativeArray<MoveStruct> Accessor; public float DeltaTime; /// <summary> /// JobSystem側で実行する処理 /// </summary> /// <param name="index"></param> /// <param name="transform"></param> public void Execute(int index, TransformAccess transform) { MoveStruct accessor = this.Accessor[index]; // 動作フラグがOFFの場合は終了 if (accessor.ActiveFLG == 0) { this.Accessor[index] = accessor; return; } //------------------------- // Z方向へ移動 //------------------------- // 移動距離計算 float vo = accessor.Vo; float deltaDist = vo * DeltaTime; // 現在の位置取得 Vector3 nowPos = transform.position; // transformへ反映する nowPos.z = nowPos.z + deltaDist; transform.position = nowPos; // 累積値の更新 float Amount_Dist = accessor.Amount_Dist; Amount_Dist += deltaDist; accessor.Amount_Dist = Amount_Dist; // 結果保持 this.Accessor[index] = accessor; } }
これでJobSystemを使用して弾丸を移動させることができるようになりました。
次回の記事では、この直進動作に回転の動きを加えてみようかと思います。