MVC в Unity? Надо ли? Если да, то как?

Рейтинг: 2Ответов: 2Опубликовано: 16.01.2015

Для начала скажу, что я большой сторонник MVC и вообще не признаю (не вижу?) других вариантов организовать код. Уже имел успешный опыт применения этой идеи в PHP, и мне там это очень понравилось – наверное, в этом причина. Сейчас знакомлюсь с Unity, C# и разработкой игр в общем. Только вот что-то никак не пойму, как применить эту идею в Unity. Я успел хорошенько загуглить про MVC в Unity и понял, что народ не приветствует эту идею (и на то есть свои причины), но, тем не менее, это возможно. Вопрос в том... Что будет контроллерами? Где будет находится модель? Как работать с View? И как всё это связать?

P.S. Если вы сведущи в данном вопросе и считаете по-другому, то я не против, если вы меня таки переубедите в необходимости MVC при разработке игры на Unity, но постарайтесь, пожалуйста, объяснить свою точку зрения. :)

P.P.S. Это мой первый вопрос тут. До этого частенько тут бывал, находил интересующие вопросы и ответы на них. Теперь вот решил сам задать. =) Надеюсь, примите добролюбиво. :p

Ответы

▲ 6Принят

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

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

В сцене создаем пользовательский интерфейс, затем объект-менеджер, который будет отвечать за интеграцию интерфейса с логикой. Назовем его, например, GUIImpl. Класс должен быть наследником MonoBehaviour для того, чтобы мы могли работать с ним в редакторе. В приведенном ниже примере я использую NGUI для самого интерфейса.

Следующим шагом необходимо создать класс-описание графического элемента, например, панели. Я делаю этот класс внутренним по отношению к GUIImpl, но это на любителя.

[System.Serializable]
public class MenuPanel : GUIMenuBase {
    public GameObject SettingsButton;
    public GameObject CancelButton;
    public GameObject ExitButton;   
    public GameObject ResetButton;

    public override void Init() {
        associateButton(NewGameButton, new NewGameCommand(this));
        associateButton(SettingsButton, new SettingsCommand(this));
        associateButton(CancelButton, new CancelCommand(this));
        associateButton(ExitButton, new ExitCommand(this));
    }
    public override void Show() {
        base.Show();
    }
    public override void Hide() {
        base.Hide();
    }
}

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

[System.Serializable]
public abstract class GUIMenuBase : IGUIMenu {  
    public GameObject Instance; 
    protected Dictionary<GameObject, ICommand> associations = new Dictionary<GameObject, ICommand>();
    protected List<GameObject> activeButtons;

    public GUIMenuBase() {
    }
    public abstract void Init();
    public GameObject GetGOInstance() {
        return Instance;
    }
    public virtual void Show() {
        GUILogic.ShowMenu(this);
    }
    public virtual void Hide() {
        GUILogic.HideMenu(this);
    }
    public virtual bool CanShow(GameObject guiItem) {
        return true;
    }
    public virtual bool IsShown(GameObject guiItem) {
        return true;
    }
    public virtual bool IsAvalibale(GameObject guiItem) {
        return true;
    }
    public virtual bool CanButtonClick(GameObject button) {
        if (!associations.ContainsKey(button))
            return false;

        return associations[button].CanExecute(button);
    }
    public virtual void ButtonClick(GameObject button) {
        if (!associations.ContainsKey(button))
            return;

        if (associations[button].CanExecute(button))        
            associations[button].Execute(button);
    }
    public virtual void OnSourceChanged() {
        foreach(var item in associations) {
            UIButton buttonScript = item.Key.GetComponent<UIButton>();
            if (buttonScript != null) {
                buttonScript.isEnabled = item.Value.CanExecute(item.Key);
                buttonScript.enabled = true;
            }
        }
    }
    protected virtual void adjustPanelButtons() {
    }
    protected virtual void associateButton(GameObject guiItem, ICommand command) {
        associate(guiItem, command);
        UIEventListener.Get(guiItem).onClick += ButtonClick;
    }
    protected virtual void associate(GameObject guiItem, ICommand command) {
        associations.Add(guiItem, command);
    }
    protected virtual void removeAssociation(GameObject guiItem) {      
        if (associations.ContainsKey(guiItem))
            associations.Remove(guiItem);
    }
}

IGUIMenu

public interface IGUIMenu {
    void Show();
    void Hide();
    bool CanShow(GameObject guiItem);
    bool IsShown(GameObject guiItem);
    bool IsAvalibale(GameObject guiItem);
    bool CanButtonClick(GameObject button);
    void ButtonClick(GameObject button);
    void OnSourceChanged();
}

IGUIMenu содержит методы для работы с различными элементами. Я привел интерфейс работы только с кнопками. Далее реализуем действие, по клику на кнопки.

public class NewGameCommand : CommandBase {
    public  NewGameCommand(GUIMenuBase aPanel) 
        : base(aPanel) {}   
    public override void Execute(GameObject button) {
        if (!CanExecute (button))
            return;
        DatabaseManagerFactory.GetDefaultDatabaseManager().Reset(); 
        Application.LoadLevel(Application.loadedLevel);
    }               
}

public abstract class CommandBase : ICommand {
    protected GUIMenuBase panel;
    public CommandBase(GUIMenuBase aPanel) {
        panel = aPanel;
    }
    public virtual bool CanExecute(GameObject button) {
        return true;
    }
    public virtual void Execute(GameObject button) {
    }               
}

public interface ICommand {     
    void Execute        (GameObject context);
    bool CanExecute     (GameObject context);
}

В принципе это все. Остается лишь реализовать класс GUILogic, которой содержит логику работы панелей. Затем открываем редактор, находим наш объект для управления пользовательским интерфейсом и назначаем элементы различных панелей соответствующим полям. Таким образом, для каждой панели должен быть свой класс, который содержит все необходимые поля. Пожалуйста, обратите внимание на поле public GameObject Instance; класса GUIMenuBase. Instance - это объект самой панели. Для большинства операций с панелью нам понадобиться ее корневой элемент.

▲ 1

MVC в Юнити однозначно не нужен, как и DI. Он в принципе под это не заточен. В Юнити нет моделей, контроллеров, вьюшек, точек входа, контекстов... В Юнити есть GameObject, на который добавляются Components. Мы можем обратиться к любому объекту по ссылке на GameObject и получить с него любой компонент через GetComponent<>

С большой натяжкой можно сказать, что View (отображение в сцене) -- это GameObject с компонентами MeshFilter+MeshRenderer, а Controller (поведение) и Model (данные) - это скрипты, отвечающие за его логику. Ну ещё в качестве Model может выступать ScriptableObject (специальный тип ассета для хранения данных).

Более того, в Юнити даже ООП толком не используется, так как вы можете создавать префабы / варианты префабов, и комбинируя на них разные скрипты, меняя их значения в инспекторе, добиваться разного поведения безо всякого наследования и полиморфизма. Наследование тяжело поддерживать, а провести декомпозицию на компоненты, и поменять один компонент на другой -- не проблема.

Умоляю, не используйте бездумно "паттерны" ради архитектурных изысков, потому что потом приходится копаться в чужом проекте и пытаться понять, "что хотел сказать автор". Я попадал на проекты, где пытались применять Zenject, Entitas или MVC, и это был кошмар для разработчика.

Юнити не следует паттернам не потому что "тяп-ляп и в продакшн", как несправедливо заметил кто-то выше, а потому что у него свои собственные удобные инструменты и богатое API, не такое как в веб-разработке или нативной разработке. Потрудитесь их изучить, прежде чем садиться писать код.

Если хотите хороший пример, как правильно организовывать проект в Юнити -- посмотрите, например, замечательный 3D Game Kit. Никаких MVC, никаких инъекций зависимостей, но код чистый, проект легок для понимания, компоненты взаимозаменяемы, и на его основе легко что-то свое собрать.

P.S. Хотел даже на Хабр тиснуть статью на эту тему, но боюсь, закидают фекалиями местные "профи".