{"id":14279,"url":"\/distributions\/14279\/click?bit=1&hash=4408d97a995353c62a7353088166cda4ded361bf29df096e086ea0bbb9c1b2fc","title":"\u0427\u0442\u043e \u0432\u044b\u0431\u0435\u0440\u0435\u0442\u0435: \u0432\u044b\u0435\u0445\u0430\u0442\u044c \u043f\u043e\u0437\u0436\u0435 \u0438\u043b\u0438 \u0437\u0430\u0435\u0445\u0430\u0442\u044c \u0440\u0430\u043d\u044c\u0448\u0435?","buttonText":"","imageUuid":""}

Сохранение игры через JSON

Введение:

В Unity существует встроенный функционал для сохранения простых типов данных. Таких как: float, int, string. Речь идет о классе PlayerPrefs. Он удобен, когда нужно сохранить что-то простое. Например имя пользователя или счетчик уровней. Но порой нужно сохранять более сложные вещи. Коллекции объектов, расположение их в игровой сцене и даже скриншоты. Для таких целей можно использовать сериализацию через JSON. Благодаря этому способу мы можем преобразовать (сериализовать) C# класс в бинарный текстовый файл, после, чего сохранить его на устройстве, а впоследствии десериализовать (прочитать) и на основе этой информации построить сцену. В этой статье мы разберемся с тем как это делать и какие инструменты для этого предусмотрены в Unity.

Основная часть:

Подготовка сцены:

Рассмотрим ситуацию, в которой мы хотим сохранить всю сцену. Напрямую просто сохранить сцену в том виде в котором она была на момент выхода из игры - к сожалению, не выйдет. Вместо этого необходимо запомнить информацию о расположении количестве объектов из которых состоит сцена, и при запуске расставить их опять в заданном порядке.

Итак, я подготовил несколько разных объектов для наглядности, и простой скрипт, который будет спаунить их по клику соответствующей кнопкой мыши, в месте клика с рандомным вращением по оси Y. Не будем подробно останавливаться на рассмотрении данного скрипта, так как это не является нашей сегодняшней основной темой.

Главное, что он выполняет свою функцию и мы можем легко и быстро создать несколько разных объектов на сцене.

Сохранение:

Класс объекта:

Для того чтобы сохранить информацию об этих объектах в файл, нам нужен какой-то класс, через который мы получим интересующие нас параметры. В моем случае - это класс SaveableObject. Поскольку мы имеем несколько разных объектов, я создал enum, в котором объявил несколько констант для маркировки этих объектов и объявил соответствующее поле в нашем классе. И задал в инспекторе каждому из префабов его константу.

Сериализуемый класс:

Теперь я создал новый C# скрипт - SaveManager. И в этом файле создал базовый публичный класс - SavedObject. С его помощью мы и будем сериализовывать наши данные, поэтому обязательно к нему необходимо добавить атрибут Serializeable. Ну и в самом классе объявить все публичные поля, для параметров, которые мы хотим сохранить. В данном случае это позиция, поворот, и тип объекта, который мы будем спаунить при загрузке сцены.

Также нам понадобится еще один сериализуемый класс, который будет хранить в себе список этих объектов. Этот список мы и запишем в файл.

Метод сохранения параметров:

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

Метод сохранения:

Теперь в классе SaveManager, я объявлю метод Save, а нас сцене добавлю соответствующую кнопку и привяжу к ней этот метод.

В самом же методе нам необходимо для начала объявить новую SaveData, чтобы мы могли в ее список добавлять наши объекты. Ну и инициализировать сам список.

Дальше нам необходимо каким-то образом получить доступ ко всем сохраняемым объектам на сцене, я просто воспользуюсь методом FindObjectsOfType. Да, это не самый оптимальный вариант, но здесь его использование уместно, так как сохранение - это далеко не частая операция, да и нам для простоты примера так будет лучше.

Ну и в сформированной коллекции SaveableObject для каждого члена вызываем метод SaveObject и добавляем результат в список объектов нашей SaveData.

Запись в файл JSON:

Теперь когда у нас есть коллекция сериализуемых классов в другом сериализуемом классе, мы готовы записать его в файл для дальнейшего хранения. Для начала необходимо сформировать путь, по которому мы будем хранить этот файл. У каждого приложения в памяти устройства есть специальная директория, в которой разработчики могут что-то хранить.В windows, например, эту папку можно найти по пути: “C:\Users\{имя учетной записи}\AppData\LocalLow\{bundle id проекта{\{product name проекта}”Для того чтобы достучаться до этой директории можно воспользоваться публичным параметром класса Application - persistentDataPath. На его основе мы можем сформировать путь в котором будем хранить наш файл. Но также этот путь должен включать имя будущего файла, поэтому сперва сформируем его. Для этого объявим поле, в котором будем хранить имя файла по умолчанию. И также объявим переменную fileName, где будем формировать имя для текущего файла. Здесь с помощью метода Directory.GetFiles из пространства имен System.IO, получаем все файлы из директории persistentDataPath. Конечно в persistentDataPath может быть организована более сложная иерархия, с помощью метода Directory.CreateDirectory, но мы сейчас для примера будем хранить файлы прямо в корне. Теперь получаем количество этих файлов и добавляем к нему 1. Таким образом наши файлы сохранения будут называться Save1, Save2, Save3….

Теперь Сформируем сам путь файла. Здесь будет уместно воспользоваться методом Path.Combine.

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

Ну и теперь можем сформировать json с помощью метода JsonUtility.ToJson, передав в него нашу saveData. Так как это просто текст, мы можем спокойно сохранить его в переменную типа string

Далее нам необходимо записать сам файл, для этого воспользуемся методом File.WriteAllText, передав ему путь по которому будет храниться файл и наш json, в качестве содержимого

Теперь можно сделать сохранение и проверить наличие файла в соответствующей директории.

Загрузка:

Подготовка UI:

На сцене рядом с кнопкой Save, я создал ScrollRect, в котором будут появляться кнопки загрузки разных сохранений, а также создал префаб самой кнопки. И в нашем классе SaveManager, создаю соответствующие поля - ссылки на эти объекты.

Метод обновления UI:

Для того, чтобы у нас была возможность переключаться между разными сохранениями, необходимо, чтобы в UI была кнопка для каждого сохранения.

Для этого напишем метод UpdateLoadButtons, в этом методе для начала получим все имеющиеся файлы сохранений И сформируем из них лист. Именно лист, а не массив.

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

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

Теперь вызовем этот метод из старта и в конце метода Save

Теперь при создании сохранения, в UI, будут появляться соответствующие кнопки

Метод загрузки:

Сперва, сделаем наш SaveManager синглтоном, чтобы мы могли легко на него ссылаться из других классов.

Не буду подробно останавливаться, на этом паттерне, но если кратко, то его суть в том, что в классе создается статическое публичное поле типа этого же класса и в Awake, ему присваивается в качестве значения экземпляр этого класса. Таким образом, через это поле мы можем получить доступ к экземпляру из любой точки всего решения.

Теперь объявим публичный метод Load, который будет принимать номер сохранения. Используя этот номер сформируем путь до файла, с помощью, уже знакомого нам Path.Combine, а теперь получим сам файл с помощью File.ReadAllText. Ну а после этого десериализуем его с помощью JsonUtility.FromJson, в наш класс SaveData

Метод спауна:

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

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

Теперь в начале метода SpawnObjects уничтожаем всех чайлдов этого объекта.

Далее мы пробежимся по списку и инстанциируем каждый объект, но сперва нам необходимо конвертировать enum из нашего SavedObject в реальный игровой объект. Есть много способов как это сделать, все зависит от конкретного проекта, но я сейчас просто напишу такой незамысловатый конвертор,в классе, который отвечает за спаун объектов по списку, просто потому что в этом классе у меня есть список всех объектов, и не буду на нем подробно останавливаться.

Теперь мы можем спаунить наши объекты.

Вызов загрузки из кнопки:

Завершающим шагом добавим к префабу кнопки загрузки скрипт LoadButton, который будет обрабатывать нажатие на конкретную кнопку. В нем напишем метод OnButtonClick, в теле, которого вызовем метод Load в нашем синглтоне, передав в него номер трансформа кнопки в родительском объекте, прибавив 1, так как индексация чайлдов начинается с 0, а наши сохранения с 1. Ну и, конечно, не забываем добавить этот метод к кнопке в инспекторе.

Проверка результата:

Заспауним несколько объектов и создадим сохранение. Теперь, для чистоты эксперимента, перезагрузим сцену, после чего нажмем кнопку Load: Save 0. Мы увидим наши объекты расставленные в том же порядке. Можно создать несколько разных файлов сохранения и переключаться между ними.

Заключение:

В этой статье были описаны базовые принципы сохранения игрового прогресса. Применяя их можно выстраивать большие схемы сохранения, так как потенциал сериализации данных поистине бесконечен и ограничен только фантазией разработчика. Надеюсь эта статья была полезной и помогла разобраться с тем, как сохранять прогресс игры в файл json, и как потом загружать его из памяти. На этом у меня все и до новых встреч!

0
1 комментарий
Ekaterina Ilinykh

Поскорее бы дождь!!!

Ответить
Развернуть ветку
-2 комментариев
Раскрывать всегда