i-school - 2Dトップビューアドベンチャー 手順15
 3回に分けて、プレイヤーキャラと NPC(ノン・プレイヤー・キャラクター)との会話イベントの実装を行います。

 今回の手順では宴アセットのインポートを行い、アセットの会話イベントの機能を呼び出すためスクリプトを作成します。
この手順で事前に会話イベント用の処理を作成しておき、次の手順において、ボタンを押したタイミングで会話イベントが発生するように制御を行います。

 実装する処理、および処理の考え方についてはかなり深い部分での学習になりますので、1回の学習で難しくても復習しながら進めていってください。


<実装動画>
動画ファイルへのリンク


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

手順15 ー宴アセットの準備ー
18.宴アセットのインポート
19.宴のシナリオ再生エンジンを呼び出すための AdvEngineController スクリプトを作成する



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

 ・宴アセットの設定・シナリオ再生エンジンの準備
 ・シングルトン・デザインパターンによるクラスの運用方法
 ・UnityAction(デリゲート)の実装例



18.宴アセットのインポート

1.設計


 すべての手順を1回の実装で行う必要はありません。
処理の全体図を俯瞰的に管理して、その中に必要となる処理を1つずつ組み込んでいきます。

 実装したい処理のゴール地点を設定し、そのゴールにたどり着くにはどのような処理を積み重ねていけばよいかを考えてみてください。
そして処理を逆算していく中で、どんな制御があればいいのか、という設計ロジックを組み立ていきます。


2.宴アセットをアセットストアで購入してインポートする


 Unity のアセットストアで検索し、宴を購入してインポートします。

 検索窓に UTAGE と入力すると見つかります

 現時点(2022年6月)における正式なアセット名は「UTAGE3 for Unity Text Adventure Game Engine Version3」です。
直接の URL は以下の通りです。


<アセットストア 宴>
https://assetstore.unity.com/packages/tools/game-t...


アセットストアでの表示



 PackageManager 経由でアセットのインポートが完了すると、Project タブ内の Assets フォルダ内に Utage フォルダが作成されます。


フォルダ


 以上でインポートは完了です。


3.宴のマニュアルとサンプルシーンを確認する


 これから実装を行うにあたり、アセットの内容をしっかりと理解しておく必要があります。

 マニュアルには必ず目を通しておきましょう


<Unity用ビジュアルノベルツール「宴」>

ホーム




 Unity のアセットには必ずサンプルシーンが含まれています
こちらには多くのヒントがありますので、サンプルシーンも確認しておきましょう。

 宴の場合には、ビジュアルノベルのような機能を利用して、サンプルシーンが提供されています。
そのため、実際に実装した際のイメージがわかりやすいようになっています。


4.現在のシーンに宴アセットの設定を追加する


 宴を会話シーンのイベントとして利用する場合、現在制作しているシーンに、宴の機能(シナリオ再生エンジン)を追加する形で設定を行います。

 下記の公式マニュアルの手順に沿って進めてみてください。


会話シーンとして宴を使う


 同じ手順を、こちらにも提示しておきますが、まずは1人で実装が行えるか挑戦してみましょう。



 Unity の左上のメニューより、Tools > Utage > New Project から新規プロジェクトを作成します。


コマンド



 New Project のウインドウが開きます。


New Project ウインドウ


 ウインドウ内を上から順番に設定していきます。



 New Project ウインドウ内の Input New Project Name が、プロジェクトの名前です。こちらに任意の名称を付けます。
分かりやすいように今回は TopView とします。このままフォルダの名前にもなるため、日本語は NG です。


Input New Project Name を入力




 次に Select Create Type の下にあるプルダウンメニューを選択し、Add To Current Scene を選択します。
選択すると、ウインドウ内の表示内容が更新されます。


Select Create Type で Add To Current Scene を選択



 この選択をすることで、現在のシーンに宴の機能であるシナリオ再生エンジンの機能を追加出来ます
間違えないようにしてください。



 Layer Setting は宴用の Layer の設定です。宴では専用の Layer を作成する必要があり、ここで設定しています。
こちらは初期値のままでも問題ありません、任意の名前に変更してもよいです。ここでは初期値のままで利用しています。


Layer Setting





 Game Screen Size は、自分のゲーム画面のサイズ(解像度)の設定を行います。
教材のゲームでは 2880 * 1440 の解像度で Game シーンを設定しているので、そちらに変更しています。


Game Screen Size 自分のゲーム画面のサイズ(解像度)の設定をすること





 Font は宴で利用するフォントを設定を行います。現時点では初期値のままで問題ありません。
後程フォントをインポートしてから、変更を行います。


Font





 最後の Security も初期値のままで問題ありません。


 以上で設定は完了です。


設定完成時




 すべての設定を行ってから、左下にある Create のボタンを押して宴の機能を追加します。
ボタンが押せない場合には、設定が足りていない部分がありますので、見直してください。

 Unity の読み込みが入り、追加に成功すると Assets内に Input New Project Name で設定した名前のフォルダが追加されます。


フォルダの追加



 以上で宴の設定は完了です。



 フォルダの追加に合わせて、ヒエラルキーにも新しく AdvEngine Starter という名前のゲームオブジェクトが1つ追加されます。
確認しておきましょう。


ヒエラルキー画像



インスペクター画像

 


5.Excel ファイルの確認をする


 会話イベントのデータの作成については、手順17?にて行います。

 今回の手順では Excel ファイルの確認のみを行い、どのようなデータがあれば会話イベントとして再生されるのかを確認しておきます。

 TopView フォルダ内にある TopView ファイルを開きます。Excel ファイルになりますので、
Excel、あるいは Excel を開くことができるアプリを起動してファイルを開いてください。

 画面下部のシートが各役割を持っています。その中にある Start シートを選択してください。


Excel



 これが Start というラベルとなり、この Start というラベルをスクリプト内で選択すると、
このシートの内容が宴内に会話イベントとして順番に表示されていくようになっています。

 実際に動作した方がわかりやすいですが、Excel のシートと宴の機能が連携して会話イベントが発生するイメージを掴んでおいてください。


19.宴のシナリオ再生エンジンを呼び出すための AdvEngineController スクリプトを作成する

1.宴のシナリオ再生エンジンを呼び出すための AdvEngineController スクリプトを作成する


 再度マニュアルを確認しながら進めます。

 ページの中段にある「ゲーム側から宴を呼び出す」という部分を確認してください。

会話シーンとして宴を使う


 こちらの説明にもあるように、直接エンジンを参照するよりは、専用のスクリプトを用意しておくことが推奨されています。
利便性なども考えたときに、各スクリプトにエンジンの参照を用意して処理を記述するよりは、専用のスクリプトにアクセスをさせた方が処理をつくりやすいからです。

 宴のマニュアルにあるサンプルコードを元に、宴のシナリオ再生エンジンをいつでも実行できるようにするためのスクリプトを作成します。

 参照しやすいようにシングルトンクラスとして作成しています。
またデリゲートについては Action ではなく、Unity 用の UnityAction としてデリゲートを用意しています。

AdvEngineController.cs

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


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


2.<シングルトン・デザインパターンによるクラスの運用方法>


 シングルトンとは、数多くあるデザインパターンの1つです。
そのクラスのインスタンスが必ず1つであることを保証するデザインパターンのことを言います。

 LogDebugger クラスでは、このシングルトンを採用しています。
つまり、ゲーム中を通じて、この LogDebugger クラスが1つしか存在できないようになります。
実装例は複数ありますが、一番読みやすい方式で記述しています。



<シングルトンデザインパターンのクラスの作成方法>
    public static LogDebugger instance;  // クラス名と同名の型を static で宣言する

    private void Awake() {
        if (instance == null) {
            instance = this;
            DontDestroyOnLoad(gameObject);
        } else {
            Destroy(gameObject);
        }
    }



 ポイントは、自分自身の LogDebugger 型を static 修飾子付きの instance 変数として宣言していることです。
この instance 変数が LogDebugger クラス自身が代入された情報として利用することになります。

 Awake メソッドを利用して、instance 変数が null (空っぽ) である場合には、LogDebugger クラス(this)を代入します。
次の DontDestroyOnLoad メソッドは Unity が用意しているメソッドで、引数に指定されたゲームオブジェクトはシーン遷移をしても破壊されてないゲームオブジェクトになります。
この DontDestroyOnLoad メソッドはシングルトンデザインパターンにする際に一緒に用いられることが多いです。

 そして instance 変数が null ではない場合、つまり、2つ目以降の複数の LogDebugger クラスが存在する場合には、その LogDebugger クラスのゲームオブジェクトを Destroy します。
この手順により、LogDebugger クラスがアタッチされているゲームオブジェクトが常にヒエラルキー上に1つしか存在しない状態を作り出しています

 このシングルトンによってインスタンスが1つか生成されないことが保証されますので、
逆説的に考えると、この LogDebugger クラスへの参照は、いずれのクラスからであっても変数を介さずに参照を行えるようになります。



 例えば、NonPlayerCharacter というクラスがあり、その NonPlayerCharacter クラスを持つゲームオブジェクトが5つあった場合、
どの」NonPlayerCharacter クラスであるかを確定できないと、対象となる NonPlayerCharacter クラスへは参照できません。
そのため、NonPlayerCharacter 型の変数を用意して、その変数へ参照したい NonPlayerCharacter クラスを代入することによって、
はじめて NonPlayerCharacter クラスの情報を扱うことができるようになります。これが情報を扱う際の基本的な処理になります。

 ですがシングルトンである LogDebugger クラスの場合には、このインスタンスは常に1つしかないことが保証されていますので、
どの」という指定の部分が不要になります。よってクラスの特定ができているため、変数への代入が不要になります。
LogDebugger という指定はすなわち、自動的にただ1つの LogDebugger クラスの参照が行われることになるためです。

 この機能を利用して LogDebugger クラスを作成しておくことで、どのクラスからでも参照しやすい設計にしておきます。


3.<UnityAction の実装例>


 UnityAction型はUnityが用意しているデリゲートです。デリゲートはわかりやすくいうと、メソッドを代入することができる型(変数)です。

 UnityAction型はUnityEngine.Eventsとして名前空間に定義されていてますので、使用する場合には宣言が必要です。
一時的に使うだけならそのときに1回だけ宣言しても利用できますが、usingで宣言しておきましょう。

 using UnityEngine.Events;


 UnityAction型はデリゲートですので、下記のようにメソッドを代入することが出来ます。

 戻り値を持つメソッド
  public UnityAction GetSkill(SkillType chooseSkillType) {
      switch (chooseSkillType) {
          case SkillType.DeleteMaxEtoType:
	      return DeleteMaxEtoType;      <= DeleteMaxEtoTypeメソッドをUnityAction型として代入している

          // TODO スキルが増えた場合には追加する
      }
      return null;
  }

 そのため次のメソッドの引数には、DeleteMaxEtoTypeというメソッドがUnityActionの値として代入されていることになります。


 public IEnumerator SetUpSkillButton(UnityAction unityAction) {  <= この変数には、GetSkill(skillType)の処理結果 = DeleteMaxEtoTypeというメソッドが代入されている

   // UnityEventにunityActionを登録(UnityActionにはメソッドが代入されている)
      unityEvent.AddListener(unityAction); <= このAddListnerメソッドに登録される値は、DeleteMaxEtoTypeというメソッドになります

 デリゲートを使用することによって、メソッドも変数の値として代入し、他の引数と同じように同じ型内での受け渡しが可能になります。



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

<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.新しい AdvEngineController ゲームオブジェクトを作成し AdvEngineController スクリプトをアタッチする


 ヒエラルキーの空いている場所で右クリックをしてメニューを開くか、ヒエラルキーにある + ボタンを押して
Create Empty を選択し、新しいゲームオブジェクトを1つ作成します。名前は AdvEngineController に変更します。

 こちらに AdvEngineController スクリプトをドラッグアンドドロップしてアタッチしてください。
アタッチしたあとには必ず、対象となったゲームオブジェクトのインスペクターを確認して、アタッチされているかを確認してください。



ヒエラルキー画像



インスペクター画像



 これで会話イベントを実行するための機能については完成です。

 次の手順では、キャラが向いている方向に NPC が存在しているかを感知し、
存在している場合には AdvEngineController スクリプトにアクセスして、宴による会話イベントを実行する機能を追加します。



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

 次は 手順16 −NPC との会話イベントの実装− です。