Многоигровые стратегии

Во время Обзоров проектов в качестве консультанта команды Customer Success я часто работаю с клиентами, которые создают приложения, меняющие правила игры. Эти приложения имеют одно главное или тематическое меню, представляющее игроку несколько вариантов игр на выбор. В таких системах основными проблемами являются то, как сделать так, чтобы время между переключениями между играми было как можно меньше, и как обеспечить оптимальную производительность во всех играх. В этом блоге мы рассмотрим различные подходы в зависимости от потребностей проекта, а также некоторые лучшие практики, которые могут быть полезны для любой игровой среды, как с настройками геймшифтинга, так и без них.
При планировании мультиприкладной среды - будь то игры, развлечения или промышленное моделирование - самое важное решение, которое необходимо принять, это как управлять исполняемыми файлами игр. Существует множество факторов, которые могут повлиять на это решение:
- С каким количеством игр справится платформа?
- Насколько велики игры?
- Созданы ли игры на одной и той же версии Unity? Каковы "узкие места" приложения?
- Другие факторы - это целевое оборудование, память и процессор, а также скорость вращения диска (SSD vs HDD vs SD Card).
Ответы на эти вопросы и выбор способа работы с исполняемыми файлами очень важны для того, чтобы понять, нужны ли нам отдельные исполняемые файлы для каждой игры; один общий исполняемый файл для нескольких игр или сочетание обоих вариантов для обеспечения оптимальной работы приложений.
Наличие нескольких исполняемых файлов - отличный вариант для работы с играми, созданными в разных версиях Unity. При таком подходе можно сократить время переключения между играми, кэшируя исполняемый файл в памяти и оставляя каждый экземпляр в фоновом режиме. Однако хранение всех исполняемых файлов в памяти не всегда является лучшим выбором, поскольку это может отнимать много памяти. Этого следует избегать в тех случаях, когда отдельные игры занимают больше памяти, и/или когда в приложении для переключения игр много игр.
Чтобы облегчить нехватку памяти, игры могут использовать один исполняемый файл. Игры могут быть в одном проекте Unity или каждая из них может иметь свой собственный проект, при условии, что игры имеют одну и ту же версию Unity. С Unity 2022 LTS в Windows появилась возможность использовать аргумент -datafolder для передачи переменной пути через командную строку ( -datafolder <путь_к_папке> ), указывая выбранную папку данных игры для переключения. Одним из потенциальных недостатков такого подхода является более медленное время переключения игры; поэтому важно следовать лучшим практикам загрузки, чтобы уменьшить этот недостаток.
Независимо от того, какую игру мы разрабатываем и на какой платформе, важно потратить как можно меньше времени с момента выбора игры до ее полной загрузки на экран. Эта цель становится особенно важной для приложений, переключающих игры.
Отличный способ справиться с загрузкой - использовать Addressables. С Addressables содержимое загружается и высвобождается по мере необходимости. Эта стратегия отложенной загрузки - самый эффективный способ сократить время загрузки игр, поскольку она ограничивает объем данных, которые должны быть загружены при первом запуске. Кроме того, он поможет предотвратить любые фоновые действия процессора, связанные с фоновыми играми, которые могут способствовать возникновению узких мест в процессоре. Addressables: Планирование и лучшие практики - это отличная отправная точка для того, чтобы узнать больше об addressables и о том, как они могут помочь улучшить Вашу игру.
Отличный способ обеспечить более быструю загрузку, независимо от того, сколько исполняемых файлов мы используем, - это API асинхронной загрузки. При асинхронной загрузке главный поток Unity будет выполнять процесс, называемый "интеграция главного потока", который отвечает за инициализацию родных и управляемых объектов в разрезанном по времени порядке. Поскольку этот процесс выполняет некоторые операции, которые не являются безопасными для потоков, он будет происходить в главном потоке, а время, отведенное на выполнение интеграции главного потока, ограничено, чтобы предотвратить длительное зависание игры. Количество времени, которое может быть потрачено на интеграцию, определяется свойством Application.backgroundLoadingPriority. Мы рекомендуем установить значение backgroundLoadingPriority на High, или 50 мс, во время экранов загрузки, а затем вернуть его на BelowNormal (4 мс) или Low (2 мс) после завершения загрузки.
Дополнительный способ ускорить загрузку - это асинхронная загрузка текстур. Асинхронная загрузка текстур может уменьшить время загрузки за счет координации того, сколько времени и памяти используется для загрузки текстур и сеток в настройки GPU. В блоге Understanding Async Upload Pipeline содержится подробная информация о том, как работает этот процесс.
Эти приемы помогут ускорить время загрузки:
- Сведите содержание сцен к минимуму, насколько это возможно. Используйте загрузочную сцену, чтобы загружать только то, что необходимо для того, чтобы игра была в пригодном для игры состоянии, а затем загружайте дополнительные сцены, когда это необходимо.
- Отключите камеры во время загрузочных экранов.
- Отключите UI Canvases, пока они заполняются во время загрузки.
- Распараллеливайте сетевые запросы.
- Избегайте сложных реализаций Awake/Start и используйте рабочие потоки.
- Всегда используйте сжатие текстур.
- Потоковая передача больших медиафайлов (например, аудиофайлов и текстур) вместо того, чтобы хранить их в памяти.
- Избегайте JSON-сериализаторов, а вместо них используйте бинарные сериализаторы.
Как уже говорилось ранее, память - не единственная проблема для многопользовательских игровых сред, фоновая активность процессора - это тоже то, что может негативно сказаться на игровом опыте игрока. Когда в игры активно не играют, их процессор продолжает работать, заставляя активную игру работать неоптимально, создавая голодание процессора. Чтобы предотвратить зависание процессора для активной игры и других процессов платформы бэкенда, установите для параметра Run in Background значение false в Unity Settings. Запуск в фоновом режиме заставит игровой цикл Unity остановиться, пока игра не находится в фокусе. Настройку также можно динамически изменить с помощью скрипта
public class ExampleClass : MonoBehaviour
{
void Example()
{
Application.runInBackground = false;
}
}
Следует отметить, что настройка Run in Background не остановит запуск пользовательских потоков сценариев, поэтому важно перевести все потоки неигровых игр в спящий режим с помощью метода Thread.Sleep C#. Помните, что работа с фоновыми потоками в Unity требует тщательного программирования. Поскольку эти потоки не имеют прямого доступа к API Unity, вероятность возникновения проблем, таких как тупики и условия гонки, может быть выше. Для предотвращения этого требуется правильная синхронизация с главным потоком Unity. Чтобы правильно реализовать многопоточность, просмотрите раздел "Ограничения задач async и await" на странице руководства "Обзор .NET в Unity" и статью MSDN об использовании потоков и многопоточности. В Unity 6 появился класс Awaitable, который обеспечивает лучшую поддержку async/await.
Выявление и устранение причин утечек памяти может быть сложным и трудоемким делом, особенно на поздних этапах разработки. Как бы банально это ни звучало, но профилактика всегда лучше, чем лечение. Вот несколько рекомендаций, которые помогут предотвратить протечки в любой игровой среде:
- Создавая новые объекты/активы в памяти, не забудьте удалить их, если они не нужны. Если Вы используете Addressables, не забудьте освободить неиспользуемые активы.
- При загрузке/выгрузке сцен активы должны быть правильно удалены из памяти. Unity не выгружает активы автоматически, когда уровень выгружается, поэтому важно убедиться, что все доступы к ним удалены из памяти. API Resources.UnloadUnusedAssets поможет очистить активы. Однако он может вызвать скачки процессора, поскольку возвращает объект, который yield до тех пор, пока операция не будет завершена, поэтому его следует использовать в местах, не чувствительных к производительности.
- Избегайте частого использования Instantiate и Destroy GameObjects. Это может привести к ненужным управляемым выделениям, а также к затратам процессора. Однако в случаях, когда использование Destroy необходимо, убедитесь, что Вы удалили все ссылки на объект, чтобы избежать утечки объектов оболочки. Когда объект или его родители уничтожаются с помощью команды Destroy, код C# сохраняет ссылку на объект Unity, оставляя управляемый объект-обертку - его Managed Shell - в памяти. Его родная память будет выгружена, как только сцена, в которой он находится, будет выгружена, или GameObject, к которому он прикреплён, или его родители будут уничтожены через Destroy. Поэтому, если что-то другое, что не было выгружено, все еще ссылается на него, управляемая память может продолжать жить в виде объекта Leaked Shell Object.
- Будьте внимательны при реализации событий с использованием синглтонов. Экземпляры Singleton хранят ссылки на все объекты, подписавшиеся на его события. Если эти объекты живут не так долго, как экземпляр синглтона, и не отписываются от этих событий, они останутся в памяти, что приведет к утечке памяти. Если источник события будет утилизирован раньше, чем слушатели, ссылка будет очищена, а если слушатели будут правильно сняты с регистрации, ссылка также не останется. Чтобы решить и предотвратить эту проблему, мы рекомендуем реализовать паттерн Weak Event Pattern или IDisposable во всех объектах, которые слушают события синглтонов, и убедиться, что они правильно утилизируются в Вашем коде. Паттерн "Слабое событие" - это паттерн проектирования, который помогает Вам управлять памятью и сборкой мусора в событийно-ориентированном программировании, особенно когда речь идет о долгоживущих объектах. Это особенно полезно, когда у Вас есть подписчики, которые живут недолго, но издатель живет долго. Пожалуйста, помните, что это решения для C#, они работают только с событиями C# и не поддерживаются напрямую UnityEvents или Unity UI Toolkit. Поэтому мы рекомендуем применять эти решения только в Ваших скриптах, не относящихся к MonoBehaviour.
Наконец, профилирование, CI/CD-тестирование и стресс-тестирование на ранних этапах разработки могут стать настоящей экономией времени, поскольку обнаружение утечек в момент их возникновения позволит Вам оперативно устранить проблему, сэкономить время на отладку и обеспечить оптимальную производительность.