|
| 1 | +### Введение |
| 2 | + |
| 3 | +В каждой игре есть данные, с которыми работают гейм-дизайнеры. В рпг - это база данных айтемов, в матч-3 - стоимость в кристаллах инструментов из магазина, в экшенах - количество хп, на которое лечит аптечка. |
| 4 | + |
| 5 | +Для хранения таких данных существует много способов - кто-то хранит их в таблицах, в xml или json файлах, которые редактируют собственными инструментами. Unity предоставляет свой способ - Scriptable Objects (SO), которые мне нравится тем, что для их визуального представления не нужно писать свой редактор, легко делать ссылки на ассеты игры и друг на друга, а с появлением Addressables эти данные можно легко и удобно хранить вне игры и обновлять отдельно. |
| 6 | + |
| 7 | +В этой статье я хотел бы рассказать о своей библиотеке SODatabase, с помощью которой можно удобно создавать, редактировать и использовать в игре (редактировать и сериализовать) scriptable objects. |
| 8 | + |
| 9 | +### Создание и редактирование SO |
| 10 | + |
| 11 | +Создание и редактирование SOшек я веду в отдельном окне, которое чем-то похоже на окно проекта с инспектором - слева находится дерево папок (папка, в которой находятся все SOшки - группа в addressables), а справа - инспектор выделенной SOшки. |
| 12 | + |
| 13 | + |
| 14 | + |
| 15 | +Для отрисовки такого WindowEditor’а я использую библиотеку Odin Inspector. Кроме того, я использую сериализацию для SO из этой библиотеки - она значительно расширяет стандартную юнитиевскую сериализацию, позволяя хранить полиморфные классы, глубокую вложенность, ссылки на классы. |
| 16 | + |
| 17 | + |
| 18 | + |
| 19 | +Создание новых SO происходит через нажатие кнопки в этом окне - там нужно выбрать тип нужной сошки, и она создаётся в папке. Для того, чтобы тип SO появился в этом окне в качестве варианта, SO должен наследоваться от DataNode, который имеет лишь одно дополнительное поле к ScriptableObject |
| 20 | +```csharp |
| 21 | +public string FullPath { get; } |
| 22 | +``` |
| 23 | +Это путь к данной SO, с помощью которого к ней можно будет обратиться в рантайме. |
| 24 | + |
| 25 | +### Доступ к SO в игре |
| 26 | + |
| 27 | +В игре обычно нужно либо получить какую-то конкретную модель, например, SO со списком настроек какого-либо окна, либо набор моделей из папки - например, список айтемов, где модель каждого айтема представляет собой отдельный SO. |
| 28 | +Для этого в static классе SODatabase есть два основных метода, которые возвращают либо весь список моделей из нужной папки, либо конкретную модель из папки с определённым именем. |
| 29 | + |
| 30 | +```csharp |
| 31 | +public static T GetModel<T>(string path) where T : DataNode |
| 32 | + |
| 33 | +public static List<T> GetModels<T>(string path, bool includeSubFolders = false) where T : DataNode |
| 34 | +``` |
| 35 | + |
| 36 | + |
| 37 | +Уточню, что один раз в начале игры перед запросом моделей SODatabase нужно проинициализировать, чтобы обновились данные из Addressables. |
| 38 | + |
| 39 | +### Загрузка и сохранение |
| 40 | + |
| 41 | +Один из недостатков ScriptableObject по сравнению с хранением данных с сериализацией в собственном формате является то, что в них нельзя записывать данные из игры в рантайме. То есть по сути ScriptableObject предназначены для хранения статичных данных. Но любой игре нужна загрузка и сохранение, и я реализую это через те же самые SO из базы данных. |
| 42 | + |
| 43 | +Возможно это не идиоматичный способ - совмещать базу статичных моделей игры с загрузкой и сохранением динамических данных, но в моём опыте ещё ни разу не было случая, когда это создало бы какие-то неудобства, но при этом есть ряд ощутимых плюсов. Например, с помощью тех же инспекторов SOшек можно смотреть игровые данные в эдиторе и менять их. Можно удобно загружать сейвы игроков, смотреть их содержимое и редактировать в unity, не используя никаких внешних утилит и собственных редакторов для визуализации xml или других форматов. |
| 44 | + |
| 45 | +Я достигаю этого, сериализуя динамические поля в ScriptableObject с помощью JSON. |
| 46 | + |
| 47 | +Класс DataNode - родительский класс всех SO, хранящихся в SODatabase, помечен как |
| 48 | +```csharp |
| 49 | +[JsonObject(MemberSerialization.OptIn, IsReference = true)] |
| 50 | +``` |
| 51 | +и все его *JsonProperty* сериализуются в файл save.txt при сохранении игры. Соответственно при инициализации SODatabase кроме запроса данных об изменении addressables происходит *JsonConvert.PopulateObject* для каждой динамической модели из SODatabase, используя данные из этого файла. |
| 52 | + |
| 53 | +Для того, чтобы это работало гладко, я сериализую ссылки на SO (которые могут являтся динамическими полями, помеченными как JSONProperty) в строку-путь, и потом десериализую обратно в ссылки на SO при загрузке. Есть ограничение - данные на игровые ассеты динамическими быть не могут. Но это не фундаментальное ограничение, просто у меня ещё не было случая, когда такие динамические данные потребовались бы, поэтому я не реализовывал специальную сериализацию для таких данных. |
| 54 | + |
| 55 | +### Примеры |
| 56 | +В рпг для хранения информации об игроке я прямо создаю *PlayerSO*, в котором одни только динамические поля - имя, количество экспы игрока, кристаллов и так далее. Точно также для инвентаря игрока я создаю *PlayerInventorySO*, где храню список ссылок на айтемы игрока (каждый айтем представляет собой ссылку на статичный SO из SODatabase). |
| 57 | + |
| 58 | +Бывают наполовину статические, наполовину динамические данные - например, квесты. Возможно, это не лучший подход, но я прямо в моделях *QuestSO *со статической информацией о квестах (название, описание, цели и т.д.) храню динамическую информацию по прогрессу в этом квесте. Таким образом гейм-дизайнер в одном инспекторе видит всю инфу о текущем состоянии квеста и его описание. |
| 59 | + |
| 60 | + |
| 61 | + |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | + |
| 67 | + |
| 68 | + |
| 69 | + |
| 70 | + |
| 71 | + |
| 72 | + |
| 73 | + |
| 74 | + |
0 commit comments