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

В большинстве проектов, которые я видел, разработчики выполняют множество задач, которые повторяются и приводят к ошибкам, особенно когда речь идет об интеграции новых художественных активов. Например, настройка персонажа часто включает в себя перетаскивание множества ссылок на активы, установку флажков и нажатие кнопок: Установите риг модели на Humanoid, отключите sRGB для текстуры SDF, установите карты нормалей как карты нормалей, а текстуры UI как спрайты. Другими словами, тратится драгоценное время, а важнейшие шаги могут быть пропущены.
В этой статье, состоящей из двух частей, я расскажу вам о хаках, которые помогут улучшить этот рабочий процесс, чтобы ваш следующий проект прошел более гладко, чем предыдущий. Чтобы проиллюстрировать это, я создал простой прототип, похожий на RTS, где юниты одной команды автоматически атакуют вражеские здания и других юнитов. С каждым скриптовым хаком я буду улучшать один из аспектов этого процесса, будь то текстуры или модели.
Вот как выглядит прототип:
Основная причина, по которой разработчикам приходится настраивать так много мелких деталей при импорте активов, проста: Unity не знает, как вы собираетесь использовать актив, поэтому не может знать, какие настройки для него лучше всего подходят. Если вы хотите автоматизировать некоторые из этих задач, это первая проблема, которую необходимо решить.
Самый простой способ определить, для чего предназначен тот или иной актив и как он связан с другими, - это придерживаться определенного соглашения об именовании и структуры папок, например:
- Соглашение об именовании: Мы можем добавлять вещи к названию самого актива, поэтому Shield_BC.png - это базовый цвет, а Shield_N.png - карта нормалей.
- Структура папки: Knight/Animations/Walk.fbx - это явно анимация, а Knight/Models/Knight.fbx - модель, хотя они оба имеют одинаковый формат (.fbx).
Проблема в том, что это хорошо работает только в одном направлении. Поэтому, хотя вы можете уже знать, для чего нужен актив, если вам известен его путь, вы не сможете сделать вывод о его пути, если вам дана только информация о том, что этот актив делает. Возможность найти актив - например, материал для персонажа - полезна при попытке автоматизировать настройку некоторых аспектов активов. Хотя эту проблему можно решить, используя жесткое соглашение об именовании, чтобы путь был легко выводим, это все равно чревато ошибками. Даже если вы помните о правилах, опечатки встречаются часто.
Интересный подход к решению этой проблемы - использование меток. Вы можете использовать скрипт редактора, который анализирует пути к активам и присваивает им соответствующие метки. Поскольку этикетки автоматизированы, можно точно определить, какая этикетка будет у актива. Вы даже можете искать активы по их метке, используя AssetDatabase.FindAssets.
Если вы хотите автоматизировать эту последовательность действий, есть класс, который может быть очень удобным, называется AssetPostprocessor. AssetPostprocessor получает различные сообщения, когда Unity импортирует активы. Одним из них является OnPostprocessAllAssetsметод, который вызывается каждый раз, когда Unity завершает импорт активов. Он предоставит вам все пути к импортированным активам и даст возможность обработать эти пути. Для их обработки можно написать простой метод, например, следующий:
В случае с прототипом мы сосредоточимся на списке импортированных активов - как для того, чтобы попытаться отловить новые, так и для того, чтобы переместить активы. В конце концов, при изменении пути мы можем захотеть обновить метки.
Чтобы создать ярлыки, разберите путь и найдите соответствующие папки, префиксы и суффиксы имен, а также расширения. После создания меток объедините их в одну строку и установите ее на актив.
Чтобы назначить метки, загрузите актив с помощью AssetDatabase.LoadAssetAtPath, а затем назначьте ему метки с помощью AssetDatabase.SetLabels.
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Помните, что важно устанавливать ярлыки только в том случае, если они действительно изменились. Установка меток приведет к повторному импорту актива, поэтому не стоит делать этого без крайней необходимости.
Если вы проверили это, то повторный импорт не будет проблемой: Ярлыки устанавливаются при первом импорте актива и сохраняются в файле .meta, что означает, что они также сохраняются в системе контроля версий. Повторный импорт будет выполнен только в том случае, если вы переименуете или переместите свои активы.
После выполнения вышеуказанных действий все активы автоматически маркируются, как показано в примере ниже.

Импорт текстур в проект обычно связан с изменением настроек каждой текстуры. Это обычная текстура? Обычная карта? Спрайт? Линейный или sRGB? Если вы хотите изменить настройки импортера активов, вы можете снова воспользоваться AssetPostprocessor.
В этом случае вы захотите использовать OnPreprocessTexture которое вызывается непосредственно перед импортом текстуры. Это позволит вам изменить настройки импортера.
Когда дело доходит до выбора правильных настроек для каждой текстуры, необходимо проверить, с каким типом текстур вы работаете - именно поэтому на первом этапе ключевую роль играют метки.
Имея эту информацию, вы можете написать простой TexturePreprocessor:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Важно убедиться, что вы выполняете эту процедуру только для текстур, имеющих метку art (наши собственные текстуры). После этого вы получите ссылку на импортер, чтобы можно было все настроить - начиная с размера текстуры.
AssetPostprocessor имеет свойство context, с помощью которого можно определить целевую платформу. Таким образом, вы можете вносить изменения для конкретной платформы, например, устанавливать текстуры в более низком разрешении для мобильных устройств:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Затем проверьте метку, чтобы узнать, является ли текстура текстурой пользовательского интерфейса, и установите ее соответствующим образом:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Для остальных текстур установите значения по умолчанию. Стоит отметить, что Albedo - единственная текстура, в которой будет включен sRGB:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Благодаря вышеупомянутому скрипту, когда вы перетащите новые текстуры в редактор, они автоматически получат нужные настройки.
Под "упаковкой каналов" понимается объединение различных текстур в одну с помощью различных каналов. Он широко распространен и имеет множество преимуществ. Например, значение красного канала - это металлик, а значение зеленого канала - его гладкость.
Однако объединение всех текстур в одну требует от команды художников дополнительной работы. Если по какой-то причине упаковка должна быть изменена (например, изменен шейдер), команде художников придется переделать все текстуры, которые используются с этим шейдером.
Как видите, здесь есть что улучшать. Я предпочитаю использовать для упаковки каналов специальный тип актива, в котором задаются "сырые" текстуры и генерируется упакованная в канал текстура для использования в материалах.
Сначала я создаю фиктивный файл с определенным расширением, а затем использую Импортер со сценарием который выполняет всю тяжелую работу по импорту этого актива. Вот как это работает:
- Импортеры могут иметь параметры, например, текстуры, которые нужно объединить.
- В импортере можно установить текстуры как зависимость, что позволит повторно импортировать фиктивный актив при каждом изменении одной из исходных текстур. Это позволит вам перестроить сгенерированные текстуры соответствующим образом.
- У импортера есть версия. Если вам нужно изменить способ упаковки текстур, вы можете модифицировать импортер и изменить версию. Это приведет к регенерации всех упакованных текстур в вашем проекте, и все будет упаковано по-новому, немедленно.
- Приятным побочным эффектом генерации вещей в импортере является то, что сгенерированные активы находятся только в папке Library, поэтому они не заполняют ваш контроль версий.
Чтобы реализовать это, создайте объект ScriptableObject, который будет хранить созданные текстуры и служить результатом работы импортера. В примере я назвал этот класс TexturePack.
Создав его, вы можете начать с объявления класса импортера и добавления атрибута ScriptedImporterAttribute для определения версии и расширения, связанных с импортером:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
В импортере объявите поля, которые вы хотите использовать. Они появятся в Инспекторе, как и MonoBehaviours и ScriptableObjects:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.

Подготовив параметры, создайте новые текстуры из тех, которые вы задали в качестве параметров. Однако обратите внимание, что в препроцессоре (из предыдущего раздела) для этого мы установили значение isReadable равным True.
В этом прототипе вы заметите две текстуры: Albedo, которая содержит Albedo в RGB и маску для применения цвета игрока в Alpha, и текстуру Mask, которая включает металлик в красном канале и сглаживание в зеленом канале.
Хотя это, возможно, выходит за рамки данной статьи, давайте рассмотрим, как объединить Albedo и маску игрока в качестве примера. Сначала проверьте, установлены ли текстуры, и если да, то получите данные об их цвете. Затем установите текстуры в качестве зависимостей с помощью AssetImportContext.DependsOnArtifact. Как уже говорилось выше, это заставит объект пересчитываться, если какая-либо из текстур изменится.
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Вам также нужно создать новую текстуру. Для этого получите размер из TexturePreprocessor, который вы создали в предыдущем разделе, так, чтобы он соответствовал заданным ограничениям:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Затем заполните все данные для новой текстуры. Это можно значительно оптимизировать, используя Jobs и Burst (но это потребует отдельной статьи). Здесь мы будем использовать простой цикл:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Установите эти данные в текстуру:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Теперь вы можете создать метод для генерации другой текстуры очень похожим образом. Когда все будет готово, создайте основную часть импортера. В данном случае мы создадим только объект ScriptableObject, который будет хранить результаты, создавать текстуры и устанавливать результат работы импортера через AssetImportContext.
Когда вы пишете импортер, все создаваемые активы должны быть зарегистрированы с помощью AssetImportContext.AddObjectToAsset чтобы они появились в окне проекта. Выберите основной актив с помощью AssetImportContext.SetMainObject. Вот как это выглядит:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Осталось только создать фиктивные активы. Поскольку они являются пользовательскими, вы не можете использовать CreateAssetMenuатрибут. Вы должны сделать их вручную.
Используя атрибут MenuItem, укажите полный путь к меню создания актива, Assets/Create. Чтобы создать актив, используйте ProjectWindowUtil.CreateAssetWithContent, который генерирует файл с указанным вами содержимым и позволяет пользователю ввести его имя. Выглядит это следующим образом:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Наконец, создайте текстуры с канальным наполнением.
В большинстве проектов используются собственные шейдеры. Иногда они используются для добавления дополнительных эффектов, например, эффекта растворения для затухания поверженных врагов, а иногда шейдеры реализуют пользовательский художественный стиль, например, шейдеры тонов. Независимо от варианта использования, Unity создаст новые материалы с шейдером по умолчанию, и вам нужно будет изменить его, чтобы использовать пользовательский шейдер.
В этом примере шейдер, используемый для юнитов, имеет две дополнительные функции: эффект растворения и цвет игрока (красный и синий в видеопрототипе). При их внедрении в проект вы должны убедиться, что все здания и блоки используют соответствующий шейдер.
Для проверки соответствия актива определенным требованиям - в данном случае, что он использует правильный шейдер - существует еще один полезный класс: AssetModificationProcessor. С помощью AssetModificationProcessor.OnWillSaveAssetsв частности, вы будете получать уведомления, когда Unity собирается записать актив на диск. Это даст вам возможность проверить правильность актива и исправить его до того, как он будет сохранен.
Кроме того, вы можете "попросить" Unity не сохранять актив, что эффективно в тех случаях, когда обнаруженная проблема не может быть устранена автоматически. Для этого создайте метод OnWillSaveAssets :
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Чтобы обработать активы, проверьте, являются ли они материалами и имеют ли они правильные этикетки. Если они соответствуют приведенному ниже коду, значит, у вас правильный шейдер:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Удобно то, что этот код также вызывается при создании актива, а значит, новый материал будет иметь правильный шейдер.
В качестве новой функции в Unity 2022 мы также получили Варианты материалов. Варианты материалов невероятно полезны при создании материалов для юнитов. На самом деле, вы можете создать базовый материал и вывести из него материалы для каждого блока, переопределив соответствующие поля (например, текстуры) и унаследовав остальные свойства. Это позволяет установить твердые значения по умолчанию для наших материалов, которые можно обновлять по мере необходимости.
Импорт анимации аналогичен импорту текстур. Необходимо установить различные настройки, некоторые из которых можно автоматизировать.
По умолчанию Unity импортирует материалы из всех файлов FBX (.fbx). Для анимации материалы, которые вы хотите использовать, будут находиться либо в проекте, либо в FBX сетки. Дополнительные материалы из FBX анимации появляются каждый раз, когда вы ищете материалы в проекте, добавляя довольно много шума, поэтому их стоит отключить.
Для настройки рига - то есть выбора между Humanoid и Generic, а в случаях, когда мы используем тщательно настроенный аватар, и его назначения - примените тот же подход, что и при работе с текстурами. Но для анимации вы будете использовать следующее сообщение AssetPostprocessor.OnPreprocessModel. Эта функция будет вызываться для всех FBX-файлов, поэтому вам нужно отличать FBX-файлы анимации от FBX-файлов модели.
Благодаря ярлыкам, которые вы установили ранее, это не должно быть слишком сложно. Метод начинается так же, как и в случае с текстурами:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Далее вы захотите использовать риг из сетки FBX, поэтому вам нужно найти этот актив. Чтобы найти объект, снова воспользуйтесь метками. В случае с этим прототипом анимации имеют метки, заканчивающиеся на "animation", а сетки - на "model". Вы можете выполнить простую замену, чтобы получить этикетку для своей модели. Получив метку, найдите актив с помощью AssetDatabase.FindAssets с параметром "l:label-name".
При получении доступа к другим активам необходимо учитывать еще кое-что: Возможно, что в середине процесса импорта аватар еще не был импортирован, когда будет вызван этот метод. Если это произойдет, LoadAssetAtPath вернет null, и вы не сможете установить аватар. Чтобы обойти эту проблему, установите зависимость от пути к аватару. Анимация будет импортирована снова, как только аватар будет импортирован, и вы сможете установить ее там.
Если перевести все это в код, то это будет выглядеть примерно так:
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Теперь вы можете перетащить анимации в нужную папку, и если ваша сетка готова, каждая из них будет установлена автоматически. Но если при импорте анимации аватар не будет доступен, проект не сможет подхватить его после создания. Вместо этого вам придется вручную импортировать анимацию после ее создания. Это можно сделать, щелкнув правой кнопкой мыши на папке с анимацией и выбрав пункт Reimport.
Все это вы можете увидеть в примере видео ниже.
Используя те же идеи, что и в предыдущих разделах, создайте модели, которые вы будете использовать. В этом случае используйте AssetPostrocessor.OnPreprocessModel , чтобы установить настройки импортера для этой модели.
Для прототипа я настроил импортер так, чтобы он не генерировал материалы (я буду использовать те, что создал в проекте), и проверил, является ли модель юнитом или зданием (как всегда, проверив метку). Юниты генерируют аватар, но создание аватара для зданий отключено, поскольку здания не анимированы.
Для вашего проекта вы, возможно, захотите задать материалы и аниматоров (и все остальное, что вы хотите добавить) при импорте модели. Таким образом, созданный импортером префаб будет готов к немедленному использованию.
Чтобы сделать это, воспользуйтесь функцией AssetPostprocessor.OnPostprocessModel метод. Этот метод вызывается после завершения импорта модели. Он получает сгенерированный префаб в качестве параметра, что позволяет нам изменять префаб так, как мы хотим.
Для прототипа я нашел материал и контроллер анимации по метке, так же как и аватар для анимации. С помощью рендерера и аниматора в префабе я устанавливаю материал и контроллер, как в обычном игровом процессе.
Затем вы можете поместить модель в свой проект, и она будет готова к использованию в любой сцене. За исключением того, что мы не установили никаких компонентов, связанных с геймплеем, о которых я расскажу во второй части этого блога.
С помощью этих советов по созданию сценариев вы уже практически готовы к игре. Следите за следующей частью этого двухчастного цикла Tech from the Trenches в которой мы расскажем о хаках для балансировки игровых данных и многом другом.
Если вы хотите обсудить статью или поделиться своими идеями после ее прочтения, заходите на наш форум Scripting. Вы также можете связаться со мной в Twitter по адресу @CaballolD.
