Unity3Dでリアクティブリンクに到達した経緯

画像



今日は、Pixonicのいくつかのプロジェクトが、グローバルフロントエンド全体の長い間標準であったリアクティブリンクにどのように到達したかについて説明します。



私たちのプロジェクトの大部分はUnity3Dで書かれています。また、リアクティブを備えた他のクライアントテクノロジー(MVVM、Qt、数百万のJSフレームワーク)がうまく機能していて、それが当然のことと見なされている場合、Unityには組み込みまたは一般的に受け入れられているバインディングがありません。



この時までに誰かがおそらく質問をしました:「なぜ?私たちはそれを使用せず、私たちは元気に暮らしています。」



理由がありました。より正確には、問題があり、その解決策の1つは、そのようなアプローチの使用である可能性があります。その結果、ひとつになりました。そして、細部はカットされています。



まず、プロジェクトについて、その問題はそのような解決策を必要としました。もちろん、私たちはWar Robotsについて話しています。これは、開発、サポート、マーケティングなどのさまざまなチームが存在する巨大なプロジェクトです。現在、関心があるのは、クライアントプログラマーのチームとユーザーインターフェイスのチームの2つだけです。以下では、簡単にするために、それらを「コード」および「レイアウト」と呼びます。たまたま、UIのデザインやレイアウトに携わっている人もいれば、その「活性化」をしている人もいます。これは論理的であり、私の経験では、チーム編成の多くの同様の例に出くわしました。



プロジェクトの機能の流れが増えるにつれ、コードとレイアウトの相互作用が行き詰まりとボトルネックの場所になることに気づきました。プログラマーは、作業用の既製のウィジェット、レイアウト設計者を待っています-コードからのいくつかの変更を待っています。はい、この相互作用の間に多くのことが起こりました。要するに、時にはそれは混乱と先入観に変わった。



ここで説明させてください。古典的な単純なウィジェットの例、特にRefreshDataメソッドを見てください。妥当性のために追加したボイラープレートの残りの部分は、あまり注目する価値がありません。



public class PlayerProfileWidget : WidgetBehaviour
{
  [SerializeField] private Text nickname;
  [SerializeField] private Image avatar;
  [SerializeField] private Text level;
  [SerializeField] private GameObject hasUpgradeMark;
  [SerializeField] private Button upgradeButton;

  public void Initialize(ProfileService profileService)
  {
 	RefreshData(profileService.Player);

 	upgradeButton.onClick
    	.Subscribe(profileService.UpgradePlayer)
    	.DisposeWith(Lifetime);

 	profileService.PlayerUpgraded
    	.Subscribe(RefreshData)
    	.DisposeWith(Lifetime);
  }

  private void RefreshData(in PlayerModel player)
  {
 	nickname.text = player.Id;
 	avatar.overrideSprite = Resources.Load<Sprite>($"Avatars/{player.Avatar}_Small");
 	level.text = player.Level.ToString();
 	hasUpgradeMark.SetActive(player.HasUpgrade);
  }
}


これは、静的なトップダウンリンクの例です。最上位(階層内)のGameObjectのコンポーネントで、対応するタイプの下位オブジェクトのコンポーネントをリンクします。ここのすべては非常に単純ですが、あまり柔軟ではありません。



ウィジェットの機能は、新機能の出現により絶えず拡大しています。想像してみましょう。これで、アバターの周囲に境界線が表示されます。その外観は、プレーヤーのレベルによって異なります。さて、フレームの画像へのリンクを追加し、そこのレベルに対応するスプライトを浸してから、レベルとフレームを一致させるための設定を追加して、すべてをレイアウトに与えましょう。完了。



一ヶ月が経ちました。プレーヤーがメンバーの場合、クランアイコンがプレーヤーのウィジェットに表示されるようになりました。また、彼が持っているタイトルも登録する必要があります。また、アップグレードがある場合は、ニックネームを緑色に塗る必要があります。さらに、現在TextMeshProを使用しています。そしてまた...



さて、あなたはアイデアを得る。コードはますます複雑になり、さまざまな条件で大きくなりすぎています。



ここで作業するためのいくつかのオプションがあります。たとえば、プログラマーはウィジェットコードを変更し、レイアウトに変更を加えます。コンポーネントを追加し、新しいフィールドにリンクします。またはその逆:レイアウトが時間内に到着する場合があり、プログラマー自身が必要なすべてをリンクします。通常、修正はさらに数回繰り返されます。いずれにせよ、このプロセスは並行していません。両方の寄稿者が同じリソースに取り組んでいます。そして、プレハブやシーンをマージすることはまだ喜びです。



エンジニアにとって、すべては単純です。問題を見つけたら、それを解決しようとします。だからやってみました。その結果、2チーム間の接触の前線を狭める必要があると考えました。そして、リアクティブパターンは、このフロントを1つのポイントに狭めます。これは、一般にビューモデルと呼ばれます。私たちにとって、それはコードとレイアウトの間の契約として機能します。詳細に入ると、契約の意味と、2チームの並行運営を妨げない理由が明らかになります。



これらすべてについて考えたとき、いくつかのサードパーティソリューションがありました。Unity Weld、Peppermint Data Binding、DisplayFabを探していました。彼らは皆、長所と短所を持っていました。しかし、私たちにとって致命的な欠点の1つは一般的でした。つまり、私たちの目的に対するパフォーマンスの低下です。単純なインターフェイスでは正常に機能する可能性がありますが、その時点では、インターフェイスの複雑さを回避できませんでした。



この作業はそれほど難しくはなく、関連する経験さえも利用できたため、スタジオ内にリアクティブバインディングシステムを実装することにしました。



タスクは次のとおりです。



  • パフォーマンス。変更を伝播するメカニズム自体は高速である必要があります。また、フリーズがまったく満足できないゲームプレイでもこれらすべてを使用できるように、GCの負荷を減らすことが望ましいです。
  • 便利なオーサリング。これは、UIチームのメンバーがシステムを操作できるようにするために必要です。
  • 便利なAPI。
  • 拡張性。




上から下、または一般的な説明



タスクは明確であり、目標は明確です。「契約」、つまりViewModelから始めましょう。誰でも作成できる必要があります。つまり、ViewModelの実装は可能な限り単純である必要があります。これは基本的に、現在の表示状態を決定する一連のプロパティです。



簡単にするために、値を持つプロパティタイプのセットを、可能な限りbool、int、float、およびstringに制限しました。これは、一度にいくつかの考慮事項によって決定されました。



  • Unityでこれらのタイプをシリアル化するのは簡単です。
  • , -, . , Sprite -, PlayerModel , ;
  • , .


すべてのプロパティはアクティブであり、値の変更についてサブスクライバーに通知します。これらの値は常に存在するわけではありません-何らかの方法で視覚化する必要があるビジネスロジックのイベントだけがあります。この場合、値のないプロパティタイプ--eventがあります。



もちろん、インターフェースのコレクションなしでは実行できません。したがって、コレクションプロパティタイプもあります。コレクションは、その構成の変更をサブスクライバーに通知します。コレクション要素は、特定の構造またはスキーマのViewModelでもあります。このスキームは、編集時の契約にも記載されています。



ViewModelは、エディターでは次のようになります。







プロパティは、インスペクターで直接編集でき、その場で編集できることに注意してください。これにより、コードがなくてもウィジェット(またはウィンドウ、シーンなど)が実行時にどのように動作するかを確認できます。これは実際には非常に便利です。



ViewModelがバインディングシステムの上部である場合、下部はいわゆるアプリケーターです。これらは、すべての作業を行うViewModelプロパティの最終サブスクライバーです。



  • booleanプロパティの値を変更して、GameObjectまたは個々のコンポーネントを有効/無効にします。
  • stringプロパティの値に応じて、フィールドのテキストを変更します。
  • アニメーターが起動され、そのパラメーターが変更されます。
  • コレクションから目的のスプライトをインデックスまたは文字列キーで置き換えます。


アプリケーションの数は想像力と解決するタスクの範囲によってのみ制限されるため、これで停止します。



これは、一部のアプリケーターがエディターでどのように表示されるかを









示しています。柔軟性を高めるために、プロパティとアプリケーターの間でアダプターを使用できます。これらは、適用される前にプロパティを変換するためのエンティティです。さまざまなものもあります。



  • ブール値-たとえば、ブール値のプロパティを反転したり、別のタイプの値に応じてtrueまたはfalseを返す必要がある場合(レベルが15を超える場合は金色の境界線が必要です)。
  • 算術ここにコメントはありません。
  • コレクションの操作:反転、コレクションの一部のみの取得、キーによる並べ替えなど。


繰り返しになりますが、さまざまなアダプターオプションが存在する可能性があるため、続行しません。











実際、さまざまなアプリケーターとアダプターの総数は多いですが、どこでも使用される基本セットは非常に限られています。コンテンツを扱う人は、最初にこのセットを学習する必要があります。これにより、トレーニング時間がわずかに長くなります。ただし、ここで大きな問題が発生しないように、一度これに時間を割く必要があります。さらに、この問題に関するクックブックとドキュメントがあります。



レイアウトに何かが欠けている場合、プログラマーは必要なコンポーネントを追加します。同時に、圧倒的多数のアプリケーターとアダプターは普遍的であり、積極的に再利用されています。これとは別に、UnityEventを介してリフレクションに取り組むアプリケーターがまだあることに注意してください。これらは、必要なアプリケーターがまだ実装されていない場合、またはその実装が実用的でない場合に適用できます。



これは確かにレイアウトチームの作業に追加されます。しかし、私たちの場合、彼らは彼らが得るプログラマーからの自由と独立の程度にさえ満足しています。また、レイアウトの側面から作業が増えた場合は、コードの側面からすべてがはるかに簡単になります。



PlayerProfileWidgetの例に戻りましょう。コンポーネントとしてウィジェットが不要になり、すべてを直接リンクする代わりにViewModelからすべてを取得できるため、これがプレゼンターとしての仮想プロジェクトでの外観です。



public class PlayerProfilePresenter : Presenter
{
  private readonly IMutableProperty<string> _playerId;
  private readonly IMutableProperty<string> _playerAvatar;
  private readonly IMutableProperty<int> _playerLevel;
  private readonly IMutableProperty<bool> _playerHasUpgrade;

  public PlayerProfilePresenter(ProfileService profileService, IViewModel viewModel)
  {
 	_playerId = viewModel.GetString("player/id");
 	_playerAvatar = viewModel.GetString("player/avatar");
 	_playerLevel = viewModel.GetInteger("player/level");
 	_playerHasUpgrade = viewModel.GetBoolean("player/has-upgrade");

 	RefreshData(profileService.Player);

 	viewModel.GetEvent("player/upgrade")
    	.Subscribe(profileService.UpgradePlayer)
    	.DisposeWith(Lifetime);

 	profileService.PlayerUpgraded
    	.Subscribe(RefreshData)
    	.DisposeWith(Lifetime);
  }

  private void RefreshData(in PlayerModel player)
  {
 	_playerId.Value = player.Id;
 	_playerAvatar.Value = player.Avatar;
 	_playerLevel.Value = player.Level;
 	_playerHasUpgrade.Value = player.HasUpgrade;
  }
}


コンストラクターでは、ViewModelからプロパティを取得するコードを確認できます。はい、このコードでは、簡単にするためにチェックは省略されていますが、目的のプロパティが見つからない場合に例外をスローするメソッドがあります。さらに、必要なフィールドが存在することをかなり強力に保証するツールがいくつかあります。これらは資産の検証に基づいており、ここで読むことができます



多くのテキストと時間がかかるため、実装の詳細については説明しません。公的な問い合わせがある場合は、別の記事で発行することをお勧めします。実装は同じRxとそれほど変わらず、すべてが少し単純であるとだけ言っておきます。



この表は、1つのプロパティモデルと1つのアクション関数に関連付けられたInputField、Text、およびButtonを使用して500のフォームを作成するベンチマークの結果を示しています。







結論として、私は上記の目標が達成されたことを報告することができます。比較ベンチマークは、言及されたオプションと比較して、メモリと時間の両方での増加を示しています。レイアウトチームやコンテンツを扱う他の部門の人々がより意識するようになるにつれて、摩擦とブロッキングはますます少なくなります。コードの効率と品質が向上し、今では多くのことがプログラマーの介入を必要としません。



All Articles