Статьи

Сохранение и загрузка данных игры игрока в Unity

В этом уроке мы научимся реализовывать функциональность игры Сохранить / Загрузить в нашей игре. Мы начнем с сохранения необходимых данных, связанных с игроками, таких как уровень, на котором мы находимся, где мы находимся, и пример нашей учебной статистики.

Diskette with gamepad drawn on it

Если вам нужен проект для тестирования, вы можете использовать проект в конце предыдущей статьи, посвященный сохранению данных между сценами, и он идеально подходит для изучения этого урока:

Скачать

Сохранение данных между сценами в Unity — предыдущая статья
[GitHub Repository]
[ZIP Скачать]

Если вы хотите скачать готовый проект, ссылка находится в конце этой статьи.


Разбивка концепции

Для сохранения внутриигровых данных на жестком диске в формате, который можно будет понять и загрузить позже, мы будем использовать функцию .NET / Mono, известную как сериализация. Вы можете узнать больше о теории сериализации по следующим ссылкам:

Короче говоря, «сериализация» означает запись объекта .NET на жесткий диск в его сыром двоичном виде. Это может звучать немного не интуитивно, но подумайте об этом так: мы сохраняем экземпляр класса на жесткий диск.

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

Сохранение данных:

  • Получить класс, содержащий данные игрока
  • Сериализация на жесткий диск в известный файл

Загрузка данных:

  • Найти известный файл сохранения
  • Десериализовать содержимое в общий объект
  • Привести объект в тип нашего класса данных

Что нам нужно сохранить?

  • Все, что уже было в классе PlayerStatistics, которое было сохранено в разных сценах
  • Идентификатор сцены, где была сохранена игра
  • Место в сцене, где был игрок, когда игра была сохранена

Подготовка вещей

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

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

Предлагаемые решения:

Идентификация сцены? Добавление новой переменной Integer в класс пакета данных нашего проигрывателя, чтобы мы знали, на какой сцене был игрок.

Положение сцены? К сожалению, мы не можем добавить объект Transform или Vector3 в класс пакета данных Player, поскольку они не являются сериализуемыми объектами. Вместо этого нам нужно добавить три новых значения с плавающей точкой, обозначающие положение игрока X, Y и Z, и применить их к вектору положения игрока, когда мы загружаем данные.

Процедура сохранения / загрузки Для простоты пока мы назначим две горячие клавиши для сохранения и загрузки: F5 и F9 соответственно.

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

логика

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

Это объясняет поток программы из класса PlayerControl, который является основным классом, с которым мы будем иметь дело сегодня. Это класс, который отвечает за ввод игрока:

Flowchart 1

Обратите внимание на несколько странностей:

  • Глобальный контроль теперь всегда содержит общедоступный бул, в котором указано, загружаем ли мы сцену или начинаем ее заново. Он также несет копию сохраненных данных игрока.
  • Класс контроля игрока при запуске (событие загрузки сцены) всегда проверяет, загружена ли сцена из сохраненной игры или заново. Это говорит нам, нужно ли нам копировать загруженные данные или оставить их в покое.

Код

Давайте рассмотрим новую функциональность шаг за шагом.

Идентификатор сцены и положение

Во-первых, нам нужно решить две основные проблемы, перечисленные выше: идентификатор сцены и положение игрока на сцене.

Предполагая, что у нас есть класс, который содержит данные нашего игрока, например:

public class PlayerStatistics
{
    public float HP;
    public float Ammo;
    public float XP;
}

Нам нужно добавить следующее:

 [Serializable]
public class PlayerStatistics
{
    public int SceneID;
    public float PositionX, PositionY, PositionZ;

    public float HP;
    public float Ammo;
    public float XP;
}

Давайте сначала разберемся с очевидным: перед классом у нас есть собственное объявление «атрибута»: [Serializable] Это говорит Механизму, что данные в этом классе пригодны для записи в двоичной форме или «сериализации».

Мы также добавили наши ID сцены и значения позиции.

Функции сериализации

Теперь давайте напишем функции, которые будут сериализовать и десериализовать данные. Нам нужно перейти в наш GlobalObject (или аналогичный объект, который вы должны иметь):

 //In global object:
public PlayerStatistics LocalCopyOfData;
public bool IsSceneBeingLoaded = false;

    public void SaveData()
    {
        if (!Directory.Exists("Saves"))
            Directory.CreateDirectory("Saves");
            
        BinaryFormatter formatter = new BinaryFormatter();
        FileStream saveFile = File.Create("Saves/save.binary");

        LocalCopyOfData = PlayerState.Instance.localPlayerData;

        formatter.Serialize(saveFile, LocalCopyOfData);

        saveFile.Close();
    }

    public void LoadData()
    {
        BinaryFormatter formatter = new BinaryFormatter();
        FileStream saveFile = File.Open("Saves/save.binary", FileMode.Open);

        LocalCopyOfData = (PlayerStatistics)formatter.Deserialize(saveFile);
        
        saveFile.Close();
    }

Хорошо, это много кода сразу, давайте разберем его. Давайте сначала объясним функцию Save:

Проверка пути

 if (!Directory.Exists("Saves"))
   Directory.CreateDirectory("Saves");

Хотя File.Create Поэтому, если директория Saves

Двоичный форматер

 BinaryFormatter formatter = new BinaryFormatter();

Для этого потребуется добавить новое пространство имен Using выше, а именно:

 using System.Runtime.Serialization.Formatters.Binary;

Совет: напишите «BinaryFormatter» в коде и не нажимая пробел, Enter или Tab (что инициирует завершение Intellisense как в MonoDevelop, так и в Visual Studio), нажмите правую кнопку мыши на объявлении и используйте Resolve -> Add Using namespace .

файл

 FileStream saveFile = File.Create("Saves/save.binary");

Для этого потребуется следующее пространство имен: using System.IO;

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

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

Данные

 LocalCopyOfData = PlayerState.Instance.localPlayerData;

Нам нужно получить ссылку на объект, который мы сериализуем. В примере моего проекта все соответствующие данные об игроке содержатся в экземпляре класса PlayerStatistics, расположенном в единственном экземпляре класса PlayerState.

Магия

 formatter.Serialize(saveFile, LocalCopyOfData);

Вышесказанное представляет всю сложность записи класса в его сыром двоичном виде на жесткий диск. Ах, радости .NET / Mono Framework!

Вы заметите, что функция Serialize требует двух аргументов:

  • Потоковый объект. Наш FileStream является расширением объекта Stream, поэтому мы можем его использовать.
  • объект, который будет сериализован. Как видите, мы можем сериализовать буквально все (при условии, что он содержит атрибут Serializable), потому что все в платформе .NET / Mono расширено от базового класса объектов .

Не забудь это!

 saveFile.Close();

Серьезно, не забывайте это.

Если мы забудем закрыть объект Stream в нашем коде, мы столкнемся с одной из двух проблем (в зависимости от того, что произойдет раньше):

  1. Любая попытка получить доступ к файлу на жестком диске или удалить его (который может или не может быть пустым) приведет к появлению сообщения об ошибке ОС, в котором говорится, что файл используется другой программой.
  2. Попытка десериализации незамкнутого объекта приведет к остановке программы на линии десериализации, без исключений и предупреждений. Это просто перестанет давать признаки жизни.

Обратите внимание, что ни один из симптомов не дает сколько-нибудь значимой информации о незамкнутом потоке.

Хорошо, давайте посмотрим на функцию Load :

Файл (открытие)

 FileStream saveFile = File.Open("Saves/save.binary", FileMode.Open);

Вместо использования Create мы будем использовать функцию Open для получения нашего объекта Stream. Самоочевидно, правда.

Волшебная разница

 LocalCopyOfData = (PlayerStatistics)formatter.Deserialize(saveFile);

Обратите внимание, что мы еще не загружаем загруженные данные в наш экземпляр PlayerState.

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

Функции управления

Наконец, давайте где-нибудь реализуем нашу логику сохранения / загрузки.

Хорошим местом для этого примера будет класс, который обрабатывает ввод игрока. В примере проекта это будет наш класс PlayerControl.

Только для этого примера мы поместим следующий код непосредственно в функцию Update нашего класса PlayerControl, но по мере развития, нам нужно будет переместить это в ту часть кода, где игрок фактически имеет контроль (когда нет меню открыты, ролик не воспроизводится и т. д.):

 ///In Control Update():

        if (Input.GetKey(KeyCode.F5))
        {
            PlayerState.Instance.localPlayerData.SceneID = Application.loadedLevel;
            PlayerState.Instance.localPlayerData.PositionX = transform.position.x;
            PlayerState.Instance.localPlayerData.PositionY = transform.position.y;
            PlayerState.Instance.localPlayerData.PositionZ = transform.position.z;

            GlobalControl.Instance.SaveData();
        }

        if (Input.GetKey(KeyCode.F9))
        {
            GlobalControl.Instance.LoadData();
            GlobalControl.Instance.IsSceneBeingLoaded = true;

            int whichScene = GlobalControl.Instance.LocalCopyOfData.SceneID;

            Application.LoadLevel(whichScene);
        }

Функция Quicksave:

  • Сохраняет текущий идентификатор сцены в данных текущего игрока
  • Сохраняет текущее местоположение игрока в данные текущего игрока
  • Вызывает функцию для сохранения данных игрока в файл сохранения

Теперь функция Quickload немного отличается:

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

Мы устанавливаем публичную логическую переменную, которая сообщает, что сцена загружается, и инициализируем функцию LoadLevel.

Возможно, вы задаетесь вопросом: «Мы даже не копируем позицию игрока или данные PlayerStatistics, так что … зачем мы это делаем?»

Если вы помните потоковую диаграмму, представленную ранее, в функции Start в PlayerControl мы запрашиваем глобальный элемент управления для этой логической переменной, а затем копируем загруженные данные.

Это потому, что мы не можем скопировать данные и затем загрузить сцену. Данные не будут перенесены. Мы также не можем сначала загрузить сцену и скопировать данные в ту же функцию, потому что что-либо после функции LoadLevel () будет проигнорировано, так как объект и сценарий уничтожены, и новый уровень начинается с новых объектов.

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

В нашей функции PlayerControl Start () нам нужно:

 ///In Control Start()
if (GlobalControl.Instance.IsSceneBeingLoaded)
        {
            PlayerState.Instance.localPlayerData = GlobalControl.Instance.LocalCopyOfData;

            transform.position = new Vector3(
                            GlobalControl.Instance.LocalCopyOfData.PositionX,
                            GlobalControl.Instance.LocalCopyOfData.PositionY,
                            GlobalControl.Instance.LocalCopyOfData.PositionZ + 0.1f);

            GlobalControl.Instance.IsSceneBeingLoaded = false;
        }

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

Выполнено!

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

Когда вы снова включаете игру (или просто нетерпеливы, вы можете сразу же) нажать F9, и продолжите с того места, где остановились!

Если вы хотите стать модным, попробуйте написать дополнительные проверки избыточности, чтобы увидеть, существует ли файл сохранения или даже увеличивать файлы сохранения (так что вы всегда можете загрузить более раннее сохранение).

Скачать проект

Если вы хотите посмотреть, как законченная демонстрация работает или как она выглядит (или если вы застряли где-то и вам нужна помощь), проект можно загрузить из следующих мест:

Вывод

Мы сохранили и загрузили информацию об игроке и соответствующую информацию, но как насчет остального игрового мира? Что если у нас есть забрать в мире, который мы хотим сохранить Или враги мы хотим остаться мертвыми?

Это будет рассмотрено в продолжение этого урока, в ближайшее время А пока, если вы хотите подготовиться и вооружиться до зубов с помощью кода, найдите делегаты и события .NET / Mono.

Вопросов? Комментарии? Пожалуйста, оставьте их ниже, и не забудьте нажать на эту кнопку, если вам понравилась эта статья!