Последнее обновление: январь 2020 г., текст на 10 минут чтения.

Три способа построить игру на основе объектов Scriptable Object

What you will get from this page: Tips for how to keep your game code easy to change and debug by architecting it with Scriptable Objects.

These tips come from Ryan Hipple, principal engineer at Schell Games, who has advanced experience using Scriptable Objects to architect games. You can watch Ryan’s Unite talk on Scriptable Objects here; we also recommend you see Unity engineer Richard Fine’s session for a great introduction to Scriptable Objects. Thank you Ryan!

 

 

Что такое ScriptableObject?

ScriptableObject is a serializable Unity class that allows you to store large quantities of shared data independent from script instances. Using ScriptableObjects makes it easier to manage changes and debugging. You can build in a level of flexible communication between the different systems in your game, so that it’s more manageable to change and adapt them throughout production, as well as reuse components.

Три кита игровой архитектуры

Используйте модульный подход:

  • старайтесь не создавать системы, напрямую зависящие друг от друга. Например, система инвентаря должна иметь канал связи с другими системами в игре, но без жесткой привязки, потому что это усложняет перекомпоновку систем и изменение их взаимосвязей. 
  • Создавайте сцены с нуля, избегайте элементов, которые переходят из одной сцены в другую. Загрузка каждой новой сцены должна происходить заново. Это позволяет без грубых маневров создавать сцены, уникальные по своей природе, непохожие одна на другую. 
  • Настраивайте префабы так, чтобы они могли работать самостоятельно. Каждый добавленный в сцену префаб должен по возможности иметь как можно более законченную функциональность. Это помогает контролировать кодовую базу в большой команде, где сцены представляют собой список префабов, а префабы самостоятельны в своих функциях. Таким образом, большая часть проверок будет сосредоточена на уровне префабов, что снижает число конфликтов в сцене. 
  • Старайтесь делать так, чтобы каждый компонент занимался решением одной задачи. Это облегчит создание нового контента путем сочетания уже готовых компонентов.

 

Упрощайте изменение и редактирование элементов:

  • сделайте алгоритмы игры информационно-ориентированными настолько, насколько это возможно. Проектируя игровые системы как механизмы, использующие данные как инструкции, вы сможете вносить изменения в игру в любое время, даже во время ее работы. 
  • Чем выше степень модульности и компонентности архитектуры игры, тем легче она поддается изменениям, особенно руками художников и дизайнеров. Если дизайнеры смогут собрать игру воедино, не обращаясь за какими-то особыми функциями, в основном благодаря тому, что каждый компонент выполняет только одну задачу, то они смогут сочетать эти компоненты самыми разными способами, реализуя различные варианты игрового процесса и механики. Райан говорит, что большая часть классных особенностей игр, над которыми работала его команда, появилась благодаря этому подходу, который он называет «эмерджентной архитектурой». 
  • Очень важно, чтобы команда могла вносить изменения во время работы игры. Чем легче работающая игра поддается изменению, тем легче ее балансировать и настраивать значения, а возможность сохранять рабочее состояние так, как это умеют объекты Scriptable Object, — это очень удобно.

 

Упрощайте отладку:

на самом деле это кит-помощник для первых двух. Чем выше модульность вашей игры, тем легче тестировать каждый из ее элементов по отдельности. Чем легче игра поддается изменению, чем больше в ней элементов, поддающихся контролю в окне Inspector, тем легче будет ее отладка. Сделайте так, чтобы состояние отладки можно было просматривать в окне Inspector, и никогда не помечайте функцию как завершенную, пока не выработаете план по ее отладке. 

Архитектура с упором на переменные

Один из самых простых вариантов архитектуры на основе ScriptableObjects — это изолированная переменная в виде ассета. Ниже приведен пример FloatVariable, который можно легко расширить до любого сериализуемого типа.

Любой сотрудник вашей студии, независимо от уровня знаний, сможет объявить новую игровую переменную, создав новый ассет FloatVariable. Любой класс MonoBehaviour или ScriptableObject может использовать общедоступный ассет FloatVariable вместо общедоступной переменной с плавающей запятой для ссылки на новое общее значение.

И даже лучше — если один MonoBehaviour изменит значение FloatVariable, то остальные MonoBehaviour будут в курсе этого изменения. Это — основа слоя коммуникации между системами, которым не нужно ссылаться друг на друга. 

FloatVariable.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
	public float Value;
}

Пример: очки здоровья игрока

Одним из примеров такой архитектуры может быть показатель здоровья игрока (HP). В однопользовательской игре за показатель здоровья игрока может отвечать ассет FloatVariable под названием PlayerHP. При получении урона система будет отнимать его от значения PlayerHP, а при исцелении — добавлять нужное значение.

Теперь представьте префаб индикатора здоровья в сцене. Индикатор здоровья отслеживает переменную PlayerHP для обновления. Этот индикатор с легкостью можно перенастроить на другую переменную, например, PlayerMP. Индикатор здоровья не знает о существовании игрока в сцене — он просто считывает значение переменной, в которую объект игрока это значение записывает.

При такой организации вам будет очень легко добавлять новые элементы, отслеживающие переменную PlayerHP. В зависимости от показателя здоровья может менять свое поведение музыкальная система, может меняться характер атак врагов, знающих, что враг ослаблен, а экранные эффекты могут подчеркивать опасность следующей атаки и так далее. Ключевой момент в том, что скрипт Player не отправляет сообщения этим системам, а сами системы не знают о существовании GameObject игрока. Вы сможете заглянуть в Inspector во время игры и изменить значение PlayerHP для тестирования. 

Меняя значение FloatVariable, полезно копировать данные в значение Runtime, чтобы не менять значение, сохраняемое на диске в ScriptableObject. Благодаря этому классы MonoBehaviours получат доступ к значению RuntimeValue, что позволит сохранить в неизменном виде значение InitialValue, сохраненное на диске.

RuntimeValue.cs (C#)
[CreateAssetMenu]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
	public float InitialValue;

	[NonSerialized]
	public float RuntimeValue;

public void OnAfterDeserialize()
{
		RuntimeValue = InitialValue;
}

public void OnBeforeSerialize() { }
}

Архитектура событий

Одна из любимых систем, которую Райан реализует с помощью ScriptableObjects, — это система событий. Система событий помогает улучшить модульную структуру кода за счет обмена сообщениями между системами, которые не знают непосредственно о существовании друг друга. Такие системы дают возможность реагировать на изменения состояний без непрерывного отслеживания в цикле Update.

Ниже приведены примеры кода системы событий, состоящей из двух частей: ScriptableObject под названием GameEvent и класс MonoBehaviour под названием GameEventListener. Дизайнер может создать любое количество объектов GameEvent в проекте, каждый из которых будет отвечать за обмен важной информацией. Класс GameEventListener ожидает соответствующего события GameEvent и отвечает вызовом UnityEvent (который является не настоящим событием, а скорее сериализованным вызовом функции).

Пример кода: GameEvent ScriptableObject

GameEvent ScriptableObject: 

GameEvent ScriptableObject.cs (C#)
[CreateAssetMenu]
public class GameEvent : ScriptableObject
{
	private List<GameEventListener> listeners = 
		new List<GameEventListener>();

public void Raise()
{
	for(int i = listeners.Count -1; i >= 0; i--)
listeners[i].OnEventRaised();
}

public void RegisterListener(GameEventListener listener)
{ listeners.Add(listener); }

public void UnregisterListener(GameEventListener listener)
{ listeners.Remove(listener); }
}

Пример кода: GameEventListener

GameEventListener:

GameEventListener.cs (C#)
public class GameEventListener : MonoBehaviour
{
public GameEvent Event;
public UnityEvent Response;

private void OnEnable()
{ Event.RegisterListener(this); }

private void OnDisable()
{ Event.UnregisterListener(this); }

public void OnEventRaised()
{ Response.Invoke(); }
}

Система событий, обрабатывающая смерть игрового персонажа

Этот пример отвечает за обработку смерти игрока. Это момент, где возможны сильные изменения, для которого сложно определить реализацию логики. Какое событие должен запускать скрипт 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 использует ссылку на систему инвентаря. При появлении игрока скрипт опрашивает инвентарь на предмет наличия в нем предметов и создает объекты снаряжения. Интерфейс действия «использовать предмет» тоже может ссылаться на инвентарь для определения предмета, который должен взять в руки персонаж. 

Понравился ли вам этот контент?

Мы используем cookie-файлы, чтобы вам было удобнее работать с нашим веб-сайтом. Подробнее об этом можно узнать на странице, посвященной политике использования cookie-файлов.

Согласен