Как сохранять и загружать игры в Unity
В этом уроке вы узнаете, как сохранять и загружать игру в Unity, используя PlayerPrefs, Serialization и JSON.
Игры становятся все длиннее и интереснее, а некоторые из них занимают более 100 часов игрового времени. Практически невозможно представить, что игроки могут выполнить все то, что может предложить им игра, всего за один подход. Вот почему возможность для игрока сохранять свою игру — это одна из самых важных функций, которые должна иметь каждая большая игра, даже если это простой платформер.
Но как создать файл сохранения и что в нем должно быть? Нужно ли при этом использовать файл сохранения, чтобы отслеживать настройки плеера? Или может лучше использовать облачное хранилище для сохранений, чтобы в случае необходимости их можно было позже загрузить на другое устройство?
В этом уроке вы узнаете:
- Что такое сериализация и десериализация.
- Что такое PlayerPrefs и как его использовать для сохранения настроек игрока.
- Как создать файл сохранения игры и сохранить его на диск.
- Как загрузить файл сохраненной игры.
- Что такое JSON и как вы можете его использовать.
Предполагается, что у вас есть некоторые базовые практические знания о том, как работает Unity. Однако, если у вас возникнут какие-либо вопросы в ходе прохождения этого урока, то вы всегда можете освежить память воспользовавшись данным разделом. Кроме того, даже если вы новичок в C#, у вас не должно возникнуть никаких проблем, за исключением нескольких концепций, которые могут потребовать более детального изучения.
Примечание: Если вы новичок в Unity или хотите приобрести больше навыков, то вам следует ознакомиться с другими учебниками по Unity, где вы можете узнать много интересной и полезной информации.
Введение
Загрузите стартовый проект здесь. Вы будете использовать специальный код для сохранения и загрузки игры, а также изучите логику сохранения настроек игроков.
Важные концепции сохранения
Существует четыре ключевых понятия которыми можно охарактеризовать процесс сохранения в Unity:
PlayerPrefs: это специальная система кеширования для отслеживания простых настроек игрока между игровыми сессиями. Многие начинающие программисты ошибаются, думая, что они могут использовать этот инструмент в качестве системы сохранения игр. Его следует использовать только для отслеживания простых вещей, таких как графика, настройки звука, информация для входа в систему или другие основные данные, относящиеся к пользователю.
Сериализация: это своего рода магия, которая заставляет Unity корректно работать. Сериализация — это преобразование объекта в поток байтов. Чтобы лучше понимать, о чем идет речь посмотрите на этот рисунок:
Что такое «объект»? В этом случае «объект» — это любой скрипт или файл в Unity. Фактически, всякий раз, когда вы создаете сценарий MonoBehaviour, Unity использует сериализацию и десериализацию для преобразования этого файла в код C++, а затем обратно в код C#, который вы видите в окне inspector.
Примечание: Если вы являетесь Java-разработчиком или веб-разработчиком, возможно, вы знакомы с концепцией, известной как маршалинг. Сериализация и маршалинг являются синонимами, но в случае, однако между этими двумя понятиями существует большая разница. Сериализация подразумевает преобразование объекта из одной формы в другую (например, объект в байты), тогда как маршалинг — это получение параметров из одного места в другое.
Десериализация: Это процесс, противоположный сериализации, а именно преобразование потока байтов в объект.
JSON: Эта аббревиатура расшифровывается как JavaScript Object Notation, который является удобным форматом для отправки и получения данных, вне зависимости от языка. Например, у вас может быть веб-сервер, работающий на Java или PHP. Вы не можете просто отправить объект C#, но вы можете отправить JSON-версию этого объекта и позволить серверу воссоздать его локализованную версию. Вы узнаете больше об этом формате в последнем разделе, но сейчас важно просто понять, что это способ форматирования данных, чтобы сделать их мультиплатформенными для чтения (например, XML). При преобразовании в/и из JSON используются термины JSON-сериализация и JSON-десериализация соответственно.
Player Prefs
Проект, который вы скачали изначально настроен таким образом, что все, на чем вам нужно сосредоточится, — это логика сохранения и загрузки игр. Однако, если вам интересно, как все работает, то вы можете открыть все сценарии для более подробного изучения.
Откройте проект, который вы скачали, запустите сцену с именем Game и затем нажмите play.
Чтобы начать игру, нажмите кнопку «New Game». В этой игре вам необходимо использовать мышку для перемещения. Нажмите левую кнопку мыши, чтобы выстрелить и поразить цели (которые перемещаются вверх и вниз через различные промежутки времени), получать очки за каждое удачное попадание. Попробуйте и посмотрите, сколько очков вы сможете получить за 30 секунд. Чтобы вызвать меню в любое время, нажмите клавишу escape.
Эта игра довольна забавная и даже увлекательная, но без музыкального сопровождения немного скучновата. Возможно, вы заметили, что есть музыкальный переключатель, но он был выключен. Нажмите «Play», чтобы начать новую игру, но на этот раз нажмите «Music» и установите значение «On», чтобы вы могли услышать музыку, когда начнете игру. Убедитесь, что ваши колонки или наушники подключены!
Изменить настройки музыки было несложно, но если вы нажмете кнопку воспроизведения еще раз, то заметите проблему: музыка больше не воспроизводится. Чтобы исправить эту ошибку вам потребуется инструмент PlayerSettings.
Для начала создайте новый скрипт с именем PlayerSettings в папке Scripts. Поскольку вы будете использовать некоторые элементы пользовательского интерфейса, добавьте следующую строку вверху:
using UnityEngine.UI; Затем добавьте следующие переменные: [SerializeField] private Toggle toggle; [SerializeField] private AudioSource myAudio;
Именно они будут отслеживать объекты Toggle и AudioSource.
Далее добавьте следующую функцию:
public void Awake () { // 1 if (!PlayerPrefs.HasKey("music")) { PlayerPrefs.SetInt("music", 1); toggle.isOn = true; myAudio.enabled = true; PlayerPrefs.Save (); } // 2 else { if (PlayerPrefs.GetInt ("music") == 0) { myAudio.enabled = false; toggle.isOn = false; } else { myAudio.enabled = true; toggle.isOn = true; } } }
Эти настройки означают:
- Осуществляется проверка, есть ли в PlayerPrefs кэшированная настройка для кнопки «music». Если там нет никакого значения, то создается несколько ключ-значений для кнопки звука со значением 1. Кроме того, тут происходит включение и выключение переключателя AudioSource. Эти настройки будут использованы при первом запуске игры. Значение 1 используется, потому что вы не можете сохранить какое-то определенное логическое значение (но вы можете использовать 0 как false и 1 как true).
- Тут идет проверка ключа «music», сохраненного в PlayerPrefs. Если значение установлено на 1, значит на проигрывателе была включена музыка, поэтому активируется режим воспроизведения звуков и соответствующий переключатель. В противоположном случае музыку наоборот выключается, а тумблер переходит в отметку OFF.
Теперь сохраните изменения в вашем скрипте и вернитесь в Unity.
Добавьте скрипт PlayerSettings в GameObject и разверните пользовательский интерфейс GameObject. Далее вам нужно открыть меню GameObject, чтобы увидеть его дочерние элементы. Перетащите объект Music GameObject в поле Toggle сценария PlayerSettings, выберите GameObject Game и перетащите AudioSource в поле MyAudio.
Музыка настроена на работу во время игры (так как в функции «Awake» есть код), но вам все равно нужно добавить еще один код, если игрок меняет настройки во время игры. Для этого снова откройте скрипт PlayerSettings и добавьте следующую функцию:
public void ToggleMusic() { if (toggle.isOn) { PlayerPrefs.SetInt ("music", 1); myAudio.enabled = true; } else { PlayerPrefs.SetInt ("music", 0); myAudio.enabled = false; } PlayerPrefs.Save (); }
Эти настройки означают почти то же самое, что и код, который вы написали ранее, за исключением того, что в этом случае есть одно важное отличие. Этот код сначала проверяет состояние переключателя музыки, а затем соответствующим образом обновляет сохраненную настройку. Для того, чтобы этот метод был вызван и, следовательно, чтобы он мог выполнять свою функцию, вам нужно установить метод обратного вызова в Toggle GameObject. Выберите MusicObject Music и перетащите GameObject Game поверх поля объекта в разделе OnValueChanged:
Теперь к раскрывающемуся списку, в котором в данный момент написано «No Function», и выберите PlayerSettings ⇒ ToggleMusic (). Таким образом, когда во время игры пользователь активирует кнопку переключения в меню, появится функция ToggleMusic.
Теперь у вас есть все необходимые настройки, которые нужны чтобы отслеживать опции звуков. Нажмите «Play» и попробуйте изменить настройки музыки, включив или выключив соответствующий переключатель в меню.
Сохранение игры
Согласитесь, возможности и настройка PlayerPrefs не вызывает больших затруднений в использовании. С его помощью вы сможете легко сохранять другие данные, такие как графические настройки проигрывателя или информацию для входа в систему (например, токены Facebook или Twitter), и любые другие параметры конфигурации, которые нужно отслеживать для проигрывателя. Однако PlayerPrefs не предназначен для отслеживания сохранений в игре. Для этого нужно использовать сериализацию.
Первым шагом к созданию файла сохранения игры является создание класса файла сохранения. Создайте новый скрипт с именем Save и удалите пункты MonoBehaviour, Start () и Update ().
Теперь вам необходимо добавить следующие переменные:
public List<int> livingTargetPositions = new List<int>(); public List<int> livingTargetsTypes = new List<int>(); public int hits = 0; public int shots = 0;
Чтобы сохранить игру, вам нужно отслеживать, где находятся существующие роботы и какого они типа. За это должны отвечать два списка с целыми данными о количестве попаданий и числе выстрелов.
Есть еще один очень важный фрагмент кода, который вам также нужно добавить:
[System.Serializable]
Этот код дает команду Unity, что данный класс можно сериализовать, а это означает, что вы можете превратить его в поток байтов и сохранить как файл на диске.
Примечание: все эти атрибуты имеют широкий спектр применения и позволяют привязывать данные к классу, методу или определенной переменной. Вы даже можете определить свои собственные атрибуты для использования в вашем коде. Сериализация использует атрибуты [SerializeField] и [System.Serializable], которые определяют, что происходит при сериализации объекта.
Ваш скрипт Save должен выглядеть следующим образом:
using System.Collections; using System.Collections.Generic; using UnityEngine; [System.Serializable] public class Save { public List<int> livingTargetPositions = new List<int>(); public List<int> livingTargetsTypes = new List<int>(); public int hits = 0; public int shots = 0; }
Теперь откройте скрипт Game и добавьте еще один метод:
private Save CreateSaveGameObject() { Save save = new Save(); int i = 0; foreach (GameObject targetGameObject in targets) { Target target = targetGameObject.GetComponent<Target>(); if (target.activeRobot != null) { save.livingTargetPositions.Add(target.position); save.livingTargetsTypes.Add((int)target.activeRobot.GetComponent<Robot>().type); i++; } } save.hits = hits; save.shots = shots; return save; }
Этот код создаст новый экземпляр класса Save, который вы делали ранее, а затем установит значения исходя из количества существующих роботов. Кроме того, произойдет сохранение количества выстрелов игрока и попаданий.
Кнопка «Save» была подключена к методу SaveGame в скрипте Game, но в SaveGame все еще нет кода. Замените функцию SaveGame следующим кодом:
public void SaveGame() { // 1 Save save = CreateSaveGameObject(); // 2 BinaryFormatter bf = new BinaryFormatter(); FileStream file = File.Create(Application.persistentDataPath + "/gamesave.save"); bf.Serialize(file, save); file.Close(); // 3 hits = 0; shots = 0; shotsText.text = "Shots: " + shots; hitsText.text = "Hits: " + hits; ClearRobots(); ClearBullets(); Debug.Log("Game Saved"); }
Рассмотрим все по пунктам:
- Создается экземпляр Save со всеми данными текущего сеанса, сохраненными в нем.
- Создается BinaryFormatter и FileStream, передав путь для сохранения экземпляру Save. Тут же происходит процесс сериализации данных в байты, с последующим их сохранением на диск и закрытием FileStream. Теперь на вашем компьютере будет файл с именем save. Вы можете использовать любое расширение для имени файла сохранения.
- Идет сброс настроек игры в состояние по умолчанию.
Чтобы выполнить сохранение процесса, нажмите Escape в любой момент во время игры и используйте кнопку «Save». Обратите внимание, что при этом появляется сообщение о том, что игра была сохранена.
LoadGame в скрипте Game подключен к кнопке Load. Откройте скрипт Game, найдите функцию LoadGame и замените ее следующим значением:
public void LoadGame() { // 1 if (File.Exists(Application.persistentDataPath + "/gamesave.save")) { ClearBullets(); ClearRobots(); RefreshRobots(); // 2 BinaryFormatter bf = new BinaryFormatter(); FileStream file = File.Open(Application.persistentDataPath + "/gamesave.save", FileMode.Open); Save save = (Save)bf.Deserialize(file); file.Close(); // 3 for (int i = 0; i < save.livingTargetPositions.Count; i++) { int position = save.livingTargetPositions[i]; Target target = targets[position].GetComponent<Target>(); target.ActivateRobot((RobotTypes)save.livingTargetsTypes[i]); target.GetComponent<Target>().ResetDeathTimer(); } // 4 shotsText.text = "Shots: " + save.shots; hitsText.text = "Hits: " + save.hits; shots = save.shots; hits = save.hits; Debug.Log("Game Loaded"); Unpause(); } else { Debug.Log("No game saved!"); } }
Рассмотрим процесс более подробно:
- Проверяет, существует ли файл сохранения. Если это так, то происходит сброс значений выстрелов и попаданий. В противном случае происходит запись в консоль, что сохраненной игры нет.
- Подобно тому, что вы делали при сохранении игры, вы снова создадите BinaryFormatter, только на этот раз нужно предоставить поток байтов для чтения вместо записи. Таким образом, вы просто передаете путь к файлу сохранения, чтобы был создан объект Save, а FileStream закрыт.
- Даже если у вас есть информация о сохранении, вам все равно нужно преобразовать ее в состояние игры. Этот код перебирает сохраненные позиции врагов (для существующих роботов) и добавляет их в эту же позицию. Для простоты таймер сбрасывается, но вы можете исправить это, если хотите. Таким образом роботы не исчезают сразу, а у игрока есть несколько секунд, чтобы сориентироваться в игровом пространстве.
- Эта команда обновляет пользовательский интерфейс, чтобы установить правильное значение попаданий и выстрелов, с учетом значения, которое было заработано игроком ранее.
Нажмите Play, немного поиграйте в игру и попробуйте сохранится. Нажмите кнопку «Load», и вы увидите, что она загружает врагов таким же образом, какими они были до того, как вы сохранили игру. Кроме того, должны отобразиться счет и количество выстрелов, которые вы сделали.
Сохранение данных с помощью JSON
Есть еще одна хитрость, которую вы можете использовать, когда хотите сохранить данные – это возможности JSON. Вы можете создать локальное JSON-представление сохранения вашей игры, отправить его на сервер, а затем переправить JSON в виде строки на другое устройство и преобразовать его из строки обратно в JSON.
Формат JSON может немного отличаться от того, который вы могли бы использовать с кодом C#, но он довольно прост. Вот один из примеров JSON:
{ "message":"hi", "age":22 "items": [ "Broadsword", "Bow" ] }
Внешние скобки представляют собой родительскую сущность, которой является JSON. Если вы знакомы со структурой данных Dictionary, то JSON чем-то похож на это. Файл JSON представляет собой сопоставление пар ключ-значение. Обратите внимание — приведенный выше пример имеет 3 пары ключ-значение. В JSON ключи всегда являются строками, но значения могут быть объектами (то есть дочерними объектами JSON), массивами, числами или строками. Значение, установленное для ключа «message», равно «hi», значение ключа «age» — это число 22, а значение ключа «items» — это некий массив с двумя строками.
Сам объект JSON представлен типом String. Передав эти данные в виде строки, любой язык может легко воссоздать объект JSON из строки в качестве аргумента конструктора. Это действительно очень удобно и очень просто. У каждого языка есть свой способ создания объекта из этого формата. Начиная с Unity 5.3, существует собственный метод для создания объекта JSON из строки JSON.
В скрипте Game есть метод SaveAsJSON, который подключен к кнопке Save As JSON. Вам нужно заменить следующим кодом:
public void SaveAsJSON() { Save save = CreateSaveGameObject(); string json = JsonUtility.ToJson(save); Debug.Log("Saving as JSON: " + json); }
Это создает экземпляр Save и строку JSON с использованием метода ToJSON в классе JsonUtility.
Запустите игру и попробуйте уничтожить несколько целей, чтобы набрать очки. Теперь нажмите Escape, чтобы вызвать меню и используйте кнопку Save As JSON, чтобы увидеть созданную вами строку JSON:
Если вы захотите преобразовать этот JSON в экземпляр Save, то вам нужно будет использовать строку:
Save save = JsonUtility.FromJson<Save>(json);
Таким образом вы сможете загрузить файл сохранения из Интернета, а затем добавить его в свою игру.
Что делать дальше?
Вы можете скачать законченный проект тут.
Вы узнали, как использовать довольно мощный инструмент для создания качественных игр, позволяя игрокам сохранять и загружать свои игры с помощью возможностей сериализации. Вы также узнали, что такое JSON и как его использовать для реализации облачного сохранения.
Если вы хотите узнать больше о возможностях Unity, то вам следует обратиться к другим урокам, посвященным этому движку. Если вы уверенно владеете Unity и хотите стать настоящим разработчиком, то советуем вам прочитать книгу Unity Games by Tutorials, с помощью которой вы сможете сделать 4 полноценных игры с нуля.