【UE4】NPCとの会話システム実装

# 最初に

どうも、ろっさむです。

今回は、NPCとの会話システムを簡易的ではありますが、実装する手法についてまとめていこうと思います。

今回はNPCからのメッセージをただ表示させるだけですが、次回の記事ではここに選択肢も追加していきます。

## 開発環境

- UE4.26
- Windows10

# 作業手順

ざっと今回行う作業は以下の通りです。

1. 会話内容用の構造体の作成
2. 会話内容用のDataTableの作成
3. 会話ダイアログ用のウィジェットの作成
4. PlayerController内でのウィジェット作成処理
5. 話しかける処理の実装
6. 会話ループ処理の実装

少し長いですが、内容はそれほど難しくないので、順番に実装していきましょう。

## 会話内容用の構造体の作成

まずは任意のフォルダ下（例：`UI`フォルダ）にデータ格納用のフォルダを作成しておきましょう。
![image](https://user-images.githubusercontent.com/10665134/102275133-4ca59200-3f68-11eb-8983-56192eb6c56e.png)

-----------

`Data`フォルダ下にて、右クリック（又はAdd Newボタン）からメニューを表示し、`Blueprints > Structure`を選択します。
![image](https://user-images.githubusercontent.com/10665134/102275271-7fe82100-3f68-11eb-855c-34b7229aff8a.png)

今回の名前は`Struct_Dialog`にしました。
![image](https://user-images.githubusercontent.com/10665134/102275344-97bfa500-3f68-11eb-866b-12520befead0.png)

ダブルクリックして開き、データの土台を作成します。
![image](https://user-images.githubusercontent.com/10665134/102275391-a73eee00-3f68-11eb-9a45-3cdab0b5ee38.png)

1. `New Variable`を2回押下して、変数を作成。
2. 1つ目を`Tag`という名前にし、型を`Name`に設定。
3. 2つ目を`Dialog`という名前にし、型を`Text`の配列に設定（配列にするには③の四角で囲ってる部分を押下）

-----------
## 会話内容用のDataTableの作成


作成ができたら、次は実際にデータを入れるために`Data Table`アセットを作成します。

`Data`フォルダ下にて、右クリック（又はAdd Newボタン）からメニューを表示し、`Miscellaneous > Data Table`を選択します。
![image](https://user-images.githubusercontent.com/10665134/102275719-2e8c6180-3f69-11eb-990d-1708ae278a1b.png)

この時、どの構造体を選択するかを決めるダイアログが表示されますが、先ほど作成した`Struct_Dialog`に設定しておいてください。
![image](https://user-images.githubusercontent.com/10665134/102275838-57acf200-3f69-11eb-8e2c-78ee3c717c31.png)

今回のアセットの名前は`DT_Dialog`にしました。
![image](https://user-images.githubusercontent.com/10665134/102276015-917df880-3f69-11eb-8f45-ba7143ef33ac.png)

ダブルクリックして開き、データを作成します。
![image](https://user-images.githubusercontent.com/10665134/102276171-c8eca500-3f69-11eb-8ffa-61270211b3eb.png)

1. まずは`Add`を1回押して、データを1つ分作成します。
2. ここをダブルクリックしてデータ名を`Dialog0001`に変更します。
3. `Tag`には会話対象のNPCの名前など、判別用に任意の名前をつけてください。
4. `Dialog`にて実際に表示させたい会話文だけ`+`ボタンを押下して要素を追加し、文の中身を記入します。記入した数だけボタンを押して会話を進める数が増えます。

ここまで出来たらデータ作りは完了なのでSaveしてください。

あとは忘れないうちに、`Tag`でつけた名前をレベル上にいるNPCにつけましょう。
![image](https://user-images.githubusercontent.com/10665134/102276867-cd658d80-3f6a-11eb-9ae9-679c29c062ca.png)

-----------

## 会話ダイアログ用のウィジェットの作成

次に会話用ダイアログを表示するためのウィジェットを作成します。

任意のフォルダ（例えば`UI`フォルダ）下にて、右クリック（又はAdd Newボタン）からメニューを表示し、`User Interface > WBP_TextDialog`を選択します。
![image](https://user-images.githubusercontent.com/10665134/102277077-1f0e1800-3f6b-11eb-980e-a9290871534b.png)

今回のアセットの名前は`WBP_TextDialog`にしました。
![image](https://user-images.githubusercontent.com/10665134/102277113-2b927080-3f6b-11eb-99b7-b8df7657f10c.png)

まずは、ダイアログの要素を置くための`Canvas Panel`を追加しておきます（サイズや位置等は任意で変更してください）。
![image](https://user-images.githubusercontent.com/10665134/102277297-6a282b00-3f6b-11eb-9340-0ef4641bd10c.png)

`Anchors`を左上にセットしておきましょう。これで画面の大きさが変わっても位置が左上基準で固定されます。
![image](https://user-images.githubusercontent.com/10665134/102277372-86c46300-3f6b-11eb-922c-59565649eea5.png)

次に、セリフを表示するための`Text`を、先ほど作成した`Canvas Panel`にD&Dして、子要素にします。ついでにテキストの内容もプレビュー用に適当に変更しておきます。

![image](https://user-images.githubusercontent.com/10665134/102277481-b6736b00-3f6b-11eb-9162-584e737e045f.png)
![image](https://user-images.githubusercontent.com/10665134/102277524-c2f7c380-3f6b-11eb-9018-95fae16b963c.png)

テキスト内容は `Content > Text`から行えます。
![image](https://user-images.githubusercontent.com/10665134/102277621-e589dc80-3f6b-11eb-88d7-d3560b683f12.png)

この時点でCanvasはざっくり以下のような感じになっていると思います。
![image](https://user-images.githubusercontent.com/10665134/102277919-49140a00-3f6c-11eb-97b5-a3ca7493628d.png)

次に、この状態のままでは文字色と同じ色のゲーム背景が被った時に文字が見えづらくなるので、ダイアログに透過背景を作成します。

Canvasに`Image`を追加して、子要素にしてください。
![image](https://user-images.githubusercontent.com/10665134/102284482-95b11280-3f77-11eb-8305-d2ef43863ced.png)

![image](https://user-images.githubusercontent.com/10665134/102284600-c6914780-3f77-11eb-871d-34863209b917.png)

`Anchors`はCanvasの左端を基準にするように設定します。
![image](https://user-images.githubusercontent.com/10665134/102284686-f2143200-3f77-11eb-9546-7677a16bb935.png)

現在の状態だと、文字と背景が同じ表示優先度になっているため、背景を先に描画してから文字を描画するように指定します。
`Slot > ZOrder`を`-1`に設定してください。
![image](https://user-images.githubusercontent.com/10665134/102284790-2f78bf80-3f78-11eb-832d-187e85856a8c.png)

また、白色文字を見やすいように背景色をグレーなどにし、半透明程度にアルファも設定します。
![image](https://user-images.githubusercontent.com/10665134/102284877-60f18b00-3f78-11eb-8b1b-bf93db7cca46.png)

ここまでの設定を行うと、ざっくり以下のような感じになっているかと思います。
![image](https://user-images.githubusercontent.com/10665134/102289517-3c021580-3f82-11eb-87a3-87bb7149200e.png)

会話ダイアログとして使用するためには、テキスト部分を外部から差し替え可能な状態にする必要があります。ぴよぴよテキストの `Content > Text > Bind`を押下して、`Create Binding`を選択してください。
![image](https://user-images.githubusercontent.com/10665134/102289621-779cdf80-3f82-11eb-8147-397cb2d1370e.png)

自動でメソッドが作成されますが、名前を`GetDialogText`に変更しておきます。
![image](https://user-images.githubusercontent.com/10665134/102289997-6ef8d900-3f83-11eb-97cd-2a028f716746.png)

また、設定するテキストの変数も作成しておきます。名前を`DisplayText`に設定します。
![image](https://user-images.githubusercontent.com/10665134/102290063-8f289800-3f83-11eb-81cb-d58a95db9d20.png)

型は`Text`にしてください。
![image](https://user-images.githubusercontent.com/10665134/102290095-a2d3fe80-3f83-11eb-9eb2-c0a78705d92e.png)

あとは`GetDialogText`の戻り値に`DsiplayText`を接続するだけでOKです。
![image](https://user-images.githubusercontent.com/10665134/102290144-c0a16380-3f83-11eb-805e-8d53464053f7.png)

-------------

## PlayerController内でのウィジェット作成処理

作成したWidgetを使用するための初期化処理を作成していきます。この処理は今回は簡易実装なので、任意の`PlayerController`クラス内部で行います。ご容赦下さいませ。

まずは必要な変数を予め作成しておきます。

`Tag`という名前で変数を作成してください。
![image](https://user-images.githubusercontent.com/10665134/102290330-2b529f00-3f84-11eb-936c-0c9da26c7ced.png)

型は`Name`です。
![image](https://user-images.githubusercontent.com/10665134/102290347-39082480-3f84-11eb-8a2f-2312eb46b65d.png)

次に`TextDialogList`という変数を作成します。
![image](https://user-images.githubusercontent.com/10665134/102290393-4cb38b00-3f84-11eb-9640-dff6a04b63ef.png)

型は先ほど作成した`WBP_TextDialog`の配列型にします。
![image](https://user-images.githubusercontent.com/10665134/102290451-6e147700-3f84-11eb-83ce-0578f48a4747.png)

次に処理の実装を行っていきます。今回の初期化処理は`Event BeginPlay`から開始させます。

まずはデータテーブルの取得をBP内で行います。

`Get Data Table Row`というノードを作成し、`Data Table`に先ほど作成した`DT_Dialog`をセットしてください。
![image](https://user-images.githubusercontent.com/10665134/102290683-dc593980-3f84-11eb-8ede-42b0e1220188.png)

セットした後は`Out Row`ピンの上で右クリックをし、メニューから`Split Struct Pin`を選択します。
![image](https://user-images.githubusercontent.com/10665134/102290735-f8f57180-3f84-11eb-9840-05700b5a6a92.png)

そうすると、ピンの内容が`Tag`と`Dialog`に分割されるので、`Tag`を先ほど作成した変数の`Tag`の方にセットします。`Dialog`は`ForEachLoop`ノードを作成して、`Array`に繋ぎましょう。
![image](https://user-images.githubusercontent.com/10665134/102290844-2c380080-3f85-11eb-8ea9-9560e72946c7.png)

ここからウィジェットを作成する処理に移ります。`Create Widget`ノードを作成し、`Class`を`WBP_TextDialog`に設定します。
![image](https://user-images.githubusercontent.com/10665134/102290935-58ec1800-3f85-11eb-8c42-9cb00734bbaf.png)

次に以下の処理を追加します。

1. `ForEachLoop`ノードに繋ぎ、会話の数だけWidgetを生成するようにします。
2. 同時にWidget内部の`DisplayText`には`Dialog`のテキストをセットするようにします。
3. 最後に、作成したWidgetを`TextDialogList`に追加しましょう。
![image](https://user-images.githubusercontent.com/10665134/102291209-fb0c0000-3f85-11eb-8956-92e146d9eb41.png)

ここまで作成した処理は後程また使うので、関数化しておきましょう。処理を範囲選択して右クリックし、メニューから`Collapse to Function`を選択します。
![image](https://user-images.githubusercontent.com/10665134/102291279-21ca3680-3f86-11eb-8697-2bd806aff65f.png)
![image](https://user-images.githubusercontent.com/10665134/102291311-33134300-3f86-11eb-98d0-770c274b10e2.png)

関数名は`CreateDialog`にします。
![image](https://user-images.githubusercontent.com/10665134/102291369-4c1bf400-3f86-11eb-8280-d3818fac5a8f.png)

-------------

## 話しかける処理の実装

今回、話しかけるために使用するボタンはSpace Barとしておきます。すでにジャンプなどで使用している場合は別のボタンのイベントを作成するなどして使用してください。
![image](https://user-images.githubusercontent.com/10665134/102291508-9a30f780-3f86-11eb-973b-cf3e9261180b.png)

まずは、会話対象のアクターを見つけるところから始まります。今回はTrace処理を使用します。`MultiCapsuleTraceByChannel`ノードを作成します。
`Radius`と`Half Height`は任意の値、`Trace Channel`は今回は`Visibility`にしてください。また、Traceにて自分自身をHitさせないように`Ignore Self`にはチェックをつけておいてください。
![image](https://user-images.githubusercontent.com/10665134/102291601-d06e7700-3f86-11eb-81e7-7f89c23a7af2.png)

このTraceノードのStartに値を設定するための関数を用意しておきます。名前を`GetLocationOfTraceStart`にしておきます。
![image](https://user-images.githubusercontent.com/10665134/102291750-1fb4a780-3f87-11eb-8231-a9eb5a882f89.png)

関数の設定は以下の通りとなります。

- このクラス内でしか使用しないので`Access Specifier`を`Private`に変更
- 値を返すだけなので`Pure`にチェック
- `Outputs`には`Location`という名前の`Vector`型変数を返すように設定
![image](https://user-images.githubusercontent.com/10665134/102291903-6e624180-3f87-11eb-91cc-182e4c622c4c.png)

関数内の処理は以下のようにします。
1. `Get Controlled Pawn`でコントローラが所有しているアクターの取得
2. アクターのLocation取得(`GetActorLocation`)とアクターの前方向のベクトル取得(`GetActorForwardVector`)
3. ベクトルの数値に対して任意の数値を掛けるように設定
4. アクターの現在場所と前方ベクトルに数値を掛けた数を足して`Location`ピンに設定
![image](https://user-images.githubusercontent.com/10665134/102291943-8afe7980-3f87-11eb-8932-7c47766e3ae2.png)

ここまで作成したら、この関数を複製して、`End`ピンに入力するようの関数を作成します。`GetLocationOfTraceStart`の名前を右クリックして`Duplicate`を押下してください。
![image](https://user-images.githubusercontent.com/10665134/102292141-ee88a700-3f87-11eb-9641-0d4bfde4e50c.png)

複製した関数を`GetLocationOfTraceEnd`という名前にしておきます。
![image](https://user-images.githubusercontent.com/10665134/102293432-6f48a280-3f8a-11eb-8c10-9da5dec72eb2.png)

こちらでは、`GetActorForwardVector`と掛け合わせてる数値を`150`に変更しているだけです。
![image](https://user-images.githubusercontent.com/10665134/102293506-9acb8d00-3f8a-11eb-8440-2faa075c0986.png)
（もしできそうなら入力ピンを作成して、そこに掛ける数値を入れるようにし、関数の共通化を行っても良いかもしれません）

作成した二つの関数を`MultiCapsuleTraceByChannel`に繋ぎます。
![image](https://user-images.githubusercontent.com/10665134/102293631-d49c9380-3f8a-11eb-9a67-5f2dcbc360f7.png)

次に、会話用NPCを格納する変数を用意しておきます。今回の私の環境の場合は`BP_Enemy`というクラスの型を持つ変数`Enemy`を用意しています。各自の環境で任意の型の変数を作成して置いてください。
![image](https://user-images.githubusercontent.com/10665134/102294181-0c580b00-3f8c-11eb-8d74-da41e34adf6a.png)
![image](https://user-images.githubusercontent.com/10665134/102294194-167a0980-3f8c-11eb-809d-fddf20ed78a2.png)

続いて、Trace以降の処理を作成していきます。

1. TraceでHitした情報を`ForEachLoopWithBreak`ノードに繋ぎます。
2. Hitした情報を分割するノード（`BreakHitResult`）に接続し、そこから`HitActor`が存在するか否かを確認します（`IsValid`）。
3. HitしたActorが`Tag`と同じTagを持っているか、`ActorHasTag`で確認します。
4. Tag情報を持っている場合は、そのActorをCastし、先ほど用意していた変数に格納します。このActorが見つかった時点で処理は継続する必要がないため、`ForEachLoopWithBreak`ノードのBreakへ繋ぎます。
![image](https://user-images.githubusercontent.com/10665134/102294789-60172400-3f8d-11eb-8348-d6d6ee51bdbb.png)

`ForEachLoopWithBreak`ノードの`Completed`はループが中断されたか、完了した際に実行されます。

ここからダイアログ用のWidgetを表示させる処理に移ります。

1. まずは`TextDialogList`の0番目をGETし、有効かどうかを確認します。
2. 0番目の要素が既に表示されているかのチェックを`IsInViewport`で確認します。
3. 表示されていなければ、`AddtoViewport`に繋いで表示します。
![image](https://user-images.githubusercontent.com/10665134/102295132-1a0e9000-3f8e-11eb-8465-fb440403f193.png)

これで最初の会話のダイアログ表示ができるようになります。続けて、会話ダイアログをボタンを押すたびに会話が続いている分のダイアログを表示させるようにします。

1.  0番目の要素が既に表示されているかのチェックから、表示されていれば、その要素を`TextDialogList`から取り除きます。
2.  次のダイアログが存在していれば再度表示用のノードに繋ぎます。
![image](https://user-images.githubusercontent.com/10665134/102295310-6bb71a80-3f8e-11eb-830a-a5d7ea976a6a.png)

これで、基本的な会話ダイアログの表示はできるようになりました。

---------

## 会話ループ処理の実装

NPCと会話するにあたって、何度でも話しかけられるようにしたい！という場合があると思います。

ここで会話のループ処理を実装してみましょう。

まずは、ループ判定用の変数を用意します。名前を`IsLoop`にします。
![image](https://user-images.githubusercontent.com/10665134/102295372-88ebe900-3f8e-11eb-9998-f5d72636d06a.png)

型は`Boolean`で、デフォルト値を`true`にしておきます。
![image](https://user-images.githubusercontent.com/10665134/102295503-ccdeee00-3f8e-11eb-9a3c-ed5b036b75de.png)

先ほどの会話ダイアログの表示処理の最後の方に以下の処理を付け加えます。

1. 次のダイアログが存在していなければ、`IsLoop`がtrueかチェックを行います。
2. trueの場合は、`CreateDialog`を呼び出し、会話の最初から再生ができるようにします。

![image](https://user-images.githubusercontent.com/10665134/102295688-3fe86480-3f8f-11eb-912c-a8897d5e3e58.png)

## 完成

上記の作業を完了させると以下のような感じになるかと思います。ちなみにこれは別途、話しかけてきたアクターの方を向くような処理をいれています。

![dialog](https://user-images.githubusercontent.com/10665134/102297348-7378be00-3f92-11eb-8f8e-e84b1d8d8f02.gif)

# 最後に

お疲れ様でした。
ここまで結構長くなってしまいましたが、これで会話システムの基本的な部分は実装できるのではないかと思います。
ただ、この作りでは話しかけられるNPCの種類が増えていった時や、会話をランダムにしたい時、同じNPCでもフラグによって会話内容を変えたい時などに対応ができません。それにPlayerControllerが肥大化してしまう可能性があります。Widgetを会話文だけ作っているので一時的にメモリも多めに確保してしまうかと思います（実際1つのWidgetだけで作成できるとも思います）。
ただその辺はあくまでも今後拡張していくものであって、今回の記事では土台部分を学べるなーくらいで考えて頂けると幸いです。

もしここから発展させるのであれば、csvやドキュメントなどで、マップやタグ、フラグリストとセリフ内容を項目として用意し、そのデータをUE4側で吸って扱っていくのが良いかと思います。

# もしものエラー

Widget編集回りでまれによくあるんですが、Colorセッティング時にエディタ側がUndoきかなくなったり、ゲームプレイが出来なくなる時があります。そういった時は速やかにCtrl+Shift+Sで全保存かけて、UE4の再起動を行ってください。

# 参考

- [Unreal Engine 4 アクションゲーム ブループリント入門](https://www.amazon.co.jp/dp/B07VZZ3HGZ/ref=cm_sw_r_tw_dp_x_a6w2Fb9361NZ9)
- [ブループリントの構造体変数](https://docs.unrealengine.com/ja/ProgrammingAndScripting/Blueprints/UserGuide/Variables/Structs/index.html)
- [UE4 DataTableで特定の数値を持ってくる](http://mozpaca.hatenablog.com/entry/20161124/1479985011)
- [[UE4] CSVデータを扱う方法　DataTable編](https://historia.co.jp/archives/779/)
- [【UE4】会話ウィンドウを作ろう](https://yutakaworks.hatenablog.com/entry/2017/10/27/080000)