Как стать автором
Обновить

Вариант Mvvm в Unity

Уровень сложностиПростой
Время на прочтение5 мин
Количество просмотров7.8K

Довольно часто слышу вопросы про реализацию этого паттерна, поэтому здесь я постараюсь показать рабочий вариант исполнения.

Дисклеймер

Мне справедливо заметили, что надо указать уровень, на который рассчитана статья. У меня чуть больше 2 лет коммерческого опыта, а представленная реализация - единственная, с которой я работал над несколькими айдлерами. Поэтому статья рассчитана на тех, кто хочет поработать с паттерном, но не может найти реализаций в открытом доступе. И я нисколько не претендую на абсолютность решения.

Зачем это вообще нужно?

В современных играх есть огромное количество UI элементов, которые надо показывать игроку. При этом у них обычно есть какие‑то взаимодействия между собой, одни элементы должны показываться поверх других, а иногда должна происходить замена одних элементов на другие. Чтобы предотвратить превращение процесса работы с UI в боль пользуются различными архитектурными паттернами, позволяющими упорядочить происходящее и снизить требования к личной оперативной памяти разработчика.

Вольный пересказ Википедии

MVVM — архитектурный паттерн, отделяющий разработку пользовательского интерфейса от разработки бизнес логики.

MVVM состоит из 3, но не совсем, частей:

  1. Model (модель), где происходит логики игры: прокачиваются способности, умирают враги, повышается уровень и тд. Так как здесь речь про работу с UI, о модели я буду говорить как о всем, что не пользовательский интерфейс. Там может быть все что угодно от god object до ECS.

  2. View (вью) с визуальной частью. Это как раз слой UI: слайдеры, прогресс бары, менюшки, а также кнопки, в которые может жать пользователь

  3. View model (вью модель) выступает связующим звеном между моделью и вью. Этот слой собирает данные из модели для показа, а также вызывает изменения модели.

  4. Binder (биндер). Слой, помогающий связать вью и вью модель в автоматическом режиме. Почему‑то совершенно игнорируется вики на русском языке, хотя, если верить английской версии сайта, это самый важный компонент паттерна, а сам паттерн можно называть model‑view‑binder.

View model

Создадим слой для вью модели. Она собирает данные и готовит их для вью, а также имеет возможность взаимодействовать с моделью по запросам вью

public interface IViewModel : IDisposable
{
}

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

View

Здесь все немножко сложнее. Для начала я введу дополнительное слово — экран. Экран — это цельный объект, в котором может быть много разных UI элементов. Например, окно прокачки персонажа — экран. Инвентарь — экран. Полоска прогресса производственного генератора — не экран, а вот окно с генератором и его настройками — экран. Экран сам руководит своими UI элементами: может их показывать, скрывать, менять у них настройки и тд. Связывать в мью моделью мы будем именно экраны.

При первом приближении должно получиться что‑то такое:

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

   public virtual void Dispose()
   {
   }
}

Сейчас нет никакой возможности связать вью и вью модель, а так же нет никакого бинда, хотя английская википедия настаивает, что это важно. Так что надо во view добавить немного функционала. После дополнения получится такой вариант:

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

Теперь есть связь экрана с его вью моделью. Можно независимо готовить данные для каждого экрана и настраивать сами экраны.

Вот пример экрана и его вью модели:

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

Осталось добавить точку взаимодействия с этой системой, которая будет открывать и закрывать экраны по запросу

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

Из этой точки можно смело расширять функционал, добавляя статические экраны, всплывающие подсказки, диалоговые окна.

Абстрактный пример, призванный показать зоны ответственности

Допустим, мы подходим к сундуку и хотим посмотреть, что у него внутри. Сундук в нашей системе считается частью модели. При клике на сундук он у UIManager вызывает метод BindAndShow, в который передает только что созданную вью модель для экрана. Теперь мы видим экран с содержимым сундука.

Также предположим, что можно прямо на этом экране смотреть описания предметов, наводя на них указатель. В этом случае элемент экрана будет обращаться к вью модели экрана сундука с просьбой показать «выпадашку». Вью модель экрана сундука обращается к UIManager и вызывает у него тот же самый метод, передавая ему данные предмета из сундука.

P.S

Идея реализации принадлежит не мне, поэтому будет правильно прикрепить ссылку на гит оригинала.

Теги:
Хабы:
Всего голосов 3: ↑1 и ↓2-1
Комментарии16

Публикации

Истории

Работа

Unity разработчик
15 вакансий

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн