原カバンは鞄のお店ではありません。

Unityを使ったゲーム制作のあれこれを綴っていきます。

【Unity】JobSystemを使ってSTGの弾を撃つ

最初に

ちょっと前の記事で「STGだけど弾を撃たないゲームを作る」、とか言ったなぁ!

 

あれは嘘だ!!

 

f:id:Karvan:20190820204009g:plain

バン!バン!バン!

というわけで

作業スケジュールが詰まっている割に何故か夏休みは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;       // 動作フラグ 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での処理の流れ

  1. Jobのインスタンスを生成する
  2. Jobへパラメータを引き渡す
  3. Job処理を実行する
  4. Job処理の完了を待つ

f:id:Karvan:20190820205524p:plain

ここで注意することは、前述のようにJobSystemで処理する為の情報はNativeArrayで渡されますが、対象となるObjectのTransformはTransformAccessArrayという型でジョブには渡ってくる、ということです。

このNativeArrayとTransformAccessArrayはリンク付けはされていないので、複数のObjectを扱う場合はこちら側で意識してIndexを合わせる必要があります。

f:id:Karvan:20190820205643p:plain

 

例えばJobSystemでの処理結果をNativeArrayに保持して次のJob実行時に引き渡すような処理をする場合、TransformAccessArrayとのIndexが一致していないと、Objectは意図しない動きをすることになります

f:id:Karvan:20190820205746p:plain

 

また、注意する事はもう一つあって、それは

  • 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の後に実行しないとエラーが出る、という理由からです。

  

f:id:Karvan:20190820210401p:plain

 こんな感じでエラーが大量にでる。

 

ちなみに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を使用して弾丸を移動させることができるようになりました。
次回の記事では、この直進動作に回転の動きを加えてみようかと思います。

 

 

◇プライバシーポリシー

●個人情報の利用目的

当ブログでは、メールでのお問い合わせ、メールマガジンへの登録などの際に、名前(ハンドルネーム)、メールアドレス等の個人情報をご登録いただく場合がございます。

これらの個人情報は質問に対する回答や必要な情報を電子メールなどをでご連絡する場合に利用させていただくものであり、個人情報をご提供いただく際の目的以外では利用いたしません。

●個人情報の第三者への開示

当サイトでは、個人情報は適切に管理し、以下に該当する場合を除いて第三者に開示することはありません。

・本人のご了解がある場合
・法令等への協力のため、開示が必要となる場合

個人情報の開示、訂正、追加、削除、利用停止
ご本人からの個人データの開示、訂正、追加、削除、利用停止のご希望の場合には、ご本人であることを確認させていただいた上、速やかに対応させていただきます。

アクセス解析ツールについて

当サイトでは、Googleによるアクセス解析ツール「Googleアナリティクス」を利用しています。

このGoogleアナリティクスはトラフィックデータの収集のためにCookieを使用しています。このトラフィックデータは匿名で収集されており、個人を特定するものではありません。
この機能はCookieを無効にすることで収集を拒否することが出来ますので、お使いのブラウザの設定をご確認ください。

●免責事項

当サイトからリンクやバナーなどによって他のサイトに移動された場合、移動先サイトで提供される情報、サービス等について一切の責任を負いません。

当サイトのコンテンツ・情報につきまして、可能な限り正確な情報を掲載するよう努めておりますが、誤情報が入り込んだり、情報が古くなっていることもございます。

当サイトに掲載された内容によって生じた損害等の一切の責任を負いかねますのでご了承ください。

●プライバシーポリシーの変更について

当サイトは、個人情報に関して適用される日本の法令を遵守するとともに、本ポリシーの内容を適宜見直しその改善に努めます。

修正された最新のプライバシーポリシーは常に本ページにて開示されます。