もうUI作るときにInspectorでTextとかButtonをペタペタ登録する作業をやめたい

追記:公開しました → https://blog.kyubuns.dev/entry/2020/09/03/212257


f:id:kyubuns:20200830181950p:plain

Unity上でUIを作るとき、こんな感じでSerializeFieldにButtonやらTextやらをペタペタ登録するじゃないですか。
そもそもめっちゃ面倒だし、更新したときに参照張り替え忘れるし、やめたい!!!!

そこで、こんなものを考えてみました。

f:id:kyubuns:20200830182147j:plain

root.Get<Text>("HogeButton/Text").text = "Hoge";
root.Get<Text>("FugaButton/Text").text = "Fuga";
root.Get<Text>("PiyoButton/Text").text = "Piyo";

Rootのオブジェクトから、 root.Get<Text>("HogeButton/Text") で参照が取れます。
UIの構造が変わってHogeButtonの場所が変わっても、そのRoot以下でHogeButtonという名前がユニークならコードはこのままで動きます。


同じ構造のもの(例ではボタン)が3つあるときは、こんな風に同じ用に扱ったりも出来る。

public void Start()
{
    var hogeButton = root.GetChild("HogeButton");
    var fugaButton = root.GetChild("FugaButton");
    var piyoButton = root.GetChild("PiyoButton");

    SetButtonText(hogeButton, "Hoge");
    SetButtonText(fugaButton, "Fuga");
    SetButtonText(piyoButton, "Piyo");
}

private void SetButtonText(IMapper button, string labelText)
{
    button.Get<Text>("Text").text = labelText;
}

そもそも構造が同じなら、1つだけ作っておいてコードから簡単に増やして同じ用に扱える。
(綺麗に並んでるのは親オブジェクトにLayoutGroupがついてるから。)

var hogeButton = root.GetChild("HogeButton");
hogeButton.Get<Text>("Text").text = "Hoge";

var fugaButton = hogeButton.Duplicate();
fugaButton.Get<Text>("Text").text = "Fuga";

var piyoButton = hogeButton.Duplicate();
piyoButton.Get<Text>("Text").text = "Piyo";

f:id:kyubuns:20200830182615j:plain


Layouterという仕組みを使えば、上から下、左から右に同じアイテムを並べることも簡単。
UIって同じものいくつか並べるって作業めっちゃ多いんですよね。

var original = root.GetChild("HogeButton");
using (var layouter = Layouter.TopToBottom(original))
{
    foreach (var button in new[] { "Hoge", "Fuga", "Piyo" })
    {
        var newButton = layouter.Create();
        newButton.Get<Text>("Text").text = button;
    }
}

UIを文字列で参照引っ張ってくるなんて狂気の沙汰じゃねぇ!っていう自分みたいな人向けに、コード生成機能もついてます。
さらに、UIを更新して参照するべきオブジェクトが無くなっていた!というケースのチェックが自動で出来るようにもする予定。
これがないと自分の精神が持たないのでv1.0って言うときには必ず入ります。

完全に自動生成にすればランタイムの速度ももっと上がるんですが、
UIをAssetBundleにしたときにScriptの更新が必須になるとめっちゃ不便なので文字列参照としています。

public class Sample : MonoBehaviour
{
    [SerializeField] private UICache root = default;

    public void Start()
    {
        var ui = new UIElements(root);
        ui.HogeButtonText.text = "Hoge";
        ui.FugaButtonText.text = "Hoge";
    }
}

// ↓ ここから自動生成
public class UIElements
{
    public GameObject Root { get; }
    public Button HogeButton { get; }
    public Text HogeButtonText { get; }
    public Button FugaButton { get; }
    public Text FugaButtonText { get; }

    public UIElements(IMapper mapper)
    {
        Root = mapper.Get();
        HogeButton = mapper.Get<Button>("HogeButton");
        HogeButtonText = mapper.Get<Text>("HogeButton/Text");
        FugaButton = mapper.Get<Button>("FugaButton");
        FugaButtonText = mapper.Get<Text>("FugaButton/Text");
    }
}

型を保持したままUI要素をDuplicateすることも出来ます。
Initializeメソッドを書かないといけないのがダサいけどまあ仕方ないかな・・・

public void Start()
{
    var ui = new UIElements(root);

    using (var editor = Layouter.TopToBottom(ui.HogeButton))
    {
        foreach (var buttonLabel in new[] { "Hoge", "Fuga", "Piyo" })
        {
            var newObject = editor.Create();
            newObject.Text.text = buttonLabel;
        }
    }
}

public class UIElements : IMappedObject
{
    public IMapper Mapper { get; private set; }
    public ButtonElements HogeButton { get; private set; }

    public UIElements(IMapper mapper)
    {
        Initialize(mapper);
    }

    public void Initialize(IMapper mapper)
    {
        Mapper = mapper;
        HogeButton = mapper.GetChild<ButtonElements>("HogeButton");
    }
}

public class ButtonElements : IMappedObject
{
    public IMapper Mapper { get; private set; }
    public Text Text { get; private set; }

    public void Initialize(IMapper mapper)
    {
        Mapper = mapper;
        Text = mapper.Get<Text>("Text");
    }
}

ローカライズ対応もこんな感じ。
パスとテキストをマスターデータに出してしまえば、後からマスターだけでUIのテキスト変更が可能。

root.Localize(new Dictionary<string, string>
{
    { "./Text", "Title" },
    { "HogeButton/Text", "Hoge" },
    { "FugaButton/Text", "Fuga" },
    { "PiyoButton/Text", "Piyo" },
});

root.Batch(new Dictionary<string, Action<Text>>
{
    { "./Text", x => x.text = "Title" },
    { "HogeButton/Text", x => x.text = "Hoge" },
    { "FugaButton/Text", x => x.text = "Fuga" },
    { "PiyoButton/Text", x => x.text = "Piyo" },
});

使ってみる

興味ある!って方は、ぜひ試してみてください。
まだまだインターフェイスなど破壊的な変更が入る可能性が高いです。

github.com

インポート

UnityPackageManagerに https://git@github.com/kyubuns/AnKuchen.git?path=Unity/Assets/AnKuchen を突っ込んでください。

使い方

管理したいオブジェクトの親に、UICacheコンポーネントを貼り付けて、「Update」ボタンを忘れずに押します。
これでキャッシュが作られます。ランタイムで作ってもいいんですけど気持ち遅いのでEditor上で作っておきます。
あとはコードから参照して使うだけ。