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

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

【Unity】Dotweenの便利なコールバックについて

不正不満を口にする

はてなブログは「画像のアップロードに失敗しました」とかメッセージ出す癖に、ページをリロードするとちゃんと投稿欄にアップロードされているのはやめろ。おかげで同じ動画が投稿欄を埋めて尽くしているんですが。
それからTwitterの「〇〇さんが▲▲さんのツイートにいいねしました」という通知は不要です。私にどうしろというのか。

 

そんな感じで不正不満だけは止まらない屈折した日々を過ごしている所為なのか新作ゲームの進捗はさっぱり進みません。

f:id:Karvan:20190924232422p:plain

この三連休ではアセットストアから敵用のモデルを購入してカスタマイズしたぐらい

 

Dotweenのコールバック

さて、Unityでオブジェクトをアレコレ動かす際にお世話になるDotween
移動(Move)や回転(Rotate)以外にも前回の記事でも使用した経路移動(DoPath)、ジャンプ(DoJump)など、色々便利な関数が用意されているのですが、それと同じようにコールバックも色々なタイミングで使えるように用意されています。

 

  •  OnComplete:Tweener、Sequenceの完了時コールバック
  • OnStart:こちらはTweener、Sequenceの開始時のコールバック

 

おそらく使用頻度が高いのは上記の二つだと思うのですが、これ以外に使い勝手が良いものを上げると

 

・PrependCallback

Tweenerの開始時コールバック
Sequenceの場合は各Tweenerの開始時コールバック
OnStartはSequence全体の開始時ですが、こちらはSequenceの各動作(Tweener)毎の開始前に呼ばれます。

 

・AppendCallback(Sequence用)

Sequenceの各Tweenerの終了時コールバック
OnCompleteと違い各動作(Tweener)毎の完了時に呼ばれます

 

・OnWaypointChange(DOPath、DOLocalPath用)

経路として指定した座標に到達するたびに呼ばれます。
引数としてDOPath,DOLocalPathで指定したVector3配列のIndexが渡されます。

 

 ちょっと使ってみよう

丁度前回の記事でDOLocalPathとDoJumpをSequenceに設定してキャラクターを動かしていたので、それを利用してコールバックの動作を確認しましょう。

 

前回のキャラクター移動

f:id:Karvan:20190917204540g:plain


上の動画では以下の動作をSequenceに設定しています。

  1.  Start~Path3まで:DOLocalPathによる移動
  2. Jump1~Jump3まで:DoJumpを繰り返し
  3. Jump3~Goalまで:DOLocalPathによる移動

f:id:Karvan:20190924233353p:plain

 

ここで、1のDOLocalPathにてOnWaypointChangeを設定しPath1のポイントに達したら、Sequenceを一時停止してキャラクターのAnimationを変更します。

 

まず、Sequence側にOnWaypointChangeを指定します。

    PathSeq.Append(
            myObj.transform.DOLocalPath(Path1Array, 6.0f, PathType.CatmullRom)
                                .SetLookAt(0.05f, Vector3.forward)
                                .OnWaypointChange(x => CallBack_WaitPoint(x))
                                .OnComplete(() => CallBack_Path1())

        );

コールバック関数側でAnimationの変更とSequenceの一時停止、再開を行うようにします

    public void CallBack_WaitPoint(int index)
    {
        if (index == 1)
        {
            // Sequenceの一時停止
            PathSeq.TogglePause();
            
            // 座るAnimation
            myAnim.SetInteger("Animation", 4);

            StartCoroutine(SequenceRestart());
        }
    }

    private IEnumerator SequenceRestart()
    {
        yield return new WaitForSeconds(3.0f);

        // Sequenceの再開
        PathSeq.TogglePause();
        
        // 歩くAnimation
        myAnim.SetInteger("Animation", 1);
    }

次に、2のDoJumpではPrependCallbackを使用して3回目のジャンプ開始前にSequenceを一時停止してキャラクターのAnimationを変更します。

Sequence側にPrependCallbackを指定

   for(int iCnt = 0; iCnt < Jump1Array.Length; iCnt++)
   {
       PathSeq.Append(
               myObj.transform.DOJump(Jump1Array[iCnt], 0.5f, 1, 1.0f)
                               .PrependCallback(() => CallBack_PreJump())
                               .PrependInterval(0.5f)
                               .OnComplete(() => CallBack_CompJump())
           );
   }

コールバック関数側でAnimationの変更とSequenceの一時停止、再開。ここでは回数を計るためにOnCompleteのコールバックにてジャンプの完了回数をカウントアップしています。

    public void CallBack_PreJump()
    {
        // 3回目のジャンプ開始前の場合
        if (JumpComp == 2)
        {
            // Sequenceの一時停止
            PathSeq.TogglePause();
            
            // 座るAnimation
            myAnim.SetInteger("Animation", 4);
            
            
            StartCoroutine(SequenceStepRestart());
            return;
        }

        // ジャンプAnimation
        myAnim.SetInteger("Animation", 2);
    }

    private IEnumerator SequenceStepRestart()
    {
        yield return new WaitForSeconds(3.0f);

        // Sequenceの再開
        PathSeq.TogglePause();
        
        // ジャンプAnimation
        myAnim.SetInteger("Animation", 2);
    }

    public void CallBack_CompJump()
    {
        // ジャンプの完了回数をカウントアップ
        JumpComp++;
    }

これを実際に動作させるとこんな感じになります。

f:id:Karvan:20190922161358g:plain

コールバックを使うと色々な動きが可能となるので積極的に使ってみましょう。

 

【Unity】DotweenのSequenceでDoLocalJumpを使う時は注意が必要

三連休

「三連休ですね~、何か予定があるんですか?」という質問が世の中で一番嫌いな皆さんこんにちは。オイオイ、いくら行楽の秋だからと言って誰もがドライブしたりBBQしたり観光したりするとは思うなよ、と言うことで、理容師のお姉さんへ、答えに窮する質問を投げかけるのはやめて下さい、それから髪を洗う時に不意に頭のツボを押すのもやめて下さい、変な声出そうになった。

 

この機会に知ってほしい事があるの~!!

というわけで、ダラダラ説明するよりまずは下の動画を見てほしい

f:id:Karvan:20190917202614g:plain

 

まるで某巨大変身ヒーローみたいな仕草でキャラクターが画面外に飛んで行っていますが、別に意図的にそうした訳ではなく、本来は階段をジャンプしながら登って先に進む、という動作をDotweenのSequenceを使って実装した結果です。

 

Dotweenって何?Sequenceって美味しいの?っていう人は下のリンクを参照してください

qiita.com

要はGameObjectの移動を連続させて行う際に使用するAPIで、今回の場合はキャラクターに対して

  1. 階段までの経路を移動する
  2. 一段目の段差をジャンプして移動する
  3. 二段目の段差をジャンプして移動する
  4. 三段目の段差をジャンプして移動する
  5. 階段の端まで移動する

という動作を連続で行うように設定しています。

この時、1.と5.の移動にはDoPathによる経路移動を、2.3.4.のジャンプにはDoLocalJumpを使用します。

 

DoPathによる経路移動については下記参照

www.karvan1230.com

ソース的にはこんな感じです。

 

   PathSeq = DOTween.Sequence();

   // 階段までの経路移動
   PathSeq.Append(
           myObj.transform.DOLocalPath(Path1Array, 6.0f, PathType.CatmullRom)
                               .SetLookAt(0.05f, Vector3.forward)

       );

   // 各段差をジャンプして移動
   for(int iCnt = 0; iCnt < Jump1Array.Length; iCnt++)
   {
       PathSeq.Append(
               myObj.transform.DOLocalJump(Jump1Array[iCnt], 0.5f, 1, 1.0f)
           );
   }

   // 階段の端まで移動する
   PathSeq.Append(
       myObj.transform.DOLocalPath(Path2Array, 4.0f, PathType.CatmullRom)
                           .SetLookAt(0.05f, Vector3.forward)

   );
 

何故飛んで行ってしまうのか?

ソースだけを眺めていると特におかしな処理はしていないので、移動先の座標さえ間違っていなければ意図通りの動作をすると思うのですが、結果としては大ジャンプを繰り返して画面外へ消える動作となっています。

 

これはきっとDotweenのバグに違いない、ということで、下のモデルを使って検証してみることにしました。

f:id:Karvan:20190917203415p:plain


赤いCubeをSequenceを使って移動させるのですが、図の通りに同じ高さの場所をDOLocalJumpで移動させる場合と、異なる高さをDOLocalJumpで移動させる場合について比較してみます。

まずは同じ高さを移動する場合

f:id:Karvan:20190917203511g:plain

 

特に問題なく移動できます。
次は違う高さを移動する場合

f:id:Karvan:20190917203606g:plain

 想定以上に飛び上がって移動し、最終位置もずいぶん高い位置になっています。


これをSequenceを使用せずにDOLocalJumpを連続で使用した場合は

f:id:Karvan:20190917203706g:plain

きちんと指定先にジャンプ移動します。


これは垂直方向へ飛びあがる場合も同じで、Sequenceを使った場合とDOLocalJumpを連続で使用した場合では動作が全く異なります。

 

Sequenceを使った場合

f:id:Karvan:20190917203853g:plain

 

DOLocalJumpを連続で使用した場合

f:id:Karvan:20190917203947g:plain

 

Sequenceを使った場合は指定した位置以上へ飛び上がる結果となります。

また、これは飛び上がる場合だけでなく下へ飛び降りる場合も同じで、ジャンプを繰り返すほど下へ落ち込む軌道になります。

 

想定するに最終的な高さ(Y座標)の差異が、次のジャンプの計算に影響していると思われ、ジャンプを繰り返すほど大きなジャンプ軌道を描くようになり、最終的な位置も異なるようになります。


まぁこれらの挙動(不具合)について言及しているサイトを見つけることが出来なかったので想像の範囲なのですが、これまでの事象をまとめると

SequenceでDOLocalJumpを使用する場合は同じ高さ(Y座標)を移動する場合に限る

ということになります。

 

対処法

まぁ、Sequenceを使用せずにその都度DOLocalJumpを使用すればいいことなんですが、どうしてもSequenceを使いたい、ジャンプの時だけ別口で処理をするのは面倒くさい、という場合は

SequenceではDOLocalJumpではなくDOJumpを使用してください

移動先の座標をローカル座標ではなくワールド座標に変更するもの忘れずに。

 

先程のソースでDOLocalJumpとなっている箇所をDOJumpに変更した結果が以下の動画になります。

 

f:id:Karvan:20190917204540g:plain

きちんと想定通りに動作してくれました。めでたしめでたし。
 

 

【進捗報告】弾幕

Kingdom Come: Deliverance

夏も終わり、秋口らしく長雨が続き時々晴れたら蒸し暑い、という涼と暑がどっちつかずの季節になってきました。

その所為なのか私の周りでは体調を崩す人も多く、私自身も疲れが中々取れない、モチベーションが上がらない、という日々が続いていて、ゲーム制作を進めないといけないとはわかっていても何故か手にはPS4のコントローラーが握られていたり、一歩進んでは休み、一歩進んではPS4ボタンを押し、一歩進んでは15世紀初頭の中央ヨーロッパを探索する、を繰り返す進捗となっています。

 

『Kingdom Come: Deliverance』というゲームの話なんですが、中世暗黒時代のチェコ(ボヘミア地方)を舞台としたオープンワールドRPGで非常に自由度が高いゲーム性ながら、魔法もモンスターもない世界観が地味過ぎて日本では2万本未満のセールスとなっているゲームです。

 

モンハンみたいに巨大なモンスターとかダンジョンがあるわけではなく、せいぜい敵国の兵士とか野盗らを相手に剣を振ったり弓を撃ったりするだけで、主人公もイケメンでも美少女でもない無精ひげの青年だったりするので本当に見た目は地味なのですが、善人プレイも悪人プレイもどちらでもこなせる自由度の高さと、それに伴うストーリー展開の豊富さにハマってしまい、ほぼ毎日プレイしています。赤の他人の主人公を看病して救ってくれた親切な粉屋のオッサンからうける最初クエストが「死体から指輪盗んで来い」とか、暗黒時代の闇深さが現れていてメッチャ面白い。

 

そんな訳で

現在制作中の2.5Dシューティングゲームですが、遂に敵側の方も弾が撃てるようになりました!パチパチ。

 

以前の記事で書いたようにJobSystemを使って弾を動かしているのですが、平行移動に加えて公転運動と伸縮運動が行えるので色々なパターンの弾丸を作りました。

 

例えば

f:id:Karvan:20190910212542g:plain

一列に並んで移動するパターン、とか

 

f:id:Karvan:20190910212700g:plain

リング状に並んで移動するパターン、とか
発射時の弾の位置を変えているだけなんで簡単に作れました。


後は

f:id:Karvan:20190910212823g:plain

巷のSTGでよく見かけるような円陣が広がりながら回転するパターン
真上から見たパターンはお馴染みでしたが、横から見るとこんな風に見えます。

 

最後に球状に弾丸を配置して回転

f:id:Karvan:20190910212950g:plain

なんだか弾幕っぽい感じになりました。

 

まぁ弾幕STGにする気はないんですが・・・

 

【Unity】Animation実行中にイベントを実行する

大雨

今年はやっぱり冷夏だったのか、夏休み期間が終わるのと同時ぐらいに「猛暑日」というワードも全く聞かれなくなって、どちらかと言えば「大雨」というワードがトレンドで賑う季節になっています。
私の住んでいる福岡では2日連続で大雨・洪水警戒レベル5を達成するという偉業を成し遂げて、しかも丁度帰宅の時間でピークを迎えるとか、もう帰れないかと思った、という貴重な経験を2日連続で味わう事ができました。ありがとうございます。もう二度と御免です。

 

さて・・・

前回、前々回の記事で紹介した通り、やっとSTGらしく弾を撃てるようなりました。

f:id:Karvan:20190903212917g:plain

 

未だ敵側の弾丸制御は未実装なんですが、やっぱり自機側だけでも弾を撃てると画面に派手さが加わりますね。

 

これで自機側の機能(武器)としては直線的に飛ぶ弾丸と、ロックオンターゲットに向かっての科学忍法『火の鳥』の二つになります。

 

ここで『火の鳥』って何?という方に説明すると、『火の鳥』言うのは下の動画のように、自機が敵に向かってKamikaze Attackする戦法の事を言います。
Kamikaze Attackの精神は最強なので、ゲーム内では最強の攻撃方法となる予定です。

 

f:id:Karvan:20190723234207g:plain

 

二つの攻撃方法を切り替えたい

攻撃方法が二つあるのですが、各それぞれで制御するモジュールは異なります。
弾丸の方は前述のようにJobSystem(Script)にて制御しますが、『火の鳥』の方はAnimation動作で自機の動きを制御しています。

 

この為、攻撃方法を切り替える際には互いに同期をとる必要があるのですが、Animationを使用してオブジェクトを動かす際には、開始時にトリガーを指定するだけで、その完了を待って次の動作を行いたい場合にはちょっとした工夫が必要なります。
(・・・っていうのが今回の記事の主旨)

 

Script側の設定

最初にアニメーション終了時に呼び出すコールバック関数を作成します。

ここで原則としてAnimation側からコールバックを受け取れるのは、Animatorと同じオブジェクトに付与されているScriptだけになります。Animatorが別オブジェクトのスクリプトに対してコールバック関数を呼び出すことはできません。

 

今回の場合は『火の鳥』のアニメ動作完了後にJobSystemへ弾丸発射の依頼をするだけなので、下記のように一行だけです。 

f:id:Karvan:20190903212404p:plain

 

 Animation側の設定

次にAnimation側でイベント発行(今回は完了時)のタイミングと、その時に呼び出すコールバック関数を指定します。

 

Animationウィンドウを開き、アニメーション終了時のKeyFrame上あたりで右クリックします。

 

f:id:Karvan:20190903213035p:plain

そうするとAdd AnimationEventという吹き出しが出てくるのでそれを選択します。

 

f:id:Karvan:20190903213134p:plain

選択後にInspectorウィンドウにAnimatorと同じオブジェクトに付与されているScriptのpublic関数が表示されるので、コールバック関数になるものを指定します。

 

f:id:Karvan:20190903213200p:plain

指定後はこんな感じになります。

 

実際の動き

これらの設定後に実際のゲーム上で動作確認した結果がコレです。

f:id:Karvan:20190903213249g:plain

 

次は敵側の弾丸制御かぁ・・・

 

【Unity】JobSystemを使って弾を回転させながら撃つ

夏の思ひで

盆過ぎあたりから秋雨前線が北上してきた影響なのか、朝夕は涼しくなって猛暑が過ぎたことを実感できる気候になってきました。
ということは夏も終わり、と言う事なんですが、社会人になってからは夏だろうが冬だろうが代わり映えのない日々を過ごしているので、夏を振り返るとか、夏の終わりを寂しく思うとか、そんな傷心な気持ちになったりしません。

まぁ、無理やり思い返してみても、夏季休暇中に二回ライブを観に行って二回とも隣の席が巨汗デブとセーラ服を着たおっさんという奇人変人だったとか、珍しく遠出をしたら何故か上司と鉢合わせになったとか、碌な思い出しかないので大人しく季節が過ぎていくのを静観したいと思います。

 

前回の続き

そんな訳で前回の続きとなります。

前回ではJobSystemを使って弾丸を真直ぐ飛ばす処理を作りましたが、今回はそれに回転の動きを加えてみたいと思います。
まぁ、ゲームとしては弾丸が回る必要はないんですがね。

 

f:id:Karvan:20190827204755g:plain

こんな感じで回ります。

 

公転処理

今作中、自機の発射する弾丸は弾3つで1組の構成となっています。
弾丸はZ方向へ移動し、かつ弾3つの中心点を原点としてZ軸を回転軸に公転運動を行います。

f:id:Karvan:20190827204932p:plain

 

通常、このような構成で公転運動を実装する場合、公転原点に空のオブジェクトを生成し、弾丸はその子オブジェクトして定義、親オブジェクトを回転させることで弾丸を公転させる処理を取ったりします。

が、今回は各弾丸にそれぞれに対して公転運動の計算を行います。

 

公転運動の計算については、@mao_氏がQuiitaに書かれた下記の記事中の「任意点周りの回転行列の算出について」の章を参考してMatrixを使用して計算しています。(なのでソースの転記はしません。ご了承ください。)

 

qiita.com

処理順番を制御する

公転運動はMatrixで計算するとして、弾丸はZ方向へ移動し続けるので、当然、公転原点となる場所も移動します。
なのでJobSystemを使ってZ方向への移動と公転運動を行う場合は、移動計算用のJOBと公転計算用のJOBの二つを用意して、先に移動計算用JOBでZ方向へ移動させ、その完了後に公転計算用JOBで公転運動の計算と反映を行う、という順序になります。

f:id:Karvan:20190827205945p:plain
 

このJOBを待ち合わせて実行させるのは非常に簡単で、待ち合わせる側のJOBのSchedule関数にて第二引数に待ち合わせ対象のJOBハンドルを設定する、だけで実装できます。

 

    JobHandle _jobMoveHandle;           // 移動用
    JobHandle _jobRevolutionHandle;     // 公転運動用

    void Update()
    {
        // 移動用JOB領域設定
        BlockMotionUpdate obstractMoveJob = new BlockMotionUpdate()
        {
            Accessor = this._moveStructs,
            DeltaTime = Time.deltaTime,
        };

        // 公転用JOB領域設定
        RevolutionMotionUpdate revolutionJob = new RevolutionMotionUpdate()
        {
            Accessor = this._revolutionStructs,
            DeltaTime = Time.deltaTime,
        };
        
        // JobSystemの完了待ち
        this._jobMoveHandle.Complete();
        this._jobRevolutionHandle.Complete();

        // 移動後のチェック
        UpdatedCheck();
        
        // JobのSchedule発行
        this._jobMoveHandle = obstractMoveJob.Schedule(this._transformAccessArray);
        this._jobRevolutionHandle = revolutionJob.Schedule(this._transformAccessArray, _jobMoveHandle);

        // Jobの実行
        JobHandle.ScheduleBatchedJobs();
    }

上記の

  this._jobRevolutionHandle =
revolutionJob.Schedule(this._transformAccessArray, _jobMoveHandle);

にて、第二引数に移動用のJOBハンドルを指定して、移動計算後に公転運動の計算が行われるようになります。

移動後のチェック処理(UpdatedCheck)で、移動用JOBから引き渡された出力パラメータ(NativeArray)の移動ベクトルを元に、公転運動用の原点位置を移動ベクトル分だけ移動させて公転運動用の入力パラメータ(NativeArray)へ引き渡します。

    /// <summary>
    /// 移動後のチェック処理
    /// </summary>
    public void UpdatedCheck()
    {
        for (int iCnt = 0; iCnt < _moveStructs.Length; iCnt++)
        {
            // 移動ベクトルの取得
            Vector3 CurrentVec =  this._moveStructs[iCnt].CurrentVec;
            
            // 公転原点の移動
            Vector3 RotOrigin = this._revolutionStructs[iCnt].RotOrigin;
            RotOrigin = RotOrigin + CurrentVec;
            
            // 公転運動用の入力パラメータへ反映
            this._revolutionStructs[iCnt].RotOrigin = RotOrigin;
        }
    }

上記の処理を行う為に、移動計算用JOBでは移動ベクトルを結果として保持するように変更します。

    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;
            }
            
            // 現在の位置取得
            Vector3 nowPos = transform.position;

            // 移動方向ベクトル取得
            Vector3 purVec = accessor.purVec;

            //-------------------------
            // 移動方向ベクトルに沿って移動
            //-------------------------
            // 移動距離計算
            float vo = accessor.Vo;
            float deltaDist = vo * DeltaTime;

            // 移動後の位置計算
            Vector3 modPos = nowPos + deltaDist * purPos.normalized;

            // transformへ反映する
            transform.position = modPos;

            // 累積値の更新
            float Amount_Dist = accessor.Amount_Dist;
            Amount_Dist += deltaDist;
            accessor.Amount_Dist = Amount_Dist;

            // 今回の移動ベクトル
            accessor.CurrentVec = modPos - nowPos;
            
            // 結果保持
            this.Accessor[index] = accessor;
        }
    }

Update関数内で原点の移動を行うので公転運動計算用JOBではMatrixに沿って公転運動の計算を行うだけになります。

    /// <summary>
    /// 公転運動用計算
    /// </summary>
    struct RevolutionMotionUpdate : IJobParallelForTransform
    {
        public NativeArray<RotCalStruct> Accessor;
        public float DeltaTime;

        /// <summary>
        /// JobSystem側で実行する処理
        /// </summary>
        /// <param name="index"></param>
        /// <param name="transform"></param>
        public void Execute(int index, TransformAccess transform)
        {
            RotCalStruct accessor = this.Accessor[index];

            // 公転無しの場合は処理終了
            if (accessor.isRevFLG == 0) return;

            // 原点位置設置
            Vector3 originPoint = accessor.RotOrigin;

            // 公転計算用のMatrix取得
            // (ここは先程のリンクの記事を参考にしてください・・・)
            float rotVo = accessor.CurrentAngle;
            Matrix matrix = CalRoundAtMatrix(Vector3.zero,
                                             accessor.RodAxis,
                                             rotVo);
                                              
            // 現在の位置取得
            Vector3 nowPos = transform.position;

            // 公転計算用のMatrix取得
            Vector3 CalResPos = matrix.MultiplyPoint3x4(nowPos);

            // 計算結果をTransformに反映
            transform.position = CalResPos;

            // 結果保持
            this.Accessor[index] = accessor;

        }
    }

 

これを応用して

こんな感じでJOBを待ち合わせることで色々な動作が実現することができるので、この公転運動に加えて公転半径の収縮の運動も別JOBで計算するようにしました。

 

f:id:Karvan:20190827205840g:plain

結果としてはこんな感じ、3つの弾丸が回りながら一点に収束していきます。

まぁ、これもSTGとしては必要な機能ではないんですが・・・

 

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

 

 

【Unity】Rigidbodyを使ってオブジェクトを吹き飛ばす

夏本番

別に夏の事は嫌いではありませんが、普通に最高気温が体温を超えるのはやめて下さい。
あと『表現の不自由』がそんなに表現したいたら中国に行って習近平の肖像でも燃して下さい。(これも表現の自由です)

 

壊したい

さて、現在制作中のゲームがシューティングゲームということで、当然のことながら何かしらの物が壊れるとか吹き飛ぶといった破壊表現が必要となってくるわけですが、まぁオーソドックスにパーティクルを表示してオブジェクトを非表示化する、みたいな表現だけではちょっと味気ないので、構造物が壊れて吹き飛ぶような演出も実装してみよう、というのが今回の目的。

 

f:id:Karvan:20190806231527p:plain

こんな感じで壊したい

 

Unityではそれ(破壊処理)用のアセットもあったりして、Exploderあたりが有名なんでしょうか、メッシュ破壊系のアセットを導入すれば簡単なのですが、それなりにお値段もするし、だいたいドキュメントは英語で読むのが面倒!というわけで自力で何とかしたい。

 

メッシュ破壊は無理でもオブジェクト本体を飛ばすのはスクリプト何とか出来るので、そちらで実装する方法を考えます。

 

Rigidbodyを使う

Unityでは物理演算はエンジン側で行ってくれるので、有難くそちらを利用します。

 

RigidbodyにはAddExplosionForceというメソッドが用意されていて、こちらはその名前の通り、爆発の威力、爆発点、爆発半径を与えると爆発の状態をエミュレートした力をRigidbody加えてくれます。

 

 

Rigidbody.AddExplosionForce(
		float Force, 		// 爆発の威力
		Vector3 Position, 	// 爆発点
		float Radius);		// 爆発半径

 

例えば複数のオブジェクトで構成されている構造体に対して、オブジェクトそれぞれにこのAddExplosionForceを適用してやればまさに爆弾が爆発したような演出が可能になる。。。はず。

 

ターゲット

今回は以下のような六角形の床と天板、柱が並んでいる簡単な構造体を使ってみました。
当然のことならが全てのオブジェクトにはColliderとRigidbodyが設定されています。

 

f:id:Karvan:20190806232027p:plain

 

これらにAddExplosionForceで力を加えてやります。ここで第二引数のPositionにはワールド座標を指定することに注意です。ローカル座標ではありません

 

また、吹き飛ばしたい全てのオブジェクトのRigidbodyに対してAddExplosionForceを加えることが必要です。


今回のターゲットの場合は全て構造体の子オブジェクトとして定義しているので、Findでオブジェクトを取得し、そのオブジェクに対してGetComponentからRigidbodyを取り出してAddExplosionForceを加えます。

 

 

float ExplosionPosition = this.transform.position;

foreach (Transform child in ExplosionObjRoot)
{
	child.GetComponent<Rigidbody>()
			.AddExplosionForce(ExplosionForce,
                               ExplosionPosition,
                               ExplosionRadius);
}

 

で、構造物の中央のワールド座標からAddExplosionForceを加えた結果がこれ

 

f:id:Karvan:20190806232329g:plain

 

f:id:Karvan:20190806232354p:plain

あ、いや、わかるけどさー。もうちょっと、こう・・・

 

というわけで、爆発点を構造体中央からちょっとだけずらして指定すると

 

f:id:Karvan:20190806232442g:plain

オブジェクトの移動に回転が加わるようになりました。

 

ただ、これでもちょっと味気ないので、今度は床と天板の中に小さいオブジェクトを仕込んでやり、そちらにもAddExplosionForceを加えるように変更。

 

f:id:Karvan:20190806232531p:plain

ごちゃごちゃしているのが仕込んだオブジェクト

 

これらに対しても先ほどと同じようにAddExplosionForceを加え、かつ、0.1秒後に床と天板のオブジェクト自体を非表示化する処理を追加します。

f:id:Karvan:20190806232615g:plain

 

f:id:Karvan:20190806232354p:plain

これでいいか・・・

 

 

で、これを実際のゲームに組み込んでみました。

f:id:Karvan:20190806232758g:plain

どうでしょうか?破壊されているように見えますかね?

 

◇プライバシーポリシー

●個人情報の利用目的

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

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

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

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

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

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

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

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

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

●免責事項

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

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

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

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

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

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