paint-brush
StatefulUI: 状態とマークアップに基づく Unity UI ライブラリ@dmitrii
2,926 測定値
2,926 測定値

StatefulUI: 状態とマークアップに基づく Unity UI ライブラリ

Dmitrii Ivashchenko12m2023/05/17
Read on Terminal Reader

長すぎる; 読むには

Dmitrii Ivashchenko は、MY.GAMES のソフトウェア エンジニアです。この記事では、要素の状態とマークアップに基づいた Unity でのユーザー インターフェイスの開発について説明します。説明されているアプローチは、[UI Toolkit] や他の UI 構築システムには適用できません。
featured image - StatefulUI: 状態とマークアップに基づく Unity UI ライブラリ
Dmitrii Ivashchenko HackerNoon profile picture
0-item
1-item

みなさん、こんにちは。私の名前はドミトリー・イヴァシュチェンコです。MY.GAMES のソフトウェア エンジニアです。この記事では、要素の状態とマークアップに基づいた Unity でのユーザー インターフェイスの開発について説明します。

イントロ

まず最初に、 Unity UI (uGUI) テクノロジのコンテキストで説明することに注意してください。ドキュメントによると、Unity UI は依然としてランタイムに推奨されています。ここで説明するアプローチは、 UI ToolkitIMGUI 、またはその他の UI 構築システムには適用できません。



Unityプロジェクトでは、 MonoBehaviourから継承され、多数のSerializeFieldフィールドが散りばめられた View クラスに基づいて構築された UI 実装に遭遇することがよくあります。このアプローチでは、UI の動作を完全に制御できますが、ビュー レベルとプレゼンター レベルで大量のコードを記述することも必要になります (使用するアーキテクチャに応じて異なります)。


多くの場合、プロジェクト開発が続くにつれて、これらのクラスは信じられないほどのサイズに膨張し、GameObject 自体のコンポーネントは内部オブジェクトへの膨大な数のリンクで覆われます。



このようなコンポーネントの変更も楽しいものではありません。クラス内の新しい要素への参照を取得するには、 SerializeField追加し、コードを再コンパイルし、プレハブ コンポーネント内で新しいフィールドを見つけて、必要なオブジェクトをそこにドラッグする必要があります。プロジェクトが成長するにつれて、コンパイル時間、フィールドの数、プレハブの編成の複雑さも増加します。


その結果、 MonoBehaviourの大きくてオーバーロードされたサブクラス (または、好みに応じて多数の小さなサブクラス) が作成されます。


また、このような UI の動作の変更はプログラマのタスクであり、そのタスクには、コード レビュー、マージ競合の解決、テストによるコード カバレッジなど、関連するすべてのコストがかかることも考慮する価値があります。


複数の状態を持つウィンドウの実装に焦点を当てたいと思います。多くのバリエーションを見てきましたが、それらは 2 つのアプローチに分類できます。


  1. まず、ウィンドウ状態の変更はコードを使用して行われます。テキストの色の変更、画像の変更、アニメーションの再生、画面上のオブジェクトの移動を行うには、関連するすべてのオブジェクトとパラメーターに対応するSerializeFieldが必要であり、要件に従って機能させるために大量のコードが記述されます。 。当然のことながら、これを処理できるのはプログラマだけであり、実装は時間がかかり、高価で、非常に効率的であることがわかります (多くの場合、誰もが気づくよりもはるかに効率的です)。
  2. もう一つのアプローチは「全能のアニメーター」と言えます。 View クラスに加えて、Animator Controller が作成され、パラメータを通じて制御されます。新しい Animator が新しいウィンドウに表示され、ウィンドウを表示するときの FPS が低下し始めるまで続きます。



uGUI を使用する際のいくつかの困難を強調したので、この問題を解決するための別のアプローチについて話したいと思います。

ステートフルUI

私の得意プロジェクトの 1 つに取り組んでいる間、Unity で構造化 UI 開発用のライブラリを開発しました。その後、チームと私は本番環境でテストし、結果に満足しました。


ライブラリのソース コードは GitHub でダウンロードできます

ステートフルコンポーネント

ライブラリの重要な要素はStatefulComponentコンポーネントです。このコンポーネントは各画面のルート ゲームオブジェクトに配置され、タブ全体に分散された内部要素への必要な参照がすべて含まれています。



各リンクには、その役割に基づいて名前が付けられます。コードの観点から見ると、ロールのセットは通常のenumです。 UI 要素のタイプ (ボタン、画像、テキストなど) ごとに別個のロールのセットが用意されています。


 public enum ButtonRole { ... } public enum ImageRole { ... } public enum TextRole { ... } ...


ロールはコンポーネントから直接生成されるため、 enum手動で編集する必要はありません。これらのenum要素は作成直後に使用できるため、ロールの作成時に再コンパイルを待つ必要もありません。


マージの競合を単純化するために、列挙値は要素の名前に基づいて計算されます。


 [StatefulUI.Runtime.RoleAttributes.ButtonRoleAttribute] public enum ButtonRole { Unknown = 0, Start = -1436209294, // -1436209294 == "Start".GetHashCode() Settings = 681682073, Close = -1261564850, Quests = 1375430261, }


これにより、あなたと同僚がたまたま異なるブランチでボタンの新しいロールを同時に作成した場合でも、プレハブ内のシリアル化された値が壊れるのを避けることができます。


UI 要素の各タイプ (ボタン、テキスト、画像) は独自のタブにあります。



ロールを使用すると、プレハブ内のすべての要素の完全なマークアップが実現されます。画像やテキストにアクセスするためにSerializeFieldのセットは必要なくなりました。たとえば、スプライトを置き換えるには、 StatefulComponentへの参照を 1 つ持ち、目的の画像の役割を知るだけで十分です。


現在アクセスできる要素のタイプは次のとおりです。


  • ボタン、画像、トグル、スライダー、ドロップダウン、ビデオプレーヤー、アニメーター
  • UnityEngine.UI.TextおよびTextMeshProUGUIを含むテキスト
  • TextInputs ( UnityEngine.UI.InputFieldおよびTMP_InputFieldを含む)
  • オブジェクト — 任意のオブジェクトへの参照用。


注釈付きオブジェクトを操作するための対応するメソッドがあります。コードでは、 StatefulComponentへの参照を使用することも、 StatefulViewからクラスを継承することもできます。


 public class ExamplePresenter { private StatefulComponent _view; public void OnOpen() { _view.GetButton(ButtonRole.Settings).onClick.AddListener(OnSettingsClicked); _view.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); _view.GetSlider(SliderRole.Volume).onValueChanged.AddListener(OnVolumeChanged); } } public class ExampleScreen : StatefulView { private void Start() { SetText(TextRole.Title, "Hello World"); SetTextValues(TextRole.Timer, hours, minutes, seconds); SetImage(ImageRole.UserAvatar, avatarSprite); } }

テキストとローカリゼーション

テキストを含むタブには、役割とオブジェクトへのリンクに加えて、次の列が含まれています。


  • コード: ローカライズ用のテキストキー
  • 「ローカライズ」チェックボックス: テキストフィールドがローカライズの対象であることを示すインジケーター
  • 値: オブジェクトの現在のテキスト内容
  • ローカライズ済み: コード フィールドのキーによって見つかった現在のテキスト



ライブラリには、翻訳を処理するための組み込みサブシステムが含まれていません。ローカリゼーション システムに接続するには、 ILocalizationProviderインターフェイスの実装を作成する必要があります。これは、たとえば、バックエンド、ScriptableObjects、または Google Sheets に基づいて構築できます。


 public class HardcodeLocalizationProvider : ILocalizationProvider { private Dictionary<string, string> _dictionary = new Dictionary<string, string> { { "timer" , "{0}h {1}m {2}s" }, { "title" , "Título do Jogo" }, { "description" , "Descrição longa do jogo" }, }; public string GetPhrase(string key, string defaultValue) { return _dictionary.TryGetValue(key, out var value) ? value : defaultValue; } }


[ローカリゼーションをコピー] ボタンをクリックすると、コード列と値列の内容が Google スプレッドシートへの貼り付けに適した形式でクリップボードにコピーされます。

内部コンポーネント

多くの場合、再利用を容易にするために、UI の個別の部分が個別のプレハブに抽出されます。 StatefulComponent使用すると、各コンポーネントが独自の子インターフェイス要素でのみ動作するコンポーネントの階層を作成することもできます。


[内部コンポーネント] タブでは、内部コンポーネントにロールを割り当てることができます。



構成されたロールは、他のタイプの要素と同様にコード内で使用できます。


 var header = GetInnerComponent(InnerComponentRole.Header); header.GetButton(ButtonRole.Close).onClick.AddListener(OnCloseClicked); header.SetText(TextRole.Title, "Header Title"); var footer = GetInnerComponent(InnerComponentRole.Footer); footer.GetButton(ButtonRole.Continue).onClick.AddListener(OnContinueClicked); footer.SetText(TextRole.Message, "Footer Message");

コンテナ

類似した要素のリストを作成するには、 ContainerViewコンポーネントを使用できます。インスタンス化用のプレハブとルート オブジェクト (オプション) を指定する必要があります。編集モードでは、 StatefulComponentを使用して要素を追加および削除できます。



インスタンス化されたプレハブのコンテンツをマークアップするには、 StatefulComponentを使用すると便利です。ランタイムでは、メソッドAddInstance<T>AddStatefulComponent 、またはFillWithItemsを使用してコンテナーにデータを設定できます。


 var container = GetContainer(ContainerRole.Players); container.Clear(); container.FillWithItems(_player, (StatefulComponent view, PlayerData data) => { view.SetText(TextRole.Name, data.Name); view.SetText(TextRole.Level, data.Level); view.SetImage(ImageRole.Avatar, data.Avatar); });


標準のObject.Instantiate()オブジェクトの作成に適さない場合は、たとえば Zenject を使用したインスタンス化などでこの動作をオーバーライドできます。


 StatefulUiManager.Instance.CustomInstantiateMethod = prefab => { return _diContainer.InstantiatePrefab(prefab); };


内部コンポーネントとコンテナは、それぞれStatefulComponentの静的ネストと動的ネストを提供します。


プレハブのマークアップ、ローカリゼーション、インスタンス化について検討しました。ここで、最も興味深い部分である状態に基づいた UI の開発に移ります。

状態の概念を、プレハブに対する変更の名前付きセットとして考えます。この場合の名前はStateRole列挙型のロールであり、プレハブへの変更の例は次のとおりです。


  • ゲームオブジェクトの有効化と無効化
  • Image オブジェクトのスプライトまたはマテリアルを置き換える
  • 画面上のオブジェクトを移動する
  • テキストとその外観の変更
  • アニメーションの再生
  • など - 独自のタイプのオブジェクト操作を追加できます


一連の変更 (状態の説明) は、「状態」タブで構成できます。構成された状態はインスペクターから直接適用できます。



構成された状態は、 SetStateメソッドを使用してコードから適用できます。


 switch (colorScheme) { case ColorScheme.Orange: SetState(StateRole.Orange); break; case ColorScheme.Red: SetState(StateRole.Red); break; case ColorScheme.Purple: SetState(StateRole.Purple); break; }


[ツール] タブで、 [有効化時に初期状態を適用] パラメータが有効になっている場合、オブジェクトのインスタンス化時にすぐに適用される状態を構成できます。


状態を使用すると、View クラス レベルで必要なコードの量を大幅に削減できます。画面の各状態をStatefulComponentの一連の変更として記述し、ゲームの状況に応じてコードから必要な状態を適用するだけです。

州ツリー

実際、状態に基づいて UI を開発するのは非常に便利です。そのため、時間の経過とともに別の問題が発生します。プロジェクトが進行するにつれて、単一ウィンドウの状態のリストが手に負えないほど長くなる可能性があり、そのためナビゲートするのが困難になります。さらに、他の州との関連でのみ意味をなす州もあります。この問題を解決するために、Statful UI には State Tree という別のツールがあります。 「States」タブの「State Tree Editor」ボタンをクリックするとアクセスできます。


チェストの報酬ウィンドウを作成する必要があるとします。ウィンドウには 3 つのフェーズがあります。


  • チェストのアニメーション紹介 (ステートイントロ)
  • チェストから 3 つの異なるタイプの報酬がループして表示されます (報酬の状態に応じて、 MoneyEmoji 、およびCards の状態が表示され、チェストから出現する報酬のアニメーションがトリガーされます)。
  • 獲得したすべての報酬を 1 つのリストに表示 (状態Results )


親状態 (この例ではReward ) は、子状態が呼び出されるたびに適用されます。



構成されたStatefulComponentを管理するには、コンポーネントに必要なデータを設定し、適切なタイミングで状態を切り替える、最小限のシンプルでわかりやすいコードが必要になります。


 public void ShowIntro() { SetState(StateRole.Intro); } public void ShowReward(IReward reward) { // Update the inner view with the reward reward.UpdateView(GetInnerComponent(InnerComponentRole.Reward)); // Switch on the type of reward switch (reward) { case ICardsReward cardsReward: SetState(StateRole.Cards); break; case IMoneyReward moneyReward: SetState(StateRole.Money); break; case IEmojiReward emojiReward: SetState(StateRole.Emoji); break; } } public void ShowResults(IEnumerable<IReward> rewards) { SetState(StateRole.Results); // Fill the container with the rewards GetContainer(ContainerRole.TotalReward) .FillWithItems(rewards, (view, reward) => reward.UpdateView(view)); }

ステートフル API とドキュメント

ロールは、コード内で後で使用するために、リンクと状態に名前を付ける便利で明確な方法を提供することを目的としています。ただし、状態の説明に長すぎる名前が必要になる場合があり、このリンクが指すもの、または状態がどのような動作を反映しているかについて短いコメントを残した方が便利な場合があります。このような場合、 StatefulComponentの各リンクと状態に説明を追加できます。



各タブにある [API をコピー] ボタンと [ドキュメントをコピー] ボタンにすでに気づいているかもしれません。これらは、選択したセクションの情報をコピーします。これらに加えて、[ツール] タブにも同様のボタンがあり、これらはすべてのセクションの情報を一度にコピーします。 「API のコピー」ボタンをクリックすると、このStatfulComponentオブジェクトを管理するために生成されたコードがクリップボードにコピーされます。以下は報酬ウィンドウの例です。


 // Insert the name of the chest here SetText(TextRole.Title, "Lootbox"); // Button to proceed to the reward issuance phase GetButton(ButtonRole.Overlay); // Button to display information about the card GetButton(ButtonRole.Info); // Container for displaying the complete list of awarded rewards GetContainer(ContainerRole.TotalReward); // Insert the card image here SetImage(ImageRole.Avatar, null); // Animated appearance of a chest SetState(StateRole.Intro);


[ドキュメントのコピー] ボタンをクリックすると、このプレハブのドキュメントがマークダウン形式でクリップボードにコピーされます。


 ### RewardScreen Buttons: - Overlay - Button to proceed to the reward issuance phase - Info - Button to display information about the card Texts: - Title - Insert the name of the chest here Containers: - TotalReward - Container for displaying the complete list of awarded rewards Images: - Avatar - Insert the card image here States: - Intro - Animated appearance of a chest - Cards - Displaying rewards in the form of a card - Money - Displaying rewards in the form of game currency - Emoji - Displaying rewards in the form of an emoji - Results - Displaying a complete list of issued rewards


これほど詳細な指示が示されたこの画面を実装する場合、間違いを犯すのは非常に困難であることは明らかです。プロジェクトのナレッジ ベースで、UI 組織に関する最新情報を簡単に維持できます。


同時に、ステートフル UI により、UI プレハブの作成を委任できます。実際、状態ベースのマークアップを使用すると、プレハブの動作をプログラマに渡す前に完全にテストできます。これは、ゲーム デザイナー、テクニカル デザイナー、さらには別の UI 開発者がプレハブを準備できることを意味します。さらに、コードとプレハブの間にAPIが作成されるため、プログラミングとプレハブの設定を並行して行うことができます。必要なのは、事前にAPIを策定することだけです。ただし、プレハブを構成するタスクがプログラマに残っている場合でも、ステートフル UI を使用すると、この作業が大幅に高速化されます。

結論

これまで見てきたように、ステートフル UI により、UI 要素の状態の操作が大幅に簡素化されます。 SerializeFields の作成、コードの再コンパイル、膨大な数の View クラス フィールド間での参照の検索に長いサイクルが必要なくなりました。 View クラス自体では、オブジェクトのオン/オフやテキストの色の変更などの繰り返し操作のために大量のコードを記述する必要がなくなりました。


このライブラリを使用すると、プロジェクト内のレイアウトの整理、プレハブ内のオブジェクトのマーク、状態の作成、UI 要素へのリンク、UI 管理用の API とドキュメントの提供に対する一貫したアプローチが可能になります。また、UI プレハブの作成を委任することもでき、UI プレハブの作業が高速化されます。


今後、プロジェクトのロードマップには次の項目が含まれます。


  • 状態の機能を拡張し、新しいタイプのアニメーション、状態でのサウンドの再生など、説明での新しいタイプの UI 変更をサポートします。


  • テキストと画像に色を付けるためのカラー パレットのサポートの追加


  • ゲームオブジェクトの再利用によるアイテムのリストのサポートの追加


  • より多くの Unity UI 要素のサポート


  • ローカライズのために追加されたテキストのアンロードを自動化する


  • テストフレームワークの実装。プレハブの徹底的なマークアップがあるため、セットアップが簡単な ScriptableObject ベースのシナリオを次の形式で作成できます。


    1. ButtonRole.Settingsボタンをクリックします。

    2. TextRole.SomeText内のテキストが「何らかの値」と等しいことを確認します。

    3. ImageRole.SomeImage内の画像をチェックして、特定のスプライトと等しいことを確認します。


  • チュートリアルシステムです。テストと同様に、マークされたレイアウトを使用すると、「ボタンにポインターを表示するButtonRole.UpgradeHero 」のような指示の形式で ScriptableObject ベースのチュートリアル シナリオを作成できます。


プロジェクトのソース コードは GitHub で入手できます。問題を作成したり、ライブラリに貢献したりすることは歓迎です。