※Unity従来のuGUIではなく、UI Toolkit(UI Elements)の方に関するトピックです。
えー本日はですね、単刀直入に概要から入らせて頂きますとですね、まず、下のようにUI ToolKitで作成したUIがあります。
こちらですね。こちらはUI ToolkitのUI Builderのスクリーンショットでして、あのう、これは試作品に過ぎませんので適当なボタンやら何やらが散乱しておるわけなんですけれども、注目して頂きたいのは赤枠の部分でありまして、こちらがゲーム画面を表示する場所であってですね、この赤枠の部分のVisualElementだけはPickingMode = Ignoreになっておってマウスカーソルのクリックイベントなどを遮らないようになっています。つまりこの中だけはUIではないという扱いにしたいわけです。
Center SpaceだけはPickingMode = Igonre
中央部はCenterSpaceと命名が出ていますけれども、それのPickingModeがIgnoreに設定されてあってUIではないと。で、逆にその周囲、つまりCenterSpace以外の部分は全てUIという扱いにしたいわけなんですね。このUI部分には説明文やその他のUIコントロール等を置く予定となっています。
で、何をしたいかと言いますと、中央部以外の、UI部分にカーソルがあるかどうかを判定したいと。
そこでいくつかのことを試しました。
UI Elementにはマウスカーソルが入ったり出たりするときにトリガーしてくれるイベントというものがあります。
ですのでこのようにですね、:
CenterSpaceだけを取得して、そこにマウスが入っている間はUI上にはない、逆にマウスがそこから出たらその周囲は全てUIですから、UIに入ったと、いう判定ができると。
実に簡単に済んでしまいました。
ところがこれは、あのう、実際には動作しないですね。何でかと言いますと、CenterSpaceはPicking Mode = Ignoreになっておるからなんですね。カーソルイベントを遮らないようにIgnoreになっているわけですが、Picking ModeがIgnoreになっているとイベントを発行してくれないので使えない訳です。うんともすんともいわない。
ですからこのやり方は使えませんでした。
もちろん、Picking Mode = Positionになっておる場合であれば、このやり方でそのまま問題なく使えます。
では逆に、周囲のUI部分にイベントを仕掛けておけばいいじゃないかと。反対の発想になるわけです。
とは言っても、UI部分は色々なパーツで出来ておって、その1つ1つにイベントを登録しておくなどというしち面倒くさいことはようしない。それだもんですから、ルートのVisualElementに、上と同様の、イベント時のコールバックでIsCursorOnUIの値を切り替えることにしたわけです。
今度は周囲がメインなのでIsCursorOnUIに入れる値はtrue/falseが逆になっていますが、やっていることは同じです。
しかしCenterSpaceもルートVisualElementの子の1つでありますから、これではCenterSpaceにカーソルが乗っている間もtrueになってしまうじゃないかと、皆さん思われるかしれません。
けれどもこれでうまく行くんです。なぜかと言いますと先ほど申し上げました通りCenterSpaceはPickingModeがIgnoreになっていますから、CenterSpaceの上にカーソルが入るとMouseLeaveEventが発行されてですね、ルートVisualElementから出た、という判定になるわけです。
ただ、もちろんCenterSpaceの下(親レイヤー)にPickingMode = Ignoreでない要素が挟まっている場合にはこの方法は使えません。しかしこの例では下に他の要素はなかったものですから、うまく行きました。
これで万々歳・・・と思っておったですが、実はこの方法では思わぬ落とし穴に遭遇しました。
これは後から追加したもんですから先ほどの画像には載っていなかったんですが、UI上にあるDropdownをエキスパンド(展開)している間だけは、MouseLeaveEventが発動してしまうようで、カーソルがUIの外に出た判定になってしまう。結果、IsCursorOnUIがfalseになってしまうんですね。
ドロップダウンが開いている間だけは、UI上にカーソルがないと判定されてしまう。
他のコントロールでは大丈夫でした。つまりSliderやInputField、チェックボックスなどでは問題は起こらないんです。が、Dropdownコントロールを展開した時だけはカーソルが去った判定になってしまう。もちろん、全てのコントロールで試したわけではありませんので他にもそうなってしまうものがあるかもしれません。
これはドロップダウンの展開によって出てくる部分だけは一時的に(動的に)作られた部分なので何か別のイベント領域の管轄になっているものと思われる。
一見完璧に動作するように見えたのに、このたった1つの穴のために使えなくなってしまったんです。
これには参りましたですね。
では、と考えましたんですが。
イベントではなく、矩形領域をベースにした処理にしたらばどうかと。
これは例えばVisualElementのworldBound.Contains()や、panel.Pick()あたりの手法が該当します。
ChatGPTに聞いた結果、Contains()を使って以下のようなメソッドを作りました。
これのveとして、CenterSpaceを渡すわけです。
CenterSpaceはPickingMode=Ignoreだからイベントは発行してくれないけれども、領域の予約はしておるはずじゃないかと。つまり領域系の処理であれば問題なく使えるんじゃないかと思ったわけです。
これは、結果的には当たりでした。ちゃんと領域ベースの処理では動いてくれます。
これがあのう、結果的にはそうだったんですが、実はこの時点ではまだ動かなかったんです。
どんな問題があったかというとですね、無反応ではなくなったんですが、何か反応がおかしいと。変な場所でtrue/falseが切り替わってしまう。
どうも実際のUIの座標とカーソル座標がズレているらしいんです。
ははぁこりゃ何だな、と。スケールが何かおかしいんじゃないかと、それでUIDocumentが使用しておるところのUI Panel Settingsを確認してみたところ、「Constant Phsiclal Size」になっておったんで「Constant Pixel Size」にしたら座標が合うようになりました。
しかしこれじゃあScale Modeが別だったらズレたままで使えなくなってしまう。「Constant Pixel Size」の時のみ正しく動くというのじゃ実用的じゃないわけです。
そこで調べてみてですね、最終的に以下のようにする必要があるとわかりました。
ChatGPTも教えてくれなかったですが、scaledPixelsPerPointという比率の値で座標を割っているんですね。
最終的な形
なお、scaledPixelsPerPointはrootVisualElementから取ってきたものでも問題ありませんでした。
このようにですね、1つのことを実現するにもいくつもの罠や道草を避けていかねばならんものですから、プログラミングというのは天才でない人間にとっては実に消耗戦であります。
プログラミング言語やソフトウェアを選ぶとき、その機能や仕様がまず注目されて、3番目くらいに「コミュニティやフォーラム(情報)の充実度」というのが挙げられることが多い印象ですが、実際に問題にぶつかった時の解決などを含めたパフォーマンスを測る方法があったとしたら、「検索したときに出てくる情報の充実度」というのは、我々の想像以上に何かを選ぶ上で大きなウェイトを占めているんじゃないかと思いましたですね。