最初に
どうも、ろっさむです。
今回は、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の再起動を行ってください。