Поради та рекомендації по роботі з Unity3D



Я опублікував першу статтю «50 рад по роботі з Unity» 4 роки тому. Незважаючи на те, що більша її частина все ще актуальна, багато чого змінилося з наступних причин:

  • Unity став краще. Наприклад, тепер я можу довіряти лічильнику FPS. Можливість використання Property Drawers знизила необхідність написання користувальницьких редакторів (Custom Editors). Спосіб роботи з префабами став менше вимагати заданих вбудованих префабов (nested prefabs) та їх альтернатив. Скриптуемые об'єкти стали більш дружніми.

  • Покращилася інтеграція з Visual Studio, налагодження стала набагато простіше і зменшилася потреба в «мавп'ячий» дебаггинге.

  • Стали краще сторонні інструменти та бібліотеки. В Asset Store з'явилося дуже багато ассетов, що спрощують такі аспекти, як візуальна налагодження і логування. Велика частина коду нашого власного (безкоштовного) плагіна Розширення описана в моїй першій статті (і багато з нього описано тут).

  • Удосконалено контроль версій. (Але, може бути, я просто навчився використовувати його більш ефективно). Наприклад, тепер не потрібно створювати множинні або резервні копії для префабов.

  • Я став досвідченішим. За останні 4 роки я попрацював над багатьма проектами в Unity, в тому числі над купою прототипів ігор, завершеними іграми, такими як Father.IO, і над нашим основним ассетом Unity Grids.
Ця стаття є первісною версією статті, переробленої з урахуванням всього вищепереліченого.

Перш ніж перейти до порад, спочатку я залишу невелика примітка (таке ж, як і в першій статті). Ці поради підходять не до всіх проектів Unity:

  • Вони засновані на моєму досвіді роботи над проектами в складі невеликих команд (від 3 до 20 осіб).

  • структурованості, можливості повторного використання, ясності коду та інших аспектів є своя ціна: від розміру команди, обсягу проекту та цілей проекту залежить те, чи варто платити цю ціну. Наприклад, для геймджема ви все це використовувати не будете.

  • Використання багатьох рад — питання смаку (можливо, є різні, але все одно хороші техніки для будь-якого з перерахованих тут рад).
На сайті Unity також є рекомендації по роботі над проектами (однак більшість з них спрямовані на підвищення продуктивності проектів) (всі вони англійською):

Робочий процес
1. З самого початку визначтеся з масштабом і створюйте всі одного масштабу. Якщо ви цього не зробите, можливо, пізніше вам доведеться переробляти ассеты (наприклад, анімація не завжди правильно масштабується). Для 3D-ігор напевно найкраще взяти 1 одиницю Unity дорівнює 1 метру. Для 2D-ігор, які не використовують освітлення і фізику, зазвичай підходить 1 одиниця Unity, рівна 1 пікселя (у «робочому» дозволі). Для UI (і 2D-ігор) виберіть робочий дозвіл (ми використовуємо HD або 2xHD) і створюйте все ассеты під масштаб в цьому дозволі.

2. Зробіть кожну сцену запускається. Це дозволить вам не перемикатися між сценами для запуску гри і прискорить таким чином процес тестування. Це може бути складним, якщо ви використовуєте передаються між завантаженнями сцен (persistent) об'єкти, які потрібні у всіх сценах. Один із способів добитися цього — зробити передаються об'єкти синглтонами, які будуть завантажувати себе самі, якщо вони відсутні в сцені. Синглтоны докладніше розглядаються в іншому раді.

3. Застосовуйте контроль вихідного коду і навчитеся використовувати його ефективно.

  • Сериализируйте ассеты як текст. насправді це не зробить сцени і префабы більш сумісними, але при цьому буде простіше відслідковувати зміни.

  • Освойте стратегію обміну сценами і префабами. Зазвичай над сценою або префабом не повинні працювати кілька осіб. У маленькій команді перед початком роботи над сценою або префабом може бути достатньо попросити всіх не працювати над ними. Може бути корисним використання фізичних токенів, що позначають того, хто в даний момент працює над сценою (ви можете працювати над сценою, тільки якщо у вас на столі лежить відповідний токен).

  • Використовуйте теги як закладок.

  • Виберіть стратегію розгалуження і дотримуйтеся її. Оскільки поєднання сцен і префабов неможливо зробити плавним, організація розгалуження може стати досить складним. Який би спосіб розгалуження ви не вибрали, він повинен працювати з вашою стратегією обміну сценами і префабами.

  • Використовуйте подмодули з обережністю. Подмодули можуть стати відмінним способом підтримки повторно використовуваного коду, проте існує кілька небезпек:

    • Метафайли для різних проектів в загальному випадку неоднакові. Зазвичай це не є проблемою для коду, не використовує MonoBehaviour або скриптуемые об'єкти, однак для MonoBehaviour і скриптуемых об'єктів використання подмодулей може призвести до втрати коду.

    • Якщо ви працюєте над кількома проектами (один або кілька з яких використовують подмодули), то іноді ви можете зіткнутися з «лавиною оновлень», коли необхідно виконати декілька ітерацій pull-merge-commit-push для різних проектів, щоб стабілізувати код у всіх проектах (а якщо під час цього процесу ще хтось вносить зміни, лавина може стати безперервної). Одним із способів мінімізації цього ефекту є внесення змін до подмодули з проектів, які до них ставляться. При цьому проекти, що використовують подмодули, повинні будуть завжди виконувати pull, і їм ніколи не доведеться робити push.
4. Завжди відокремлюйте тестові сцени від коду. Виконуйте коміти тимчасових ассетов і скриптів в репозиторій і видаляйте їх з проекту, коли закінчите роботу з ними.

5. Виконуйте оновлення інструментів (особливо Unity) одночасно. Unity вже набагато краще зберігає зв'язку при відкритті проекту з відмінних від поточної версії, однак зв'язки все одно іноді губляться, якщо члени команди працюють в різних версіях.

6. Імпортуйте ассеты сторонніх розробників в чистий проект і імпортуйте новий пакет для свого використання вже звідти. При безпосередньому імпорті в проект ассеты іноді можуть призводити до проблем:

  • Можливо виникнення колізій (файлів або імен), особливо для ассетов, що містять файли в корені папки Plugins, або для тих, які використовують у своїх прикладах ассеты з Standard Assets.

  • Вони можуть бути невпорядкованими і розкидати свої файли по всьому вашому проекту. Це стає особливою проблемою, якщо ви вирішуєте не використовувати його і хочете видалити його.
Щоб ассеты були в більшій безпеці, користуйтеся наступними інструкціями:

1. Створіть новий проект і імпортуйте ассет.
2. Запустіть приклади і переконайтеся, що вони працюють.
3. Упорядкуйте ассет в більш відповідну структуру папок. (Зазвичай я не підганяю ассет під свою власну структуру папок. Але я перевіряю, що всі файли знаходяться в одній папці і що у важливих місцях немає файлів, які можуть замінити вже наявні файли мого проекту.)
4. Запустіть приклади і переконайтеся, що вони все ще працюють. (Іноді траплялося, що ассет «ламався», коли я переміщав його складові, але зазвичай такої проблеми не виникає.)
5. Тепер видаліть складові, які вам не потрібні (такі як приклади).
6. Переконайтеся, що ассет раніше компілюється і префабы все ще мають всі свої зв'язки. Якщо залишилося ще щось незапущенное, протестуйте його.
7. Тепер виберіть всі ассеты і експортуйте пакет.
8. Імпортуйте його в свій проект.

7. Автоматизуйте процес складання. Це корисно навіть у невеликих проектах, але особливо це корисно, коли:

  • необхідно виконати складання безлічі різних версій гри,
  • потрібно робити збірки іншим членам команди з різним рівнем технічного досвіду або
  • вам необхідно внести невеликі зміни в проект, перш ніж можна буде виконувати його складання.
Інформацію про те, як це зробити, читайте в Unity Builds Scripting: Basic and advanced possibilities.

8. Документуйте свої налаштування. Велика частина документації повинна знаходитися в коді, але дещо-що необхідно задокументувати за його межами. Змушувати розробників копирсатися в коді у пошуках налаштувань означає витрачати їх час. Документовані налаштування підвищують ефективність (якщо підтримується актуальність документів). Документуйте наступне:

  • Використання тегів.
  • Використання шарів (для колізій, culling і raycasting — вказуйте, що в якому шарі не повинно бути).
  • Глибина GUI для шарів (що над чим повинно розташовуватися).
  • Установки сцени.
  • Структура складних префабов.
  • Вибрані ідіоми.
  • Налаштування збірки.
Загальні поради по коду
9. Розміщуйте весь свій код в просторі імен. Це дозволяє уникнути конфлікту коду ваших власних бібліотек і стороннього коду. Але не покладайтеся на простору імен, коли прагнете уникнути конфліктів коду з важливими класами. Навіть якщо ви використовуєте інші простори імен, не беріть в якості імен класів «Object», «Action» або «Event».

10. Використовуйте затвердження (assertions). Затвердження корисні для тестування інваріанти в коді і допомагають позбавитися від логічних помилок. Затвердження доступні через клас Unity.Assertions.Assert. Вони перевіряють умову і записують в консоль повідомлення, якщо воно неправильно. Якщо ви не знаєте, для чого можуть бути корисні затвердження див. The Benefits of programming with assertions (a.k.a. assert statements).

11. Не використовуйте рядка ні для чого, крім відображення тексту. зокрема, не використовуйте рядка для ідентифікації об'єктів або префабов. Існують винятки (Unity все ще є деякі елементи, до яких можна отримати доступ тільки через ім'я). У таких випадках визначайте такі рядки як константи у файлах, таких як AnimationNames або AudioModuleNames. Якщо такі класи стають некерованими, застосовуйте вкладені класи, щоб ввести щось на зразок AnimationNames.Player.Run.

12. Не використовуйте Invoke і SendMessage. Ці методи MonoBehaviour викликають інші методи по імені. Методи, які викликаються по імені, важко відстежити в коді (ви не зможете знайти «Usages», а SendMessage має широку область видимості, яку відстежити ще складніше).

Можна легко написати власну версію Invoke c допомогою Coroutine і actions C#:

public static Coroutine Invoke(this MonoBehaviour monoBehaviour, Action, action, float time)
{
return monoBehaviour.StartCoroutine(InvokeImpl(action, time));
}

private static IEnumerator InvokeImpl(Action action, float time)
{
yield return new WaitForSeconds(time);

action();
}

Потім ви можете використовувати її в MonoBehaviour таким чином:

this.Invoke(ShootEnemy); //де ShootEnemy - це невозвращающий значення (void) метод без параметрів.

Зауваження: хтось запропонував використовувати в якості альтернативи клас ExecuteEvent, частина системи подій Unity. Поки що я знаю про нього не так багато, але схоже, що його варто вивчити детальніше.)

13. Не дозволяйте спауненным (spawned) об'єктів заплутувати ієрархію при виконанні гри. Встановіть в якості батька для них об'єкт у сцені, щоб при виконанні гри було простіше знаходити об'єкти. Можна використовувати пустий (empty) ігровий об'єкт або навіть сінглтон (див. нижче в цій статті) без поведінки (behaviour), щоб простіше було отримувати до нього доступ у коді. Назвіть цей об'єкт DynamicObjects.

14. Будьте точні при використанні null в якості допустимих значень, і уникайте їх там, де це можливо.

Значення null корисні при пошуку некоректного коду. Однак якщо ви придбаєте звичку ігнорувати null, некоректний код буде успішно виконуватися і ви ще довго не помітите помилок. Більш того, вона може з'явитися глибоко всередині коду, оскільки кожен шар ігнорує змінні null. Я намагаюся взагалі не використовувати null як допустиме значення.

Я віддаю перевагу наступну ідіому: не виконувати перевірку на null і дозволити кодом вивалитися при виникненні проблеми. Іноді повторно використовуваних методах я перевіряю змінну null і видаю виняток замість того, щоб передавати її іншим методам, до яких вона може призвести до помилки.

У деяких випадках значення null може бути допустимим, і тому оброблятися іншим способом. У подібних випадках потрібно додати коментар із зазначенням причин того, що значення може дорівнює null.

Звичайний сценарій часто використовується для значень, що настроюються в інспекторі. Користувач може вказати значення, але якщо він цього не зробить, буде використовуватися значення за замовчуванням. Кращий спосіб зробити це — використовувати клас Optional«T», який обертає значення T. (Це трохи схоже на Nullable«T».) Можна використовувати спеціальний рендерер властивостей для візуалізації поля з прапорцем і показувати поле значення тільки коли прапорець встановлений. (На жаль, неможливо використовувати безпосередньо generic-клас, необхідно розширити класи для певних значень T.)

[Serializable]
public class Optional
{
public bool useCustomValue;
public T value;
}

У своєму коді ви можете використовувати його таким чином:

health = healthMax.useCustomValue ? healthMax.Value : DefaultHealthMax;

Зауваження: багато люди підказують мені, що краще використовувати struct (не створює сміття і не може бути null). Однак це означає, що ви не зможете використовувати його в якості базового класу для non-generic-класів так, щоб застосовувати його для полів, які можна використовувати в інспекторі.

15. Якщо ви використовуєте корутины (Coroutines), навчитеся використовувати їх ефективно. Корутины можуть бути зручним способом вирішення багатьох проблем. Однак вони складні в налагодженні, і з їх допомогою ви можете легко перетворити код в хаос, в якому ніхто, навіть ви, не розбереться.

Ви повинні розуміти:

  • Як виконувати корутины паралельно.
  • Як виконувати корутины послідовно.
  • Як створювати нові корутины з існуючих.
  • Як створювати власні корутины з допомогою CustomYieldInstruction.
//Це сама корутина
IEnumerator RunInParallel()
{
yield return StartCoroutine(Coroutine1());
yield return StartCoroutine(Coroutine2());
}

public void RunInSequence()
{
StartCoroutine(Coroutine1());
StartCoroutine(Coroutine1());
}

Coroutine WaitASecond()
{
return new WaitForSeconds(1);
}

16. Використовуйте методи розширень для роботи з компонентами, що мають загальний інтерфейс. Зауваження: Схоже, що GetComponent та інші методи тепер також працюють і для інтерфейсів, тому ця рада надлишковий) Іноді зручно отримувати компоненти, що реалізують певний інтерфейс або знаходити об'єкти з такими компонентами.

У реалізації нижче використовується typeof замість generic-версій цих функцій. Generic-версії не працюють з інтерфейсами, а typeof працює. Нижче представлений метод обертає його в generic-методи.

public static TInterface GetInterfaceComponent(this Component thisComponent)
where TInterface : class
{
return thisComponent.GetComponent(typeof(TInterface)) as TInterface;
}

17. Використовуйте методи розширення (extension methods), щоб зробити синтаксис більш зручним. Наприклад:

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

transform.position = newPosition;
}
...
}

18. Використовуйте більш «м'яку» альтернативу GetComponent. Іноді додавання залежностей через RequireComponent може бути неприємним, воно не завжди можливо або прийнятно, особливо коли ви викликаєте GetComponent для чужого класу. В якості альтернативи може використовуватися наступне розширення GameObject, коли об'єкт повинен видавати повідомлення про помилку, якщо він не знайдений.

public static T GetRequiredComponent(this GameObject obj) where T : MonoBehaviour
{
T component = obj.GetComponent();

if(component == null)
{
Debug.LogError("Очікується компонент типу "
+ typeof(T) + ", але він відсутній", obj);
}

return component;
}

19. Уникайте використання різних ідіом для виконання однакових дій. У багатьох випадках існують різні ідіоматичні способи виконання дій. У таких випадках виберіть одну ідіому і використовуйте її для всього проекту. І ось чому:

  • Деякі ідіоми погано сумісні. Використання однієї ідіоми спрямовує розробку в напрямку, не придатне для іншої ідіоми.

  • Використання однієї ідіоми для всього проекту дозволяє учасникам проекту краще розуміти, що відбувається. При цьому структура та код стають більш зрозумілими і знижується вірогідність помилок.
Приклади груп ідіом:

  • Корутины — кінцеві автомати.
  • Вбудовані префабы — прив'язані префабы — god-префабы.
  • Стратегії розподілу даних.
  • Способи використання спрайтів для станів в 2D-іграх.
  • Структура префабов.
  • Стратегії спаунинга.
  • Способи знаходження об'єктів: за типом/імені/тегу/шару/посиланню.
  • Способи групування об'єктів: за типом/імені/тегу/шару/масиву посилань.
  • Способи виклику методів інших компонентів.
  • Пошук груп об'єктів/self-registration.
  • Контроль порядку виконання (використання настройки порядку виконання Unity — yield-логіки — Awake / Start і Update / Late Update — manual methods — довільна архітектура
  • Вибір об'єктів / положень / цілей у грі мишею: менеджер вибору — локальне самоврядування.
  • Зберігання даних при зміні сцен: PlayerPrefs або за допомогою об'єктів, які не знищуються (Destroy) при завантаженні нової сцени.
  • Способи поєднання (блендінг, додавання і нашарування) анімації.
  • Обробка введення (центральна — локальна)
20. Створіть і підтримуйте свій власний клас часу, щоб зробити роботу з паузами зручніше. Оберніть Time.DeltaTime і Time.TimeSinceLevelLoad для управління паузами і масштабом часу. Для використання класу вимагається дисципліна, але він робить все набагато простіше, особливо при виконанні з різними лічильниками часу (наприклад, анімації інтерфейсу та ігрові анімації).

Зауваження: Unity підтримує unscaledTime і unscaledDeltaTime, які роблять власний клас часу надлишковим в багатьох ситуаціях. Але він все одно може корисний, якщо масштабування глобального часу впливає на компоненти, які ви не писали небажаними способами.

21. Користувальницькі класи, які потребують оновлення, не повинні мати доступ до глобального статичного часу. Замість цього вони повинні отримувати дельту часу як параметр методу Update. Це дозволяє використовувати ці класи при реалізації системи паузи, описаної вище, або коли ви хочете прискорити або сповільнити поведінку користувача класу.

22. Використовуйте загальну структуру для виконання викликів WWW. В іграх з великим об'ємом комунікацій з сервером зазвичай існують десятки викликів WWW. Незалежно від того, чи використовуєте ви сирої клас WWW Unity або плагін, зручно буде написати тонкий шар поверх, який буде працювати як boilerplate.

Зазвичай я визначаю метод Call (окремо для Get і Post), корутину CallImpl і MakeHandler. По суті, метод Call створює за допомогою методу MakeHandler «суперобработчик» (super hander) з парсера, обробник on-success та on-failure. Також він викликає корутину CallImpl, яка формує URL, виконує виклик, що очікує його завершення, а потім викликає «суперобработчик».

Ось як це приблизно виглядає:

public void Call<T>(string call, Func<string, T> parser, Action<T> onSuccess, Action<string> onFailure)
{
var handler = MakeHandler(parser, onSuccess, onFailure);
StartCoroutine(CallImpl(call, handler));
} 

public IEnumerator CallImpl<T>(string call, Action<T> handler)
{
var www = new WWW(call);
yield return www;
handler(www);
}

public Action<WWW> MakeHandler<T>(Func<string, T> parser, Action<T> onSuccess, Action<string> onFailure)
{
return (WWW www) =>
{
if(NoError(www)) 
{
var parsedResult = parser(www.text);
onSuccess(parsedResult);
}
else
{
onFailure("Текст помилки");
}
}
}

У такого підходу є кілька переваг.

  • Він дозволяє уникнути написання великого обсягу boilerplate-коду
  • Він дозволяє обробляти потрібні елементи (наприклад, відображення загружающегося компонента UI або обробка певних загальних помилок) в першу чергу.
23. Якщо у вас багато тексту, помістіть його у файл. Не розміщуйте його в поля для редагування в інспекторі. Зробіть так, щоб його можна було швидко змінювати, не відкриваючи редактор Unity, і особливо без необхідності збереження сцени.

24. Якщо ви плануєте локалізацію, відокремте всі рядки в одне місце. Існує кілька способів зробити це. Один з них — це визначити клас Text з рядковим полем типу public для кожного рядка, за замовчуванням, наприклад, буде встановлено англійську. Інші мови будуть дочірніми класів та повторно ініціалізують поля з мовними аналогами.

Більш складний спосіб (він підходить при великому обсязі тексту або високому числі мов) — зчитування електронної таблиці і створення логіки для вибору потрібного рядка на підставі вибраної мови.

Дизайн класів
25. Вирішіть, як будуть використовуватися інспектовані поля, і зробіть це стандартом. Є два способи: зробити поля public, або зробити їх private і позначити як [SerializeField]. Останнє «більш коректно», але менш зручно (і цей спосіб не дуже популяризується самої Unity). Що б ви не вибрали, зробіть це стандартом, щоб розробники у вашій команді знали, як інтерпретувати поле public.

  • Інспектовані поля є public. В цьому випадку public означає: «змінна може безпечно змінюватися дизайнером в процесі виконання програми. Не ставте її значення в коді».

  • Інспектовані поля є private і позначені як Serializable. В цьому випадку public означає: «можна безпечно змінювати цю змінну в коді» (тому їх буде не дуже багато, а в MonoBehaviours і ScriptableObjects не буде полів public).
26. Ніколи не робіть змінні компонентів public, якщо вони не повинні настроюватися інспектора. Інакше будуть змінюватися дизайнером, особливо якщо незрозуміло, що вони роблять. В деяких випадках цього не можна уникнути (наприклад, якщо якийсь скрипт редактора повинен використовувати змінну). В цьому випадку потрібно використовувати атрибут HideInInspector, щоб приховати її в інспекторі.

27. Використовуйте property drawers, щоб зробити поля більш зручними для користувачів. Property drawers можна використовувати для налаштування контролів (controls) в інспекторі. Це дозволить для вас створювати контроли, найбільш підходящі під вид даних і вставляти захист (наприклад, обмеження значень змінних). Використовуйте атрибут Header для впорядкування полів, а атрибут Tooltip — для надання дизайнерам додаткової документації.

28. Віддавайте перевагу property drawers, а не користувальницьким редакторам (custom editors). Property drawers реалізуються за типами полів, а значить, вимагають набагато менше часу на реалізацію. Їх також зручніше використовувати повторно – після реалізації типу їх можна використовувати для того ж типу в будь-якому класі. Користувальницькі редактори реалізуються в MonoBehaviour, тому їх складніше використовувати повторно і вони вимагають більше роботи.

29. За замовчуванням «запечатывайте» MonoBehaviours (застосовуйте модифікатор sealed). В загальному випадку MonoBehaviours Unity не дуже зручні для наслідування:

  • Спосіб, яким Unity викликає такі message-методи, як Start і Update, ускладнює роботу цих методів у підкласах. Якщо ви не будете уважні, буде викликаний не той елемент, або ви забудете викликати базовий метод.

  • Коли використовуються користувальницькі редактори, зазвичай потрібно скопіювати ієрархію успадкування для редакторів. Якщо комусь потрібно буде розширити один з ваших класів, то потрібно створити власний редактор або обмежитися тим, що створили ви.
У випадках, коли спадкування потрібно, не використовуйте message-методів Unity, якщо цього можна уникнути. Якщо ви все-таки їх використовуєте, не робіть їх віртуальними. При необхідності можна визначити порожню віртуальну функцію, яка викликається з message-методу, яку дочірній клас може перевизначити (override) для виконання додаткових дій.

public class MyBaseClass
{
public sealed void Update()
{
CustomUpdate();
... // update цього класу 
}

//Викликається до того, як цей клас виконує свій update
//Перевизначення для виконання вашого коду update.
virtual public void CustomUpdate(){};
}

public class Child : MyBaseClass
{
override public void CustomUpdate()
{
//Виконуємо якісь дії
}
}

Це запобіжить випадковому перевизначення вашого коду класом, але все одно дозволяє задіяти повідомлення Unity. Я не люблю такий порядок, тому що він стає проблематичним. У наведеному вище прикладі дочірнього класу може знадобитися виконання операцій відразу після того, як клас виконав власний update.

30. Відокремлюйте інтерфейс від ігрової логіки. Компоненти інтерфейсу в цілому не повинні нічого знати про гру, в якій вони використовуються. Передавайте їм дані, які потрібно відобразити, і підпишіть на події, що перевіряються при взаємодії користувача з компонентами UI. Компоненти інтерфейсу не повинні виконувати ігрову логіку. Вони можуть фільтрувати дані, що вводяться, перевіряючи їх правильність, але основні правила повинні виконуватися не в них. У багатьох іграх-головоломках елементи поля є розширенням інтерфейсу, і не повинні містити правил. (Наприклад, шахова фігура не повинна розраховувати дозволені для неї ходи.)

Введена інформація також повинна бути відокремлена від логіки, що діє на підставі цієї інформації. Використовуйте контролер введення, сообщяющий актору про необхідність руху, актор сам приймає рішення про те, коли потрібно рухатися.

Ось спрощений приклад компонента UI, що дозволяє користувачеві вибрати зброю із заданого списку. Єдине, що знають ці класи про гру, це клас Weapon (і тільки тому, що клас Weapon — корисний джерело даних, що цей контейнер повинен відображати). Гра теж нічого не знає про контейнері; їй потрібно лише зареєструвати подію OnWeaponSelect.

public WeaponSelector : MonoBehaviour
{
public event Action OnWeaponSelect {add; remove; } 
//GameManager може реєструвати це подія

public void OnInit(List weapons)
{
foreach(var weapon in weapons)
{

var button = ... //Створює дочірню кнопку і додає її в ієрархію 

buttonOnInit(weapon, () => OnSelect(weapon)); 
// дочірня кнопка відображає опцію, 
// та надсилає повідомлення про натискання цього компоненту
}
}
public void OnSelect(Weapon weapon)
{
if(OnWepaonSelect != null) OnWeponSelect(weapon);
}
}

public class WeaponButton : MonoBehaviour
{
private Action<> onClick;

public void OnInit(Weapon weapon, Action onClick)
{
... //установка спрайту і тексту зброї

this.onClick = onClick;
}

public void OnClick() //Прив'язуємо цей метод як OnClick компонента UI Button
{
Assert.IsTrue(onClick != null); //Не повинно відбуватися

onClick();
} 
}

31. Розділіть конфігурацію, стан і допоміжну інформацію.

  • Змінні конфігурації — це змінні, які настроюються в об'єкті для визначення об'єкта через його властивості. Наприклад, maxHealth.
  • Змінні стану — це змінні, які повністю визначають поточний стан об'єкта. Це змінні, які необхідно зберігати, якщо ваша гра підтримує збереження. Наприклад, currentHealth.
  • Допоміжні (bookkeeping) змінні використовуються для швидкості, зручності і перехідних станів. Вони можуть бути цілком визначені змінних стану. Наприклад, previousHealth.
Розділивши ці типи змінних, ви будете розуміти, що можна змінювати, що потрібно зберігати, що потрібно відправляти/отримувати по мережі. Ось простий приклад такого поділу.

public class Player
{
[Serializable]
public class PlayerConfigurationData
{
public float maxHealth;
}

[Serializable]
public class PlayerStateData
{
public float health;
}

public PlayerConfigurationData configuration;
private PlayerState stateData;

//допоміжна інформація
private float previousHealth;

public float Health
{
public get { return stateData.health; }
private set { stateData.health = value; }
}
}

32. Не використовуйте пов'язані індексами масиви типу public. Наприклад, не визначайте масив зброї, масив куль і масив частинок таким чином:

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;
}

Такий код виглядає приємніше, але, що важливіше, так складніше зробити помилки при налаштуванні даних в інспекторі.

33. Уникайте використання масивів для структур, які не є послідовностями. Наприклад, у гравця є три типи атак. Кожна використовує поточний зброю, але генерує різні кулі і різну поведінку.

Ви можете спробувати засунути три кулі в масив, а потім використовувати логіку такого типу:

public void FireAttack()
{
/// поведінка
Fire(bullets[0]);
}

public void IceAttack()
{
/// поведінка
Fire(bullets[1]);
}

public void WindAttack()
{
/// поведінка
Fire(bullets[2]);
}

Enums можуть виглядати гарніше в коді…

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

… але не в інспекторі.

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

[Serializable]
public class Bullets
{
public Bullet fireBullet;
public Bullet iceBullet;
public Bullet windBullet;
}

Це означає, що інших даних Fire, Ice і Wind немає.

34. Групуйте дані в сериализируемые класи, щоб все виглядало зручніше в інспекторі. Деякі елементи можуть мати десятки налаштувань. Пошук потрібної змінної може стати кошмаром. Щоб спростити собі життя, дотримуйтесь цих інструкцій:

  • Визначте окремі класи для груп змінних. Зробіть їх public і serializable
  • В основному класі визначте public змінні кожного визначеного вище типу.
  • Не ініціалізувати ці змінні в Awake або Start; вони сериализируемые, тому Unity подбає про них сама.
  • Ви можете вказати значення за замовчуванням, призначивши при їх визначенні.
Так ви створите сворачиваемые в інспекторі групи змінних, якими легше управляти.

[Serializable]
public class MovementProperties //Не MonoBehaviour!
{
public float movementSpeed;
public float turnSpeed = 1; //вказуємо значення за замовчуванням
}

public class HealthProperties //Не MonoBehaviour!
{
public float maxHealth;
public float regenerationRate;
}

public class Player : MonoBehaviour
{
public MovementProperties movementProeprties;
public HealthPorperties healthProeprties;
}

35. Зробіть не є MonoBehaviour класи Serializable, навіть якщо вони не використовуються для полів public. Це дозволить переглядати поля класу в інспекторі, коли він знаходиться в режимі Debug mode. Це працює і для вкладених класів (private або public).

36. Намагайтеся не змінювати в коді настроюються в інспекторі дані. Настроюється в інспекторі змінна — це змінна конфігурації, і з нею потрібно поводитись як з константою при виконанні програми, а не як з змінної стану. Якщо ви будете дотримуватися це правило, вам буде простіше писати методи, що скидають стан компонента на первісне, при цьому ви будете чітко розуміти, що робить змінна.

public class Actor : MonoBehaviour
{
public float initialHealth = 100;

private float currentHealth;

public void Start()
{
ResetState();
} 

private void Respawn()
{
ResetState();
} 

private void ResetState()
{
currentHealth = initialHealth;
}
}

Патерни
Патерни — це способи рішення часто виникаючих проблем стандартними методами. Книга Роберта Нистрома «Патерни програмування ігор» (можна прочитати безкоштовно онлайн) — цінний ресурс для розуміння того, як патерни застосовні для вирішення проблем, що виникають при розробці ігор. У самій Unity є безліч таких патернів: Instantiate — це приклад патерну «прототип» (prototype); MonoBehaviour — це версія патерну «шаблонний метод» (template), UI і анімації використовується патерн «спостерігач» (observer), а новий движок анімації використовує скінченні автомати (state machines).

Ці поради відносяться до використання патернів конкретно в Unity.

37. Використовуйте для зручності синглтоны (патерн «одинак»). Наступний клас автоматично зробить сінглтоном будь успадковує його клас:

public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
protected static T instance;

//Повертає екземпляр цього сінглтона.
public static T Instance
{
get
{
if(instance == null)
{
instance = (T) FindObjectOfType(typeof(T));

if (instance == null)
{
Debug.LogError("У сцені потрібен примірник " + typeof(T) + 
", але він відсутній.");
}
}

return instance;
}
}
}

Синглтоны корисні для менеджерів, наприклад для ParticleManager, AudioManager або GUIManager.

(Багато програмісти налаштовані проти класів, розпливчасто званих XManager, бо це вказує на те, що для класу вибрано погане ім'я або у нього занадто багато непов'язаних один з одним завдань. В цілому, я з ними згоден. Однак в іграх є всього кілька менеджерів, і вони выолняют в іграх одні і ті ж завдання, так що ці класи фактично є ідіомами.)

  • Не використовуйте синглтоны для унікальних екземплярів префабов, не є менеджерами (наприклад, Player). Дотримуйтеся цього принципу, щоб не ускладнювати ієрархію успадкування і внесення певних типів змін. Краще зберігайте посилання на них у GameManager (або в більш відповідному класі God ;-) ).
  • Визначте властивості static і методи для змінних і методів public, які часто використовуються за межами класу. Це дозволить вам писати GameManager.Player замість GameManager.Instance.player.
Як пояснено в інших радах, синглтоны корисні для створення точок спауна за замовчуванням і об'єктів, що передаються між завантаженнями сцен і зберігають глобальні дані.

38. Використовуйте кінцеві автомати (state machines) для створення різного поведінки в різних станах або для виконання коду при зміні станів. Легкий кінцевий автомат має безліч станів і для кожного стану ви можете вказати дії, що виконуються при вході або перебування в стані, а також дія оновлення. Це дозволити зробити код більш чистим і менш схильним до помилок. Хороша ознака того, що вам знадобиться кінцевий автомат: код методу Update містить конструкції if або switch, змінюють його поведінку, або такі змінні як hasShownGameOverMessage.

public void Update()
{
if(health <= 0)
{
if(!hasShownGameOverMessage) 
{
ShowGameOverMessage();
hasShownGameOverMessage = true; //При респауні значення стає false
}
}
else
{
HandleInput();
} 
}

З великою кількістю станів такий тип коду може стати заплутаним, кінцевий автомат зробить його набагато ясніше.

39. Використовуйте поля типу UnityEvent для створення шаблону «спостерігач» (observer) в інспекторі. Клас UnityEvent дозволяє пов'язувати методи, які отримують до чотирьох параметрів у испекторе, з допомогою того ж інтерфейсу UI, що і події в Buttons. Це особливо корисно при роботі з введенням.

40. Використовуйте патерн «спостерігач», щоб визначати, коли змінюється значення поля. Проблема виконання коду тільки при зміні змінної часто виникає в іграх. Ми створили стандартне рішення цієї проблеми з допомогою generic-класу, що дозволяє реєструвати події зміни змінних. Нижче представлений приклад зі здоров'ям. Ось як він створюється:

/*Спостережуване значення*/ health = new ObservedValue(100);
health.OnValueChanged += () => { if(health.Value <= 0) Die(); };

Тепер ви можете міняти його де завгодно, не перевіряючи в кожному місці його значення, наприклад, ось так:

if(hit) health.Value -= 10;

Коли здоров'я стає нижче 0, викликається метод Die. Детальні обговорення та реалізацію див. у цьому посте.

41. Використовуйте для префабов патерн Actor. (Це «нестандартний» патерн. Основна ідея взята з презентації Кірана Лорда (Kieran Lord).)

Actor (актор) — це основний компонент префаба. Зазвичай це компонент, що забезпечує «індивідуальність» префаба, і той, з яким найбільш часто буде взаємодіяти код більш високого рівня. Actor часто використовує інші компоненти – помічники (helper) – для того ж об'єкта (іноді для дочірніх об'єктів), щоб виконати свою роботу.

При створенні об'єкта «кнопка» через меню Unity створюється ігровий об'єкт з компонентами Sprite і Button (і дочірній з компонентом Text). У цьому випадку актором буде Button. Головна камера також зазвичай має кілька компонентів (GUI Layer, Flare Layer, Audio Listener), прикріплені до компоненту Camera. Camera тут є актором.

Для правильної роботи актора можуть знадобитися інші компоненти. Можна зробити префаб більш надійним і корисним з допомогою наступних атрибутів компонента-актора:

  • Використовуйте RequiredComponent для визначення всіх компонентів, які потрібні актору в тому ж ігровому об'єкті. (Актор тоді зможе завжди безпечно викликати GetComponent без необхідності перевірки, чи не було обчислене значення null.)
  • Використовуйте DisallowMultipleComponentщоб уникнути прикріплення декількох екземплярів того ж компонента. Актор завжди зможе викликати GetComponent, не турбуючись про поведінку, яка повинна бути, коли прикріплено кілька компонентів).
  • Використовуйте SelectionBase, якщо у об'єкта-актора є дочірні об'єкти. Так його буде простіше вибрати у вікні сцени.
[RequiredComponent(typeof(HelperComponent))]
[DisallowMultipleComponent]
[SelectionBase]
public class Actor : MonoBehaviour
{
...//
}

42. Використовуйте генератори випадкових і паттернизированных потоків даних. (Це нестандартний патерн, але ми вважаємо його надзвичайно корисним.)

Генератор схожий на генератор випадкових чисел: це об'єкт з методом Next, викликаним для отримання нового елемента певного типу. У процесі конструювання генератори можна змінювати для створення широкого діапазону патернів і різних типів випадковості. Вони корисні, бо дозволяють зберігати логіку генерування нового елемента окремо від ділянки коду, в якому потрібний елемент, що робить код набагато чистіше.

Ось кілька прикладів:

var generator = Generator
.RamdomUniformInt(500)
.Select(x => 2*x); //Генерує парні числа від 0 до 998

var generator = Generator
.RandomUniformInt(1000)
.Where(n => n % 2 == 0); //Робить те ж саме

var generator = Generator
.Iterate(0, 0, (m, n) => m + n); //Числа Фібоначчі

var generator = Generator
.RandomUniformInt(2)
.Select(n => 2*n - 1)
.Aggregate((m, n) => m + n); //Випадкові стрибки з кроком 1 або -1

var generator = Generator
.Iterate(0, Generator.RandomUniformInt(4), (m, n) => m + n - 1)
.Where(n >= 0); //Випадкова послідовність, збільшує середню

Ми вже використовували генератори для спауна перешкод, зміни кольорів фону, процедурної музики, генерування послідовності букв для створення слів, як в іграх зі словами і для багато чого іншого. З допомогою наступної конструкції генератори можна вдало використовувати для управління корутинами, що повторюються із змінними інтервалами:

while (true)
{
//Що робимо

yield return new WaitForSeconds(timeIntervalGenerator.Next());
}

Прочитайте постщоб більше дізнатися про генераторах.

Префабы і скриптуемые об'єкти
43. Використовуйте префабы для всього. Єдиними ігровими об'єктами в сцені, які не є префабами (або частинами префабов), повинні бути папки. Навіть унікальні об'єкти, які використовуються тільки один раз, повинні бути префабами. Це спрощує внесення змін, які не потребують зміни сцени.

44. Прив'язуйте префабы до префабам; не прив'язуйте екземпляри до екземплярів. Зв'язку з префабами зберігаються при перетягуванні префаба в сцену, згідно з примірниками — ні. Прив'язування до префабам там, де це можливо, зменшує витрати на налаштування сцени і знижує необхідність зміни сцен.

Скрізь, де тільки можливо, встановлюйте зв'язки між екземплярами автоматично. Якщо вам потрібно зв'язати примірники, встановлюйте зв'язку програмно. Наприклад, префаб Player може зареєструвати себе в GameManager, коли він запускається, або GameManager може знайти префаб Player, коли він запускається.

45. Не робіть сітки корінням префабов, якщо ви хочете додавати інші скрипти. При створенні префаба з сітки спочатку зробіть батьком сітки порожній ігровий об'єкт, і нехай буде коренем. Прив'язуйте скрипти до кореня, а не до сітки. Так вам буде простіше замінити сітку інший сіткою, не втрачаючи значення, налаштовані в інспекторі.

46. Використовуйте скриптуемые об'єкти, а не префабы для переданих даних конфігурації.

Якщо ви так зробите:

  • сцени будуть менше
  • ви не зможете помилково внести зміни в одну сцену (примірник префаба)
47. Використовуйте скриптуемые об'єкти для даних рівнів. Дані рівнів часто зберігаються в XML або JSON, але використання замість них скриптуемых об'єктів має ряд переваг:

  • Їх можна буде редагувати Editor. Так простіше буде перевіряти дані, і такий спосіб більш зручний для нетехнічних дизайнерів. Більш того, ви можете використовувати користувальницькі редаторы, щоб редагувати їх було ще простіше.
  • Вам не потрібно буде хвилюватися про читання/запису і парсингу даних.
  • Поділ і вбудовування, а також управління отриманими ассетами стануть простішими. Так ви зможете створювати рівні з будівельних блоків, а не з масивної конфігурації.
48. Використовуйте скриптуемые об'єкти для конфігурування поведінки в інспекторі. Скриптуемые об'єкти зазвичай пов'язані з даними конфігурування, але вони також дозволяють використовувати «методи» як дані.

Розглянемо сценарій, в якому у вас є тип Enemy, і у кожного ворога є якийсь набір суперсил SuperPowers. Можна зробити їх звичайними класами і отримати їх список в класі Enemy, але без інтерфейсу редактора ви не зможете налаштувати список різних суперсил (кожної зі своїми властивостями) в інспекторі. Але якщо ви зробите ці суперсили ассетами (реалізуєте їх як ScriptableObjects), то у вас вийде!

Ось як це працює:

public class Enemy : MonoBehaviour
{
public SuperPower superPowers;

public UseRandomPower()
{
superPowers.RandomItem().UsePower(this);
}
}

public class BasePower : ScriptableObject
{
virtual void UsePower(Enemy self)
{
}
}

[CreateAssetMenu("BlowFire", "Blow Fire")
public class BlowFire : SuperPower
{
public strength;
override public void UsePower(Enemy self)
{
///програма використання суперсили blow fire
}
}

При використанні цього патерну не потрібно забувати про наступних обмеженнях:

  • Скриптуемые об'єкти не можуть надійно бути абстрактними. Замість цього використовуйте конкретні базові глассы і видавайте NotImplementedExceptions в методах, які повинні бути абстрактними. Також можна визначити атрибут Abstract і відзначити їм класи і методи, які повинні бути абстрактними.

  • Generic скриптуемые об'єкти не можуть бути сериализированы. Однак можна використовувати generic базові класи і сериализировать тільки підкласи, які визначають generic.
49. Використовуйте скриптуемые об'єкти для спеціалізації префабов. Якщо конфігурація двох об'єктів відрізняється лише деякими властивостями, то зазвичай вставляють два примірники на сцену і налаштовують ці властивості примірниках. Зазвичай краще зробити окремий клас властивостей, який може відрізнятися між двома типами, окремим класом скриптуемого об'єкта.

Це забезпечує більшу гнучкість:

  • Можна успадковувати з класу спеціалізації, щоб створювати специфічні властивості для різних типів об'єкта.
  • Налаштування сцени стає набагато більш безпечною (ви просто вибираєте потрібний скриптуемый об'єкт замість налаштування всіх властивостей, щоб зробити об'єкт потрібного типу).
  • У процесі виконання додатка так простіше управляти цими об'єктами через код.
  • За наявності декількох примірників двох типів ви будете впевнені в сталості їх властивостей при внесенні змін.
  • Можна розбити набори конфігураційних змінних на набори, які можна буде змішувати і поєднувати.
Ось простий приклад такої установки.

[CreateAssetMenu("HealthProperties.asset", "Health Properties")]
public class HealthProperties : ScriptableObject
{
public float maxHealth;
public float resotrationRate;
}

public class Actor : MonoBehaviour
{
public HealthProperties healthProperties;
}

При великій кількості спеціалізацій можна визначити спеціалізацію як звичайний клас, і використовувати їх список в скриптуемом об'єкті, пов'язаному з відповідним місцем, в якому можна його застосовувати (наприклад, в GameManager). Для забезпечення його безпеки, швидкості та зручності буде потрібно трохи більше «клею»; нижче наведено приклад мінімально можливого використання.

public enum ActorType
{
Vampire, Wherewolf
}

[Serializable]
public class HealthProperties
{
public ActorType type;
public float maxHealth;
public float resotrationRate;
}

[CreateAssetMenu("ActorSpecialization.asset", "Actor Specialization")]
public class ActorSpecialization : ScriptableObject
{
public List healthProperties;

public this[ActorType]
{
get { return healthProperties.First(p => p.type == type); } //Небезпечна версія!
}
}

public class GameManager : Singleton 
{
public ActorSpecialization actorSpecialization;

...
}

public class Actor : MonoBehaviour
{
public ActorType type;
public float health;

//Приклад використання
public Regenerate()
{
health 
+= GameManager.Instance.actorSpecialization[type].resotrationRate;
}
}

50. Використовуйте атрибут CreateAssetMenu, щоб автоматично додати створення ScriptableObject в меню Asset/Create.

Налагодження
51. Навчіться ефективно використовувати інструменти налагодження Unity.

  • Додавайте об'єкти context в конструкції Debug.Log, щоб знати, де вони генеруються.
  • Використовуйте Debug.Break для паузи гри в редакторі (наприклад, це корисно, коли ви хочете виконати умови помилки і вам потрібно досліджувати властивості компонента в цьому кадрі).
  • Використовуйте функції Debug.DrawRay і Debug.DrawLine для візуальної налагодження (наприклад, DrawRay дуже корисна при налагодженні причин «непопадання» ray cast).
  • Використовуйте для візуальної налагодження Gizmos. Можна також використовувати gizmo renderer за межами mono behaviours з допомогою атрибута DrawGizmo.
  • Використовуйте інспектор в режимі налагодження (щоб бачити з допомогою інспектора значення полів private при виконанні програми в Unity).
52. Навчіться ефективного використання відладчика вашої IDE. См., наприклад, Debugging Unity games in Visual Studio.

53. Використовуйте візуальний відладчик, малює графіки зміни значень з часом. Він надзвичайно зручний для налагодження фізики, анімацій та інших динамічних процесів, і в особливості нерегулярно виникаючих помилок. Ви зможете побачити цю помилку на графіку і відстежити інші змінні, що змінюються в момент помилки. Також візуальний контроль робить очевидними певні типи дивної поведінки, наприклад, занадто часто змінювані значення або відхилення без явних причин. Ми використовуємо Monitor Components, але існують і інші інструменти візуальної налагодження.

54. Користуйтеся зручною записом в консоль. Використовуйте розширення редактора, що дозволяє кодувати кольором висновок за категоріями і фільтрувати висновок згідно з цими категоріями. Ми застосовуємо Editor Console Pro, але є і інші розширення.

55. Використовуйте інструменти тестування Unity, особливо для тестування алгоритмів і математичного коду. См., наприклад, туторіал Unity Test Tools або пост Unit testing at the speed of light with Unity Test Tools.

56. Застосовуйте інструменти тестування Unity для виконання «чорнових» тестів. Інструменти тестування Unity придатні не тільки для формальних тестів. Їх також можна використовувати для зручних «чорнових» тестів, які виконуються в редакторі без запуску сцени.

57. Використовуйте гарячі клавіші, щоб робити скріншоти. Багато баги пов'язані з візуальним відображенням, і набагато простіше повідомляти про них, якщо можна зробити знімок екрану. Ідеальна система повинна мати лічильники PlayerPrefs, щоб скріншоти не перезаписувати. Скріншоти не потрібно зберігати в папці проекту, щоб співробітники випадково не фіксували (commit) їх в репозиторії.

58. Використовуйте гарячі клавіші для друку снепшотов важливих змінних. Вони дозволять реєструвати інформацію, коли під час гри відбувається несподівані події, які можна досліджувати. Набір змінних звичайно ж залежить від гри. Підказками для вас можуть стати типові помилки, що виникають у грі. Наприклад, положення гравця і ворогів або «стан думання» AI-актора (скажімо, шлях, по якому він намагається наслідувати).

59. Реалізуйте опції налагодження, щоб спростити тестування. Приклади:

  • Розблокувати всі предмети.
  • Відключити ворогів.
  • Вимкнути GUI.
  • Зробити гравця невразливим.
  • Відключити весь ігровий процес.
Будьте уважні, щоб випадково не зафіксувати опції налагодження в репозиторії; зміна цих опцій може заплутати інших розробників в колективі.

60. Визначте константи для гарячих клавіш налагодження, і тримайте їх в одному місці. Клавіші налагодження, на відміну від ігрового введення, зазвичай не обробляються в одному місці. Щоб уникнути конфліктів гарячих клавіш, в першу чергу визначайте константи. Альтернативою є обробка всіх клавіш в одному місці, незалежно від того, мають вони функції налагодження чи ні. (Недолік такого підходу в тому, що цього класу можуть знадобитися тільки для цього додаткові посилання на об'єкти).

61. При процедурної генерації сіток отрисовывайте або спауньте невеликі сфери у вершинах. Це дозволить вам переконатися, що вершини знаходяться в потрібних місцях і вони потрібного розміру, перш ніж починати роботу з трикутниками і UV для відображення сіток.

Продуктивність
62. Будьте обережні з загальними рекомендаціями по дизайну і структури для забезпечення продуктивності.

  • Часто такі поради засновані на міфах і не перевіряються тестами.
  • Іноді рекомендації перевірені тестами, але тести неякісні.
  • Буває, що поради перевірені якісними тестами, але вони нереалістичні або застосовуються в іншому контексті. (Наприклад, можна просто довести, що використання масивів швидше, ніж generic lists. Проте в контексті реальної гри ця різниця майже завжди незначна. Ще можна додати, що якщо тести проводилися на обладнанні, що відрізняється від цільових пристроїв для вас, то їх результати можуть бути у вашому випадку марні.)
  • Іноді рада буває правильним, але вже застарілим.
  • Іноді рекомендація корисна. Але тут може виникнути необхідність компромісу: іноді повільні, але виконані в строк гри краще, ніж швидкі, але запізнілі. Сильно оптимізовані ігри можуть з більшою ймовірністю містити хитрий код, що затримує реліз.
  • Корисно враховувати поради по забезпеченню продуктивності, щоб знаходити джерела справжніх проблем швидше, ніж описаним вище процесом.
63. Як можна раніше починайте регулярно тестувати гру на цільових пристроях. Пристрої мають різні характеристики продуктивності; не дозволяйте їм підкидувати вам сюрпризи. Чим раніше ви дізнаєтеся про проблеми, тим більш ефективно ви зможете їх вирішувати.

64. Навчіться ефективного використання профайлера для відстеження причин проблем з продуктивністю.

65. При необхідності використовуйте сторонній профайлер для більш точного набору. Іноді профайлер Unity не може надати чітку картинку того, що відбувається: у нього можуть закінчитися кадри профайлу, або глибокий профайлинг настільки гальмує гру, що результати тестів не мають сенсу. У цьому випадку ми використовуємо наш власний профайлер, але ви можете знайти альтернативні в Asset Store.

66. Заміряйте ефект поліпшень продуктивності. При внесенні змін для підвищення продуктивності заміряйте її, щоб переконатися, що зміна дійсно покращує продуктивність. Якщо зміна неизмеряемо або незначно, відмовтеся від нього.

67. Не пишіть менш читабельний код для підвищення продуктивності. Винятки:

  • У вас є проблема, знайдена в коді профайлером, ви виміряли покращення після зміни, і поліпшення настільки добре, що варто погіршення зручності підтримки.

    АБО
  • Ви точно знаєте, що робите.
Стандарт присвоєння імен і структура папок
68. Дотримуйтесь задокументованим угоди про присвоєння імен і структурі папок. Завдяки стандартизованому присвоєння імен та структурі папок простіше шукати об'єкти і розбиратися в них.

Швидше за все, ви захочете створити своє власне угода про присвоєння імен і структуру папок. Ось одне для прикладу.

Загальні принципи присвоєння імен
  1. Називайте речі своїми іменами. Птиця повинна називатися Bird.
  2. Виберіть імена, які можна вимовити і запам'ятати. Якщо ви робите гру про майя, не називайте рівень QuetzalcoatisReturn (ВозвращениеКетцалкоатля).
  3. Підтримуйте сталість. Якщо ви вибрали ім'я, дотримуйтеся його. Не називайте щось buttonHolder в одному випадку і buttonContainer в іншому.
  4. Використовуйте Pascal case, наприклад: ComplicatedVerySpecificObject. Не використовуйте пробіли, символи підкреслення або дефіси, з одним винятком (див. розділ «Привласнення імен для різних аспектів одного елемента»).
  5. Не використовуйте номери версій або слова для позначення ступеня виконання (WIP, final).
  6. Не використовуйте абревіатури: DVamp@W повинен називатися DarkVampire@Walk.
  7. Використовуйте термінологію дизайн-документа: якщо в документі анімація смерті називається Die, то використовуйте DarkVampire@Die, а не DarkVampire@Death.
  8. Залишайте найбільш конкретний опис зліва: DarkVampire, а не VampireDark; PauseButton, а не ButtonPaused. Наприклад, буде простіше знайти кнопку паузи в інспекторі, якщо не всі назви кнопок починаються зі слова Button. [Багато воліють зворотний принцип, тому що так угрупування візуально виглядає більш очевидною. Проте імена, на відміну від папок, не призначені для групування. Імена потрібні для розрізнення об'єктів одного типу, щоб можна було знаходити їх швидко і просто.]
  9. Деякі імена утворюють послідовності. Використовуйте в цих іменах числа, наприклад, PathNode0, PathNode1. Завжди починайте нумерацію з 0, а не 1.
  10. Не використовуйте числа елементів, що не утворюють послідовність. Наприклад, Bird0, Bird1, Bird2 повинні називатися Flamingo, Eagle, Swallow.
Привласнення імен для різних аспектів одного елемента
Використовуйте символи підкреслення між основним іменем і частиною, що описує «аспект» елемента. Наприклад:

  • Стану кнопок GUI EnterButton_Active, EnterButton_Inactive
  • Текстури DarkVampire_Diffuse, DarkVampire_Normalmap
  • Скайбокси JungleSky_Top, JungleSky_North
  • Групи LOD DarkVampire_LOD0, DarkVampire_LOD1
Не використовуйте цю угоду для розрізнення різних типів елементів, наприклад, Rock_Small, Rock_Large повинні називатися SmallRock, LargeRock.

Структура
Схема сцен, папки проекту і папки скриптів повинна мати схожий шаблон. Нижче представлені приклади, які можна використовувати.

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

MyGame
Helper
Design
Scratchpad
Materials
Meshes
Actors
DarkVampire
LightVampire

Structures
Buildings

Props
Plants


Resources
Actors
Items

Prefabs
Actors
Items

Scenes
Menus
Levels
Scripts
Tests
Textures
UI
Effects

UI
MyLibray

Plugins
SomeOtherAsset1
SomeOtherAsset2
...

Структура сцени

Main
Debug
Managers
Cameras
Lights
UI
Canvas
HUD
PauseMenu

World
Ground
Props
Structures

Gameplay
Actors
Items

Dynamic Objects

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

Debug
Gameplay
Actors
Items

Framework
Graphics
UI
...
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.