最初に
どうも、ろっさむです。
今回は、NPCとの会話システムを簡易的ではありますが、実装する手法についてまとめていこうと思います。
今回はNPCからのメッセージをただ表示させるだけですが、次回の記事ではここに選択肢も追加していきます。
開発環境
- UE4.26
- Windows10
作業手順
ざっと今回行う作業は以下の通りです。
- 会話内容用の構造体の作成
- 会話内容用のDataTableの作成
- 会話ダイアログ用のウィジェットの作成
- PlayerController内でのウィジェット作成処理
- 話しかける処理の実装
- 会話ループ処理の実装
少し長いですが、内容はそれほど難しくないので、順番に実装していきましょう。
会話内容用の構造体の作成
まずは任意のフォルダ下(例:UIフォルダ)にデータ格納用のフォルダを作成しておきましょう。

Dataフォルダ下にて、右クリック(又はAdd Newボタン)からメニューを表示し、Blueprints > Structureを選択します。

-
New Variableを2回押下して、変数を作成。 - 1つ目を
Tagという名前にし、型をNameに設定。 - 2つ目を
Dialogという名前にし、型をTextの配列に設定(配列にするには③の四角で囲ってる部分を押下)
会話内容用のDataTableの作成
作成ができたら、次は実際にデータを入れるためにData Tableアセットを作成します。
Dataフォルダ下にて、右クリック(又はAdd Newボタン)からメニューを表示し、Miscellaneous > Data Tableを選択します。

この時、どの構造体を選択するかを決めるダイアログが表示されますが、先ほど作成したStruct_Dialogに設定しておいてください。

- まずは
Addを1回押して、データを1つ分作成します。 - ここをダブルクリックしてデータ名を
Dialog0001に変更します。 -
Tagには会話対象のNPCの名前など、判別用に任意の名前をつけてください。 -
Dialogにて実際に表示させたい会話文だけ+ボタンを押下して要素を追加し、文の中身を記入します。記入した数だけボタンを押して会話を進める数が増えます。
ここまで出来たらデータ作りは完了なのでSaveしてください。
あとは忘れないうちに、Tagでつけた名前をレベル上にいるNPCにつけましょう。

会話ダイアログ用のウィジェットの作成
次に会話用ダイアログを表示するためのウィジェットを作成します。
任意のフォルダ(例えばUIフォルダ)下にて、右クリック(又はAdd Newボタン)からメニューを表示し、User Interface > WBP_TextDialogを選択します。

今回のアセットの名前はWBP_TextDialogにしました。

まずは、ダイアログの要素を置くためのCanvas Panelを追加しておきます(サイズや位置等は任意で変更してください)。

Anchorsを左上にセットしておきましょう。これで画面の大きさが変わっても位置が左上基準で固定されます。

次に、セリフを表示するためのTextを、先ほど作成したCanvas PanelにD&Dして、子要素にします。ついでにテキストの内容もプレビュー用に適当に変更しておきます。
この時点でCanvasはざっくり以下のような感じになっていると思います。

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

現在の状態だと、文字と背景が同じ表示優先度になっているため、背景を先に描画してから文字を描画するように指定します。
Slot > ZOrderを-1に設定してください。

また、白色文字を見やすいように背景色をグレーなどにし、半透明程度にアルファも設定します。

ここまでの設定を行うと、ざっくり以下のような感じになっているかと思います。

会話ダイアログとして使用するためには、テキスト部分を外部から差し替え可能な状態にする必要があります。ぴよぴよテキストの Content > Text > Bindを押下して、Create Bindingを選択してください。

自動でメソッドが作成されますが、名前をGetDialogTextに変更しておきます。

また、設定するテキストの変数も作成しておきます。名前をDisplayTextに設定します。

あとはGetDialogTextの戻り値にDsiplayTextを接続するだけでOKです。

PlayerController内でのウィジェット作成処理
作成したWidgetを使用するための初期化処理を作成していきます。この処理は今回は簡易実装なので、任意のPlayerControllerクラス内部で行います。ご容赦下さいませ。
まずは必要な変数を予め作成しておきます。
型は先ほど作成したWBP_TextDialogの配列型にします。

次に処理の実装を行っていきます。今回の初期化処理はEvent BeginPlayから開始させます。
まずはデータテーブルの取得をBP内で行います。
Get Data Table Rowというノードを作成し、Data Tableに先ほど作成したDT_Dialogをセットしてください。

セットした後はOut Rowピンの上で右クリックをし、メニューからSplit Struct Pinを選択します。

そうすると、ピンの内容がTagとDialogに分割されるので、Tagを先ほど作成した変数のTagの方にセットします。DialogはForEachLoopノードを作成して、Arrayに繋ぎましょう。

ここからウィジェットを作成する処理に移ります。Create Widgetノードを作成し、ClassをWBP_TextDialogに設定します。

次に以下の処理を追加します。
-
ForEachLoopノードに繋ぎ、会話の数だけWidgetを生成するようにします。 - 同時にWidget内部の
DisplayTextにはDialogのテキストをセットするようにします。 - 最後に、作成したWidgetを
TextDialogListに追加しましょう。
ここまで作成した処理は後程また使うので、関数化しておきましょう。処理を範囲選択して右クリックし、メニューからCollapse to Functionを選択します。


話しかける処理の実装
今回、話しかけるために使用するボタンはSpace Barとしておきます。すでにジャンプなどで使用している場合は別のボタンのイベントを作成するなどして使用してください。

まずは、会話対象のアクターを見つけるところから始まります。今回はTrace処理を使用します。MultiCapsuleTraceByChannelノードを作成します。
RadiusとHalf Heightは任意の値、Trace Channelは今回はVisibilityにしてください。また、Traceにて自分自身をHitさせないようにIgnore Selfにはチェックをつけておいてください。

このTraceノードのStartに値を設定するための関数を用意しておきます。名前をGetLocationOfTraceStartにしておきます。

関数の設定は以下の通りとなります。
- このクラス内でしか使用しないので
Access SpecifierをPrivateに変更 - 値を返すだけなので
Pureにチェック -
OutputsにはLocationという名前のVector型変数を返すように設定
関数内の処理は以下のようにします。
-
Get Controlled Pawnでコントローラが所有しているアクターの取得 - アクターのLocation取得(
GetActorLocation)とアクターの前方向のベクトル取得(GetActorForwardVector) - ベクトルの数値に対して任意の数値を掛けるように設定
- アクターの現在場所と前方ベクトルに数値を掛けた数を足して
Locationピンに設定
ここまで作成したら、この関数を複製して、Endピンに入力するようの関数を作成します。GetLocationOfTraceStartの名前を右クリックしてDuplicateを押下してください。

複製した関数をGetLocationOfTraceEndという名前にしておきます。

こちらでは、GetActorForwardVectorと掛け合わせてる数値を150に変更しているだけです。

(もしできそうなら入力ピンを作成して、そこに掛ける数値を入れるようにし、関数の共通化を行っても良いかもしれません)
作成した二つの関数をMultiCapsuleTraceByChannelに繋ぎます。

次に、会話用NPCを格納する変数を用意しておきます。今回の私の環境の場合はBP_Enemyというクラスの型を持つ変数Enemyを用意しています。各自の環境で任意の型の変数を作成して置いてください。


続いて、Trace以降の処理を作成していきます。
- TraceでHitした情報を
ForEachLoopWithBreakノードに繋ぎます。 - Hitした情報を分割するノード(
BreakHitResult)に接続し、そこからHitActorが存在するか否かを確認します(IsValid)。 - HitしたActorが
Tagと同じTagを持っているか、ActorHasTagで確認します。 - Tag情報を持っている場合は、そのActorをCastし、先ほど用意していた変数に格納します。このActorが見つかった時点で処理は継続する必要がないため、
ForEachLoopWithBreakノードのBreakへ繋ぎます。
ForEachLoopWithBreakノードのCompletedはループが中断されたか、完了した際に実行されます。
ここからダイアログ用のWidgetを表示させる処理に移ります。
- まずは
TextDialogListの0番目をGETし、有効かどうかを確認します。 - 0番目の要素が既に表示されているかのチェックを
IsInViewportで確認します。 - 表示されていなければ、
AddtoViewportに繋いで表示します。
これで最初の会話のダイアログ表示ができるようになります。続けて、会話ダイアログをボタンを押すたびに会話が続いている分のダイアログを表示させるようにします。
これで、基本的な会話ダイアログの表示はできるようになりました。
会話ループ処理の実装
NPCと会話するにあたって、何度でも話しかけられるようにしたい!という場合があると思います。
ここで会話のループ処理を実装してみましょう。
まずは、ループ判定用の変数を用意します。名前をIsLoopにします。

型はBooleanで、デフォルト値をtrueにしておきます。

先ほどの会話ダイアログの表示処理の最後の方に以下の処理を付け加えます。
- 次のダイアログが存在していなければ、
IsLoopがtrueかチェックを行います。 - trueの場合は、
CreateDialogを呼び出し、会話の最初から再生ができるようにします。
完成
上記の作業を完了させると以下のような感じになるかと思います。ちなみにこれは別途、話しかけてきたアクターの方を向くような処理をいれています。
最後に
お疲れ様でした。
ここまで結構長くなってしまいましたが、これで会話システムの基本的な部分は実装できるのではないかと思います。
ただ、この作りでは話しかけられるNPCの種類が増えていった時や、会話をランダムにしたい時、同じNPCでもフラグによって会話内容を変えたい時などに対応ができません。それにPlayerControllerが肥大化してしまう可能性があります。Widgetを会話文だけ作っているので一時的にメモリも多めに確保してしまうかと思います(実際1つのWidgetだけで作成できるとも思います)。
ただその辺はあくまでも今後拡張していくものであって、今回の記事では土台部分を学べるなーくらいで考えて頂けると幸いです。
もしここから発展させるのであれば、csvやドキュメントなどで、マップやタグ、フラグリストとセリフ内容を項目として用意し、そのデータをUE4側で吸って扱っていくのが良いかと思います。
もしものエラー
Widget編集回りでまれによくあるんですが、Colorセッティング時にエディタ側がUndoきかなくなったり、ゲームプレイが出来なくなる時があります。そういった時は速やかにCtrl+Shift+Sで全保存かけて、UE4の再起動を行ってください。
















