Для начала разберемся, что такое 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)
SysWoW
23.12.2019 16:28Делал что-то схожее, только все три сущности наследовал от MonoBehaviour. В вашем примере модель не редактируется через Inspector, а это самая частая сущность для редактирования, что явно минус. В итоге я отказался от такого подхода, потому что уходило много времени на создание и поддержку классов, и ради элемента с одним параметром приходилось городить три класса. Я сделал вывод, что для домашних проектов это слишком избыточно, а для больших — вполне себе вариант.
vildafaust Автор
23.12.2019 16:32Доброго времени суток. Я прошу заметить, что в данной реализации все данные, что нужны Model — например возьмем машину. Много разных видов (например разные скорости). Простая реализация создаем CarModel и CarDiscription, который в свою очередь является [Serialize]. Тогда мы можем создать Scriptable object, в котором создадим List и уже в Inspector будем настраивать каждый дискрипшн, а Model будет хранить лишь нужный нам дескрипшн.
math_coder
23.12.2019 16:52Это больше MVP, чем MVC.
vildafaust Автор
23.12.2019 16:59Я бы не согласился. В данном Controller можно реализовать методы Enable и Disable, в которых можно привязывать и отвязывать события. Тем самым мы можем не уничтожать объект, а контролировать его. Нужен он нам сейчас или нет. Например реализовав pull manager для таких объектов, и отключаться в Disable у view Active тем самым скрывая, но не уничтожая
math_coder
24.12.2019 00:12+1Я ничего не понял из того что вы сейчас сказали, но просто сравните с каноническими диаграммами:
и
У вас не три равноправных компонента как в MVC, у вас два компонента + тонкая прослойка между ними, то есть MVP.
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) } }
vildafaust Автор
23.12.2019 17:10Намного проще — да. Но если тебе надо сохранять данные условно? Придется лезть в каждую View такую и сохранять параметры. Происходит перемешивание обязанностей + нельзя контролировать. Что если я хочу ее отключить? Кто будет управлять этим
sgrogov
24.12.2019 10:17сохранять данные условно
Что вы имеете ввиду под «условно»? Можете привести пример для ясности?
Tsvetik
Почему в PlayerController
не вызывается _model SetNewHealth()?
vildafaust Автор
Имхо конечно, но view лишь обновляет Health, логично что происходит Change, а не Set. за установку параметров отвечает лишь Model.
Tsvetik
Тогда, на мой взгляд, здесь что-то лишнее. Либо Controller либо Model. Думаю, их можно объединить в один класс.
vildafaust Автор
Тогда потеряется смысл вообще разделять классы. И мы получим спагети код
Tsvetik
Не вижу никакой полезной нагрузки в данной реализации controller. Он служит всего-лишь тонкой прослойкой между Model и View, ну еще пару обработчиков ивентов сопрягает.
Вот кусок вашего кода
На мой взгляд, здесь лишнее звено.
Должно быть сразу
Зачем создавать лишнюю сущность в виде Controller.ChangeHealth?
koeshiro
По идеи model должен только хранить, максимум валидировать данные, а основная логика работы должна быть в controller. Однако пример в данной статье сильно упрощён, и в конкретно данном примере в controller весь смысл только в демонстрации mvc.