17 апреля 2020

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

Frontend-разработчик в Foquz. https://www.cat-in-web.ru/
Рассказываем с примерами кода на C# про два способа сохранения игровых данных в Unity: метод сериализации и PlayerPrefs для пользовательских предпочтений.
Сохранение игровых данных в Unity

Одна из самых важных вещей в игре – сохранение пользовательских данных: настроек и игровых результатов. Когда-то игры были короткими, и сохранять там было особенно нечего. В лучшем случае игра записывала самый высокий балл для составления рейтинга. Но технологии стремительно развивались, и геймдев не оставался в стороне.

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

Подготовка

Unity предлагает сразу два способа сохранять игровые данные – попроще и посложнее:

  • Попроще – встроенная система PlayerPrefs. Устанавливаете значение для ключа, нажимаете Save – и все готово.
  • Посложнее – сериализация данных и запись в файл для дальнейшего использования.

У обоих методов есть преимущества и недостатки, поэтому для конкретного случая важно выбрать правильный вариант. Для демонстрации нам потребуется некоторая минимальная конфигурация. Создадим новый проект в Unity, за основу для простоты возьмем 2D-шаблон.

Создание нового проекта в Unity
Создание нового проекта в Unity

Добавим два скрипта – SavePrefs и SaveSerial – для реализации двух методов.

Чтобы создать скрипт, кликните правой кнопкой мыши в окне Assets и выберите пункты Create -> C# Script.

Создание скрипта на C# в Unity
Создание скрипта на C# в Unity

Начнем с более простого способа – SavePrefs.

Кликните два раза по скрипту, чтобы открыть его в редакторе Visual Studio.

Простой способ: PlayerPrefs

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

        int intToSave;
float floatToSave;
string stringToSave = "";
    

С помощью метода OnGui создадим пользовательский интерфейсдля визуального управления этими переменными.

  • Две кнопки – для увеличения значений intToSave и floatToSave.
  • Текстовое поле – для переменной stringToSave.
  • Несколько лейблов для отображения текущих значений переменных.
  • Три кнопки действий, чтобы сохранить, загрузить и сбросить данные.
        void OnGUI()
{
  if (GUI.Button(new Rect(0, 0, 125, 50), "Raise Integer"))
    intToSave++;
  if (GUI.Button(new Rect(0, 100, 125, 50), "Raise Float"))
    floatToSave += 0.1f;

  stringToSave = GUI.TextField(new Rect(0, 200, 125, 25), stringToSave, 15);

  GUI.Label(new Rect(375, 0, 125, 50), "Integer value is " 
    + intToSave);
  GUI.Label(new Rect(375, 100, 125, 50), "Float value is "
    + floatToSave.ToString("F1"));
  GUI.Label(new Rect(375, 200, 125, 50), "String value is " 
    + stringToSave);

  if (GUI.Button(new Rect(750, 0, 125, 50), "Save Your Game"))
    SaveGame();
  if (GUI.Button(new Rect(750, 100, 125, 50), "Load Your Game"))
    LoadGame();
  if (GUI.Button(new Rect(750, 200, 125, 50), "Reset Save Data"))
    ResetData();
}
    

Сохранение

Создадим метод SaveGame, который будет отвечать за сохранение данных:

        void SaveGame()
{
  PlayerPrefs.SetInt("SavedInteger", intToSave);
  PlayerPrefs.SetFloat("SavedFloat", floatToSave);
  PlayerPrefs.SetString("SavedString", stringToSave);
  PlayerPrefs.Save();
  Debug.Log("Game data saved!");
}
    

Как видим, для сохранения данных с PlayerPrefs нужно лишь несколько строчек кода. Здесь мы устанавливаем ключи настройки ("SavedInteger" или "SavedFloat") и их значения передаем в соответствующие методы объекта PlayerPrefs. После того, как все нужные данные записаны, сохраняем их, вызвав метод PlayerPrefs.Save. Выводим сообщение в отладочную консоль, о том, что операция успешно выполнена.

Должно быть, вам интересно, где сейчас физически находятся эти данные. Они записываются в файл в папке проекта. В Windows его можно найти по адресу HKEY_CURRENT_USER\Software\Unity\UnityEditor\[company name]\[project name]. Именно отсюда запускается игра из редактора. В exe-файле их можно найти по адресу HKEY_CURRENT_USER\Software\[company name]\[project name]. На Mac OS согласно документации файлы PlayerPrefs находятся в папке ~/Library/Preferences, в файле с названием unity.[company name].[product name].plist.

 Переменные PlayerPrefs в файловой системе Windows
Переменные PlayerPrefs в файловой системе Windows

Загрузка

Загрузка сохраненных данных – это, по сути, сохранение наоборот. Необходимо взять значения, хранящиеся в PlayerPrefs и записать их в переменные.

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

Если данных нет, выведем в консоль сообщение об ошибке.

        void LoadGame()
{
  if (PlayerPrefs.HasKey("SavedInteger"))
  {
    intToSave = PlayerPrefs.GetInt("SavedInteger");
    floatToSave = PlayerPrefs.GetFloat("SavedFloat");
    stringToSave = PlayerPrefs.GetString("SavedString");
    Debug.Log("Game data loaded!");
  }
  else
    Debug.LogError("There is no save data!");
}
    

Сброс

Для удаления всех данных, хранящихся в PlayerPrefs, нужно использовать метод PlayerPrefs.DeleteAll.

        void ResetData()
{
  PlayerPrefs.DeleteAll();
  intToSave = 0;
  floatToSave = 0.0f;
  stringToSave = "";
  Debug.Log("Data reset complete");
}
    

В методе ResetData мы очищаем хранилище, а также обнуляем все переменные.

Теперь проверим весь этот код в деле. Сохраните файл и вернитесь в редактор Unity. Прикрепите скрипт SavePrefs к какому-нибудь объекту, например, к Main Camera.

 Прикрепление скрипта SavePrefs
Прикрепление скрипта SavePrefs
***

Теперь запустите игру и начните взаимодействовать с GUI-элементами. Изменяйте переменные, нажимая на кнопки и заполняя текстовое поле. Когда будете готовы, сохраните данные кнопкой Save Your Game. После этого остановите и перезапустите игру и нажмите на кнопку Load Your Game. Если вы всё сделали правильно, значения переменных немедленно изменятся на те, что вы сохранили в предыдущем запуске.

Чтобы очистить PlayerPrefs, кликните Reset Save Data.

Использование PlayerPrefs для сохранения данных. Скриншот работающего проекта
Использование PlayerPrefs для сохранения данных. Скриншот работающего проекта

Недостатки

Этот способ кажется простым и эффективным. Почему бы всегда не использовать PlayerPrefs для сохранения пользовательских данных?

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

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

Другая проблема – в недостаточной гибкости. В PlayerPrefs вы можете сохранять только числа и строки, поэтому он не подходит для данных, имеющих сложную структуру.

К счастью, у нас есть еще один способ, более гибкий и безопасный.

Сложный способ: Сериализация

Для демонстрации сложного способа сохранения данных в Unity откроем скрипт SaveSerial.

Снова определим переменные и создадим интерфейс для управления ими. Метод OnGUI похож на тот, что мы только что писали:

        int intToSave;
float floatToSave;
bool boolToSave;

void OnGUI()
{
  if (GUI.Button(new Rect(0, 0, 125, 50), "Raise Integer"))
    intToSave++;
  if (GUI.Button(new Rect(0, 100, 125, 50), "Raise Float"))
    floatToSave += 0.1f;
  if (GUI.Button(new Rect(0, 200, 125, 50), "Change Bool"))
    boolToSave = boolToSave ? boolToSave = false : boolToSave = true;

  GUI.Label(new Rect(375, 0, 125, 50), "Integer value is " 
    + intToSave);
  GUI.Label(new Rect(375, 100, 125, 50), "Float value is " 
    + floatToSave.ToString("F1"));
  GUI.Label(new Rect(375, 200, 125, 50), "Bool value is " 
    + boolToSave);

  if (GUI.Button(new Rect(750, 0, 125, 50), "Save Your Game"))
    SaveGame();
  if (GUI.Button(new Rect(750, 100, 125, 50), "Load Your Game"))
    LoadGame();
  if (GUI.Button(new Rect(750, 200, 125, 50), "Reset Save Data"))
    ResetData();
}
    

Для сериализации данных потребуется добавить несколько директив using:

        using System;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
    

Сохранение

Создадим новый сериализуемый класс SaveData, который будет содержать сохраняемые данные.

        [Serializable]
class SaveData
{
  public int savedInt;
  public float savedFloat;
  public bool savedBool;
}
    
Скрипт SaveSerial
Скрипт SaveSerial

Обратите внимание, три переменные в классе SaveData соответствуют переменным из класса SaveSerial. Для сохранения мы будем передавать значения из SaveSerial в SaveData, а затем сериализовать последний.

Добавим в класс SaveSerial метод SaveGame:

        void SaveGame()
{
  BinaryFormatter bf = new BinaryFormatter(); 
  FileStream file = File.Create(Application.persistentDataPath 
    + "/MySaveData.dat"); 
  SaveData data = new SaveData();
  data.savedInt = intToSave;
  data.savedFloat = floatToSave;
  data.savedBool = boolToSave;
  bf.Serialize(file, data);
  file.Close();
  Debug.Log("Game data saved!");
}
    

Объект BinaryFormatter предназначен для сериализации и десериализации. При сериализации он отвечает за преобразование информации в поток бинарных данных (нулей и единиц).

FileStream и File нужны для создания файла с расширением .dat. Константа Application.persistentDataPath содержит путь к файлам проекта: C:\Users\[user]\AppData\LocalLow\[company name].

В методе SaveGame создается новый экземпляр класса SaveData. В него записываются текущие данные из SaveSerial, которые нужно сохранить. BinaryFormatter сериализует эти данные и записывает их в файл, созданный FileStream. Затем файл закрывается, в консоль выводится сообщение об успешном сохранении.

Загрузка

Метод LoadGame – это, как и раньше, SaveGame наоборот:

        void LoadGame()
{
  if (File.Exists(Application.persistentDataPath 
    + "/MySaveData.dat"))
  {
    BinaryFormatter bf = new BinaryFormatter();
    FileStream file = 
      File.Open(Application.persistentDataPath 
      + "/MySaveData.dat", FileMode.Open);
    SaveData data = (SaveData)bf.Deserialize(file);
    file.Close();
    intToSave = data.savedInt;
    floatToSave = data.savedFloat;
    boolToSave = data.savedBool;
    Debug.Log("Game data loaded!");
  }
  else
    Debug.LogError("There is no save data!");
}
    
  • Сначала ищем файл с сохраненными данными, который мы создали в методе SaveGame.
  • Если он существует, открываем его и десериализуем с помощью BinaryFormatter.
  • Передаем записанные в нем значения в переменные класса SaveSerial.
  • Выводим в отладочную консоль сообщение об успешной загрузке.

Если файла с данными не окажется в папке проекта, выведем в консоль сообщение об ошибке.

Сброс

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

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

Если файла нет, выводим сообщение об ошибке.

        void ResetData()
{
  if (File.Exists(Application.persistentDataPath 
    + "/MySaveData.dat"))
  {
    File.Delete(Application.persistentDataPath 
      + "/MySaveData.dat");
    intToSave = 0;
    floatToSave = 0.0f;
    boolToSave = false;
    Debug.Log("Data reset complete!");
  }
  else
    Debug.LogError("No save data to delete.");
}
    
***

Скрипт метода сериализации готов, теперь его можно проверить в деле. Сохраните код, вернитесь в Unity и запустите игру. Привяжите скрипт SaveSerial к объекту Main Camera (не забудьте деактивировать предыдущий).

Деактивация скрипта Save Prefs
Деактивация скрипта Save Prefs

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

В этот раз файл будет сохранен по "постоянному пути данных" игры. В Windows это C:\Users\username\AppData\LocalLow\project name, в Mac – ~/Library/Application Support/companyname/productname согласно документации.

Перезапустите игру и загрузите данные, нажав на кнопку Load Your Game. Значения переменных должны измениться на те, что вы сохранили ранее.

Также вы можете удалить все сохраненные данные кнопкой Reset Save Data.

 Использование сериализации для сохранения данных. Скриншот работающего проекта
Использование сериализации для сохранения данных. Скриншот работающего проекта

Заключение

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

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

Сериализация и запись в файл – более сложный путь, но он дает вам бóльшую гибкость и безопасность.

Какие именно данные сохранять и каким способом – зависит от особенностей проекта.

Больше полезной информации вы найдете на наших телеграм-каналах «Библиотека программиста» и «Книги для программистов».

Источники

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ