How We Came to Reactive Linking in Unity3D

image



Today I will talk about how some projects at Pixonic came to what has become the norm for the entire global front-end - reactive linking.



The vast majority of our projects are written in Unity 3D. And, if other client technologies with reactivity are doing well (MVVM, Qt, millions of JS frameworks), and it is taken for granted, there are no built-in or generally accepted bindings in Unity.



By this time, someone probably had a question: “Why? We don’t use that and we live well. ”



There were reasons. More precisely, there were problems, one of the solutions to which could be the use of such an approach. As a result, it became one. And the details are under the cut.



First, about the project, the problems of which required such a solution. Of course, we are talking about War Robots - a giant project with many different teams of development, support, marketing, etc. We are now interested in only two of them: the team of client programmers and the team of the user interface. In what follows, for simplicity, we will refer to them as “code” and “layout”. It so happened that some people are engaged in the design and layout of the UI, while the “revitalization” of all this is done by others. This is logical, and in my experience I have come across many similar examples of team organization.



We noticed that with the growing flow of features on the project, the interaction between code and layout becomes a place of deadlocks and a bottleneck. Programmers are waiting for ready-made widgets for work, layout designers - for some modifications from the code. Yes, a lot of things happened during this interaction. In short, sometimes it turned into chaos and procrastination.



Let me explain now. Take a look at the classic simple widget example - especially the RefreshData method. The rest of the boilerplate I just added for believability, and it is not worth special attention.



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);
  }
}


This is an example of static top-down linking. In the component of the top (in the hierarchy) GameObject, you link components of the corresponding types of lower objects. Everything here is extremely simple, but not very flexible.



The functionality of widgets is constantly expanding with the advent of new features. Let's imagine. There should now be a border around the avatar, the appearance of which depends on the player's level. Okay, let's add a link to the Image of the frame and immerse the sprite corresponding to the level there, then add the setting for matching the level and the frame and give it all to the layout. Done.



A month has passed. Now a clan icon appears in the player's widget, if he is a member. And you also need to register the title that he has there. And the nickname needs to be painted green if there is an upgrade. In addition, we are now using TextMeshPro. And also ...



Well, you get the idea. The code becomes more and more, it becomes more and more complicated, overgrown with various conditions.



There are several options for working here. For example, the programmer modifies the widget code, gives the changes to the layout. They add and link components to new fields. Or vice versa: the layout may arrive in time in advance, the programmer himself will link everything that is needed. Usually, there are several more iterations of fixes. In any case, this process is not parallel. Both contributors are working on the same resource. And merging prefabs or scenes is still a pleasure.



For engineers, everything is simple: if you see a problem, you try to solve it. So we tried. As a result, we came to the idea that it was necessary to narrow the front of contact between the two teams. And reactive patterns narrow this front to one point - what is commonly called the View Model. For us, it acts as a contract between code and layout. When I get down to the details, the meaning of the contract will become clear, and why it does not block the parallel operation of two teams.



At the time when we just thought about all this, there were several third-party solutions. We were looking towards Unity Weld, Peppermint Data Binding, DisplayFab. They all had their pros and cons. But one of the fatal drawbacks for us was common - poor performance for our purposes. They may work fine on simple interfaces, but by that time we could not avoid the complexity of the interfaces.



Since the task did not seem prohibitively difficult, and even there was relevant experience, it was decided to implement a reactive binding system inside the studio.



The tasks were as follows:



  • Performance. The mechanism for propagating changes itself must be fast. It is also desirable to reduce the load on the GC so that you can use all this even in gameplay, where freezes are not at all happy.
  • Convenient authoring. This is necessary so that the guys from the UI team can work with the system.
  • Convenient API.
  • Extensibility.




Top to bottom, or general description



The task is clear, the goals are clear. Let's start with the "contract" - the ViewModel. Any person should be able to form it, which means that the ViewModel should be implemented as simply as possible. It's basically just a set of properties that determine the current display state.



For simplicity, we have limited the set of property types with values ​​to bool, int, float and string as much as possible. This was dictated by several considerations at once:



  • Serializing these types in Unity is effortless;
  • , -, . , Sprite -, PlayerModel , ;
  • , .


All properties are active and inform subscribers about changes to their values. These values ​​are not always present - there are just events in business logic that need to be visualized somehow. In this case, there is a property type without a value - event.



Of course, you can't do without collections in interfaces either. Therefore, there is also a collection property type. The collection notifies subscribers of any change in its composition. Collection elements are also ViewModels of a certain structure or schema. This scheme is also described in the contract when editing.



In the ViewModel editor looks like this:







It should be noted that properties can be edited directly in the inspector and on the fly. This allows you to see how the widget (or window, or scene, or whatever) will behave at runtime even without code, which is very convenient in practice.



If the ViewModel is the top of our binding system, then the bottom is the so-called applicators. These are the final subscribers of the ViewModel properties that do all the work:



  • Enable / disable GameObject or individual components by changing the value of the boolean property;
  • Change the text in the field depending on the value of the string property;
  • The animator is launched, its parameters are changed;
  • Substitute the desired sprite from the collection by index or string key.


I will stop at this, since the number of applications is limited only by imagination and the range of tasks that you solve.



This is how some of the applicators look in the editor:









For more flexibility, adapters can be used between properties and applicators. These are entities for transforming properties before being applied. There are also many different ones:



  • Boolean - for example, when you need to invert a boolean property or return true or false depending on a value of another type (I want a golden border when the level is above 15).
  • Arithmetic . No comment here.
  • Operations on collections : invert, take only part of a collection, sort by key, and much more.


Again, there can be a great variety of different adapter options, so I will not continue.











In fact, although the total number of different applicators and adapters is large, the basic set used everywhere is very limited. A person working with content needs to study this set first, which slightly increases the training time. However, you need to devote time to this once, so that further there are no big problems here. Moreover, we have a cookbook and documentation on this matter.



When the layout lacks something, programmers add the necessary components. At the same time, the vast majority of applicators and adapters are universal and are actively reused. Separately, it should be noted that we still have applicators that work on reflection via UnityEvent. They are applicable in cases where the required applicator has not yet been implemented or its implementation is impractical.



This certainly adds to the work of the layout team. But in our case, they are even happy with the degree of freedom and independence from programmers that they get. And if the work has increased from the side of the layout, then from the side of the code everything is now much easier.



Let's go back to the PlayerProfileWidget example. This is how it now looks in our hypothetical project as a presenter, because we no longer need a Widget as a component, and we can get everything from the ViewModel instead of linking everything directly:



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;
  }
}


In the constructor, you can see the code getting properties from the ViewModel. Yes, in this code, checks are omitted for simplicity, but there are methods that will throw an exception if they do not find the desired property. In addition, we have several tools that provide a pretty strong guarantee that the required fields are present. They are based on asset validation, which you can read about here .



I will not go into implementation details, as it will take a lot of text and your time. If there is a public inquiry, it would be better to issue it in a separate article. I will only say that the implementation is not very different from the same Rx, only everything is a little simpler.



The table shows the results of a benchmark that creates 500 forms with InputField, Text and Button, associated with one property of the model and one action function.







As a conclusion, I can report that the above goals have been achieved. Comparative benchmarks show gains both in memory and in time relative to the options mentioned. As the layout team and people from other departments who deal with content become more aware, friction and blocking becomes less and less. The efficiency and quality of the code have increased, and now many things do not require programmer intervention.



All Articles