Что такое scriptableobject в unity
ScriptableObject
A ScriptableObject is a data container that you can use to save large amounts of data, independent of class instances. One of the main use cases for ScriptableObjects is to reduce your Project’s memory usage by avoiding copies of values. This is useful if your Project has a Prefab that stores unchanging data in attached MonoBehaviour scripts.
Every time you instantiate that Prefab, it will get its own copy of that data. Instead of using the method, and storing duplicated data, you can use a ScriptableObject to store the data and then access it by reference from all of the Prefabs. This means that there is one copy of the data in memory.
Just like MonoBehaviours, ScriptableObjects derive from the base Unity object but, unlike MonoBehaviours, you can not attach a ScriptableObject to a GameObject. Instead, you need to save them as Assets in your Project.
When you use the Editor, you can save data to ScriptableObjects while editing and at run time because ScriptableObjects use the Editor namespace and Editor scripting. In a deployed build, however, you can’t use ScriptableObjects to save data, but you can use the saved data from the ScriptableObject Assets that you set up during development.
Data that you save from Editor Tools to ScriptableObjects as an asset is written to disk and is therefore persistent between sessions.
Using a ScriptableObject
The main use cases for ScriptableObjects are:
To use a ScriptableObject, create a script in your application’s Assets folder and make it inherit from the ScriptableObject class. You can use the CreateAssetMenu attribute to make it easy to create custom assets using your class. For example:
Attach the above script to a GameObject in your Scene. Then, in the Inspector, set the Spawn Manager Values field to the new SpawnManagerScriptableObject that you set up.
Set the Entity To Spawn field to any Prefab in your Assets folder, then click Play in the Editor. The Prefab you referenced in the Spawner instantiates using the values you set in the SpawnManagerScriptableObject instance.
If you’re working with ScriptableObject references in the Inspector, you can double click the reference field to open the Inspector for your ScriptableObject. You can also create a custom Editor to define the look of the Inspector for your type to help manage the data that it represents.
Unity3D: архитектура игры, ScriptableObjects, синглтоны
Сегодня речь пойдет о том, как хранить, получать и передавать данные внутри игры. О замечательной вещи под названием ScriptableObject, и почему она замечательна. Немного затронем пользу от синглтонов при организации сцен и переходов между ними.
Данная статья описывает частичку долгого и мучительного пути разработки игры, различные примененные в процессе подходы. Скорее всего, здесь будет много полезной информации для новичков и ничего нового для «ветеранов».
Связи между скриптами и объектами
Первый вопрос, встающий перед начинающим разработчиком — как связать все написанные классы вместе и настроить взаимодействия между ними.
Самый простой способ — указать ссылку на класс напрямую:
А затем — вручную привязать скрипт через инспектор.
У этого подхода как минимум один существенный недостаток — когда количество скриптов переваливает за несколько десятков, и каждый из них требует две-три ссылки на друг друга, игра быстро превращается в паутину. Одного взгляда на неё достаточно, чтобы вызвать головную боль.
Гораздо лучше (на мой взгляд) организовать систему сообщений и подписок, внутри которой наши объекты будут получать нужную им информацию — и только её! — не требуя при этом полудюжины ссылок друг на друга.
Однако, прощупав тему, я выяснил, что готовые решения в Unity ругают все, кому не лень. Писать с нуля подобную систему для себя мне показалось задачей нетривиальной, а потому я иду искать более простые решения.
ScriptableObject
Знать о ScriptableObject надо, по сути, две вещи:
Каждый из которых хранит в себе всю полезную информацию и, возможно, ссылки на другие объекты. Каждый из них достаточно один раз привязать через инспектор — больше они никуда не денутся.
Теперь мне не нужно указывать бесконечное количество ссылок между скриптами! Для каждого скрипта я могу один раз указать ссылку на моё хранилище — и он получит всю информацию оттуда.
Таким образом, вычисление скорости персонажа принимает весьма элегантный вид:
А если, скажем, ловушка должна срабатывать только на бегущего персонажа:
Причем персонажу совсем не нужно знать ничего ни о заклинаниях, ни о ловушках. Он просто получает данные из хранилища. Неплохо? Неплохо.
Но почти сразу я сталкиваюсь с проблемой. ScriptableOnject’ы не умеют хранить в себе ссылки на объекты сцены напрямую. Иными словами, я не могу создать ссылку на игрока, привязать её через инспектор и забыть про вопрос координат игрока навсегда.
И если подумать, это имеет смысл! Ассеты существуют вне сцены и могут быть доступны в любой из сцен. А что произойдет, если оставить внутри ассета ссылку на объект, находящийся в другой сцене?
Долгое время у меня работал костыль: создается публичная ссылка в хранилище, а затем каждый объект, ссылку на который нужно запомнить, эту ссылку заполнял:
Таким образом, независимо от сцены, моё хранилище первым делом получает ссылку на игрока и запоминает её. Теперь любой, скажем, враг не должен хранить в себе ссылку на игрока, не должен искать его через FindWithTag() (что довольно ресурсоёмкий процесс). Всё, что он делает — обращается к хранилищу:
Казалось бы: система идеальна! Но нет. У нас остаётся 2 проблемы.
И это порождает ряд реакций:
Можно для каждого объекта, заинтересованного в огоньке, прямо в Update() и написать, мол, так и так, каждый фрейм следи за огоньком (if (database.spellController.light.isActive)), а когда зажжется — реагируй! И плевать, что 90% времени эта проверка будет работать вхолостую. На нескольких сотнях объектов.
Или организовать все это в виде готовеньких ссылок. Получается, простенькая функция CastSpell() должна иметь доступ к ссылкам и на игрока, и на огонек, и на список врагов. И это в лучшем случае. Многовато ссылок, а?
Можно, конечно, сохранять всё важное в нашем хранилище при запуске сцены, раскидывать ссылки по ассетам, которые для этого, в общем-то, и не предназначены… Но я опять нарушаю принцип единого хранилища, превращая его в паутину ссылок.
Singleton
Вот тут в игру вступает синглтон. По сути, это объект, который существует (и может существовать) только в единственном экземпляре.
Я привязываю его к пустому объекту сцены. Назовем его GameController.
Таким образом, у меня в сцене есть объект, хранящий в себе всю информацию об игре. Более того — он может перемещаться между сценами, уничтожать своих двойников (если на новой сцене уже есть другой GameController), переносить данные между сценами, а при желании — реализовать сохранение/загрузку игры.
Из всех уже написанных скриптов можно удалить ссылку на хранилище данных. Ведь теперь мне не нужно её настраивать вручную. Из хранилища удаляются все ссылки на объекты сцены и переносятся в наш GameController (они все равно нам скорее всего понадобятся для сохранения состояния сцены при выходе из игры). А дальше я заливаю в него всю необходимую информацию удобным мне способом. Например, в Awake() игрока и врагов (и важных объектов сцены) прописывается добавление в GameController ссылки на самих себя. Так как теперь я работаю с Monobehaviour, ссылки на объекты сцены в него весьма органично вписываются.
Что у нас получается?
Любой объект может получить любую информацию об игре, которая ему нужна:
При этом совершенно не нужно настраивать ссылки между объектами, все хранится в нашем GameController.
Теперь не будет ничего сложного в сохранении состояния сцены. Ведь у нас уже есть вся необходимая информация: враги, предметы, положение игрока, хранилище данных. Достаточно выбрать ту информацию о сцене, которую нужно сохранить, и записать её в файл с помощью FileStream при выходе из сцены.
Опасности
Если вы дочитали до этого места, мне следует вас предостеречь об опасностях такого подхода.
Очень нехорошая ситуация складывается, когда много скриптов ссылаются на одну переменную внутри нашего ScriptableObject. В получении значения ничего нехорошего нет, а вот когда на переменную начинают воздействовать из разных мест — это потенциальная угроза.
Если у нас есть сохраненная переменная playerSpeed, и нам нужно, чтобы игрок двигался с разной скоростью, не следует менять playerSpeed в хранилище, следует получать её, сохранять во временную переменную и уже на неё накладывать модификаторы скорости.
Второй момент — если любой объект имеет доступ к чему угодно — это большая власть. И большая ответственность. И к ней нужно подходить с осторожностью, чтобы какой-нибудь скрипт гриба невзначай не сломал напрочь весь ваш ИИ врагов. Грамотно настроенная инкапсуляция понизит риски, но не избавит вас от них.
Также не стоит забывать о том, что синглтоны — существа нежные. Не стоит ими злоупотреблять.
Многое было почерпнуто из официальных туториалов по Unity, что-то — из неофициальных. До чего-то мне пришлось доходить самому. А значит, у вышеизложенных подходов могут быть свои опасности и недостатки, которые я упустил.
Что такое ScriptableObject?
Теперь для того, чтобы можно было создавать данные непосредственно в редакторе Unity необходимо добавить атрибут CreateAssetMenu у класса объекта данных. Познакомимся с его параметрами, ближе:
Теперь мы с легкостью можем добавлять новых юнитов легким движением руки.
Структура проекта и загрузка всех данных
А я расскажу основные практики, которые использую сам при создании прототипов и своих проектов.
Resources зачем она нужна?
Папка Resources в отличии от всех других папок в структуре проекта обладает отличительным свойством. Доступ к ассетам из этой папки можно осуществить непосредственно из кода. Все данные из этой папки сохраняются при билде и могут быть использованы позже, даже если вы ни разу не использовали эти ассеты. Поэтому стоит аккуратно выбирать, какие данные вы собираетесь там хранить.
Для того, чтобы загрузить данные всех возможных юнитов непосредственно внутри игры можно воспользоваться следующим кодом:
Послесловие
Надеюсь, что мне удалось вас убедить, что ScriptableObject действительно полезный инструмент и мощный инструмент при создании игр, а если нет, то тогда ждите продолжения статьи с более широким раскрытием всех возможностей ScriptableObject.
О возможностях использования ScriptableObject можно почитать в следующих статьях:
Смотрите также:
Комментарии
лёш, ты же не против если я дополню? )
скриптаблы, для понимания могут храниться на уровне ассета и на уровне сцены.
ниже про кейс со сценой.
может создаться иллюзия, что раз скриптаблы обеспечивают ссылочность, то они могут быть хорошим решением для работы внутри ваших компонентов (тем убедительнее, что юнити сами юзают их таким образом в одном их своих видео). Но это не так и использование скриптаблов таким образом это плохая практика. По мануалам юнити рекомендует (и не без оснований) использовать скриптаблы только как ассеты.
Так же ты не упомянул несколько важных деталей про скриптейблы:
Devion, только рад за дополнение, Спасибо)
хм, а вот это интересно, не пробовал никогда такой кейс.
т.к. это UnityEngine.Object то для него обязательно учитывать операции Undo, помечать объект грязным при изменениях, и всё вытекающее.
А это я так понимаю для ситуации, когда пишется кастомный редактор для них, так?
скриптаблы не заменяют собой json, иногда нужно вынести конфиги за сам билд, тут в любом случае не будет никакого скриптабла.
Скриптуемый объект (ScriptableObject)
ScriptableObject это класс, который позволяет вам хранить большое количество передаваемой информации независимо от образцов скрипта. Не путайте этот класс с классом под названием SerializableObject, который является классом редактора и служит для других целей. Представьте на мгновение, что вы создали префаб со скриптом, который имеет массив из миллиона целых чисел. Массив занимает 4 мегабайта памяти и принадлежит префабу. Каждый раз создавая экземпляр этого префаба, вы создаёте и экземпляр этого массива. Если вы создадите 10 игровых объектов, тогда в итоге размер занимаемый массивами для этих 10 экземпляров будет равен 40 мегабайтам.
Unity сериализует все типы примитивов, строк, массивов, списков, специфичных типов для Unity, таких как Vector3 и ваших собственных классов с атрибутом Serializable в качестве копий, относящихся к объекту, в котором они определены. Данное означает, что если вы создали класс ScriptableObject и сохранили в нём объявляемый массив из миллиона целых чисел, тогда этот массив будет передаваться вместе с этим образцом. При этом экземпляры считают, что обладают разными данными. Поля ScriptableObject или любые UnityEngine.Object поля, такие как MonoBehaviour, Mesh, GameObject и т.д, в противоположность значениям хранятся в ссылках. Если у вас есть скрипт, ссылающийся на ScriptableObject, содержащий миллион целых чисел, Unity сохранит в данных скрипта лишь ссылку на ScriptableObject. ScriptableObject в свою очередь хранит массив. 10 экземпляров префаба, которые ссылаются на класс ScriptableObject, который использует 4 мегабайта памяти, в итоге заняли бы 4 мегабайта вместо 40, о которых шла речь немного раньше.
Класс ScriptableObject необходимо использовать в тех случаях, когда нужно снизить расход памяти путём избежания копирования значений, но его также можно использовать для определения включаемых наборов данных. В качестве примера для иллюстрации его работы представьте себе NPC магазин в РПГ игре. Вы можете создать несколько ассетов вашего ShopContents (содержимого магазина) ScriptableObject, каждый из которых определял бы набор предметов, доступных для покупки. В случае, когда игра разделена на три зоны, в каждой зоне продаются свои предметы. Скрипт вашего магазина будет ссылаться на объект ShopContents, чтобы определить какие предметы доступны в данный момент. Для большего количества примеров, пожалуйста посетите руководство по скриптингу.
Once you have defined a ScriptableObject-derived class, you can use the CreateAssetMenu attribute to make it easy to create custom assets using your class.
Подсказка: при работе в инспекторе с экземплярами ScriptableObject вы можете дважды нажать на поле ссылки, чтобы открыть инспектор для своего ScriptableObject (скриптуемого объекта). Вы также можете создать пользовательский редактор для определения вида инспектора своего типа, чтобы помочь управлять данными, которые он представляет.
Три способа построить игру на основе объектов 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 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.
Используйте модульный подход:
Упрощайте изменение и редактирование элементов:
Упрощайте отладку:
на самом деле это кит-помощник для первых двух. Чем выше модульность вашей игры, тем легче тестировать каждый из ее элементов по отдельности. Чем легче игра поддается изменению, чем больше в ней элементов, поддающихся контролю в окне Inspector, тем легче будет ее отладка. Сделайте так, чтобы состояние отладки можно было просматривать в окне Inspector, и никогда не помечайте функцию как завершенную, пока не выработаете план по ее отладке.
Один из самых простых вариантов архитектуры на основе ScriptableObjects — это изолированная переменная в виде ассета. Ниже приведен пример FloatVariable, который можно легко расширить до любого сериализуемого типа.
Любой сотрудник вашей студии, независимо от уровня знаний, сможет объявить новую игровую переменную, создав новый ассет FloatVariable. Любой класс MonoBehaviour или ScriptableObject может использовать общедоступный ассет FloatVariable вместо общедоступной переменной с плавающей запятой для ссылки на новое общее значение.
И даже лучше — если один MonoBehaviour изменит значение FloatVariable, то остальные MonoBehaviour будут в курсе этого изменения. Это — основа слоя коммуникации между системами, которым не нужно ссылаться друг на друга.
Одним из примеров такой архитектуры может быть показатель здоровья игрока (HP). В однопользовательской игре за показатель здоровья игрока может отвечать ассет FloatVariable под названием PlayerHP. При получении урона система будет отнимать его от значения PlayerHP, а при исцелении — добавлять нужное значение.
Теперь представьте префаб индикатора здоровья в сцене. Индикатор здоровья отслеживает переменную PlayerHP для обновления. Этот индикатор с легкостью можно перенастроить на другую переменную, например, PlayerMP. Индикатор здоровья не знает о существовании игрока в сцене — он просто считывает значение переменной, в которую объект игрока это значение записывает.
При такой организации вам будет очень легко добавлять новые элементы, отслеживающие переменную PlayerHP. В зависимости от показателя здоровья может менять свое поведение музыкальная система, может меняться характер атак врагов, знающих, что враг ослаблен, а экранные эффекты могут подчеркивать опасность следующей атаки и так далее. Ключевой момент в том, что скрипт Player не отправляет сообщения этим системам, а сами системы не знают о существовании GameObject игрока. Вы сможете заглянуть в Inspector во время игры и изменить значение PlayerHP для тестирования.
Меняя значение FloatVariable, полезно копировать данные в значение Runtime, чтобы не менять значение, сохраняемое на диске в ScriptableObject. Благодаря этому классы MonoBehaviours получат доступ к значению RuntimeValue, что позволит сохранить в неизменном виде значение InitialValue, сохраненное на диске.
Одна из любимых систем, которую Райан реализует с помощью ScriptableObjects, — это система событий. Система событий помогает улучшить модульную структуру кода за счет обмена сообщениями между системами, которые не знают непосредственно о существовании друг друга. Такие системы дают возможность реагировать на изменения состояний без непрерывного отслеживания в цикле Update.
Ниже приведены примеры кода системы событий, состоящей из двух частей: ScriptableObject под названием GameEvent и класс MonoBehaviour под названием GameEventListener. Дизайнер может создать любое количество объектов GameEvent в проекте, каждый из которых будет отвечать за обмен важной информацией. Класс GameEventListener ожидает соответствующего события GameEvent и отвечает вызовом UnityEvent (который является не настоящим событием, а скорее сериализованным вызовом функции).