【Unity】簡単!軽い!自作LayoutGroup

AvatarPosted by

はじめに

この記事はGRIPHONE Advent Calendar 2021 8日目の記事です。

こんにちは!クライアントの鈴木です。

私の携わっているプロダクトではインゲームでのゲーム体験を重視するため、UIの処理コストを削減する必要がありました。そのため、VerticalLayoutGroupやHorizontalLayoutGroupなどの自動でUIを整列するAuto Layoutのコンポーネントを極力使用しないようにしています。しかし、一部UIは動的に整列させる必要があり、今回自分でLayoutGroupを作成することになりました。

UnityのAuto Layoutについて

Auto LayoutによるGetComponentとLayoutのリビルド処理の詳細については下記サイトが大変参考になります。

https://logmi.jp/tech/articles/320743

こういった問題があるため、Unityの公式ではできるだけAuto Layoutを使わない方がパフォーマンスに良く、必要な場合は自作した方が良いことが明言されています。

今回は以下二点を意識して実装しました。

  • GetComponentを使用せずに並べる対象を設定すること
  • UIを整列するタイミングはこちらで制御すること

この方針で、スクリプトから整列する対象のUIと整列するタイミングを制御できるようにしました。

作成したもの

以下作成したものの動画です。

中央揃えで整列させる動画
アンカーの設定

作成したコード全体です。

using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(CustomHorizontalLayoutGroup))]
public class CustomHorizontalLayoutGroupEditor : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();
            
        if (!(target is CustomHorizontalLayoutGroup layoutGroup))
        {
            return;
        }
            
        if (GUILayout.Button("下のやつを一気に登録"))
        {
            var targetList = new List<RectTransform>();
            for (var i = 0; i < layoutGroup.transform.childCount; i++)
            {
                var child = layoutGroup.transform.GetChild(i);
                targetList.Add(child.transform as RectTransform);
            }

            layoutGroup.SetLayoutTarget(targetList);
            EditorUtility.SetDirty(target);
        }
            
        if (GUILayout.Button("整列させる"))
        {
            layoutGroup.SetLayoutTarget(layoutGroup.TargetList).Align();
            EditorUtility.SetDirty(target);
        }
    }
}

public class CustomHorizontalLayoutGroup : MonoBehaviour
{
    /// <summary>
    /// 整列させる対象を設定
    /// </summary>
    public CustomHorizontalLayoutGroup SetLayoutTarget(List<RectTransform> targetList)
    {
        _targetList = targetList;
        return this;
    }

    /// <summary>
    /// マージンを設定
    /// </summary>
    public CustomHorizontalLayoutGroup SetMargin(float margin)
    {
        _margin = margin;
        return this;
    }

    /// <summary>
    /// 整列させる
    /// </summary>
    public void Align()
    {
        AlignInternal(_targetList);
    }

    /// <summary>
    /// 整列させる処理の本体
    /// </summary>
    private void AlignInternal(List<RectTransform> targetList)
    {
        var currentX = 0f;
        var totalWidth = 0f;
        for (var index = 0; index < targetList.Count; index++)
        {
            var rectTransform = targetList[index];
            if (!rectTransform.gameObject.activeSelf)
            {
                continue;
            }

            // 整列させる
            rectTransform.anchorMax = _reverse
                ? new Vector2(1f, rectTransform.anchorMax.y)
                : new Vector2(0f, rectTransform.anchorMax.y);
            rectTransform.anchorMin = _reverse
                ? new Vector2(1f, rectTransform.anchorMin.y)
                : new Vector2(0f, rectTransform.anchorMin.y);
            rectTransform.pivot = _reverse
                ? new Vector2(1f, rectTransform.pivot.y)
                : new Vector2(0f, rectTransform.pivot.y);
            rectTransform.anchoredPosition = new Vector2(currentX, rectTransform.anchoredPosition.y);

            var width = rectTransform.rect.width;
            currentX = _reverse ? currentX - width - _margin : currentX + width + _margin;

            // 末尾の場合は幅の合計にマージンを含めない
            if (index == targetList.Count - 1)
            {
                totalWidth += width;
            }
            else
            {
                totalWidth += width + _margin;
            }
        }

        // 中央揃えなどの並べ方を簡単にするためにアタッチされているオブジェクトのサイズを変える
        SetWidth(RectTransform, totalWidth);
    }

    /// <summary>
    /// サイズをセットする
    /// </summary>
    private void SetSize(RectTransform trans, Vector2 newSize)
    {
        Vector2 oldSize = trans.rect.size;
        Vector2 deltaSize = newSize - oldSize;
        trans.offsetMin = trans.offsetMin - new Vector2(deltaSize.x * trans.pivot.x, deltaSize.y * trans.pivot.y);
        trans.offsetMax = trans.offsetMax +
                          new Vector2(deltaSize.x * (1f - trans.pivot.x), deltaSize.y * (1f - trans.pivot.y));
    }

    /// <summary>
    /// 幅をセットする
    /// </summary>
    private void SetWidth(RectTransform trans, float newSize)
    {
        SetSize(trans, new Vector2(newSize, trans.rect.size.y));
    }

    /// <summary>
    /// レイアウト対象
    /// </summary>
    public List<RectTransform> TargetList => _targetList;

    /// <summary>
    /// このオブジェクトのレクトトランスフォーム
    /// </summary>
    private RectTransform RectTransform =>
        _rectTransform != null ? _rectTransform : _rectTransform = transform as RectTransform;

    private RectTransform _rectTransform;

    [SerializeField, Header("レイアウト対象")] private List<RectTransform> _targetList = new List<RectTransform>();

    [SerializeField, Header("マージン")] private float _margin;

    [SerializeField, Header("逆方向から並べるか")] private bool _reverse;
}

作成したコードはかなり簡単なものですが、単純な要件であればこれで十分かと思います。親のサイズをコンテンツに合わせて変えることでレイアウトを中央揃えにすることも容易にできます。

Layoutのリビルド処理の様子も確認してみる

一応Layoutのリビルド処理をプロファイラーで確認してみます。UnityのHorizontalLayoutGroupで200個のUIを並べてからある一つの要素のRectTransformをいじってみると以下のようなスパイクを確認できました。

UnityのHorizontalLayoutGroup計測
UnityのHorizontalLayoutGroup計測結果(拡大画像)

画像の”Layout”と表示されているのがLayoutのリビルドの処理です。8~9msほど時間がかかっていて、それがPlayerLoopの大半を占めているようです。試しに今回作成した自作Layoutでも試してみました。

※今回は計測のためUpdateでUIを整列する処理(Alignメソッド)を呼んでいます。

自作したHorizontalLayoutGroup計測
自作したHorizontalLayoutGroup計測結果(拡大画像)

PlayerLoopが1~2msほどになり、Layoutのリビルドの処理が軽減されているようです!

終わりに

今回は自作のLayoutを作ってみました。計測してみると、UnityのHorizontalLayoutGroupはPlayerLoopが8~9msほどかかっていましたが、自作のHorizontalLayoutGroupは1~2msほどに抑えられていました。ぬるぬる動くUIで快適なゲーム体験が作れるようになりたいですね!ご高覧ありがとうございました。