ぼっちプログラマのメモ

UE4とかVRとかについて書いたり書かなかったり。

【UE4】Subsystem, GAS, DataAssetを活用して実装した会話システムについて雑多に書いてみた

f:id:pafuhana1213:20211213133528p:plain

はじめに

本記事はUnreal Engine (UE) Advent Calendar 2021(カレンダー1)の13日目の記事です!
12日目はじゅる (@xxJulexx) | Twitterさんによる
qiita.com
でした!DMX周りは基本的な知識もあまりないのでこういった記事は大変助かります…いつもありがとうございます!

アドベントカレンダーの記事はいつも何を書こうか悩むのですが…以前に解説してほしいと要望のあった『Subsystem, GAS, DataAssetを活用して実装した会話システム』について書こうと思います!ちなみに、実際に動いてる様子はこちら。

個人的には手応えはある程度感じてますが…まだまだ実装途中の部分もあったり…プロジェクトや実現したい表現によって色々変わってくると思うので…あくまで一例として参考程度に読んで頂けますと幸いです!そういったこともあり、作り方というよりも設計・思想の話が多めになってます。

あと、本記事はSubsystem, GAS, DataAssetに関する講演を既に読んだ・見た方を対象としています。もしまだという方は先にこちらをどうぞ。

www.slideshare.net
www.youtube.com

やりたかったこと・目指したこと

まずは会話システムを作る上でどういう目標・モチベがあったのかについて当時のメモからコピペ
(ここがないと後ほど解説する実装の意図がわからなくなるので!)。

個人開発なのでカメラカットを工夫したりキャラを移動させたりなどのリッチな作りにはしない!何回もそれで失敗した!

  • 昔ながらのキャラを横に並べた会話システムにしよう。アドベンチャーゲームも沢山してきたし必要な要素は分かるはず
  • 絵はかけないので3Dモデルを表示する形にするしかにない

 - 3Dモデルの強みを活かすため、表情・モーションなどは指定できるようにしよう。
 - Control Rig使えばある程度プロシージャルにできれば工数抑えつつ少し見栄え良くなる!はず

できるだけ実装が一部に集中しないように!何回もそれで失敗した!

  • 窓口はSubsystem

  - Subsystemにすればどこからでも会話処理をリクエストできる!
  - あくまで窓口。実際の処理・データの管理はソフト参照の管理Actorで行う。Subsystemは常駐物なので強く紐づくデータは最小限に!
  - 定期的にメモリを綺麗にしたいのでWorldSubsystemにしよう

  • 会話シーン管理の実装はActor

  - 上述の理由と後述のGASを使いたいという理由から、大本の管理はActorで。
  - BPでガリガリ実装できるというのも楽。Tickで回すものはないしイケルイケル
  - レベルが変われば破棄されるので、少し設計が悪くなっても定期的にデータ・メモリをお掃除できる

  • セリフ単体の管理はGameplayAbility(GA)

  - 会話全体の管理と、各セリフ毎のキャラ・テキスト・サウンド制御の実装は切り離したい!
    - セリフ毎にActor生成するのは非効率だし、UObjectで1から作るのも面倒。なのでGAで
  - 開始・実行中・終了・排他制御などの管理側は必要な要素が揃ってるので実装・管理が楽!のはず
  - 1セリフ - 1GA にして、1会話シーンは複数のGAを順に処理していく構成に。個人的にイメージしやすいし、スキップ対応とかも楽になるはず
    - コマンドのように発行するイメージ。しゃべるだけでなく、演出のみなどの制御もできそう

  • 会話シーンのデータ管理はData Asset

  - 結果を見ながらキャラ制御を微調整することになりそうなので、実行中に編集できるData Assetを参照する形が良さそう
  - 1会話シーン - 1DataAsset という構成は管理・メンテしやすいはず…?
    - 会話シーンは関連するアセットが増えるので、DataAssetなら参照関係を比較的整理しやすい!はず
    - 会話用DataAssetがセリフ用DataAssetを持つ形はアセットが膨大になって管理できない気がしたのでボツ
    - 会話用DataAssetがセリフ用Structを配列として持つ形に。GAはこのStructを元に各制御を行う
    - セリフ用Struct内の項目は多分多くなるので、将来的にはDataTableまたはEditorUtilityWidgetを編集エディタとして使おう…
  - フォーマットがある程度固まってからDataTableとの連携も考える
    - 最悪、テキスト部分だけDataTable管理にすれば多言語対応はなんとか…


という感じで当時色々考えてたようです。
あとは「実装・データをそれなりの粒度で分けてるし、まあ後から何とでもなるやろ!」的なことを思ってた記憶

最終的にどんな構成になったのか

会話処理のリクエス

f:id:pafuhana1213:20211212194255p:plain
Subsystem経由で再生したい会話用DataAssetを指定してリクエストというシンプルな形に。あくまでリクエストにしているのは、何らかの理由で会話を実行できない際にTalk Managerが拒否する仕組みにしてるからです(現状は成功・失敗の通知はEvent Dispatcherのみでやってますが今後はLatetntノード化する予定)。

Subsystemに関しては、レベルに必ず配置する必要があるActorを自動生成するためのものを作っていたので、そこで会話シーン管理用ActorであるTalk Managerを生成・管理してます。ちなみに、生成・管理以外の実装はSubsytem側には殆どありません。常駐物なのでなるべく実装・データは外部に置きたいためです。

あとは、1会話シーンをTalk Group, その中の1セリフ・演出をTalk Item(Struct)としてDataAssetで管理してます。Talk Itemにはセリフや演出を表現する上で必要な各データを管理していて、現状はこんな感じです。多分まだまだまだ変わります。

最終的にはDataAssetを直接渡すのではなく、DataAssetと紐づくIDをDataTableで管理してそのIDを渡す形になるかなと考えてます。

Talk Managerがやってること

f:id:pafuhana1213:20211212200447p:plain
↑がTalkManagerの全実装です。なんか色々ノードやコメントがありますが、やってることは単純です。

  • 会話で使用する全アセットのロード
  • 会話キャラ・キャプチャ用Actorの初期化
  • セリフ用GAを実行。終わったら次のセリフを実行または会話終了
  • 会話表示用のUMGの生成・破棄
  • ゲームパッドなどの入力に応じて、現在再生中の会話・セリフを制御

自画自賛タイム
Managerってなると実装が膨れがちですが…処理・役割を分散してるおかげでシンプルですね!必要となるアセット類もData Assetに逃しているので、Talk Managerの参照関係もシンプルになってるのもいい感じです(常駐物の参照関係が重くなってると、レベル移動時などに大変なことになりますからね…)

補足:会話で使用する全アセットのロード

DataAssetは基本的にソフト参照にしているので明示的にロード処理を行う必要があります。全ロードだけでなく部分的にロードしたいケースもあるので、DataAssetのBPにてロード処理支援用のノードをいくつも用意してます。なので、Talk Manager上ではそれほどノードが複雑になってません。

また、今回のようにキャラをカメラに寄った位置に配置すると、Texture Streamingの影響でキャラのテクスチャがボケた状態で表示されてしまう場合があります。そのため、このロードのタイミングでテクスチャのMipを強制的にロードしたりしてます。これらを全部同時にするとカクつきの原因になったりもするので実行タイミングをずらしたりしてます。
qiita.com

補足:会話キャラ、背景のキャプチャについて

f:id:pafuhana1213:20211213094341p:plain
特に目立った工夫もなくSceneCaptureComponent2Dを使ってます。ただそのまま使うと複数キャラ表示するときに負荷が問題になりそうなので、

  • ShowOnlyActorsを使って対象のキャラだけを描画
  • General / Advanced Show Flagsを適切に設定して、不要なレンダリング機能は動作しないようにする(特にポストプロセス
  • キャプチャ頻度は明示的に調整できるようにする(実行環境、表示するキャラ数などによって頻度を調整)

ということは注意してます。ちなみにキャプチャ処理はキャラ毎に行って、別々のRenderTargetTextureに焼き込んでます。これは従来の2Dイラストを使った会話システムでよく見かける演出を入れたかったからです(横からスライドしたり、フェードしたり、横に振動したり…)

f:id:pafuhana1213:20211213100921g:plain
またキャラ以外の背景に関しても会話開始直前のタイミングでキャプチャして、その結果を会話表示用UMGで背景として使っています。

会話シーン中にキャラ以外をポーズで止めることはよくあると思うのですが、実はポーズ中でも描画処理は走ってしまっています。そのため、もし画面上にキャラが2体いると合計3回も描画処理が走ってしまいます!これはやばそうです。

なので、FrameGrabber機能を使って会話直前の画面を保存し、その後にSet Enable World Renderingノードを使って会話キャラ以外の描画処理を止めています。ただFrameGrabberは少し処理重めでカクつきの原因にはなっているので、将来的には下記記事の方法を取り入れたいなぁと思ってます。
qiita.com

セリフ用GA

f:id:pafuhana1213:20211213104824p:plain
GA側で行っているのは、DataAssetにあるTalk Itemに設定された各データを元に

  • 会話用キャラのアニメーション再生
  • 会話用UMGの制御(テキスト更新、各種演出など)

を行っています。といってもキャラ・UMG側で用意されている処理を実行するだけなので、GA上で何か複雑な処理やActorを生成したりなどはしてません。既にTalk Managerが生成しているものに対して処理を発行して、それらの処理が終わったことを認識したらセリフ・演出は終了したとして自身を破棄するという形です。コマンドとして動作することに専念しているので、気軽に発行・破棄できて我ながらいい感じかもって思ってます。

f:id:pafuhana1213:20211213112754p:plain
ちなみに、GAからData Asset, 会話用キャラ・UMGへのアクセスはSubsystem経由からのTalk Managerから取得しています。Payload(GameplayEventData)という機能を使ってGAにデータを送ることは可能なのですが…色々カスタムする必要があって少々面倒であったり、このGAはTalk Managerから呼ばれることを前提としているのでGAから取得しにいっています。どこからでもアクセスできるSubsystemの強みですね!
qiita.com

会話表示UI

こちらは更に特に難しいことはしてなく、キャプチャしたキャラ・背景をImageウィジェットで表示して動かしてるぐらいです。

振り返ってみての感想

今回のような構成にしたのはSubsystem, GAS, DataAssetがマイブームだったのは正直大きいのですが、これらの機能を使おうとすると結果的にデータ・イベントドリブンの実装に自然となるのは大きなメリットだなぁと思います!

DataAssetによって各処理・表示を変えるようにすれば、データドリブンで量産にも強いですし参照関係の整理もしやすいです。また、改めて考えるとスクリプト的な役割にもなってて便利です(他人に作業を任せるならExcel・DataTable対応などが必要になりますが)。
Subsystem, GASに関してもそれぞれ役割・機能が明確だったり、特にGASに関しては(いい意味で)Tickや他処理との連携が制限されてるのでイベントドリブンにした方が設計が自然になるというのもいい所です。

特に個人開発だと他人との作業分担がないので一箇所でガーッと作ってしまいがちですが、こういった機能を活用して役割・機能を分散しておくと各箇所での実装がシンプルになりますし、様々なトラブルに対しての対策・対応もしやすくなるかと思います。最終手段として、問題になってる機能だけを取り除くとかもしやすいです。

正直な所とりとめのない記事にはなってしまいましたが、Subsystem, GAS, DataAssetを具体的にどう使ったらいいのか分からない!という方のヒントに少しでもなればいいなと思います。


明日の14日目はアンコウ@♨️行きたい (@dgtanaka) | Twitterさんによる「UE4HDRについて少々」です!
HDRなんもわからん状態なのですごく楽しみです!