うにてぃブログ

主にUnityとC#に関する記事を書いていきます

【Unity】Unmask の部分をクリック可能にする

hacchi-man.hatenablog.com

以前 Unmask の記事を書きましたが、Unmask したからには
その部分のクリックをしたいと思ったのでクリック処理を追加してみました

ICanvasRaycastFilter を使う方法もありますが、Mask内にボタンを設置する必要があったので
利用しづらいと考え別の方法で実装しました

動画でも画像でもわからない感じだったので Unmask している画像だけ貼っておきます

f:id:hacchi_man:20201215212743p:plain

仕組み

IPointerClickHandler を追加して、Unmask 部分をクリック可能にする

public class UnMask : Mask, IPointerClickHandler
  
void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
{

EventSystem.current.RaycastAll より クリックした場所から再度 Ray を飛ばして
Raycast が ヒットするオブジェクト一覧を取得する

この際、当たる順番に取得できるので、先頭から順番に Mask じゃないところまでループさせる

var hits = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, hits);
if (hits.Count <= 0)
    return;
 
GameObject hitObj = null;
foreach (var hit in hits)
{
    if (HasInParent(hit.gameObject.transform, _rootMask.transform))
        continue;
 
    hitObj = FindParentIEventSystemObject(hit.gameObject);
    break;
}

ヒットした場合そのオブジェクトが Unmask が無い場合の対象になるので
そのオブジェクトに対して、クリック処理を無理やり実行する

これにより擬似的にクリックしたことにできる

もっとクリック感を出したい場合は、以下の Interface を追加して正しく伝播すればできると思います

ExecuteEvents.Execute<IPointerEnterHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerEnter((PointerEventData)ev));
ExecuteEvents.Execute<IPointerDownHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerDown((PointerEventData)ev));
ExecuteEvents.Execute<IPointerClickHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerClick((PointerEventData)ev));
ExecuteEvents.Execute<IPointerUpHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerUp((PointerEventData)ev));
ExecuteEvents.Execute<IPointerExitHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerExit((PointerEventData)ev));

配置

オブジェクトの配置や Raycast に関しては適当でもできるようにしたかったのですが、正しく設定しないと動作しない

Canvas<br>
├ Button (Button)
└ Mask (Mask, Image, Raycast: true)
    ├ Unmask (Unmask, Image, Raycast: true)
    └ FadeImage (Image, Raycast: false)

コード

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Rendering;
using UnityEngine.UI;
 
public class UnMask : Mask, IPointerClickHandler
{
    [NonSerialized]
    private Material _maskMaterial;
 
    [SerializeField]
    private Mask _rootMask;
 
    protected override void OnDisable()
    {
        if (graphic != null)
        {
            graphic.SetMaterialDirty();
            if (graphic is MaskableGraphic)
                ((MaskableGraphic) graphic).isMaskingGraphic = false;
        }
 
        StencilMaterial.Remove(_maskMaterial);
        _maskMaterial = null;

         MaskUtilities.NotifyStencilStateChanged(this);
    }
 
    public override Material GetModifiedMaterial(Material baseMaterial)
    {
        if (!MaskEnabled())
            return baseMaterial;
 
        var rootSortCanvas = MaskUtilities.FindRootSortOverrideCanvas(transform);
        var stencilDepth = MaskUtilities.GetStencilDepth(transform, rootSortCanvas);
        if (stencilDepth >= 8)
        {
            Debug.LogWarning("Attempting to use a stencil mask with depth > 8", gameObject);
            return baseMaterial;
        }
 
        var desiredStencilBit = 1 << stencilDepth;
 
        var maskMaterial = StencilMaterial.Add(
            baseMaterial,
            desiredStencilBit - 1,
            StencilOp.Zero,
            CompareFunction.Always,
            showMaskGraphic ? ColorWriteMask.All : 0, desiredStencilBit - 1,
            desiredStencilBit | (desiredStencilBit - 1)
        );
 
        StencilMaterial.Remove(_maskMaterial);
        _maskMaterial = maskMaterial;

        return _maskMaterial;
    }
 
    void IPointerClickHandler.OnPointerClick(PointerEventData eventData)
    {
        var hits = new List<RaycastResult>();
        EventSystem.current.RaycastAll(eventData, hits);
        if (hits.Count <= 0)
            return;

        GameObject hitObj = null;
        foreach (var hit in hits)
        {
            if (HasInParent(hit.gameObject.transform, _rootMask.transform))
                continue;

            hitObj = FindParentIEventSystemObject(hit.gameObject);
            break;
        }
 
        if (hitObj == null)
            return;
 
        ExecuteEvents.Execute<IPointerEnterHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerEnter((PointerEventData)ev));
        ExecuteEvents.Execute<IPointerDownHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerDown((PointerEventData)ev));
        ExecuteEvents.Execute<IPointerClickHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerClick((PointerEventData)ev));
        ExecuteEvents.Execute<IPointerUpHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerUp((PointerEventData)ev));
        ExecuteEvents.Execute<IPointerExitHandler>(hitObj, eventData, (handler, ev) => handler.OnPointerExit((PointerEventData)ev));
    }
 
    private static bool HasInParent(Transform transform, Transform target)
    {
        var current = transform;
        while (current.parent != null)
        {
            if (current == target)
                return true;
            current = current.parent;
        }
        return false;
    }
 
    private GameObject FindParentIEventSystemObject(GameObject gameObject)
    {
        var current = gameObject.transform;
        while (current != null)
        {
            var target = current.gameObject.GetComponent<IEventSystemHandler>();
            if (target != null)
                return current.gameObject;

            current = current.parent;
        }
        return null;
    }
}
 
[CustomEditor(typeof(UnMask))]
public class UnmaskEditor : Editor
{
    private SerializedProperty _script;
    private SerializedProperty _rootMask;
 
    private void OnEnable()
    {
        _script = serializedObject.FindProperty("m_Script");
        _rootMask = serializedObject.FindProperty("_rootMask");
    }
 
    public override void OnInspectorGUI()
    {
        serializedObject.UpdateIfRequiredOrScript();
        using (new EditorGUI.DisabledScope(true))
        {
            EditorGUILayout.PropertyField(_script);
        }
 
        EditorGUILayout.PropertyField(_rootMask);

        serializedObject.ApplyModifiedProperties();
    }
}