13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

一人UE4 & C++Advent Calendar 2020

Day 12

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

Last updated at Posted at 2020-12-16

最初に

どうも、ろっさむです。

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

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

開発環境

  • UE4.26
  • Windows10

作業手順

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

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

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

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

まずは任意のフォルダ下(例:UIフォルダ)にデータ格納用のフォルダを作成しておきましょう。
image


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

今回の名前はStruct_Dialogにしました。
image

ダブルクリックして開き、データの土台を作成します。
image

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

会話内容用のDataTableの作成

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

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

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

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

ダブルクリックして開き、データを作成します。
image

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

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

あとは忘れないうちに、Tagでつけた名前をレベル上にいるNPCにつけましょう。
image


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

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

任意のフォルダ(例えばUIフォルダ)下にて、右クリック(又はAdd Newボタン)からメニューを表示し、User Interface > WBP_TextDialogを選択します。
image

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

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

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

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

image
image

テキスト内容は Content > Textから行えます。
image

この時点でCanvasはざっくり以下のような感じになっていると思います。
image

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

CanvasにImageを追加して、子要素にしてください。
image

image

AnchorsはCanvasの左端を基準にするように設定します。
image

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

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

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

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

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

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

型はTextにしてください。
image

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


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

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

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

Tagという名前で変数を作成してください。
image

型はNameです。
image

次にTextDialogListという変数を作成します。
image

型は先ほど作成したWBP_TextDialogの配列型にします。
image

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

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

Get Data Table Rowというノードを作成し、Data Tableに先ほど作成したDT_Dialogをセットしてください。
image

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

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

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

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

  1. ForEachLoopノードに繋ぎ、会話の数だけWidgetを生成するようにします。
  2. 同時にWidget内部のDisplayTextにはDialogのテキストをセットするようにします。
  3. 最後に、作成したWidgetをTextDialogListに追加しましょう。
    image

ここまで作成した処理は後程また使うので、関数化しておきましょう。処理を範囲選択して右クリックし、メニューからCollapse to Functionを選択します。
image
image

関数名はCreateDialogにします。
image


話しかける処理の実装

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

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

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

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

  • このクラス内でしか使用しないのでAccess SpecifierPrivateに変更
  • 値を返すだけなのでPureにチェック
  • OutputsにはLocationという名前のVector型変数を返すように設定
    image

関数内の処理は以下のようにします。

  1. Get Controlled Pawnでコントローラが所有しているアクターの取得
  2. アクターのLocation取得(GetActorLocation)とアクターの前方向のベクトル取得(GetActorForwardVector)
  3. ベクトルの数値に対して任意の数値を掛けるように設定
  4. アクターの現在場所と前方ベクトルに数値を掛けた数を足してLocationピンに設定
    image

ここまで作成したら、この関数を複製して、Endピンに入力するようの関数を作成します。GetLocationOfTraceStartの名前を右クリックしてDuplicateを押下してください。
image

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

こちらでは、GetActorForwardVectorと掛け合わせてる数値を150に変更しているだけです。
image
(もしできそうなら入力ピンを作成して、そこに掛ける数値を入れるようにし、関数の共通化を行っても良いかもしれません)

作成した二つの関数をMultiCapsuleTraceByChannelに繋ぎます。
image

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

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

  1. TraceでHitした情報をForEachLoopWithBreakノードに繋ぎます。
  2. Hitした情報を分割するノード(BreakHitResult)に接続し、そこからHitActorが存在するか否かを確認します(IsValid)。
  3. HitしたActorがTagと同じTagを持っているか、ActorHasTagで確認します。
  4. Tag情報を持っている場合は、そのActorをCastし、先ほど用意していた変数に格納します。このActorが見つかった時点で処理は継続する必要がないため、ForEachLoopWithBreakノードのBreakへ繋ぎます。
    image

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

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

  1. まずはTextDialogListの0番目をGETし、有効かどうかを確認します。
  2. 0番目の要素が既に表示されているかのチェックをIsInViewportで確認します。
  3. 表示されていなければ、AddtoViewportに繋いで表示します。
    image

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

  1. 0番目の要素が既に表示されているかのチェックから、表示されていれば、その要素をTextDialogListから取り除きます。
  2. 次のダイアログが存在していれば再度表示用のノードに繋ぎます。
    image

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


会話ループ処理の実装

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

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

まずは、ループ判定用の変数を用意します。名前をIsLoopにします。
image

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

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

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

image

完成

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

dialog

最後に

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

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

もしものエラー

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

参考

13
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?