Вариант Mvvm в Unity

от автора

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

Дисклеймер

Мне справедливо заметили, что надо указать уровень, на который рассчитана статья. У меня чуть больше 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

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


ссылка на оригинал статьи https://habr.com/ru/articles/725444/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *