効率的なスクロールビューを実装するためのOSS『Loop Scroll Rect』使い方まとめ

効率的なスクロールビューを実装するためのOSS『Loop Scroll Rect』使い方をまとめました。

Unity 2020.3.15f2
Loop Scroll Rect 1.1.0

Loop Scroll Rectとは?

Unity標準のスクロールビューは、要素の数が増えるほど処理負荷とメモリの使用量が大きくなるため、スケール可能にするためには工夫が必要です。
例えば表示範囲外のGameObjectを非アクティブにしたり、オブジェクトプールを使って使い回したりすることが考えられます。

Loop Scroll Rectを使うとこのような効率的なスクロールビューを簡単に実現することができます。
日本ではあまり使われてなさそうな気がしますが、現時点で約1400Starsの人気ライブラリで、更新頻度も高めです。

github.com

このライブラリはMITライセンスのOSSとして公開されています。
インストールするにはPackage Managerから以下のURLを入力します。

シンプルなスクロールビューを実装する

スクロールビューを作成する

さてそれではまずシンプルな縦スクロールビューを実装してみます。
横スクロールビューは割愛しますが、基本的には縦と同じ手順で作成できます。

最初にGameObject > UI > Loop Vertical Scroll Rectからセットアップ済みのGameObjectを生成します。
このGameObjectのルートがスクロールビューの範囲となるので、大きさを適切に設定しておきます。

次その子GameObjectであるContentの設定をします。
Vertical Layout Groupがアタッチされているので、Control Child SizeのHeightにチェックを入れておきます。
その他の設定は任意で、通常のVertical Layout Groupと同様に行えます。

Content

要素のPrefabを作成する

次にその子としてスクロールビューの要素となるGameObjectを作成していきます。
Loop Scroll Rectでは、スクロールビューの高さをこの要素のPreferred Heightを使って計算します。
したがってLayout Elementなどを使ってPreferred Heightを設定します。

Layout Element

Text Mesh ProなどのオブジェクトはLayout ElementがなくてもPreferred Heightが計算されるため、このあたりは必要に応じて設定するようにしてください。

要素が作れたらこれをPrefabにしておきます。
今回は下図のようにImageとTextで構成されるPrefabを作りました。

Prefab

スクリプトを作成する

次にLoopScrollRectを初期化するためのスクリプトを作成します。
以下のようにLoopScrollPrefabSourceLoopScrollDataSourceを実装したオブジェクトをLoopScrollRectアサインします。

using TMPro;
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(LoopScrollRect))]
[DisallowMultipleComponent]
public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollDataSource
{
    [SerializeField] private GameObject _prefab;

    public int totalCount = -1;

    private void Start()
    {
        var scrollRect = GetComponent<LoopScrollRect>();
        scrollRect.prefabSource = this;
        scrollRect.dataSource = this;
        scrollRect.totalCount = totalCount;
        scrollRect.RefillCells();
    }

    // LoopScrollPrefabSourceの実装
    // GameObjectが新しく表示のために必要になった時に呼ばれる
    GameObject LoopScrollPrefabSource.GetObject(int index)
    {
        // 今回は毎回Instantiate(プールしてない)
        return Instantiate(_prefab);
    }

    // LoopScrollPrefabSourceの実装
    // GameObjectが不要になった時に呼ばれる
    void LoopScrollPrefabSource.ReturnObject(Transform trans)
    {
        // 今回は毎回Destroy(プールしてない)
        DestroyImmediate(trans.gameObject);
    }

    // LoopScrollDataSourceの実装
    // 要素が表示される時の処理を書く
    void LoopScrollDataSource.ProvideData(Transform trans, int index)
    {
        trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = index.ToString();
    }
}

LoopScrollPrefabSourceLoopScrollRectからGameObjectが要求・返却された時の処理を定義します。
表示に使わないGameObjectを非アクティブにしたり、オブジェクトプーリングをしたい時にはこの辺りに処理を書きます。

LoopScrollDataSourceには各要素を表示する際のセットアップ処理を定義します。

動作確認

それでは一度動作確認をしてみます。

上記のコンポーネントLoop Scroll Rectコンポーネントと同じGameObjectにアタッチし、Prefabプロパティに先ほど作ったPrefabをアサインします。
TotalCountは100に設定しておきます。

これで再生すると以下のような結果が得られます。

結果

なおついでにスクロールバーを設定していますが、これの設定方法は通常のScrollRectと同様の設定方法なので割愛します。

オブジェクトを使い回す

さてここまでの実装では画面内に入るたびにGameObjectをインスタンス化し、画面外に出るたびに破棄していました。
これは非効率なのでオブジェクトを使い回すことで効率化します。

Unityでは2021からオブジェクトプールが実装されたので、今回はこれを使ってみます。

docs.unity3d.com

実装は以下の通りです。

using TMPro;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;

[RequireComponent(typeof(LoopScrollRect))]
[DisallowMultipleComponent]
public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollDataSource
{
    [SerializeField] private GameObject _prefab;

    public int totalCount = -1;
    private ObjectPool<GameObject> _pool;

    private void Start()
    {
        // オブジェクトプールを作成
        _pool = new ObjectPool<GameObject>(
            // オブジェクト生成処理
            () => Instantiate(_prefab),
            // オブジェクトがプールから取得される時の処理
            o => o.SetActive(true),
            // オブジェクトがプールに戻される時の処理
            o =>
            {
                o.transform.SetParent(transform);
                o.SetActive(false);
            });

        var scrollRect = GetComponent<LoopScrollRect>();
        scrollRect.prefabSource = this;
        scrollRect.dataSource = this;
        scrollRect.totalCount = totalCount;
        scrollRect.RefillCells();
    }

    void LoopScrollDataSource.ProvideData(Transform trans, int index)
    {
        trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = index.ToString();
    }

    GameObject LoopScrollPrefabSource.GetObject(int index)
    {
        // オブジェクトプールからGameObjectを取得
        return _pool.Get();
    }

    void LoopScrollPrefabSource.ReturnObject(Transform trans)
    {
        // オブジェクトプールにGameObjectを返却
        _pool.Release(trans.gameObject);
    }
}

これでオブジェクトが使い回されるようになりました。

無限ループするビューを作る

さてここまでの実装では、スクロールし続けてTotalCountに設定した数が表示された時点でスクロールが止まります。
このときスクロールを止めずに最初の要素にループさせるには、TotalCountに-1を入力します。
またこの場合、LoopScrollDataSource.ProvideDataに渡されるインデックスの値は無限に増えていくため、そこから以下のようにデータのインデックスを求める必要があります(コメント部分)。

using TMPro;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;

[RequireComponent(typeof(LoopScrollRect))]
[DisallowMultipleComponent]
public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollDataSource
{
    [SerializeField] private GameObject _prefab;

    public int totalCount = -1;
    
    // データ数
    public int dataCount = 100;
    
    private ObjectPool<GameObject> _pool;

    private void Start()
    {
        _pool = new ObjectPool<GameObject>(
            () => Instantiate(_prefab),
            o => o.SetActive(true),
            o =>
            {
                o.transform.SetParent(transform);
                o.SetActive(false);
            });

        var scrollRect = GetComponent<LoopScrollRect>();
        scrollRect.prefabSource = this;
        scrollRect.dataSource = this;
        scrollRect.totalCount = totalCount;
        scrollRect.RefillCells();
    }

    void LoopScrollDataSource.ProvideData(Transform trans, int index)
    {
        // データのインデックスを求める
        var dataIndex = (int)Mathf.Repeat(index, dataCount);
        trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = dataIndex.ToString();
    }

    GameObject LoopScrollPrefabSource.GetObject(int index)
    {
        return _pool.Get();
    }

    void LoopScrollPrefabSource.ReturnObject(Transform trans)
    {
        _pool.Release(trans.gameObject);
    }
}

これを実行すると以下の結果が得られます。

結果

要素がループしていることがわかります。

また上図の通り、無限ループを行う際にはスクロールバーが機能しませんのでご注意ください(機能したとしても挙動がわけわからなくなるのでそれはそう)。

要素ごとに高さを変える

要素ごとに高さを変えるには以下のようにLoopScrollDataSource.ProvideDataでPreferred Heightを変更します。

using TMPro;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;

[RequireComponent(typeof(LoopScrollRect))]
[DisallowMultipleComponent]
public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollDataSource
{
    [SerializeField] private GameObject _prefab;

    public int totalCount = -1;
    public int dataCount = 100;
    
    private ObjectPool<GameObject> _pool;

    private void Start()
    {
        _pool = new ObjectPool<GameObject>(
            () => Instantiate(_prefab),
            o => o.SetActive(true),
            o =>
            {
                o.transform.SetParent(transform);
                o.SetActive(false);
            });

        var scrollRect = GetComponent<LoopScrollRect>();
        scrollRect.prefabSource = this;
        scrollRect.dataSource = this;
        scrollRect.totalCount = totalCount;
        scrollRect.RefillCells();
    }

    void LoopScrollDataSource.ProvideData(Transform trans, int index)
    {
        var dataIndex = (int)Mathf.Repeat(index, dataCount);
        
        // 要素ごとに高さを変える
        var height = dataIndex % 2 == 0 ? 50 : 100;
        trans.GetComponent<LayoutElement>().preferredHeight = height;
        
        trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = dataIndex.ToString();
    }

    GameObject LoopScrollPrefabSource.GetObject(int index)
    {
        return _pool.Get();
    }

    void LoopScrollPrefabSource.ReturnObject(Transform trans)
    {
        _pool.Release(trans.gameObject);
    }
}

実行結果は以下のようになります。今回は奇数のインデックスのセルと偶数のインデックスのセルでそれぞれ別の高さを設定しています。

結果

要素ごとにPrefabを変える

次に要素ごとにPrefabを変更してみます。
複数のPrefabを使用する場合、Loop Vertical Scroll Rectコンポーネントの代わりにLoop Vertical Scroll Rect Multiコンポーネントを使用します。
InspectorをDebugモードにしてコンポーネントを差し替えるのが簡単です。

差し替え

スクリプトは以下のように変更します。
LoopScrollDataSourceの代わりにLoopScrollMultiDataSourceを実装します。
その他の変更点はコメントとして記述しました。

using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;

[RequireComponent(typeof(LoopScrollRect))]
[DisallowMultipleComponent]
public sealed class Example : MonoBehaviour, LoopScrollPrefabSource, LoopScrollMultiDataSource // LoopScrollMultiDataSourceに変更
{
    // Prefabを複数指定できるように
    [SerializeField] private GameObject[] _prefabs;

    public int totalCount = -1;
    public int dataCount = 100;
    
    // PoolもPrefabと同じだけ生成
    private ObjectPool<GameObject>[] _pools;
    
    private Dictionary<GameObject, int> _prefabIndexMap = new Dictionary<GameObject, int>();

    private void Start()
    {
        // Prefabごとにプールを生成する
        _pools = new ObjectPool<GameObject>[_prefabs.Length];
        for (var i = 0; i < _prefabs.Length; i++)
        {
            var prefab = _prefabs[i];
            var pool = new ObjectPool<GameObject>(
                () => Instantiate(prefab),
                o => o.SetActive(true),
                o =>
                {
                    o.transform.SetParent(transform);
                    o.SetActive(false);
                });
            _pools[i] = pool;
        }

        // LoopScrollRectMultiに変更
        var scrollRect = GetComponent<LoopScrollRectMulti>();
        scrollRect.prefabSource = this;
        scrollRect.dataSource = this;
        scrollRect.totalCount = totalCount;
        scrollRect.RefillCells();
    }

    // LoopScrollMultiDataSourceを実装
    void LoopScrollMultiDataSource.ProvideData(Transform trans, int index)
    {
        var dataIndex = (int)Mathf.Repeat(index, dataCount);
        trans.GetChild(0).GetComponent<TextMeshProUGUI>().text = dataIndex.ToString();
    }

    GameObject LoopScrollPrefabSource.GetObject(int index)
    {
        // Indexに応じて違うPrefabを使う
        var prefabIndex = index % _prefabs.Length;
        var instance = _pools[prefabIndex].Get();
        _prefabIndexMap.Add(instance, prefabIndex);
        return instance;
    }

    void LoopScrollPrefabSource.ReturnObject(Transform trans)
    {
        // Indexに応じたプールを取得して返却
        var instance = trans.gameObject;
        var prefabIndex = _prefabIndexMap[instance];
        _prefabIndexMap.Remove(instance);
        _pools[prefabIndex].Release(instance);
    }
}

実行結果は下図の通りとなります。
今回は3種類のPrefabを設定しています。

結果

グリッドスクロールビューを実装する

最後に要素をグリッドで並べたスクロールビューを実装します。
グリッドスクロールビューを使うためにはVertical Layout Groupの代わりにGrid Layout Groupを使用します。
大半のプロパティは任意ですが、ConstraintをFlexibleにすることはできません。

Grid Layout Group

またグリッドスクロールビューの場合、セルの高さはPreferred HeightではなくGrid Layout GroupCell Sizeから取得されます。
したがって各要素にLayout Elementをアタッチする必要はありません。

グリッドスクロールビューの実行結果は以下の通りとなります。

またグリッドスクロールビューの場合、セルの高さはPreferred HeightではなくGrid Layout GroupのCell Sizeから取得されます。
したがって各要素にLayout Elementをアタッチする必要はありません。 グリッドスクロールビューの実行結果は以下の通りとなります。

結果

参考

docs.unity3d.com