Unity 6.3
0 онлайн 86 гостей 3 в системе
Вход
Модульная игровая архитектура на ScriptableObjects Глава 4 из 12 Оригинал, стр. 17

Контейнеры данных

Русский

Контейнеры данных

Чаще всего объекты ScriptableObject используют как контейнеры общих статических данных, особенно конфигурации игры, которая не изменяется во время выполнения. Типичные варианты применения объектов ScriptableObject: - Инвентарь: тип предмета, значок, редкость, эффекты - Характеристики врагов, игроков и предметов: здоровье, урон, скорость, параметры ИИ - Наборы аудио: группы клипов для шагов, звуков UI и зацикленных фоновых звуков - Конфигурационные файлы: настройки сложности, кривые прогресса, таблицы появления объектов

- Данные диалогов Во время выполнения эти данные можно хранить в компонентах MonoBehaviour, но это не всегда эффективно. Как показало сравнение выше, компоненты MonoBehaviour создают дополнительные накладные расходы, поскольку им нужен объект GameObject и, по умолчанию, Transform. В результате ради хранения одного значения приходится создавать большой объем неиспользуемых данных.

Чтобы убедиться в этом, создайте новый GameObject с пустым MonoBehaviour. Затем откройте сериализованный объект в текстовом редакторе. Он будет выглядеть примерно так:

Новый GameObject с минимальным MonoBehaviour

Сравните его с пустым ScriptableObject: структура такого объекта заметно компактнее.

ScriptableObject сокращает накладные расходы на хранение данных.

ScriptableObject уменьшает объем занимаемой памяти, поскольку обходится без GameObject и Transform. В крупных коммерческих проектах это может иметь существенное значение. Кроме того, данные хранятся на уровне проекта, что особенно удобно, если к ним требуется обращаться из нескольких сцен.

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

Данные ScriptableObject и постоянные данные Когда ScriptableObject называют контейнерами данных, обычно имеют в виду статическую или общую конфигурацию, которую не требуется сохранять после изменений во время выполнения. Изменения данных ScriptableObject сохраняются в редакторе Unity, подобно изменениям материала или префаба, но во время выполнения собранного приложения они не записываются. Изменения экземпляра ScriptableObject в ходе игры существуют только в памяти и теряются после завершения сеанса.

Постоянные данные, которые нужно сохранять между сеансами, обычно записывают в другом формате, например JSON, XML, MessagePack или Protocol Buffers. Подробнее см. раздел «Двойная сериализация» ниже. В сборке игры данные ScriptableObject можно изменять во время выполнения, например с помощью переменных ScriptableObject и Runtime Set, но такие изменения временны. При запуске нового сеанса данные вернутся к состоянию, которое было зафиксировано во время сборки. Для задач постоянного хранения воспринимайте данные ScriptableObject как доступные только для чтения. Изменяемые постоянные данные следует хранить во внешнем хранилище с поддержкой чтения и записи.

Устранение дублирования данных Представьте тысячу объектов GameObject, каждый с пользовательским компонентом MonoBehaviour, содержащим несколько полей. Если каждый компонент хранит собственную копию одних и тех же значений, в памяти дублируется большой объем данных. Это неэффективно, особенно когда данные одинаковы для всех экземпляров и не меняются во время выполнения.

объект

объект

объект

объект

объект

данные

данные

данные

данные

данные

Множество объектов с одинаковыми локальными данными приводит к лишнему расходу памяти и снижению производительности.

Вместо дублирования статических данных поместите их в ScriptableObject. Тогда каждый из тысячи объектов сможет ссылаться на общий ассет с данными. Объекты будут хранить только ссылку, а не отдельную копию самих данных.

объект

объект

объект

объект

объект

общие данные

Совместное использование данных множеством объектов через ScriptableObject

В проектировании программного обеспечения такая оптимизация известна как паттерн Flyweight, или «Приспособленец». Подобная реструктуризация кода устраняет множество копий одних и тех же значений и сокращает расход памяти. Паттерны проектирования

Паттерны проектирования помогают создавать более гибкий и удобный для сопровождения код, что особенно важно в постоянно меняющихся игровых проектах. Скачайте бесплатную электронную книгу «Повышаем уровень кода с паттернами проектирования и SOLID», чтобы подробнее узнать о принципах SOLID и паттернах проектирования. Ссылка на ScriptableObject занимает гораздо меньше места, чем полная копия данных. По мере роста проекта экономия памяти благодаря устранению дубликатов может стать значительной.

Устранение дублирования данных экономит ресурсы

Memory Profiler сравнивает расход памяти при дублировании данных (A) и совместном использовании данных (B).

Таким способом можно хранить большие объемы общих данных. Используйте ScriptableObject для следующих задач: - Сохранение данных в течение сеанса работы в редакторе Unity - Сохранение данных в виде ассета для использования во время выполнения В отличие от компонентов MonoBehaviour, объекты ScriptableObject нельзя прикреплять к GameObject. Вместо этого их сохраняют как ассеты проекта. Это особенно удобно, если префаб использует неизменяемые данные в своих компонентах MonoBehaviour.

Пример рефакторинга Рассмотрим компонент MonoBehaviour, который управляет здоровьем NPC. Его класс можно определить следующим образом:

C#
public class NPCHealthUnrefactored : MonoBehaviour {
    [Range(10, 100)] public int MaxHealth;
    [Range(10, 100)] public int HealthThreshold;

public int CurrentHealth; } Такой код работает, но содержит данные, которые не должны меняться во время выполнения. Если компонент NPCHealthUnrefactored прикреплен ко множеству объектов, в памяти возникает большое количество ненужных копий.

MonoBehaviour NPCHealth до рефакторинга

Данные, которые не требуется изменять, можно перенести в ScriptableObject:

C#
[CreateAssetMenu(fileName="NPCConfig")] public class NPCConfigSO : ScriptableObject {
    [Range(10, 100)] public int maxHealth;
    [Range(10, 100)] public int healthThreshold;

} Атрибут CreateAssetMenu настраивает команду меню. При необходимости в нем можно указать имя файла по умолчанию через fileName и порядок пункта меню через order.

Соглашения по оформлению кода в этом руководстве Для наглядности и простоты чтения многие примеры кода в этом руководстве упрощены, в частности используются открытые поля. В рабочем коде применяйте закрытые поля и открытые свойства, чтобы улучшить инкапсуляцию и гибкость. Атрибут SerializeField позволяет отображать закрытые поля в окне Inspector редактора Unity. Соглашение об именовании также помогает отличать скрипты ScriptableObject от скриптов MonoBehaviour. Например, к имени класса можно добавлять суффикс Data или SO. Это необязательно, но позволяет лучше организовать проект и избежать неоднозначности.

По мере роста кодовой базы рекомендуется создать руководство по стилю и последовательно ему следовать. Подробнее см. электронную книгу «Руководство по стилю C# для Unity 6». После рефакторинга компонент NPCHealth упрощается до следующего вида:

C#
public class NPCHealth: MonoBehaviour {
    // Ссылка на наш ScriptableObject

    public NPCConfigSO Config;
    public int CurrentHealth;
}

Теперь MonoBehaviour содержит ссылку на новый ScriptableObject. После рефакторинга содержимое окна Inspector выглядит почти так же, но данные разделены между объектами.

Ссылка на ScriptableObject

Рефакторинг разделяет данные между MonoBehaviour и ScriptableObject

Пользовательские окна Inspector

При переносе данных в ScriptableObject они оказываются в двух местах: в ассете ScriptableObject и в MonoBehaviour, который на него ссылается. Чтобы упростить работу с компонентами MonoBehaviour, можно создать пользовательский редактор Unity. Ниже приведен пример для NPCHealth.

Пользовательское окно Inspector отображает переменные ScriptableObject внутри MonoBehaviour.

Так можно просматривать переменные NPCConfig вместе с остальными свойствами MonoBehaviour. Если выбрать исходный префаб NPCHealth, значения обоих объектов легко изменить в одном месте. Для пользовательского редактора Unity достаточно нескольких строк кода: - Создайте класс, производный от Editor, и сохраните его в папке Editor. Примените атрибут CustomEditor, указав тип NPCHealth. - Подготовьте временный экземпляр Editor для ScriptableObject NPCConfig. - В методе OnInspectorGUI создайте Editor для компонента NPCHealth. - Отрисуйте содержимое окна Inspector базового класса и нового пользовательского окна Inspector.

C#
using UnityEditor;
[CustomEditor(typeof(NPCHealth))] public class NPCHealthEditor : Editor {
    private Editor editorInstance;
    private void OnEnable() {
        // Сбросить экземпляр Editor editorInstance = null;

    }
    public override void OnInspectorGUI() {
        // Проверяемый целевой компонент NPCHealth npcHealth = (NPCHealth)target;

        if (editorInstance == null) editorInstance = Editor.CreateEditor(npcHealth.config);
        // Показать переменные MonoBehaviour base.OnInspectorGUI();
C#
// Отрисовать окно Inspector для ScriptableObject editorInstance.DrawDefaultInspector();

}
}

Этот пример можно развить с помощью пользовательских отрисовщиков свойств и атрибутов редактора. Это позволит сделать работу с объектами ScriptableObject еще удобнее.

Архитектурные преимущества ScriptableObject позволяет четко разделить общие и индивидуальные данные. Все уникальные динамические данные конкретного экземпляра GameObject остаются в MonoBehaviour, а общие данные хранятся в ScriptableObject. Однако преимущества архитектуры на основе ScriptableObject не ограничиваются экономией памяти.

Такая реорганизация архитектуры кода дает несколько преимуществ: - Дизайнеры могут меньше зависеть от разработчиков: если данные и логика хранятся в одном MonoBehaviour, разработчики и гейм-дизайнеры рискуют мешать работе друг друга. Когда два человека изменяют разные части одного префаба или сцены, возникает конфликт слияния, на устранение которого уходит время. Вынос общих данных в небольшие отдельные файлы и ассеты снижает риск таких проблем. Архитектура на основе ScriptableObject также позволяет дизайнерам создавать игровой процесс, не обращаясь постоянно к программисту.

При совместной работе с данными заранее определите понятный процесс взаимодействия между командами. Четкие договоренности и грамотно установленные границы ответственности помогут избежать проблем. Также могут потребоваться дополнительные проверки ошибок или валидация данных, например атрибут Range или метод OnValidate для ограничения допустимых значений. - Общие данные редактируются быстрее и с меньшим риском ошибок: теперь все изменения вносятся централизованно. Например, чтобы изменить параметр всех NPC, достаточно скорректировать его в одном месте, после чего новое значение применится ко всем затронутым компонентам во всех сценах.

Это уменьшает вероятность ошибок, возникающих при ручном массовом редактировании множества отдельных объектов GameObject. Перенос данных в ScriptableObject также упрощает контроль версий и помогает предотвращать конфликты слияния, когда участники команды работают с одной сценой или префабом. - Сохраняйте изменения игрового процесса, сделанные в режиме Play: режим Play в редакторе Unity позволяет дизайнерам экспериментировать с игровым процессом и настройками. Однако изменения в MonoBehaviour теряются после выхода из режима Play, поскольку Unity удаляет временную копию сцены.

ScriptableObject является ассетом, поэтому изменения его значений сохраняются независимо от того, находится ли Unity в режиме Play. Это удобно, если параметры требуется настраивать во время выполнения. Однако это может стать недостатком, если изменения понадобится отменить. Используйте Unity Version Control или другую систему контроля версий, чтобы при необходимости всегда можно было восстановить прежнее состояние проекта. Подробнее см. руководство «Рекомендации по контролю версий и организации проекта (издание для Unity 6)». - Сокращайте время загрузки сцен: при сохранении сцены или префаба Unity сериализует все их содержимое, включая каждый GameObject, все прикрепленные к нему компоненты и каждое открытое поле. При этом Unity не проверяет данные на дублирование.

Перенос данных в ScriptableObject может уменьшить размер сцен и префабов и заметно ускорить их загрузку и сохранение.

Переменные ScriptableObject Общие контейнеры данных можно сделать еще более специализированными: один ScriptableObject будет представлять всего одно значение. Например, можно создать класс ScriptableObject с именем IntVariable и единственным открытым полем value:

C#
using UnityEngine;
[CreateAssetMenu(menuName = "Variables/Int", order = 1)] public class IntVariableSO : ScriptableObject { public int value; }
Затем IntVariable можно использовать в MonoBehaviour. Например, класс PlayerHealth будет выглядеть так: public class PlayerHealth : MonoBehaviour { public IntVariableSO health; }

Обычно ScriptableObject воспринимается как хранилище неизменяемых значений, однако в него можно добавить методы, которые обновляют данные во время выполнения и восстанавливают начальное значение после выхода из режима Play. Таким образом, ScriptableObject может фактически выполнять роль переменной и хранить целые числа, числа с плавающей точкой, логические значения и другие данные. После этого дизайнеры смогут самостоятельно задавать данные для игровой логики, не обращаясь каждый раз к разработчику. Но такой подход требует предварительного планирования. Вместе с дизайнерами определите, как распределить ответственность за создание данных игрового процесса. Главное - установить четкие границы совместной работы. Например, команда программистов может подготовить объекты ScriptableObject для системы инвентаря, а команда дизайнеров затем заполнит характеристики и поведение каждого игрового предмета. Дополнительные скрипты редактора Unity могут сделать этот процесс почти бесшовным. Например, можно дать пользователю возможность переключать поля в окне Inspector между общим значением из ScriptableObject и константой. Тогда команда гейм-дизайна получит больше свободы и сможет переопределять данные ScriptableObject для отдельных экземпляров.

Пример IntVariable на основе ScriptableObject из проекта Unity Atoms с открытым исходным кодом

О реализации такого поведения в собственных проектах рассказывается в докладе «Игровая архитектура с ScriptableObject» с конференции Unite Austin. Также можно скачать проект Unity Atoms с открытым исходным кодом и изучить рабочую реализацию переменных ScriptableObject.

Двойная сериализация В Unity можно сочетать разные способы сериализации. Например, работать с объектами ScriptableObject в редакторе Unity, а затем сохранять их данные в другом месте, например в файле JSON или XML. Такой подход позволяет использовать сильные стороны каждого формата. Форматы JSON и XML подходят для постоянных данных, например сохранений игры или настроек. С ними не всегда удобно работать в редакторе Unity, зато их легко изменять вне Unity в любом текстовом редакторе. ScriptableObject, напротив, удобен в редакторе Unity и легко заменяется обычным перетаскиванием. Однако изменять его вне Unity и распространять среди сообщества игроков сложнее. Сочетание форматов сериализации открывает дополнительные возможности, например редактирование уровней и поддержку модификаций. Во время сборки скрипт может преобразовать внешние файлы в объекты ScriptableObject, которые загружаются быстрее обычного текста. Конфиденциальные данные, например сведения о виртуальной валюте и учетных записях, следует надежно хранить на серверах. В то же время доступ сообщества к части игровых данных может обогатить игровой процесс и позволить пользователям экспериментировать с уровнями-песочницами.

Представьте ScriptableObject, который описывает структуру игрового уровня. Он может содержать набор объектов Transform, задающих расположение префабов, начальную конфигурацию и другие параметры. Игровые скрипты будут использовать эти данные для сборки каждого уровня. Например, стены и начальные позиции игры можно хранить в ScriptableObject:

C#
[CreateAssetMenu(fileName ="LevelLayout")] public class LevelLayout : ScriptableObject {
    public Vector3[] wallPositions = new Vector3[2];
    public Vector3[] playerPositions = new Vector3[2];
    public Vector3[] goalPositions = new Vector3[2];
    public Vector3 ballPosition;
}
Эти данные определяют компоновку уровня. Скрипты управления уровнем считывают сведения из объекта LevelLayout, а затем создают экземпляры префабов в нужных местах. Пользовательский скрипт может экспортировать те же данные на диск с помощью JsonUtility. В результате за пределами редактора Unity создается текстовый файл, который пользователи смогут изменять сторонними инструментами. Чтобы загрузить измененный пользователем уровень, метод ScriptableObject.CreateInstance может создать ScriptableObject во время выполнения. Затем данные из файла JSON считываются в этот объект. Ниже показан пример метода LoadLevelFromJson: using System.IO;
public class LevelManager : MonoBehaviour {
    public ScriptableObject levelLayout;
    public void LoadLevelFromJson(string jsonFile) {
        if (levelLayout == null) {
            levelLayout = ScriptableObject. CreateInstance<LevelLayout>();
        }
        var importedFile = File.ReadAllText(jsonFile);
        JsonUtility.FromJsonOverwrite(importedFile, levelLayout);
    }
}

Пользовательские данные заменяют содержимое ScriptableObject, после чего измененный извне уровень можно использовать в игре как любой другой. Для самого приложения разницы нет. Обязательно проверьте этот механизм в примере проекта. При загрузке измененного файла JSON пользовательский уровень переопределяет стандартные данные уровня в ScriptableObject.

Настройки по умолчанию

Игровой процесс

Экземпляр времени выполнения

Настройки или моды

JSON-файл

Сочетайте форматы сериализации для большей гибкости

Примечание. При десериализации JSON в объекты ScriptableObject с помощью JsonUtility необходимо использовать метод FromJsonOverwrite. JsonUtility не создает новый объект для загрузки данных JSON, а записывает их в уже существующий объект. Значения в классах и объектах обновляются без дополнительных выделений памяти.

Защита данных Простой пример модификации выше показывает один из вариантов применения ScriptableObject. Однако, открывая игровые данные для изменений, соблюдайте осторожность, чтобы игроки не могли вмешаться в работу остальных частей приложения. Распространенные способы защиты данных, которые не должны изменяться модификациями: - Шифрование: зашифруйте файлы данных, чтобы их было сложнее прочитать или изменить. Это затруднит вмешательство пользователей в критически важные данные. - Цифровые подписи: алгоритм вычисления цифрового отпечатка позволяет проверить, не были ли файлы данных подменены или изменены. - Проверка на стороне сервера: если игра использует данные, хранящиеся на сервере, проверяйте их на сервере до передачи в игру и отклоняйте любые сведения с признаками вмешательства. Ни один метод не обеспечивает абсолютной защиты, поэтому обычно стоит сочетать несколько подходов. Это поможет не допустить, чтобы изменения игроков привели к ошибкам или уязвимостям в игре.

English

Data containers

The most common use for ScriptableObjects is as data containers for shared static data, particularly for game configuration data that doesn’t change at runtime. Typical use cases of ScriptableObjects may include: —

Inventories like item type, icon, rarity, effects

Enemy, player, or item statistics like health, damage, speed, AI parameters

Audio collections like audio clip groups for footsteps, UI sounds, or ambient loops

Config Files like difficulty settings, progression curves, spawn tables

Dialogue data

At runtime, you could store this data on MonoBehaviours, but doing so can be inefficient. As you saw in the previous comparison, MonoBehaviours carry extra overhead since they require a GameObject – and by default a Transform – to act as a host. That means that you need to create a lot of unused data before storing a single value.

To see for yourself, generate a new GameObject with an otherwise empty MonoBehaviour. Then, open the serialized object in a text editor and it will look something like this:

A new GameObject with a barebones MonoBehaviour

Compare that with an empty ScriptableObject and you see its notably leaner:

A ScriptableObject reduces overhead for storing data.

The ScriptableObject slims down this memory footprint and drops the GameObject and Transform which can have a significance at large scale commercial projects. It also stores the data at the project level. That can be helpful, especially if you need to access the same data from multiple scenes.

The extra data from a MonoBehaviour might not impact your application’s performance at first, but as your game grows to commercial scale, with many more objects, it will become noticeable.

ScriptableObject data versus persistent data When ScriptableObjects are referred to as data containers, this usually refers to static or shared configuration data that does not need to change at runtime in a permanent way. While changes to ScriptableObject data do persist within the Editor (much like modifying a material or a prefab), these changes won’t save at runtime in an application build. Any changes made to a ScriptableObject instance during gameplay exist only in memory and are lost when the session ends. Persistent data that needs to be saved from one session and then loaded into another is typically stored in a different file format (e.g., JSON, XML, MessagePack, Protocol Buffers, and so on). See Dual Serialization below for more details. It’s possible to change ScriptableObject data at runtime (e.g., ScriptableObject variables and Runtime Sets) in the game build, but these changes are temporary. Starting a new game session will revert the ScriptableObject data back to its original state at build time. Think of ScriptableObject data as “read-only” for persistent data purposes. Persistent data should be “read-write” and stored externally.

Reducing duplicate data Imagine you have a thousand GameObjects with custom MonoBehaviours, each with several fields. If each component holds its own copy of these values, you’re duplicating a lot of data in memory. This is inefficient, especially when the data is identical across instances and doesn’t change at runtime.

Many objects with duplicate, local data leads to performance inefficiencies.

Instead of duplicating this static data, you can funnel it into a ScriptableObject. Then, each of the thousand objects can point to this shared data asset. Each object stores a reference to the data rather than copying the data itself.

Many objects sharing data via a ScriptableObject

In software design, this is an optimization known as the flyweight pattern. Restructuring your code in this way avoids copying a lot of values and reduces your memory footprint.

Design patterns Design patterns can help developers create more maintainable and flexible code, which can be useful in the often-changing world of game development. Download the free e-book Level up your code with design patterns and SOLID for more about SOLID principles and design patterns.

A reference to the ScriptableObject (instead of a full copy of the data) is comparatively small. As you scale up, the memory savings from not duplicating data can become significant.

The Memory Profiler compares memory usage of duplicate (A) versus shared data (B).

You can store large quantities of shared data in this manner. Consider ScriptableObjects for: —

Saving and storing data during an Editor session

Saving data as an Asset for use at runtime

Unlike MonoBehaviours, ScriptableObjects can’t be attached to a GameObject. Instead, you save them as assets in your project. This is especially useful if you have a prefab that uses unchanging data in its MonoBehaviours.

Refactoring example Consider a MonoBehaviour that controls an NPC’s health. You might define its class like this:

C#
public class NPCHealthUnrefactored : MonoBehaviour {
    [Range(10, 100)] public int MaxHealth;
    [Range(10, 100)] public int HealthThreshold;

public int CurrentHealth; } This works, but you have data that you won’t expect to change at runtime. If you have many objects with NPCHealthUnrefactored attached, this can lead to a lot of unnecessarily duplicated data.

An NPCHealth MonoBehaviour before refactoring

Any data that does not need to change can move it into a ScriptableObject:

C#
[CreateAssetMenu(fileName="NPCConfig")] public class NPCConfigSO : ScriptableObject {
    [Range(10, 100)] public int maxHealth;
    [Range(10, 100)] public int healthThreshold;

} Use the CreateAssetMenu attribute to configure the menu action. You can optionally specify the default fileName or menu item order.

Code conventions in this guide Many of the code samples in this guide are simplified for illustrative purposes and easier readability (e.g., public fields). In production, use private fields and public properties for additional encapsulation and flexibility. Apply the SerializeField attribute to private fields to make them appear in the Editor’s Inspector. A naming convention can also help differentiate scripts for ScriptableObjects from MonoBehaviours. One way to achieve this is to add a “Data” or “SO” suffix at the end of the class name. While this isn’t necessary, it can help keep your project organized and reduce ambiguity. It’s recommended that you maintain and follow a code style guide as your codebase grows. See Create a C# style guide (Unity 6 edition) for more information.

Then the refactored NPCHealth component simplifies to this:

C#
public class NPCHealth: MonoBehaviour {
    // Reference to our ScriptableObject

    public NPCConfigSO Config;
    public int CurrentHealth;
}

The MonoBehaviour now contains a reference to this new ScriptableObject. In the Inspector, everything after refactoring looks similar, except the data is split.

Refactoring splits data between a MonoBehaviour and ScriptableObject

Custom Inspectors When separating data into a ScriptableObject, you have data contained in two places, the ScriptableObject asset and the MonoBehaviour referencing it. To make your MonoBehaviours easier to navigate, consider creating a custom editor. Below is an example for the NPCHealth.

A custom Inspector shows the ScriptableObject’s variables in the MonoBehaviour.

This allows you to inspect the NPCConfig’s variables alongside the other properties in the MonoBehaviour. If you select the original NPCHealth prefab, you can easily edit values in both objects. A custom editor requires only a few lines of code: —

Derive a new class from Editor, and store this in a folder named “Editor.” Apply the CustomEditor attribute with the NPCHealth type.

Reserve a temporary editor for the NPCConfig ScriptableObject.

In OnInspectorGUI, create the editor for the NPCHealth component.

Draw the inspectors from the base class and new custom Inspector.

C#
using UnityEditor;
[CustomEditor(typeof(NPCHealth))] public class NPCHealthEditor : Editor {
    private Editor editorInstance;
    private void OnEnable() {
        // Reset the editor instance editorInstance = null;

    }
    public override void OnInspectorGUI() {
        // The inspected target component NPCHealth npcHealth = (NPCHealth)target;

        if (editorInstance == null) editorInstance = Editor.CreateEditor(npcHealth.config);
        // Show the variables from the MonoBehaviour base.OnInspectorGUI();
C#
// Draw the ScriptableObjects inspector editorInstance.DrawDefaultInspector();

}
}
You can expand on this example with custom property drawers and editor attributes. This can even make for a better user experience when working with ScriptableObjects.

Architectural benefits With ScriptableObjects, you can cleanly separate shared and unshared data. Anything unique and dynamic to the GameObject instance remains inside the MonoBehaviour, while the shared data is stored in the ScriptableObject. Architecture with ScriptableObjects, however, goes beyond just saving memory.

There are a few benefits to restructuring the code architecture: —

Designers can work more independently from software developers: Storing data and logic on a single MonoBehaviour creates the potential for developers and game designers to step over each other’s work. If two people change different parts of the same prefab or scene, this results in a time-wasting merge conflict. Breaking off shared data into smaller files and assets reduces these problems. Architecting with ScriptableObjects also enables designers to build gameplay without always relying on a programmer.

Just be prepared to define a clear workflow between your teams when sharing data. Good communication and establishing some boundaries can help prevent issues here. Some extra error checking or data validation may be necessary as well (e.g., use a Range attribute or OnValidate to prevent bad values). —

Editing shared data is faster and less error-prone: Changes to the shared data now happen all at once. If you need to modify a setting for your NPCs, for example, you could adjust it in just one location, and have it propagate the changes for every affected component in every scene. This reduces any potential errors from mass editing a large number of individual GameObjects by hand. Offloading data into ScriptableObjects can also help with version control and prevent merge conflicts when teammates work on the same scene or prefab.

Save gameplay tweaks in Play mode: Play mode in the Editor is an opportunity for designers to experiment with gameplay and settings. However, any modifications made to MonoBehaviours are lost when exiting Play mode because Unity discards the temporary copy of the scene. Because the ScriptableObjects are assets, changes to their values are saved regardless of whether Unity is in Play mode. This can be useful if you want to make adjustments at runtime. This can, however, also be a liability if you want to revert those changes. Just remember to rely on Unity Version Control or another version control system, so you can always restore your work if necessary. See the guide Version control and project organization best practices (Unity 6 edition) for more information.

Improve scene loading times: When saving a scene or prefab, Unity serializes everything inside of them. That includes every GameObject, every component attached to those GameObjects, and every public field. Unity does this without checking for duplicate data. Moving data to ScriptableObjects can reduce your scene and prefab sizes which can noticeably impact loading and saving.

ScriptableObject variables You can make your shared data containers even more granular with a ScriptableObject representing just one value. For instance, you could create a ScriptableObject class called IntVariable that holds one public field called value:

C#
using UnityEngine;
[CreateAssetMenu(menuName = "Variables/Int", order = 1)] public class IntVariableSO : ScriptableObject { public int value; }
Then, you could use the IntVariable in a MonoBehaviour. Structuring a PlayerHealth class would then look like this: public class PlayerHealth : MonoBehaviour { public IntVariableSO health; }
Though we normally think of ScriptableObjects as holding unchanging values, you can give them methods that update this data at runtime (and reset to an initial value when exiting Play mode). In this way, you can make ScriptableObjects that essentially function as variables – containing integers, floats, booleans, and so on. Your designers can then reserve data for game logic without needing a software developer each time they want to do it. However, this requires planning to be successful. Decide with your designers how to divide authoring gameplay data. The key is to set some boundaries on how to collaborate. For example, the programming team might do the initial setup of ScriptableObjects for use with an inventory system. Then, the design team could use those to fill in each item’s in-game stats or behaviors. With some extra Editor scripting, this can become a near-seamless experience. Another possibility is making the fields in the Inspector toggle between using a shared value from a ScriptableObject and a constant. This can allow the game design team greater freedom to override the ScriptableObject data per instance.

Example of a ScriptableObject-based IntVariable from the open source Unity Atoms project

See the Game architecture with ScriptableObjects presentation from Unite Austin for how to implement this behavior in your own projects. You can also download the open source Unity Atoms project to see a working implementation of ScriptableObject variables.

Dual serialization You can mix how to serialize data within Unity. This allows you to work with ScriptableObjects in the Editor, but then store their data in another location, such as a JSON or XML file. This allows you to take advantage of each format’s strengths. File formats like JSON and XML are suitable for storing persistent data, such as save game data or settings, but can be difficult to work with in the Editor; however, they’re easy to modify outside of Unity with any text editor. In contrast, ScriptableObjects work well in the Editor and can be swapped with a quick dragand-drop operation. However, they aren’t easy to modify outside of Unity or share within your community of players. Mixing serialized formats could open up new possibilities for your game, such as level editing or modding. At build time, a script can convert the other files into ScriptableObjects, which is faster to load than plain text. While you’ll want to keep some sensitive data safely tucked away on your servers (e.g., virtual currency, account information), exposing part of your game data to the community may enhance gameplay by allowing sandbox levels for user experimentation.

Imagine a ScriptableObject that defines your game level layout. It may simply contain a number of Transforms that define placement of prefabs, starting configurations, and so on. Your game scripts will use this data to assemble each level. Imagine the walls and starting positions of a game, stored within a ScriptableObject:

C#
[CreateAssetMenu(fileName ="LevelLayout")] public class LevelLayout : ScriptableObject {
    public Vector3[] wallPositions = new Vector3[2];
    public Vector3[] playerPositions = new Vector3[2];
    public Vector3[] goalPositions = new Vector3[2];
    public Vector3 ballPosition;
}
This defines how you set up the level. Your level management scripts can read the data from the LevelLayout object, then instantiate your prefabs in their correct positions. A custom script can use JsonUtility to export this same data to disk. This results in a text file outside of the Editor that your users can modify with external tools. To load a custom modded level, ScriptableObject.CreateInstance can generate a ScriptableObject at runtime. Then, read the text from the JSON file to populate the ScriptableObject. This LoadLevelFromJson example method shows that in action: using System.IO;
public class LevelManager : MonoBehaviour {
    public ScriptableObject levelLayout;
    public void LoadLevelFromJson(string jsonFile) {
        if (levelLayout == null) {
            levelLayout = ScriptableObject. CreateInstance<LevelLayout>();
        }
        var importedFile = File.ReadAllText(jsonFile);
        JsonUtility.FromJsonOverwrite(importedFile, levelLayout);
    }
}

Your custom data replaces the contents of the ScriptableObject and allows you to use this externally modded level like any other in your game. The application is none the wiser. Be sure to see this work for yourself in the sample project. If we load a modified JSON file, this customized level overrides the default level data on the ScriptableObject.

Mix serialized formats for more flexibility

Note: When deserializing JSON into ScriptableObjects with JsonUtility, you must use the FromJsonOverwrite method. Instead of creating a new object and loading the JSON data into it, JsonUtility loads the JSON data into an existing object. This updates the values stored in classes or objects without any allocations.

Protect your data The simple mod example described above demonstrates one possible application of ScriptableObjects. However, when exposing game data for modification, you should exercise caution to avoid players tampering with the rest of your application. Here are common ways to protect anything that you don’t want modded: —

Encryption: Use encryption to protect data files from being easily read or modified. This can make it more difficult for users to alter critical data.

Digital signatures: You can use a fingerprint algorithm to verify that your data files have not been tampered with.

Server-side validation: If your game relies on data that is stored on a server, check the data on the server before it is used in the game, and reject any data that appears to have been manipulated.

No single approach is foolproof, and it’s generally a good idea to use a combination of these techniques so your players don’t introduce any bugs or vulnerabilities into your game.