Сохранения unity как изменить

Рассказываем с примерами кода на C# про два способа сохранения игровых данных в Unity: метод сериализации и PlayerPrefs для пользовательских предпочтений.

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

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

Подготовка

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_USERSoftwareUnityUnityEditor[company name][project name]. Именно отсюда запускается игра из редактора. В exe-файле их можно найти по адресу HKEY_CURRENT_USERSoftware[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]AppDataLocalLow[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:UsersusernameAppDataLocalLowproject name, в Mac – ~/Library/Application Support/companyname/productname согласно документации.

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

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

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

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

Заключение

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

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

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

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

Unity categorizes most saved information into either scene changes or project-wide changes.

  • To save all current scene and project-wide changes, go to File > Save (or Save as).
  • To save Project-wide changes, but not Scene changes, go to File > Save Project.

Note: If you edit in Prefab Mode, saving through File > Save only saves changes to the open Prefab. Exit the Prefab Mode to save wider changes.

Unity saves some information automatically while you work in the Editor. See Automatic saves for more details.

Scene changes

Scene changes include modifications to GameObjectsThe fundamental object in Unity scenes, which can represent characters, props, scenery, cameras, waypoints, and more. A GameObject’s functionality is defined by the Components attached to it. More info
See in Glossary
in the SceneA Scene contains the environments and menus of your game. Think of each unique Scene file as a unique level. In each Scene, you place your environments, obstacles, and decorations, essentially designing and building your game in pieces. More info
See in Glossary
. For example:

  • If you add, move, or delete a GameObject.
  • If you change a GameObject’s parameters in the InspectorA Unity window that displays information about the currently selected GameObject, asset or project settings, allowing you to inspect and edit the values. More info
    See in Glossary
    window.

Project-wide changes

Project-wide changes in Unity apply to your entire project rather than a specific scene. For example, if you create a temporary scene to test changes, you can save the project and not the scene.

Project-wide changes include:

  • Project SettingsA broad collection of settings which allow you to configure how Physics, Audio, Networking, Graphics, Input and many other areas of your project behave. More info
    See in Glossary
    : When you save your project, Unity saves changes to the Project Settings in the Library folder, in these files:

    • Input: InputManager.asset
    • Tags And Layers: TagManager.asset
    • Audio: AudioManager.asset
    • Time: TimeManager.asset
    • Player: ProjectSettings.asset
    • Physics: DynamicsManager.asset
    • Physics 2D: Physics2DSettings.asset
    • Quality: QualitySettings.asset
    • Graphics: GraphicsSettings.asset
    • Network: NetworkManager.asset
    • Editor: EditorUserSettings.asset
  • Build Settings: Unity saves changes to the Build Settings in the Library folder as EditorBuildSettings.asset.

  • Changed assets: When you save project-wide settings, Unity saves any unsaved assets.

    Note: Some asset types have an Apply button in the Inspector. Unity will not save these unless you select Apply.

  • Dirty assets: Unity saves Dirty assets, which are files on your disk that are modified in the software but not saved yet. You can use custom Editors and scriptsA piece of code that allows you to create your own Components, trigger game events, modify Component properties over time and respond to user input in any way you like. More info
    See in Glossary
    to mark an Asset as dirty in one of these ways:

    • Use the SerializedObject class with SerializedProperties.
    • Use the Undo class to record modifications.
    • Use SetDirty.

Automatic saves

Unity automatically saves the following changes to your disk:

  • New assets: Unity automatically saves new assets when you create them but you need to save later changes.
  • Asset Import Settings: For the changes to take effect with most assets, you need to select Apply in the Inspector window. Unity saves the changes when you select Apply.
  • Baked data: When you have data that is set to Baked in your project, Unity saves this data after the bake finishes. This includes:

    • Baked Lighting data
    • Baked navigation data
    • Baked occlusion cullingA that disables rendering of objects when they are not currently seen by the camera because they are obscured (occluded) by other objects. More info
      See in Glossary
      data
  • Script execution order changes: After you select Apply, Unity saves this data into each script’s .meta file.

Иногда сохранения просто не подразумевает жанр... Если вы пишете не казуалку под веб и не беспощадный суровый рогалик, без сохранения данных на диск не обойтись.
Как это делается в Unity? Вариантов тут достаточно — есть класс PlayerPrefs в библиотеке, можно сериализовать объекты в XML или бинарники, сохранить в *SQL*, можно, в конце-концов, разработать собственный парсер и формат сохранения.
Рассмотрим поподробнее с первые два варианта, и заодно попробуем сделать меню загрузки-сохранения со скриншотами.

Будем считать, что читающий дальше базовыми навыками обращения с этим движком владеет. Но при этом можно не подозревать о сущестовании в его библиотеке PlayerPrefs, GUI, и ещё в принципе не знать о сериализации. С этим всем и разберёмся.
А чтобы эта заметка не стала слишком уж увлекательной и полезной, ориентирована она самый неактуальный в мобильно/планшетно/онлайновый век вариант — сборку под винду (хотя, конечно, более общих моментов достаточно).

  • Кстати, пару недель назад на Хабре была статья, где автор упомянул, что Unity3D проходят в курсе компьютерной графики на кафедре информатики питерского матмеха. Занятный факт, немало говорящий о популярности движка.
    Хотя насколько это в целом хорошая идея — на мой взгляд, тема для дискуссии. Может быть, обсудить это было бы даже интереснее вопросов сериализации =)

Ещё небольшой дисклеймер — я не профи ни в одной из раскрываемых тем, так что если какие-то вещи можно сделать правильнее-проще-удобнее — исправления и дополнения очень приветствуются.

1. PlayerPrefs

Удобный встроенный класс. Работает с int, float и string. Довольно прозрачный, но мне всё равно встречались на форумах обороты в духе «не могу понять PlayerPrefs» или «надо бы как-нибудь разобраться с PlayerPrefs», так что посмотрим на него на простом примере.

1.1 Примитивное использование в рамках одной сцены: QuickSave & QuickLoad по хоткеям.

Быстрый пример использования. Допустим, у нас одна сцена и персонаж на ней. Скрипт SaveLoad.cs прикреплен к персонажу. Будем сохранять самое простейшее — его положение.

using UnityEngine;
using System.Collections;

public class SaveLoad : MonoBehaviour {

	public Transform CurrentPlayerPosition;
	
	void Update () {
		
		if(Input.GetKeyDown(KeyCode.R))
			savePosition();
		
		if(Input.GetKeyDown(KeyCode.L))
			if (PlayerPrefs.HasKey("PosX"))  // проверяем, есть ли в сохранении подобная информация
				loadPosition();

		if(Input.GetKeyDown(KeyCode.D))
			PlayerPrefs.DeleteAll();	// очистка всей информации для этого приложения
	}

	public void savePosition(){
		
		Transform CurrentPlayerPosition = this.gameObject.transform;
		
		PlayerPrefs.SetFloat("PosX", CurrentPlayerPosition.position.x); // т.к. автоматической работы 
		PlayerPrefs.SetFloat("PosY", CurrentPlayerPosition.position.y); // с массивами нет, разбиваем на
		PlayerPrefs.SetFloat("PosZ", CurrentPlayerPosition.position.z);  // отдельные float и записываем
		
		PlayerPrefs.SetFloat("AngX", CurrentPlayerPosition.eulerAngles.x); 
		PlayerPrefs.SetFloat("AngY", CurrentPlayerPosition.eulerAngles.y);
		
		PlayerPrefs.SetString("level", Application.loadedLevelName); // ещё можно писать/читать строки
		PlayerPrefs.SetInt("level_id", Application.loadedLevel); // и целые
	}
	
	public void loadPosition(){
	
		Transform CurrentPlayerPosition = this.gameObject.transform;
	
		Vector3 PlayerPosition = new Vector3(PlayerPrefs.GetFloat("PosX"), 
					PlayerPrefs.GetFloat("PosY"), PlayerPrefs.GetFloat("PosZ"));
		Vector3 PlayerDirection = new Vector3(PlayerPrefs.GetFloat("AngX"), // генерируем новые вектора 
					PlayerPrefs.GetFloat("AngY"), 0);  // на основе загруженных данных
	
		CurrentPlayerPosition.position = PlayerPosition; // и применяем их
		CurrentPlayerPosition.eulerAngles = PlayerDirection;
	}
}

Конечно, тут применение PlayerPrefs довольно надумано — фактически против обычных переменных оно добавляет нам только возможность загрузки игру с места сохранения после выхода.

Зато весь основной интерфейс класса виден: для каждого из трех типов Get / Set по ключу, проверка вхождения по ключу, очистка. Нет смысла даже разбирать ScriptReference, всё очевидно по названиям функций: PlayerPrefs

Однако на одной всё же стоит остановиться подробнее, PlayerPrefs.Save. В описании говорится, что вообще дефолтно юнити пишет PlayerPrefs на диск только при закрытии приложения — в общем-то логично, учитывая, что класс ориентирован не на внутренний обмен данными, и на их сохранение между сеансами. Соответственно, Save() предполагается использовать только для периодических сохранений на случай крэша.

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

Ко всем ключам в конце добавлен их DJBX33X-хеш (Bernshtein hash with XOR).

UnityGraphicsQuality сохраняется всегда автоматически, и действительно при закрытии приложения. Это Quality level из Edit -> Project Settings Quality, оно же QualitySettings.SetQualityLevel.

Можно при запущенном приложении модифицировать сохранённое значение в реестре, потом затребовать его из программы — и мы увидим, что вернулся модифицированный вариант. Т.е. не стоит думать что во время работы программы PlayerPrefs — что-то вроде аналога глобальных переменных, а работа с диском не происходит.

2. Сериализация в XML

Говорим сериализация, подразумеваем бинарный код. Такое встречается, но на самом деле сериализовать можно в любой формат. По сути это перевод структуры данных или состояния объекта в хранимый/передаваемый формат. А десериализация, соответственно — восстановление объекта по сохраненным/полученным данным.

Вообще Mono умеет и бинарную сериализацию, и XML (System.Xml.Serialization), но есть один момент: большинство классов Unity не сериализуются напрямую. Невозможно просто взять и сериализовать GameObject, или класс, наследующий MonoBehavoir: придётся завести дополнительно внутренний сериализуемый класс, содержащий нужные данные, и работаеть, используя его. Но XmlSerializer хотя бы кушает автоматически Vector3, а BinarySerializer, afaik, даже этого не умеет.

2.1 Суть примера

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

2.2 Сериализуемые классы для данных

XmlSerializer умеет работать с классами, данные в которых состоят из других сериализуемых классов, простых типов, большинства элементов Collections[.Generic]. Обязательно наличие у класса пустого конструктора и public-доступ ко всем сериализуемым полям.
Некторые типы из библиотеки Юнити (вроде Vector3, содержащего всего три интовых поля) успешно проходят этот фейсконтроль, но большинство, особенно более сложных, его фейлят.

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

Создадим новый скрипт в Standard Assets:

using UnityEngine;
using System.Collections.Generic;
using System.Xml.Serialization; 
using System;


[XmlRoot("RoomState")]
[XmlInclude(typeof(PositData))] 
public class RoomState {		//  класс, содержащий состояние комнаты в целом

	[XmlArray("Furniture")]
	[XmlArrayItem("FurnitureObject")]
	public List<PositData> furniture = new List<PositData>(); // список из перемещаемых предметов

	public RoomState() { }   // пустой конструктор

	public void AddItem(PositData item) {	// добавление элементов - будем этим пользоваться
		furniture.Add(item);			// при генерации дефолтной версии локации
	}
	
	public void Update(){    // функция, по которой данные этого класса-дубликата объектов 
		foreach (PositData felt in furniture) // будут обновляться
			felt.Update();		
	}
}

В квадратных скобках идут атрибуты для управления XML-сериализацией. Тут они фактически влияют только на имена тегов в генерируемом *.xml, и строго говоря, необходимости в них нет. Но пусть будут, для наглядности :) Если вам почему-то вдруг важно, как будет выглядеть xml-код, то возможности атрибутов, конечно шире.

Дальше там же добавим базовый класс для предметов из списка и сколько угодно наcледуемых от него. Хотя… для примера хватит и одного:

[XmlType("PositionData")]
[XmlInclude(typeof(Lamp))] 
public class PositData
{
	protected GameObject _inst;	// тут храним ссылку на отражаемый объект
	public GameObject inst { set { _inst = value; } }
	
	[XmlElement("Type")]
	public string Name { get; set; }  // это будет название префаба из Resourses
	
	[XmlElement("Position")]
	public Vector3 position {get; set; } 
	
	public PositData() { }
	
	public PositData(string name, Vector3 position)
	{
		this.Name = name;
		this.position = position;
	}
	
	public virtual void Estate(){  }   // для "доработки" объекта после создания
	
	public virtual void Update(){  // обновление нашего рефлектора
		position = _inst.transform.position;  // согласно реальной информации об объекте
	}
	
}

[XmlType("Lamp")] 
public class Lamp : PositData   // лампочка, кроме положения, может ещё быть вкл/выкл
{	
	[XmlAttribute("Light")]
	public bool lightOn { get; set; }
	
	public Lamp() { }
	
	public Lamp(string name, Vector3 position, bool lightOn): base(name, position) {
		this.lightOn = lightOn;
	}
	
	public override void Estate(){
		if (!lightOn) ((Light)(_inst.GetComponentInChildren(typeof(Light)))).enabled = false;
	}		// исходим из того, что в префабе компонент Light включен 
	
	public override void Update(){
		base.Update();
		lightOn = ((Light)_inst.GetComponentInChildren(typeof(Light))).enabled;
	}  // lightOn = включен ли компонент Light на лампе
}

Итак, сериализуемые классы готовы. Сделаем теперь ещё класс для дополнительного упрощения сериализации созданного типа RoomState.

2.3 Непосредственно сериализация

Тоже в Standard Assets сделаем класс с парой статических методов, которыми будем в дальнейшем пользоваться:

using System.Xml.Serialization; 
using System;
using System.IO; 
	
public class Serializator {
	
	static public void SaveXml(RoomState state, string datapath){

		Type[] extraTypes= { typeof(PositData), typeof(Lamp)};
		XmlSerializer serializer = new XmlSerializer(typeof(RoomState), extraTypes); 

		FileStream fs = new FileStream(datapath, FileMode.Create); 
		serializer.Serialize(fs, state); 
		fs.Close(); 

	}
	
	static public RoomState DeXml(string datapath){

		Type[] extraTypes= { typeof(PositData), typeof(Lamp)};
		XmlSerializer serializer = new XmlSerializer(typeof(RoomState), extraTypes); 
		
		FileStream fs = new FileStream(datapath, FileMode.Open); 
		RoomState state = (RoomState)serializer.Deserialize(fs); 
		fs.Close(); 

		return state;
	}
}

Здесь XmlSerializer мы создаём через конструктор Constructor (Type, Type[])
FileStream открываем по адресу сохранения, передаваемого конкретной локацией.

Использование

Итак, все вспомогательные инструменты готовы, можно приступать к самой комнате. На объект комнаты вешаем:

public class RoomGen : MonoBehaviour {

	private RoomState state;	// отражающий класс
	private string datapath;	// путь к файлу сохранения для этой локации
	
	void Start () {
		datapath = Application.dataPath + "/Saves/SavedData" + Application.loadedLevel + ".xml";

		if (File.Exists(datapath))	// если файл сохранения уже существует
			state = Serializator.DeXml(datapath);  // считываем state оттуда
		else 
			setDefault();		// иначе задаём дефолт
		
		Generate();	// 	генерируем локацию по информации из state
		
	}
	
	void setDefault(){	
		state = new RoomState();
		// chair, table, lamp - нужные префабы из Resourses
		state.AddItem(new PositData("chair", new Vector3(15f, 1f, -4f)));
		state.AddItem(new PositData("chair", new Vector3(10f, 1f, 0f)));
		state.AddItem(new PositData("table", new Vector3(5f, 1f, 4f)));
		state.AddItem(new Lamp("lamp", new Vector3(5f, 4f, 4f), true));

	}
	
	void Generate(){
		foreach (PositData felt in state.furniture){  // для всех предметов в комнате
			felt.inst = Instantiate(Resources.Load(felt.Name), felt.position, Quaternion.identity) as GameObject;
			// овеществляем их
			felt.Estate(); // и задаём дополнительные параметры
		}				
	}
	
	void Dump() {
		state.Update(); // вызов обновления state
        	Serializator.SaveXml(state, datapath); // и его сериализация
	}
}

Напоследок, сделаем вызов RoomGen.Dump(). Пусть, например, по триггерам на дверях, которые являются дочерними объектами относительно комнаты (объекта с компонентом RoomGen):

using UnityEngine;
using System.Collections;

public string nextRoom;

public class Door : MonoBehaviour {
	
	void OnTriggerEnter(Collider hit)
	{
		
		if (hit.gameObject.tag == "Player")
		{
			SendMessageUpwards("Dump");
			Application.LoadLevel(nextRoom);
		}
	}
}

Вот и всё. Здесь опущено собственно взаимодействие с предметами и процесс изменения их состояния, но это несложно добавить. Для первоначального теста можно просто добавить в скрипт пару устанавливащих состояния функций по хоткеям, или ставить на паузу и двигать руками.

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

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

3. Save/Load через меню

Наверное, актуальнее было бы реализовать вариант с выбором/созданием пользователя и внутренними автоматическими сохранениями. Если вашей игре требуется серьёзное меню Save/Load, то вряд ли вы сейчас читаете эту статейку для профанов.

Но я жду не дождусь новогодних праздников, когда можно будет наконец увидеться с сестрой и за пару вечеров добить классическую American McGee’s Alice, так что сделаем Save/Load почти как там. Со скриншотами. Заодно будет повод покопаться в GUI, текстурах и других увлекательных вещах.

3.1 Главное меню

Чтобы сделать загрузку и сохранение через меню, нам, как ни странно, понадобится меню. Конечно, можно сделать его самостоятельно через объекты на сцене, можно заюзать готовые решения ироде NGUI , но мы пока воспользуемся GUI из штатной библиотеки.

  • Scripting Reference
    Для начала пригодятся:
    OnGUI() — функция MonoBehaviour для отрисовки GUI и обработки связанных с ним событий. Нечто вроде Update(), но специально для GUI и вызываться может чаще, чем каждый фрейм.

    GUI.Button

    static bool Button(Rect position, String text);
    static bool Button(Rect position, Textureimage );
    

    функция кнопки. Рисует её в рамках заданного прямоугольника, реагирует на нажатие, возвращая true. Конструкторов больше, но нам хватит этих.

    GUI.BeginGroup, GUI.EndGroup

    static void BeginGroup (Rect position);
    static void EndGroup ();
    

    Группировка элементов гуи, полезна в основном переопределением границ относительно которых вычисляется положение вложенных элементов (дефолтно это границы экрана, в данном случает — прямоугольник position).

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

public class MenuScript : MonoBehaviour {
	
	public Texture2D backgroundTexture;  	// фон для режима меню
	public const int mainMenuWidth = 200; 	// ширина кнопок в главном меню

	private int menutype = 0; 		// текущий тип меню - пригодится дальше
	private bool menuMode = true;	// включено ли меню
	private bool gameMode = false;	// запущена ли собственно игра -
										// false  только до первого Load / New Game
	private void Awake(){
		DontDestroyOnLoad(this);	 // объект меню не будет разрушаться при загрузке новых сцен
	}

	void Update () {		
			if (Input.GetKeyDown(KeyCode.Escape)){  // warning! Это может быть не совсем очевидно: Input.GetKey()
			
				if(gameMode)	
					if (menutype == 0 || !menuMode){ // если мы в игре или на нижнем уровне меню
						switchGameActivity();		// остановливаем/запускаем игровые события
												//  (в данном случае просто движение камеры)
						menuMode = !menuMode;
					}
				menutype = 0;  // с более глубоких уровней меню возвращает в корень
			}					// из игрового режима грузит главное меню
	}
	
	private void OnGUI(){		
	
		if (menuMode){
			
			if(backgroundTexture != null)		// очевидно, опционально
				GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), backgroundTexture);
			
			switch (menutype){
				case 0: drawMainMenu(); break;
				case 1:
				case 2: drawSaveLoadMenu(); break;
			}
		}
	}

	private void drawMainMenu(){	

		GUI.BeginGroup (new Rect (Screen.width / 2 - mainMenuWidth/2, Screen.height / 2 - 180, mainMenuWidth, 240));
		
			if (gameMode)
				if(GUI.Button(new Rect(0,0, mainMenuWidth,30) , "Resume")){
	        		menuMode = false;
					switchMouseLook();
				}
			
			if(GUI.Button(new Rect(0, 40, mainMenuWidth, 30) , "New Game")){
				menuMode = false;
				gameMode = true;
				Application.LoadLevel("first_scene");
			}
			
			if (gameMode)
				if(GUI.Button(new Rect(0, 2*40, mainMenuWidth, 30) , "Save"))
					menutype = 1;
		
			if(GUI.Button(new Rect(0, ((gameMode)? 3 : 2)*40, mainMenuWidth, 30) , "Load")){ 
				menutype = 2;	// Почему не "2 + gameMode"? C#, nuff said.
		
			if(GUI.Button(new Rect(0, ((gameMode)? 4 : 3)*40, mainMenuWidth, 30) , "Options")){}
			
			if(GUI.Button(new Rect(0, ((gameMode)? 5 : 4)*40, mainMenuWidth, 30) , "Quit Game")){
				Application.Quit();
			}
			
		GUI.EndGroup();	
		
	}

	void switchGameActivity(){	// в данном случае cчитается, что на активной камере есть скрипт MouseLook,
		Camera mk = Camera.main;  // поворачивающий камеру по движению мыши. Вообще тут стоит
		MouseLook ml = mk.GetComponent<MouseLook>();	 //  приостанавливать обработку всех событий ввода
		if (ml != null) ml.enabled = !ml.enabled; 	// и всю динамику
	}
	
}

Главное меню до и после начала игры

3.2 Рисуем меню загрузки / сохранения

Функция drawSaveLoadMenu() у нас уже вызывается при menutype>0, но пока не написана. Исправим это упущение. Пока просто научимся рисовать наши меню и вызывать собственно функции загрузки/сохранения.

  • Scripting Reference
    GUI.SelectionGrid — рисует сетку кнопок, но по сути это одновариантый селект. Всегда выбран один вариант, возвращает номер выбранного.

    static int SelectionGrid (Rect position , int defaultSelected, Texture[] images, int elsInRow);
    static int SelectionGrid (Rect position , int defaultSelected, string[] texts, int elsInRow);
    

    Количество — исходя из размеров передаваемого массива. Вообще предназначен для использования как-то так:

    public int selGridInt = 0;  // на старте задаём выбранное по дефолту
    public string[] selStrings = new string[] {"Grid 1", "Grid 2", "Grid 3", "Grid 4"};
    
    void OnGUI() { // и дальше держим и изменяем эту переменную
     	selGridInt = GUI.SelectionGrid(new Rect(25, 25, 100, 30), selGridInt, selStrings, 2); 
    				//  возвращает selGridInt если ничего не произошло, 
     }  		// и индекс нового элемента, если был клик по одному из элементов сетки
    

  • Суть
    Кажется, это не совсем то, что нам требуется — нам-то нужно выбрать один раз и сразу отреагировать. Но SelectionGrid спокойно ест грязный хак — индекс вне пределов реального массива. Т.е. мы всегда будем передавать, допустим, -1 и тогда сможем отслеживать собственно событие клика.

public const int slotsAmount = 10;  // количество слотов загрузки/сохранения
									// массив наших скриншотов. подробнее о нём дальше,
private Texture2D[] saveTexture = new Texture2D[slotsAmount]; // пока важно, что если слот содержит 
		//сохранение, то соответствующий элемент массива содержит текстуру в виде скриншота, иначе null
private void drawSaveLoadMenu(){
	
	if(GUI.Button(new Rect(Screen.width / 2 - 100, Screen.height * 2/3 + 50, 200, 30) , "Back"))
		menutype = 0;
	
	int slot = GUI.SelectionGrid(
						new Rect(	// подстраиваем под размер экрана сетку 5x2
								Screen.width / 2 - Screen.height * 5/9, // с соотношением
								Screen.height/3,	// сторон кнопки 4:3
								Screen.height * 10/9,
								Screen.height/3
						),
						-1,  // индекс вне пределов массива
						saveTexture,  // если null, то просто не рисует
						5);  // количество элементов в строке
	
	if (slot >= 0)  // выполнится только в случае клика
		if (menutype == 1) savegame(slot);  	// Если это было меню сохранения - сохраняем
		else if (menutype == 2 && saveTexture[slot] != null) loadgame (slot);
} 	 // Если загрузки и слот не пуст - загружаем. Информацию о функциональности слотов можно хранить отдельно,
 // но тут мы просто воспользовались тем же массивом текстур

Меню Load на SelectionGrid — внешне ничем не отличается от соответствующего Save

Основное, что мне в этом решении не нравится, это что в меню загрузки не содержащие сохранений слоты остаются относительно активными — внешне отличаются только отсутствием текстуры, реагируют на наведение. Поэтому бонусом — сетка ручками, вместо неактивных слотов рисуем Box, для активных Button.
Заодно добавим резиновости: количество слотов в строке задаётся, размер слотов подстраивается под экран. Правда, тут они уже квадратные, но встроить произвольное соотношение сторон будет несложно :) Ну и заодно min/max width/height из GUILayout и прочая обработка напильником.

public const int slotsAmount = 10;  
public const int hN = 5;	
public const int margin = 20;

static private int vN = (int)Mathf.Ceil((float)slotsAmount/hN);	
private Texture2D[] saveTexture = new Texture2D[slotsAmount];	
private int slotSize = ((Screen.width*vN)/(Screen.height*hN) >= 1) ? Screen.height/(vN + 2) : Screen.width/(hN + 2);

private void drawSaveLoadMenu(){
	
	GUI.BeginGroup (new Rect (	Screen.width / 2 -  (slotSize*hN - margin) / 2,
								Screen.height / 2 - (slotSize*vN - margin) / 2,
								slotSize*hN - margin, slotSize*vN + 40));
	
	for (int j = 0; j < vN; j++)
		for (int i = 0, curr = j*hN; (curr = j*hN + i) < slotsAmount; i++){
			if (menutype == 2 && saveTexture[curr] == null)
				GUI.Box(new Rect(slotSize*i, slotSize*j, slotSize - margin, slotSize - margin), "");
			else 
				if(GUI.Button(new Rect(slotSize*i, slotSize*j, slotSize - margin, slotSize - margin),
															saveTexture[curr])){
					if (menutype == 1) savestuff(curr);
					else if (menutype == 2) loadstuff (curr);
				}				
	}
	
	if(GUI.Button(new Rect(slotSize*hN/2 - 100, slotSize*vN , 200, 30) , "Back"))
		menutype = 0;
	
	GUI.EndGroup();	
}

Меню Load на Button и Box — теперь пустые слоты неактивны

3.3 Текстуры, скриншоты

Итак, с момента создания нашего объекта меню мы будем держать массив текстур. Памяти он занимает немного и нам гарантирован в ним мгновенный доступ. На самом деле, тут и альтернативы особой нет — не пихать же работу с диском в onGUI().

Как мы уже видели, при создании нашего меню создаём и массив:

private Texture2D[] saveTexture = new Texture2D[slotsAmount];

Сохранять мы будем не только информацию сейвов, но и информацию о них, а точнее — какие именно слоты содержат сохранения. Как хранить — выбор каждого, можно по параметру 0/1 на каждый слот, можно строку из 0/1, но мы сделаем некрасиво :) и возьмём битовый вектор в int. В какой момент и как он сохраняется, увидим позже, пока просто читаем.
Добавим в Start():

int gS = PlayerPrefs.GetInt("gamesSaved");
for (int i = 0; i < slotsAmount && gS > 0; i++, gS/=2) 
	if (gS%2 != 0){
		saveTexture[i] = new Texture2D(Screen.width/4, Screen.height/4);  // тут тот же размер, в котором пишем на диск
		saveTexture[i].LoadImage(System.IO.File.ReadAllBytes(Application.dataPath + "/tb/Slot" + i + ".png"));
	} 	// и адрес, конечно, тоже совпадает с тем, куда сохраняем.

Ну и собственно главное в данном вопросе — как скрины сохранять? Напрашивается вариант Application.CaptureScreenshot, но тут сразу два подвоха. Во-первых, они сохраняются в полном размере, а поскольку в кончном итоге понадобятся нам только thumbnails, логичнее сразу сделать ресайз. Во-вторых, мы же держим массив текстур, придётся в него снова считывать с диска? Не очень-то здорово.

Функцию взятия и записи скриншота вызывать будем позже, а пока заранее выделим в Coroutine:

IEnumerator readScreen(int i){
	
	yield return new WaitForEndOfFrame(); // так мы и избежим ошибок, и не заскриншотим само меню сохранения :)
	
	int adjustedWidth = Screen.height * 4/3; // допустим, мы хотим держать определенное соотношение сторон
	Texture2D tex1 = new Texture2D(adjustedWidth, Screen.height);
	// кропаем в свежесозданную текстуру нужный участок экрана
	tex1.ReadPixels(new Rect((Screen.width - adjustedWidth)/2, 0, adjustedWidth, Screen.height), 0, 0, true);
	tex1.Apply();

	//создаем новую текстуру нужного размера
	Texture2D tex = new Texture2D(Screen.height/3, Screen.height/4, TextureFormat.RGB24, true);
	tex.SetPixels(tex1.GetPixels(2)); // и применяем наш суровый и беспощадный ресайз	
	Destroy(tex1);
	tex.Apply();
	
	saveTexture[i] = tex;		// сохраняем в массив текстур
	
	FileStream fs = System.IO.File.Open(Application.dataPath + "/tb/Slot" + i + ".png", FileMode.Create); 
	BinaryWriter binary = new BinaryWriter(fs);
	binary.Write(tex.EncodeToPNG());	// и на диск
	fs.Close();

}

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

3.4 Собственно реализация сохранения загрузки

Итак, вроде бы с шелухой разобрались. Научились минимально работе с GUI, сделали простое главное меню, меню Save/Load, научились работать со скриншотами.

Как реализовать взаимодействие между объектами сцены, параметры которых мы будем сохранять и нашим меню?

1. Если мы будем записывать только состояние такого же создаваемого с первой сцены и неразрушаемого далее объекта (например, игрок, его параметры и инвентарь) — можно сразу держать прямую ссылку.

2. GameObject.Find и GameObject.FindWithTag тут использовать практически не стыдно — загрузка/сохранение — разовое событие. Можно искать напрямую, а поскольку сцены могут содержать разную информацию — то, как вариант, добавлять на каждую специальный объект с определенным тегом, к которому и будет прикручен скрипт сохранения/загрузки собственно данной сцены, тут уже можно держать прямые ссылки на требуемые объекты.

3. < место зарезервировано под иные варианты оптимальнее, предлагайте! >

А пока рассмотрим такой простой вариант. Сохранять будем только сцену и положение игрока. Игрок в каждой сцене пересоздаётся, но всегда вид от первого лица, и соответственно к игроку прикреплена камера.
Через неё и будем получать доступ. В ниже представленной функции вся эта специфика — в двух строках помеченных //!, и её не сложно локально заменить, остальное привязано к уже написанному нами выше коду.

void savegame(int i)
{
	
	PlayerPrefs.SetInt("slot" + i + "_Lvl", Application.loadedLevel);  // сохраняем текущий уровень

	// !
	Interface сi = Camera.main.GetComponent<Interface>();
	if (сi != null)  сi.save(i); 	// а основную работу с PlayerPrefs делегируем самому объекту

	menuMode = false; // выходим из меню
	switchGameActivity(); // возобновляем игру

	// немного простой битовой магии. Апдейтим информацию о функциональности слотов.
	// Как уже сказано, можно использовать и более читаемые варианты.
	// в текущей реализации это нам пригодится только при следующем запуске
	PlayerPrefs.SetInt("gamesSaved", PlayerPrefs.GetInt("gamesSaved") | (1 << i));
	
	StartCoroutine(readScreen(i)); // делаем скриншот уже после выхода из меню	
}

Если делать скриншот заранее, то он во-первых, может не пригодится, а во-вторых, нужно ещё успеть. А так, с учётом заблокированности камеры в режиме меню, результат примерно тот же.

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

void loadgame(int i)
{
	if (gameMode)	// меню Load доступно сразу, так что тут пригодится проверка
		switchGameActivity();

	PlayerPrefs.SetInt("Load", i); // сохраняем информацию о том, какой слот загружен

	Application.LoadLevel(PlayerPrefs.GetInt("slot" + i + "_Lvl"));  // загружаем нужный уровень

	menuMode = false;
	gameMode = true; // на случай, если игра ещё не запускалась
}

Сделаем теперь поведение, который будем вешать на камеры:

public class Interface : MonoBehaviour {
	
	private Transform CurrentPlayerPosition;

	public virtual void Start () {
		int load = PlayerPrefs.GetInt("Load");  // проверяем, не создан ли объект
		if (load >= 0){  		// в рез-те загрузки сохранения
			load(load);
			PlayerPrefs.SetInt("Load", -1);  // обнуляем Load
		}
	}
	
	public virtual void  save(int i)
	{	// получаем текущую позицию игрока и сохраняем все её параметры
		CurrentPlayerPosition = this.gameObject.transform.parent.transform;
		PlayerPrefs.SetFloat("slot" + i + "_PosX", CurrentPlayerPosition.position.x);
		PlayerPrefs.SetFloat("slot" + i + "_PosY", CurrentPlayerPosition.position.y);
		PlayerPrefs.SetFloat("slot" + i + "_PosZ", CurrentPlayerPosition.position.z);
	}
	
	public virtual void load(int i) {
		CurrentPlayerPosition = this.gameObject.transform.parent.transform; 
		
		// создаем новый вектор на основе загруженных параметров
		Vector3 PlayerPosition = new Vector3(PlayerPrefs.GetFloat("slot" + i + "_PosX"), 
					PlayerPrefs.GetFloat("slot" + i + "_PosY"), PlayerPrefs.GetFloat("slot" + i + "_PosZ"));

		CurrentPlayerPosition.position = PlayerPosition;  // и применяем его, изменяя 
	}		// положение игрока на сохраненное

}

Надо заметить „дальше как-нибудь сами“ было определенной степенью лукавства: loadgame() меню и load() объекта определенно обменялись информацией, только вот через известное место — реестр. Сохранять туда откровенно временную переменную — ход не слишком красивый. Можно изменить на прямой вызов load(), а без изменения текущей общей структуры — держать переменную в меню, и в Start() загружаемого объекта добавить поиск объекта меню и получение нужной информации.

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

public class InterfaceWAng : Interface {
	
	public override void Start () {
		base.Start();
	}
	
	public override void save(int i)
	{
		base.save(i);
		PlayerPrefs.SetFloat("slot" + i + "_AngX", CurrentPlayerPosition.eulerAngles.x);
		PlayerPrefs.SetFloat("slot" + i + "_AngY", CurrentPlayerPosition.eulerAngles.y);
	}
	
	public override void load(int i)
	{
		base.load(i);
		Vector3 PlayerDirection = new Vector3(PlayerPrefs.GetFloat("slot" + i + "_AngX"), 
					PlayerPrefs.GetFloat("slot" + i + "_AngY"), 0);

		CurrentPlayerPosition.eulerAngles = PlayerDirection;
	}

}

Конечно, здесь данным уже пригодилась бы защита. Поскольку поскольку вся фактическая работа с PlayerPrefs тут выделена в отдельные функции save() / load(), заменить их содержательную часть будет не сложно. На что? Можно аналогично примеру из части 2 держать класс-рефлектор, и сериализовать его через BinarySerializer.
Другой неплохой вариант — прикрутить, например, SQLite. Правда, по слухам, на js с ней работать удобнее, чем на шарпе, но и на последнем всё в конечном итоге заводится. Кто хочет попробовать, начать можно отсюда.


Этот текст никогда бы не получился без:

гугла
docs.unity3d.com
wiki.unity3d.com
forum.unity3d.com
answers.unity3d.com
stackoverflow.com

и хабра. Спасибо им.
Надеюсь, всё это принесёт кому-нибудь пользу, и никому — вреда :)

Узнайте, как сохранять и загружать игру в 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

Смотрите также:

Как сделать игру Match3 на UnityКак сделать игру «Match3» на Unity

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

Большинство проектов созданных в Unity часто имеют систему хранения игровых данных. Эта система включает в себя инструменты для сохранения и загрузки данных. Как и где хранить эти данные часто зависит от того что это за игра, кто в нее играет и какое кол-во данных необходимо сохранить. Обычно различают два вида хранения данных: локальнуюоблачную (удаленную) и комбинированную.

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

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

Комбинированную систему обычно используют в проектах, нацеленных как на одиночную игру, так и на многопользовательскую. В таких проектах необходимо хранить данные локально и удаленно.

В этой статье рассмотрим локальный тип хранения данных, и для этого в Unity есть очень простой инструмент PlayerPrefs.

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

Методы работы

Для начала рассмотрим способы записи данных в реестр с помощью PlayerPrefs.

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

  • SetInt. Метод используется для записи целого числа(integer) в реестр.
  • SetFloat. Метод для записи числа с “плавающей” запятой или дробного числа(float).
  • SetString. Метод для записи текстовых данных.

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

  • GetInt. Метод используется для считывания целого числа(integer) из реестра.
  • GetFloat. Метод для считывания дробного числа(float).
  • GetString. Метод для считывания текстовых данных.

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

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

Начнем с простого сохранения кол-ва уничтоженных кораблей.

Создадим небольшой скрипт Control унаследованный от MonoBehaviour.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  
  5. }

В числовой переменной kills будем хранить кол-во уничтоженных кораблей.

Теперь добавим метод сохранения Save.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  
  5.  public void Save() {}
  6.  
  7. }

В игре этот метод вызывается через UI кнопку.

После нажатия этой кнопки переменная kills запишется в реестр под указанным ключом.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  
  5.  public void Save() {
  6.   string key = “MyGame”;
  7.   PlayerPrefs.SetInt(key, this.kills);
  8.   PlayerPrefs.Save();
  9.  }
  10.  
  11. }

И так первым действие указываем в переменной key ключ под которым необходимо будет записать данные, пусть, к примеру название ключа будет MyGame, далее вызываем метод SetInt в который передаем ключ и переменную kills, в конце завершаем запись данных в реестре с помощью метода Save.

Пробуем протестировать.

Проверить записи данных можно в реестре. Для быстрого входа в реестр необходимо нажать комбинацию кнопок Win + R, после чего в окошке “Выполнить” ввести regedit и нажать “Ok”.

Далее необходимо найти раздел с игрой. Все данные unity проектов хранятся в разделе HKEY_CURRENT_USER/Software/Unity/UnityEditor/DefaultCompany в этом разделе находим проектом по названию, там и будут храниться все записи программы.

В разделе “Параметр” можно увидеть название ключа под которым записаны данные, а в разделе “Значение” число равное кол-ву уничтоженных кораблей в игре.

Именно в этом разделе мы будем хранить все остальные данные из игры.

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

Теперь необходимо произвести чтение данных из реестра.

Загрузку будет проводить при старте игры, для этого заведем новый метод Start в скрипте Control.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  
  5.  private void Start() {
  6.   Load();
  7.  }
  8.  
  9.  private void Load() {
  10.   string key = “MyGame”;
  11.  }
  12.  
  13.  /*…метод Save…*/
  14.  
  15. }

В методе Load, в переменную key укажем ключ под которым записаны наши данные.

Теперь с помощью условия проверим: существуют ли наш ключ в реестре, для этого используем метод HasKey.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  
  5.  private void Start() {
  6.   Load();
  7.  }
  8.  
  9.  private void Load() {
  10.   string key = “MyGame”;
  11.   if (PlayerPrefs.HasKey(key)) {
  12.    
  13.   }
  14.  }
  15.  
  16.  /*…метод Save…*/
  17.  
  18. }

Если ключ существует значит можно загрузить данные из реестра.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  
  5.  private void Start() {
  6.   Load();
  7.  }
  8.  
  9.  private void Load() {
  10.   string key = “MyGame”;
  11.   if (PlayerPrefs.HasKey(key)) {
  12.    this.kills = PlayerPrefs.GetInt(key);
  13.   }
  14.  }
  15.  
  16.  /*…метод Save…*/
  17.  
  18. }

Проверяем.

Отлично, данные загрузились.

Комплексные данные

И так, теперь мы научились сохранять и загружать самые элементарные данные из реестра. Теперь попробуем проделать все тоже самое с кол-во очков в игре, для этого объявим новую дробную переменную scores.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  
  6. }

Теперь немного расширим метод Save, чтобы сохранить эту новую переменную в реестр.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  
  6.  public void Save() {
  7.   string key = “MyGame”;
  8.   PlayerPrefs.SetInt(key, this.kills);
  9.   PlayerPrefs.SetFloat(key, scores);
  10.   PlayerPrefs.Save();
  11.  }
  12.  
  13. }

В методе Load проведем аналогичные действия только по загрузке переменной scores.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  
  6.  private void Start() {
  7.   Load();
  8.  }
  9.  
  10.  private void Load() {
  11.   string key = “MyGame”;
  12.   if (PlayerPrefs.HasKey(key)) {
  13.    this.kills = PlayerPrefs.GetInt(key);
  14.    this.scores = PlayerPrefs.GetFloat(key);
  15.   }
  16.  }
  17.  
  18.  /*…метод Save…*/
  19.  
  20. }

Запускаем игру, чтобы проверить работоспособность системы.

Текстовые данные

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

Кол-во жизней базы это тоже дробное число float, а так как ячейка дробного числа уже занята переменной scores, то получается, что мы не сможем поместить еще одну. В этом случае на помощь приходят текстовые данные. Мы просто преобразуем все данные для сохранения в текст и запишем его в реестр в текстовую ячейку, которая все еще пустая. Для удобного преобразования множества данных в текст и обратно используем JSONUtility.

JSON – это удобный текстовый формат хранения данных. Он преобразует любой объект в читаемый текст и обратно. С помощью него можно хранить практически любое кол-во данных в виде текста.

И так объявим новую переменную health в скрипте Control где будем хранить кол-во жизней базы.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  public float health = 100;
  6.  
  7. }

Теперь нам нужен объект который будет хранить все эти три переменные. Для этого подойдет простой класс SaveDataСоздадим новый скрипт SaveData и уберем у него наследование от MonoBehaviour.

  1. public class SaveData {
  2.  
  3.  public int kills;
  4.  public float scores;
  5.  public float health;
  6.  
  7. }

Переходим в метод Save, откуда сотрем последние два действия SetInt и SetFloat.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  public float health = 100;
  6.  
  7.  public void Save() {
  8.   string key = “MyGame”;
  9.  
  10.   SaveData data = new SaveData();
  11.  
  12.   PlayerPrefs.Save();
  13.  }
  14.  
  15. }

Сначала создаем новый экземпляр класса SaveData, после чего наполняем его данными.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  public float health = 100;
  6.  
  7.  public void Save() {
  8.   string key = “MyGame”;
  9.  
  10.   SaveData data = new SaveData();
  11.   data.kills = this.kills;
  12.   data.scores = this.scores;
  13.   data.health = this.health;
  14.  
  15.   PlayerPrefs.Save();
  16.  }
  17.  
  18. }

Теперь необходимо преобразовать объект data в текст, для чего воспользуемся методом ToJson класса JsonUtility.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  public float health = 100;
  6.  
  7.  public void Save() {
  8.   string key = “MyGame”;
  9.  
  10.   SaveData data = new SaveData();
  11.   data.kills = this.kills;
  12.   data.scores = this.scores;
  13.   data.health = this.health;
  14.  
  15.   string value = JsonUtility.ToJson(data);
  16.  
  17.   PlayerPrefs.Save();
  18.  }
  19.  
  20. }

После чего сохраняем полученный текст в реестр с помощью метода SetString.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  public float health = 100;
  6.  
  7.  public void Save() {
  8.   string key = “MyGame”;
  9.  
  10.   SaveData data = new SaveData();
  11.   data.kills = this.kills;
  12.   data.scores = this.scores;
  13.   data.health = this.health;
  14.  
  15.   string value = JsonUtility.ToJson(data);
  16.   PlayerPrefs.SetString(key, value);
  17.  
  18.   PlayerPrefs.Save();
  19.  }
  20.  
  21. }

Теперь необходимо проделать действия по загрузке данных в методе Load и перевести текст обратно в объект SaveData с помощью того же JSONUtility.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  public float health = 100;
  6.  
  7.  private void Start() {
  8.   Load();
  9.  }
  10.  
  11.  private void Load() {
  12.   string key = “MyGame”;
  13.   if (PlayerPrefs.HasKey(key)) {
  14.    string value = PlayerPrefs.GetString(key);
  15.   }
  16.  }
  17.  
  18.  /*…метод Save…*/
  19.  
  20. }

Как и раньше проверяем существование ключа, после чего загружаем текст из реестра. Далее преобразуем полученный текст в объект SaveData с помощью метода FromJson класса JsonUtility.

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  public float health = 100;
  6.  
  7.  private void Start() {
  8.   Load();
  9.  }
  10.  
  11.  private void Load() {
  12.   string key = “MyGame”;
  13.   if (PlayerPrefs.HasKey(key)) {
  14.    string value = PlayerPrefs.GetString(key);
  15.  
  16.    SaveData data = JsonUtility.FromJson<SaveData>(value);
  17.   }
  18.  }
  19.  
  20.  /*…метод Save…*/
  21.  
  22. }

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

  1. public class Control : MonoBehaviour {
  2.  
  3.  public int kills = 0;
  4.  public float scores = 0f;
  5.  public float health = 100;
  6.  
  7.  private void Start() {
  8.   Load();
  9.  }
  10.  
  11.  private void Load() {
  12.   string key = “MyGame”;
  13.   if (PlayerPrefs.HasKey(key)) {
  14.    string value = PlayerPrefs.GetString(key);
  15.  
  16.    SaveData data = JsonUtility.FromJson<SaveData>(value);
  17.    this.kills = data.kills;
  18.    this.scores = data.scores;
  19.    this.health = data.health;
  20.   }
  21.  }
  22.  
  23.  /*…метод Save…*/
  24.  
  25. }

Запускаем для проверки.

Сохранение и загрузка работают исправно. Переходим в реестр и проверяем данные.

Теперь в разделе “Значение” мы видим текст со всеми переменными и их значениями.

Заключение

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

Ознакомится с проектом из статьи можно по ссылке.

В этом уроке вы узнаете, как сохранять и загружать игру в 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;

      }

    }

  }

Эти настройки означают:

  1. Осуществляется проверка, есть ли в PlayerPrefs кэшированная настройка для кнопки «music». Если там нет никакого значения, то создается несколько ключ-значений для кнопки звука со значением 1. Кроме того, тут происходит включение и выключение переключателя AudioSource. Эти настройки будут использованы при первом запуске игры. Значение 1 используется, потому что вы не можете сохранить какое-то определенное логическое значение (но вы можете использовать 0 как false и 1 как true).
  2. Тут идет проверка ключа «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», и выберите PlayerSettingsToggleMusic (). Таким образом, когда во время игры пользователь активирует кнопку переключения в меню, появится функция 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");
}

Рассмотрим все по пунктам:

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

Чтобы выполнить сохранение процесса, нажмите 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!");
  }
}

Рассмотрим процесс более подробно:

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

Нажмите 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 полноценных игры с нуля.

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

  • Чтобы сохранить все текущие изменения сцены и всего проекта, выберите Файл > Сохранить (или Сохранить как).
  • Чтобы сохранить изменения всего проекта, но не изменения сцены, выберите Файл > Сохранить проект.

Примечание. Если вы редактируете в режиме Prefab, сохраните через Файл > Сохранить сохраняет изменения только в открытом префабе. Выйдите из Prefab Mode, чтобы сохранить более широкие изменения.

Unity автоматически сохраняет некоторую информацию, пока вы работаете в редакторе. Дополнительные сведения см. в разделе Автоматическое сохранение.

Сцена меняется

К изменениям сцены относятся модификации GameObjectsфундаментального объекта в сценах Unity, который может представлять персонажей, реквизит, декорации, камеры, путевые точки и многое другое. Функциональность GameObject определяется прикрепленными к нему компонентами. Подробнее
См. в Словарь
в СценаСцена содержит окружение и меню вашей игры. Думайте о каждом уникальном файле сцены как об уникальном уровне. В каждой сцене вы размещаете свое окружение, препятствия и декорации, по сути проектируя и создавая свою игру по частям. Подробнее
См. в Словарь
. Например:

  • Если вы добавляете, перемещаете или удаляете игровой объект.
  • Если вы изменяете параметры игрового объекта в инспектореокне Unity, в котором отображается информация о выбранном игровом объекте, активе или проекте настройки, позволяющие просматривать и редактировать значения. Дополнительная информация
    См. в окне Словарь
    .

Изменения в рамках проекта

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

Общие изменения проекта включают:

  • Настройки проектаБольшой набор настроек, которые позволяют настраивать физику, звук, работу в сети, графику, ввод и многое другое. ведут себя другие области вашего проекта. Подробнее
    См. в Словарь
    : Когда вы сохраняете свой проект, Unity сохраняет изменения в настройках проекта в папке Library в этих файлах:

    • Input: InputManager.asset
    • Tags And Layers: TagManager.asset
    • Audio: AudioManager.asset
    • Time: TimeManager.asset
    • Player: ProjectSettings.asset
    • Physics: DynamicsManager.asset
    • Physics 2D: Physics2DSettings.asset
    • Quality: QualitySettings.asset
    • Graphics: GraphicsSettings.asset
    • Network: NetworkManager.asset
    • Editor: EditorUserSettings.asset
  • Настройки сборки: Unity сохраняет изменения настроек сборки в папке Библиотека как EditorBuildSettings.asset.

  • Измененные ресурсы: когда вы сохраняете настройки для всего проекта, Unity сохраняет все несохраненные ресурсы.

    Примечание. Некоторые типы ресурсов имеют кнопку Применить в Инспекторе. Unity не сохранит их, пока вы не выберете Применить.

  • Грязные активы: Unity сохраняет грязные активы, то есть файлы на вашем диске, измененные в программе, но еще не сохраненные. Вы можете использовать пользовательские редакторы и скриптыКусок кода, который позволяет вам создавать свои собственные Компоненты, запускать игровые события, изменять свойства Компонентов с течением времени и реагировать на ввод данных пользователем любым удобным для вас способом. Подробнее
    См. в Словарь
    , чтобы пометить объект как грязный одним из следующих способов:

    • Используйте класс SerializedObject с SerializedProperties.
    • Используйте класс Undo для записи изменений.
    • Используйте SetDirty.

Автоматическое сохранение

Unity автоматически сохраняет следующие изменения на вашем диске:

  • Новые ресурсы: Unity автоматически сохраняет новые ресурсы при их создании, но вам нужно сохранить изменения позже.
  • Настройки импорта объектов. Чтобы изменения вступили в силу для большинства объектов, необходимо выбрать Применить в окне Инспектора. Unity сохраняет изменения, когда вы выбираете Применить.
  • Запеченные данные. Если в вашем проекте есть данные, для которых установлено значение «Запеченные», Unity сохраняет эту дату после завершения запекания. Это включает в себя:

    • Запеченные данные освещения
    • Запеченные навигационные данные.
    • Запеченное отсечение окклюзииФункция, которая отключает визуализацию объектов, когда они в данный момент не видны камере, поскольку они скрыты ( закрыты) другими объектами. Подробнее
      Посмотреть в данных Словарь
  • Изменение порядка выполнения скрипта: после того, как вы нажмете Применить, Unity сохранит эти данные в файле .meta каждого скрипта.

The Player Prefs class in Unity is used to save small amounts of data from your game to disk, which allows you to store and recall single string, integer and float values between gameplay sessions.

This can be extremely useful for saving and loading basic sets of data, such as option settings or simplified game saves.

However, if you’ve tried to use Player Prefs before, particularly to save a game, you may have read or been told not to use them for anything more advanced than basic preference values.

And while there’s some truth to that, there are also many reasons why you would choose to use Player Prefs, so long as you understand how they work.

But don’t worry, in this article I’ll show you how to use Player Prefs in Unity, how they work and how you can use them to save different kinds of data, so that you can decide for yourself how to use them in your own project.

Here’s what you’ll find on this page:

  • How to use Player Prefs in Unity
    • Using default values with Player Prefs
    • When to use the Player Prefs Save function
    • How to save options settings using Player Prefs
    • Saving and loading multiple settings with Player Prefs
    • Saving to multiple slots with Player Prefs
    • How to save a boolean using Player Prefs
    • How to save a colour using Player Prefs
  • Using Player Prefs for game saves
    • Alternatives to Player Prefs
  • Finding Player Prefs in Unity
    • The Player Prefs location
    • How to view a list of all Player Prefs

Let’s get started…

The Player Prefs class in Unity allows you to save basic data between gameplay sessions, such as options settings, that can be saved and reloaded, even after a game has been closed.

It works by storing a string, float or integer value to disk with a key, which is a text string that’s used to store and recall a preference.

For example, to save an integer, simply call the Set Int function of the Player Prefs class and pass in the key you want to use to retrieve it again.

Like this:

public void SaveNumber()
{
    PlayerPrefs.SetInt("myNumber", 100);
}

This will create a new stored preference under that name or, if the key already exists, it will overwrite it, using the new value.

To load the preference again, all you need to do is call Get Int, passing in the same key.

Like this:

public void LoadNumber()
{
    int loadedNumber = PlayerPrefs.GetInt(myNumber);
}

The Set Int function allows you to save and load an integer value however, similar methods are also available to store other data types, such as a float.

Like this:

public void SaveFloat()
{
    PlayerPrefs.SetFloat("myFloat", 0.1234f);
}
public void GetFloat()
{
    float loadedFloat = PlayerPrefs.GetFloat("myFloat");
}

Or a text string, 

Like this:

public void SetString(string value)
{
    PlayerPrefs.SetString("myString", "Hello!");
}
public void GetString()
{
    string loadedString = PlayerPrefs.GetString("myString");
}

The Player Prefs values are stored locally on the device in a preferences file, meaning that they will exist after the game is closed.

This means that, if you want to remove a preference key, you’ll need to manually delete it using a script, even when working in the editor.

To delete a key that you’ve created, simply call the Delete Key function of the Player Prefs class, passing in the key you want to remove.

Like this:

PlayerPrefs.DeleteKey("myString");

This will clear a specific key from Player Prefs file.

Alternatively, to delete all created keys, use the Delete All function.

Like this:

PlayerPrefs.DeleteAll();

Delete All clears everything, so it should be used with caution, especially if you’re saving any kind of progress data using Player Prefs.

However, if you’re using Player Prefs to only store options settings, you might use Delete All to reset everything, returning all custom settings back to their default values.

Using default values with Player Prefs

volume option control visualisation

When loading a preference value, if the key that you enter doesn’t exist, the default value for the data type will be returned instead, which will either be 0 for float and integer values or blank for a string.

However, it’s also possible to specify a custom default value that will be used if the key doesn’t exist.

For example, when getting a float value for a volume setting, if the preference doesn’t exist, instead of using 0, I could use a custom default value of 0.75 instead.

Like this:

float volume = PlayerPrefs.GetFloat("masterVolume", 0.75f);

This can be useful for loading settings where using an automatic default might be a bad thing, such as setting the volume to 0, simply because no player preferences exist.

Alternatively, it’s possible to check that a key exists before trying to load it, using the Has Key method.

Like this:

float volume;
if (PlayerPrefs.HasKey(key))
{
    volume = PlayerPrefs.GetFloat("masterVolume");
}

This can be useful for checking that a key exists before trying to do anything with it.

But… there’s a slight problem with this method.

While the Has Key function will reliably check to see if the key exists in the Player Prefs file, it doesn’t check what type of value is stored.

This means that, if you save a key as a float, for example, using the Set Float function, and then try to find it with its key, the Has Key function will correctly find it, and return true, showing that a key exists under that name.

However, if you then try to retrieve the key, which in this case is a float value, using either the Get Int or Get String functions, no key of that type will be found and a default value will be used instead.

This means that if you accidentally try to retrieve the wrong type of value, you’ll get a default value back as if it doesn’t exist, even if you check using the Has Key function first.

This happens because only one key value can exist for a single preference, but that key can be any one of the supported types, either a string, a float or an integer. This means that it’s possible to overwrite a key with a new data type, causing any other Get function to return nothing.

When to use the Player Prefs Save function

Most of the time, you won’t need to do anything to save Player Prefs changes to disk.

While they only exist in the game while it’s running, they’re automatically written to disk when the application quits.

However, if the game crashes, or is forced to close unexpectedly, you may lose any changes that were made.

This can be avoided by using the Save function, which will manually write all Player Prefs changes to disk when it’s called.

Like this:

PlayerPrefs.Save();

This can cause a slight pause, so it’s generally a good idea to only use the Save function occasionally and when it makes the most sense to do so.

Such as when exiting out of an options menu, for example.

Typically, the Player Prefs class is meant for storing options settings and similar preferences.

Which, for single, simple values, can be very straightforward.

However, while many options fit comfortably into a float, string or integer data structure, there are many types that don’t.

Realistically, if you’re using Player Prefs to store options settings, the values you save might include many different types of data.

So how can you manage a whole set of options values, that might include unsupported or complex data types?

How to save options settings with Player Prefs

Visualisation of player options

Player Prefs is ideal for saving and loading options settings.

While the Player Prefs class makes it easy to save and load a single float, string, or integer value, chances are, if you’re using Player Prefs to manage a set of options, you’re probably going to need to save other kinds of value as well.

So how can you manage multiple options with different, unsupported, data types?

Saving and loading multiple settings with Player Prefs

It’s relatively straightforward to save and load multiple settings using Player Prefs.

All you really need to do is group each preference that you want to use in a Save function and a Load function.

Like this:

public float sfxVolume;
public float musicVolume;
public float dialogueVolume;
public void LoadSettings()
{
    sfxVolume = PlayerPrefs.GetFloat("sfxVolume");
    musicVolume = PlayerPrefs.GetFloat("musicVolume");
    dialogueVolume = PlayerPrefs.GetFloat("dialogueVolume");
}
public void SaveSettings()
{
    PlayerPrefs.SetFloat("sfxVolume", sfxVolume);
    PlayerPrefs.SetFloat("musicVolume", musicVolume);
    PlayerPrefs.SetFloat("dialogueVolume", dialogueVolume);
    PlayerPrefs.Save();
}

This will allow you to load and save an entire set of options for a player whenever you need to.

Saving to multiple slots with Player Prefs

A lot of the time, you may only need to create a single set of Player Pref values.

Typically, Player Prefs are user-specific, meaning that different settings will be used for different user accounts, for example when logging into a computer with a different account.

However, there may be times when want to support multiple instances of the same preference value. Such as to allow multiple users to save custom settings within the same profile, or to create settings presets that can be stored in different slots.

Exactly how you do this will depend on how you manage different player slots or presets in your game.

However, a simple method of creating multiple versions of the same data set using Player Prefs is to simply add an index to every key.

Like this:

public int slot;
public float sfxVolume;
public float musicVolume;
public float dialogueVolume;
public void LoadSettings()
{
    sfxVolume = PlayerPrefs.GetFloat("sfxVolume" + slot);
    musicVolume = PlayerPrefs.GetFloat("musicVolume" + slot);
    dialogueVolume = PlayerPrefs.GetFloat("dialogueVolume" + slot);
}
public void SaveSettings()
{
    PlayerPrefs.SetFloat("sfxVolume" + slot, sfxVolume);
    PlayerPrefs.SetFloat("musicVolume" + slot, musicVolume);
    PlayerPrefs.SetFloat("dialogueVolume" + slot, dialogueVolume);
}

This allows you to save and load different versions of the same data, without changing the key.

While it’s relatively straightforward saving supported data types using Player Prefs, not all settings fit so neatly into a string, float or integer.

What if you want to store a boolean value for example?

How to save a boolean using Player Prefs

While there isn’t a built-in method of storing a boolean value using Player Prefs, it’s still possible to save and load booleans by converting their true or false value to an integer.

This works by simply checking the boolean when you save it, passing in a 0 if it’s false, or a 1 if it’s true.

Like this:

bool myBool;
PlayerPrefs.SetInt("myBool", myBool ? 1 : 0);

In this example I’ve used the Ternary Operator, which is a shorthand method of returning one of two values, depending on the condition of another.

Then, when you want to load the boolean value, all you need to do is check to see if the saved integer is equal to 1.

Like this:

myBool = PlayerPrefs.GetInt("myBool") == 1;

If it is, the boolean will be set to true, and if it’s not it’ll be set to false.

So, while this method doesn’t actually store the boolean value, it stores an equivalent integer that can be checked against to set the boolean to true or false accordingly.

This approach of converting a value into a supported type can be used to store all kinds of variables that aren’t natively supported by Player Prefs.

Such as colours for example.

How to save a colour using Player Prefs

It’s possible to store a colour variable using Player Prefs by converting the colour’s values to a readable string, which can be saved and then recalled.

This works by using the Colour Utility class, which can convert a colour to a string that represents its Red, Green and Blue values.

Like this:

Color myColor = Color.red;
string colorString = ColorUtility.ToHtmlStringRGB(myColor);

This converts the value into a Hex colour code, which is a six-character code preceded by a number sign (#), that can be stored as a string.

Like this:

PlayerPrefs.SetString("myColor", ColorUtility.ToHtmlStringRGB(myColor));

To recall the colour, simply use the Colour Utility class to parse the string (remembering to add a leading #), passing the resulting colour out to the variable you’d like to store it in.

Like this:

public Color myColor;
public void LoadSettings()
{
    ColorUtility.TryParseHtmlString("#" + PlayerPrefs.GetString("myColor"), out myColor);
}

The Colour Utility class is convenient for converting colour values to a format that can be used with Player Prefs.

And while not all data types have a function that can be used to convert the value in this way it’s often possible to convert the individual components of a complex variable into a supported format in some way.

Such as Vector 3 values which, by default, are not supported, but can be recreated using the individual floats of the X, Y and Z position values.

Like this:

public GameObject player;
public void LoadPosition()
{
    Vector3 loadedPosition = new Vector3(
            PlayerPrefs.GetFloat("posX"),
            PlayerPrefs.GetFloat("posY"),
            PlayerPrefs.GetFloat("posZ")
        );
    player.transform.position = loadedPosition;
}
public void SavePosition()
{
    Vector3 playerPosition = player.transform.position;
    PlayerPrefs.SetFloat("posX", playerPosition.x);
    PlayerPrefs.SetFloat("posY", playerPosition.y);
    PlayerPrefs.SetFloat("posZ", playerPosition.z);
    PlayerPrefs.Save();
}

This method of deconstructing a variable, storing it as separate values and reconstructing it when it needs to be loaded, can be a useful way of storing complex data using Player Prefs.

Which means that it’s possible to use Player Prefs to store more than just settings.

For example, you could use Player Prefs to create a basic save state, that loads a player’s position, health or number of coins.

So why shouldn’t you?

Is it ok to use Player Prefs for game saves?

And if not, why not?

Using Player Prefs for game saves

game save slot visualisation

You may have heard that using Player Prefs for game saves is a bad idea. So… is it?

While it is possible to store game-related data using the Player Prefs class, it’s not really designed for storing any kind of complex information that’s important to the player’s progress in the game.

So what’s the problem?

Mainly the problem with using Player Prefs for game saves is to do with the technical limitations of the Player Prefs class and how the file is saved to disk.

For example, while it is possible to deconstruct a complex variable into smaller parts that can be saved using Player Prefs, this can quickly become inconvenient and can cause complications when data needs to be grouped.

A high score entry, for example, that could include a combined name and score, might typically be stored in a single class which then might be stored in a list, containing all the high scores. Saving and loading the list object would make it easy to load and sort all of the scores that exist, allowing you to display the top ten.

While recreating the same functionality with Player Prefs is technically possible, it’s not convenient and can take a lot of work. Since one of the main reasons to use Player Prefs is for its simplicity when dealing with basic values, if you’re going to put in the extra work required to save object data in this way, then you may as well use a more suitable option for saving to disk, as it will likely involve a lot of the same processes anyway.

Another reason that Player Prefs may not be suitable for game saves is to do with how they’re stored.

For example, by default, Player Prefs are not encrypted. A player could, technically, change the Player Prefs document to give themself more gold, a higher score, or to modify their progress through the game.

Also, while many platforms store the Player Prefs document in a file, on Windows, Player Prefs is saved to the Registry.

While this isn’t an unusual practice for storing a game’s configuration, you wouldn’t normally expect to find game saves there, as storing large amounts of data in the registry could, potentially, contribute to poor performance or could cause players to lose their game saves if they move a game’s installation folder.

For basic game saves, using smaller amounts of data, it’s not necessarily a huge problem to use Player Prefs to save your game. So long as you can accept that it’s not what the system was designed for, and that, on PC, some users may not expect to find their game save data in the Registry. 

However, while it is technically possible to use Player Prefs as a method of saving complex data, if you need to save larger amounts of information, you may be better off using an alternative option.

Alternatives to Player Prefs

Using the Player Prefs class is one of the most convenient methods of saving data in Unity, however, it’s not always the most suitable.

Luckily, it’s not the only option for saving persistent data in Unity.

For example, saving data to a file using JSON or XML, can be very straightforward and is extremely useful for storing more complex data types, such as a list of high scores with names.

However, one of the most convenient methods for saving and loading game data of all kinds is to use a dedicated asset. Easy Save, for example, which has been an Asset Store favourite for many years, allows you to save just about anything to a file, easily. It also includes encryption, visual scripting support and cloud support, making it an extremely convenient and fully-featured option.

Finding Player Prefs in Unity

When working with Player Prefs, because you normally can’t view the preference values that are stored from inside the editor, it can be difficult to know what has been saved and where.

So how can you make working with Player Prefs easier?

One option is to find the stored Player Prefs file on your device.

So where are Player Prefs saved?

The Player Prefs location

The location of the Player Prefs file depends on the device that’s being used and whether or not the preference was set from the editor or a standalone build.

Here’s where you’ll find the Player Prefs file:

Player Prefs location (working in the editor)

  • MacOS: /Library/Preferences/unity.Company.Project.plist
  • Windows (in the Registry): HKCUSoftwareUnityUnityEditorCompanyProject

Player Prefs location (in the finished game)

  • MacOS: ~/Library/Preferences/com.Company.Project.plist
  • Windows (in the Registry): HKCUSoftwareCompanyProject 
  • Windows Store Apps: %userprofile%AppDataLocalPackagesProductPackageIdLocalStateplayerprefs.dat
  • Linux: ~/.config/unity3d/Company/Product
  • Android: /data/data/pkg-name/shared_prefs/pkg-name.v2.playerprefs.xml
  • iOS: /Library/Preferences/BundleIdentifier.plist

For more information on where and how Unity stores Player Prefs files, see Unity’s official documentation here.

Once you’ve found the Player Prefs file, you’ll be able to manually check it for changes and make sure that your preferences are saving as expected.

PlayerPrefs location mac (screenshot)

Finding the Player Prefs file on your system can make working with them easier to visualise.

Checking Player Prefs files for changes

Player Prefs are normally saved during On Application Quit, so you may not notice the preferences change until a game is closed or until you exit play mode.

It can be much easier, however, to simply view and manage all of your project’s Player Prefs from inside the Unity editor, using an asset.

How to view a list of all Player Prefs

While there isn’t a built-in method of viewing a project’s saved Player Prefs, other than manually finding the file and opening it, it is possible to view and manage all of a project’s stored preferences by using a free asset from the Unity Asset Store.

PlayersPrefs Editor and Utilities is a free, simple to use asset, that shows you a list of all of your Player Prefs.

Player Prefs editor screenshot

The easiest way to work with Player Prefs is by using a free viewer plugin from the Asset Store.

From there you can delete, add and even encrypt the Player Pref values.

It’s an incredibly useful tool and takes all of the guesswork out of working with Player Prefs.

Now it’s your turn

Now I want to hear from you.

How are you saving data in Unity?

Are you using Player Prefs? Or a different option?

And what have you learned about working with persistent data that you know someone else will find useful?

Whatever it is, let me know by leaving a comment below.

Get Game Development Tips, Straight to Your inbox

Get helpful tips & tricks and master game development basics the easy way, with deep-dive tutorials and guides.

My favourite time-saving Unity assets

Rewired (the best input management system)

Rewired is an input management asset that extends Unity’s default input system, the Input Manager, adding much needed improvements and support for modern devices. Put simply, it’s much more advanced than the default Input Manager and more reliable than Unity’s new Input System. When I tested both systems, I found Rewired to be surprisingly easy to use and fully featured, so I can understand why everyone loves it.

DOTween Pro (should be built into Unity)

An asset so useful, it should already be built into Unity. Except it’s not. DOTween Pro is an animation and timing tool that allows you to animate anything in Unity. You can move, fade, scale, rotate without writing Coroutines or Lerp functions.

Easy Save (there’s no reason not to use it)

Easy Save makes managing game saves and file serialization extremely easy in Unity. So much so that, for the time it would take to build a save system, vs the cost of buying Easy Save, I don’t recommend making your own save system since Easy Save already exists.

Совсем недавно мы опубликовали серию уроков (1 часть, 2 часть, 3 часть, 4 часть) по созданию простой игры, используя очень распространенный игровой движок — Unity. В этой статье мы покажем, как организовать систему управления сохраненными играми в Unity. Мы будем писать меню как в Final Fantasy, где игроку предоставляется возможность создать новое уникальное сохранение или же продолжить уже существующее. Итак, к концу урока вы научитесь:

  • сохранять игру и загружать уже имеющуюся, используя сериализацию;
  • использовать статические переменные для сохранения данных при изменении сцены.

Подготовка к сериализации

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

Мы будем ссылаться на этот сценарий из других скриптов, а потому сделаем класс статичным, добавив ключевое слово static между public и class. Также не забудем удалить два автоматически созданных метода, поскольку нам не потребуется крепить скрипт ни к какому игровому объекту.

Полученный сценарий должен выглядеть вот так:

using UnityEngine;
using System.Collections;
 
public static class SaveLoad {
 
}

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

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

Первая строка позволит нам использовать динамические списки. Вторая — даст нужный функционал операционной системы по сериализации данных. А последняя директива позволяет нам работать с потоками ввода и вывода. Иными словами, она используется для создания и чтения файлов.

Отлично, теперь мы готовы к сериализации данных!

Создание класса, с возможностью сериализации

Игры, похожие на Final Fantasy (то есть многие RPG), предлагают игроку выбрать класс персонажа. Например, рыцарь, разбойник или маг. Создайте новый скрипт, назовите его Game и объявите в нем переменные:

using UnityEngine;
using System.Collections;
 
[System.Serializable]
public class Game { 
 
    public static Game current;
    public Character knight;
    public Character rogue;
    public Character wizard;
 
    public Game () {
        knight = new Character();
        rogue = new Character();
        wizard = new Character();
    }
         
}

Обратите внимание на строчку [System.Serializable], которая говорит движку, что этот скрипт может быть сериализован. Круто, не так ли? Как говорит официальная документация, Unity умеет сериализовать следующие типы данных:

  • Все базовые типы (int, string, float, bool и т.д.).
  • Некоторые встроенные типы (Vector2, Vector3, Vector4, Quaternion, Matrix4x4, Color, Rect и LayerMask).
  • Все классы, унаследованные от UnityEngine.Object (GameObject, Component, MonoBehavior, Texture2D и AnimationClip).
  • Перечисляемый тип (Enums).
  • Массивы и списки типов данных, перечисленных выше.

Первая переменная, объявленная в нашем классе, — current — является статической ссылкой на экземпляр игры. Когда мы будем сохранять или загружать какие-либо данные, нам потребуется обратиться к «текущей» игре. При использовании статических переменных сделать это особенно просто, без вызова лишних методов. Очень удобно!

Обратите внимание на класс Character. Его еще у нас нет — давайте создадим новый скрипт с таким названием:

using UnityEngine;
using System.Collections;
 
[System.Serializable] 
public class Character {
 
    public string name;
 
    public Character () {
        this.name = "";
    }
}

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

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

Сохранение игры

Что происходит по нажатию на кнопку «Загрузить игру»? Правильно — показывается список уже сохраненных игры, которые мы можем восстановить. Так давайте создадим список игр и назовем его savedGames:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
 
public static class SaveLoad {
 
    public static List<Game> savedGames = new List<Game>();
 
}

А теперь напишем статическую функцию сохранения игры:

public static void Save() {
    savedGames.Add(Game.current);
    BinaryFormatter bf = new BinaryFormatter();
    FileStream file = File.Create (Application.persistentDataPath + "/savedGames.gd");
    bf.Serialize(file, SaveLoad.savedGames);
    file.Close();
}

Разберемся с тем, что же выполняет эта функция. Сначала мы добавляем в ранее созданный список текущую игру. Так как в скрипте Game мы использовали статическую переменную current, то мы можем достаточно просто обратиться к ней: Game.current. После этого нам следует сериализовать список, для чего и создается экземпляр класса BinaryFormatter.

Далее при помощи класса FileStream мы создаем новый файл под названием savedGames.gd в специальной директории, в которой и должны храниться все игровые данные. Для названия файла сохранений будем использовать savedGames, а расширением будет gd (от словосочетания game data).

Примечание автора В качестве расширения файла вы можете использовать все, что угодно. Так, например, в играх The Elder Scrolls используется .esm.

Далее вызывается метод Serialize экземпляра класса BinaryFormatter, который и сохраняет в файл сериализованные данные. И на этом все — файл закрывается. Наша игра сохранена!

Загрузка игры

А здесь все довольно просто. Так как при сохранении игры мы создали файл и записали в него сериализованный список, то сейчас нам придется открыть его и десериализовать имеющиеся в нем данные:

public static void Load() {
    if(File.Exists(Application.persistentDataPath + "/savedGames.gd")) {
        BinaryFormatter bf = new BinaryFormatter();
        FileStream file = File.Open(Application.persistentDataPath + "/savedGames.gd", FileMode.Open);
        SaveLoad.savedGames = (List<Game>)bf.Deserialize(file);
        file.Close();
    }
}

Алгоритм предельно прост. Для начала мы проверяем, существует ли файл сохранений. Если существует, то мы создаем новый экземпляр класса BinaryFormatter, открываем файл savedGames.gd и записываем в список savedGames десериализованные данные, считанные из файла.

Обратите внимание на 5 строчку. Метод Deserialize вернет нам битовую последовательность и, присвоив списку savedGames то, что возвращает функция Deserialize, мы ни к чему хорошему не придем. И поэтому нам следует полученную битовую последовательность преобразовать (привести) к типу List <Game>.

Примечание автора Тип данных, к которому вы приводите десериализованные данные, может быть совершенно разным. Например: Player.lives = (int)bf.Deserialize(file);.

Вывод

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

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
 
public static class SaveLoad {
 
    public static List<Game> savedGames = new List<Game>();
             
    //методы загрузки и сохранения статические, поэтому их можно вызвать откуда угодно
    public static void Save() {
        SaveLoad.savedGames.Add(Game.current);
        BinaryFormatter bf = new BinaryFormatter();
        //Application.persistentDataPath это строка; выведите ее в логах и вы увидите расположение файла сохранений
        FileStream file = File.Create (Application.persistentDataPath + "/savedGames.gd");
        bf.Serialize(file, SaveLoad.savedGames);
        file.Close();
    }   
     
    public static void Load() {
        if(File.Exists(Application.persistentDataPath + "/savedGames.gd")) {
            BinaryFormatter bf = new BinaryFormatter();
            FileStream file = File.Open(Application.persistentDataPath + "/savedGames.gd", FileMode.Open);
            SaveLoad.savedGames = (List<Game>)bf.Deserialize(file);
            file.Close();
        }
    }
}

Таковы основы работы с игровыми данными в Unity. В файле проекта вы можете найти несколько других скриптов, которые демонстрируют, как я применяю написанные нами функции и как я отображаю данные при помощи Unity GUI.

Перевод статьи «How to Save and Load Your Players’ Progress in Unity»

Saving data is critical for any game. Whether you need to save high scores, preferences, or a game state, Unity offers a variety of methods – from PlayerPrefs to serializing data, encrypting it, and writing to a file.

Updated June 23rd, 2021: As part of Unite Now 2020, I created a session with tips on data persistence in Unity. It covers some of the common ways to save and load data in your Unity project, but it’s by no means an exhaustive list. That is to say, there are more ways to serialize data than you’ll ever need, and each approach solves a particular problem and comes with its own set of strengths and weaknesses. This blog post will cover the same common methods that I discussed in the Unite Now session.

PlayerPrefs

PlayerPrefs are not made to save game states. However, they’re useful, so we’ll discuss them. You can use PlayerPrefs to store a player’s preferences between sessions, such as quality settings, audio volume or other non-essential data. PlayerPrefs are stored somewhere on your device, separate from your project. The exact location varies depending on your operating system, but it’s usually somewhere that’s globally accessible and managed by your OS. The stored data is in simple key-value pairs. Because of their ease of access, they aren’t safe from users who wish to open and modify them, and they can be deleted by accident since they’re saved outside of the project and managed by your OS.

PlayerPrefs are relatively easy to implement and require only a few lines of code, but they only support Float, Int and String-type values, making it challenging to serialize large, complex objects. A determined user can overcome this limitation by converting their saved data into some format represented by one of these basic types, but I don’t recommend it since there are better tools to store your data.

Finally, since each Unity application stores all its PlayerPrefs in a single file, it’s not well-suited for handling multiple save files or cloud saves, both of which require you to store and receive save data from a different location.

JSON

JSON is a human-readable data format. That is, it’s easily understood by people and machines alike – which has both advantages and disadvantages. It’s much easier to debug your saved data or create new save data for testing purposes when you can read and understand it, but, on the other hand, it’s easy for players to read and modify the data as well. The ability to read and change data is useful if you support modding but detrimental if you want to prevent cheating. In addition to these concerns, since JSON is a text-based format, it’s more expensive for machines to parse. That is, it’s slower to read and uses more memory than binary alternatives. So, if you have lots of data, you may want to consider options that aren’t text-based. Every use case is different, and it’s these kinds of tradeoffs that lead developers to create many other data formats.

JSON is standardized and widely used in many different applications. As a result, all platforms support it strongly, which is helpful when building cross-platform games. JSON was developed as a communication protocol for web browsers, making it inherently good for sending data over a network. Because of this, JSON is excellent for sending and receiving data from a server backend.

JsonUtility

JsonUtility is Unity’s built-in API for serializing and deserializing JSON data. Similar to PlayerPrefs, it’s also relatively easy to implement. However, unlike PlayerPrefs, you must save the JSON data yourself, either in a file or over a network. Handling the data storage yourself makes it easy to manage multiple save files because you can store each file in a different location. To make this easier, I wrote a basic file manager, which is available in this example repository.

It’s important to mention that JsonUtility isn’t a fully featured JSON implementation. If you’re used to working with JSON data, you may notice the lack of support for specific features. If you’re interested in comparing the performance of different JSON solutions, try this benchmarking project. Keep in mind that it’s best to test on your target device if possible.

The same limitations constrain JsonUtility as the internal Unity serializer – that is to say, if you can’t serialize a field in the Inspector, you won’t be able to serialize it to JSON. To work around these limitations, you could create Plain Old Data types (or PODS) to hold all your save data. When it comes time to save, transfer your data from their runtime types into a POD, and save that to a disk. If needed, you can also create custom serialization callbacks to support types that Unity’s serializer doesn’t support by default.

On the topic of JsonUtility, EditorJsonUtility is another useful tool. Whereas JsonUtility works for any MonoBehaviour or ScriptableObject-based object, EditorJsonUtility will work for any Unity engine type. So you could create a JSON representation of any object in the Unity Editor – or go in the other direction and create an asset from a JSON file.

Other libraries

Aside from the built-in serialization options, there are other external libraries that you could use as well. Unless you specifically need to use a text-based format for their readability, it’s best to go with a binary-based serializer:

Binary Tools

  • MessagePack is an efficient binary serializer. It’s performant and relatively easy to use. Like JSON, it’s available on nearly all platforms, so you can use it to send data across networks to communicate with backend servers. You can read more about it here.
  • ProtoBuf and Protobuf-net is another similar binary serializer. It’s also fast and efficient. Google developed it as a performant alternative to existing formats like XML. Like JSON and MessagePack, It’s also well suited for communication over networks.
  • BinaryFormatter is a DotNet library to store your objects in a binary format directly. However, BinaryFormatter has dangerous security vulnerabilities and should be avoided. I repeat, do not use BinaryFormatter. Learn more information about the security risks here.

Text Tools

  • EasySave is a well-supported and popular plug-in available on the Unity Asset Store. It allows you to save all your data without writing any code, which is excellent for beginners. It also has a powerful and flexible API that makes it ideal for advanced users as well. It isn’t free, but it’s worth the price if you’re looking for a fully featured out-of-the-box solution.
  • JSON.Net is a free and open source JSON implementation for all DotNet platforms. Unlike the built-in JsonUtility, it’s fully featured. However, this comes at a cost because it’s significantly less performant than the built-in JsonUtility. The standard version doesn’t support all of Unity’s platforms, but there is a modified version available in the Unity Asset Store that adds support.
  • XML is an alternative data format. Like JSON, it’s relatively human-readable and has some features that may be useful for your specific application, such as namespaces. DotNet has built-in support for XML.

Data security

When security comes up, most people think of encryption first. However, when it comes to storing data locally on a player’s device, encryption is relatively easy to overcome. Even without breaking the encryption, users can manipulate the data directly in memory with freely available tools. In other words, it’s safe to assume that anything that’s stored locally is untrustworthy. 

If you need real security, your best option is to keep your data on a server where users can’t modify it. For this to work, the application shouldn’t send any data directly to the server because users could still manipulate it. Instead, the application can only send commands to the server, let the server change the data, and then send the results back to the application. So if data security is vital for you, it’s best to know as soon as possible because it will affect your project’s architecture.

For more information about serialization, check out the manual page. If you’d like to see this in action, check out the accompanying Unite Now session.

Понравилась статья? Поделить с друзьями:
  • Сообщение cmos checksum error
  • Сохранение невозможно ошибка диска фотошоп
  • Сообщение a disk read error occurred
  • Сохнут листья у бегонии причины как исправить
  • Соо ошибки скания