Mvvm option in Unity

Quite often I hear questions about the implementation of this pattern, so here I will try to show a working version.

Why is this even needed?

In modern games, there are a huge number of UI elements that need to be shown to the player. At the same time, they usually have some kind of interaction with each other, some elements should be shown on top of others, and sometimes some elements should be replaced by others. To prevent the process of working with the UI from becoming a pain, various architectural patterns are used to streamline what is happening and reduce the requirements for the developer’s personal RAM.

Free paraphrase of Wikipedia

MVVM is an architectural pattern that separates user interface development from business logic development.

MVVM consists of 3, but not quite, parts:
1.Model (model) where the logic of the game takes place: abilities are pumped, enemies die, the level rises, and so on. Since here we are talking about working with the UI, I will talk about the model as about everything that is not a user interface. There can be anything from god object to ECS.

2. View (view) with the visual part. This is just the UI layer: sliders, progress bars, menus, as well as buttons that the user can press

3. View model (I twist model) acts as a connecting link between model and I twist. This layer collects data from the model for display and also causes changes to the model.

4. Binder (binder). A layer that helps to link the view and the view model in automatic mode. For some reason, the wiki in Russian is completely ignored, although, according to the English version of the site, this is the most important component of the pattern, and the pattern itself can be called model-view-binder.

view model

Let’s create a layer for the view model. It collects data and prepares it for view, and also has the ability to interact with the model on view requests.

public interface IViewModel : IDisposable
{
}

public abstract class BaseViewModel : IViewModel
{
   public virtual void Dispose()
   {
   }
}

view

Everything is a little more complicated here. To begin with, I will introduce an additional word – screen. A screen is a single object that can contain many different UI elements. For example, a character’s leveling window is a screen. Inventory – screen. The progress bar of the production generator is not a screen, but the window with the generator and its settings is a screen. The screen itself manages its UI elements: it can show them, hide them, change their settings, and so on. We will connect the screens in the mew model.

At first approximation, you should get something like this:

public abstract class BaseScreen : MonoBehaviour
{
   public abstract void Show();
   public abstract void Close();

   public virtual void Dispose()
   {
   }
}

Now there is no way to link the view and the view model, and there is also no bind, although the English Wikipedia insists that this is important. So we need to add some functionality to the view. After the addition, it will look like this:

public abstract class BaseScreen : MonoBehaviour
{
   public abstract Type ModelType { get; }

   public abstract void Show();
   public abstract void Close();
   public abstract void Bind(object model);

   public virtual void Dispose()
   {
   }
}

public abstract class AbstractScreen<TModel> : BaseScreen where TModel:IViewModel
{
   public override Type ModelType => typeof(TModel);
   protected TModel _model;

   public override void Show()
   {
       gameObject.SetActive(true);
   }

   public override void Close()
   {
       gameObject.SetActive(false);
   }

   public override void Bind(object model)
   {
       if (model is TModel)
           Bind((TModel) model);
   }

   public void Bind(TModel model)
   {
       _model = model;
       OnBind(model);
   }

   protected abstract void OnBind(TModel model);
}

Now there is a connection of the screen with its view model. You can independently prepare data for each screen and customize the screens themselves.

Here is an example screen and its view model:

public class ConcreteScreen : AbstractScreen<ConcreteViewModel>
{
   [SerializeField] private Text _health;
   [SerializeField] private Button _someButton;
   private ConcreteViewModel _model1;

   private void Start()
   {
       _someButton.onClick.AddListener(AddHealth);
   }

   protected override void OnBind(ConcreteViewModel model)
   {
       _model1 = model;
       _health.text = model.Health.ToString();
   }

   private void AddHealth()
   {
       _model1.DoSomething();
   }
}

public class ConcreteViewModel : BaseViewModel
{
   private readonly IGameData _gameData;
   public int Health => _gameData.Health;
   public ConcreteViewModel(IGameData gameData)
   {
       _gameData = gameData;
   }
   public void DoSomething()
   {
       _gameData.AddHealth();
   }
}

Binder

It remains to add a point of interaction with this system, which will open and close screens on demand

public class UiManager : MonoBehaviour
{
   private IEnumerable<BaseScreen> _screens;
   private Dictionary<Type, BaseScreen> _screensMap;
   private Dictionary<Type, BaseScreen> _shownScreens;
  
   public void Init(IEnumerable<BaseScreen> screens)
   {
       foreach (var screen in _screens)
       {
           screen.gameObject.SetActive(false);
       }
       _screensMap = _screens.ToDictionary(e => e.ModelType, e => e);
   }

   public void BindAndShow<TModel>(TModel model) where TModel : IViewModel
   {
       if (_screensMap.TryGetValue(typeof(TModel), out var screen))
       {
           screen.Bind(model);
           screen.Show();
           _shownScreens.Add(typeof(TModel), screen);
       }
   }

   public void Hide<TModel>() where TModel : IViewModel
   {
       if (_shownScreens.TryGetValue(typeof(TModel), out var screen))
       {
           screen.Dispose();
           screen.Close();
           _shownScreens.Remove(typeof(TModel));
       }
   }
}

From this point, you can safely expand the functionality by adding static screens, tooltips, dialog boxes.

An abstract example designed to show areas of responsibility

Let’s say we come to a chest and want to see what’s inside. The chest in our system is considered part of the model. When clicking on the chest, it calls the BindAndShow method from the UIManager, into which it passes the newly created view model for the screen. Now we see a screen with the contents of the chest.

Let’s also assume that you can look at the descriptions of items right on this screen by hovering over them. In this case, the screen element will call the view of the chest screen model with a request to show the “drop-down”. The view model of the chest screen calls the UIManager and calls the same method on it, passing it the item data from the chest.

PS

The idea of ​​​​implementation does not belong to me, so it will be correct to attach link on the original git.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *