i-school - 2DタイルマップRPG 手順30
 会話イベントのメッセージを複数ページ設定して、1ページずつ順番に表示する機能を実装します。
ポイントとしては、1ページ当たりの文字数の量に関わずに、表示される文字の速度を一定にすることです。


<実装動画 速度が一定でないと、文字の数が多いと表示が早くなってしまう>
動画ファイルへのリンク


<実装動画 文字の数に関わず、表示される速度を一定にする>
動画ファイルへのリンク


手順30 ー複数ページのメッセージ表示とページ送り機能機能の実装ー
52.EventData スクリプトを修正し、複数のページ分の文字列を登録できるようにする変数を追加する
53.DialogController スクリプト、NonPlayerCharacter スクリプト、UIManager スクリプト、PlayerController スクリプトを修正し、
   会話イベントのメッセージを複数ページ表示し、ページ送りをする処理を追加する



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

 ・String.Length プロパティ
 ・WaitUntil クラス



52.EventData スクリプトを修正し、複数のページ分の文字列を登録できるようにする変数を追加する

1.設計


 現在の EventData クラスには複数のページ分を登録できる変数がありません。
こちらを修正し、1つの変数内に複数のページの情報を登録して管理できるようにします。

 異なる変数で管理を行わないのは、ページの数がイベントによって増減するためです。
例えば、3ページあるイベントと1ページのイベントの場合、多い方のイベントに合わせて変数を用意することになり、
1ページ分のイベントの場合には、不要となる変数が出てしまいます。

 このように、同じ情報の塊で、かつ、その内容が可変するような値については配列や List を利用することを考えていくようにしてください。

 今回は string 型の配列を用意して、イベントに応じてその要素数を変更することで、各イベントのページ数に対応できるような設計にしています。


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


 dialog 変数の型を配列にして、複数のページを登録できる状態にします。変数名も複数形に変更します。
1つの配列の要素が1ページ分のメッセージとして運用するようにします。


EventData.cs

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


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



53.DialogController スクリプト、NonPlayerCharacter スクリプト、UIManager スクリプト、PlayerController スクリプトを修正し、会話イベントのメッセージを複数ページ表示し、ページ送りをする処理を追加する

1.設計


 会話イベントのメッセージの表示機能を実行しているのは DialogController スクリプトです。
そのため、修正が必要な部分としては、DialogController スクリプト内にあるメッセージの表示用のメソッド内の処理を見直すことが大切になります。
 
 メソッドを修正する、ということは、その呼び出し元である他のスクリプトも合わせて見直す必要がある、ということにもなります。
このとき、メソッドの引数が変更になっていたり、メソッドの戻り値が変更になっている場合には、必然的にメソッドの呼び出し側のスクリプトも修正が必要になります。

 この DialogController スクリプトのメソッドについては、引数として文字列の情報が渡されていますが、この情報は1ページ分の情報です。
今回は複数のページに対応するようになっているので、この部分も見直し対象となります。

 処理を変更する場合、このように1つずつ、処理を行っている場所と、その処理に行きつくまでに実行されるメソッドをすべて洗い出して
修正が必要になるかどうかを精査していくことが重要です。
 
 では、会話イベントの一連の処理の流れをスタートからゴールまで見ていきましょう。

<会話イベントの流れ>
 1.PlayerController スクリプトにおいて、NonPlayerCharacter スクリプトの PlayTalk メソッドが実行される
 2.NonPlayerCharacter スクリプトにおいて、会話イベントウインドウが固定型の場合には UIManager スクリプトの OpenTalkWindow メソッドが実行され、その中でさらに DialogController スクリプトの DisplayDialog メソッドが実行される
   稼働型のウインドウの場合には、DialogController スクリプトの DisplayDialog メソッドが実行される
 3.DialogController スクリプトの DisplayDialog メソッドにおいて会話イベントのメッセージが表示される。表示される会話のメッセージの文字列は、NonPlayerCharacter スクリプトから引数として渡されるものを利用する
 4.メッセージ表示後に DialogController スクリプトの HideDialog メソッドにおいて会話イベントを終了するメソッドが実行される

 現在の処理の内容として、4つのスクリプトのメソッドを実行していくことによって会話イベントが実行されていることがわかります。
そのため、今回のように会話イベントイベントのメッセージの処理方法を変更する場合には、1〜4のすべての処理を見直して、修正を行っていく必要があると言えます。

 この一連の流れをしっかりと理解しておかないと、処理を修正することが出来ませんので、もう一度、処理の流れを見直してみましょう。



 処理の修正については、ゴール地点である【4】の処理から見直していきます。このとき【3】も同じスクリプトになりますので
まずは DialogController スクリプト内の処理を見直して修正を行うようにします。その後、手前に戻りながら修正をしていくことで、
必要な処理が完成している状態で進めていくことが出来るようになります。


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


 いままでは DisplayDialog メソッドにおいてウインドウの表示とメッセージの表示を行っていましたが、
今回はメッセージの表示については新しくメソッドを用意して利用するようにロジックを変更します。

 これはページ送りの機能を実装するに当たり、コルーチンの処理を利用して「アクションボタンを押すまでページ送りを待機させる」実装を行いたいためです。
そのため、DisplayDialog メソッド自体もコルーチンメソッドとし、新しく作成する PlayTalkEventProgress メソッドもコルーチンメソッドとしています。

 またすべてのページが表示されているかどうかを判定するための変数と、会話中であるかどうかを判定するための変数をそれぞれ bool 型で用意しています。
この値を判定して、各ページが表示されているのか、また、全ページが表示されて会話がおわっているのか、というゲーム内の状態を変数によって作りだすことで、状態を使った判定をできるようにしています。

 なお、アクションボタンには通常は NPC に話しかけるという機能が実装されていますので、
会話イベント中にはこの機能を一時停止する処理を実装することで、アクションボタンをページ送りの機能として利用するようにしています。



 表示する文字数が増えた場合にも対応できるように、DOText メソッドの第2引数の指定方法を変更しています。
いままでは文字数に関係なく固定値で 1.0 秒で表示していたので、1ページ当たりの文字数は反映されず、
そのため、文字数が少ないときと、文字数が多いときで速度が変わってしまっていました。

 解決策としては、1ページの表示にかける時間を固定するのではなく、
1ページ当たりの文字数を反映して表示する時間を自動的に変更するという方法を検討します。

 今回の実装では「1ページ当たりの文字数 × 表示時間」という書式を利用して第2引数の表示時間として設定することにより、
1ページ当たりの文字数に合わせて自動的に速度が一定になるように変更しています。


DialogController.cs

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


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


3.<String.Length プロパティ>


 文字列の長さ(文字数)を取得することができます。

  // 1ページ分の文字を、1文字当たり 0.1 秒ずつかけて等速で表示。表示終了後、isClick を true にして文字が全文表示された状態にする
  txtDialog.DOText(dialog, dialog.Length * wordSpeed).SetEase(Ease.Linear).OnComplete(() => { isClick = true; });

 dialog 変数は string 型ですので、こちらに Length プロパティを適用することで、各ページ当たりに用意されている文字数を取得できます。
その値に wordSpeed 変数を乗算しているため、1ページ当たりの文字数に応じて、画面に表示する文字が自動的に一定の速度になります。

 例えば、dialog.Lenght の値が 10 である場合、そのページの文字数は 10 文字になります。
この値に wordSpeed = 1.0f を乗算した場合、10 * 0.1f で 1.0 秒になります。
このように Lenght の値によって時間が変わるため、常にその時間をかけて文字を表示することで文字数の大小にかかわらず、一定の速度で文字が表示されます。

参考サイト
MicroSoft
String.Lenght
https://docs.microsoft.com/ja-jp/dotnet/api/system...
初心者エンジニアのための備忘録 様
[C#] 文字列処理 文字列の長さ取得(Length)
https://turtle-engineers.com/c-length/


4.<WaitUntil クラス>


 WaitUntil クラスは、コルーチン内で命令できる待機処理です。yield return をつけて new (インスタンス)します

 WaitUntil クラスにはコンストラクタ・メソッドがあり、インスタンスされると、引数で指定した条件を満たすまで、処理を一時中断して待機させることが出来ます。

  // 1ページの文字が全文表示されている場合かつ、アクションボタンを押すと次のメッセージ表示
  yield return new WaitUntil(() => Input.GetButtonDown("Action") && isClick);

 今回の場合の条件は、【アクションボタンを押した、かつ、isClick 変数が true になったら】という2つの条件を満たすことが必要になります。
1ページ分のメッセージが全文表示されることと isClick 変数は true になりますので、メッセージが全文表示中に画面をクリックすることにより、処理は再度再開されて、次の処理に移ります。
今回は foreach 文内の処理になっていますので、次の処理とはすなわち、foreach 文の先頭に戻り、次の反復処理を実行する、という内容になります。
次に表示するべきメッセージない場合には、そこでメッセージの表示は終了します。

参考サイト
Unity公式スクリプトリファレンス
WaitUntil
https://docs.unity3d.com/ja/current/ScriptReferenc...


5.NonPlayerCharacter スクリプトを修正する


 DialogController スクリプトと UIManager スクリプトにあるメソッドを実行する部分を修正しています。
DialogController スクリプトにある DisplayDialog メソッドについては、コルーチンメソッドに変更されているので、
呼び出す命令もコルーチンメソッドを呼び出す書式に変更を行います。

 このとき、引数として NonPlayerCharacter スクリプト自身を渡すことにより、DialogController スクリプトでの会話イベント終了後に、
その処理の実行を行った NonPlayerCharacter スクリプトへと処理を戻すことが出来るようにしています。

 スクリプトのメソッドを実行すると、呼び出し側には呼び出し先の情報はありますが、
反対に、呼び出された側には呼び出し元の情報があるとは限りません。

 NonPlayerCharacter スクリプトは DialogController スクリプトの存在を知っていて変数で管理していますが、
DialogController スクリプトの方は、NonPlayerCharacter スクリプトの存在は知らない、という状態になっているのです。

 そのため、DialogController スクリプトから呼び出し元である NonPlayerCharacter スクリプトを知る、つまり情報を得るためには、
呼び出し元である NonPlayerCharacter スクリプトが DialogController スクリプトに自分自身(NonPlayerCharacter スクリプト)の情報を引数を介して渡すことが最も効率的な処理になります。

 このように相互に参照できる状態にしておくことにより、DialogController スクリプト側からも
NonPlayerCharacter スクリプトに対して命令を実行できるようになります。



NonPlayerCharacter.cs

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


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


6.UIManager スクリプトを作成する


 NonPlayerCharacter スクリプトより呼び出されるメソッドに引数を追加して、NonPlayerCharacter スクリプトの情報を
DialogController スクリプトに渡せるようにします。 

 また DialogController スクリプトの DisplayDialog メソッドがコルーチンメソッドになっているため、
呼び出し命令自体もコルーチンメソッドを呼び出す書式に変更しています。


UIManager.cs

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

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


7.PlayerController スクリプトを修正する


 アクションボタンを押して NPC に話しかけた際に会話中の状態にすることで
アクションボタンを押した際の処理を「会話メッセージを1ページ分送る」という機能に変更しています。

 この処理自体は DialogController スクリプト内に記述されていますので、
PlayerController スクリプト側では、アクションボタンの機能を「会話中の場合には機能させない」という状態にします。

 こうすることで1つのボタンの挙動を一元化し、処理の競合を防いでいます。

 また、先ほどの NonPlayerCharacter スクリプトと DialogController スクリプトとの関係と同じように
NonPlayerCharacter スクリプトに PlayerController スクリプトの情報を引数を介して渡すことにより、
相互の依存関係を構築して、NonPlayerCharacter スクリプト側からも PlayerController スクリプトを制御できる状態にしておきます。

 この関係を利用することで、会話イベントが終了したことを PlayerController スクリプトに通知してもらって
会話中の状態を終了させます。この時点で、アクションボタンは通常の操作の状態に戻ることになります。


PlayerController.cs

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


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


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


 処理の全容をしっかりつかんでから、どの部分をデバッグするのか把握した上でゲームを実行して確認を行っていきましょう。


<実装動画 文字の数に関わず、表示される速度を一定にする>
動画ファイルへのリンク





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

 次は 手順31 ーEventData 用のデータベースの改修ー です。
NPC との進行型の会話イベントを実装するための準備を行います。