Как сохранять и загружать данные игры в Unity

Узнайте, как сохранять и загружать игру в Unity с помощью PlayerPrefs, Serialization и JSON. Дополните свой пользовательский опыт знаниями о том, как сохранять и загружать данные.

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

Но как создать файл сохранения и что в нем должно быть? Вам также нужно использовать файл сохранения, чтобы отслеживать настройки игрока? Как насчет отправки сохранений в интернет, чтобы их можно было позже загрузить на другое устройство?

В этом уроке вы узнаете:

  • Что такое сериализация и десериализация.
  • Что такое PlayerPrefs и как его использовать для сохранения настроек игрока.
  • Как создать файл сохранения игры и сохранить его на диск.
  • Как загрузить файл сохранения игры.
  • Что такое JSON и как его использовать.

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

Примечание. Если вы новичок в Unity или хотите получить больше навыков в Unity, вам следует ознакомиться с другими уроками по Unity, где вы можете узнать о множестве тем Unity от C# до того, как работает пользовательский интерфейс.

Приступая к работе

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

Важные концепции сохранения

Есть четыре ключевых концепций сохранения в Unity:

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

Сериализация: это волшебство, благодаря которому Unity работает. Сериализация — это преобразование объекта в поток байтов. Определение может показаться расплывчатым, но взгляните на этот рисунок:

Схема использования сериализации в Unity

Что такое «объект»? В этом случае «объектом» является любой скрипт или файл в Unity. Фактически, всякий раз, когда вы создаете скрипт MonoBehaviour, Unity использует сериализацию и десериализацию для преобразования этого файла в код C++, а затем обратно в код C#, который вы видите в окне инспектора. Если вы когда-либо добавляли [SerializeField], чтобы что-то отображалось в инспекторе, теперь вы имеете представление о том, что происходит.

Примечание. Если вы Java-разработчик или веб-разработчик, возможно, вы знакомы с концепцией, известной как маршаллинг. Сериализация и маршаллинг в общих чертах синонимичны, но если вам интересно, какова будет строгая разница, сериализация — это преобразование объекта из одной формы в другую (например, объекта в байты), тогда как маршаллинг — это получение параметров из одного места в другое.

Десериализация: это именно то, на что похоже. Это противоположность сериализации, а именно преобразование потока байтов в объект.

JSON: это обозначает нотацию объектов JavaScript, которая является удобным форматом для отправки и получения данных, не зависящих от языка. Например, у вас может быть веб-сервер, работающий на Java или PHP. Вы не можете просто отправить объект C#, но вы можете отправить JSON-представление этого объекта и позволить серверу воссоздать его локализованную версию. Вы узнаете больше об этом формате в последнем разделе урока, а пока что просто знайте, что это просто способ форматирования данных, чтобы сделать их удобочитаемыми для разных платформ (например, XML). При преобразовании в JSON и обратно используются термины сериализация JSON и десериализация JSON соответственно.

Player Prefs

Этот проект настроен так, что все, на чем вы будете сосредоточены, — это на логике сохранения и загрузки игр. Однако, если вам интересно, как все это работает, не бойтесь открыть все скрипты и посмотреть, что происходит, и не стесняйтесь задавать вопросы, если вам нужна помощь.

Откройте проект, затем откройте сцену с именем Game и нажмите на кнопку воспроизведения.

Чтобы начать игру, нажмите на кнопку New Game. Чтобы воспроизвести игру, вы просто перемещаете мышь, и пистолет будет следовать за вашим движением. Нажмите левой кнопкой мыши, чтобы выстрелить и поразить цели (которые переворачиваются вверх и вниз через различные промежутки времени), чтобы получить игровой счет. Попробуйте и посмотрите, какой счет вы сможете набрать за тридцать секунд. Чтобы вызвать меню в любое время, нажмите кнопку esc на клавиатуре.

Какой бы забавной ни была эта игра, без музыки она могла бы быть немного суховата. Вы могли заметить, что есть переключатель музыки, но он был выключен. Нажмите на play, чтобы начать новую игру, но на этот раз установите переключатель Music на включенное положение, и при запуске игры вы услышите музыку. Убедитесь, что колонки компьютера включены!

Изменить настройки музыки было просто, но нажмите на кнопку воспроизведения еще раз, и вы заметите проблему: музыка больше не проверяется. Несмотря на то, что вы меняли настройки музыки ранее, это изменение не отслеживалось. Это то, в чем PlayerPrefs преуспевает.

Создайте новый скрипт с именем 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;
      }
    }
  }

После настройки скрипт будет:

  1. Проверять, есть ли в PlayerPrefs кэшированная настройка для ключа «music». Если там нет значения, он создает пару ключ-значение для музыкального ключа со значением 1. Он также включает переключатель и включает AudioSource. Это будет запущено при первом запуске игры. Значение 1 используется, потому что вы не можете сохранить логическое значение (но вы можете использовать 0 как false и 1 как true).
  2. Проверять ключ «music», сохраненный в PlayerPrefs. Если значение установлено на 1, в проигрывателе была музыка, поэтому он включает музыку и устанавливает переключатель в положение «включено». В противном случае он отключает музыку и переключатель.

Теперь сохраните изменения в скрипте и вернитесь в Unity.

Добавьте скрипт PlayerSettings в игровой объект Game. Затем разверните игровой объект UI, а затем игровой объект Menu, чтобы открыть его дочерние элементы. Затем перетащите игровой объект Music в поле Toggle скрипта PlayerSettings. Затем выберите игровой объект Game и перетащите AudioSource в поле MyAudio.

Настройка параметров скрипта PlayerSettings в окне Inspector редактора Unity

Музыка настроена на работу при запуске игры (поскольку в функции 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. Выберите игровой объект Music и перетащите игровой объект Game на поле объекта в разделе OnValueChanged:

Установка функции-обработчика для кнопки переключения в окне Inspector редактора Unity

Выберите раскрывающийся список, в котором сейчас указано No Function, и выберите PlayerSettings -> ToggleMusic (). Когда кнопка переключения в меню нажата, она вызывает функцию ToggleMusic.

Выбор метода для обработки события нажатия кнопки в окне Inspector редактора Unity

Теперь у вас есть все необходимое, чтобы отслеживать настройку музыки. Нажмите 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");
}

Рассматриваем комментарий за комментарием:

  1. Создать экземпляр Save со всеми данными текущего сеанса, сохраненными в нем.
  2. Создать BinaryFormatter и FileStream, передав путь для сохраняемого экземпляра Save. Он сериализует данные (в байты), записывает их на диск и закрывает FileStream. Теперь на вашем компьютере будет файл с именем gamesave.save. Файл .save использовался только в качестве примера, и вы могли использовать любое расширение для имени сохранения файла.
  3. Это просто сбрасывает игру, так что после сохранения игроком все находится в состоянии по умолчанию.

Чтобы сохранить игру, нажмите Escape в любой момент во время игры и нажмите кнопку Save. Вы должны заметить, что все сбрасывается, и в выводе консоли отображается примечание о том, что игра была сохранена.

Вывод сообщений в консоль редактора Unity

LoadGame в скрипте Game связана с кнопкой Load. Откройте скрипт игры и найдите функцию 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!");
  }
}

Рассмотрим код детально:

  1. Проверить, существует ли файл сохранения. Если это так, он очищает роботов и счет. В противном случае на консоль выводится сообщение об отсутствии сохраненной игры.
  2. Подобно тому, что вы делали при сохранении игры, вы снова создаете BinaryFormatter, только на этот раз вы предоставляете ему поток байтов для чтения вместо записи. Таким образом, вы просто передаете ему путь к файлу сохранения. Он создает объект Save и закрывает FileStream.
  3. Несмотря на то, что у вас есть информация о сохранении, вам все равно нужно преобразовать ее в состояние игры. Этот код просматривает сохраненные позиции роботов (для живых роботов) и добавляет робота в эту позицию. Он также устанавливает для него правильный тип. Для простоты таймеры сброшены, но вы можете удалить это, если хотите. Это предотвращает немедленное исчезновение роботов и дает игроку несколько секунд, чтобы сориентироваться в мире. Кроме того, для простоты анимация движения робота вверх настроена на завершение, поэтому роботы, частично движущиеся вверх при сохранении, будут отображаться полностью вверх при загрузке игры.
  4. Это обновляет пользовательский интерфейс, чтобы установить правильные попадания и выстрелы, и устанавливает локальные переменные, чтобы, когда игрок стреляет или попадает в цель, он продолжает отсчитывать значение, которое было ранее. Если вы не сделали этого шага, в следующий раз, когда игрок выстрелит или поразит цель, отображаемые значения будут установлены на 1.

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

Сохранение данных с помощью JSON

Есть еще один прием, который вы можете использовать, когда хотите сохранить данные, - это 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. Вы создадите JSON-представление рекорда игрока, а затем распечатаете его на консоли. Но вы расширяете эту логику, отправляя JSON на сервер.

В скрипте Game есть метод с именем SaveAsJSON, который подключен к кнопке SaveAsJSON. Замените SaveAsJSON следующим кодом:

public void SaveAsJSON()
{
  Save save = CreateSaveGameObject();
  string json = JsonUtility.ToJson(save);

  Debug.Log("Saving as JSON: " + json);
}

Это создает экземпляр Save, как и раньше. Затем он создает строку JSON, используя метод ToJSON в классе JsonUtility. Затем он выводит результат на консоль.

Начните игру, поразите несколько целей, затем нажмите Escape, чтобы открыть меню. Нажмите кнопку SaveAsJSON, и вы увидите созданную вами строку JSON:

Вывод данных сохранения в консоль редактора Unity

Если вы хотите преобразовать этот JSON в экземпляр Save, вы должны просто использовать:

Save save = JsonUtility.FromJson(json);

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

Куда двигаться дальше?

Вы можете скачать итоговые файлы проекта вверху страницы.

Теперь вы получили мощный инструмент для создания отличных игр, позволяющий игрокам сохранять и загружать свои игры с помощью магии сериализации. Вы также узнали, что такое JSON и как его можно использовать для сохранения в облаке. Вы также узнали, для чего используется PlayerPrefs (настройки!) И для чего он не используется (сохранение игры).

Вы всегда можете оставить здесь комментарий, если у вас есть какие-либо вопросы по этому руководству, присоединяйтесь к обсуждению ниже!

Автор перевода: Jean Winters

Источник: How to Save and Load a Game in Unity

Комментировать

Почта не публикуется.Обязательные поля отмечены *