ぐーるらいふ

底辺。

【Unity】uGUI Scroll List で Recycle したい (初心者向け)【再利用】

お疲れ様です。ぐーるです。
また一ヶ月ぐらい空いてしまいました。

「あ、そういえば一個思いついた」
と下書きにメモだけしてて、保存して
放置してた記事があったのを二重に思い出しました。

今回の単語は

unity UGUI Scroll List Recycle

です。

marginとか追記するの忘れた…時間見つけて追記しとこう。自分用のメモ。
あと最上段で引っ張ると判定に入ってIndexOutOfException吐いちゃうかもいかんいかん

  • > 追記しました。(2019/07/30)


サンプルプロジェクトを用意しましたので、
手っ取り早く見たい方はこちらを!

github.com

unityでスクロールリストをまず作ってみよう

よくある縦リストをunityで作る場合はこんな手順になります。

  1. ScrollRectを作る
  2. ViewPort(Mask)をよしなに作る
  3. ScrollBarを作る
  4. VerticalLayoutGroupとContentSizeFitterを作る
  5. LayoutElementを持ったprefabなどを複数追加するとリスト表示出来る

(ScrollViewをベースに作るのがおすすめです)

f:id:ghoul_life:20190729234203p:plain

要素を使いまわそう

まず、要素を使い回すには、以下のものを消します。

  • VeritcalLayoutGroup
  • ContentSizeFitter

えっ?て思うかも知れませんが、ここを自作する形になります。
大丈夫、難しくないです。

f:id:ghoul_life:20190730003734g:plain

ソースコードをまるっと

以下のソースを作成します。

/// <summary>
/// 要素を再利用するレイアウトグループ(Vertical)
/// </summary>
public class RecycleLayoutGroup : MonoBehaviour
{
    
    [SerializeField] RectTransform _itemElementPrefab; // リスト内の1要素
    [SerializeField] int _instantateItemCount = 9; // 何個作っておくか
    [SerializeField] float _margin = 10; // 隙間

    private int SAMPLE_ITEM_COUNT = 100; // サンプルで作成する数

    // 生成したアイテムはLinkedListで管理する
    public LinkedList<RectTransform> itemList = new LinkedList<RectTransform>();

    // cacheして処理の高速化を図る
    private RectTransform _rectTransform;
    private float _itemSizeheight = -1;

    // field
    protected float _diffPreFramePositionY = 0; // 一つ前の位置
    protected int currentItemNo = 0; // 現在の一番上の位置

    void Start()
    {
        _itemElementPrefab.gameObject.SetActive(false);

        for (int i = 0; i < _instantateItemCount; i++)
        {
            var item = GameObject.Instantiate(_itemElementPrefab) as RectTransform;
            item.SetParent(transform, false);
            item.name = "Recycle Item " + i.ToString();
            item.anchoredPosition = new Vector2(0, -ItemHeight * i);
            itemList.AddLast(item);

            item.gameObject.SetActive(true);

            OnUpdateItem(i, item.gameObject);
        }


        // 先に全アイテム分だけscrollRectを広げておくのだ
        var delta = rectTransform.sizeDelta;
        delta.y = ItemHeight * SAMPLE_ITEM_COUNT;
        rectTransform.sizeDelta = delta;
    }

    void Update()
    {
        if (itemList.First == null)
        {
            return;
        }

        // 下
        while (anchoredPositionY - _diffPreFramePositionY < -ItemHeight * 2)
        {
            _diffPreFramePositionY -= ItemHeight;

            var item = itemList.First.Value;
            itemList.RemoveFirst();
            itemList.AddLast(item);

            var pos = ItemHeight * _instantateItemCount + ItemHeight * currentItemNo;
            item.anchoredPosition = new Vector2(0, -pos);

            OnUpdateItem(currentItemNo + _instantateItemCount, item.gameObject);

            currentItemNo++;
        }

        // 上
        while (anchoredPositionY - _diffPreFramePositionY > 0)
        {
            _diffPreFramePositionY += ItemHeight;

            if(currentItemNo > 0)
            {
                var item = itemList.Last.Value;
                itemList.RemoveLast();
                itemList.AddFirst(item);

                currentItemNo--;

                var pos = ItemHeight * currentItemNo;
                item.anchoredPosition = new Vector2(0, -pos);
                OnUpdateItem(currentItemNo, item.gameObject);
            }   
        }
    }

    private void OnUpdateItem(int itemIndex, GameObject gameObject)
    {
        if (itemIndex < 0 || itemIndex >= SAMPLE_ITEM_COUNT)
        {
            gameObject.SetActive(false);
        }
        else
        {
            gameObject.SetActive(true);

            var listElement = gameObject.GetComponentInChildren<ItemElement>();
            listElement.SetMessage("message == " + itemIndex);
        }
    }


    //-----------------------------------------------------------------------
    // getter

    private float anchoredPositionY
    {
        get
        {
            return -rectTransform.anchoredPosition.y;
        }
    }


    public float ItemHeight
    {
        get
        {
            if (_itemElementPrefab != null && _itemSizeheight == -1)
            {
                _itemSizeheight = _itemElementPrefab.sizeDelta.y + _margin;
            }
            return _itemSizeheight;
        }
    }

    protected RectTransform rectTransform
    {
        get
        {
            if (_rectTransform == null) _rectTransform = GetComponent<RectTransform>();
            return _rectTransform;
        }
    }
}

これをVerticalLayoutGroupの代わりに使えばOKです。

f:id:ghoul_life:20190730004034p:plain

終わり!なんて言うとブーイングの嵐なので、ちょっと解説をば。

Start

    void Start()
    {
        _itemElementPrefab.gameObject.SetActive(false);

        for (int i = 0; i < _instantateItemCount; i++)
        {
            var item = GameObject.Instantiate(_itemElementPrefab) as RectTransform;
            item.SetParent(transform, false);
            item.name = "Recycle Item " + i.ToString();
            item.anchoredPosition = new Vector2(0, -ItemHeight * i);
            itemList.AddLast(item);

            item.gameObject.SetActive(true);

            OnUpdateItem(i, item.gameObject);
        }


        // 先に全アイテム分だけscrollRectを広げておくのだ
        var delta = rectTransform.sizeDelta;
        delta.y = ItemHeight * SAMPLE_ITEM_COUNT;
        rectTransform.sizeDelta = delta;
    }

ここでは、使い回す前提で指定個数しかprefabをinstantiateしないでおきます。
で、その際のanchorPositionを各アイテムの高さをずらして配置します。
今までであれば、VerticalLayoutGroupがやってくれていたことを自作するイメージです。

指定個数のprefabを作り終わったら、
このリスト全体の大きさを表示したい全ての項目分広げておきます。
(ここがポイントです。)

Update

スクロール処理はScrollRectがまかないます。
ScrollRectがcontentに指定しているRectTransformを動かします。
このupdateで動いた値をチェックして、逐一情報を更新します。
下に動かすか、上に動かすかで、リストの付与先(First or Last)が違うのに注意してください。

   void Update()
    {
        if (itemList.First == null)
        {
            return;
        }

        // 下
        while (anchoredPositionY - _diffPreFramePositionY < -ItemHeight * 2)
        {
            _diffPreFramePositionY -= ItemHeight;

            var item = itemList.First.Value;
            itemList.RemoveFirst();
            itemList.AddLast(item);

            var pos = ItemHeight * _instantateItemCount + ItemHeight * currentItemNo;
            item.anchoredPosition = new Vector2(0, -pos);

            OnUpdateItem(currentItemNo + _instantateItemCount, item.gameObject);

            currentItemNo++;
        }

        // 上
        while (anchoredPositionY - _diffPreFramePositionY > 0)
        {
            _diffPreFramePositionY += ItemHeight;

            if(currentItemNo > 0)
            {
                var item = itemList.Last.Value;
                itemList.RemoveLast();
                itemList.AddFirst(item);

                currentItemNo--;

                var pos = ItemHeight * currentItemNo;
                item.anchoredPosition = new Vector2(0, -pos);
                OnUpdateItem(currentItemNo, item.gameObject);
            }   
        }
    }

OnUpdateItem

ここでカラムの更新を行います。
最初に作った指定個数より少ない場合に注意。

    private void OnUpdateItem(int itemIndex, GameObject gameObject)
    {
        if (itemIndex < 0 || itemIndex >= SAMPLE_ITEM_COUNT)
        {
            gameObject.SetActive(false);
        }
        else
        {
            gameObject.SetActive(true);

            var listElement = gameObject.GetComponentInChildren<ItemElement>();
            listElement.SetMessage("message == " + itemIndex);
        }
    }

結果

簡単に要素を使い回すスクロール処理を作ることが出来ました。
今回の例では縦でしたが、xにすればもちろん横でも出来ます。

簡単なのに効果大です。
是非使ってみて下さい。

注意

「リストに入れるprefabの種類がいくつかあるんだけど…?」
こうなってくるとリサイクルがちょっと複雑になりそうです。
あくまでここでは単一のリストの例ってことで。

お疲れ様でした。
次はまた当分先になりそう!
サークル活動、ゲーム作りの方も頑張ります。