最初に
ちょっと前の記事で「STGだけど弾を撃たないゲームを作る」、とか言ったなぁ!
あれは嘘だ!!
バン!バン!バン!
というわけで
作業スケジュールが詰まっている割に何故か夏休みは1週間もらえたので、その期間を利用して制作中のゲームで弾が撃てるようになりました。どうもありがとう。
まぁ、上の画像でもわかるように弾丸が前に飛ぶだけの単純な動作なのですが、それだけではつまらいので、弾を撃つ処理にはJobSystemを使っています。
JobSystemについては丁度1年ぐらい前にも記事でまとめたのですが、実際のゲームに対して利用したものを紹介するのは今回が初めてになりますね。
www.karvan1230.com
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;
public float Vo;
public float Amount_Dist;
public MoveStruct(float argVo, Vector3 argPos)
{
this.ActiveFLG = 1;
this.Vo = argVo;
this.Amount_Dist = 0.0f;
}
}
List<GameObject> objList = new List<GameObject>();
<summary>
</summary>
public void Init_Test()
{
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()関数のソースは以下のようになります。
JobHandle jobMoveHandle;
float Threshold;
void Update()
{
MotionUpdate obstractMoveJob = new MotionUpdate()
{
Accessor = this._moveStructs,
DeltaTime = Time.deltaTime,
};
this.jobMoveHandle.Complete();
UpdatedCheck();
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)
{
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>
</summary>
<param name="index"></param>
<param name="transform"></param>
public void Execute(int index, TransformAccess transform)
{
MoveStruct accessor = this.Accessor[index];
if (accessor.ActiveFLG == 0)
{
this.Accessor[index] = accessor;
return;
}
float vo = accessor.Vo;
float deltaDist = vo * DeltaTime;
Vector3 nowPos = transform.position;
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を使用して弾丸を移動させることができるようになりました。
次回の記事では、この直進動作に回転の動きを加えてみようかと思います。