三連休
海の日だからって皆が海に行くとは思うなよ!ということで三連休は部屋に閉じこもる平常運転な休日だったのですが、連休中にやろうと思っていたCutieCircuitのiOS版のリリースはTextureを小さくしてもipaサイズの問題解消とはいかず、いよいよAssetBundleに手をだすしかないことが確定したので心折れて、現実逃避の意味も含めてちょっと違うことにチャレンジしました。
C# JobSystem
Unity 2018.1の目玉機能のひとつに「C# JobSystem」(以下 JobSystem)というものがあります。
JobSystemとは、ざっくり言うと「Unityで並列処理を可能とする機能」です。
もともとC#自体にはThread処理機能がありますが、JobSystemではUnity側が内部で使用しているWorkerThreadをC#の(ユーザ側の)スクリプト処理にも開放することで並列処理を可能としている機能なので、Unity側が使いやすいようにAPIを用意してくれていて、C#のThread処理を使うよりはロジックの実装がかなり楽ちん、らしいです。
ここら辺の話は以下のリンク先で詳しい解説が記載されています。
tsubakit1.hateblo.jp
せっかくだから使ってみよう
ロジックの実装がかなり楽、とはいえ、幾つか決まりがあります。
JobSystemを実装する場合は、この決まりに沿って処理を構築する必要があります。
- 並列処理で処理する関数(構造体)で使用する領域(メモリ)を事前に確保する
- 入力用の領域に並列処理用関数(構造体)へ渡すデータを設定する
- Sceduleを実行して、JobSystemにジョブを発行依頼
- Completeで処理が終了まで待つ
- 処理結果を出力用の領域から取得する
- 1.で確保した領域を解放する
手順が多くて難しそうな印象を受けるかもしれませんが、一つ一つの作業はそれほど難しくありません。むしろ一回覚えれば簡単に作れてしまいます。
ただ、メモリの確保にはNativeContainerと呼ばれるコンテナを使う必要があることや、処理が終ったら必ずメモリを解放しなければいけないこと、メモリの確保の仕方が何種類かあること、などは慣れないと戸惑うかもしれませんが、最初のうちは「おまじないみたいなもの」として捉えて、とりあえず見よう見まねで実装しておけばよいです。
詳しいことは全て先ほどのリンク先に書かれているのでそちらを参考してもらうとして、このJobSystemを使って実際私が作ったものがこちら
ドーン
3つの球体がそれぞれ違う速度で自転と公転をしています。
JobSystemを使って自転と公転それぞれの回転をフレーム毎に並列で計算しています。
公転の計算と移動
各球体の回転と移動はIJobParallelForTransformを継承したジョブ実行用の構造体にて行います。
ジョブを実行すると各球体毎にIJobParallelForTransformのExecute関数が呼ばれます。
このExecute関数にはTransformAccessという引数が渡ってくるので、このTransformAccessを経由して球体を移動させます。
なので並列処理側の動作としては
- 1フレーム分の角度回転した移動量を計算する
- TransformAccessのpositionから現在位置を取得し、移動量分を加算する
- 加算した位置をTransformAccessのpositionに設定する
という流れになります。
ちなみに、IJobParallelForTransformではTransform.RotateAroundと言ったAPIを使うことができないようなので、行列計算を使い自力で公転の移動量を計算しています。
これは以下のリンクのロジックを参考にしました。
qiita.com
実際のソースはこんな感じ
<summary>
</summary>
struct RevolutionMotionUpdate : IJobParallelForTransform
{
public NativeArray<RotCalStruct> Accessor;
void IJobParallelForTransform.Execute(int index, TransformAccess transform)
{
RotCalStruct accessor = this.Accessor[index];
ApplyPosition(accessor.Matrix, transform, accessor);
this.Accessor[index] = retAccessor;
}
<summary>
</summary>
<param name="matrix"></param>
<param name="transform"></param>
<param name="data"></param>
void ApplyPosition (Matrix4x4 matrix, TransformAccess transform, RotCalStruct data)
{
Vector3 RevOriPos = new Vector3(data.RevOri_X,data.RevOri_Y,data.RevOri_Z);
Vector3 TargetPos = transform.position;
Vector3 TempPos = TargetPos - new Vector3(data.RevOri_X,
data.RevOri_Y,
data.RevOri_Z);
Vector3 CalResPos = matrix.MultiplyPoint3x4(TempPos);
transform.position = RevOriPos + CalResPos;
}
}
ここで、最初のほうに記述されている
public NativeArray<RotCalStruct> Accessor;
この部分が入力・出力用の領域(メモリ)となります。
RotCalStructは自前の回転計算用データで、メイン側で1フレーム毎の回転角度や公転原点を設定し、並列処理側でそれを参照する形で使用しています。
自転の計算と移動
こちらも公転と同様にIJobParallelForTransformを継承したジョブ実行用の構造体にて行います。
<summary>
</summary>
struct RotationMotionUpdate : IJobParallelForTransform
{
public NativeArray<RotCalStruct> Accessor;
public void Execute(int index, TransformAccess transform)
{
RotCalStruct accessor = this.Accessor[index];
accessor.CurrentRot += accessor.CurrentAngle;
transform.rotation = Quaternion.AngleAxis(accessor.CurrentRot, Vector3.up);
this.Accessor[index] = accessor;
}
}
こちらの場合は、Executeが呼ばれる度にCurrentRotを加算更新しメイン側へ返却、メイン側はそれを保持して次回のコール時に使用して球体を回転させています。
メイン側の処理
メイン側では前述した回転角度や公転原点を設定のほかに、TransformAccessArrayの配列を定義しています。
そこに動作対象となる3つの球体のtransformを設定することで3つの球体それぞれの並列処理(IJobParallelForTransform)が呼ばれる仕組みとなっています。
<summary>
</summary>
struct RotCalStruct
{
public float CurrentAngle;
public float CurrentRot;
public Matrix4x4 Matrix;
public float RevOri_X;
public float RevOri_Y;
public float RevOri_Z;
public RotCalStruct(float currentAngle)
{
this.CurrentAngle = currentAngle;
this.CurrentRot = 0f;
this.Matrix = new Matrix4x4();
this.RevOri_X = 0;
this.RevOri_Y = 0;
this.RevOri_Z = 0;
}
}
JobHandle _jobRevolutionHandle;
JobHandle _jobRotatioHandle;
NativeArray<RotCalStruct> _revolutionStructs;
NativeArray<RotCalStruct> _rotationStructs;
TransformAccessArray _planetTransformAccessArray;
void Start()
{
this._revolutionStructs = new NativeArray<RotCalStruct>(planetArray.Length,
Allocator.Persistent);
this._rotationStructs = new NativeArray<RotCalStruct>(planetArray.Length,
Allocator.Persistent);
for (int i = 0; i < planetArray.Length; i++)
{
RotCalStruct acsess = new RotCalStruct(jobSetting[i].revolAngle);
acsess.Matrix = Rotate(acsess);
acsess.RevOri_X = originPlanet.transform.position.x;
acsess.RevOri_Y = originPlanet.transform.position.y;
acsess.RevOri_Z = originPlanet.transform.position.z;
this._revolutionStructs[i] = acsess;
RotCalStruct acsess2 = new RotCalStruct(jobSetting[i].rotatAngle);
acsess2.RodAxis = (int)jobSetting[i].RotAxis;
acsess2.AxisAngle = jobSetting[i].RotAxisAngle;
this._rotationStructs[i] = acsess2;
}
this._planetTransformAccessArray = new TransformAccessArray(planetArray.Length);
this._planetTrses = new Transform[planetArray.Length];
for (int i = 0; i < planetArray.Length; ++i)
{
var trs = planetArray[i].transform;
this._planetTransformAccessArray.Add(trs);
this._planetTrses[i] = trs;
}
}
<summary>
</summary>
<param name="deltaTime"></param>
<param name="data"></param>
<returns></returns>
static Matrix4x4 Rotate(RotCalStruct data)
{
Matrix4x4 m = Matrix4x4.identity;
float x = 0f, y = 0f, z = 0f;
m.SetTRS(new Vector3(x, y, z), Quaternion.identity, Vector3.one);
float oriX = data.RevOri_X;
float oriY = data.RevOri_Y;
float oriZ = data.RevOri_Z;
float rot = data.CurrentAngle * Mathf.Deg2Rad;
float sin = Mathf.Sin(rot);
float cos = Mathf.Cos(rot);
m.m00 = cos;
m.m01 = -1 * (oriX * cos) + oriX - oriZ * sin;
m.m02 = sin;
m.m20 = -1 * sin;
m.m21 = oriX * sin + oriZ - oriZ * cos;
m.m22 = cos;
return m;
}
void Update()
{
RevolutionMotionUpdate revolutionJob = new RevolutionMotionUpdate()
{
Accessor = this._revolutionStructs,
};
RotationMotionUpdate rotationJob = new RotationMotionUpdate()
{
Accessor = this._rotationStructs,
};
this._jobRevolutionHandle.Complete();
this._jobRotatioHandle.Complete();
this._jobRevolutionHandle = revolutionJob.Schedule(this._planetTransformAccessArray);
this._jobRotatioHandle = rotationJob.Schedule(this._planetTransformAccessArray);
JobHandle.ScheduleBatchedJobs();
}
void OnDestroy()
{
this._jobRevolutionHandle.Complete();
this._jobRotatioHandle.Complete();
this._revolutionStructs.Dispose();
this._rotationStructs.Dispose();
this._planetTransformAccessArray.Dispose();
}
Update()内にて公転のJob、自転のJobをそれぞれ発行し、Completeで待ち合わせています。
各JobはScheduleで発行しますが、実際の動作開始はScheduleBatchedJobsになります。
ちなみにUpdateの先頭でCompleteを行っているのは、JOB発行⇒完了待ち(同じフレーム)、よりも JOB発行⇒次フレーム⇒完了待ち、のほうがより高速化するからです。(・・・と資料に書いてあった)
実際に作ってみて
JobSystemの実装自体は難しいものではありませんでした、どちらかといえばY軸周りの公転用マトリックスを作るのに時間が掛かったぐらいです。(久しぶりに自力で行列計算した)
JobSystemを使えば弾幕系シューティングのように大量のオブジェクトを動かすときなどに効果を発揮すると思います。
ただ、JobSystemではUnityAPが殆ど使えないので、複雑な動きをさせたい場合などは今回のように自力でロジックを組む必要がありそうで、使いどころの見極めが重要になると感じました。
最後に動かす球体を70個ぐらいに増やしたバージョンも作ったので見てやってください。
ド、ドーン