Повышение эффективности работы со сценами с помощью ScriptableObjects

Управление несколькими сценами в Unity может быть непростой задачей, и улучшение этого рабочего процесса имеет решающее значение как для производительности игры, так и для продуктивности вашей команды. Здесь мы поделимся некоторыми советами по настройке рабочих процессов Scene таким образом, чтобы их можно было масштабировать для больших проектов.
Большинство игр включает в себя несколько уровней, а уровни часто содержат более одной сцены. В играх, где Сцены относительно небольшие, вы можете разбить их на различные секции с помощью префабов. Однако, чтобы включить или инстанцировать их во время игры, вам нужно ссылаться на все эти префабы. Это означает, что по мере того, как ваша игра становится больше и ссылки занимают все больше места в памяти, эффективнее использовать Scenes.
Вы можете разбить свои уровни на одну или несколько Unity Scenes. Поиск оптимального способа управления ими становится ключевым. Вы можете открыть несколько сцен в редакторе и во время выполнения, используя многосценное редактирование. Разделение уровней на несколько Сцен также облегчает командную работу, поскольку позволяет избежать конфликтов слияния в таких инструментах совместной работы, как Git, SVN, Unity Collaborate и т. п.
В видео ниже мы покажем, как загрузить уровень более эффективно, разбив игровую логику и различные части уровня на несколько отдельных Unity Scenes. Затем, используя режим Additive Scene-loading при загрузке этих сцен, мы загружаем и выгружаем необходимые части вместе с игровой логикой, которая является постоянной. Мы используем префабы в качестве "якорей" для сцен, что также обеспечивает большую гибкость при работе в команде, поскольку каждая сцена представляет собой часть уровня и может редактироваться отдельно.
Вы по-прежнему можете загружать эти сцены в режиме редактирования и нажимать кнопку Play в любое время, чтобы визуализировать их все вместе при создании дизайна уровня.
Мы покажем два различных метода загрузки этих сцен. Первый - дистанционный, который хорошо подходит для неинтерьерных уровней, таких как открытый мир. Эта техника также полезна для некоторых визуальных эффектов (например, тумана), чтобы скрыть процесс загрузки и выгрузки.
Вторая техника использует триггер для проверки того, какие Сцены нужно загрузить, что более эффективно при работе с интерьерами.
Теперь, когда все управление осуществляется внутри уровня, вы можете добавить слой поверх него, чтобы лучше управлять уровнями.
Мы хотим отслеживать различные Сцены для каждого уровня, а также все уровни на протяжении всего игрового процесса. Один из возможных способов сделать это - использовать статические переменные и паттерн singleton в скриптах MonoBehaviour, но с этим решением есть несколько проблем. Использование паттерна singleton позволяет создавать жесткие связи между вашими системами, поэтому он не является строго модульным. Эти системы не могут существовать отдельно и всегда будут зависеть друг от друга.
Еще одна проблема связана с использованием статических переменных. Поскольку в инспекторе их не видно, для их установки необходимо изменить код, что затрудняет художникам и дизайнерам уровней удобное тестирование игры. Если вам нужно, чтобы данные разделялись между различными сценами, вы используете статические переменные в сочетании с DontDestroyOnLoad, но последнего следует избегать, когда это возможно.
Для хранения информации о различных сценах вы можете использовать ScriptableObject, который является сериализуемым классом, используемым в основном для хранения данных. В отличие от скриптов MonoBehaviour, которые используются как компоненты, прикрепленные к игровым объектам, ScriptableObjects не прикреплены ни к каким игровым объектам и поэтому могут совместно использоваться в различных сценах всего проекта.
Вы хотите иметь возможность использовать эту структуру не только для уровней, но и для меню "Сцены" в вашей игре. Для этого создайте класс GameScene, который будет содержать различные общие свойства уровней и меню.
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Обратите внимание, что класс наследуется от ScriptableObject, а не от MonoBehaviour. Вы можете добавить столько свойств, сколько нужно для вашей игры. После этого шага вы можете создать классы Level и Menu, которые наследуют от только что созданного класса GameScene - таким образом, они также являются ScriptableObjects.
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Добавив атрибут CreateAssetMenu в верхней части, вы сможете создать новый уровень из меню Assets в Unity. То же самое можно сделать для класса Menu. Вы также можете включить перечисление, чтобы иметь возможность выбирать тип меню в инспекторе.
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Теперь, когда вы можете создавать уровни и меню, давайте добавим базу данных, в которой будут перечислены уровни и меню для удобства использования. Вы также можете добавить индекс, чтобы отслеживать текущий уровень игрока. Затем можно добавить методы для загрузки новой игры (в данном случае будет загружен первый уровень), для воспроизведения текущего уровня и для перехода на следующий уровень. Обратите внимание, что между этими тремя методами меняется только индекс, поэтому вы можете создать метод, который загружает уровень с индексом, чтобы использовать его несколько раз.
Неизвестный тип блока "codeBlock", укажите для него сериализатор в свойстве `serializers.types`.
Есть также методы для меню, и вы можете использовать тип enum, который вы создали ранее, чтобы загрузить конкретное меню - просто убедитесь, что порядок в enum и порядок в списке меню совпадают.
Теперь вы можете создать уровень, меню или базу данных ScriptableObject из меню Assets, щелкнув правой кнопкой мыши в окне проекта.

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

Пришло время назвать эти методы. В этом примере кнопка Next Level на пользовательском интерфейсе (UI), которая появляется, когда игрок достигает конца уровня, вызывает метод NextLevel. Чтобы прикрепить метод к кнопке, нажмите кнопку с плюсом в событии On Click компонента Button, чтобы добавить новое событие, затем перетащите объект Scenes Data ScriptableObject в поле объекта и выберите метод NextLevel из ScenesData, как показано ниже.

Теперь вы можете проделать тот же процесс для других кнопок - переиграть уровень или перейти в главное меню, и так далее. Вы также можете ссылаться на объект ScriptableObject из любого другого скрипта, чтобы получить доступ к различным свойствам, например, к AudioClip для фоновой музыки или профилю постобработки, и использовать их в уровне.
- Минимизация погрузки/разгрузки
В скрипте ScenePartLoader, показанном в видео, видно, что игрок может входить и выходить из коллайдера много раз, вызывая повторную загрузку и выгрузку сцены. Чтобы избежать этого, вы можете добавить корутин перед вызовом методов загрузки и выгрузки сцены в сценарии, и остановить корутин, если игрок покидает триггер.
- Соглашения об именовании
Еще один общий совет - использовать в проекте надежные соглашения об именовании. Команда должна заранее договориться о том, как называть различные типы активов - от сценариев и сцен до материалов и других вещей в проекте. Это облегчит работу над проектом и его сопровождение не только вам, но и вашим коллегам. Это всегда хорошая идея, но в данном конкретном случае она имеет решающее значение для управления сценами с помощью ScriptableObjects. В нашем примере использовался прямой подход, основанный на имени сцены, но существует множество различных решений, которые в меньшей степени зависят от имени сцены. Вам следует избегать строкового подхода, потому что если вы переименуете сцену Unity в данном контексте, в другой части игры эта сцена не загрузится.
- Индивидуальная оснастка
Один из способов избежать зависимости от имени во всей игре - настроить сценарий так, чтобы он ссылался на Scenes как на тип Object. Это позволяет перетащить актив сцены в инспектор, а затем безопасно получить его имя в сценарии. Однако, поскольку это класс редактора, у вас нет доступа к классу AssetDatabase во время выполнения, поэтому вам нужно объединить обе части данных для решения, которое работает в редакторе, предотвращает человеческие ошибки и по-прежнему работает во время выполнения. Вы можете обратиться к интерфейсу ISerializationCallbackReceiver, чтобы посмотреть пример реализации объекта, который при сериализации может извлекать строковый путь из актива Scene и сохранять его для использования во время выполнения.
Кроме того, вы можете создать пользовательский инспектор, чтобы упростить быстрое добавление сцен в Build Settings с помощью кнопок, вместо того чтобы добавлять их вручную через это меню и поддерживать их синхронизацию.
В качестве примера такого инструмента можно привести эту замечательную реализацию с открытым исходным кодом от разработчика JohannesMP (это не официальный ресурс Unity).
В этом посте показан лишь один способ, с помощью которого ScriptableObjects могут улучшить ваш рабочий процесс при работе с несколькими сценами, объединенными с префабами. Разные игры имеют совершенно разные способы управления Сценами - нет единого решения, подходящего для всех игровых структур. Имеет смысл внедрить собственный инструментарий, соответствующий организации вашего проекта.
Мы надеемся, что эта информация поможет вам в вашем проекте или, возможно, вдохновит вас на создание собственных инструментов управления сценами.
Сообщите нам в комментариях, если у вас возникли вопросы. Мы будем рады узнать, какие методы управления сценами вы используете в своей игре. И не стесняйтесь предлагать другие варианты использования, о которых вы хотели бы рассказать в будущих статьях блога.