Привет, Хабр. Это снова я, Илья Кудинов, QA-инженер из компании Badoo. Но сегодня я расскажу не о тестировании (этим я уже занимался в понедельник), а о геймдеве. Нет, этим мы в Badoo не занимаемся, разрабатывать компьютерные игры — моё хобби.
Профессионалы индустрии, не судите строго. Все советы в этой статье адресованы начинающим разработчикам, решившим попробовать свои силы в Unity. Многие советы можно отнести к разработке в целом, но я постараюсь добавить в них Unity-специфику. Если вы можете посоветовать что-то лучше, чем предлагаю я — пишите в комментариях, буду обновлять статью.
Я мечтал разрабатывать игрушки с детства. Наверное, уже в далёком 1994 году, когда мне подарили мою первую Dendy, я думал: “Как была бы здолава, если бы вот в этай иглушке было бы ещё всякое классное...” В средней школе я начал учиться программировать и вместе с товарищем делал свои первые играбельные поделки (ох, как мы их любили!). В институте мы с друзьями строили наполеоновские планы о кардинальном изменении индустрии с помощью нашей совершенно новой темы…
А в 2014 году я начал изучать Unity и наконец-то НА САМОМ ДЕЛЕ начал делать игры. Однако вот беда: я никогда не работал программистом. У меня не было опыта настоящей корпоративной разработки (до этого я всё делал “на коленке”, и, кроме меня, в моём коде никто бы не разобрался). Я умел программировать, но я не умел делать это хорошо. Все мои знания Unity и C# ограничивались скудными ещё на тот момент официальными туториалами. А мой любимый способ познавать мир — делать ошибки и учиться на них. И я наделал их предостаточно.
Сегодня я расскажу о некоторых из них и покажу, как их избежать (ах, если бы я знал всё это три года назад!)
Для того чтобы понять все используемые в материале термины, достаточно предварительно пройти один-два официальных туториала Unity. Ну, и иметь хоть какое-то представление о программировании.
Ах, мой класс
Было:
монструозный класс
Стало:
За несколько часов работы я смог урезать несколько сотен строк трудноподдерживаемого кода и сэкономить часы нервного копания в го плохом коде.
Что, суть моего совета в том, чтобы не писать гигантские классы, спасибо, Кэп? Нет. Мой совет: дробите вашу логику на атомарные классы ещё до того, как они станут большими. Пусть сначала ваши объекты будут иметь три-четыре осмысленных компонента по десятку строк в коде каждого, но ориентироваться в них будет не сложнее, чем в одном из 50 строк, зато при дальнейшем развитии логики вы не окажетесь в такой ситуации, как я. Заодно появляется больше возможностей для переиспользования кода — например, компонент, отвечающий за здоровье и получение урона, можно прилепить и игроку, и противникам, и даже препятствиям.
Умный термин — Interface segregation principle.
Каким бы простым ни казалось на первый взгляд проектирование объектов в Unity (“Программирование мышкой, фуууу”), не нужно недооценивать эту составляющую разработки. Да-да, я вот недооценивал. Прямо по пунктам:
При этом C# — такая душка, что позволяет не плодить эти параметры, и вот эти два вызова будут работать идентично:
Умный термин — Single responsibility principle.
Я этим занялся значительно позже, чем стоило. Я уже писал статью о том, как это может помочь при разработке как программисту, так и геймдизайнеру. Помимо кастомных инспекторов для отдельных атрибутов и целых компонентов, Editor GUI можно использовать для огромного количества вещей. Создавать отдельные вкладки редактора для просмотра и изменения SAVE-файлов игры, для редактирования сценариев, для создания уровней… Возможности — безграничны! Да и потенциальная экономия времени просто восхитительна.
Даже если вы не уверены, что будете переводить игру на другие языки. Впиливать локализацию в уже сформировавшийся проект — невыносимая боль. Можно придумать самые разные способы локализации и хранения переводов. Жаль, что Unity не умеет самостоятельно выносить все строки в отдельный файл, который поддаётся локализации “из коробки” и без доступа к остальному коду приложения (как, например, в Android Studio). Вам придётся писать такую систему самому. Лично я использую для этого два решения, пусть и не очень изящные.
Оба они базируются на моём собственном классе
В нём ещё есть кучка строк с защитой от ошибок и проверки на заполненность полей, сейчас я их убрал для читабельности. Можно хранить переводы как массив, но по ряду причин я всё же выбрал отдельные поля.
Вся “магия” — в методе неявного преобразования в строку. Благодаря ему вы в любом месте кода можете вызвать что-то типа такого:
— и сразу же получить в строке text нужный перевод в зависимости от текущего языка в настройках игрока. То есть в большинстве мест при добавлении локализации даже не придётся изменять код — он просто будет продолжать работать со строками, как и раньше!
Первый вариант локализации очень простой и подходит для игр, где совсем мало строк, и все они расположены в UI. Мы просто добавляем каждому объекту с переводимым компонентом
Заполняем все строки переводов в инспекторе — и вуаля, готово!
Для игр, где лексем больше, я использую другой подход. У меня есть
Кстати, полезная вещь — язык системы игрока (по сути, его локализационные предпочтения) можно получить с помощью, например, вот такого кода:
Умный термин — Dependency inversion principle.
Это может показаться излишним, но теперь некоторые мои игры пишут в лог практически каждый чих. С одной стороны, это дико захламляет консоль Unity (которая, к сожалению, не умеет заниматься никакой удобной фильтрацией), с другой — вы можете открыть в любом удобном вам софте для просмотра логов исходные лог-файлы и составлять по ним любые удобные вам отчёты, которые помогут заниматься как оптимизацией приложения, так и поиском аномалий и их причин.
Я делал глупости. Предположим, мы хотим как-то хранить настройки различных уровней какой-то игры:
Компонент
Про мой класс Singleton можно почитать в уже упомянутой статье.
Мой первоначальный подход был такой:
Казалось бы, всё работает исправно. Но затем таких методов становилось всё больше, сущности разрастались, появлялись прокси-методы в других классах… В общем, наступил спагетти-ад. Поэтому в конечном счёте я решил вынести все методы для работы с миссиями в саму структуру
что сделало код гораздо более читабельным и удобноподдерживаемым.
Во многих источниках говорится, что
Класс
Постоянно писать данные в поля
Например, можно создать единый SAVE-объект, который хранит в себе все настройки и данные игрока:
Пишем простую систему, которая занимается ленивой инициализацией этого объекта (при первом запросе читает его из
Для того чтобы манипулировать таким объектом как строкой для
Вот вы запустили свою игру. Она работает, вы радуетесь. Поиграли в неё минут 15, поставили на паузу, чтобы проверить этот любопытный ворнинг в консоли… ОБОЖЕМОЙ, ПОЧЕМУ У МЕНЯ В СЦЕНЕ 745 ОБЪЕКТОВ В КОРНЕ??? КАК МНЕ ЧТО-НИБУДЬ НАЙТИ???
Разбираться в этом мусоре очень сложно. Поэтому старайтесь придерживаться двух правил:
Кладите все создаваемые через
Удаляйте объекты, которые больше не нужны. Например, у некоторых моих надстроек есть вызов
Очень дорогая с точки зрения ресурсов функция для поиска объектов. Да ещё и завязана она на имени объекта, которое нужно каждый раз изменять как минимум в двух местах (в сцене и в коде). То же можно сказать и про
Если уж очень приспичит, обязательно кешируйте в переменную каждый вызов, чтобы не делать его больше одного раза. Или вообще сделайте связи объектов через инспектор.
Но можно делать и более изящно. Можно использовать класс — хранилище ссылок на объекты, в который регистрируется каждый потенциально нужный объект, сохранить в него мета-объект
Кстати, компонент Transform имплементирует интерфейс
Важно: в отличие от большинства других функций для поиска объектов, transform.Find() возвращает даже отключенные (gameObject.active == false) в данный момент объекты.
Особенно если художник — это вы сами. Особенно если художник никогда раньше не работал над играми и IT-проектами в целом.
Дать много советов по текстурам для 3D-игр я не смогу — сам ещё глубоко в это не закапывался. Важно научить художника сохранять все картинки с POT-габаритами (Power Of Two, чтобы каждая сторона картинки была степенью двойки, например, 512х512 или 1024х2048), чтобы они эффективнее сжимались движком и не занимали драгоценные мегабайты (что особенно важно для мобильных игр).
А вот рассказать грустных историй про спрайты для 2D-игр я могу много.
Что это такое? По хорошему, майлстоун (milestones — камни, которые в былые времена устанавливали вдоль дороги каждую милю для отмечания расстояний) — это определённое состояние проекта, когда он достиг поставленных на данный момент целей и может переходить к дальнейшему развитию. А может и не переходить.
Наверное, это была наша главная ошибка при работе над дебютным проектом. Мы поставили перед собой очень много целей и шли ко всем сразу. Всегда что-то оставалось недоделанным, и мы никак не могли сказать: “А вот теперь проект действительно готов!”, потому что к имеющемуся функционалу постоянно хотелось добавить что-то ещё.
Не надо так делать. Лучший способ развития игры — точно знать конечный набор фич и не отходить от него. Но это уж больно редко бывает, если речь идёт не о крупной индустриальной разработке. Игры часто развиваются и модернизируются прямо в процессе разработки. Так как же вовремя остановиться?
Составьте план версий (майлстоунов). Так, чтобы каждая версия была завершённой игрой: чтобы не было никаких временных заглушек, костылей и недореализованного функционала. Так, чтобы на любом майлстоуне было не стыдно сказать: “На этом мы и закончим!” и выпустить в свет (или навсегда закрыть в шкафу) качественный продукт.
Глупый я был три года назад, да? Надеюсь, вы не будете повторять мои ошибки и сэкономите много времени и нервов. А если вы боялись даже попробовать начать заниматься разработкой игр, может быть, я смог вас хоть немного на это мотивировать.
P. S. Я подумываю о написании туториала вида “Делаем игрушку для хакатона за сутки с нуля”, по которому человек без знания Unity и навыков программирования смог бы написать свою первую игру. На русском языке качественных туториалов такого формата не очень много. Как думаете, стоит попробовать?
Профессионалы индустрии, не судите строго. Все советы в этой статье адресованы начинающим разработчикам, решившим попробовать свои силы в Unity. Многие советы можно отнести к разработке в целом, но я постараюсь добавить в них Unity-специфику. Если вы можете посоветовать что-то лучше, чем предлагаю я — пишите в комментариях, буду обновлять статью.
Я мечтал разрабатывать игрушки с детства. Наверное, уже в далёком 1994 году, когда мне подарили мою первую Dendy, я думал: “Как была бы здолава, если бы вот в этай иглушке было бы ещё всякое классное...” В средней школе я начал учиться программировать и вместе с товарищем делал свои первые играбельные поделки (ох, как мы их любили!). В институте мы с друзьями строили наполеоновские планы о кардинальном изменении индустрии с помощью нашей совершенно новой темы…
А в 2014 году я начал изучать Unity и наконец-то НА САМОМ ДЕЛЕ начал делать игры. Однако вот беда: я никогда не работал программистом. У меня не было опыта настоящей корпоративной разработки (до этого я всё делал “на коленке”, и, кроме меня, в моём коде никто бы не разобрался). Я умел программировать, но я не умел делать это хорошо. Все мои знания Unity и C# ограничивались скудными ещё на тот момент официальными туториалами. А мой любимый способ познавать мир — делать ошибки и учиться на них. И я наделал их предостаточно.
Сегодня я расскажу о некоторых из них и покажу, как их избежать (ах, если бы я знал всё это три года назад!)
Для того чтобы понять все используемые в материале термины, достаточно предварительно пройти один-два официальных туториала Unity. Ну, и иметь хоть какое-то представление о программировании.
Не засовывайте всю логику объекта в один MonoBehaviour
Ах, мой класс
MonsterBehaviour
в нашей дебютной игре! 3200 строк спагетти-кода в его худшие дни. Каждая необходимость вернуться к этому классу вызывала у меня лёгкую дрожь, и я всегда старался отложить эту работу так надолго, как только мог. Когда спустя чуть больше года после его создания я-таки добрался до его рефакторинга, я не только разбил его на базовый класс и несколько наследников, но и вынес несколько блоков функционала в отдельные классы, которые добавлял в объекты прямо из кода с помощью gameObject.AddComponent()
, поэтому мне не пришлось изменять уже накопившиеся префабы.Было:
монструозный класс
MonsterBehaviour
, хранивший в себе все персональные настройки монстров, определявший их поведение, анимацию, прокачку, нахождение пути и всё-всё-всё.Стало:
- абстрактный класс
MonsterComponent
, от которого наследуются все прочие компоненты и который занимается их связыванием и, к примеру, базовой оптимизацией в виде кеширования результатов вызоваgameObject.GetComponent<T>();
- класс
MonsterStats
, в который геймдизайнер заносит параметры монстров. Он их хранит, изменяет с уровнем и отдаёт другим классам по запросу; - класс
MonsterPathFinder
, который занимается поиском путей и хранит в статических полях сгенерированные данные для оптимизации алгоритма; - абстрактный класс
MonsterAttack
с наследниками под разные виды атаки (оружием, когтями, магией...), которые контролируют всё, что касается боевого поведения монстра — тайминги, анимацию, применение особых приёмов; - ещё много дополнительных классов, реализующих всяческую специфическую логику.
За несколько часов работы я смог урезать несколько сотен строк трудноподдерживаемого кода и сэкономить часы нервного копания в го плохом коде.
Что, суть моего совета в том, чтобы не писать гигантские классы, спасибо, Кэп? Нет. Мой совет: дробите вашу логику на атомарные классы ещё до того, как они станут большими. Пусть сначала ваши объекты будут иметь три-четыре осмысленных компонента по десятку строк в коде каждого, но ориентироваться в них будет не сложнее, чем в одном из 50 строк, зато при дальнейшем развитии логики вы не окажетесь в такой ситуации, как я. Заодно появляется больше возможностей для переиспользования кода — например, компонент, отвечающий за здоровье и получение урона, можно прилепить и игроку, и противникам, и даже препятствиям.
Умный термин — Interface segregation principle.
Не забывайте про ООП
Каким бы простым ни казалось на первый взгляд проектирование объектов в Unity (“Программирование мышкой, фуууу”), не нужно недооценивать эту составляющую разработки. Да-да, я вот недооценивал. Прямо по пунктам:
- Наследование. Всегда приятно вынести какую-то общую логику нескольких классов в общий базовый класс. Иногда это имеет смысл сделать заранее, если объекты “идеологически” похожи, пусть и не имеют пока общих методов. Например, сундуки на уровне и декоративные факелы на стенах поначалу не имели ничего общего. Но когда мы начали разрабатывать механику тушения и зажигания факелов, пришлось выносить из сундуков в общий класс механику взаимодействия с ними игрока и показ подсказок в интерфейсе. А мог бы и сразу догадаться. А ещё у меня есть общий базовый класс для всех объектов, являющийся надстройкой над MonoBehaviour, с кучкой полезных новых функций.
- Инкапсуляция. Даже не буду объяснять, насколько полезной может быть установка правильных областей видимости. Упрощает работу, снижает вероятность глупой ошибки, позволяет удобнее дебажиться… Здесь ещё полезно знать про две директивы —
[HideInInspector]
, скрывающую в инспекторе публичные поля компонента, которые не стоит править в объектах (впрочем, имеет смысл по возможности вообще избегать публичных полей, это плохая практика — вместо них лучше использовать property, спасибо Charoplet за напоминание), и[SerializeField]
, напротив, отображающую в инспекторе приватные поля (что бывает очень полезно для более удобного дебага). - Полиморфизм. Здесь вопрос исключительно в красоте и лаконичности кода. Одна из моих любимых штук для поддержки полиморфизма в C# — универсальные шаблоны. Например, я написал такие простые и удобные методы для выдёргивания случайного элемента произвольного класса из L
ist<T>
(а делаю я это очень часто):
protected T GetRandomFromList<T>(List<T> list)
{
return list[Random.Range(0, list.Count)];
}
protected T PullRandomFromList<T>(ref List<T> list)
{
int i = Random.Range(0, list.Count);
T result = list[i];
list.RemoveAt(i);
return result;
}
При этом C# — такая душка, что позволяет не плодить эти параметры, и вот эти два вызова будут работать идентично:
List<ExampleClass> list = new List<ExampleClass>();
ExampleClass a = GetRandomFromList<ExampleClass>(list);
ExampleClass a = GetRandomFromList(list);
Умный термин — Single responsibility principle.
Изучите Editor GUI
Я этим занялся значительно позже, чем стоило. Я уже писал статью о том, как это может помочь при разработке как программисту, так и геймдизайнеру. Помимо кастомных инспекторов для отдельных атрибутов и целых компонентов, Editor GUI можно использовать для огромного количества вещей. Создавать отдельные вкладки редактора для просмотра и изменения SAVE-файлов игры, для редактирования сценариев, для создания уровней… Возможности — безграничны! Да и потенциальная экономия времени просто восхитительна.
Думайте о локализации с самого начала
Даже если вы не уверены, что будете переводить игру на другие языки. Впиливать локализацию в уже сформировавшийся проект — невыносимая боль. Можно придумать самые разные способы локализации и хранения переводов. Жаль, что Unity не умеет самостоятельно выносить все строки в отдельный файл, который поддаётся локализации “из коробки” и без доступа к остальному коду приложения (как, например, в Android Studio). Вам придётся писать такую систему самому. Лично я использую для этого два решения, пусть и не очень изящные.
Оба они базируются на моём собственном классе
TranslatableString
:[System.Serializable]
public class TranslatableString
{
public const int LANG_EN = 0;
public const int LANG_RU = 1;
public const int LANG_DE = 2;
[SerializeField] private string english;
[SerializeField] private string russian;
[SerializeField] private string german;
public static implicit operator string(TranslatableString translatableString)
{
int languageId = PlayerPrefs.GetInt("language_id");
switch (languageId) {
case LANG_EN:
return translatableString.english;
case LANG_RU:
return translatableString.russian;
case LANG_DE:
return translatableString.german;
}
Debug.LogError("Wrong languageId in config");
return translatableString.english();
}
}
В нём ещё есть кучка строк с защитой от ошибок и проверки на заполненность полей, сейчас я их убрал для читабельности. Можно хранить переводы как массив, но по ряду причин я всё же выбрал отдельные поля.
Вся “магия” — в методе неявного преобразования в строку. Благодаря ему вы в любом месте кода можете вызвать что-то типа такого:
TranslatableString lexeme = new TranslatableString();
string text = lexeme;
— и сразу же получить в строке text нужный перевод в зависимости от текущего языка в настройках игрока. То есть в большинстве мест при добавлении локализации даже не придётся изменять код — он просто будет продолжать работать со строками, как и раньше!
Первый вариант локализации очень простой и подходит для игр, где совсем мало строк, и все они расположены в UI. Мы просто добавляем каждому объекту с переводимым компонентом
UnityEngine.UI.Text
вот такой компонент:public class TranslatableUIText : MonoBehaviour
{
public TranslatableString translatableString;
public void Start()
{
GetComponent<UnityEngine.UI.Text>().text = translatableString;
}
}
Заполняем все строки переводов в инспекторе — и вуаля, готово!
Для игр, где лексем больше, я использую другой подход. У меня есть
Singleton
-объект LexemeLibrary
, который хранит в себе карту вида “id лексемы” => “сериализованный TranslatableString
”, из которой я и получаю лексемы в нужных мне местах. Заполнять эту библиотеку можно любым удобным способом: ручками в инспекторе, через кастомный интерфейс (привет, Editor GUI) или путём экспорта/импорта CSV-файлов. Последний вариант прекрасно работает с аутсорс-переводчиками, но требует немного больше труда для избежания ошибок.Кстати, полезная вещь — язык системы игрока (по сути, его локализационные предпочтения) можно получить с помощью, например, вот такого кода:
void SetLanguage(int language_id)
{
PlayerPrefs.SetInt("language_id", language_id);
}
public void GuessLanguage()
{
switch (Application.systemLanguage) {
case SystemLanguage.English:
SetLanguage(TranslatableString.LANG_EN);
return;
case SystemLanguage.Russian:
SetLanguage(TranslatableString.LANG_RU);
return;
case SystemLanguage.German:
SetLanguage(TranslatableString.LANG_DE);
return;
}
}
Умный термин — Dependency inversion principle.
Пишите подробные логи!
Это может показаться излишним, но теперь некоторые мои игры пишут в лог практически каждый чих. С одной стороны, это дико захламляет консоль Unity (которая, к сожалению, не умеет заниматься никакой удобной фильтрацией), с другой — вы можете открыть в любом удобном вам софте для просмотра логов исходные лог-файлы и составлять по ним любые удобные вам отчёты, которые помогут заниматься как оптимизацией приложения, так и поиском аномалий и их причин.
Создавайте самодостаточные сущности
Я делал глупости. Предположим, мы хотим как-то хранить настройки различных уровней какой-то игры:
public struct Mission
{
public int duration;
public float enemyDelay;
public float difficultyMultiplier;
}
public class MissionController : Singleton<MissionController>
{
public Mission[] missions;
public int currentMissionId;
}
Компонент
MissionController
сидит в каком-нибудь объекте, содержит в себе настройки всех миссий игры и доступен из любого места кода через MissionController.Instance
.Про мой класс Singleton можно почитать в уже упомянутой статье.
Мой первоначальный подход был такой:
Mission
хранит в себе только параметры, а MissionController
занимается всеми прочими запросами. Например, чтобы получить лучший счёт игрока на определённом уровне я использовал методы видаMissionController.GetHighScore(int missionId)
{
return PlayerPrefs.GetInt("MissionScore" + missionId);
}
Казалось бы, всё работает исправно. Но затем таких методов становилось всё больше, сущности разрастались, появлялись прокси-методы в других классах… В общем, наступил спагетти-ад. Поэтому в конечном счёте я решил вынести все методы для работы с миссиями в саму структуру
Mission
и стал получать рекорды миссии, например, таким образом:MissionController.GetCurrentMission().GetHighScore();
что сделало код гораздо более читабельным и удобноподдерживаемым.
Не бойтесь использовать PlayerPrefs
Во многих источниках говорится, что
PlayerPrefs
нужно использовать очень осторожно и при каждом возможном случае вместо этого самостоятельно сериализовывать данные и писать их в собственные файлы. Раньше я старательно готовил свой формат бинарного файла для каждой сохраняемой сущности. Теперь я так не делаю. Класс
PlayerPrefs
занимается тем, что хранит пары «ключ => значение» в файловой системе, причём работает одинаково на всех платформах, просто хранит свои файлы в разных местах.Постоянно писать данные в поля
PlayerPrefs
(и читать их) — плохо: регулярные запросы к диску никому добра не делают. Однако можно написать простую, но разумную систему, которая поможет этого избежать.Например, можно создать единый SAVE-объект, который хранит в себе все настройки и данные игрока:
[System.Serializable]
public struct Save
{
public string name;
public int exp;
public int[] highScores;
public int languageId;
public bool muteMusic;
}
Пишем простую систему, которая занимается ленивой инициализацией этого объекта (при первом запросе читает его из
PlayerPrefs
, кладёт в переменную и при дальнейших запросах использует уже эту переменную), все изменения пишет в этот объект и сохраняет его обратно в PlayerPrefs
только при необходимости (например, при выходе из игры и изменении ключевых данных).Для того чтобы манипулировать таким объектом как строкой для
PlayerPrefs.GetString()
и PlayerPrefs.SetString()
, достаточно использовать сериализацию в JSON:Save save = newSave;
string serialized = JsonUtility.ToJson(newSave);
Save unserialized = JsonUtility.FromJson<Save>(serialized);
Следите за объектами в сцене
Вот вы запустили свою игру. Она работает, вы радуетесь. Поиграли в неё минут 15, поставили на паузу, чтобы проверить этот любопытный ворнинг в консоли… ОБОЖЕМОЙ, ПОЧЕМУ У МЕНЯ В СЦЕНЕ 745 ОБЪЕКТОВ В КОРНЕ??? КАК МНЕ ЧТО-НИБУДЬ НАЙТИ???
Разбираться в этом мусоре очень сложно. Поэтому старайтесь придерживаться двух правил:
Кладите все создаваемые через
Instantiate()
объекты в какие-нибудь объектные структуры. Например, у меня в сцене теперь всегда есть объект GameObjects
с подобъектами-категориями, в которые я кладу всё, что создаю. Во избежание человеческих ошибок в большинстве случаев у меня существуют надстройки над Instantiate()
вроде InstantiateDebris()
, которые сразу же кладут объект в нужную категорию.Удаляйте объекты, которые больше не нужны. Например, у некоторых моих надстроек есть вызов
Destroy(gameObject, timeout);
с заранее прописанным для каждой категории тайм-аутом. Благодаря этому мне не нужно париться об очистке таких вещей, как пятна крови на стенах, дырки от пуль, улетевшие в бесконечность снаряды…Избегайте GameObject.Find()
Очень дорогая с точки зрения ресурсов функция для поиска объектов. Да ещё и завязана она на имени объекта, которое нужно каждый раз изменять как минимум в двух местах (в сцене и в коде). То же можно сказать и про
GameObject.FindWithTag()
(я бы вообще предложил отказаться от использования тегов — всегда можно найти более удобные способы определения типа объекта).Если уж очень приспичит, обязательно кешируйте в переменную каждый вызов, чтобы не делать его больше одного раза. Или вообще сделайте связи объектов через инспектор.
Но можно делать и более изящно. Можно использовать класс — хранилище ссылок на объекты, в который регистрируется каждый потенциально нужный объект, сохранить в него мета-объект
GameObjects
из предыдущего совета и искать нужные объекты в нём через transform.Find()
. Всё это гораздо лучше, чем опрашивать каждый объект в сцене о его имени в поисках необходимого, а потом всё равно упасть с ошибкой, потому что ты недавно этот объект переименовал.Кстати, компонент Transform имплементирует интерфейс
IEnumerable
, а значит, можно удобно обходить все дочерние объекты объекта таким образом:foreach (Transform child in transform) {
child.gameObject.setActive(true);
}
Важно: в отличие от большинства других функций для поиска объектов, transform.Find() возвращает даже отключенные (gameObject.active == false) в данный момент объекты.
Договоритесь с художником о формате изображений
Особенно если художник — это вы сами. Особенно если художник никогда раньше не работал над играми и IT-проектами в целом.
Дать много советов по текстурам для 3D-игр я не смогу — сам ещё глубоко в это не закапывался. Важно научить художника сохранять все картинки с POT-габаритами (Power Of Two, чтобы каждая сторона картинки была степенью двойки, например, 512х512 или 1024х2048), чтобы они эффективнее сжимались движком и не занимали драгоценные мегабайты (что особенно важно для мобильных игр).
А вот рассказать грустных историй про спрайты для 2D-игр я могу много.
- Объединяйте однотипные спрайты (а тем более отдельные спрайты одной анимации) в общую картинку. Если вам нужно 12 спрайтов размером 256х256 пикселей, то не нужно сохранять 12 картинок — гораздо удобнее сделать одну картинку размером 1024х1024 пикселей, и в ней разложить спрайты по сетке со стороной в 256 пикселей и воспользоваться автоматической системой разбивания текстуры на спрайты. Останется четыре свободных места — не беда, вдруг понадобится добавить ещё картинок такого типа. Важно: если слотов под спрайты станет не хватать, то скажите своему художнику увеличивать полотно до новых степеней двойки только направо и вверх; в этом случае вам не придётся править мета-данные для уже имеющихся спрайтов — они останутся на тех же координатах. UPD by KonH: вместо ручной расстановки спрайтов удобнее воспользоваться встроенной утилитой SpritePacker. Сам я её не трогал ещё, так что подробнее расказать пока не могу (:
- Обязательно рисуйте все спрайты проекта в одном масштабе, даже если они всё-таки оказываются на разных текстурах. Не представляете, сколько времени я потратил на подгон значений Pixels per unit для разных спрайтов монстров, чтобы в игровом мире они были соответствующих размеров. Сейчас на каждой текстуре у меня есть неиспользуемое изображение главного персонажа, чтобы можно было сравнивать соответствие масштабов. Ничего сложного — а столько времени и нервов экономит!
- Выравнивайте все однотипные спрайты относительно одного общего Pivot’а. В идеале — центра картинки или середины какой-нибудь стороны. Например, все спрайты оружия игрока стоит располагать в слоте (или на отдельной картинке) так, чтобы точка, за которую игрок будет это оружие держать, была ровно в центре. Иначе придётся выставлять этот Pivot руками в редакторе; это будет неудобно, про это можно забыть — и персонаж будет держать копьё за самый кончик или топор за основание лезвия. Очень глупый персонаж.
Устанавливайте майлстоуны
Что это такое? По хорошему, майлстоун (milestones — камни, которые в былые времена устанавливали вдоль дороги каждую милю для отмечания расстояний) — это определённое состояние проекта, когда он достиг поставленных на данный момент целей и может переходить к дальнейшему развитию. А может и не переходить.
Наверное, это была наша главная ошибка при работе над дебютным проектом. Мы поставили перед собой очень много целей и шли ко всем сразу. Всегда что-то оставалось недоделанным, и мы никак не могли сказать: “А вот теперь проект действительно готов!”, потому что к имеющемуся функционалу постоянно хотелось добавить что-то ещё.
Не надо так делать. Лучший способ развития игры — точно знать конечный набор фич и не отходить от него. Но это уж больно редко бывает, если речь идёт не о крупной индустриальной разработке. Игры часто развиваются и модернизируются прямо в процессе разработки. Так как же вовремя остановиться?
Составьте план версий (майлстоунов). Так, чтобы каждая версия была завершённой игрой: чтобы не было никаких временных заглушек, костылей и недореализованного функционала. Так, чтобы на любом майлстоуне было не стыдно сказать: “На этом мы и закончим!” и выпустить в свет (или навсегда закрыть в шкафу) качественный продукт.
Заключение
Глупый я был три года назад, да? Надеюсь, вы не будете повторять мои ошибки и сэкономите много времени и нервов. А если вы боялись даже попробовать начать заниматься разработкой игр, может быть, я смог вас хоть немного на это мотивировать.
P. S. Я подумываю о написании туториала вида “Делаем игрушку для хакатона за сутки с нуля”, по которому человек без знания Unity и навыков программирования смог бы написать свою первую игру. На русском языке качественных туториалов такого формата не очень много. Как думаете, стоит попробовать?