50 советов по работе с Unity

Unity LogoНаткнулся на англоязычную статью 2012-го года, от немецкого автора, фрилансера-геймдевелопера, разработчика инструментов для Юнити – Herman Tulleken. В статье, сведены 50 советов по работе в Юнити. Советы основаны на опыте работы автора в проектах, с командами от 3 до 20 человек. Автор предупреждает, что не все из них, могут быть применены в каждом проекте; многие советы – дело вкуса. От себя добавлю, что Юнити эти 3 года не стоял на месте, и возможно некоторые советы, могут быть уже не актуальны. Перевода я не нашел. Подумал, а почему бы мне ее не перевести ее, для своего сайта.

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


 

Процесс

1. Избегайте разветвленных ассетов. Всегда, должна быть только одна версия, любого ассета. Если вам абсолютно необходимо ответвить новую версию префаба, сцены или сетки, следуйте процессу, это сделает ясным, какая версия является правильной. “Неправильная” версия должна иметь броское имя, например, использовать двойное подчеркивание префикс:  __MainScene_Backup . Ветвление префабов требует определенного процесса, чтобы сделать его безопасным (см в разделе Префабы).

2. Каждый член команды, должен иметь второй экземпляр проекта для тестирования, если вы используете систему контроля версий. Все изменения должны делаться, и тестироваться в нём, и затем вносится в “чистовую” версию. Никто не должен делать какие-либо изменения в их “чистовых” экземплярах. Это особенно полезно, чтобы отловить недостающие ассеты.

3. Рассмотрите возможность использования внешних инструментов для редактирования уровня. Юнити не лучший инструмент для этого. Например, мы использовали TuDee для создания уровней на основе 3D-тайлов, где есть такие полезные функции, как: привязка к сетке, 2D вид, быстрый выбор тайлов. Простое инстанцирование префабов из XML-файлов.

4. Рассмотрите сохранение уровней в XML, а не в сценах. Это прекрасный метод:

  • это избавляет от необходимости настраивать каждую сцену;
  • это делает загрузку намного быстрее (если большинство объектов являются общими для всех сцен).
  • это делает легче объединение сцен.
  • это делает более легким отслеживание данных уровня.

Вы по-прежнему сможете использовать Unity в качестве редактора уровня (хотя и не должны). Вам нужно будет написать код для сериализации и десериализации данных,  для загрузки уровня в редакторе и во время выполнения, а также для сохранения уровня в редакторе. Вам также может понадобиться, имитировать ID систему Unity, для поддержания ссылок между объектами.

5. Рассмотрите написание пользовательского инспектора. Написать пользовательский инспектор довольно просто, но система Unity имеет много недостатков:

  • Он не позволяет воспользоваться преимуществами наследования.
  • Он не позволяет определить компоненты инспектора на уровне поля, только на уровне класса. Например, если каждый игровой объект, имеет поле типа  SomeCoolType , который вы хотите отобразить в инспекторе иначе, вы должны написать инспектор для каждого класса.

Вы можете решить эти вопросы, повторной реализацией системы инспекторов. Используя несколько приемов рефлексии, это не так сложно, как кажется. Детали, представлены в конце статьи.

Организация сцены

6. Используйте именованные пустые объекты, как папки. Тщательно организовывайте свои сцены, чтоб облегчить поиск объектов в них.

7. Помещайте при редактировании свои префабы и папки (пустышки) в нулевые координаты (0, 0, 0). Если префаб/пустышка редактируется не для позиционирования, он должен быть в нуле. Таким образом, уменьшается вероятность возникновения проблем с локальными и мировыми системами координат, и код как правило проще.

8. Минимизируйте использование смещений для компонентов GUI. Смещения должны использоваться для компоновки компонентов, только относительно их родительского контейнера; они не должны полагаться на позиционирование своих прародителей. Смещения не должны отменять друг друга, чтобы правильно отображаться. Это в основном для предотвращения такого рода вещей:

Родительский контейнер произвольно размещен в (100, -50). Ребенок, предназначенный для позиционирования в точке (10, 10), помещен в (90, 60) [относительно родительского].

Эта ошибка распространена, когда контейнер невидим, или не имеет визуального представления вообще.

9. Размещайте вашу землю на y=0. Это облегчает задачу размещение объектов на земле.

10. Делайте игру, готовую к запуску в каждой сцене. Это значительно сокращает этап тестирования. Для этого нужно сделать 2 вещи:

Во-первых, предоставить в сцене все данные, требующиеся от предшествующих сцен, если они не доступны.

Во-вторых, создайте объекты, которые должны сохраняться между загрузками сцены, следующим образом:

myObject = FindMyObjectInScene();

if (myObjet == null)
{
    myObject = SpawnMyObject();
}

Арт

11. Размещайте пивот поинты (точки вращения) в основании объектов и персонажей, а не в центре. Это упрощает установку объектов, точно на поверхность. Так же это позволяет работать с 3d, как с 2d в игровой логике, AI, и даже в физике, когда это необходимо.

12. Ориентируйте все ваши объекты, включая персонажей, “лицом” вдоль одной и той же оси (положительное или отрицательное направление оси Z). Многие алгоритмы упрощаются, когда все объекты лицом направлены в одном направлении. Тут имеются ввиду, локальные оси самого объекта, а не мировые.

13. Позаботьтесь о правильном масштабе своих ассетов, до импорта в движок. При масштабе (1, 1, 1) они должны иметь задуманные размеры. Для сравнения, можно использовать ссылку на объект куба в юнити, находящийся в стандартных ассетах. Выберите подходящее соотношения к мировым единицам, и придерживайтесь его.

14. Чтобы, было легче размещать и ориентировать GUI и партиклы, используйте two-poly plane (плоскость из двух треугольников). Сориентируйте плоскость лицом по оси Z (см. пункт 12) и вкладывайте в нее GUI или партиклы.

15. Создайте и применяйте тестовые арты:

  • Меченые квадраты для скайбоксов.
  • Текстуру с сеткой.
  • Различные “плоские” цвета для шейдерного тестирования: белый; черный; 50% серого; красный; зеленый; синий; пурпурный; желтый; голубой.
  • Градиенты для шейдерного тестирования: от черного к белом; от красного к зеленому; от красного к синему; от зеленого к синему.
  • Текстуру “чекер” (черно-белые квадраты, по типу шахматной доски).
  • Сглаженные и жесткие карты нормалей.
  • Готовое и настроенное освещение, для быстрого создания тестовых сцен.

Префабы

16. Используйте префабы абсолютно для всего. Единственные объекты, которые не должны быть префабом, это папки. Даже уникальные объекты, которые будут использованы только раз, должны быть префабом. Это облегчает внесение изменений в сцены, не трогая сами сцены. (Дополнительным преимуществом является то, что это делает построение атласа спрайтов надежнее, при использовании EZGUI).

17. Используйте отдельные префабы для специализации, не специализируйте экземпляры. Например, если у вас 2 типа врагов, отличающихся друг от друга только свойствами, сделайте отдельный префаб для этих свойств и свяжите их. Это позволит:

  • вносить изменения для каждого типа в одном месте;
  • вносить изменения без изменения сцены.

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

18. Связывайте префабы с префабами; не связывайте экземпляры с экземплярами. Ссылки на префаб поддерживаются, при помещении префаба в сцене; ссылки на экземпляры нет. Ссылки на префабы, когда это возможно, упрощают настройку сцены, и уменьшают необходимость ее изменения.

19. Насколько это, возможно, устанавливайте связи между экземплярами автоматически. Если вам необходимо установить связь между экземплярами, делайте это программно (в коде). Например, префаб игрока может зарегистрировать себя в  GameManager , когда он стартует. Или  GameManager  может сам найти экземпляр префаба игрока, когда он стартует.

Не ставьте меши, в корень префаба, если собираетесь добавлять другие скрипты. Когда вы делаете префаб из меша, поместите его в пустой объект, сделав, таким образом, корнем пустышку, и уже на нее вешайте скрипты. Так намного проще заменить меш другим мешем, не потеряв каких-то значений, которые вы установили в инспекторе.

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

20. Используйте безопасный процесс ветвления префабов. Объясним на примере префаба  Player .

Делать опасные изменения в префабе, нужно следующим образом:

  • Скопировать префаб  Player .
  • Переименовать дубликат на  __Player_Backup .
  • Сделайте изменения в префабе  Player .
  • Если все работает, удалить __Player_Backup .

Не называйте дубликат  Player_New , и не вносите в него изменения!

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

Разработчик 1:

  • Скопировать префаб  Player .
  • Переименовать его в  __Player_WithNewFeature  или  __Player_ForDev .
  • Сделать изменения в дубликате, закоммитить / передать 2-му разработчику.

Разработчик 2:

  • Внести изменения в новый префаб.
  • Скопировать префаб  Player , и назвать его  __Player_Backup .
  • Перетащить экземпляр  __Player_WithNewFeature  на сцену.
  • Перетащите экземпляр на оригинальный префаб  Player .
  • Если все работает, удалить  __Player_Backup  и  __Player_WithNewFeature .

Расширения и MonoBehaviourBase

21. Расширьте MonoBehaviour своим поведением, и получайте все ваши компоненты от него. Это позволяет реализовать некоторую общую функциональность, такие как типобезопасного вызова (Invoke), и более сложные вызовы (например, случайный, и т.д.).

22. Определите безопасные методы для методов Invoke, StartCoroutine и Instantiate. Определите делегат Task, и используйте его, для определения методов, которые не зависят от имени. Например:

public void Invoke(Task task, float time)
{
    Invoke(task.Method.Name, time);
}

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

Реализации ниже используют  TypeOf , вместо обобщенных версий этих функций. Обобщенные версии не работают с интерфейсами. Методы ниже, обертывают TypeOf-версии  в обобщенные методы.

//Defined in the common base class for all mono behaviours
public I GetInterfaceComponent<I>() where I : class
{
   return GetComponent(typeof(I)) as I;
}
 
public static List<I> FindObjectsOfInterface<I>() where I : class
{
   MonoBehaviour[] monoBehaviours = FindObjectsOfType<MonoBehaviour>();
   List<I> list = new List<I>();
 
   foreach(MonoBehaviour behaviour in monoBehaviours)
   {
      I component = behaviour.GetComponent(typeof(I)) as I;
 
      if(component != null)
      {
         list.Add(component);
      }
   }
 
   return list;
}

24. Используйте расширения, чтобы сделать синтаксис более удобным. Например:

public static class CSTransform
{
   public static void SetX(this Transform transform, float x)
   {
      Vector3 newPosition =
         new Vector3(x, transform.position.y, transform.position.z);
 
      transform.position = newPosition;
   }
   ...
}

25. Используйте безопасную альтернативу GetComponent. Иногда внедрение зависимостей компонентов (через  RequiredComponent ), может быть болезненным. Например, это затрудняет изменение компонентов в инспекторе (даже, если они имеют тот же базовый тип). В качестве альтернативы, может быть использовано, следующее расширение  GameObject , для вывода ошибки, если компонент не найден.

public static T GetSafeComponent<T>(this GameObject obj) where T : MonoBehaviour
{
   T component = obj.GetComponent<T>();
 
   if(component == null)
   {
      Debug.LogError("Expected to find component of type "
         + typeof(T) + " but found none", obj);
   }
 
   return component;
}

Идиомы

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

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

Примеры групп идиом:

  • Сопрограммы vs конечного автомата.
  • Вложенные префабы vs связанные префабы vs “Бог”-префабы.
  • Стратегии разделения данных.
  • Способы использования спрайтов для состояний в 2D-игр.
  • Структура префаба.
  • Стратегии спауна (spawning).
  • Способы поиска объектов: по типу vs по имени vs по тегу vs по слою vs ссылке.
  • Способы группировки объектов: по типу vs по имени vs по тегу vs по слою vs массиву или ссылкам.
  • Поиск групп объектов vs self-registration.
  • Контроль порядка выполнения (использовать установленный порядок исполнения Unity vs логику yield vs Awake/Start и Update/Late Update vs ручные методы vs в любом порядке).
  • Выделение объектов / позиции / целей с помощью мыши в игре: менеджер выделения vs локальный self-management.
  • Хранение данных между изменениями сцены: через PlayerPrefs, или объекты, которые не уничтожаются при загрузке новой сцены.
  • Способы комбинирования (смешивания, добавления и наслаивание) анимации.

(*vs – против)

Время

27. Держите свой собственный класс времени, чтобы сделать паузу легче. Оберните  Time.DeltaTime  и  Time.TimeSinceLevelLoad  для учета пауз и временной шкалы. Использование его требует дисциплины, но облегчит работу с разными таймерами (например, анимация интерфейса и игровая анимация).

Спаун объектов

28. Не позволяйте порождаемым объектам загромождать иерархию, когда игра запущена. Установите для них родителя – объект сцены, для облегчения поиска объектов, при запущенной игре. Вы можете воспользоваться пустым объектом игры, или даже единичным объектом без поведения, чтобы легко получить доступ из кода. Вызовите этот объект  DynamicObjects .

Дизайн классов

29. Используйте Синглтоны для удобства. Класс ниже, сделает синглтоном автоматически, любой класс производный от него:

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
   protected static T instance;
 
   /**
      Returns the instance of this singleton.
   */
   public static T Instance
   {
      get
      {
         if(instance == null)
         {
            instance = (T) FindObjectOfType(typeof(T));
 
            if (instance == null)
            {
               Debug.LogError("An instance of " + typeof(T) +
                  " is needed in the scene, but there is none.");
            }
         }
 
         return instance;
      }
   }
}

Синглтоны полезны, для различных менеджеров, таких как  ParticleManager , или  AudioManager , или  GUIManager .

  • Избегайте использования синглтонов для уникальных экземпляров префабов, которые не менеджеры (например, экземпляр Игрока). Не соблюдение этого принципа, усложняет иерархию наследований, и делает некоторые изменения сложнее. Вместо этого, держите ссылки на такие экземпляры в вашем  GameManager  (или другом подходящем “Бог”-классе;-) ).
  • Определите статические свойства и методы для публичных переменных и методов, которые часто используются за пределами класса. Это позволит вам писать  GameManager.Player  вместо  GameManager.Instance.player .

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

public float __aVariable;

31. Отделяйте интерфейс от логики игры. Это, по сути паттерн MVC.

Каждый контроллер ввода, должен лишь давать команды соответствующим компонентам, чтобы дать им знать, что контроллер был вызван. Например, в логике контроллера, можно решать, какие команды давать, основываясь на состоянии игрока. Но это плохо (например, это приведет к дублированию логики, если добавлено несколько контроллеров). Вместо этого, объект Игрока должен быть уведомлен о намерениях двигаться вперед, а затем на основе текущего состояния (замедлен или оглушен, например) установить скорость и обновить направление игрока. Контроллеры должны делать только то, что относится к их собственному состоянию (контроллер не меняет состояние, когда игрок изменяет состояние, поэтому контроллер вообще не должен знать о состоянии игрока). Другим примером является смена оружия игроком. Правильный способ сделать это в  Player , с помощью метода  SwitchWeapon(Weapon newWeapon) , который можно вызвать из GUI. GUI не должен манипулировать трансформациями, родителями и т.д..

Любой компонент интерфейса, должен только поддерживать данные, и выполнять обработку, связанную с его собственным состоянием. Например, отображение карты, на основе расчетов движения игрока. Тем не менее, это данные состояния игры, и не принадлежат к GUI. GUI должен лишь отображать данные состояния игры, хранить их нужно в другом месте. Картографические данные должны быть сохранены в другом месте (в  GameManager , например).

Объекты геймплея не должны знать практически ничего о GUI. Единственным исключением является режим паузы, которая может контролироваться глобально посредством  Time.timeScale  (что не является хорошей идеей). Геймплейные объекты должны знать, если игра приостановлена. И это всё. Поэтому, не должно быть ссылок, на GUI-компоненты из объектов геймплея.

В общем, по идее, если вы удалите все классы GUI, игра должна компилироваться.

Вы также, должны быть в состоянии, повторно реализовать графический интерфейс и ввод без необходимости написания новой логики игры.

32. Разделяйте состояние игры и промежуточные данные. Промежуточные данные используются для скорости или удобства, и могут быть извлечены из состояния. Делая такое разделение, вы упрощаете:

  • сохранение состояния игры
  • отладку состояния игры

Один из способов сделать это, определение класса  SaveData  для каждого класса игровой логики:

[Serializable]
PlayerSaveData
{
   public float health; //public for serialisation, not exposed in inspector
}
 
Player
{
   //... bookkeeping variables (переменные промежуточных данных)
 
   //Don’t expose state in inspector. State is not tweakable.
   private PlayerSaveData playerSaveData;
}

33. Отделяйте конфигурацию специализаций.

Рассмотрим на примере двух врагов с одинаковыми мешами, но с разными настройками экземпляров (например, разные сила и скорость). Есть разные пути разделить данные. Вот один из тех, что я предпочитаю, особенно когда объекты порождены, или игра сохранена. (Настройки объектов не являются состоянием, это конфигурационные данные, так что они не должны сохраняться. Когда объект загружен, или порожден, конфигурационные данные, загружаются отдельно автоматически.)

  • Определите шаблонный класс для каждого класса игровой логики. Для экземпляра врага, мы определим класс  EnemyTemplate . Где будут храниться, все отличительные характеристики.
  • В классе игровой логики, определите переменную типа нашего шаблона.
  • Сделайте префаб врага, и два шаблонных префаба  WeakEnemyTemplate  и  StrongEnemyTemplate .
  • При загрузке и порождении объектов, задайте переменной шаблона нужный шаблон.

Этот метод может стать довольно сложным (а иногда, излишне сложным, так что будьте осторожны!).

Например, лучше будет использование полиморфизма обобщения, мы можем определить класс, так:

public class BaseTemplate
{
   ...
}
 
public class ActorTemplate : BaseTemplate
{
   ...
}
 
public class Entity<EntityTemplateType> where EntityTemplateType : BaseTemplate
{
   EntityTemplateType template;
   ...
}
 
public class Actor : Entity <ActorTemplate>
{
   ...
}

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

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

public void SelectWeapon(int index)
{
   currentWeaponIndex = index;
   Player.SwitchWeapon(weapons[currentWeapon]);
}
 
public void Shoot()
{
   Fire(bullets[currentWeapon]);
   FireParticles(particles[currentWeapon]);   
}

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

[Serializable]
public class Weapon
{
   public GameObject prefab;
   public ParticleSystem particles;
   public Bullet bullet;
}

Код выглядит аккуратнее, а главное, так труднее совершить ошибку в настройке данных в инспекторе.

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

public void FireAttack()
{
   /// behaviour
   Fire(bullets[0]);
}
 
public void IceAttack()
{
   /// behaviour
   Fire(bullets[1]);
}
 
public void WindAttack()
{
   /// behaviour
   Fire(bullets[2]);
}

Перечисления могут сделать ваш код лучше …

public void WindAttack()
{
   /// behaviour
   Fire(bullets[WeaponType.Wind]);
}

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

[Serializable]
public class Bullets
{
   public Bullet FireBullet;
   public Bullet IceBullet;
   public Bullet WindBullet;
}

Здесь предполагается, что нет других данных для огня, льда и воздуха.

37. Группируйте данные в сериализуемых классах, чтобы сделать параметры в инспекторе упорядоченнее. Некоторые объекты могут иметь десятки параметров. Это может стать кошмаром, при попытке найти правильную переменную в инспекторе. Чтобы упростить это дело, выполните следующие действия:

  • Определите отдельные классы для групп переменных. Сделайте их публичными и сериализуемыми.
  • В основном классе, определите публичные переменные каждого типа, как указанно ранее.
  • Не инициализируйте эти переменные в  Awake  или  Start ; так как они сериализуемые, Unity позаботится об этом.
  • Вы можете указать значения по умолчанию, до присвоения значений в определении.

Это сгруппирует переменные в инспекторе в блоки, и облегчит управление ими.

[Serializable]
public class MovementProperties //Not a MonoBehaviour!
{
   public float movementSpeed;
   public float turnSpeed = 1; //default provided
}
 
public class HealthProperties //Not a MonoBehaviour!
{
   public float maxHealth;
   public float regenerationRate;
}
 
public class Player : MonoBehaviour
{
   public MovementProperties movementProeprties;
   public HealthPorperties healthProeprties;
}

Текст

38. Если у вас много сюжетного текста, поместите его в файл. Не помещайте его в полях для редактирования в инспекторе. Сделайте, чтоб его легко было изменить, без редактора Unity, и особенно, без необходимости сохранения сцены.

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

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

Тестирование и отладка

40. Реализуйте графический логгер для отладки физики, анимации и AI. Это сделает отладку значительно быстрее. Смотри тут (eng) – http://devmag.org.za/2011/01/25/make-your-logs-interactive-and-squash-bugs-faster/

41. Реализуйте HTML-логгер. В некоторых случаях, логгирование может все еще быть полезным. Имея логи, которые легче распарсить (имеют цветовую маркировку, имеет несколько представлений, записывает скриншоты) сделает лог-отладку намного более приятной. Смотри тут (eng) – http://devmag.org.za/2011/02/09/using-graphs-to-debug-physics-ai-and-animation-effectively/

42. Реализуйте свой собственный FPS-счетчика. Да. Никто не знает, что на самом деле измеряет FPS-счетчик Unity, но это не скорость кадров. Реализуйте собственный, так чтобы FPS соответствовал интуиции и визуальному контролю.

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

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

45. Реализуйте опции отладки для упрощения тестирования. Некоторые примеры:

  • Разблокировать все предметы (айтемы).
  • Отключить врагов.
  • Отключить GUI.
  • Сделать игрока непобедимым.
  • Отключить весь геймплей.

46. Для команд, которые достаточно малы, сделайте префаб с опциями отладки, для каждого члена команды. Положите идентификатор пользователя в файл, который не коммитится, и читается, когда игра запускается. И вот, почему:

  • Члены команды не закоммитят свои опции отладки по случайности, и никого не затронут.
  • Изменение опций отладки не изменит сцену.

47. Держите сцену со всеми элементами геймплея. Например, сцена со всеми врагами, все объекты, с которыми можно взаимодействовать, и т.д. Это делает легкой проверку функциональности, без необходимости играть слишком долго.

48. Определите константы для отладки сочетания клавиш, и держите их в одном месте. Клавиши отладки не нормально (не удобно) обрабатываются в едином месте, как и остальные части игрового ввода. Чтобы избежать конфликтов между сочетаниями клавиш, определите константы в одном месте. Альтернативой является, обрабатывать все клавиши в одном месте, независимо от того, является функция отладочной, или нет. (Недостатком является то, что этому классу могут понадобиться дополнительные ссылки на объекты).

Документация

49. Документируйте ваши настойки. Большинство документации должно быть в коде, но некоторые вещи должны быть задокументированы вне кода. Документация, повышает эффективность (если она актуальная).

Документируйте следующее:

  • Использование слоя (для коллизий, Culling, и Raycasting – по сути то, что должно быть на слое).
  • Использование тэгов.
  • Глубина GUI для слоев (что должно отображаться, поверх чего).
  • Настройка сцены.
  • Идиома предпочтений.
  • Структуры префабов
  • Слои анимации.

Стандарт именования и структура папок

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

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

Общие принципы именования.
  • Называйте вещи своими именами. Птица должна называться Bird.
  • Выбирайте имена, легко произносимые и запоминающиеся. Если вы делаете игру о Майя, не называйте свой уровень QuetzalcoatisReturn.
  • Будьте последовательными. Выбрав имя, придерживайтесь его.
  • Используйте стиль PascalCase, например: ComplicatedVerySpecificObject. Не используйте пробелы, подчеркивания или дефисы, с одним исключением (см. “Именования различных аспектов одного и того же”).
  • Не используйте номера версий, или слова для обозначения стадии прогресса (WIP, финал).
  • Не используйте аббревиатуры: DVamp@W должно быть DarkVampire@Walk.
  • Используйте терминологию из дизайн документации: если в документе анимация смерти называется Die, то надо использовать имя DarkVampire@Die, а не DarkVampire@Death.
  • Держите наиболее специфический дескриптор слева: DarkVampire, вместо VampireDark; PauseButton, вместо ButtonPaused. Легче, например, найти кнопку паузы в инспекторе, когда не все кнопки, начинаются со слова “Button”. [Многие люди предпочитают делать наоборот, потому что, это делает группировки, визуально более очевидными. Имена существуют не для группировок, для этого есть папки. Имена для различения объектов, одного и того же типа, и они должны помогать делать это быстро.]
  • Некоторые имена образуют последовательность. Используйте номера в этих названиях, например, PathNode0, PathNode1. Всегда начинайте с 0, а не 1.
  • Не используйте цифры для вещей, которые не образуют последовательность. Например, Bird0, Bird1, Bird2, должно быть Flamingo, Eagle, Swallow.
  • Ставьте префикс временным объектам в виде двойного подчеркивания __Player_Backup.
Именования различных аспектов одного и того же

Используйте подчеркивание между основным именем, и тем, что описывает “Аспект”. Например:

  • Состояние GUI-кнопок: EnterButton_Active, EnterButton_Inactive.
  • Текстуры: DarkVampire_Diffuse, DarkVampire_Normalmap.
  • Скайбоксы: JungleSky_Top, JungleSky_North.
  • LOD-группы: DarkVampire_LOD0, DarkVampire_LOD1.

Не используйте, это соглашение просто, для различия типов предметов, например, Rock_Small, Rock_Large, должно быть SmallRock, LargeRock.

Структура

Организация ваших сцен, папки проекта и папки скриптов, должны следовать аналогичной схеме.

Структура папок:

Materials
GUI
Effects
Meshes
   Actors
      DarkVampire
      LightVampire
      ...
   Structures
      Buildings
      ...
   Props
      Plants
      ...
   ...
Plugins
Prefabs
   Actors
   Items
   ...
Resources
   Actors
   Items
   ...
Scenes
   GUI
   Levels
   TestScenes
Scripts
Textures
GUI
Effects
...

Структура сцены:

Cameras
Dynamic Objects
Gameplay
   Actors
   Items
   ...
GUI
   HUD
   PauseMenu
   ...
Management
Lights
World
   Ground
   Props
   Structure
   ...

Структура папки скриптов:

ThirdParty
   ...
MyGenericScripts
   Debug
   Extensions
   Framework
   Graphics
   IO
   Math
   ...
MyGameScripts
   Debug
   Gameplay
      Actors
      Items
      ...
   Framework
   Graphics
   GUI
   ...

Как переопределить отрисовку инспектора.

1. Определите базовый класс для всех ваших редакторов:

BaseEditor<T> : Editor
where T : MonoBehaviour
{
   override public void OnInspectorGUI()
   {
      T data = (T) target;
 
      GUIContent label = new GUIContent();
      label.text = "Properties"; //
 
      DrawDefaultInspectors(label, data);
 
      if(GUI.changed)
      {         
         EditorUtility.SetDirty(target);
      }
   }
}

2. Используйте рефлексию и рекурсию, чтобы составить компоненты:

public static void DrawDefaultInspectors<T>(GUIContent label, T target)
   where T : new()
{
   EditorGUILayout.Separator();
   Type type = typeof(T);      
   FieldInfo[] fields = type.GetFields();
   EditorGUI.indentLevel++;
 
   foreach(FieldInfo field in fields)
   {
      if(field.IsPublic)
      {
         if(field.FieldType == typeof(int))
         {
            field.SetValue(target, EditorGUILayout.IntField(
            MakeLabel(field), (int) field.GetValue(target)));
         }   
         else if(field.FieldType == typeof(float))
         {
            field.SetValue(target, EditorGUILayout.FloatField(
            MakeLabel(field), (float) field.GetValue(target)));
         }
 
         ///etc. for other primitive types
 
         else if(field.FieldType.IsClass)
         {
            Type[] parmTypes = new Type[]{ field.FieldType};
 
            string methodName = "DrawDefaultInspectors";
 
            MethodInfo drawMethod =
               typeof(CSEditorGUILayout).GetMethod(methodName);
 
            if(drawMethod == null)
            {
               Debug.LogError("No method found: " + methodName);
            }
 
            bool foldOut = true;
 
            drawMethod.MakeGenericMethod(parmTypes).Invoke(null,
               new object[]
               {
                  MakeLabel(field),
                  field.GetValue(target)
               });
         }      
         else
         {
            Debug.LogError(
               "DrawDefaultInspectors does not support fields of type " +
               field.FieldType);
         }
      }         
   }
 
   EditorGUI.indentLevel--;
}

Описанный выше метод использует вспомогательный метод:

private static GUIContent MakeLabel(FieldInfo field)
{
   GUIContent guiContent = new GUIContent();      
   guiContent.text = field.Name.SplitCamelCase();      
   object[] descriptions =
      field.GetCustomAttributes(typeof(DescriptionAttribute), true);
 
   if(descriptions.Length > 0)
   {
      //just use the first one.
      guiContent.tooltip =
         (descriptions[0] as DescriptionAttribute).Description;
   }
 
   return guiContent;
}

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

3. Определите новые пользовательские редакторы.

К сожалению, вы все равно должны определить класс, для каждого MonoBehaviour. К счастью, эти определения могут быть пустыми, вся фактическая работа делается в базовом классе.

[CustomEditor(typeof(MyClass))]
public class MyClassEditor : BaseEditor<MyClass>
{}

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


 

Оригиналhttp://devmag.org.za/2012/07/12/50-tips-for-working-with-unity-best-practices/

Перевод Максим Саликов.

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