Привет, Хабр. Это снова я, Илья Кудинов, QA-инженер из компании Badoo. Но сегодня я расскажу не о тестировании (этим я уже занимался в понедельник), а о геймдеве. Нет, этим мы в Badoo не занимаемся, разрабатывать компьютерные игры — моё хобби.

Профессионалы индустрии, не судите строго. Все советы в этой статье адресованы начинающим разработчикам, решившим попробовать свои силы в 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# — универсальные шаблоны. Например, я написал такие простые и удобные методы для выдёргивания случайного элемента произвольного класса из List<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 и навыков программирования смог бы написать свою первую игру. На русском языке качественных туториалов такого формата не очень много. Как думаете, стоит попробовать?

Комментарии (28)


  1. dmonin
    27.12.2017 20:20

    Привет. А в итоге, что за игра-то? Понял, что это 2d что-то, но не более.


    1. Relz Автор
      27.12.2017 20:20

      Хабр не любит пиара — и конкретизация нашей игры пост богаче не сделает (:


      1. nizkopal
        27.12.2017 21:18

        Я думаю, ссылка в комментарии, да еще и по просьбе двух людей (а я тоже хочу) — это святое! За такое Хабр не обидется.


        Ссылку, ссылку!


        1. Relz Автор
          27.12.2017 21:50

          Виталий, это низко.


      1. j0ez
        27.12.2017 22:36

        Спасибо за пост, выцепил для себя пару полезных советов! Сам сейчас нахожусь в ситуации, когда программировать вроде как умею, а опыта в Enterprise-разработке нет, и тоже потихоньку пилю свою игру на Юнити.
        P.S. Присоединяюсь к требующим ссылку на игру))


        1. Relz Автор
          27.12.2017 22:36

          Наверное, мы уже достаточно глубоко в ветке комментов и сюда никто не залезет? Наш дебютный проект — store.steampowered.com/app/375560/DungeonRift


          1. StjarnornasFred
            28.12.2017 14:55

            Ну вот, а в чём проблема?)) Многие непонятно почему так боятся 'случайного пиара", что усердно добавляют в текст конструкции вроде «одна хорошая компания», «магазин на букву А», «известная игра про футбол» и так далее. А в чём проблема назвать полностью? Что плохого в том, чтобы вольно или невольно пропиарить то, что лично ты считаешь хорошим?


            1. Relz Автор
              28.12.2017 15:10

              Пару раз видел когда это приводило к всплескам ярости «Хабр не для рекламы!» и занижению как статьи так и кармы. Я не очень в восторге от такой перспективы, так что если счастливее меня прямые упоминания не сделают — постараюсь их избежать (:


  1. leotsarev
    28.12.2017 09:22

    Классическая ошибка. В PullRandomFromList вы зря передаёте List как ref.
    Ref нужен, если вы хотите иметь возможность заставить аргумент начать показывать на другой лист. А возможность поменять лист у вас и так есть.


    1. leotsarev
      28.12.2017 09:24

      В первом методе можно принимать не List, а IReadOnlyList. Так вы сможете передать туда не только List, но и все, что поддерживает этот интерфейс


      1. Relz Автор
        28.12.2017 09:53

        Благодарю, исправлюсь (:


    1. LeonThundeR
      28.12.2017 12:00
      +1

      Все верно. А еще было бы лучше и удобнее оформить эти методы как extension, что бы вызывать таким образом: ExampleClass a = list.GetRandomFromList();


  1. IamNoExist
    28.12.2017 09:48
    +1

    Не написали ИМХО одну важную вещь про крайнюю полезность SerializedObject, с помощью него можно реализовывать логику объекта для которого не всегда требуются GameObject'ы, например предметы инвентаря.


    1. Relz Автор
      28.12.2017 09:58

      Дааа, самому бы сначала не полениться и разобраться со ScriptableObject до конца т.т
      А то, в принципе-то, необходимости в них нет, они просто делают удобнее, поэтому как до некритичной темы я постоянно ленюсь до них добраться. Так что это ошибка новичка, которую я делаю всё время (:


  1. Satim
    28.12.2017 09:53
    +1

    Спасибо, нашел несколько полезных советов для себя)


  1. KonH
    28.12.2017 10:33

    Объединяйте однотипные спрайты (а тем более отдельные спрайты одной анимации) в общую картинку

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


  1. Charoplet
    28.12.2017 10:58

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

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


  1. Shersh
    28.12.2017 11:45

    Умный термин — Interface segregation principle.

    ISP про другое. Тут умный термин — Single responsibility principle из того же SOLID


    1. Relz Автор
      28.12.2017 12:03

      SRP я упоминаю чуть ниже (:


      1. Shersh
        28.12.2017 12:05
        +1

        Да, но ведь он там не к месту :) И где Liskov substitution principle?


  1. deeand
    28.12.2017 14:26
    +1

    Я за туториал вида “Делаем игрушку для хакатона за сутки с нуля”.


    1. Relz Автор
      28.12.2017 14:27
      +1

      Очень хочется определиться с начальным уровнем аудитории. Это кто-то, понимающий хоть что-то в программировании и ему не надо будет объяснять что такое класс или условный оператор, или это человек, впервые севший за компьютер? Пока не могу однозначно решить.


      1. Newbilius
        28.12.2017 14:39

        Это кто-то, понимающий хоть что-то в программировании и ему не надо будет объяснять что такое класс или условный оператор

        Однозначно для таких. Первых лучше отдельно хоть чуток подружить с программированием, после чего уже переходить к шагу «делаем игру» :-)


  1. aahart
    28.12.2017 17:14

    Простите, но:

    удобнее воспользоваться встроенной утилитой утилитой SpritePacker.


    1. Relz Автор
      28.12.2017 17:15

      Благодарю, я иногда дно (:


  1. Igor_Sib
    28.12.2017 18:18
    +2

    Зачем хранить в памяти все текста на всех языках в рантайме? Проще подгружать 1 язык, при смене — подгружать следующий. В примерах Unity есть урок как сделать локализацию.


    1. Relz Автор
      28.12.2017 18:22

      Это такое весьма абстрактное «проще». Имплементировать это может быть чуть-чуть сложнее, а экономия памяти будет сколько, сотня-другая килобайт? При том, сколько памяти занимают игры на Unity сами собой это абсолютно не принципиальная разница.

      Но да, так, конечно, делать правильнее.


      1. Igor_Sib
        28.12.2017 18:30
        +1

        От проекта зависит. У нас в одном проекте локализации всех текстов и квестов 1 мб (русский и основные европейские языки). Причем менеджер локализаций написан 1 раз давно и кочует из проекта в проект.