マウスでは押せるメニューなのに、パッドで下を押すと選びたいボタンを飛ばしてしまう。別の画面では、開いている間だけゲームが重くなる。UIの見た目ができた後には、操作のしやすさと、動かすための負荷も気になってきます。
この2つは分けて調べます。前半では 入力を受けるボタンと移動順 を決め、後半では 表示を更新する回数と、その負荷 を確かめます。同じ小さなメニューを使い、どこを変えると何が変わるかを見ていきましょう。

この記事でわかること
- フォーカス、マウスのHover、選択枠の違い
- ExplicitとStopで、ボタンの移動順を決める方法
- 毎フレームの更新を、必要なときの更新へ変える手順
- 変更前後の測り方と、Invalidation / Retainerの役割
フォーカスは、キー入力の行き先
フォーカス は、キーボードやゲームパッドの入力を受けるUIの対象です。ボタンにフォーカスがあれば、Enterやパッドの決定ボタンで、そのボタンを押せます。方向キーは、そこから次の選択先へ移るために使います。
一方、Hover(ホバー) は、マウスポインターが部品の上にある状態です。マウスが「操作を確認」の上にあっても、キー入力のフォーカスは「再開」にある、という状態は起こり得ます。

さらに 、フォーカスを持っていることと、それを枠や色で見せることも別です。選択先が変わっているのに、見た目が同じで気付けない場合もあります。まずは どのボタンが入力を受けているか見える状態 にすると、調べやすくなります。
準備:2ボタンのメニューを、3ボタンへ広げる
ゲームパッド対応の記事で作った WBP_PadMenu を使います。「再開」と「操作を確認」があり、MキーまたはパッドのMenu/Startで開き、「再開」を押すとゲームへ戻れるメニューです。まだ作っていない場合は、リンク先のメニュー作成から進めてください。
今回はそのWidgetへ部品を足します。開閉に使うBP_InputPracticeとPadMenuWidgetは、そのまま使います。ゲーム時間を止めるポーズ処理は加えません。 フォーカスと表示更新を、まずこの条件で確かめます。
Designerで、既存のVertical Boxを次の順にします。CountButtonはHelpButtonを複製し、名前と表示する文字を変更すると揃えやすくなります。
| 部品 | 名前 | 表示・設定 |
|---|---|---|
| Text | 見出し用 | メニュー |
| Button+Text | ResumeButton | 再開 |
| Button+Text | HelpButton | 操作を確認 |
| Button+Text | CountButton | 数を更新 |
| Text | HelpText | 最初は空欄。既存の操作説明を表示する場所 |
| Text | 数の見出し用 | 目印の数 |
| Text | CountText | 最初は「未集計」。Is Variableをオン |
3つのButtonは Is VariableとIs Focusableをオン、VisibilityをVisible、Is Enabledをオンにします。Is Focusableはキー入力の選択先になれるか、Is Enabledは操作を受け付けるかの設定です。
ボタンが重ならないよう、Vertical Boxの高さを300程度へ広げ、子のSizeをAuto、各ButtonのPaddingを8程度にします。中央アンカーとAlignment (0.5, 0.5)を保ち、Designerで並びを確認してください。レイアウトの基本はUMG入門で扱っています。

練習中は「プロジェクト設定」→「User Interface」の Render Focus RuleをAlways にします。フォーカスのある部品に枠を表示する設定です。マウスはボタンの上から外して、キーで移った枠を見ます。
Navigationで、次のボタンを指定する
Navigation は、方向キーやTabを押したときに、フォーカスをどう移すかの設定です。Designerで Button自体 を選び、DetailsのNavigationを開きます。中のTextではなく、入力を受けるButtonに設定します。
最初に使うルールは、次の3つです。
| ルール | 動き | 使う場面 |
|---|---|---|
| Escape | その方向へ進み、次に選べる部品を自動で探す | 自動探索に任せられる並び |
| Explicit | 指定した部品へ移る | 次に選ぶボタンを固定したい |
| Stop | その方向へ移らず、今の部品に留まる | メニューの端で止めたい |
EscapeはキーボードのEscキーを設定する項目ではありません。Navigationのルール名です。自動探索のままでも素直な並びなら使えますが、複雑な配置で期待する移動先が決まっているなら、Explicitで指定すると意図を確認しやすくなります。
上下の移動を決める
今回のメニューは、上から順に選び、両端で止まる形にします。Explicitを選ぶと出る指定先へ、次のButton名を入れます。候補が出ない場合は、名前とIs Variableを確認してコンパイルします。
| 選択中のButton | Up | Down |
|---|---|---|
| ResumeButton | Stop | Explicit → HelpButton |
| HelpButton | Explicit → ResumeButton | Explicit → CountButton |
| CountButton | Explicit → HelpButton | Stop |
3つとも Left / RightはStop にします。横方向へ移る項目がないためです。
Tabも使えるように、NextをDownと同じ指定、PreviousをUpと同じ指定にします。NextはTabで先へ、PreviousはShift+Tabで前へ進む経路です 。上下だけ決めて終えると、Tabでは違う順になることがあるので、別々に確認します。

一箇所変えて、飛び先を確かめる
コンパイル・保存してPlayし、Mでメニューを開きます。下キーで「再開 → 操作を確認 → 数を更新」と移り、一番下でもう一度下を押すと止まれば、設定どおりです。上キーでは逆順に戻ります。
次にPlayを止め、ResumeButtonのDownだけを一時的にCountButtonへ変えてみます。再び開いて下を押すと、「操作を確認」を飛ばすはずです。見た目では隣でも、Explicitは指定した相手へ移る ことが分かります。確認後はHelpButtonへ戻します。
移動先が意外なときは、まず「今のボタンに、どの方向のルールが入っているか」を見ます。非表示や無効になったButtonを指定したままなら、その画面の状態に合わせて有効な相手へ設定し直します。
最初のボタンを選ぶ・選べない理由を調べる
Navigationは、すでに選んでいる場所から次へ移る設定です。メニューを開いた直後の選択先は、別に決めます。
ゲームパッド記事の FocusFirstButton は、WBP_PadMenuの中で Set User Focus を呼ぶ関数です。TargetはResumeButton、Player ControllerはGet Owning Playerにします。「このプレイヤーの入力先を、このButtonへ移す」という指定です。

開く側の処理は、次の順を保ちます。
- Create WidgetでWBP_PadMenuを作り、PadMenuWidgetへ保存する。
- 同じWidgetをAdd to Viewportで表示する。
- Set Input Mode UI OnlyでUIへ入力を向け、カーソルを表示する。
- 最後に、そのWidgetのFocusFirstButtonを呼ぶ。
この順番の後に、別のWidgetへフォーカスを渡す処理があると、今の指定は上書きされます。「Set User Focusを置いたか」だけでなく、呼んだ後に何が動くか も確認します。
| 症状 | 確認するところ |
|---|---|
| 開いた直後に選択枠がない | FocusFirstButtonの実行線、ResumeButtonのIs Focusable、Render Focus Rule |
| マウスで押せるがキーで選べない | UIへの入力モード、最初のButtonへのフォーカス |
| Set User Focusで選べない | Targetが実際のButtonか、表示済み・有効か、ControllerがOwning Playerと同じか |
| 一度選ばれるがすぐ別へ移る | 後続の入力モード変更、別のSet Focus、画面の再作成 |
| 非表示から戻した後に選べない | Visibleへ戻した後のフォーカス指定と、Navigationの指定先 |
今回のUI Onlyでは、開いた後の操作をWidgetが受けます。ゲーム側のMキーでも閉じる構成ではなく、ResumeButtonで閉じ、Game Onlyへ戻します。Game and UIにすれば、UIが処理しなかった入力をゲーム側も受けられますが、UIが処理したキーまで必ず届くわけではありません。
UIの負荷は、更新処理と表示の仕事を分けて見る
次は、同じメニューの表示更新です。画面に出ている数字が変わらなくても、その裏で毎フレーム同じ検索や計算をしていることがあります。

フレーム は、ゲームが画面を更新する一回分です。WidgetのEvent Tickから処理を呼ぶと、表示中の各フレームで繰り返します。TextやPercentへ設定するBindingも、値を繰り返し読み取ります。小さな処理でも、部品の数と呼ぶ回数が増えると負荷が積み重なります。
一方、UMG側にも、部品の並びや大きさを計算し、描画に必要な情報を用意する仕事があります。実際に画面へ描くGPUの仕事もあります。自分のBlueprintだけを減らしても、これらの負荷が大きければ、全体の速さはあまり変わりません。
まずは、見た目を変えずに 同じ計算を繰り返す必要があるか を考えます。その効果を測ってから、部品の数や描画の仕組みへ進みましょう。
実践:数の表示を、必要なときだけ更新する
メニューに、レベルへ置いた目印の数を表示します。ここで数えるのは ワールドに存在するActor です。プレイヤーが持っているアイテムの数とは区別してください。
1. 数える目印を3個置く
Actorを親とするBlueprintを作り、BP_UIPracticeMarker と名付けます。Static Mesh Componentを追加してCubeを選び、Collision PresetsをNoCollisionにします。物理シミュレーションはオフのままです。
Class DefaultsでStart with Tick Enabledをオフにし、このActorには毎フレームの処理を作りません。コンパイル・保存して、レベルの見える場所へ3個置きます。配置を数える練習なので、ゲーム中に生成・削除する処理はまだ加えません。
2. 目印の数を表示する関数を作る
WBP_PadMenuに、入力も出力もない関数 RefreshPracticeMarkerCount を作ります。
- 関数の入口からGet All Actors Of Classへ白い実行線をつなぎ、Actor ClassにBP_UIPracticeMarkerを指定する。
- Out ActorsからLengthを置く。
- Lengthの結果を整数からTextへ変換し、Set TextのIn Textへ渡す。
- CountTextをGetで置き、Set TextのTargetへつなぐ。
- Get All Actors Of Classの白い実行出力をSet Textへつなぐ。
Get All Actors Of Class は、指定した種類のActorを探して、Array(配列) で返します。配列は、複数の値をまとめて 持つ入れ物です。Lengthでその要素数を読むと、今回なら3になります。
数値を表示用の Text へ変えるには、Lengthから引き出してTo Textの整数用ノードを選びます。検索する処理には白い実行線を通しますが、Lengthと型変換は値を作るノードなので、白い線を通しません。

この計算に、実行の順番をつなぎます。

2枚は同じ関数の配線を分けて示したものです。Get All Actors Of Classは1個だけ置き、データ線と実行線を両方つなぎます。
CountTextのTextにはBindingを付けず、この関数のSet Textで更新します。
3. いったん毎フレーム呼び、呼ばれ方を見る
WBP_PadMenuのイベントグラフで、Event TickからRefreshPracticeMarkerCountを呼びます。コンパイルしてPlayし、メニューを開きます。「目印の数」の下に3と出れば、検索から表示までつながっています。
見た目は3のままですが、処理は繰り返されています。確かめるには、RefreshPracticeMarkerCountの入口へPrint Stringを一時的に挟み、In Stringを Count refreshed、Print to Screenをオフ、Print to Logをオンにします。メニューを開くと、Output Logへ同じ文が続けて出ます。確認は短時間で済ませてください。
このログは 呼ばれていることを確認するためのもの です。ログ出力にも負荷があるので、後で性能を測るときには外します。
4. 開いたときと、更新ボタンを押したときへ移す
Playを止め、Event Tickから関数へつないだ白い線を外します。代わりに次の2箇所から、同じRefreshPracticeMarkerCountを呼びます。
CountButtonのクリックイベントは、DesignerでそのButtonを選び、DetailsのEventsにあるOn Clickedの「+」から追加できます。
| 呼ぶタイミング | 目的 |
|---|---|
| WBP_PadMenuのEvent Construct | 作られたメニューへ、最初の数を表示する |
| CountButtonのOn Clicked | 「数を更新」を押したときに、数を調べ直す |
すでにEvent Constructへ処理がある場合は、その末尾へ続けます。今回の開閉では毎回Widgetを作るため、開くと初回の集計が行われます。Event Constructは表示階層の作り直しでも再実行される場合があり、Widgetの生涯で必ず1回というイベントではありません。

再びメニューを開くと3が出ます。そのまま待ってもログは増えず、「数を更新」を押すと1行増えるはずです。表示する数は同じでも、調べる回数を減らせました。 確認後はPrint Stringを外します。

Playを止めて目印を1個追加し、再び開いて4になることも試してください。練習では目印の数を実行中に変えないので、待つ間ずっと調べる必要がなかったわけです。
実際のゲームで所持品やHPが変わる場合は、それを変更した処理からUIへ知らせます。初回の表示と、その後の変更通知を組み合わせる方法はHPバーを作る実践へつながります。
同じ条件で、変更前後を測る
呼び出し回数が減ったことと、ゲーム全体が目に見えて速くなったことは別です。目印3個の練習では、処理が小さく、時間の差が測定の揺れに埋もれることもあります。
負荷のある実際の画面を調べるときは、計測 → 一箇所を変更 → 同じ条件で再計測 の順にします。
- Print Stringなどの確認用ログを外す。
- Play中のコンソールで
stat unitを実行する。メニューをUI Onlyで開く前に表示しておく。 - 同じ場所でキャラクターを止め、同じ カメラ・画面サイズでメニューを開き、Gameなどの時間を見る。
- Playを止め、調べたい更新処理だけを変更する。
- 同じ手順でメニューを開き直し、しばらく観察して比べる。
Gameは、ゲームスレッドの処理時間 です。ゲームの多くの処理が動くCPU側の時間で、UIだけの数字ではありません。単位のmsはミリ秒で、1秒の1000分の1です。一瞬の値だけで判断せず、同じ操作で何度か比べます。

今回の練習なら、Tickから呼ぶ版と、Construct/クリックから呼ぶ版を比べます。メニューを開く前後だけでは、Widget作成などの別の負荷も混ざります。ポーズを使う画面では、ゲームを止めた側と止めていない側を、そのまま変更前後の比較にしないようにします。
差が見えなければ、無理に「改善した」と結論付ける必要はありません。どの処理が時間を使うか、さらに細かく追う場合はUnreal Insightsの記事へ進みます。エディタで傾向をつかんだ後は、配信先の環境でも確認します。
Invalidation BoxとRetainer Boxを使い分ける
更新する回数を整理しても、部品が多い画面では配置や描画の準備に負荷がかかります。そのときに検討するのが、計算した情報や描いた結果を キャッシュ する仕組みです。キャッシュは、一度作ったものを保存して、次にも使うことです。

Invalidation Box:変わらない部分の情報を使い回す
Invalidation Box は、子Widgetの配置や描画に使う情報を保存します。変化がない間はその情報を再利用し、必要な部分が変わったら計算し直します。
この「保存した情報が古くなった」という印が Invalidation です。UIが無効になって操作できなくなる、という意味ではありません。
たとえば、説明文や固定のアイコンが多いパネルが候補です。Designerでそのまとまりを選び、「Wrap With」からInvalidation Boxで包み、同じ画面で前後を比べます。部品を毎フレーム増減したり、大きさを変え続けたりする部分では、情報を長く再利用できません。
頻繁に変わる小さな部品だけを Is Volatile にする方法もあります。その部品の描画情報を毎回作る扱いにし、周囲の静的な部分と分けます。画面全体を一律にVolatileへする設定ではありません。
プロジェクトで Global Invalidation が有効なら、ウィンドウ全体でこの仕組みを使います。その中では個別のInvalidation Boxによるキャッシュは使われなくなるため、Boxを追加する前に Slate.EnableGlobalInvalidation の設定も確認します。
Retainer Box:UIを一枚の絵へ描いて使う
Retainer Box は、子のUIをいったん Render Target という画像の保存先へ描き、それを画面に表示します。画面へ直接並べて描く代わりに、一枚の絵へまとめるイメージです。
その絵を更新するフレームも調整できます。たとえば、変更が即座に見えなくてもよい表示をDesignerでRetainer Boxに包み、Render Rulesを次の組み合わせにすると、更新間隔の違いを試せます。
| 設定 | 試す値 | 意味 |
|---|---|---|
| Render On Phase | オン | 指定したフレームの順番で更新する |
| Phase Count | 3 | 更新の順番を3つに分ける |
| Phase | 0 | そのうち0番の順番で更新する |
| Render On Invalidation | オフ | 子の変更要求による追加更新を、この比較では使わない |
この設定では、定期的な更新が3フレームに1回になります。Phaseは秒数ではなく、更新する順番 です。ゲームのフレームレートが変われば、実時間での更新間隔も変わります。Render On Invalidationをオンにすると、子の変更要求でも更新するため、この周期以外の更新も起こります。

更新間隔を空けた分、表示の変化が遅れて見える場合があります。特に選択枠や押した反応は、遅れると操作しにくくなります。今回の3ボタンのメニュー全体を、とりあえずRetainerで包む必要はありません。
Retainerは追加の画像メモリを使い、描き直すときにも負荷がかかります。描画回数をまとめたいのか、更新を間引きたいのか、マテリアルでノイズなどの演出を加えたいのかを決めて選びます。Invalidation Boxの効果が小さければ何でもRetainerへ置き換える、という選び方はしません。
おまけ:画面を広げるときの確認
隠すことと、作らないことを分ける
HiddenやCollapsedは表示状態です。Collapsedはレイアウト上の場所も空けますが、すでに作ったWidgetのメモリまで消す操作ではありません。 大量のタブや一覧を最初から全部作って隠しておくと、作成時とメモリの負荷が残ります。
長い一覧では、見える行を中心に作るList Viewなどの仕組みを検討します。Scroll Boxで囲むだけで、全項目の作成が自動的に省かれるわけではありません。
選択先が消える場面を試す
設定パネルを閉じた後は、そのパネルを開いたボタンへ戻す。選択中の項目を削除したら、残っている隣の項目へ移す。このように 表示を変える処理と、次に選べる場所を決める処理 を合わせます。
フォーカスを色で見せたい場合、通常のButton StyleのHoveredだけではパッドの選択を表せません。Has User Focusなどで選択状態を調べ、見た目へ反映する仕組みを用意します。まずは今回のAlwaysの枠で、移動と決定が成立するか確かめてから作り込みます。
実際にどのWidgetが画面を構成しているか調べるには、Widget Reflectorも使えます。メニューの構造が複雑になった段階で、表示されている部品の階層をたどる助けになります。
ポーズ画面へ広げる
ポーズメニューの記事では、画面の開閉と入力先に加え、ゲーム時間を止めて戻します。今回決めたNavigationは、ポーズを加えても必要です。まずMで開き、選び、「再開」で歩けるところまでを一周させてから、時間の制御を加えましょう。
まとめ
フォーカスの問題は「最初に誰が入力を受けるか」と「次にどこへ移るか」を分けると調べやすくなります。負荷の問題は、表示が同じでも繰り返している処理を見つけ、必要なタイミングへ移してから測ります。
まずは3つのボタンを順に選べること、集計がメニューを開いたときと更新ボタンを押したときだけ呼ばれることを確かめてください。その上で、実際に負荷の大きい部分へInvalidationやRetainerを使うと、目的と効果を見失わずに調整できます。
参考:Navigationのルール、Set User Focus、UMGの最適化指針、UI Invalidation、Retainer Box。