i-school - 2Dタップシューティングゲーム 発展11
 今後エネミーの移動方法が増えることを想定して、エネミーの移動方法を別の手法に変更を行います。
これはリファクタリングになりますので、ゲーム上の挙動は変わらず、スクリプトの内容を改修する内容になります。

 以下の内容で順番に実装を進めていきます。

発展11 −エネミーの移動用のスクリプタブル・オブジェクトの作成と運用−
21.エネミーの移動用の MoveEventSO スクリプタブル・オブジェクトを作成する
22.エネミーの移動方法に応じて MoveEventSO スクリプタブル・オブジェクトに用意されたメソッドを UnityAction に登録して移動を実行する



 新しい学習内容は、以下の通りです。

 ・スクリプタブル・オブジェクトの新しい使い方
 ・UnityAction(デリゲート)の使い方
 ・DOAnchorPos メソッド



21.エネミー用の移動用のスクリプタブル・オブジェクトを作成する

1.設計


 この手順では、エネミーの移動方法についてリファクタリングを行います。
新しい機能がありますので、書いてある内容が難しい、実装したいけど処理が読めない、という場合にはこの手順は飛ばしてください
自分のスキルに合わせて学習を行っていただければ問題ありません。あとで振り返ってみてください。



 エネミー用のデータベースとして利用する目的で EnemyDataSO スクリプトを作成し、
EnemyDataSO スクリプタブル・オブジェクトを作成して、設定を行ってきました。

 スクリプタブル・オブジェクトにはこのようにデータベースとして利用できる部分の他に、
特定のゲームオブジェクトに依存しない(紐づかない)、という特性を生かして、
関連するメソッドを登録してメソッドを扱うデータベースとして利用する方法があります。

 現在エネミーの移動については EnemyController スクリプトに記述をしてきました。
今の所2つの移動方法がメソッドとして作成されていますが、これには2つの大きな問題点があります。

 まず1つが、移動方法が1つ増えるたびに、EnemyController スクリプト自体が肥大化してしまうということ、
もう1つは、エネミーが利用する移動方法は1つのはずなのに、他に利用しない移動の情報も管理しているということ、この2つです。

 例えばエネミーの種類を増やして移動方法を5種類に増やす、と仮定して考えてみてください。
5つの移動用のメソッドを EnemyController スクリプトに追加することになりますが、
実際に5つのメソッドをすべて利用するエネミーは存在しません。いずれか1つの移動用のメソッドがあれば充分なはずです。

 そしてメソッドが増えるということは、EnemyController スクリプトに記述される処理が必然的に増えていきます。
移動処理以外のメソッドはエネミーの管理や運行に合わせて必要なメソッドや変数で構成されていますので、
本来は必要な情報のみで1つのスクリプトが構成されていることが理想的です。

 この部分の問題点を解決する方法の1つとして、今回はスクリプタブル・オブジェクトを利用します。
もちろん、他にも方法はありますので、いくつかある中の1つ、として捉えておくことが大切です。視野は広く持っておきましょう。



 スクリプタブル・オブジェクト内にエネミーの移動用のメソッドを用意し、エネミーの移動方法に合わせて適切なメソッドを準備します。
EnemyController スクリプト側では、スクリプタブル・オブジェクトより、エネミーの移動方法に合わせたメソッドを1つ、取得します。
そしてそれを実行する、という流れになります。

 この処理を実装するためには、UnityAction という Unity が用意している処理を利用します。
UnityAction はデリゲートと呼ばれる C# プログラムの機能を利用している処理です。
デリゲートとはメソッドを参照する型のことを言います。
 
 実際に利用する場合には、メソッドを登録しておける変数、というニュアンスで考えてもらっても問題ありません。
変数の値としてメソッドを登録しておくことで、その変数を実行する形で、登録されているメソッドを実行することができる処理ととらえるといいです。

 実際に作成しながら処理を見ていきます。


2.MoveEventSO スクリプトを作成する


 スクリプタブル・オブジェクトを作成するために必要な情報や記述の方法は覚えていますか?
発展2 に詳細な手順がありますので、復習しながら処理を記述していきましょう。

 今回はデータベースとして利用する訳ではありませんので、List や入れ子クラスなどの宣言は不要です。
代わりに、今まで EnemyController スクリプトに記述されていた移動方法によって分岐を行うメソッドと、移動の処理を行うメソッドを記述方法を変えて用意します。

 変数では const キーワードを利用しています。このキーワードを宣言した変数の値は定数として扱われるようになり、ゲーム内での値の変更が出来なくなります
そのため、ゲーム内で利用する際に定値(ゲーム開始から終わりまで変わらない値)を扱う場合には宣言しておくことで、値の変更を防ぐことが出来ます。

参考サイト
Microsoft const (C# リファレンス)
https://docs.microsoft.com/ja-jp/dotnet/csharp/lan...


MoveEventSO.cs

 <= クリックすると開きます


 スクリプトを作成したらセーブします。


3.MoveEventSO スクリプタブル・オブジェクトを作成する


 Unity の左上のメニューより、Assets => Create => Create MoveEventSO を選択します。
新しく MoveEventSO というファイルが作成されます。名前はそのままで構いません。

 MoveEventSO スクリプタブル・オブジェクトを Datas フォルダへ移動してください。
スクリプタブル・オブジェクトを作成したら、Datas フォルダ内で管理するようにします。

 データベースとして数値を管理するために利用する訳ではありませんので、設定は不要です。
MoveEventSO スクリプタブル・オブジェクトのインスペクターを見ても登録できるような情報はありません。


4.EnemyGenerator スクリプトを修正して、MoveEventSO スクリプタブル・オブジェクトを利用できる状態にする


 スクリプタブル・オブジェクトはいずれかのスクリプトに変数を用意することで利用可能になります。
エネミーの移動方法を設定し、移動処理を実行しているアセットになりますので、今回も EnemyGenerator スクリプトに
public 修飾子の MoveEventSO 型の変数を用意してインスペクターよりアサインできるようにしましょう。

 なお、型は MoveEventSO 型での宣言になりますが、アサイン可能なのは、MoveEventSO スクリプタブル・オブジェクトです。
MoveEventSO スクリプトは直接アサインできません。何故ならば、アサイン出来る情報は、ゲームオブジェクトか、特定のアセットに限られるためです。
(今までのアサインを思い出してください。ヒエラルキーにあるゲームオブジェクト、あるいはプレファブのみをアサインしているはずです)
混同しやすいので間違えないようにしてください。

 また無事にエネミーの種類ごとに List が作成されることが確認できましたので、
normalEnemyDatas 変数と bossEnemyDatas 変数については pulbic 修飾子ではなく、private 修飾子に変更しておいてください。


EnemyGenerator.cs

 <= クリックすると開きます。


 スクリプトを修正したらセーブします。



 EnemyGenerator ゲームオブジェクトのインスペクターを確認して、private 修飾子に変更した変数の表示がなくなり、
新しく public 修飾子で宣言した変数が表示されているかを確認してください。


EnemyGenerator ゲームオブジェクト インスペクター画像




5.EnemyGenerator ゲームオブジェクトの設定を行う


 MoveEventSO スクリプタブル・オブジェクト用の変数を宣言していますので、Datas フォルダにある
MoveEventSO スクリプタブル・オブジェクトをドラッグアンドドロップしてアサインして登録してください。

 MoveEventSO スクリプトは登録できません。スクリプタブル・オブジェクトの方を選択してください。


EnemyGenerator ゲームオブジェクト インスペクター画像



 以上で設定は完了です。これで MoveEventSO スクリプタブル・オブジェクトを利用することができるようになりました。


22.エネミーの移動方法に応じて MoveEventSO スクリプタブル・オブジェクトに用意されたメソッドを UnityAction に登録して移動を実行する

1.設計


 エネミーの移動用のメソッドをすべて MoveEventSO スクリプタブル・オブジェクトにまとめて記述しました。
こちらをエネミーのデータを参照したときのように、エネミーの MoveType の種類に応じて、対象となる移動用メソッドを取得して利用出来るようにします。
 
 ここでは先ほど説明した、UnityAction という機能を利用します。この型を変数を用意することで、移動用のメソッドを変数に代入することが出来ます。
そのため、EnemyController スクリプトにある移動用のメソッドを一切使わずに、移動の処理を実行出来るようになります。

 手順としては、エネミーにある EnemData クラス内にある MoveType を利用して、どの移動方法にするが分岐する部分は同じですが、
ここでメソッドを実行するのではなく、移動用のメソッドを UnityAction 型の変数に登録するようにします。

 その後、EnemyController スクリプトでは登録されたメソッドを実行します。
このようにすることで、EnemyController スクリプトには、移動用のメソッドを書くことなく、エネミーの移動方法に応じた移動用のメソッドを処理出来るようになります。

<ロジックの流れ>
 1.EnemyController スクリプトに UnityActon 型の変数を用意する
 2.EnemyController スクリプトから、MoveEventSO スクリプタブル・オブジェクトの GetMoveEvent メソッドを実行する。引数として、エネミーの移動方法(MoveType)を渡す
    この処理を実行するに際し、GetMoveEvent メソッドには戻り値として UnityActon 型で移動用のメソッドを取得して戻してくれるので、受け取って登録するための UnityActon 型の変数を左辺に用意しておく
 3.EnemyController スクリプトにて、UnityActon 型の変数に登録された移動用のメソッドを実行する。エネミーの移動方法に応じた移動が処理される

 以上のような流れになります。

参考サイト
Unity公式スクリプトリファレンス
UnityAction
https://docs.unity3d.com/ja/current/ScriptReferenc...
Kan のメモ帳 様
デリゲート(Delegate)やイベント(Event)とは【C#】
https://kan-kikuchi.hatenablog.com/entry/Delegate


2.EnemyController スクリプトを修正する


 UnityActon 型を宣言するためには using に宣言の追加が必要になります。

 UnityActon 型には <T> というジェネリック型(任意の型)での型パラメータの指定が出来ます。最大で4つまで指定できます。
今回は UnityAction<Transform, float> 型という型になり、処理を実行する際に型パラメータで指定した型の情報が必要になります。


EnemyController.cs


 スクリプトを修正したらセーブします。


3.UnityAction に関する一連の処理を読み解く


 処理を順番に読み解いてみましょう。

<EnemyController.cs>
  // 変数を用意する
  private UnityAction<Transform, float> moveEvent; 

 ここに移動用のメソッドを取得して代入します。

  // MoveEventSO スクリプタブル・オブジェクトの GetMoveEvent メソッドを実行し、戻り値で移動用のメソッドを受け取る。ここで移動方法を決定
  moveEvent = this.enemyGenerator.moveEventSO.GetMoveEvent(enemyData.moveType);

 ↓ 右辺の処理は、以下の通り

<MoveEventSO スクリプタブル・オブジェクト GetMoveEvent メソッド。moveType で分岐し、UnityActon<Transform, float> 型で値が戻る>
public UnityAction<Transform, float> GetMoveEvent(MoveType moveType) {
  switch (moveType) {
      case MoveType.Straight:
          return MoveStraight;
        case MoveType.Meandering:
            return MoveMeandering;
        case MoveType.Boss_Horizontal:
            return MoveBossHorizontal;
        default:
            return Stop;
    }
}

  ↓ 右辺の処理が終了すると、エネミーの moveType に応じた移動用のメソッドが1つだけ戻り値として取得できる

<EnemyController.cs>
  // MoveEventSO スクリプタブル・オブジェクトの GetMoveEvent メソッドを実行し、戻り値で移動用のメソッドを受け取る。ここで移動方法を決定
 // どちらも UnityAction<Transform, float> であるので、代入が成立する
  moveEvent = UnityAction<Transform, float> 型のメソッドが1つ戻ってくる

  ↓ moveEvent 変数には移動用のメソッドが登録されているので、Invoke メソッドを実行して処理をする
  // Invoke メソッドを実行すると、moveEvent 変数に登録されたメソッド(今回は移動用のメソッド)を実行する。UnityActon <Transform, float>型なので、実行にあたって、指定された型の情報を指定する
  moveEvent.Invoke(transform, enemyData.moveDuration);

 変数の中にメソッドが登録できる、というのは不思議に思えるかもしれませんが、以前からこれに近い処理は実装しています。
Button クラスの AddListener メソッドがこれに当たります。引数に、ボタンを押した際に実行したいメソッドを登録していたはずです。



 今回の UnityAction の機能でポイントとなるのは、移動用のメソッドに対して必要な引数の情報を型パラメータという形で指定していることです。
UnityAction<Transform, float> 型とは、すべての移動用のメソッドで使用する引数の型を指定していることが分かります。

 確認してみましょう。

<MoveEventSO スクリプタブル・オブジェクト>
    public void MoveStraight(Transform tran, float duration) {
        //Debug.Log(tran);
        Debug.Log("直進");

        tran.DOLocalMoveY(moveLimit, duration);
    }

 デリゲートには複数のメソッドを登録して順番に実行するという機能もありますし、実行するタイミングを自分で設定できるので、活用できると便利な機能になります。
少しずつ覚えていきましょう。 


4.ゲームを実行して動作を確認する


 リファクタリングをおこなったので、ゲーム上の機能は何も変わりません。
そのため、この処理を実行した際に、今までと同じように動くことが大切です。

 もしも以前のように上手く移動が機能しないのであれば、それはリファクタリングの際に記述誤りがあります。
処理を1つずつ順番に追っていって、どの部分で正常に動いていないかを特定し、エラー箇所を探して修正してください。


5.<蛇行処理の補足>


 蛇行移動の処理について補足説明します。

    private void MoveMeandering(UnityEngine.Transform tran, float duration)
    {
        Debug.Log("蛇行");

        tran.DOLocalMoveX(tran.position.x + 150, 1f).SetLoops(-1, LoopType.Yoyo).SetEase(Ease.Linear);  //<= ここ。DOLocalMoveX(tran.position.x + 150, 1f)で動く距離は固定している
        tran.DOLocalMoveY(moveLimit, duration);
    }

 ソースコードでは左右の移動幅は固定しているはずですが、ゲームを実行すると、同じ敵であっても異なる移動幅になっています。


<検証動画>
動画ファイルへのリンク


 この処理は動いてはいますが、実は、DOTween の第1引数に対して、正しくない指定をわざとしている処理になります。
 
 DOTween で利用しているメソッドは、DOLocalMoveX メソッドです。
そのため、本来であれば第1引数には、移動させたいゲームオブジェクトの LocalPosition を指定する必要があります。
これは利用するDOTween のメソッドにより、引数の指定が決まるためです。

 ポイントは利用しているメソッドと引数の指定です。
DOMove メソッドであれば第1引数はワールド座標(通常の Position) を指定します。
ただし、DOLocalMove メソッドのように、Local の単語がメソッドにあれば、第1引数にはローカル座標(localPosition) を指定することで、本来の正しい処理になります。
これは他の DOTween の場合も同様です。名前をヒントに、利用する引数が変わります。

 ですが今回は、tran.position の形で、Local ではなく、ワールド座標での Position を指定しています。

tran.DOLocalMoveX(tran.position.x + 150, 1f).SetLoops(-1, LoopType.Yoyo).SetEase(Ease.Linear);

 そのため、計算がうまくいかず、移動の幅がその都度、ちぐはぐになっています。

 ただし、移動自体はするのと、規則性のなさが敵の移動っぽくていいと感じたので
今回はこれを、敵のランダム性という部分で利用しようとして、そのままわざと正しくない指定をしています。



 以上のことから、プログラム内で定数で移動幅をしているのに、移動幅が固定ではなくなぜ変わるのか? というと
引数の指定先が正しくないため、毎回、不安定な挙動になっている」ということになります。

 下記のように DOLocalMoveX メソッドの第1引数を正しい指定先に修正することで、毎回、同じ幅だけを移動する処理になります。

tran.DOLocalMoveX(tran.localPosition.x + 150, 1f).SetLoops(-1, LoopType.Yoyo).SetEase(Ease.Linear);


6.<DOAnchorPos メソッド>


 基本的に Canvas 内でのオブジェクトの移動は、常に Canvas か、あるいは他のオブジェクトの子オブジェクトになっているものを動かす可能性が高いので、
親からみた座標で相対的に移動ができる Local を利用するのが正しいですし、そうすることですべて同じ挙動になります。

 また、Canvas のオブジェクトは Anchor の位置によってオブジェクトの位置が設定されます。
そのため、アンカーが中央値(Middle、Center) でない場合、DOLocalMove で移動させると予期しない方向へ移動することがあります。

 これを回避するため、DOTween には DOAnchorPos という、UI 用の移動のメソッドが用意されています。
こちらを利用するもの1つの方法です。こちらも DOMove と同じで、3軸まとめて動かすメソッドと、各軸を指定して動かすメソッドがそれぞれあります。

 その場合、メソッドの命令先は Transform ではなくて、RectTransform に対して実行命令を出す形になります。
その代わりに、DOAnchorPos であれば、Anchor の位置に関係なく、LocalPosition で指定した位置に正しく移動できます

 下記のように利用できます。試しに書き換えてみてください。

private void MoveMeandering(UnityEngine.Transform tran, float duration)
{
    Debug.Log("蛇行");

    // DOAnchorPosX、DOAnchorPosY の場合
    RectTransform rectTran = tran.GetComponent<RectTransform>();
    rectTran.DOAnchorPosX(tran.localPosition.x + 150, 1f).SetLoops(-1, LoopType.Yoyo).SetEase(Ease.Linear);
    rectTran.DOAnchorPosY(moveLimit, duration);
    
    // DOLocalMoveX の場合
    //tran.DOLocalMoveX(tran.localPosition.x + 150, 1f).SetLoops(-1, LoopType.Yoyo).SetEase(Ease.Linear);
    //tran.DOLocalMoveY(moveLimit, duration);
}


<修正後の動画 ー同じ移動幅に固定化されるー>
動画ファイルへのリンク


参考サイト
原カバンは鞄のお店ではありません。 様
【Unity】DOTweenを使ったuGUIアニメーション
Zenn オオバ@ohbashunsuke 様
uGUI - RectTransformのトゥイーン|【Unity】DOTweenの教科書〜スクリプトでアニメーションを操るバイブル〜



 以上でこの手順は終了です。

 次は 発展12 エネミー用のバレットの作成と自動生成処理の実装− です。