» » » Unity и MVC: как прокачать разработку игры

 

Unity и MVC: как прокачать разработку игры

Автор: admin от 17-04-2016, 19:11, посмотрело: 350

[quote]От переводчика
Привет, Хабр!

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

Перевод публикую с разрешения сайта Toptal, где выложен оригинал автора Eduardo Dias da Costa.[/quote]

Обычно программисты знакомятся с профессией, начиная с Hello World. Затем ставят всё большие и большие цели и каждая новая задача приводит к важному уроку: чем больше проект, тем запутаннее код.


Unity и MVC: как прокачать разработку игры

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


Поэтому существуют шаблоны проектирования; они — сборники правил для стандартизированного структурирования проекта, которые помогают разделить и организовать большую кодовую базу, и упростить работу с незнакомым кодом.


Unity и MVC: как прокачать разработку игры

Эти правила, когда их придерживаются все разработчики проекта, облегчают поддержку, навигацию по старому коду и написание нового. Меньше времени уходит на планирование подхода к разработке. Но так как проблемы от случая к случаю разнятся, шаблоны — не серебряная пуля. Нужно тщательно рассматривать слабые и сильные стороны каждого, прежде чем выбрать подходящий для конкретной ситуации.


В этом обучающем материале я расскажу о своём опыте работы с популярным игровым движком Unity3D и применении шаблона Модель-Представление-Контроллер (Model-View-Controller, MVC). За семь лет работы и борьбы со спагетти-кодом в игровых проектах, я добился отличной структуры кода и скорости разработки при его использовании.


Начну с разъяснения о базовой архитектуре Unity: шаблоне Сущность-Компонент (Entity-Component, EC), а затем расскажу, как поверх него строится MVC. И приведу в пример маленький проект-макет.


Мотивация


Чтобы адаптировать существующие шаблоны проектирования под конкретную задачу, программисты вынуждены изменять их. Эта свобода в программировании — доказательство, что мы не нашли единственно верной архитектуры софта. Эта статья тоже не подразумевается, как решение всех проблем. Она показывает возможности двух хорошо известных шаблонов проектирования: EC и MVC.


Шаблон Сущность-Компонент


Сущность-Компонент (EC) — шаблон проектирования, где первым делом определяется иерархия элементов, из которых состоит приложение (Сущности), а затем указываются логика и данные для каждого из них (Компоненты). В «программерских» терминах, Сущность может являться объектом с массивом из нуля и более Компонентов. Опишем её так: some-entity [component0, component1, ...]


Пример простого дерева EC:


- app [Application]
   - game [Game]
      - player [KeyboardInput, Renderer]
      - enemies
         - spider [SpiderAI, Renderer]
         - ogre [OgreAI, Renderer]
      - ui [UI]
         - hud [HUD, MouseInput, Renderer]
         - pause-menu [PauseMenu, MouseInput, Renderer]
         - victory-modal [VictoryModal, MouseInput, Renderer]
         - defeat-modal [DefeatModal, MouseInput, Renderer]

EC хороший шаблон для нивелирования проблем множественного наследования, когда запутанная структура классов может порождать проблемы вроде проблемы бриллианта (the diamond problem): класс D, наследуемых от B и C с общим классом A, может содержать конфликты из-за разного переопределения возможностей A классами B и C.


Unity и MVC: как прокачать разработку игры

Подобные проблемы часто встречаются при активном использовании наследования.


Если разбить задачи и обработчики данных на небольшие Компоненты, они могут присоединяться (attach) к Сущностям и переиспользоваться без множественного наследования, которого нет в C# и javascript, основных языка программирования Unity.


Где Сущность-Компонент не оправдывает ожиданий


Находясь уровнем выше вот ссылка на мой MVC фреймворк Unity MVC. Он структурирован под AMVCC, поэтому там ты найдёшь базовые классы, необходимые в большинстве приложений.[/quote]

Теперь взглянем на структуру скриптов «10 Bounces».


Для незнакомых с устройством Unity, я кратко поясню, как взаимодействуют GameObjects. «Компоненты» из шаблона Сущность-Компонент представлены классом MonoBehaviour. Чтобы он стал доступен во время работы приложения, разработчик должен в редакторе перетащить (drag-n-drop) исходный файл на GameObject (который является «Сущностью» в шаблоне EC) или использовать команду AddComponent(). После этого скрипт будет инстанцирован и готов к использованию.


Объявим два класса.


Application («A» в AMVCC) — основной класс, у которого будет только один инстанс, содержащий ссылки на все инстанцированные в игре элементы. Внутри объявим три публичные переменные: model, view и controller, которые обеспечат доступ к корневым объектам MVC.


Element — вспомогательный базовый класс, дающий дочерним инстансам MVC доступ к Application.


Помни, что оба класса — наследники MonoBehaviour. Они — «Компоненты», прикреплённые к GameObject «Сущностям».


// BounceApplication.cs

// Base class for all elements in this application.
public class BounceElement : MonoBehaviour
{
   // Gives access to the application and all instances.
   public BounceApplication app { get { return GameObject.FindObjectOfType<BounceApplication>(); }}
}

// 10 Bounces Entry Point.
public class BounceApplication : MonoBehaviour
{
   // Reference to the root instances of the MVC.
   public BounceModel model;
   public BounceView view;
   public BounceController controller;

   // Init things here
   void Start() { }
}

Отнаследуем от BounceElement базовые классы MVC. Обычно BounceModel, BounceView и BounceController выступают контейнерами специализированных MVC объектов, но в упрощённом примере только у Представления будет вложенная структура. Модели и Контроллеру хватит по одному скрипту.


// BounceModel.cs

// Contains all data related to the app.
public class BounceModel : BounceElement
{
   // Data
   public int bounces;  
   public int winCondition;
}

// BounceView .cs

// Contains all views related to the app.
public class BounceView : BounceElement
{
   // Reference to the ball
   public BallView ball;
}

// BallView.cs

// Describes the Ball view and its features.
public class BallView : BounceElement
{
   // Only this is necessary. Physics is doing the rest of work.
   // Callback called upon collision.
   void OnCollisionEnter() { app.controller.OnBallGroundHit(); }
}

// BounceController.cs

// Controls the app workflow.
public class BounceController : BounceElement
{
   // Handles the ball hit event
   public void OnBallGroundHit()
   {
      app.model.bounces++;
      Debug.Log(“Bounce ”+app.model.bounce);
      if(app.model.bounces >= app.model.winCondition)
      {
         app.view.ball.enabled = false;
         app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball
         OnGameComplete();
      } 
   }

   // Handles the win condition
   public void OnGameComplete() { Debug.Log(“Victory!!”); }
}

Все скрипты созданы, теперь можно закрепить их за GameObjects и сконфигурировать.


Иерархия будет такой:


- application [BounceApplication]
    - model [BounceModel]
    - controller [BounceController]
    - view [BounceView]
        - ...
        - ball [BallView]
        - ...

Рассмотрим, как это выглядит в редакторе Unity на примере BounceModel:
Unity и MVC: как прокачать разработку игры
BounceModel с полями bounces и winCondition.


После установки скриптов и запуска игры, вывод в Консоль будет таким:
Unity и MVC: как прокачать разработку игры


Уведомления (notifications)


Когда шар ударяется о землю, его представление вызывает метод app.controller.OnBallGroundHit(). Нельзя сказать, что это «неправильный» способ слать уведомления в приложении, но по моему опыту куда удобнее использовать простую систему нотификаций, реализованной в классе Application.


Обновим BounceApplication:


// BounceApplication.cs

class BounceApplication 
{
   // Iterates all Controllers and delegates the notification data
   // This method can easily be found because every class is “BounceElement” and has an “app” 
   // instance.
   public void Notify(string p_event_path, Object p_target, params object[] p_data)
   {
      BounceController[] controller_list = GetAllControllers();
      foreach(BounceController c in controller_list)
      {
         c.OnNotification(p_event_path,p_target,p_data);
      }
   }

   // Fetches all scene Controllers.
   public BounceController[] GetAllControllers() { /* ... */ }
}

Теперь понадобится новый скрипт, где разработчики будут указывать имена событий, уведомления о которых могут прийти:


// BounceNotifications.cs

// This class will give static access to the events strings.
class BounceNotification
{
   static public string BallHitGround = “ball.hit.ground”;
   static public string GameComplete  = “game.complete”;
   /* ...  */
   static public string GameStart     = “game.start”;
   static public string SceneLoad     = “scene.load”;
   /* ... */
}

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


Теперь адаптируем BallView и BounceController для работы с новой системой.


// BallView.cs

// Describes the Ball view and its features.
public class BallView : BounceElement
{
   // Only this is necessary. Physics is doing the rest of work.
   // Callback called upon collision.
   void OnCollisionEnter() { app.Notify(BounceNotification.BallHitGround,this); }
}

// BounceController.cs

// Controls the app workflow.
public class BounceController : BounceElement
{
   // Handles the ball hit event
   public void OnNotification(string p_event_path,Object p_target,params object[] p_data)
   {
      switch(p_event_path)
      {
         case BounceNotification.BallHitGround:
            app.model.bounces++;
            Debug.Log(“Bounce ”+app.model.bounce);
            if(app.model.bounces >= app.model.winCondition)
            {
               app.view.ball.enabled = false;
               app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball
               // Notify itself and other controllers possibly interested in the event
               app.Notify(BounceNotification.GameComplete,this);            
            }
         break;

         case BounceNotification.GameComplete:
            Debug.Log(“Victory!!”);
         break;
      } 
   }
}

В проектах побольше может быть много уведомлений. Чтобы избавиться от огромных switch-case, целесообразно создавать специализированные контроллеры и обрабатывать в них разные группы уведомлений.


AMVCC в реальном мире


«10 Bounces» показывает простейший вариант использования шаблона AMVCC. Чтобы использовать его на практике, придётся оттачивать мышление в рамках трёх категорий MVC и учиться визуализировать сущности в виде упорядоченной иерархии.


В больших проектах разработчики сталкиваются с более сложными сценариями и часто сомневаются, является та или иная сущность Представлением или Контроллером, а может стоит разделить класс на части.


Практические правила (от Эдуардо)


Это не «Универсальный гид по организации MVC», а набор простых правил, которые помогают мне разделить Модель, Представление и Контроллер.


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


Разделение классов


Модели



  • Содержат основные данные приложения вроде здоровья или запаса патронов игрока.

  • Сериализуются, десериализуются и/или конвертируются между типами.

  • Загружают/сохраняют данные (локально или по Сети).

  • Сообщают Контроллерам о прогрессе операции.

  • Хранят состояние конечного автомата игры.

  • Никогда не вызывают Представления.


Представления



  • Могут получать данные из Моделей для отображения текущего состояния игры. Например, метод Представления player.Run() может использовать model.speed для наглядного отображения способностей игрока.

  • Никогда не изменяет Модели.

  • Строго следует назначению класса. Например:

    • PlayerView никогда не детектирует ввод или изменение состояния игры.

    • Представление работает как чёрный ящик с интерфейсом и оповещает о важных событиях.

    • Не хранит важных данных вроде скорости, здоровья, жизней и так далее.



Контроллеры



  • Не хранят основные данные игры.

  • Могут фильтровать уведомления от Представлений.

  • Обновляют и используют данные Моделей.

  • Управляют действиями на сцене Unity.


Иерархия классов


Я понимаю, какие классы нуждаются в разделении, когда в переменных появляется слишком много префиксов или явно прослеживается возможность ветвления (вроде классов Player в MMO или видов Gun в FPS). Но для полноты статьи я не мог обойти этот момент стороной.


Например, Модель с данными игроков имеет множество переменных playerDataA, playerDataB ,… или Контроллер, обрабатывающий уведомления игрока, — методы OnPlayerDidA, OnPlayerDidB,… Мы хотим уменьшить количество кода и избавиться от префиксов player и OnPlayer. Так как Модель проще для понимания, я продемонстрирую это на её примере.


Я обычно начинаю с единственной Модели, которая содержит все данные игры:


// Model.cs

class Model
{
   public float playerHealth;
   public int playerLives;

   public GameObject playerGunPrefabA;
   public int playerGunAmmoA;

   public GameObject playerGunPrefabB;
   public int playerGunAmmoB;

   // Ops Gun[C D E ...] will appear...
   /* ... */

   public float gameSpeed;
   public int gameLevel;
}

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


// Model.cs

class Model
{
   public PlayerModel player;  // Container of the Player data.
   public GameModel game;      // Container of the Game data.
}

// GameModel.cs

class GameModel
{
   public float speed;         // Game running speed (influencing the difficulty)
   public int level;           // Current game level/stage loaded
}

// PlayerModel.cs

class PlayerModel
{
   public float health;        // Player health from 0.0 to 1.0.
   public int lives;           // Player “retry” count after he dies.
   public GunModel[] guns;     // Now a Player can have an array of guns to switch ingame.
}

// GunModel.cs

class GunModel
{
   public GunType type;        // Enumeration of Gun types.
   public GameObject prefab;   // Template of the 3D Asset of the weapon.
   public int ammo;            // Current number of bullets
   public int clips;           // Number of reloads possible
}

При такой организации классов, за раз разработчик рассматривает одну логическую единицу, что упрощает понимание кода. Давай представим шутер от первого лица с множеством наименований оружия. Данные в классе GunModel, позволяют создать набор префабов для каждого вида оружия и затем использовать их в игре. Префаб — заранее подготовленный GameObject, готовый к быстрому копированию и переиспользованию.


А если информация об оружии содержится в одном классе в переменных вроде gun0Ammo, gun1Ammo, gun0Clips и т.д., тогда, столкнувшись с необходимостью сохранить данные Gun, понадобится сохранить Model целиком, включая нежелательные данные о Player. В этом случае очевидно, что будет лучше создать новый класс GunModel.
Unity и MVC: как прокачать разработку игры
Улучшение иерархии классов


Как всегда, есть две стороны медали. Иногда излишнее разделение по категориям усложняет код. Только опыт поможет наилучшим образом структурировать MVC в твоём проекте.


[quote]Открыт новый навык геймдева: Unity с MVC.[/quote]

Заключение


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


Также очень рекомендую изучить другие шаблоны и поискать полезные. Начать можно со статьи в Википедии, там приведён отличный список шаблонов и их характеристик.


Если тебе понравился шаблон AMVCC, и ты хочешь протестировать его, не забудь попробовать мою библиотеку Unity MVC, которая содержит все основные классы необходимые для приложения в стиле AMVCC.



Источник: Хабрахабр

Категория: Программирование » Game Development

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *