以下の内容はhttps://tech-blog.cluster.mu/entry/2024/10/18より取得しました。


内製のUnity UI Frameworkの開発から導入・運用

こんにちは、クラスターでUnityエンジニアをしているsenchaです! 本記事ではUnityの内製UI Framework「shiranui」の開発と、導入について紹介いたします。

clusterが抱えていたUnityでのUI開発の問題点

clusterのUnity UI開発では以下の問題点を抱えていました。

UI例: メインメニュー画面

  • 画面遷移実装の複雑さ
    • 画面遷移を1つの神classが行っており、煩雑になっている
    • 遷移履歴の拡張性が乏しく、1つ前の画面までしか保存できていない
    • 画面から戻る処理を各画面のenum型で分岐処理しているため、複数の戻り先がある場合に複雑になる
  • UI実装のコストの高さ
    • スクロールリストの実装やその他UIコンポーネントとロジックの繋ぎこみの書き方が揃っておらず、各画面・コンポーネントごとにコードを書いてる
      • UI component はほぼ素の uGUIを使用
      • 新規画面作成時にコピペコードが多い
      • class設計が強制されない
    • 画面遷移のための animation を都度作成する必要がある
    • viewのコードでアニメーションとロジックが分離できていない

上記の問題を解消し「UnityでのUI開発を10倍楽にするぞ!」をコンセプトに始まったのがshiranuiです。third-partyのUI Frameworkを使う選択肢もありましたが、以下のことを理由に内製で作ることに決めました。

  • カスタマイズ性と柔軟性
    • clusterのクライアントアプリではDesktop/Mobile/VRと様々なプラットフォームで開発を行っており、全てのプラットフォームを見据えてUI開発を行えることが理想
  • パフォーマンスの最適化
    • clusterのクライアントアプリでは特にMobileでのパフォーマンスを最適化したい需要があり、パフォーマンスの計測・最適化を行いたい

UI Framework 「shiranui」とは

内製のUI Frameworkを開発していく上でコードネームをshiranuiと名づけました。

shiranuiは 「海岸から海に出られた天皇が、夜の海で彷徨った際に遥か前方に現れた火の光の方向に進むことで助けられた(引用: Wikipedia)」 という日本書紀の逸話から着想を得て命名しました。shiranuiを使い開発を進めることでUI開発という暗闇の中で道筋を示すという由来があります。 他にも命名には画像のような意味が込められています。かっこいい。

shiranuiでできること

shiranuiでは以下のことをサポートします。

  • UI画面の生成・破棄
  • 画面遷移
  • 独自のUIコンポーネント
  • 汎用AnimationView

shiranuiでは単一の画面をPageと呼びます。 Pageには画面遷移に伴うライフサイクルが存在します。 ライフサイクルを起点に画面の振る舞いを記述できるため、ロジックとアニメーションについての関心の分離が可能です。また、shiranuiではUI実装のコード記述をMVVMアーキテクチャで行います。画面遷移の表示時にViewModelがViewへBindされます。各UIコンポーネントについてはshiranuiでコンポーネントとViewStateが1:1で定義されています。例えば、ButtonコンポーネントであればShiranuiButton : ShiranuiButtonStateとなっています。コンポーネントに対するViewStateを宣言的にBindすることでUIを実装することができます。

このようなアーキテクチャに変更した大きな理由としてはスクロールリストの実装とその他UIコンポーネントでロジックの繋ぎこみ処理の書き方が揃っていなかったことが挙げられます。UI実装の書き方に一貫性を持たせるため、MVVMへ変更することにしました。

shiranuiの基本的な使い方(Sample画面の表示)

本章ではshiranuiの使い方について解説していきます。shiranuiを用いた画面表示は以下の手順で行えます。

  1. Page, View, ViewModel, Coordinator をそれぞれ用意する
  2. Installerを定義し、FactoryとしてBindしておく
  3. PageNavigatorに対してPageの型でPush, Popを行うことで画面を表示する

詳しく説明していきます。 試しにSampleの画面を表示してみましょう。コードとしては下記のような記述となります。まずはPageを定義し、必要なView, ViewModelを作成します。

// 単一の画面を定義
class SamplePage : Shiranui.Page {}

class SamplePageViewModel
{
    readonly ReactiveProperty<ShiranuiButtonState> closeButtonState = new(new(() => Close()));

    public IReadonlyReactiveProperty<ShiranuiButtonState> CloseButtonState => closeButtonState;

    void Close() { }
}

class SamplePageView : ShiranuiView<SamplePageViewModel>
{
    [SerializeFiled] ShiranuiButton closeButton;    

    public override Binder Bind(SamplePageViewModel viewModel)
    {
        return new Binder
        (
            closeButton.Bind(viewModel.CloseButtonState)
        );
    }
}

SamplePageViewにはSamplePageViewModelがBindされます。Viewが保持しているButtonコンポーネントはViewModel内のButtonStateをBindすることで宣言的に処理を記述できます。

また、shiranuiではプラットフォームによる画面遷移処理のロジックを分離できるようにCoordinatorというものを取り入れています。これにより、プラットフォーム毎の画面実装の共通化を行えます。今度はCoordinatorを追加し、ViewModelに画面を閉じる処理を追加してみましょう。

// 画面遷移処理のロジック
interface ISamplePageCoordinator
{
    void Close();
}

class NonVRSamplePageCoordinator : ISamplePageCoordinator
{
    void ISamplePageCoordinator.Close()
    {
        // NonVR用の画面を閉じる処理
    }
}

class VRSamplePageCoordinator : ISamplePageCoordinator
{
    void ISamplePageCoordinator.Close()
    {
        // VR用の画面を閉じる処理
    }
}

class SamplePageViewModel
{
    readonly ReactiveProperty<ShiranuiButtonState> closeButtonState = new(new(() => Close()));

    public IReadonlyReactiveProperty<ShiranuiButtonState> CloseButtonState => closeButtonState;

    readonly ISamplePageCoordinator coordinator;

    [Inject]
    public SamplePageViewModel(ISamplePageCoordinator coordinator)
    {
        this.coordinator = coordinator;
    }


    void Close()
    {   
        // 画面を閉じる
        coordinator.Close();
    }
}

そして、遷移時のアニメーションについてはロジックを司るSamplePageViewとは別のSampleTransitionViewとして定義することができロジックとの関心の分離が可能となっています。 これらの実装はPageInstallerで各クラスを指定しているため、画面内での設計を強制することができます。

// 画面のアニメーションを行うView
class SampleTransitionView : ShiranuiView
{
    [SerializeField] CanvasGroup canvasGroup;

    public override UniTask WillAppearAsync(CancellationToken token)
    {
        // 表示時アニメーション処理    
        canvasGroup.blockRaycasts = false;
    }

    public override void DidAppear()
    {
        canvasGroup.blockRaycasts = true;
    }

    // 非表示時
    public override UniTask WillDisappearAsync(CancellationToken token)
    {
        canvasGroup.blockRaycasts = false;
    }

    // 非表示後
    public override void DidDisappear()
    {

    }

}

// NonVR用のInstaller
class NonVRSamplePageInstaller : PageInstaller<SamplePage, SamplePageViewModel, SamplePageView, NonVRSamplePageCoordinator>
{
    [SerializeField] SamplePageView view;
    [SerializeField] SampleTransitionView transitionView;

    public override void InstallBindings()
    {
        // よしなにBindする
        base.InstallBindings();
    }
}

これで画面を表示する準備が整いました。次は実際に画面を表示してみましょう。 画面遷移に伴う、生成・破棄はPageContainerを介してshiranui内で行います。PageContainerはPageFactoryContextのイベントを購読するため、画面の履歴を管理するPageStackをBindしPageStackに対してPush/Popを行うことで画面の表示・非表示を行うことができます。

// 画面の遷移と履歴を管理する
class PageNavigator : IInitializable
{
    readonly IPageContainer pageContainer;
    readonly PageStack pageStack = new();

    public void Push(PageFactoryContext context)
    {
        pageStack.Push(cotext);
    }

    public void Pop()
    {
        pageStack.Pop();
    }

    void IInitializable.Initializa()
    {
        pageContainer.Bind(pageStack.TopValueChanged);
    }
}

// Sample画面の表示
pageNavigator.Push(new PageFactoryContext<SamplePage>());
// Sample画面を閉じる
pageNavigator.Pop();

shiranuiの開発

先んじてshiranuiの使い方について解説していきましたが、clusterへ導入するにあたりshiranui自体の開発についてお話ししていきます。shiranuiは外部パッケージとして開発されており、clusterのunityprojectには依存しない形で制作されました(unityprojectについてはこちらの記事をご覧ください「本日クラスターに入社したUnity Engineerが読む記事」の紹介)。

開発にあたり、いきなり本開発に取り掛かるのではなくPoCから行いコードの使い心地などを確認していきます。PoCを行うことで、初期段階ではMVPに則った形で実装して触り心地を確かめていたのですが、宣言的にかけた方が良いと判断しMVVMに変更することを決めました。

ある程度開発方針が固まったら次はdesign docを書き、設計方針の合意を取っていきます(clusterのdesign docについての詳細はこちらの記事をご覧ください「clusterの設計ってどうやってるの?」)。

設計方針についても合意ができたらいよいよ本開発です。といっても本開発は基本的にdesign docに記載した方針で実装していくだけです。開発にあたって一つ特徴を挙げるならばshiranuiではテストを導入しています。画面遷移の根幹部分を司ることもあり、プロジェクトの品質を保つためにテストを導入することで安定性に寄与します。

このようにshiranui自体の開発を行いました。

clusterのunityprojectへshiranuiを導入する

shiranuiの開発が一通り完了したため、次はunityprojectへの導入を行いました。 まずはclusterのoutroom画面と呼ばれる2D UI画面への導入を行います(主にメニューやそこから遷移できる2D UI画面を指します)。

outroom画面 例1: プロフィール画面

outroom画面 例2: 探索画面

今回の導入では、クラス内部のリファクタ等は行わない方針のため、shiranui UIコンポーネントへの置き換え等は行わず逐次インクリメンタルに行う方針です。既存の画面はMVPで記述されており、UIコンポーネントを導入するにはMVVMにリファクタする必要があります。この修正を行うには範囲が広すぎるため、画面遷移のみの変更としました。 導入へはmilestoneを3つに区切ります。

  1. 画面生成を置き換える
  2. outroom 全画面をPageで定義
  3. WindowCreatorにBindされているFactoryをPageFactoryに変更
  4. 各画面のMonoInstallerをPageInstallerへ変更
  5. 画面遷移を置き換える(破棄は行わない)
  6. 遷移を行っている神classからIPageContainerに差し替える
  7. ライフサイクルイベントの使用と破棄
  8. ライフサイクルイベントでAnimation等の待機を行う
  9. 画面の破棄についてもライフサイクルイベント発行後行われるようにする

clusterの2D UIは約30画面ほど存在し、それらを全て置き換えます。影響範囲が広いため、導入時の課題として下記のような点が挙げられました。

  • 画面によっては独自のルーティングを行っており、兼ね合いが大変
  • 画面遷移の導線では無限にスタックする画面がある
  • 通常のプレイモードに加え、アバターにアクセサリーをつけられるモードや自身でワールドを作成することができるクラフトモードを行き来する画面が存在し、スタックの取り扱いが難しい

これらの問題は歴史的経緯で積み重なっているものや、UIごとの設計が異なっていることから起こっています。一括で解消するのは難しいので、前述した通り画面内でのリファクタは導入後にインクリメンタルに行うこととし、画面遷移処理の置き換えに注力しました。

この時shiranuiではMVVMアーキテクチャを使用することにしていますが、内部のリファクタを行わないようにすべくMVPアーキテクチャのまま置き換えられるようにしました。

shiranuiの運用

画面遷移の処理まで導入が完了したら、次は運用です。今後は他のエンジニアがshiranuiを使い新しい画面を作っていけるようにする必要があります。 特に前述した通り、shiranuiのUIコンポーネントを導入すると画面のclassレベルでのアーキテクチャをMVVMへ変更する必要があります。これまでのclusterの画面ではMVPを用いて作られていたため、ガイドライン等で変更を周知する必要があります。

そこで、社内のunityエンジニア向けへ展開するために諸々を用意しました。

  • README, Sampleを用意
  • 新しい2D UI 画面の書き方を大統一する旨の展開用のdocを用意
    • MVVMに合わせたディレクトリ構造であったり、設計に一貫性が持てるようなガイドラインを記述
  • slackでUnityエンジニア向けにアナウンスし周知

また新規画面開発が走りそうになった場合には個別にサポートしたり、既存の画面に改修が入りそうな場合にはshiranui UIコンポーネントの導入を提案してみるなど細々とした活動も行っています。

上記のような活動を一通り行い、shiranuiの導入は一旦完了しました。

最後に

shiranuiのPoC含め開発から導入には約1年ほど費やしており、自分としては一大プロジェクトでした。今は無事導入も完了して運用フェーズとなっており一安心しております。 内製のUnity UI Frameworkの作成を検討している方のお力になれれば幸いです。




以上の内容はhttps://tech-blog.cluster.mu/entry/2024/10/18より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14