UMG ViewModelとは
UMG ViewModelは、Unreal Engine 5で導入されたUIとゲームロジックを分離するデザインパターンです。
プレイヤーのHPとか、ゲームの残り時間をUMGに反映するのって面倒ですよね?それを適切に管理&簡単に実装できるようになる仕組みのことです。
“UMG Viewmodel”というプラグインを有効にすることで利用できますが、残念ながらBeta版の為、本番運用で使うことはおススメ致しません。(早く Production Ready になってくれ~)

前提条件
今回の解説は Unreal Engine 5.7.1(ランチャー版) での実行結果となります。その他のバージョンでは、若干画面構成が違ったりする場合がありますのでご注意ください。
EpicGames様による公式の解説ページは以下にあります
https://dev.epicgames.com/documentation/ja-jp/unreal-engine/umg-viewmodel-for-unreal-engine
以下で解説する内容と重複する部分があると思いますが、なるべく簡単に使えるように解説していきます。
基本編(まずは使ってみよう)
1.ブランクプロジェクトを作成
ブランクプロジェクトでプロジェクトを始めます。プロジェクト名は”UMGMvvm”としました。

2.プラグインの有効化
「編集」→「プラグイン」から”UMG Viewmodel”を有効にしてください。(その後再起動)

3.UMGを作成
今回は”WBP_MVVMTest”という名前で作成しました。(コンテンツドロワー上で右クリック、「ユーザーインターフェース」→「ウィジェットブループリント」からUMGを新規作成し、デザイナーでキャンバスパネルとその配下にProgressBarを適当に配置してください。これがプレイヤーのHPとなります。)

4.ViewModelBPの作成
MVVMViewModelBaseを継承した新しいブループリントを作成します。今回は”VM_PlayerState”という名前で作成しました。(コンテンツドロワー上で右クリック、「ブループリントクラス」を選択し、”すべてのクラス”を展開後、”MVVMViewModelBase”でフィルタリングして下さい)

5.ViewModel実装
上記4.で作成した”VM_PlayerState”を開き、変数に”PlayerHP”(Float型)を追加してください。この時、詳細パネル内の「フィールド通知」にもチェックを入れてください。この変数がUMG上に表示するプレイヤーのHPの数値となります。

6.UMGのバインディング
“WBP_MVVMTest”に戻り、「ウィンドウ」から「バインディングを表示」を選択してください。

7.バインディングでビューモデルの追加
「このエディタではウィジェットがバインドできるビューモデルが必要です。ここでビューモデルを追加しますか?」と表示されるので、「ビューモデルを追加」ボタンをクリックしてください。

8.ビューモデル選択
先ほど作成した”VM_PlayerState”が表示されるので、左クリックで選択した状態でダイアログ下部の「選択」ボタンをクリックしてください。

9.バインディング定義の追加
ProgressBarに対して、VM_PlayerStateに含まれるPlayerHPを割り当てます。まずは、UMGのデザイナー上でProgressBarを選択状態にしたら、バインディングを表示ダイアログの左上にある「+ウィジェットProgressBarを追加」をクリックしてください。

10.バインディング設定
ペンアイコンをクリックして、VM_PlayerStateのPlayerHPの値をProgressBarのPercentにバインドするようにしてください。(ペンアイコンをクリックすると、バインドする値を選択できるようになります。)
この設定を行うことでViewModelの変数に設定した値が、ウィジェットのプロパティに反映されます。(細かいことを言うと、値→プロパティだけの反映ではなく、双方向の反映も可能ですが今回は値→プロパティの一方向のみを解説します)

11.レベルブループリントにメインロジックの実装
動作確認として、一旦すべてレベルブループリントにロジックを書いてしまいます。(本当は適切なクラスに書くべきですが、まずは動作確認の為お許しを)”VM_PlayerState”を”WBP_MVVMTest”に紐づけてから、”VM_PlayerState”の”PlayerHP”を更新している処理となります。

12.動作確認
早速プレイしてみると、画面上にProgressBarのHPゲージが表示され、最初はゲージが0.1の位置にありましたが、1秒後に0.2の位置に移動するはずです。UMG側のBPは一切触っていないのに、”VM_PlayerState”の”PlayerHP”を更新するだけでUMGに反映されたことが分かります。

これで、UMG Viewmodel のファーストステップは完了しました。”VM_PlayerState”はUMGに表示する数値だけに専念していて、画面への反映や見た目は”UMG_MVVMTest”が担当しているので、実装責任の分離ができていて、プログラム全体の見通しが良いことが分かります。
基本的な使い方だけであれば、これで完了です。ここから先は、もっと有効的な使い方を紹介していきます。
プレイヤーのHPをバーだけじゃなくて、数値でも表示したい!
上記に続いて、HPバーの右側にHPの数値をテキストで表示してみたいと思います。”VM_PlayerState”のPlayerHPはFloat型の数値なので、これをテキスト型に変更してUMGに渡す必要がありそうですね。
1.ViewModelにPlayerHPをテキスト型で返却する関数を作成
“VM_PlayerState”に”PlayerHP”をテキスト型で受け取れる関数(GetPlayerHPText)を実装します。PlayerHP(Float型)は0~1でHPを管理していたのですが、見栄えが悪いので100倍してから文字列にしてみましょう。

2.作成した関数に「フィールド通知」を設定
“PlayerHP”に変化があった場合に、この関数の結果も自動的にUMGに反映する必要があるので、関数にも「フィールド通知」を有効にする必要があります。手順は、関数を選択状態にして、「純粋」「Const」にチェックを入れたら「フィールド通知」にもチェックを入れてください。その後、「実行」にもチェックを入れてください。

3.変数側の「フィールド通知」を設定
最後に変数に定義してあるPlayerHPを選択して、「フィールド通知」の右側にあるドロップダウンから”GetPlayerHPText”にチェックを入れてください。これで、”PlayerHP”に変更があった場合に、”GetPlayerHPText関数”も再評価され、UMGに結果が反映されるようになります。

4.UMGにテキストウィジェットを追加
UMG側にもテキストウィジェットを追加して、バインドを定義しないといけません。まずは、テキストウィジェットを追加します。これは、単純に配置するだけですね。

5.バインディングを表示ダイアログを表示
追加したテキストを選択した状態で「バインディングを表示」ダイアログを開きます。

6.バンディング定義の追加
ダイアログ左上の「ウィジェットテキストを追加」をクリックして、VM_PlayerStateのGetPlayerHPTextの値をテキストウィジェットのTextにバインドする定義を作成します。

7.バンディング定義の確認
最終的にはこんな感じです。

8.動作確認
実行すると、ProgressBarの下にヒットポイントがテキストで表示されるようになります。

レベルブループリントに実装しているプレイヤーのHPを更新する処理を一切更新せずに、UMG上のテキスト表示のHPを追加することができました。ViewModelをバインドするダイアログでは、ウィジェットの値だけでなく、様々なパラメータに対しても変数をバインドすることができるので、単純にUMGに反映する以外の使い方もありそうです。
実践編
今はレベルブループリントにロジックを実装してしまいましたが、本格的な利用方法の例として、以下の仕様で実装してみようと思います。
- BPだけで実装する
- UMGに表示するのはプレイヤーのHPとゲームの残り時間
- プレイヤーのHPはProgressBarだけでなく、文字と円型ゲージでも表示する
- UMGはPlayerControllerで作成(CreateWidget)し表示(Add to Viewport)する
- プレイヤーのHPはPlayerStateで管理
- ゲームの残り時間はGameStateで管理
これ以降は、上記の実装を理解している前提での説明になるので細かい説明を省く可能性があります。
完成イメージはこんな感じです。(UMGの配置がめっちゃ雑でごめんなさい)

今まで説明してきた基本的な使い方だけではダメで、いくつか設計面・実装面を考慮する必要がありそうです。実践編ではそのあたりもしっかり作っていきましょう。
1.ViewModelの作成(GameState用ViewModelの追加)
ViewModelはPlayerState(PlayerHP用)と、GameState(残り時間)で2つ用意します。とは言っても、VM_PlayerStateは基本編のもののまま、VM_GameStateも作りは全く同じで管理する変数がHPから残り時間に変わっただけです。(VM_GameStateの残り時間はFloat型をInt型に変換しているのでミリ秒を切り捨てて表示している違いがあります)


2.PlayerState実装
PlayerState(PS_MVVM)を作成します。ここではPlayerHPの管理を想定しています。BeginPlayでVM_PlayerStateのインスタンスを生成して変数に保存し、TickでHPを減らす処理を実装しています。HPを減らしたら、変数に保存しておいたVM_PlayerStateのPlayerHPを更新しているだけです。(実際にはダメージを受けたらHPを減らす処理にしてください)

3.GameStateの実装
GameState(GS_MVVM)を作成します。ここでは残り時間の管理を想定しています。上記のPlayerStateのHPが残り時間に変わっただけなので、GameStateとPlayerStateはほぼ同じ構成です。

4.ウィジェットに残り時間とPlayerHP用の円型ゲージを表示するエリアを追加
UMGのデザインを実装します。残り時間用のテキストブロックと、HP用の円型ゲージのImageウィジェットを追加しました。

5.バインディング定義
ここが結構重要です。「ビューモデル」ウインドウ内でVM_PlayerStateとVM_GameStateの2つのViewModelを追加しておき、「バインディングを表示」ダイアログにて、各ウィジェットに対して値をバインディングしていきます。ただし、円型ゲージにおいてはProgressBarと違ってウィジェット自体のプロパティで円を描画するワケではないので、WBP_MVVMTestにPlayerHP(Float型)の変数を追加したうえで、その変数に対して直接ViewModeのPlayerHPをバインドしています。つまり、ViewModelの値は各ウィジェットのプロパティ以外にも、UMGに定義した変数にも直接バインドすることができます。

6.円型ゲージの描画実装
PlayerHPの値を円型ゲージに反映する処理ですが、一旦TickでUMG内の変数であるPlayerHPの値を反映する方式で実装してみましょう。

参考ですが、円型ゲージのマテリアルは以下のようになっています。

7.PlayerController実装
最後にPlayerControllerでUMGを表示させます。(本来はもっと適切なタイミングで実装する必要はありますが)GameStateのViewModelとPlayerStateのViewModelをそれぞれ取得し、”UMG_MVVMTest”のViewModelにセットしてから、Add to ViewportでUMGを画面に表示しているだけです。




8.GameMode
GameModeは、上記で作ったクラスをデフォルト値として設定し、これをレベルのゲームモードに設定するだけです。

9.動作確認
実行すると自動的に残り時間と、プレイヤーのHPが減少するUMGが表示されます。円型ゲージも、残り時間もすべてが正常に動いています。

これで、実践編の実装は完了です。各実装箇所において明確に責任の分離ができており、非常にデバッグや変更がしやすい構成となりました。
10.ブラッシュアップ
上記6.の解説内でUMGのTickを使っていることが気に入りません。これには理由があり、ViewModelのバインディング機能だけでは、ViewModelの変数(や関数の戻り値)と、UMGのウィジェットの変数やプロパティのバインディングしか実装できない為、仕方なくこの方法で実装していました。(円型ゲージの値はウィジェットのプロパティで制御するのではなく、マテリアルパラメータで制御しているのでバインディングでは連動ができない為)
この部分については、ViewModel(VM_PlayerState)のPlayerHPをSetterから更新する仕組みに変更し、Setter関数の中でイベントディスパッチャーを定義したうえで、イベントディスパッチャーを呼び出す仕組みを作ります。

PlayerStateのPlayerHPをViewModelに渡す部分の処理を、変数に直接渡す方法から、Setter関数経由に変更します。

これで、ViewModelのPlayerHPが変更された場合にイベントディスパッチャーが呼び出されるようになったので、UMG側ではイベントを監視して、PlayerHPに変化があったら円型ゲージのマテリアルパラメータを更新するようにします。
重要なのは、UMGにViewModelを追加すると、イベントグラフの変数として使えるようになる為、VM_PlayerStateからOnUpdatePlayerHPイベントのバインドをConstructで定義しているところです。

今回は円型ゲージの更新にイベントディスパッチャーを使用しましたが、例えばHPが減少した時にHPゲージを揺らすアニメーションをさせたい場合なども、同様の方法で実現が可能です。
まとめ
UMG ViewModelを使用することで、実装の責任範囲を明確にすることができました。UMG ViewModelが無いとPlayerStateで管理しているPlayerHPをUMGに反映する時に、UMGの中でPlayerHPをテキスト表示する為の変換をしたり、数値を100倍にする等、UMGの実装なのに「UIの表示」だけに集中することができませんでした。(逆に、PlayerState側でそられの値を編集すると、PlayerStateの守備範囲を超えてしまう)
UMG ViewModelを導入することで、PlayerStateはPlayerHPの値の管理とViewModelへの伝達に集中することができ、ViewModel内では値の変換とUMGとの連携が責任範囲となり、UMGはUIを表示することに集中すればよくなるため、不具合の原因追及や仕様変更への対応速度が大幅の向上します。
まだBeta版のプラグインではありますが、この便利さを味わってしまうと早くProduction Readyになってほしいという気持ちが抑えられません!

No responses yet