Пропуск перезагрузки домена: более быстрая итерация с использованием функции MethodReload

Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.
Мы все через это проходили: ты меняешь какой-то игровой код, а потом ждешь, пока Редактор завершит перезагрузку домена. Процесс может замедлить время ваших итераций и действительно истощить ваше творчество. Не говоря уже о дополнительном времени, потраченном на итерации на устройстве, где каждый цикл требует полной сборки.
Наш ответ на это — MethodReload. Мы продемонстрировали это на сцене на Unite Seoul, и теперь это доступно как экспериментальный пакет. В этой статье мы рассмотрим, как использовать MethodReload, каковы его ограничения и как он соотносится с инструментами горячей перезагрузки, которые, возможно, уже есть в вашем проекте.
Почему мы застряли в перезагрузке
Немного истории здесь поможет, потому что причина существования перезагрузки домена — это также причина, по которой так трудно от нее избавиться.
Play Mode был построен на AppDomains Mono, где изменения кода запускали знакомый процесс: снос домена, загрузка новых сборок, повторный JIT и затем возобновление в том же процессе. Годами это была настоящая суперсила, и это большая часть того, почему итерации в Unity казались быстрыми по сравнению с созданием игрока.
Это также тупой инструмент, что означает, что во время перезарядки всё исчезает, а потом всё возвращается, независимо от того, изменили вы один метод или сто. Кроме того, IL2CPP вообще не поддерживает перезагрузку доменов, поэтому каждое изменение требует пересборки всего игрока.
Вы можете спрашивать себя: «Разве CoreCLR это исправит?» и ответ: Не волшебно. Хотя CoreCLR избавляется от domain reloads, ему все равно приходится перезагружать сборки, и MethodReload может помочь и здесь.
Как использовать MethodReload
Как упоминалось выше, MethodReload теперь доступен и поставляется в com.unity.pipeline, в настоящее время в версии 0.7.0-exp.1.
Процесс использования MethodReload довольно прост. Просто пометьте один или несколько методов атрибутом [MethodReload], запустите наблюдателя, войдите в режим Play Mode или прикрепите игрока для разработки, а затем отредактируйте тело ваших методов. Запущенный экземпляр подхватывает новое тело при следующем вызове.
Однако MethodReload не заменяет полные перезагрузки домена. Более крупные изменения все равно проходят через обычную компиляцию. Но для мелких доработок, составляющих большую часть итерационного цикла, это превращает долгое ожидание в дело секунд, как в Редакторе, так и на устройстве.
Попробуй
Самый быстрый способ понять MethodReload — запустить его, поэтому, если вы хотите протестировать это самостоятельно, вот несколько шагов, чтобы начать. Примечание: поскольку это экспериментальная функция, процесс может измениться в будущем и отличаться от описанного здесь
1. Установите пакет (требуется Unity 6.0+). Вы можете установить его через Unity CLI или использовать Менеджер пакетов, чтобы вручную добавить пакет Pipeline, используя опцию «Установить пакет по техническому имени» и эту строку: com.unity.pipeline@0.7.0-exp.1.
2. Отметьте методы, по которым вы итерируетесь. Атрибут должен присутствовать до того, как вы войдете в режим Play или соберете игрока, потому что проверка диспетчеризации вплетена во время компиляции. Методы с тегами должны быть public (экземпляром или статическим) и возвращать void или IEnumerator; все остальное пропускается во время сборки с предупреждением.
using Unity.Pipeline.MethodReload;
using UnityEngine;
public class Spinner : MonoBehaviour
{
public float rotationSpeed = 90f;
[MethodReload]
public void Update()
{
transform.Rotate(Vector3.up, rotationSpeed * Time.deltaTime);
}
}3. Запустите наблюдателя. Чтобы ваш MethodReload увидел ваши изменения в реальном времени, вам нужно запустить Watcher MethodReload. Сделайте это, перейдя в Настройки проекта → Конвейер → Редактор, а затем в разделе «Метод переподключения наблюдателя интерпретатора» нажмите «Начать наблюдение».

В то время как наблюдатель покрывает все дерево Ассетов, сохраненный .cs компилируется только в том случае, если он содержит атрибут MethodReload, поэтому несвязанные сохранения ничего вам не стоят.
4. Если тестирование в редакторе: войдите в Режим игры. После запуска любые измененные помеченные фрагменты кода будут скомпилированы и внедрены на месте. Как только вы выйдете из режима игры, ваши изменения будут скомпилированы и пройдут обычную перезагрузку домена.
5. Если тестирование на устройстве: подключите устройство. Создайте игрока для разработки с включенным драйвером конвейера времени выполнения. Подключите устройство с помощью Autoconnect Profiler или прикрепите его через окно Profiler или Console. С этого момента каждое сохранение компилируется в Редакторе, используя препроцессорные определения player's и отправляется каждому подключенному игроку. Перезагрузка не требуется ни в одном из направлений.
Две детали рабочего процесса, которые стоит знать заранее:
- Автообновление выключено во время работы наблюдателя, поэтому сохранение перезагружает вместо того, чтобы вызывать перезагрузку домена; другие ресурсы не будут импортированы, пока вы не нажмете Ctrl/Cmd+R, а остановка наблюдения вернет ваши настройки.
- Часы переживают перезагрузки домена: они повторно применяют изменения, которые стерла перезагрузка в режиме Play, и повторно отправляют их игроку, который переподключается, поскольку перезапущенный игрок загружает исходный скомпилированный код.
А как насчет использования CLI?
Если вы предпочитаете работать в терминале, хотите работать через агента или хотите отправить каждый помеченный метод, а не только то, что изменилось с начала наблюдения, та же функциональность предоставляется в виде команд CLI Unity:
# Reload in the Editor through the interpreter
unity command reload_file_editor_interpreter "Assets/Scripts/Spinner.cs"
# Compile in the Editor, push to connected players (folders work too)
unity command reload_file_player_interpreter "Assets/Scripts" --player 2Ограничения
Нет способа обойти перезагрузку домена без компромиссов, а ограничения MethodReload находятся в трех местах: какие изменения он может применить, какой C# он может выполнять и как быстро он выполняется.
Изменяются только тела помеченных методов. Перезагрузка никогда не затрагивает места вызовов, поля или компоновку типов. Никаких новых полей, никаких новых классов. Это ограничение и делает его безопасным для применения к работающему экземпляру, и именно вокруг него вы строите план: решите, над какими методами вы будете работать, и пометьте их. Перезагрузка can может объявить новый метод и вызвать его из перезагруженного тела, но этот метод не может быть точкой входа для перезагрузки, потому что ничто скомпилированное не знает о том, что вызывать его до вашей следующей реальной компиляции.
Он запускает подмножество C#. Интерпретатор собран вручную и должен оставаться достаточно маленьким, чтобы поместиться в игроке, поэтому он не реализует весь C#. В теле [MethodReload] вы не можете использовать:
awaitиTask,try/catch/finally,lock, синтаксис запросов LINQ,checkedдесятичный,динамический, типы отражения, типы потоков- параметры
ref,out, иin, возвращаемые значения и локальные переменныеref, многомерные массивы (разрежены допустимы), обобщенные методы
Что делает работает, покрывает большую часть игрового кода: примитивы, вызовы методов, циклы, делегаты, интерполяция строк, корутина yield, Collections. Анализатор Roslyn поставляется с пакетом и помечает нарушения в вашей IDE только в помеченных участках, поэтому вы узнаете об этом во время набора текста, а не после отправки.
На устройстве интерпретируемый код может вызывать только то, что предоставляет игрок. IL2CPP удаляет код без ссылок, поэтому перезагруженный объект, который вызывает член UnityEngine или BCL, который ваш билд никогда не использовал, не имеет ничего, что можно было бы вызвать. В Редакторе отсутствующие члены связываются по требованию. На устройстве проверка на наличие впереди выявляет отсутствующих членов в виде предупреждений; если код доходит до одного из них, он выбрасывает исключение. Правило большого пальца: меняйте то, что метод делает, а не набор API, от которого он зависит.
Интерпретируемый код медленнее, чем скомпилированный код. Такова природа интерпретатора, независимо от того, является ли сравнение IL2CPP заранее скомпилированным или Mono и CoreCLR только в рантайме. Это влияет только на помеченные методы в сборках для разработки. В релизных сборках нет пролога и интерпретатора; помеченное методом просто выполняется его скомпилированное тело. В конечном счете, решать вам, стоит ли потеря производительности во время итерации того прироста скорости и удобства итерации.
Корутины подхватывают изменения при создании. Метод [MethodReload] IEnumerator использует новое тело при следующем запуске StartCoroutine. Корутины, которые уже запущены, сохраняют тело, с которого они стартовали. Соедините это с [OnMethodReloaded], параметризованным методом экземпляра, который срабатывает на каждом живом экземпляре после перезагрузки, и у вас есть OnEnable без перезагрузки домена для перезапуска долгоживущих циклов:
[OnMethodReloaded]
void OnMethodReloaded()
{
StopAllCoroutines();
StartCoroutine(MainLoop());
}Как это работает под капотом
Это сочетание двух идей: первая заключается в том, что компиляция C# создает байт-код IL, а IL, который остается в пределах скромного подмножества, может быть выполнен интерпретатором вместо компиляции. Этот интерпретатор сам по себе написан на обычном C#, поэтому IL2CPP компилирует его вместе с остальной частью вашей игры, что и позволяет ему работать на устройстве.
Вторая идея — вникнуть в это. Каждый метод [MethodReload] имеет подключенный путь к интерпретатору. Ветвление к интерпретатору осуществляется путем вплетения пролога в помеченные методы во время компиляции. Декомпилировано, это выглядит так:
[MethodReload]
public void TakeDamage(int amount)
{
// weaved during compilation
if (MethodReloadRegistry.HasOverride("Player.TakeDamage") &&
MethodReloadRegistry.TryInvokeMethodReload(
"Player.TakeDamage", this, amount))
{
return;
}
// initial code
hp -= amount;
}Плетеная проверка спрашивает реестр, существует ли переопределение для этого метода. Наблюдатель и CLI заполняют этот реестр свежескомпилированным IL. Если есть запись, интерпретатор выполняет ее, а исходное тело пропускается. В противном случае скомпилированный код работает как обычно.
Как это сравнивается
Уже существуют хорошие инструменты для итерации в C# без перезагрузки домена, и мы предпочли бы быть точными в отношении того, где каждый из них применим, а не делать вид, что их не существует.
Пакеты из Asset Store методы патчинга в работающем рантайме Mono. Это дает им превосходный опыт работы в редакторе, полный C# в перезагружаемом теле, без этапа тегирования и в случае Hot Reload — режим на устройстве для сборок Mono player. Все реальные преимущества. Это также означает, что они не работают с игровым движком IL2CPP, потому что нет среды выполнения для патчинга.
.NET Hot Reload, функция в Visual Studio и Rider для .NET-приложений, применяет изменения через путь редактирования и продолжения в рантайме. Он создан для CoreCLR и не нацелен на среды выполнения Mono или IL2CPP Unity.
MethodReload занимает иное место с его интерпретаторным бэкендом, торгуя покрытием языков для охвата: подмножество C#, но работая на той же сборке разработки IL2CPP, которую вы в противном случае развертывали бы на телефоне или на консольном наборе для разработчиков, по кабелю, который у вас уже подключен, без дополнительного сервера в игроке и без открытого порта.
Честная версия: если ваша боль от итераций находится в Редакторе, существующие инструменты зрелые, и вам следует продолжать использовать то, что для вас работает. Если ваша итерационная боль живет на устройстве, и особенно на IL2CPP, это первый вариант, который туда попадает.
Финальные советы
Если вы сейчас настраиваете рабочий процесс для проекта:
- Отметьте свои горячие точки.
[MethodReload]не стоит ничего в релизных сборках, поэтому отметьте методыUpdate, настройки и ощущения от игры, с которыми вы знаете, что будете работать на устройстве. - Позвольте анализатору оставить вас внутри подмножества. Если метод нуждается в
try/catchилиawait, переместите эту часть в нетегированный вспомогательный метод и перезагрузите логику вокруг него. Анализатор сообщит вам, если вы делаете что-то не так. - Попробуйте в Редакторе сначала. Наблюдатель, или
reload_file_editor_interpreterиз CLI, пропускает вашу правку через тот же интерпретатор, который работает на устройстве. Если это работает там, то это будет работать и на оборудовании, так что вы ловите проблемы с подмножеством и отсутствующими API, не дожидаясь сборки.
Что дальше
Справочник команд, страница настройки среды выполнения и полный список ограничений находятся в com.unity.pipeline docs.
Мы призываем вас попробовать это в ваших проектах и сообщите нам, как все пройдет. Обратная связь очень ценится, и экспериментальные пакеты развиваются быстрее, когда люди активно с ними работают.
Перезагрузка домена никуда не идет для изменений, которые ее требуют. Для изменений, которые не, вы можете перестать ждать.