はじめに
チュートリアルで、フォーカスさせてここのボタン押せというのがよくあります。
その際他のボタンは押せなくして、対象のボタンだけ押せるようにする必要があります。
チュートリアルのことも考えて設計していたら制御が楽なのかもしれませんが、経験上無理やり実装していることが多い感じがします。
今まで見てきた例
・フォーカス時に無理やりボタンを最前面に持ってくる
→座標がずれたりとか調整大変
・4枚の四角形組み合わせてフォーカスしたいところだけ隙間を作る
→デザインの自由度がない
・フォーカスしている間はそもそも押させない
→ユーザー体験が悪い時がある
無理やり実装しがちで、悩まされる話題だと思うので、少しでも実装が楽になれればと思い良い方法を考えてみました。
概要
デモ
フォーカスしているところのみを押せるようにしています
ヒエラルキー構造
デフォルトの状態だと何が問題か
画面全体を覆っているレイヤーが下のレイヤーのUIをブロックしてしまい、フォーカスしている領域だけブロックさせない設定が標準でできません。
実装方法
やりたいこと
フォーカスさせたい領域が押されたら下のレイヤーに存在するオブジェクトにクリックイベントを伝播させたい。
コード全体
上記の例では、以下のコンポーネントを上記のUnmaskオブジェクトにアタッチしています。
今回のサンプル用に書いたので、実践で使う際にはある程度カスタマイズが必要になると思います。
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.EventSystems;
public class UiRaycastThrough : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler
{
private EventSystem eventSystem;
private GameObject pointerDownTarget;
private void Awake()
{
eventSystem = FindObjectOfType<EventSystem>();
}
private void OnDestroy()
{
// 破棄されたときにボタンが押された状態になってしまうことを防ぐための処理
InvokePointerUp(new PointerEventData(EventSystem.current));
}
private void OnDisable()
{
// 非アクティブになったときにボタンが押された状態になってしまうことを防ぐための処理
InvokePointerUp(new PointerEventData(EventSystem.current));
}
public void OnPointerDown(PointerEventData eventData)
{
pointerDownTarget = CheckAndInvokeEvent(eventData, ExecuteEvents.pointerDownHandler);
}
public void OnPointerUp(PointerEventData eventData)
{
InvokePointerUp(eventData);
}
public void OnPointerClick(PointerEventData eventData)
{
CheckAndInvokeEvent(eventData, ExecuteEvents.pointerClickHandler);
}
private void InvokePointerUp(PointerEventData eventData)
{
if (pointerDownTarget != null)
{
ExecuteEvents.ExecuteHierarchy(pointerDownTarget, eventData, ExecuteEvents.pointerUpHandler);
pointerDownTarget = null;
}
}
private GameObject CheckAndInvokeEvent<T>(PointerEventData eventData, ExecuteEvents.EventFunction<T> eventFunction) where T : IEventSystemHandler
{
var results = new List<RaycastResult>();
eventSystem.RaycastAll(eventData, results);
var target = results.FirstOrDefault(itr => itr.gameObject != gameObject && itr.gameObject != transform.parent.gameObject);
if (target.gameObject == null)
{
return null;
}
ExecuteEvents.ExecuteHierarchy(target.gameObject, eventData, eventFunction);
return target.gameObject;
}
}
解説
PointerHandler
IPointerXXXHandlerはお馴染みのインターフェスで解説は必要ないと思います。
Unity標準のButtonコンポーネントにもこちらのインタフェースが実装されていて、ポインター(マウス操作の場合カーソル)のイベントを受け取ることができる便利なインターフェースです。
今回のサンプルでは押したときにボタンをグレーアウトさせるのに、PointerUp,PointerDownHandler、
ボタンのクリックイベントを呼び出すためにPointerClickHandlerを使用しています。
クリックされたオブジェクトの判定
今回はフォーカスした部分にボタンが一つしかないですが、二つあった場合でもどちらがクリックされたか判定されるように対応しています。
private GameObject CheckAndInvokeEvent<T>(PointerEventData eventData, ExecuteEvents.EventFunction<T> eventFunction) where T : IEventSystemHandler
{
var results = new List<RaycastResult>();
eventSystem.RaycastAll(eventData, results);
var target = results.FirstOrDefault(itr => itr.gameObject != gameObject && itr.gameObject != transform.parent.gameObject);
if (target.gameObject == null)
{
return null;
}
ExecuteEvents.ExecuteHierarchy(target.gameObject, eventData, eventFunction);
return target.gameObject;
}
EventSystemのRaycastAllを使用すると、クリックされた場所と重なるオブジェクトをすべて取得することができます。
一番手前のレイヤー(ヒエラルキーだと一番下)から順番にresultsに格納されていきます。
今回のサンプルの場合、ボタンの上には画面全体を覆う黒い画像とフォーカスオブジェクトの二つしか存在しないので除外し、次に来るオブジェクトに対してポインターイベントを送ります。
ExecuteEvents.ExecuteHierarchy
今回こちらの関数が一番重要です。
通常EventSystemが各オブジェクトにポインターイベントを送りますが、自分でイベントを実行したい時などにこちらが使えます。
今回のサンプルコードのtargetにはボタンの中にあるテキストオブジェクトが入ってきます。
通常テキストオブジェクトにのみイベントを通知させても、ボタンのクリックイベントが呼ばれることはありませんが、ExecuteHierarchy関数が親オブジェクトを辿っていい感じにイベントを通知させてくれています。
参考
ExecuteEvents.ExecuteHierarchyの説明は以下の記事で分かりやすく解説されているのでぜひご参照ください。
Destroy・非アクティブ時の処理
ボタンを押した状態で、ポインターをボタンの領域外に持って行っているときに、フォーカスが解除された場合、ボタンが押されたままの状態になってしまうので、このタイミングでPointerUpイベントを呼び出しています。
フォーカスするのに便利なアセット
今回のサンプルでボタンの部分だけマスクをかけるために以下のパッケージを使用させていただきました。
最後に
いかがでしたでしょうか?
もし今回のサンプルが実践でお役に立てたらうれしいです!