company_banner

Ошибки новичка Unity, испытанные на собственной шкуре

    Привет, Хабр. Это снова я, Илья Кудинов, 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 и навыков программирования смог бы написать свою первую игру. На русском языке качественных туториалов такого формата не очень много. Как думаете, стоит попробовать?
    Badoo 406,63
    Big Dating
    Поделиться публикацией
    Похожие публикации

    Вакансии компании Badoo

    Комментарии 35
    • 0
      Привет. А в итоге, что за игра-то? Понял, что это 2d что-то, но не более.
      • 0
        Хабр не любит пиара — и конкретизация нашей игры пост богаче не сделает (:
        • 0

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


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

          • +1

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

          • 0
            Спасибо за пост, выцепил для себя пару полезных советов! Сам сейчас нахожусь в ситуации, когда программировать вроде как умею, а опыта в Enterprise-разработке нет, и тоже потихоньку пилю свою игру на Юнити.
            P.S. Присоединяюсь к требующим ссылку на игру))
            • 0
              Наверное, мы уже достаточно глубоко в ветке комментов и сюда никто не залезет? Наш дебютный проект — store.steampowered.com/app/375560/DungeonRift
              • –1
                Ну вот, а в чём проблема?)) Многие непонятно почему так боятся 'случайного пиара", что усердно добавляют в текст конструкции вроде «одна хорошая компания», «магазин на букву А», «известная игра про футбол» и так далее. А в чём проблема назвать полностью? Что плохого в том, чтобы вольно или невольно пропиарить то, что лично ты считаешь хорошим?
                • +1
                  Пару раз видел когда это приводило к всплескам ярости «Хабр не для рекламы!» и занижению как статьи так и кармы. Я не очень в восторге от такой перспективы, так что если счастливее меня прямые упоминания не сделают — постараюсь их избежать (:
        • +3

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

          • +1

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

            • 0
              Благодарю, исправлюсь (:
            • +2
              Все верно. А еще было бы лучше и удобнее оформить эти методы как extension, что бы вызывать таким образом: ExampleClass a = list.GetRandomFromList();
            • +1
              Не написали ИМХО одну важную вещь про крайнюю полезность SerializedObject, с помощью него можно реализовывать логику объекта для которого не всегда требуются GameObject'ы, например предметы инвентаря.
              • +1
                Дааа, самому бы сначала не полениться и разобраться со ScriptableObject до конца т.т
                А то, в принципе-то, необходимости в них нет, они просто делают удобнее, поэтому как до некритичной темы я постоянно ленюсь до них добраться. Так что это ошибка новичка, которую я делаю всё время (:
              • +1
                Спасибо, нашел несколько полезных советов для себя)
                • +2
                  Объединяйте однотипные спрайты (а тем более отдельные спрайты одной анимации) в общую картинку

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

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

                      ISP про другое. Тут умный термин — Single responsibility principle из того же SOLID
                      • 0
                        SRP я упоминаю чуть ниже (:
                        • +2
                          Да, но ведь он там не к месту :) И где Liskov substitution principle?
                      • +3
                        Я за туториал вида “Делаем игрушку для хакатона за сутки с нуля”.
                        • +2
                          Очень хочется определиться с начальным уровнем аудитории. Это кто-то, понимающий хоть что-то в программировании и ему не надо будет объяснять что такое класс или условный оператор, или это человек, впервые севший за компьютер? Пока не могу однозначно решить.
                          • +3
                            Это кто-то, понимающий хоть что-то в программировании и ему не надо будет объяснять что такое класс или условный оператор

                            Однозначно для таких. Первых лучше отдельно хоть чуток подружить с программированием, после чего уже переходить к шагу «делаем игру» :-)
                            • +1
                              подпишусь на тутор.
                              особенно хотелось бы про
                              Избегайте GameObject.Find()

                              т.е. да, я создаю SceneController, и у всех создаваемых объектов в старте идет "пропиши себя в SceneController", а потом, если надо обратиться из перчатки левой руки в ножик правой руки, то иду в голову и, по путям, через точку, -> в ножик другой руки.
                              public class LeftHandGlove: MonoBehaviour { <-- скрипт на объекте
                              ...
                              void Start()
                              {
                              GameObject _app = GameObject.Find("_app"); // DontDestroyOnLoad() persistent
                              MHead = _app.GetComponent(); // взяли голову из persist
                              MHead.LeftGlove = this.gameObject; // <-- Self Binding to ManHead


                              далее его можем модифицировать из других объектов (скриптов) в виде:

                              (...)      MHead.LeftGlove.GetComponent<LeftScript>().MyState = 200;
                              т.е.:
                              (controler 3D object ->) на нем Controller script -> Totem_3D_object -> и его Script -> и в нем public переменная
                              Тутор нужен, потому что «с разбегу» трудно понять где графический объект, а где его скриптовый компонент (или еще хуже, несколько атачнутых скриптов)" и как их искать без Find()-a… даже на (не суперовую) конструкцию выше — у меня ушло больше года.
                        • +1
                          Простите, но:
                          удобнее воспользоваться встроенной утилитой утилитой SpritePacker.
                          • 0
                            Благодарю, я иногда дно (:
                          • +2
                            Зачем хранить в памяти все текста на всех языках в рантайме? Проще подгружать 1 язык, при смене — подгружать следующий. В примерах Unity есть урок как сделать локализацию.
                            • 0
                              Это такое весьма абстрактное «проще». Имплементировать это может быть чуть-чуть сложнее, а экономия памяти будет сколько, сотня-другая килобайт? При том, сколько памяти занимают игры на Unity сами собой это абсолютно не принципиальная разница.

                              Но да, так, конечно, делать правильнее.
                              • +1
                                От проекта зависит. У нас в одном проекте локализации всех текстов и квестов 1 мб (русский и основные европейские языки). Причем менеджер локализаций написан 1 раз давно и кочует из проекта в проект.
                            • 0
                              Всё хорошо в Unity кроме её веса. Собрал на ней пару безделушек-паззлов и забросил. Присматриваюсь к Godot.
                              • 0
                                Там нужно очень досконально смотреть настройки. Порой снятие одной галочки урезает по 30-40% от общего веса игры.
                                • 0
                                  Это да, но сама среда очень громоздкая. Около гига сама Unity без никто. Godot — 40 мегабайт. Шаблоны экспорта под все платформы оптом — чуть более двухсот мегабайт. Для 2D-игрушек — самое оно.
                                  • 0
                                    А Вы не путаете Юнити с Анрилом или Краем? Там да, на Анриле чистый билд, без ничего, весит пару сотен Мб, на Крае и того больше. А на Юнити многие делают мобильные игры весом меньше 50 Мб. Я сейчас заканчиваю игру (не мобильную) на Юнити, весит около 400 Мб, из них 3/4 — вес JPEG-спрайтов. Где там «Около гига сама Unity без никто.», если честно, непонятно.
                                    • 0
                                      Мне кажется, Bookvarenko говорит про гигайбайт веса САМОГО Юнити. Среды разработки. Что, кажется, правда — у меня сейчас со всеми модулями папка с редактором (без Mono) весит 6 с небольшим гигабайт.

                                      С другой стороны, я совершенно не понимаю, как это может иметь какое-то серьёзное значение.

                                      А мои собранные на Юнити игры весят по 25-30 мегов на мобилки и 400 под десктоп (со всеми ресурсами).

                                      Так что я не совсем могу понять эту претензию (:
                                      • 0
                                        А, точно. Действительно, я неверно понял Bookvarenko. Он имел ввиду сам редактор, а я почему-то решил, что речь о весе билда игры. Вопрос снят :)

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

                              Самое читаемое