【Unity】スクロールビューに配置された要素の上でマウスホイールによるスクロールができない問題を解決する

概要

Unityにデフォルトで用意されているGUIのScroll Viewについて、これは名前の通り限られた表示領域に多くのオブジェクトを配置できるスクロール可能なレイアウト用オブジェクトですが、 これはデフォルトの状態だとマウスホイールでスクロールすることが可能です。(正確にはScrollRectコンポーネントのMovement TypeがElasticの場合にこうなるみたいです)
で、これを使っていてタイトルの件に遭遇したので原因と解決策のメモ書きです。
文字だけだとよくわからないので具体的には以下のgifを見てもらうとわかるかと思います。

f:id:Kanchi0914:20220305144613g:plain

わかりやすいようにScrollRectのScroll Sensibilityの値を上げてスクロール量を増やしていますが、Scroll View内に配置されているボタンの上にマウスカーソルがあるとき、ホイールによるスクロールが効いていないのが見て取れると思います。

原因と対応

これの原因なんですが、Scroll Viewに(正確にはScroll View内のContent Areaに)配置されている子オブジェクトに以下のコンポーネントがアタッチされているとき、そのオブジェクトがマウスのraycastをブロックすることにより発生するようです。

  1. EventTrigger
  2. ImageなどのRaycast Targetになるコンポーネント

色々試したんですが対応としては、スクロール中は上記いずれかを無効にするしかないみたいです。他にいい解決策があるような気はしていますが‥‥

using UnityEngine;
using UnityEngine.EventSystems;

public class ScrollableImage : MonoBehaviour
{
    [SerializeField] private int _threshold = 20;

    [SerializeField] private bool _enabled = true;

    private int _frameCount;
    private EventTrigger _trigger;

    private void Start()
    {
        _trigger = gameObject.GetComponent<EventTrigger>();
    }

    private void Update()
    {
        if (!_enabled) return;
        if (Input.GetAxis("Mouse ScrollWheel") != 0)
        {
            _frameCount = 0;
            _trigger.enabled = false;
        }

        if (_frameCount >= _threshold)
        {
            _trigger.enabled = true;
        } else {
            _frameCount++;
        }
    }
}

上記スクリプトをScroll Viewの子要素にアタッチするとホイールによるスクロールが効くようになります。
マウスホイールの入力自体は Input.GetAxis("Mouse ScrollWheel") で取得できるため、この入力があったときは一時的にEventTriggerを無効にしています。
_threshold という変数を用いている理由については、マウスホイールの入力は連続的ではないため、ここでは20フレームとしていますがある程度の閾値を挟まないといい感じの挙動にならなかったためです。

EventTiggerではなく、オブジェクトのRaycast Targetを無効にすることでも対応できます。
ボタンなどではボタン自体のImageだけでなく子要素のTextもRaycast Targetとなるため、自身を含む子要素すべてを無効にする必要があります。 違いがあるのかはわかりませんが、特に理由がなければ上記のEvent Triggerを無効にするほうで問題ないかと思います。

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class ScrollableImage : MonoBehaviour
{
    [SerializeField] private int _threshold = 20;

    [SerializeField] private bool _enabled = true;

    private int _frameCount;
    private List<Graphic> _imageComponents;

    private void Start()
    {
        _imageComponents = new List<Graphic> {gameObject.GetComponent<Graphic>()};
        _imageComponents = _imageComponents.Concat(gameObject.GetComponentsInChildren<Graphic>())
            .Where(i => i != null).ToList();
    }

    private void Update()
    {
        if (_imageComponents.Count == 0 || !_enabled) return;
        if (Input.GetAxis("Mouse ScrollWheel") != 0)
        {
            _frameCount = 0;
            foreach (Graphic graphic in _imageComponents)
            {
                graphic.raycastTarget = false;
            }
        }

        if (_frameCount >= _threshold)
        {
            foreach (Graphic graphic in _imageComponents)
            {
                graphic.raycastTarget = true;
            }
        } else {
               _frameCount++;
        }
    }
}

ちなみにマウスホイールの入力検知は EventTriggerType.Scroll というのがあるのでそちらでもできますが、特にホイール入力が終わったときにどうこうできるものでもないためいずれにせよ工夫が必要みたいです。

参考

Unity UI - Scroll View does not scroll with MouseWheel when mouse is over a button inside the scroll view? - Unity Forum

child objects blocking scrollrect from scrolling - Unity Forum

IDragHandler on child blocks Scrollrect scrolling - Unity Forum