UE5で会話システムを作った備忘録HD2D風-その3


目的

有料アセットとかは使わずに HD2D風(3D空間上に2Dキャラがいる状態)のマップ内で会話システムを作りたかった。
とても長くなってしまいそうなので分割して備忘録として残す。これは前回(その2)の続き。

主な流れとしては以下の通り。

  • その1
    会話システムのベース作り
  • その2
    会話・選択肢表示用のウィジェットの作成
  • その3(今ここ)
    その1,その2で作ったものを使った実際の会話フローの作成+追加機能

Interactブループリントインタフェースの作成

これはプレイヤーがNPCに近づいたとき、当該NPCが会話可能かどうかを視覚的に理解できるようにする仕組みに用いる。
(キャラクターの頭の上に名前が表示されたりとか、三角のマークが出てたりとか あるよね。それ。)
そもそもいらなければ飛ばしてもかまわない。

こんなかんじ


適当なフォルダに、新たにブループリントを作る。
右クリック → ブループリント → ブループリントインタフェースを選択。名前をBPI_Interactableとする。

これを開き、関数を三つに増やす。それぞれ名前をInteractConversationClosedSetInteractableStateと設定する。

Interact関数を選択し、詳細タブのインプット横にある+ボタンから、新しい入力引数を追加する。
名前をInstigator、ピンタイプをActorのオブジェクト参照(Actor横のマークが薄青色であればOK)、コンテナタイプを単一とする。
SetInteractableState関数を選択し、インプットから引数を追加する。名前をCan Show Interact?、タイプはBooleanとする。

NPCベースの作成

適当なフォルダに新たにブループリントを作る。
右クリック → ブループリントクラス → PaperZDCharacter※注 を選択。名前をNPC_Baseとする。これを開いてClass設定ボタンを選択し、実装インターフェースタブの追加ドロップダウンからBPI_Interactableを選択する。

POINT

※キャラクター作成時、PaperZDという2Dキャラを簡単に動かせるプラグインを使ったのでこれを選んでいる。3Dキャラを使うなら「Character」を、プラグインを利用しないスプライトのキャラクターなら「PaperCharacter」を選ぶのがよいかも。

次に変数を追加する。
名前をInteractInstigator、ピンタイプをActor(オブジェクト参照)とする。

Mesh、Sprite等から適当なデフォルトキャラクターを設定する。これはあくまでベースなので、実際に話しかけたいキャラクターのデータでなくても構わない。(もしかしたら大きさとかコリジョンは統一しなきゃいけないのかも。要調査)

コンポーネントの追加から、キャラクターの頭上に浮かせたいオブジェクトを追加し、配置する。
Michael氏の動画ではTextRendererが選択され、Construction Scriptで読み込むことでキャラクターの名前が表示されるようになっていたが、TextRendererが残念ながらデフォルトで日本語対応がなされていなかった。
ここではConeを配置することとする。
Coneの詳細タブ、レンダリングにおけるVisibleのチェックを外す

ConeのVisibleにチェックを入れるとこんな感じ。Coneは条件で出したいのでチェックは必ず外しておこう。

補足

英語でもいいからキャラクター名を呼び出したい・日本語フォントでTextRendererを使う方法が分かった!という場合は、Michael氏の動画のパート3を見ると方法が細かくわかる。

 

イベントグラフに移る。
デフォルトのノードを消し、まず Event Set Interactable Stateを配置する。
ここからリンクを伸ばしてブランチを接続する。ブランチとCan show Interactピンも接続する。
次に変数からConeをGetで呼び出す。ここからリンクを伸ばして、Set Visibilityを接続する。
ブランチのTrueピンとSetVisibilityも接続し、New Visibilityにチェックを入れる。

Cone、SetVisibilityノードをコピペして、Falseピンとも接続する。ここでのNew Visibilityにはチェックを入れない

こうなっていればヨシ。


実際に配置するNPCの作成

コンテンツドロワーに戻り、NPC_Baseを右クリック、子ブループリントを作成しますを選択してNPC_Baseの子ブループリントを追加する。名前をNPC_Mouseとする。(ここはキャラクターに合わせて各々変えてOK。大事なのは子ブループリントであること。)

ここで各々キャラクターの画像等を変えることになる。
好きに変更したら、マイブループリントタブ横の歯車マークを押し、継承した変数を表示を選択しておく。キャラクター名を頭上に表示する設定を行っていた場合は、ここの変数のデフォルト値に入力することでキャラ名を変更できる。

イベントグラフに移動し、デフォルトノードは消しておく。

インタフェースタブからInteractをダブルクリックする。「イベントInteract」という赤いノードが出ていればOK。
InteractInstigatorをセットで呼び出し、イベントInteractと2本のリンクでそれぞれ接続する。

ここまでできたら一度プレイヤーキャラクターの設定に移る。


プレイヤーの設定 - 話しかけるコマンド

まずは「話しかける」キーを設定する。
コンテンツドロワーの適当なフォルダで(Inputフォルダがいいかも)右クリック → 入力 → 入力アクション を選択する。
これにIA_Interactと名付ける。

続いてプレイヤーキャラに適用されている入力マッピングコンテキストファイルを開く。(ThirdPersonだとIMC_Defaultかな)
Mappingsタブにある+ボタンから、新たにアクションマッピングを追加する。
「なし」となっているアセット欄にIA_Interactを適用し、キーボードマークを押してから好きなキーを入力する。今回はEキーを設定した。

これらを保存する。


プレイヤーの設定 – 話しかける入力ブループリントと頭上マーク表示設定

次にプレイヤーキャラクターのブループリントを開く。
プレイヤーの動きが既にイベントグラフにたくさん設定されていることかと思う。
これに追記する。

右クリックでIA_Interactと検索すると、EnhancedInputActionノードが作成される。
変数タブの+ボタンから名前Interactable、タイプActor(オブジェクト参照)の変数を追加する。

EnhancedInputActionノードのStartedピンからリンクを伸ばし、Is Valid(?マークがついている方)を接続する。Input Objectピンには変数からInteractableをGetで呼び出したものを接続する。

is ValidノードのIs Validリンクを伸ばし、BPI Interacrtと検索してInteract(メッセージ)を接続する。
ターゲットはGetで呼び出した変数のInteractable、InstigatorにはSelf(Get a reference to self)を接続する。

InteractノードにSet Input Made UI Onlyノードを接続する。Player ControllerピンにGet Player Controllerを接続する。もうひとつGet Player Controllerを呼び出し、Return Valueピンからリンクを伸ばしてSet show Mouse Cursorノードを繋げる。Set Input~ノードの右上ピンとセットマウスカーソルノードも接続する。Show Mouse Cursorにチェックを入れる。


こんな感じ。

追記:Set Input Mode UI Onlyノードの「Flush Input」にチェックを入れるといい。
WASD等により歩行しながら話しかけるキーを入力すると、歩行のキー入力が保存されてしまい、会話しながら勝手に歩いて行ってしまう現象が起きる。FlushInputにチェックを入れることで、
この現象を回避できた。


続いて「NPCの頭の上に出る名前だのマークだのを、プレイヤーが近付いた時にのみ表示する」機能を実装する。
そのためにコンポーネントから追加でSphere Collisionを選択。これをInteractRadiusと名付ける。
詳細タブからSphere Radiusを好きな大きさに設定する。ここでは128を選択した。
この範囲にNPCが入るとNPCの頭上にマークが表示される。

コンポーネント一覧からInteractRadiusを右クリック → イベントを追加 → On Component Begin Overlapを選択する。これでグラフ上にノードが追加された。
同様に、On Component End Overlapも追加する。


まずはOn Component Begin Overlapから作成する。
Other Actorからリンクを伸ばし、not Equal(オペレータタブの「等しくない」)を接続する。not Equalノードの左下ピンにはselfノードを接続する。
not Equalノードの右側ピンを伸ばし、ブランチを接続する。ブランチの左上ピンと、On Component Begin Overlapノードの右上ピンも接続する。
ブランチのTrueの先にもうひとつブランチを繋げる。

ここでOther Actorピンからもう一本リンクを伸ばし、Does Implement Interfaceピンを接続する。InterfaceピンにはBPI_Interactableを設定する。
このノードのReturn Valueピンを、2つ目のブランチのConditionピンと接続する。

2つ目のブランチのTrueピンからリンクを伸ばし、Is Valid(?マーク)を接続する。このノードのInput ObjectにはInteractableの変数をGetで呼び出したものを接続する。
このis Validノードの Is Validピンからリンクを伸ばし、Set Interactable Stateを接続する。このターゲットもInteractableである。
このノードの右上ピンと、InteractableをSetで呼び出したものを接続する。これにさらにInteractableをSetで呼び出したものを接続し、2番目のSetノードの左上ピンとIs Validノードの Is Not Validピンを、Interactableピンと最初のノードにあるOther Actorピンを接続する。

Interactable-Set Interactible Stateの組み合わせをコピペし、二番目のセットノードの右上ピンとSet Interactable Stateノードの左上ピンを接続する。このノードのCan show Interactにチェックを入れる。

これまた長くて申し訳ないがこうなっていればヨシ。


次にOn Component End Overlapを作成する。
On Component Begin Overlapで作成したノードのうち、self-not Equal-ブランチの組み合わせと、Interactable-Set Interactable State-セットの組み合わせをコピペする。

self-not Equal-ブランチの組み合わせを先ほどと同様にリンクを接続する。
ブランチのTrueピンの先にSet Interactable Stateノードを接続する。
先頭ノードOther Actorピンからもう一本リンクを伸ばし、等しい(==)ノードを接続する。==ノードの左下ピンには変数Interactableを接続する。

次に、not Equalの右側ピンからリンクを伸ばし、And Booleanを接続する。And Booleanノードの左下ピンと==ノードも接続する。And Booleanノードの右側ピンと、ブランチのCoditionピンを接続する(Conditionピンとnot Equalノードとのリンクが切れるが、これで正しい)。


こうなっていればヨシ。


ここまでできたら、レベル上にNPC_Mouse(NPC_Baseの子ブループリントとして作ったNPCキャラ)を配置し、PlayerStartが設定されていることを確認してからテストプレイしてみよう。
冒頭に示したGifのように、一定距離まで近づいたらマークなり何なりが表示されたら成功。


NPCとの会話を実装する

やっとここまで来た!
最後にはなるがここからがメイン。実際にノードを配置しながら台詞等々を設定する。

まずAC_Dialogue_Baseを右クリックし、子ブループリントを作成する。名前をAC_Dialogue_Mouseと設定する(これは好きに決めていい。わかりやすければ何でも)。

一度NPC_Mouse(NPCのキャラクターブループリント)を開き、イベントグラフを開く。コンポーネントの追加タブから、先ほど作成したAC_Dialogue_Mouseを追加する。
これをドラッグ&ドロップでグラフ上に配置する。
ここからリンクを伸ばして、Open Conversationノードを呼び出し、接続する。もともと配置しておいたセットノードの右上ピンと、OpenConversationの左上ピンも接続する。

AC_Dialogue_Mouseに戻り、イベントグラフを開く。
デフォルトノードは消す。
関数タブの右側にある「オーバーライド」というドロップダウンを開き、Dialogueを設定する。


ここから本格的に台詞の追加・分岐の追加等を設定する。

親Dialogueノードからリンクを伸ばし、整数型でスイッチノードを接続する。SelectionピンにはDialogue Treeindexを接続する。ピンを追加ボタンを押し、ピンからリンクを伸ばしてAdd Dialogueを接続する。
スイッチノードのDefaultピンは削除する。

POINT

“ピンを追加”ボタンを押すことで、会話の条件分岐(選択肢には依らない)を追加できる。どの分岐に入るかはピンの番号で決定される。Dialogue Treeindexのデフォルト番号は0なので、何もなければ0ルートの会話が進む。会話外で分岐を制御したい場合(なんかアイテムを手に入れた とか?)は、異なるInteger変数を用意してその数値でスイッチするとよいかも。

Add DialogueノードのSpeaker、Dialogueにそれぞれ話す人の名前、台詞を入力する。
Options Textノードからリンクを伸ばし、Make Array(配列を作成)ノードを接続する。ここで選択肢を入力する。ピンを追加で選択肢の数を増やすことができる。


とりあえずこうしてみた。テストする。


上記画像にはマウスカーソルが映っていないが、実際にプレイしてみるとマウスカーソルがホバーしている選択肢の色が変化していることと思う。
うまく動かなければ、接続ピン等諸々間違えていないか確かめる。

選択肢の後の処理も実装する。

Add DialogueのStateからリンクを伸ばし、Enum_DialogueStateでスイッチノードを接続する。

POINT

「Pass Through」ピンは現在何か台詞が表示されていて、その続きを表示したい(会話を更新したい)ときに使う。「Updated」ピンはメッセージ表示後に報酬を自動的に与えたい場合等に使う らしい。

Option indexピンからリンクを伸ばし、整数型でスイッチノードを接続する。整数型でスイッチノードの左上ピンと、Passthroughピンを接続する。また、ピンを追加で0、1(先ほど作成した選択肢と同じ数値になるように)のピンを追加(Defaultは削除)する。

配列を作成- Add Dialogue – Enum_DialogueStateでスイッチの組み合わせをコピペし、整数型でスイッチの0、1のピンとAdd Dialogueノードをそれぞれ接続する。

Add Dialogueノードにそれぞれの選択肢に応じた台詞を入力する。

今回は
「チーズ持ってない?」に対して、

  • 「あるよ」→「それ欲しいから一つくれない?」(再度あげる/あげないの分岐)
  • 「ないよ」→「無念…」(一度会話を終了、Dialogue Treeindexに変更はないので 次話しかけたら同じ話をする)

という流れとする。

 

まずは「あるよ」の後の会話から。Add Dialogue、配列を作成にそれぞれ台詞を追加。「Enum_DialogueStateでスイッチ」ノードの後に、先ほどと同様、整数型でスイッチ(ピンは0,1)を接続する。この先の会話の繋げ方はこれと同じ。

次に「ないよ」の後の会話。Add Dialogueに台詞を入力する。配列を作成ノードのピンは一つとしておき、なにも入力しない。矢印マークを入力してもいいかも。
この選択肢が会話終了スイッチとなるが、選択肢の表示位置に会話終了スイッチを置きたくない場合(キー入力等で会話を終わらせたい場合)は選択肢のいらないダイアログの作成を見てほしい。
Enum_DialogueStateでスイッチのPassthroughピンに、Clear Dialogue Progressノードを接続する。
更にこの後に、Close Conversationノードを接続する。

これでこの会話はいったん終了する。

今回はこうした。

ここでテストしてもいいが、このままだと会話終了後にプレイヤーが動けなくなる。会話後に動けるようにするにはいくつか編集が必要なので、会話後に動けるようにのページを見て編集すること。


では同じようにあげる/あげないの分岐を作っていこう。
流れは、

  • 「あげる」→「ありがとう!」でいったん会話終了。
    その後話しかけるともうチーズを欲しがらず、「おいしい~」「君はなんて優しいんだ」「もぐもぐ」等の言葉をランダムで話す。
  • 「だめ」→「けちんぼ~」でいったん会話終了。
    その後話しかけると、「それ欲しいから一つくれない?」に戻る。(チーズ持ってない?からの開始ではなく、会話の途中からスタートする)

まず「あげる」の後の会話から。
今までと同様にAddDialogueノードに台詞を入力する。選択肢はないので「配列を作成」ノードには何も入力しない。
Enum_DialogueStateでスイッチのPassThroughピンからリンクを伸ばし、Set Dialog Treeindexを接続する。Dialogue Treeindexピンに1を入力する。Set Dialog Treeindexノードからリンクを伸ばし、Close Conversationを接続する。


こうなっていればヨシ。

フローの冒頭に戻り、Dialogue Treeindexノードが接続された整数型でスイッチノードのピンを増やす。(既に1が増えている場合はそのまま1のピンを使う。)

これです。先ほどTreeindexピンに入力した1は、このノードにあるどのピンから開始されたルートに進むのかを指定している。

この先に、配列を作成 – Add Dialogue – Enum_DialogueStateでスイッチ – Clear Dialogue Progress – Close Conversationの組み合わせを接続する。食べ物の所持を否定するルートからコピペしてくるのが早いかも。
今まで同様Speakerに話者の名前を入力する。Dialogueピンからリンクを伸ばし、「選択する」ノードを接続する
「ピンを追加」を選択して、Option0~Option2までとする。これにそれぞれ台詞を入力する。
indexピンからリンクを伸ばし、Random Integer in Rangeを接続する。Minの値は0,Maxの値は2とする。

POINT

これでOption0,1,2に入力した台詞をランダムで話すキャラクターが完成する!Optionの数は好きに決めて大丈夫。

 


こうなっていればヨシ!

 

次に「あげない」の後の会話。
今までと同様にAddDialogueノードに台詞を入力する。選択肢はないので「配列を作成」ノードには何も入力しない。
Enum_DialogueStateでスイッチノードのPassThroughピンからリンクを伸ばし、Remove Dialogue Progressノードを接続する。
次に、どのダイアログから再開したいかを選んで、それがAdd Dialogueノードから遡っていくつ目のAdd Dialogueノードなのかを数える
今回の場合、「それ欲しいから一つくれない?」からスタートしたい。したがって、「けちんぼ」から遡って、「それ一つくれない?」ということで1つ目のノードとなる。

ということで、Progress Steps1を代入できる。
あとはClose Conversationノードを接続すればこのルートは終了。

こうなっていればヨシ。


会話後に動けるように

このまま動かすと、会話終了後に再びプレイヤーが動き出すことができない。ここで一度AC_DialogueBaseの関数、NPC_Baseおよび各々利用しているプレイヤーのブループリントを編集しよう。

CloseConversationノードに接続されている、Remove from Parentノードの続きから。右クリックでGet Ownerを呼び出し、Return Valueピンからリンクを伸ばしてConversation Closedノードを接続する。ConversationClosedノードの左上ピンと、Remove from Parentノードの右上ピンも接続する。


こうなったかな。

次にNPC_Baseの編集。
イベントグラフを開いて、インターフェースタブからConversation Closedをダブルクリック。ノードを配置する。
変数からInteractInstigatorをGetで呼び出し、リンクを伸ばしてConversation Closedと検索し、接続する。これと先ほど配置したイベント Conversation Closedも接続する。


こうなっていればヨシ。

次にプレイヤーのブループリントを編集する。
クラス設定を選択し、実装インターフェースタブの追加ドロップダウンを選択する。ここでBPI_Interactableを選ぶ。
これでインターフェースタブからBPI_Interactableで設定した関数をここでも呼び出せるようになるので、Conversation Closedをダブルクリックする。
会話終了後にマウスカーソルを消すため、既に設定してあるEnhancedInputAction IA_Interactノードの流れから、Get Player Controller – セットの組み合わせをコピペしてくる。セットとイベントConversation Closedノードを繋げる。
Show Mouse Cursorのチェックを外す。
ここからさらにリンクを伸ばして、Set Input Mode Game Onlyを接続。(引き続きUI操作が必要ならGame and UIを選ぶといいかも。)
Player Controllerピンに、Get Player Controllerを接続する。

こうなっていればヨシ!


選択肢のいらないダイアログの作成

ここまででも十分会話は可能だが、ある程度長い台詞・文章を記載しようとすると枠が足りなくなる。
よく見るのが 選択肢は表示されず、何かしらのボタンを押すことで次の文章に更新される という方式。

Michael氏の動画にはこの方法について載っていないが、コメント等を参照して自分なりに作成した。流れとしては次のようにする。

  1. 選択肢を使用しない文章については、選択肢を1つ且つ空にする
  2. Optionボタン配置時に、選択肢が空かどうかを判定し、空だった場合はオプションボタンを見えなくする
  3. 選択肢が空だった場合は、キーボードの入力を受け付けるようにして、所定のキーボードを押した場合に次の文章へと移動する(空の選択肢を選んだ状態とする)

参考資料はウィジェットをキーボードだけで操作する-それとこれとあれかどれかとである。

弱点:次に進むキーを好きなキーに設定できない?
いろいろやってみたけど、Enter/SpaceBar以外認識しなかった。

補足

最初は「Enable Input」ノードでキーボードの入力許可が要るかなと考えたが、必要なかった。なんでだろう。何か違いが分かったら追記する。

まずはW_DialogueOptionブループリントを開き、イベントグラフを開く。
イベントConstructノードのルートから編集する。
右クリックでOption TextをGetで呼び出し、ここからリンクを伸ばしてTo Stringを接続する。ここから更にリンクを伸ばして、===(Equal Exactry(String))を接続。ここで元々このルートにあったSetText(Text)ノードからリンクを伸ばして、ブランチを接続する。ブランチの左下ピンと、===ノードの右側ピンを接続する。

以下、上画像の組み合わせをブランチグループと呼称する。

ブランチのTrueピンを伸ばし、Set Color and Opacityを接続。ターゲットにImg_Background(選択肢の背景画像)を接続する。色は完全に透明とする。そのほか選択肢の背景画像に関わるウィジェットがあれば、ここでノードを接続して透過処理を行う。
次にSet Color~ノードの右上ピンからリンクを伸ばし、Set User Focusを接続する。ターゲットピンにはBtn_Option(選択肢のボタン属性持ちウィジェット)を、Player ControllerピンにはGet Player Controllerを接続する。

Btn_OptionをGetでもう一つ呼び出し、ここからリンクを伸ばしてStyleを取得ノードを接続する。Styleピンを変数に昇格するといいらしい。
ここからリンクを伸ばし、セットを選択。

分かりづらいけど赤線部のこれ。
セットの左上ピンとSet User Focusを接続、右上ピンは新たにブランチと接続する。


ここまでがこんな感じ。
右クリックでGet Player Controllerを呼び出し、Return Valueピンからリンクを伸ばしてIs Input Key Downを接続。Keyの指定はできるけど、なぜかEnterとSpaceBarしか認識してくれない。誰か助けて。
とりあえずEnterを指定して、右側のReturn Valueピンからリンクを伸ばしてブランチのConditionピンと接続する。
ブランチのTrueピンを伸ばして、以前作ったOn Released(Btn_Option)ルートのノードを全てコピペしたものを接続する。言うてメインノードは一つしかないけど


色透過ノードの次がこうなっていればヨシ。

メインはここで終わり。
ただ、このままだとマウスホバーするとせっかく消していた選択肢が露出してしまうので、MouseHover、UnHoverにも似たような設定を行う。

とは言え上記ブランチグループを追加して、ブランチノードのTrueにImg_Background等の背景画像の色を透過設定するノードを追加し、Falseに今まで設定してたノードを移動するだけなので、説明は画像にとどめておく。

上画像はOn Hoveredだが、Unhoveredも同様である。


まとめ

以上!!!お疲れさまでした。