前回の続きです。
早速Widgetを準備していきます。まずは 子Widgetから。
キャンバスに Image を置いて、TextBlockを重ねます。
とりあえず今回は280x280にしました。
今回見た目と機能をシンプルにしたかったのが理由ですが、Buttonコンポーネントでも大丈夫。TextBlock は後から文字列を差し替えるので、Is Variable にチェックを付けています。
次に見た目をアニメーションで用意します。
まず カーソルの乗っていない Default
次にカーソルの乗った Focus
カーソルが離れた時 Unfocus すこし余韻を持たせるためにフェードさせます。
アニメーションは完成。
最後に確認。
アニメーションを一つも選択していない状態で、キャンバスの見た目が Default と同じ状態 になっていることを確かめておかないと、表示を開始した時に意図しない状態で現れます。
これでキャンバスのエディットは終了。次はブループリントです。
子Widget の仕事は3つ。
- マウスカーソルの検出
- 検出した際に親に通知する
- 見た目の変化
まず下準備として 親Widget に通知するための数値を保持しておく変数を用意します。
この変数は 親Widget から渡されてくることになるので、 Expose on Spawn にチェック付けます。Compileした際に Warning が出るので Instance Editable にもチェックを付けます。名前は ButtonID としました。
この預かった数値を、マウスカーソルを検出した際に 通知します。
その通知の仕組みを Event Dispatcher(イベントディスパッチャー) が担当。
Event Dispatcher を追加して適当な名前を付けます。
できたら、値を受け取って渡すためのピンを追加します。
ピンの名前は先の変数と同じ ButtonIDとしました。
次はマウスが乗った時に検出する関数が用意されているので、オーバーライドします。
プルダウンで大量に出てくるので、On Mouse Enter を選択。
この関数のオーバーライドは、アセットのクラスによって内容が変動します。さすがにWidgetはUIに関連するアセットなので、入力関連が多数用意されています。
選択するとグラフに赤いイベントノードが現れるので、先に用意した変数とイベントディスパッチャーをつなぎます。
これで、3つあった仕事のうち、2つの準備が整いました。残りは1つ。
- マウスカーソルの検出
- 検出した際に親に通知する
- 見た目の変化
作ったアニメーションを再生するイベントを用意します。
このイベントは、自分で再生するのではなく、親Widget から呼び出して再生するようにします。マウス以外にキー入力によっても再生する必要があるからです。
UMGのアニメーションって、再生中に別のアニメーションを再生すると、先に再生しているアニメーションを止めてくれるわけではないので、意図しない見た目になってバグか?と思えるような結果になることもしばしば。再生する際に事前にアニメーションしているものがあれば止める仕組みを用意する必要があります。わりと面倒なのでオススメの仕組みを紹介しておきます。
まずは Widget Animation型 の変数を一つ用意します。
名前を CurrentAnim(現在のアニメーション)としました。
これを使ってマクロを2つ準備します。
forceAnimStop
再生中だったら止める、再生していなかったらなにもしない、というマクロです。
startInteraction
PlayAnimation という名前のノードがあるので、スタートインタラクションという名前にしました。Setノードの右にピン、地味に便利ですよね。
このマクロを使って、アニメーション再生用のイベントを用意します。
強制的にデフォルト
強制的にフォーカス
強制的にアンフォーカス
この方法だと、直前に何らかのアニメーションが再生中でも、確実に止めることができます。事前に用意しておいた WidgetAnimation型の変数をハンドラーとして使用することで、状況に依存しないフローを組むことができます。後からアニメーションの尺が変わっても問題なく動作します。プロジェクトのマクロライブラリに置いておくといいかもしれません。
UIはユーザーが操作するので、タイミングなんて予測できないし、可能な限りタイムラグのないレスポンスが期待されます。
一応これで必要な機能は揃いましたが、仕上げにもう一つボタンのラベルを書き換える仕組みを用意します。
Text型の変数を追加します。
この変数を Event Construct でTextBlockにつないでセットします。
このつなぎ方は最初の一回のみなので、途中でボタンのラベルが変更になる場合を想定するなら、関数を用意しておくのがベスト。
これで 子Widget 完成です。
ここからは 親Widget を準備していきます。
さっそくキャンバスに WrapBox を配置します。
今回 子Widget のサイズを 280x280にしたので、WrapBox のサイズは、 余白を2pxと想定して3個並ぶので、852x852としました。
キャンバスはこのくらいにして、グラフを編集していきます。
まず初期化のための関数を用意します。プロトタイプなら特に不要ですが配列のセットが大きくなってグラフが不格好になるのでここに格納する感じです。実際のゲームの場合メニューボタンを表示する際、セーブデータの内容や進行状況によってボタンの状態を変える必要があります。そのための場所としても使えます。
まずは一番大事な フォーカス位置を覚えておくInt型の変数と、ボタンをラベル用の配列をセットします。
indexCurrent という名前にしました。
ラベルは Text型の変数を作って配列にします。
今回この関数は初期のフォーカス位置とボタンのラベルを持たせているだけです。
特に戻り値もないので、 Return ノードは置いていません。
まずはこの関数を Event Construct につないで、ボタンになる 子Widget をインスタンス化するための ForLoop ノードをつなぎます。ループ回数は 0 から 8 の 合計 9 回。
続きはこんな感じ
ForLoop で複数回すことで CreateWidget ノードが 子Widget を次々にインスタンス化するので、結果的に量産することになります。
インスタンス化したらすぐに配列にお取り置きしています。これで 子Widgetたちを 番号で管理できるようになります。配列に取り置きしつつ、ReturnValueからの同じ青いラインで バインディング や パディング の設定をして最後に WrapBox に Add Child します。順番については特に制約とかはなさそうですが、ひょっとしたらあるかもしれません。
バインドは 子Widget に作っておいたイベントディスパッチャーの名前を探します。
このノードはイベントにつながないと、コンパイルした際にエラーになるけど、回避するために行ったり来たりすると、説明がややこしくなりそうなので、いったんループ処理の全貌を載せます。
で、エラーを消すためにバインドして受信したときに呼び出されるカスタムイベントを用意します。
Force~ という関数が2つ登場しますが、これは繰り返し同じつなぎ方をすることになるので Collapse Function して作ったのですが、内容は以下になります。
配列に格納した 子Widget たちに対して、指定した番号の持つ関数を呼び出しているだけです。アニメーションの再生処理してるイベントですね。
これで、先ほどのエラーが出ていたバインドノードが解決できます。
これでマウスを動かしたときに、フォーカスおよびハイライトの切り替えができたことになります。
レベルブループリントからビューポートに追加してテストしてみましょう。
PlayerController が持っている Show Mouse Cursor を 有効にしておくと再生した時にマウスカーソルが表示されるようになります。
残るは キーボードとゲームパッドの入力対応。
子Widget を 0~8 の Index番号で扱うので、マウスの時と同様に CurrentIndex を増減すればいいだけです。
今回キレイに3x3のグリッド状に並んでいるので、計算でどうにかしたかったので、タテ(上下)方向とヨコ(左右)方向でそれぞれカスタムイベントを作りました。
moveUpDown
moveLeftRight
このイベントには、それぞれ -1 と +1 が渡されてきます。左方向は X-1、右方向は X+1、上方向は Y-1、下方向は Y+1 というふうにイメージしています。
WrapBox内の並びは左上が 0 で、右下が 8 です。タテヨコに移動するように計算してして、はみ出たらIndex番号を更新しないという仕組みです。
ヒントは
タテ方向は、上下に移動するたびに ±3 なので、3を掛けて ±3で計算します。
ヨコ方向は、現在地に±1した結果に対して3で割ると余りは 0~2 なので %(剰余)を使うと便利。
ちょっと難しかったのが、マイナスになった時と、最大数を超えた時の判定です。
なるべくブランチノードを減らしたかったのもあって論理和 OR ノードを使っています。
この辺の作りは並ぶ数や最大値が変わったり、Disable状態のものをスキップするかしないかなどで、処理の仕方が変わってくるので、臨機応変な作り方が求められます。いろんなシチュエーションで作り慣れておくしかないのかも。
最後の仕上げです。
このままだと表示開始した時、ハイライト表現が無くて、操作していいのかどうか伝わりません。ForLoop 処理が終ったところでハイライトするようにします。
親Widget は以上です。
プロトタイプということにして、レベルブループリントでキー入力の判定を取ります。
New Var 0 は ビューポートに Add to Viewport する際にPromote to Variable しておいたやつです。テストなんで横着しています。
ようやく完成です。
いかがでしょうか?
状況によってはカスタマイズが必要になると思いますが、ひとまず基本的な原理はおさえられていると思います。カーソルといいつつ正確にはフォーカスおよびハイライトの遷移なんですけどね。
まぁそれっぽいキャラを子Widgetに仕込んでおいて、表示をON/OFFするとか、以前に記事で紹介していますが、親Widget側に用意したカーソルキャラをフォーカスの場所をトレイルするようにすると、カーソル感がアップします。
ではでは
ステキなカーソルライフを!
今回の失敗
実は最初カーソルを上下左右にループするように作っていましたが、並んでいる数が少なすぎて見た目に混乱しそうだったので、ループをやめたという経緯があります。
作ってみて試さないと得られない貴重な知見でした。
あと、マウスカーソルの検出に、Enter(入ってきた)とLeave(出ていった)があるのですが、作りはじめのころ 子Widget の方で両方に対応してしまっていたので、マウスカーソルが9つの子Widgetから完全に離れてしまうと、見た目にハイライト表現が消えてしまうので、キー入力するときにわからん、となっていたのがなかなか解決できずに悩んでいました。結局、Leave の検出はやめて、完全に 遷移は親Widget に任せることにしたらスッキリと解決しました。
マウスカーソルの表示について(追記: 2020/3/18)
以下の案件をご相談いただいたので、さっそく試してみました。
- マウスおよびキーボードを操作しているときはマウスカーソルを表示
- ゲームパッドを操作しているときはマウスカーソルを非表示
PlayerController で あらかじめ Show Mouse Cursor を 有効にしていたので、これを やめればいいのでは? と思い試してみたのですが、ダメでした。
で、次にやってみたのが、Input Mode 系のノード。
Set Input Mode Game And UI でひとまず、思ったような挙動になりました。
入力を受け取るたびに、セットするのもなんかノードが勿体ない感じがします。
ふと、レベルブループリントで一度つないでおけばいいんじゃないの?と気が付いて試してみました。
が、だめでした。残念。
公式のドキュメントで詳しく触れてそうな気配がなかったのですが、探し方が足りない気がするので、いずれもう少し踏み込んで調査したいと思います。とりあえず今回はこれでなんとかなると思うのですがいかがでしょうか?
ちなみに、マウスを操作しているかどうかは、マウスの移動量をみて判定しています。
Widgetの関数に OnMouseMove というのが用意されているので、今回はこれを オーバーライドして利用しました。
MouseEventピンから、Get Cursor Delta でマウスの移動量を取り出せるので、これでマウスに触れているかどうかを判定します。
ブランチノードの右にある Switch~ ノードは、 操作しているデバイスをアイコンで説明するために用意したカスタムイベントです。今回の記事を動画にする際にコッソリ仕込んでいたものです。
こんな感じです。
画像をあらかじめ3つ用意しておいて、表示・非表示を切り替えるというイベントです。
ゲームパッドとキーボードの方は、レベルブループリントで Switch~ を呼び出しています。
まずはこんなところですけど、ちょっと自信がないので、ツッコミ等々あれば是非是非。