Совсем недавно мы опубликовали серию уроков (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»