Для начала разберемся, что такое MVC (Model View Controller)


Зачем же он нужен? Самый простой ответ — для постройки удобной и расширяемой архитектуры.


Стоит разобраться зачем нужно дробить взаимодействие на три разных класса. Во-первых разделение обязанностей, следую принципам SOLID. Во-вторых для простого взаимодействия с серверами для синхронизации данных.


Разберемся поподробнее об обязанностях на примере


PlayerView.cs — представляет собой в частности MonoBehaviour класс


using UnityEngine;

public class PlayerView : MonoBehaviour 
{

}

PlayerModel.cs — представляет собой класс данных


public class PlayerModel
{

}

PlayerController.cs — представляет собой класс связности между model и view


public class PlayerController
{
   private PlayerView _playerView;
   private PlayerModel _playerModel

   public PlayerController(PlayerView view, PlayerModel model)
   {
      _playerView = view;
      _playerModel = model;
   }
}

Вот и весь паттерн.


А теперь разберемся зачем все это нужно.


PlayerView.cs добавляем, как компонент на наш Prefab игрока.


Остальные классы создаются в каком-либо управляющем классе. Создадим для примера управляющий класс.


Создадим пустой объект на сцене и навесим компонент GameManager


GameManager.cs


public class GameManager : MonoBehaviour
{
    public GameObject _playerPrefab;
    private PlayerController _playerController;
    private PlayerModel _playerModel;

    public void Start(){
        _playerModel = new PlayerModel();

        var playerObject = Instantiate(_playerPrefab, Vector3.zero,Quaternion.identity);
        var playerView = playerObject.GetComponent<Playerview>();

        _playerController = new PlayerController(playerView, _playermodel)
    }
}

Отлично теперь на старте игры у нас будет создаваться Player. Но что дальше-то?


На самом деле далее это полет фантазии механик. Ну давайте для примера расширим Player model добавив здоровье.


PlayerModel.cs


public class PlayerModel
{
      public event Action Death;
      public event Action<float> ChangedHealth;

      private float _maxHp = 100;
      private float _currentHp;

      public PlayerModel(){
          _currentHp = maxHp;
      }

      public void SetNewHealth(float damage)
      {
         _currentHp -= damage;
         if(_currentHp > 0)
              ChangedHealth?.Invoke(_currentHp);
         else
              Death?.Invoke();
      }
}

Теперь у нашего игрока появилось Health и два event на изменение здоровья и смерть. То есть Model — представляет сосредоточение всех данных игрока и решает что нужно делать при изменении каких-либо данных.


Теперь расширим Playercontroller, чтобы мы могли взаимодействовать с данными


PlayerController.cs


public class PlayerController
{
   private PlayerView _playerView;
   private PlayerModel _playerModel

   public PlayerController(PlayerView view, PlayerModel model)
   {
      _playerView = view;
      _playerModel = model;
   }

   public void Enable()
   {
       _playerModel.Death += Death;
       _playerModel.ChangedHealth += ChangeHealth;
   }

   private void ChangeHealth(float  health){
       _playerView.Changehealth(health);
   }

   private void Death(){
       _playerView.Death();
       Disable();
   }

   public void Disable()
   {
       _playerModel.Death -= Death;
       _playerModel.ChangedHealth -= ChangeHealth;
   }
}

Следовательно давайте расширим и PlayerView


PlayerView.cs


public class PlayerView : MonoBehaviour 
{
    public void Changehealth(float health)
    {
        //Уже любыми средствами отрисовывать визуальную часть
        //это может как быть slideBar или просто Text 
    }

    public void Death()
    {
        //Например можете проиграть анимацию смерти
    }
}

Следовательно мы получили крайне удобную архитектуру создание юнита (или любого другого объекта), который может расширяться любыми механиками. Нужна мана? Или передвижение? Добавьте в модель float mana, Vector3 position.


Какой должен быть вывод? Не мешать данные View (MonoBehaviour) с простыми просчетами.
Контроллер в нашем случае находится над уровнем PlayerView и PlayerModel и следит меняется ли что-либо там и реагирует на изменения. При том PlayerController может следить так же за вьюшными ивенты, например коллезии или триггеры.

Комментарии (14)


  1. Tsvetik
    23.12.2019 14:27
    +1

    Почему в PlayerController

    private void ChangeHealth(float health){
    _playerView.Changehealth(health);
    }

    не вызывается _model SetNewHealth()?


    1. vildafaust Автор
      23.12.2019 16:39

      Имхо конечно, но view лишь обновляет Health, логично что происходит Change, а не Set. за установку параметров отвечает лишь Model.


      1. Tsvetik
        23.12.2019 19:47

        Тогда, на мой взгляд, здесь что-то лишнее. Либо Controller либо Model. Думаю, их можно объединить в один класс.


        1. vildafaust Автор
          23.12.2019 19:58

          Тогда потеряется смысл вообще разделять классы. И мы получим спагети код


          1. Tsvetik
            23.12.2019 20:32

            Не вижу никакой полезной нагрузки в данной реализации controller. Он служит всего-лишь тонкой прослойкой между Model и View, ну еще пару обработчиков ивентов сопрягает.
            Вот кусок вашего кода


            _playerModel.ChangedHealth += ChangeHealth;

            private void ChangeHealth(float health){
            _playerView.Changehealth(health);
            }

            На мой взгляд, здесь лишнее звено.
            Должно быть сразу
            _playerModel.ChangedHealth += _playerView.Changehealth


            Зачем создавать лишнюю сущность в виде Controller.ChangeHealth?


            1. koeshiro
              24.12.2019 12:39

              По идеи model должен только хранить, максимум валидировать данные, а основная логика работы должна быть в controller. Однако пример в данной статье сильно упрощён, и в конкретно данном примере в controller весь смысл только в демонстрации mvc.


  1. SysWoW
    23.12.2019 16:28

    Делал что-то схожее, только все три сущности наследовал от MonoBehaviour. В вашем примере модель не редактируется через Inspector, а это самая частая сущность для редактирования, что явно минус. В итоге я отказался от такого подхода, потому что уходило много времени на создание и поддержку классов, и ради элемента с одним параметром приходилось городить три класса. Я сделал вывод, что для домашних проектов это слишком избыточно, а для больших — вполне себе вариант.


    1. vildafaust Автор
      23.12.2019 16:32

      Доброго времени суток. Я прошу заметить, что в данной реализации все данные, что нужны Model — например возьмем машину. Много разных видов (например разные скорости). Простая реализация создаем CarModel и CarDiscription, который в свою очередь является [Serialize]. Тогда мы можем создать Scriptable object, в котором создадим List и уже в Inspector будем настраивать каждый дискрипшн, а Model будет хранить лишь нужный нам дескрипшн.


  1. math_coder
    23.12.2019 16:52

    Это больше MVP, чем MVC.


    1. vildafaust Автор
      23.12.2019 16:59

      Я бы не согласился. В данном Controller можно реализовать методы Enable и Disable, в которых можно привязывать и отвязывать события. Тем самым мы можем не уничтожать объект, а контролировать его. Нужен он нам сейчас или нет. Например реализовав pull manager для таких объектов, и отключаться в Disable у view Active тем самым скрывая, но не уничтожая


      1. math_coder
        24.12.2019 00:12
        +1

        Я ничего не понял из того что вы сейчас сказали, но просто сравните с каноническими диаграммами:
        MVP
        и
        MVC


        У вас не три равноправных компонента как в MVC, у вас два компонента + тонкая прослойка между ними, то есть MVP.


  1. sgrogov
    23.12.2019 17:08

    Ничего не имею против MVC, однако, на мой взгляд, намного проще выносить отдельные способности (здоровье, прыжки, ношение оружия и пр.) в отдельные скрипты, что-то вроде миксинов. И из таких компонентов собирать разных юнитов.

    Пример компонента здоровья
    [RequireComponent(typeof(Unit))]
    public class Health : MonoBehaviour
    {
      public float max;
      float current;
    
      public float regenerationDuration;
      public float regenerationDelay;
      float timeToRegenerate;
    
      Armor armor;
    
      void Start() {
        armor = GetComponent<Armor>();
        current = max;
      }
    
      void FixedUpdate() {
        timeToRegenerate = Mathf.Max(0f, timeToRegenerate - Time.fixedDeltaTime);
        if (timeToRegenerate == 0f) Regenerate(Time.fixedDeltaTime);
      }
    
      void Update() {
        if (current <= 0) GetComponent<Unit>().Die();
      }
    
      public void GetDamage(Damage damage) {
        float armorResistanceFactor = 0f;
        if (armor != null) {
          armorResistanceFactor = armor.GetResistanceFactor(damage.type);
        }
        current -= damage * (1 - armorResistanceFactor);
        timeToRegenerate = regenerationDelay;
      }
    
      void Regenerate(float deltaTime) {
        health += max * (ms / regenerationDuration)
      }
    }
    


    1. vildafaust Автор
      23.12.2019 17:10

      Намного проще — да. Но если тебе надо сохранять данные условно? Придется лезть в каждую View такую и сохранять параметры. Происходит перемешивание обязанностей + нельзя контролировать. Что если я хочу ее отключить? Кто будет управлять этим


      1. sgrogov
        24.12.2019 10:17

        сохранять данные условно

        Что вы имеете ввиду под «условно»? Можете привести пример для ясности?