Engine & platform

Продвинутые скриптовые хаки для редактора, которые сэкономят ваше время, часть 2

JORDI CABALLOL / UNITYSenior Software Engineer
Nov 8, 2022|15 Мин
Продвинутые скриптовые хаки для редактора, которые сэкономят ваше время, часть 2
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.

Я вернулся ко второй части! Если вы пропустили первую часть моей статьи о продвинутых скриптовых хаках для редактора, читайте ее здесь. Эта статья, состоящая из двух частей, посвящена советам редактора по улучшению рабочих процессов, чтобы ваш следующий проект прошел более гладко, чем предыдущий.

Каждый хак основан на созданном мною демонстрационном прототипе, похожем на RTS, где юниты одной команды автоматически атакуют вражеские здания и другие юниты. Для справки: вот прототип начальной сборки:

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

Для начала давайте разберемся с элементами игры. При настройке элементов игры мы часто сталкиваемся со следующим сценарием:

С одной стороны, у нас есть префабы, созданные командой художников - будь то префаб, сгенерированный импортером FBX, или префаб, который был тщательно настроен с использованием всех соответствующих материалов и анимаций, добавлением реквизита в иерархию и т. д. Чтобы использовать этот префаб в игре, имеет смысл создать на его основе вариант префаба и добавить туда все компоненты, связанные с игровым процессом. Таким образом, команда художников может изменять и обновлять префаб, и все изменения сразу же отражаются в игре. Хотя такой подход работает, если для элемента требуется всего пара компонентов с простыми настройками, он может добавить много работы, если вам нужно каждый раз настраивать что-то сложное с нуля.

С другой стороны, многие предметы будут иметь одинаковые компоненты с похожими значениями, как, например, все префабы для автомобилей или префабы для похожих врагов. Логично, что все они являются вариантами одной и той же базовой плиты. Тем не менее, этот подход идеален, если настройка искусства префаба проста (т.е. установка сетки и материалов).

Далее рассмотрим, как упростить настройку компонентов геймплея, чтобы быстро добавлять их в наши арт-префабы и использовать непосредственно в игре.

Взлом 7: Настройте компоненты с самого начала

Чаще всего для сложных элементов в игре я вижу "главный" компонент (например, "враг", "пикап" или "дверь"), который служит интерфейсом для взаимодействия с объектом, и ряд небольших многократно используемых компонентов, реализующих саму функциональность; такие вещи, как "selectable", "CharacterMovement" или "UnitHealth", а также встроенные компоненты Unity, такие как рендеры и коллайдеры.

Работа некоторых компонентов зависит от других компонентов. Например, для перемещения персонажа может потребоваться агент NavMesh. Именно поэтому в Unity есть атрибут RequireComponent, позволяющий определить все эти зависимости. Так что если для данного типа объекта существует компонент "main", вы можете использовать атрибут RequireComponent, чтобы добавить все компоненты, которые должны быть у этого типа объекта.

Например, единицы в моем прототипе имеют такие атрибуты:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Помимо указания легко находимого места в меню AddComponentMenu, включите в него все необходимые дополнительные компоненты. В данном случае я добавил Locomotion для передвижения и AttackComponent для атаки других юнитов.

Кроме того, базовый класс unit (который используется совместно со зданиями) имеет другие атрибуты RequireComponent, которые наследуются этим классом, например, компонент Health. В этом случае мне нужно добавить только компонент Soldier к объекту GameObject, а все остальные компоненты добавятся автоматически. Если я добавлю новый атрибут RequireComponent к компоненту, Unity обновит все существующие GameObject'ы новым компонентом, что облегчит расширение существующих объектов.

У RequireComponent есть и более тонкое преимущество: Если у нас есть "компонент A", которому требуется "компонент B", то добавление A в GameObject не просто гарантирует, что B тоже будет добавлен - оно гарантирует, что B будет добавлен раньше A. Это означает, что когда будет вызван метод Reset для компонента A, компонент B уже будет существовать, и мы легко получим к нему доступ. Это позволяет нам устанавливать ссылки на компоненты, регистрировать постоянные события UnityEvents и делать все остальное, что необходимо для настройки объекта. Комбинируя атрибут RequireComponent и метод Reset, мы можем полностью настроить объект, добавив всего один компонент.

Взлом 8: Поделитесь информацией о компании Unrelated Prefabs

Главный недостаток метода, показанного выше, заключается в том, что, если мы решим изменить значение, нам придется менять его для каждого объекта вручную. А если вся настройка осуществляется через код, дизайнерам становится сложно его модифицировать.

В предыдущей статье мы рассмотрели, как использовать AssetPostprocessor для добавления зависимостей и изменения объектов во время импорта. Теперь давайте воспользуемся этим, чтобы применить некоторые значения в наших префабах.

Чтобы дизайнерам было проще изменять эти значения, мы будем считывать их из префаба. Это позволяет дизайнерам легко модифицировать эту сборную конструкцию, чтобы изменить значения для всего проекта.

Если вы пишете код редактора, вы можете скопировать значения из компонента объекта в другой, воспользовавшись классом Preset.

Создайте пресет из исходного компонента и примените его к другому компоненту (компонентам) следующим образом:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

В нынешнем виде он будет переопределять все значения в префабе, но это, скорее всего, не то, чего мы хотим. Вместо этого скопируйте только некоторые значения, а остальные оставьте нетронутыми. Для этого используйте другую переопределённую функцию Preset.ApplyTo, которая принимает список свойств, которые необходимо применить. Конечно, мы можем легко создать жестко закодированный список свойств, которые мы хотим переопределить, что отлично подойдет для большинства проектов, но давайте посмотрим, как сделать это полностью универсальным.

По сути, я создал базовый Prefab со всеми компонентами, а затем создал Variant, чтобы использовать его в качестве шаблона. Затем я решил, какие значения применить из списка переопределений в Variant.

Чтобы получить переопределения, используйте PrefabUtility.GetPropertyModifications. Это позволит вам получить все переопределения во всем Prefab, поэтому отфильтруйте только те, которые необходимы для данного компонента. Следует помнить, что целью модификации является компонент базового префаба, а не компонент варианта, поэтому нам нужно получить ссылку на него, используя GetCorrespondingObjectFromSource:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Теперь мы применим все переопределения шаблона к нашим префабам. Единственная деталь, которая осталась - это то, что шаблон может быть вариантом варианта, и мы захотим применить переопределения из этого варианта.

Для этого нам нужно лишь сделать его рекурсивным:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Далее давайте найдем шаблон для наших префабов. В идеале мы хотим использовать разные шаблоны для разных типов объектов. Один из эффективных способов сделать это - поместить шаблоны в ту же папку, что и объекты, к которым мы хотим их применить.

Найдите объект с именем Template.prefab в той же папке, что и наш Prefab . Если мы не можем найти его, мы будем рекурсивно искать в родительской папке:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

На данный момент у нас есть возможность изменить шаблон Prefab, и все изменения будут отражены в Prefabs в этой папке, даже если они не являются вариантами шаблона. В этом примере я изменил цвет игрока по умолчанию (цвет, используемый, когда юнит не привязан ни к одному игроку). Обратите внимание, как обновляются все объекты:

Взлом 9: Балансировка игровых данных с помощью ScriptableObjects и электронных таблиц

При балансировке игры все статистические данные, которые необходимо настроить, распределяются по различным компонентам, хранящимся в одном префабе или ScriptableObject для каждого персонажа. Это делает процесс настройки деталей довольно медленным.

Распространенный способ облегчить ведение баланса - использование электронных таблиц. Они могут быть очень удобны, так как сводят все данные воедино, и вы можете использовать формулы для автоматического расчета некоторых дополнительных данных. Но вводить эти данные в Unity вручную может быть мучительно долго.

Вот тут-то и приходят на помощь электронные таблицы. Их можно экспортировать в простые форматы, такие как CSV(.csv) или TSV(.tsv), для чего и предназначены ScriptedImporters. Ниже приведен скриншот со статистикой юнитов в прототипе:

Пример электронной таблицы | Tech from the Trenches

Код для этого довольно прост: Создайте объект ScriptableObject со всеми статистическими данными для юнита, после чего вы сможете прочитать файл. Для каждой строки таблицы создайте экземпляр объекта ScriptableObject и заполните его данными для этой строки.

Наконец, добавьте все объекты ScriptableObjects в импортированный актив с помощью контекста. Нам также нужно добавить главный актив, который я просто установил на пустой TextAsset (поскольку мы не будем использовать главный актив для чего-либо).

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

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Теперь есть несколько объектов ScriptableObject, которые содержат все данные из электронной таблицы.

Импорт данных из электронной таблицы

Сгенерированные объекты ScriptableObjects готовы к использованию в игре по мере необходимости. Вы также можете использовать PrefabPostprocessor, который был настроен ранее.

В методе OnPostprocessPrefab у нас есть возможность загрузить этот актив и использовать его данные для автоматического заполнения параметров компонентов. Более того, если вы установите зависимость от этого актива данных, префабы будут повторно импортироваться каждый раз, когда вы изменяете данные, автоматически поддерживая все в актуальном состоянии.

Взлом 10: Ускорение итерации в редакторе

Когда вы пытаетесь создать потрясающие уровни, очень важно уметь быстро менять и тестировать их, внося небольшие коррективы и пробуя снова. Вот почему так важны быстрые итерации и сокращение количества шагов, необходимых для начала тестирования.

Одна из первых вещей, о которых мы думаем, когда речь заходит о времени итерации в Unity, - это перезагрузка домена. Перезагрузка домена актуальна в двух ключевых ситуациях: после компиляции кода для загрузки новых динамически связанных библиотек (DLL), а также при входе и выходе из режима игры. Перезагрузки домена, которая происходит при компиляции, избежать невозможно, но у вас есть возможность отключить перезагрузку, связанную с режимом воспроизведения, в Настройках проекта > Редактор > Настройки режима воспроизведения.

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

Взлом 11: Автогенерация данных

Отдельная проблема с временем итерации связана с пересчетом данных, необходимых для игры. Для этого часто требуется выбрать некоторые компоненты и нажать на кнопки, чтобы запустить перерасчеты. Например, в этом прототипе есть TeamController для каждой команды на сцене. У этого контроллера есть список всех вражеских зданий, чтобы он мог отправить отряды для их атаки. Чтобы заполнить эти данные автоматически, воспользуйтесь функцией IProcessSceneWithReport интерфейс. Этот интерфейс вызывается для сцен в двух разных случаях: во время сборки и при загрузке сцены в режиме воспроизведения. С ним вы получаете возможность создавать, уничтожать и изменять любые предметы по своему усмотрению. Однако обратите внимание, что эти изменения коснутся только сборки и режима игры.

Именно в этом обратном вызове создаются контроллеры и задается список зданий. Благодаря этому не нужно ничего делать вручную. Контроллеры с обновленным списком зданий будут там, когда начнется игра, и список будет обновляться с учетом внесенных нами изменений.

Для прототипа был создан метод утилиты, который позволяет получить все экземпляры компонента в сцене. С его помощью вы сможете получить все здания:

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Остальная часть процесса довольно тривиальна: Получите все здания, получите все команды, которым принадлежат эти здания, и создайте контроллер для каждой команды со списком вражеских зданий.

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Взлом 12: Работайте на нескольких сценах

Помимо редактируемой сцены, для игры необходимо загрузить и другие сцены (например, сцену с менеджерами, с пользовательским интерфейсом и т. д.). Это может отнять драгоценное время. В случае с прототипом холст с полосками здоровья находится в другой сцене под названием InGameUI.

Эффективным способом решения этой проблемы является добавление к сцене компонента со списком сцен, которые необходимо загрузить вместе с ней. Если загрузить эти сцены синхронно в методе Awake, то сцена будет загружена и все ее методы Awake будут вызваны в этот момент. Таким образом, к моменту вызова метода Start вы можете быть уверены, что все сцены загружены и инициализированы, что дает вам доступ к содержащимся в них данным, таким как синглтоны менеджера.

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

Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Подведение итогов

В первой и второй частях этой статьи я показал вам, как использовать некоторые из менее известных функций, которые предлагает Unity. Все описанное - лишь малая часть того, что можно сделать, но я надеюсь, что вы найдете эти хаки полезными для вашего следующего проекта или, по крайней мере, интересными.

Активы, использованные для создания прототипа, можно бесплатно найти в Asset Store:

Если вы хотите обсудить эту серию или поделиться своими идеями после ее прочтения, заходите на наш форум Scripting. На этом я заканчиваю, но вы все еще можете связаться со мной в Twitter по адресу @CaballolD. Следите за дальнейшими техническими блогами других разработчиков Unity в рамках продолжающейсясерииTech from theTrenches.