Что вы получите с этой страницы: Советы о том, как упростить изменение и отладку игрового кода, создав его на основе объектов со сценариями.
Эти советы подготовлены Райаном Хипплом, главным инженером Schell Games, имеющим опыт в разработке архитектуры игр на основе объектов Scriptable Object. Запись доклада Райана по Scriptable Object на Unite можно посмотреть здесь; также мы рекомендуем посмотреть доклад инженера Unity Ричарда Файна для знакомства с системой Scriptable Object.
ScriptableObject — это сериализуемый класс Unity, позволяющий хранить большие объемы общедоступных данных независимо от экземпляров скриптов. Этот класс упрощает изменение и отладку кода. ScriptableObject дает возможность создать гибкий слой обмена данными между различными игровыми системами, облегчая изменение и настройку, а также многократное использование компонентов в процессе работы.
Используйте модульный подход:
- старайтесь не создавать системы, напрямую зависящие друг от друга. Например, система инвентаря должна иметь канал связи с другими системами в игре, но без жесткой привязки, потому что это усложняет перекомпоновку систем и изменение их взаимосвязей.
- Создавайте сцены с нуля, избегайте элементов, которые переходят из одной сцены в другую. Загрузка каждой новой сцены должна происходить заново. Это позволяет без грубых маневров создавать сцены, уникальные по своей природе, непохожие одна на другую.
- Настраивайте префабы так, чтобы они могли работать самостоятельно. Каждый добавленный в сцену префаб должен по возможности иметь как можно более законченную функциональность. Это помогает контролировать кодовую базу в большой команде, где сцены представляют собой список префабов, а префабы самостоятельны в своих функциях. Таким образом, большая часть проверок будет сосредоточена на уровне префабов, что снижает число конфликтов в сцене.
- Старайтесь делать так, чтобы каждый компонент занимался решением одной задачи. Это облегчит создание нового контента путем сочетания уже готовых компонентов.
Упрощайте изменение и редактирование элементов:
- сделайте алгоритмы игры информационно-ориентированными настолько, насколько это возможно. Проектируя игровые системы как механизмы, использующие данные как инструкции, вы сможете вносить изменения в игру в любое время, даже во время ее работы.
- Чем выше степень модульности и компонентности архитектуры игры, тем легче она поддается изменениям, особенно руками художников и дизайнеров. Если дизайнеры смогут собрать игру воедино, не обращаясь за какими-то особыми функциями, в основном благодаря тому, что каждый компонент выполняет только одну задачу, то они смогут сочетать эти компоненты самыми разными способами, реализуя различные варианты игрового процесса и механики. Райан говорит, что большая часть классных особенностей игр, над которыми работала его команда, появилась благодаря этому подходу, который он называет «эмерджентной архитектурой».
- Очень важно, чтобы команда могла вносить изменения во время работы игры. Чем легче работающая игра поддается изменению, тем легче ее балансировать и настраивать значения, а возможность сохранять рабочее состояние так, как это умеют объекты ScriptableObject, — это очень удобно.
Упрощайте отладку:
на самом деле это кит-помощник для первых двух. Чем выше модульность вашей игры, тем легче тестировать каждый из ее элементов по отдельности. Чем легче игра поддается изменению, чем больше в ней элементов, поддающихся контролю в окне Inspector, тем легче будет ее отладка. Сделайте так, чтобы состояние отладки можно было просматривать в окне Inspector, и никогда не помечайте функцию как завершенную, пока не выработаете план по ее отладке.
Один из самых простых вариантов архитектуры на основе ScriptableObjects — это изолированная переменная в виде ассета. Ниже приведен пример FloatVariable, который можно легко расширить до любого сериализуемого типа.
Любой сотрудник вашей студии, независимо от уровня знаний, сможет объявить новую игровую переменную, создав новый ассет FloatVariable. Любой класс MonoBehaviour или ScriptableObject может использовать общедоступный ассет FloatVariable вместо общедоступной переменной с плавающей запятой для ссылки на новое общее значение.
И даже лучше — если один MonoBehaviour изменит значение FloatVariable, то остальные MonoBehaviour будут в курсе этого изменения. Это — основа слоя коммуникации между системами, которым не нужно ссылаться друг на друга.
Одним из примеров такой архитектуры может быть показатель здоровья игрока (HP). В однопользовательской игре за показатель здоровья игрока может отвечать ассет FloatVariable под названием PlayerHP. При получении урона система будет отнимать его от значения PlayerHP, а при исцелении — добавлять нужное значение.
Теперь представьте префаб индикатора здоровья в сцене. Индикатор здоровья отслеживает переменную PlayerHP для обновления. Этот индикатор с легкостью можно перенастроить на другую переменную, например, PlayerMP. Индикатор здоровья не знает о существовании игрока в сцене — он просто считывает значение переменной, в которую объект игрока это значение записывает.
После того как мы установили такую настройку, можно легко добавить другие вещи для наблюдения за PlayerHP. Музыкальное сопровождение может меняться по мере снижения уровня PlayerHP, враги могут менять схему атаки, если знают, что игрок слаб, эффекты в пространстве экрана могут подчеркивать опасность следующей атаки и так далее. Ключевой момент в том, что скрипт Player не отправляет сообщения этим системам, а сами системы не знают о существовании GameObject игрока. Вы сможете заглянуть в Inspector во время игры и изменить значение PlayerHP для тестирования.
Меняя значение FloatVariable, полезно копировать данные в значение Runtime, чтобы не менять значение, сохраняемое на диске в ScriptableObject. Благодаря этому классы MonoBehaviours получат доступ к значению RuntimeValue, что позволит сохранить в неизменном виде значение InitialValue, сохраненное на диске.
Одна из любимых систем, которую Райан реализует с помощью ScriptableObjects, — это система событий. Система событий помогает улучшить модульную структуру кода за счет обмена сообщениями между системами, которые не знают непосредственно о существовании друг друга. Такие системы дают возможность реагировать на изменения состояний без непрерывного отслеживания в цикле Update.
Ниже приведены примеры кодасистемы событий, состоящей из двух частей: ScriptableObject под названием GameEvent и класс MonoBehaviour под названием GameEventListener. Дизайнер может создать любое количество объектов GameEvent в проекте, каждый из которых будет отвечать за обмен важной информацией. Класс GameEventListener ожидает соответствующего события GameEvent и отвечает вызовом UnityEvent (который является не настоящим событием, а скорее сериализованным вызовом функции).
GameEvent ScriptableObject:
GameEventListener:
Этот пример отвечает за обработку смерти игрока. Это момент, где возможны сильные изменения, для которого сложно определить реализацию логики. Какое событие должен запускать скрипт Player — отображение интерфейса окончания игры или смену музыки? Должны ли враги проверять факт смерти игрока в каждом кадре? Система событий позволяет избежать таких проблемных зависимостей.
После смерти игрока скрипт Player вызывает функцию Raise для события OnPlayerDied. Скрипту Player нет необходимости знать, каким системам это интересно, поскольку сообщение доступно всем системам. Интерфейс Game Over UI ожидает события OnPlayerDied и начинает анимацию, скрипт камеры на основе события может начать затемнение, а система музыки — изменить оформление. Мы также можем настроить врагов на ожидание события OnPlayerDied, после которого может срабатывать анимация насмешки или переход в состояние покоя.
Такая схема невероятно упрощает добавление новых откликов на смерть игрока. Кроме того, это облегчает тестирование отклика на смерть игрока путем вызова функции Raise для события с помощью тестового кода или кнопки в Inspector.
Система событий, разработанная студией Schell Games, со временем стала гораздо сложнее и обрела функции, позволяющие внедрять данные и автоматически генерировать типы. Приведенный пример когда-то был прообразом сегодняшней системы.
Scriptable Object не обязательно должны содержать лишь данные. Попробуйте реализовать в MonoBehaviour любую систему, и вы поймете, что реализацию вполне можно переместить в ScriptableObject. Вместо того чтобы размещать InventoryManager в классе MonoBehaviour DontDestroyOnLoad, попробуйте поместить его в ScriptableObject.
Поскольку объект не привязан к сцене, то у него нет компонента Transform и нет функции Update, но он будет сохранять состояние между загрузками сцены без необходимости в особой инициализации. Если вам нужен скрипт для доступа к инвентарю, используйте общедоступную ссылку на объект системы инвентаря. Это упрощает замену на тестовый инвентарь или набор для режима обучения.
Здесь можно представить, как скрипт Player использует ссылку на систему инвентаря. При появлении игрока скрипт опрашивает инвентарь на предмет наличия в нем предметов и создает объекты снаряжения. Интерфейс действия «использовать предмет» тоже может ссылаться на инвентарь для определения предмета, который должен взять в руки персонаж.