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
    439.70
    Big Dating
    Share post

    Comments 36

      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. Он имел ввиду сам редактор, а я почему-то решил, что речь о весе билда игры. Вопрос снят :)
                                0
                                Не забывайте про ООП

                                Это, скорее, вредный совет, чем полезный.
                                ООП, по сути, реализовано на уровне движка. Дробите код на простые компоненты (PlayerMove, PlayerHealth, PlayerAttack и т.д.), выводите параметры в инспектор (maxHealth, speed, damage...) — и далее легко наклепать префабов, который легко кастомизируются, можно навесить нужные компоненты, удалять ненужные, играться с параметрами. А с появлением Prefab Variants и NestedPrefabs вообще надобность в наследовании отпала. Теперь можно сделать Enemy, BigEnemy, FastEnemy и т.д., которые будут отличаться вариантами настроек, и сохранить их как Variants.
                                Лучше создать пару отдельных компонентов (PlayerMove и EnemyMove), чем наследовать их от базового Move, а потом потребуется создать BossMove, у которого абсолютно другая логика перемещения. И ты задолбаешься исправлять базовый класс, чинить наследников, и отгадывать, какой функционал унаследовался, а какой нет.
                                Меньше тупого следования паттернам, меньше ООП, и больше юзания функционала движка.

                                Пишите подробные логи.

                                … и не забывайте убирать их перед релизом.
                                Это ж какое раздолье для хакеров! Представь, что ты оставил лог, который выводит логин-пароль или инфу об игровой валюте.
                                К тому же, это лишние вызовы методов — лишняя затрата ресурсов.

                                Я делал глупости. Предположим, мы хотим как-то хранить настройки различных уровней какой-то игры

                                Для этого существует ScriptableObject. Хранить их в struct — это чушь.

                                Например, можно создать единый SAVE-объект, который хранит в себе все настройки и данные игрока

                                НИКОГДА так не делайте. Очередной подарок хакерам.
                                Любой ребенок себе сможет накрутить 9999 голдов и бесконечное здоровье.
                                Сэйвы нужно хранить в зашифрованном бинарнике. А критичные данные (типа игровой валюты, прокачки, ценных предметов) — на сервере, особенно это ММО касается. Если в синглплеере еще допустимо читерство (в конце концов, сам себе портит удовольствие то игры), то в многопользовательских играх — ни в коем случае.

                                Only users with full accounts can post comments. Log in, please.