はじめに
モバイルアプリを筆頭に「マウス(タッチ)のみで操作を完結させる」というのはよくあることかと思います。
そこで悩ましいのが、開発用やサブ入力としてキーボードなどでもクリックやタップ相当の操作をしたい!という状況です。
今までは専用メソッドを作るなどゴリ押しで対応してしまっていたのですが、どうにもナンセンスな気がして改めて調べ直したのでここに共有しておこうと思います。
前置き
onClick.Invoke
まず考えるのが、イベントの発火メソッド(Button なら onClick.Invoke())を呼び出すことです。
_button.onClick.Invoke();
が、これは非アクティブだろうとenable == false
だろうとinteractable == false
だろうと実行してしまうため、その辺りのケアが必要です。
OnPointerClick
とすれば次はイベント受け取りコールバックメソッド(Button なら OnPointerClick())を呼び出す方法です。
_button.OnPointerClick(new PointerEventData(EventSystem.current));
これは非アクティブ等の状態も見るらしく前述の問題はなく、これで事足りることも多そうです。ただ、イベントを受け取るコンポーネントが複数ある場合があり(音は別のスクリプトで再生している等)、合わせて正しく再現しようと思うと、イベントハンドラを持つコンポーネントすべてで実行しなければなりません。
オブジェクトでイベントを直接実行する ExecuteEvents.Execute
もっと簡単にオブジェクトをクリックしたという状態を起こすようなことはできないのかな、と色々調べていたところ ExecuteEvents.Execute という API があることを知りました。
これはゲームオブジェクトに対してクリック等(IEventSystemHandler 系)のイベントを実行するというものです。
例えば、Button コンポーネントをもつゲームオブジェクトへクリックイベントを送るコードは次のようになります。
ExecuteEvents.Execute(_button.gameObject, new PointerEventData(EventSystem.current), ExecuteEvents.pointerClickHandler);
これで Button のゲームオブジェクトに対してイベントを送ることができる(=他にイベントを受け取るコンポーネントがあればそれにもイベントが飛ぶ)ので、マウスでのクリックにかなり近い動作になると言えます。
ヒエラルキーも考慮する ExecuteEvents.ExecuteHierarchy
ExecuteEvents.Execute はゲームオブジェクトに直でイベントを送るため、例えばタッチガード1等、他のオブジェクト配置には無関心です。
もしそこまで考えるなら少し手間が必要で、対象の位置へレイキャストを飛ばしてイベントを発生させるゲームオブジェクトを取得する(もう少し具体的にはオブジェクトの位置に Canvas の GraphicRaycaster からレイキャストを飛ばして一番最初に当たったやつでイベントを発生させる)感じになるかと思います。
ところが少し困ったことに、uGUI 特有のイベントのヒエラルキー伝搬(?)で**「一番最初に当たったオブジェクト」が直接のイベント実行対象でない場合2があります。
しかしそこはさすが Unity、それをやってくれる ExecuteEvents.ExecuteHierarchy という API がピンポイントでありました。
例えば次のようなコードを書けば、クリックが最初に当たったゲームオブジェクトがイベントを発生させる対象でなくても親オブジェクトを辿ってイベントを発生させる対象を探して実行してくれて**、かつ、(タッチガードのような)親子関係にないオブジェクトで遮られていれば反応しないという、ほぼクリックしたのと同じ状態を再現できます。
// ※_raycaster は Canvas に付いている GraphicRaycaster
PointerEventData eventDataCurrent = new PointerEventData(EventSystem.current);
// イベントデータをボタンの位置に移動
eventDataCurrent.position = RectTransformUtility.WorldToScreenPoint(_raycaster.eventCamera, _button.transform.position);
// イベント受け取り可能なオブジェクトを取得
var hits = new List<RaycastResult>();
_raycaster.Raycast(eventDataCurrent, hits);
if (hits.Count > 0)
{
// 最前面のオブジェクトにクリックイベントを実行
ExecuteEvents.ExecuteHierarchy(hits[0].gameObject, eventDataCurrent, ExecuteEvents.pointerClickHandler);
}
おわりに
私は今回この API を初めて知りましたが、かなり昔のバージョンからあったようですね。
本記事が何かのお役に立てば幸いです。