Формат уровня
Что лежит в папке уровня, какие файлы едут вместе с ним, какие остаются на устройстве и как устроен JSON внутри
Папка уровня
Уровень - это папка. Каждое имя файла в ней зафиксировано в FileNames:
| Файл | Модель | Что внутри |
|---|---|---|
level.json или level.blob | Level | содержимое: настройки, объекты и события, аудио, ресурсы, подсказки |
metadata.json или metadata.blob | LevelMeta | название, описание, авторы, лицензия, запись о каждом ресурсе, ссылка на обложку |
logo.png или logo.jpg | - | обложка |
| трек, картинки, шрифты | - | медиафайлы, на которые ссылается уровень |
Расширение и есть формат. Внутри файлов оно нигде не записано. Читатель смотрит, какой из файлов существует: level.json или level.blob
Уровень и метаданные выбирают формат независимо. Например, встроенный уровень new-zero-demo хранит level.blob рядом с metadata.json
metadata.json - отдельный файл специально. Каталог показывает тысячу уровней по тысяче маленьких файлов метаданных. Ни один уровень при этом не открывается
Ресурсы
Ресурс адресуется парой uri и uri_type (ResourceUriType):
| Значение | Тип | Где файл |
|---|---|---|
| 1 | LevelPath | внутри папки уровня. Только этот тип делает уровень переносимым |
| 2 | AbsolutePath | где-то ещё на этом устройстве |
| 3 | DirectUrl | скачивается по сети |
| 4 | StreamingAssets | поставляется с игрой, а не с уровнем |
AbsolutePath получается, когда уровень создают вокруг песни. Экспорт архива копирует такой файл внутрь. Подробнее - Архивы и защита
Обложка доступна через LevelMeta.LevelLogo, как любой другой ресурс. logo.png или logo.jpg - решают байты самого файла, а не имя исходной картинки
Что не едет вместе с уровнем
Эти файлы лежат рядом с папкой levels, никогда не внутри папки уровня. Архивирование, отправка или удаление уровня их не трогают:
| Папка или файл | Что это | Почему остаётся |
|---|---|---|
backups/ | автосохранения редактора, по папке на id уровня, backup_level_<время>.<расширение> | копия внутри папки, которую она защищает, удаляется вместе с ней |
stats/ | рекорды и прогресс по LevelId и statistics.json | присланный уровень приходил бы уже пройденным |
settings.json | UserSettings, настройки игрока на всё устройство | они принадлежат игроку, а не уровню |
resources/themes, effects, shapes, prefabs | общие библиотеки устройства | они общие для всех уровней на устройстве |
reports/ | диагностические отчёты | - |
Ключи JSON
JSON всегда пишется компактно, без отступов. Чтобы читать глазами, отформатируйте его в текстовом редакторе
Длина ключа зависит от того, сколько раз он встречается в уровне:
- ключ, который встречается раз на файл, пишется полными словами в
snake_case:level_id,min_generation - ключ, который повторяется тысячи раз, сокращается до нескольких букв:
objs,f,e,v
Ключи занимают 52% байтов файла уровня, поэтому это правило, а не вкусовщина. Все ключи объявлены в Names.cs
Полиморфное значение пишется как [тег, данные]. Простая строка - [0,{"v":"Author"}], локализованная - [1,{"strs":[...]}]
Обёртки идентификаторов вроде ObjectId пишутся голым числом или строкой
Идентификаторы
Знак целочисленного идентификатора имеет смысл. 0 всегда значит "не задан":
| Идентификатор | Ключ JSON | Положительный | Отрицательный |
|---|---|---|---|
ObjectId | id, и pid для родителя | объект уровня | объект игры: -1 камера, -2 локальный игрок, -3 корень шаблона префаба |
| идентификаторы ресурсов | txid, fnid, auid, byid, ttid | ресурс, который поставляется с игрой | собственный ресурс уровня |
AudioId аудиодорожки | aid | дорожка этого уровня | не используется |
pid, равный 0, значит, что у объекта нет родителя. Числа меньше -3 существуют только во время работы игры и в файл не попадают
У Marker, Checkpoint и BeatSegment нет идентификатора. Их адрес - место во времени: у маркера и чекпоинта это кадр f, у сегмента ритма - первый кадр его промежутка sp (сегменты не пересекаются). Если такой объект сдвинуть, его адрес изменится
Переопределение префаба называет поле номером, а не ключом JSON. Установка хранит свои переопределения в списке mod. Ключ key каждого переопределения держит три числа:
id- объект внутри шаблона, а не его копия в уровнеf- постоянный номер поляi- элемент поля-списка или-1для поля целиком
Номер закреплён за полем навсегда. Переименование ключа JSON этого поля не ломает переопределения, уже сохранённые в уровнях
Внешний "g"
Каждый корень сериализации завёрнут в конверт с двумя ключами:
{"g":1,"v":{"level_id":"18df5f61-3aa4-4812-bf69-d357f2201bc3","vrs":"1.0", ... }}
g - поколение модели (Names.Generation), v - данные
Конверты вкладываются. Внутри Level свой конверт есть у LevelSettings, GameLevel, AudioLevel, LevelResources и LevelHints. Как работают поколения - Версии и миграции
g и vrs - разные числа. vrs - версия уровня от автора (LevelMeta.LevelVersion), к формату она отношения не имеет. Не меняйте g вручную: файл с g выше, чем знает сборка, не читается целиком