[Unity]uGUIでRectTransfromが確定したときに何かしたい…みたいな話

ざっくりまとめ。

  • uGUI で複雑(込み入った)な UI を組んだときにサイズとか確定するの遅い
    • Awake() 内ではもちろん未確定
    • Start() でも未だ未確定(単純なものならここで決まっていることもある)
  • OnRectTransformDimensionsChange を使うと良い
    • 読んで字のごとく。。変更されるたびに発火します

です。

Advent Calendar の季節ですし書きます(笑)

環境

  • Unity v2018.2.17

やりたいこと

uGUI で UI 組んでて... ロジック側でその位置座標やサイズなどを使いながらなにかしたいときってありませんか?
width の何割かでなにかをするとか...。座標をみながら、何かを調整するとか...。

問題

ところが、複雑な UI を組んでいると、RectTransfrom が確定されるのが遅いのです。
Vertical Layout Group とか Horizontal Layout Group とか使っていると...

たとえば下記のような構成

ugui_tree

Panel に Vertical Layout Group コンポーネントがはってあって
Row に Horizontal Layout Group コンポーネントがはってあります。

これの Left の RectTransfrom が確定されるのはいつでしょうか..?
ということでこんなの書いてみました

public class LeftPanel
{
    [SerializeField] private RectTransfrom rectTransform;
    
    void Awake()
    {
        Debug.Log("Awake:" + rectTransform.rect.ToString());
    }

    void Start()
    {
        Debug.Log("Start:" + rectTransform.rect.ToString());
        Invoke("SomethingMethod", 0.1f);
    }
    
    void SomethingMethod()
    {
        Debug.Log("SomethingMethod:" + rectTransform.rect);
    }
}

実行してみると...

consolelog

Start() の時点でも決まっていません!
(まぁ Invoke の記述があったら気づくでしょうけど(笑))

0.1f だけ遅延させてやっているけど、まぁ1フレーム後でも良いかもしれないし、もしかしたら 0.2f 必要かもしれない...よくわからん 🤔
みたいなのが正直なところ...

0.1f 後にやるというのを我慢したとしても、ソレ以降に何かしらの要因でまた RectTransfrom が変わったときには対応できません... 😢

OnRectTransformDimensionsChange というのがあります

RectTransform が変わるたびに発火します!
ってことでこれを使えばやりたいことが実現できそう。
が、MonoBehaviour ではなく UIBehaviour なので、OnCollisionEnter() のようにすぐには使えません。

ってことでサブクラスをつくってみた

OnRectTransformDimensionsChange を使うためにサブラスを作ってみました。

using Handler = System.Action;

public class DimensionsChangedNotification: UnityEngine.EventSystems.UIBehaviour, IHandler
{
    private Handler handlers;

    override protected void OnRectTransformDimensionsChange()
    {
        if (handlers == null) {
            return;
        }
        handlers.Invoke();
    }

    #region "IHandler"
    public void AddHandler(Handler handler)
    {
        handlers += handler;
    }

    public void RemoveHander(Handler handler)
    {
        handler -= handler;
    }
    #endregion
}

public interface IHandler
{
    void AddHandler(Handler handler);
    void RemoveHander(Handler handler);
}

こういう感じのものを。
で、件の Left のオブジェクトにこの DimensionsChangedNotification を加えてはっつけておくと
それで、 LeftPanel は以下の様に変更

public class LeftPanel
{
    void Awake()
    {
        GetComponent<IHandler>().AddHandler(OnDimensionsChanged);
    }

    void OnDestroy()
    {
        GetComponent<IHandler>().RemoveHander(OnDimensionsChanged);
    }
    
    void OnDimensionsChanged()
    {
        Debug.Log("Changed:" + rectTransform.rect);
    }
}

これで、実行してみると...

consolelog2

ちゃんと確定されるまで自動で呼び続けられます。
(4ステップもあるんだ(笑))

これで、柔軟な UI 組んでても大丈夫ですね! 💪

ソレ、UniRx でできるよ?

なのです 😇

void Awake()
{
    var trigger = AddComponent<ObservableRectTransformTrigger>();
    trigger.OnRectTransformDimensionsChangeAsObservable().Subscribe(
        onNext: (_) => {
            // Changed!!
        }
    ).AddTo(gameObject);
}

最高かよ!

補足

Unity の PlayerSettings で Scripting Runtime Version.NET 4.x Equivalent を有効にしておくと
DimensionsChangedNotification にある

if (handlers == null) {
    return;
}
handlers.Invoke();

という記述を

handlers?.Invoke();

と書き換えられます!
ほかにも 4.x にしておくといろいろ便利なものが使えるようになるので、環境的に許されるなら変えておくことをおすすめ!

参考

C#6.0時代のUnity - Qiita