Главная страница » Как сохранить проект unity на флешку

Как сохранить проект unity на флешку

  • автор:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. >

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

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicvoid Save () <>
  4. >

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

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicvoid Save () <
  4. string key = “ MyGame” ;
  5. PlayerPrefs . SetInt ( key, this . kills );
  6. PlayerPrefs . Save ();
  7. >
  8. >

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

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

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

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

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

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

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

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. privatevoid Start () <
  4. Load ();
  5. >
  6. privatevoid Load () <
  7. string key = “ MyGame” ;
  8. >
  9. /*…метод Save…*/
  10. >

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

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. privatevoid Start () <
  4. Load ();
  5. >
  6. privatevoid Load () <
  7. string key = “ MyGame” ;
  8. if ( PlayerPrefs . HasKey ( key )) <
  9. >
  10. >
  11. /*…метод Save…*/
  12. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. privatevoid Start () <
  4. Load ();
  5. >
  6. privatevoid Load () <
  7. string key = “ MyGame” ;
  8. if ( PlayerPrefs . HasKey ( key )) <
  9. this . kills = PlayerPrefs . GetInt ( key );
  10. >
  11. >
  12. /*…метод Save…*/
  13. >

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

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

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicvoid Save () <
  5. string key = “ MyGame” ;
  6. PlayerPrefs . SetInt ( key, this . kills );
  7. PlayerPrefs . SetFloat ( key, scores );
  8. PlayerPrefs . Save ();
  9. >
  10. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. privatevoid Start () <
  5. Load ();
  6. >
  7. privatevoid Load () <
  8. string key = “ MyGame” ;
  9. if ( PlayerPrefs . HasKey ( key )) <
  10. this . kills = PlayerPrefs . GetInt ( key );
  11. this . scores = PlayerPrefs . GetFloat ( key );
  12. >
  13. >
  14. /*…метод Save…*/
  15. >

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

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

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

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

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

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicfloat health = 100 ;
  5. >

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

  1. publicclass SaveData <
  2. publicint kills ;
  3. publicfloat scores ;
  4. publicfloat health ;
  5. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicfloat health = 100 ;
  5. publicvoid Save () <
  6. string key = “ MyGame” ;
  7. SaveData data = new SaveData ();
  8. PlayerPrefs . Save ();
  9. >
  10. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicfloat health = 100 ;
  5. publicvoid Save () <
  6. string key = “ MyGame” ;
  7. SaveData data = new SaveData ();
  8. data . kills = this . kills ;
  9. data . scores = this . scores ;
  10. data . health = this . health ;
  11. PlayerPrefs . Save ();
  12. >
  13. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicfloat health = 100 ;
  5. publicvoid Save () <
  6. string key = “ MyGame” ;
  7. SaveData data = new SaveData ();
  8. data . kills = this . kills ;
  9. data . scores = this . scores ;
  10. data . health = this . health ;
  11. stringvalue = JsonUtility . ToJson ( data );
  12. PlayerPrefs . Save ();
  13. >
  14. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicfloat health = 100 ;
  5. publicvoid Save () <
  6. string key = “ MyGame” ;
  7. SaveData data = new SaveData ();
  8. data . kills = this . kills ;
  9. data . scores = this . scores ;
  10. data . health = this . health ;
  11. stringvalue = JsonUtility . ToJson ( data );
  12. PlayerPrefs . SetString ( key, value );
  13. PlayerPrefs . Save ();
  14. >
  15. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicfloat health = 100 ;
  5. privatevoid Start () <
  6. Load ();
  7. >
  8. privatevoid Load () <
  9. string key = “ MyGame” ;
  10. if ( PlayerPrefs . HasKey ( key )) <
  11. stringvalue = PlayerPrefs . GetString ( key );
  12. >
  13. >
  14. /*…метод Save…*/
  15. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicfloat health = 100 ;
  5. privatevoid Start () <
  6. Load ();
  7. >
  8. privatevoid Load () <
  9. string key = “ MyGame” ;
  10. if ( PlayerPrefs . HasKey ( key )) <
  11. stringvalue = PlayerPrefs . GetString ( key );
  12. SaveData data = JsonUtility . FromJson < SaveData >( value );
  13. >
  14. >
  15. /*…метод Save…*/
  16. >

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

  1. publicclass Control : MonoBehaviour <
  2. publicint kills = 0 ;
  3. publicfloat scores = 0f ;
  4. publicfloat health = 100 ;
  5. privatevoid Start () <
  6. Load ();
  7. >
  8. privatevoid Load () <
  9. string key = “ MyGame” ;
  10. if ( PlayerPrefs . HasKey ( key )) <
  11. stringvalue = PlayerPrefs . GetString ( key );
  12. SaveData data = JsonUtility . FromJson < SaveData >( value );
  13. this . kills = data . kills ;
  14. this . scores = data . scores ;
  15. this . health = data . health ;
  16. >
  17. >
  18. /*…метод Save…*/
  19. >

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

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

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

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

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

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

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

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

Подготовка

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

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

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

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

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

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

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

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

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

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

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

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

  • Две кнопки – для увеличения значений intToSave и floatToSave .
  • Текстовое поле – для переменной stringToSave .
  • Несколько лейблов для отображения текущих значений переменных.
  • Три кнопки действий, чтобы сохранить, загрузить и сбросить данные.

Сохранение

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

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

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

/Library/Preferences , в файле с названием unity.[company name].[product name].plist .

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

Загрузка

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

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

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

Сброс

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

В методе 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 похож на тот, что мы только что писали:

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

Сохранение

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

Скрипт SaveSerialСкрипт SaveSerial

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

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

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

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

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

Загрузка

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

  • Сначала ищем файл с сохраненными данными, который мы создали в методе SaveGame.
  • Если он существует, открываем его и десериализуем с помощью BinaryFormatter .
  • Передаем записанные в нем значения в переменные класса SaveSerial .
  • Выводим в отладочную консоль сообщение об успешной загрузке.

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

Сброс

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

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

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

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

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

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

В этот раз файл будет сохранен по «постоянному пути данных» игры. В Windows это C:\Users\username\AppData\LocalLow\project name , в Mac –

/Library/Application Support/companyname/productname согласно документации.

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

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

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

Заключение

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

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

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

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

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

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

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

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

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

1. PlayerPrefs

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

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

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

Конечно, тут применение 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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *