[Из песочницы] Бинарная сериализация в Unity 3D/Visual Studio Application
В процессе разработки плагина для Unity 3D понадобилось сделать хранение относительно большого количества данных. В моем случае это хранение данных нодов для визуального программирования (так же применим и к реализации сохранения игры). Способ хранения должен отвечать заданным требованиям: Высокая скорость обработки;
Высокий уровень сжатия данных;
Возможность хранения своих классов и структур;
Чтение\запись в Unity, а так же в отдельной программе (Visual Studio Application, C#);
Работать со старыми версиями сохраненных данных (при изменении структуры);
Не должен требовать наличие дополнительно установленных пакетов и др. ПО у пользователей;
Работать на мобильных устройствах;
Язык: C#.
В результате я остановился на двоичной сериализации. Данный способ отвечает всем заданным требованиям, но лишает возможности просмотра и редактирования уже сериализованных данных в текстовом редакторе. Но это не проблема, так как для этого предназначена программа для редактирования.ПрограммаПервой задачей было сделать сериализацию и десериализацию данных в программе. Я написал простенькую программу, которая будет редактировать и сериализовать данные нодов в кастомном классе Nodes в виде (ID, Объект) в коллекции Dictionary. Объектов будет очень много, поэтому ID ноды будет храниться 16-разрядным типом данных short.Класс Nodes, для начала будет самый простой. Помечаем его как Serializable.
[Serializable ()]
public class NodesV1
{
public Dictionary
short CalcNewItemIndex () { short Index = -1; //Переменная позиции while (Nodes.Name.ContainsKey (++Index)); //Инкрементируем индекс пока найдется свободное место return Index; //Возвращает индекс свободного места } Сериализация Два шага, которых нужно выполнить на данном этапе, это заставить сериализатор работать с нашим классом NodesV1 и сделать учет на то, что структура данных сериализуемого\ десериализуемого объекта будет меняться (в процессе разработки она будет изменяться не раз).Второй шаг не обязательный, но если изменить структуру- десериализовать файл с прошлой структурой не получится (но в некоторых случаях если добавить в конец новые данные, то старого файла обычно проходит без проблем).
Для начала нужен класс, который будет работать над сериализацией/десериализацией, в нем же заставим сериализатор работать с нашим классом.
Код класса сериализации\десериализации
public class SaveLoad_Data
{
BinaryFormatter bformatter = new BinaryFormatter ();
public void Save (object data, string filepath)//Функция сериализации
{
Stream stream = File.Open (filepath + ».txt», FileMode.Create);//Открываем поток
BinaryFormatter bformatter = new BinaryFormatter ();
bformatter.Binder = new ClassBinder ();//Обучаем сериализатор работать с нашим классом
bformatter.Serialize (stream, data);//Cериализуем
stream.Close ();//Закрываем поток
}
public object Load (string filepath)//Функция десериализации
{
byte[] data = File.ReadAllBytes (filepath + ».txt»);//Читаем наш файл
MemoryStream stream = new MemoryStream (data);//Создаем поток с нашими данными
bformatter.Binder = new ClassBinder ();//Обучаем десериализатор
NodesV1 _NodesV1 = (NodesV1)bformatter.Deserialize (stream);//Десериализуем
stream.Close ();//Закрываем поток
return _NodesV1;//Возвращаем данные
}
}
public sealed class ClassBinder: SerializationBinder //
{
public override Type BindToType (string assemblyName, string typeName)
{
if (! string.IsNullOrEmpty (assemblyName) && ! string.IsNullOrEmpty (typeName))
{
Type typeToDeserialize = null;
assemblyName = Assembly.GetExecutingAssembly ().FullName;
typeToDeserialize = Type.GetType (String.Format (»{0}, {1}», typeName, assemblyName));
return typeToDeserialize;
}
return null;
}
}
Запуск сериализации\десериализации
NodesV1 Nodes;//Класс с данными нодов, которые будем сериализовать
private void Form1_Load (object sender, EventArgs e) //При загрузке программы
{
Nodes = new NodesV1();//Создаем экземпляр класса нодов, с которым будем работать в программе
//Инициализируем переменные
Nodes.Name = new Dictionary
Добавим проверку версии:
public class SaveLoad_Data { BinaryFormatter bformatter = new BinaryFormatter (); public void Save (object data, string filepath)//Функция сериализации { int Version;//Переменная версии сериализатора BinaryFormatter bformatter = new BinaryFormatter (); bformatter.Binder = new ClassBinder ();//Здесь мы обучаем сериализатор работать с нашим классом MemoryStream streamReader = new MemoryStream (); bformatter.Serialize (streamReader, data);//Cериализуем Version = Convert.ToInt32(data.GetType ().ToString ().Replace («HabrSerialis.NodesV»,»));//Берем номер версии сериализатора с имени класса byte[] arr = streamReader.ToArray ();//Байтовый массив данных byte[] versionBytes = BitConverter.GetBytes (Version);//преобразуем версию в байты byte[] result = new byte[arr.Length + 4]; // //сделаем массив, к который мы запишем данные и версию. int — 4 байта Array.Copy (arr, 0, result, 4, arr.Length);//пишем данные Array.Copy (versionBytes, 0, result, 0, versionBytes.Length);//пишем версию File.WriteAllBytes (filepath + ».txt», result);//пишем в файл streamReader.Close ();//Закрываем поток } public object Load (string filepath)//Функция десериализации { byte[] back = File.ReadAllBytes (filepath + ».txt»);//Читаем наш файл int versionBack = BitConverter.ToInt32(back, 0);//Определяем версию byte[] data = new byte[back.Length — 4]; // вырезаем данные без версии Array.Copy (back, 4, data, 0, back.Length — 4);//копируем данные без версии в новый массив MemoryStream stream = new MemoryStream (data);//Создаем поток с нашими данными bformatter.Binder = new ClassBinder ();//Обучаем десериализатор if (versionBack == 1)//Если это версия 1 { NodesV1 _NodesV1 = (NodesV1)bformatter.Deserialize (stream);//используем десериализатор версии 1 stream.Close ();//Закрываем поток return _NodesV1; } return null; } } public sealed class ClassBinder: SerializationBinder { public override Type BindToType (string assemblyName, string typeName) { if (! string.IsNullOrEmpty (assemblyName) && ! string.IsNullOrEmpty (typeName)) { Type typeToDeserialize = null; assemblyName = Assembly.GetExecutingAssembly ().FullName; typeToDeserialize = Type.GetType (String.Format (»{0}, {1}», typeName, assemblyName)); return typeToDeserialize; } return null; } } Теперь создадим в программе несколько нодов и запустим сериализацию. Полученный файл нам еще понадобится.Теперь проверяем, работает ли. Допустим, наша структура изменилась, мы добавили переменную Permission (Perm). Создадим класс с новой структурой:
[Serializable ()]
public class NodesV2
{
public Dictionary
Изменим код проверки версии в десериализаторе:
if (versionBack == 1)//Если версия 1
{
NodesV1 _NodesV1 = (NodesV1)bformatter.Deserialize (stream);//используем десериализатор версии 1
stream.Close ();//Закрываем поток
NodesV2 NodeV2ret = new NodesV2();//Создаем экземпляр класса который будем возвращать
NodeV2ret.Name = _NodesV1.Name; //Копируем имя как есть
NodeV2ret.Text = _NodesV1.Text; //Копируем текст как есть
NodeV2ret.Perm = new Dictionary
public class HabrSerialis: MonoBehaviour
{
NodesV2 Nodes;
SaveLoad_Data _LoadNodes;
void Start ()
{
Nodes = new NodesV2();
_LoadNodes = new SaveLoad_Data ();
Nodes = (NodesV2)_LoadNodes.Load («HabrSerialisText»);
}
float Offset;
void OnGUI ()
{
Offset = 100;
for (short i = 0; i < Nodes.Name.Count; i++)
{
Nodes.Name[i] = GUI.TextField(new Rect(Offset, 100, 100, 30), Nodes.Name[i]);
Nodes.Text[i] = GUI.TextArea(new Rect(Offset, 130, 100, 200), Nodes.Text[i]);
Offset += 120;
}
if (GUI.Button(new Rect(10, 10, 70, 30), "Save"))
{
_LoadNodes.Save(Nodes, "HabrSerialisText");
}
}
}
[Serializable()]
public class NodesV1
{
public Dictionary
else if (versionBack == 2)//Если версия 2 — используем текущий (последний на данный момент) десериализатор { NodesV2 _NodesV2 = (NodesV2)bformatter.Deserialize (stream);//десериализуем и записываем stream.Close ();//Закрываем поток return _NodesV2; } ////////////////////////////////////////////////////////////// return null; } } public sealed class ClassBinder: SerializationBinder { public override Type BindToType (string assemblyName, string typeName) { if (! string.IsNullOrEmpty (assemblyName) && ! string.IsNullOrEmpty (typeName)) { Type typeToDeserialize = null; assemblyName = Assembly.GetExecutingAssembly ().FullName; typeToDeserialize = Type.GetType (String.Format (»{0}, {1}», typeName, assemblyName)); return typeToDeserialize; } return null; } } Как видим, код класса обрабатывающий сериализацию тот же самый, только вместо: byte[] back = File.ReadAllBytes (filepath + ».txt»); мы будем использовать: TextAsset asset = Resources.Load (filepath) as TextAsset; byte[] back = asset.bytes; Если скрипт не планируется запускать на мобильных устройствах (или аналогичных), можно ничего не трогать, только подправить пути: byte[] back = File.ReadAllBytes («Assets/Resources/» + filepath + ».txt»); После сохранения объектов кнопкой Save нужно свернуть и развернуть Unity, чтобы обновленный бинарный файл импортировался и обновился.Исходный код программы using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization; using System.Reflection;
namespace HabrSerialis
{
public partial class Form1: Form
{
public Form1()
{
InitializeComponent ();
}
/////////////////////////////////////////////////////////////////////////////////////////////////
short _SelectedNodeID = 0; //Индекс выбранного элемента
NodesV2 Nodes;//Класс с данными нодов, которые будем сериализовать
private void Form1_Load (object sender, EventArgs e) //При загрузке программы
{
Nodes = new NodesV2();//Создаем экземпляр класса нодов, с которым будем работать в программе
//Инициализируем
Nodes.Name = new Dictionary
_SaveNodes.Save (Nodes, @«C:\UnityProjects\Blueprint_Editor_Plugin\Assets\Resources\HabrSerialisText»); } private void button2_Click (object sender, EventArgs e)//Десериализуем { SaveLoad_Data _LoadNodes = new SaveLoad_Data ();
Nodes = (NodesV2)_LoadNodes.Load (@«C:\UnityProjects\Blueprint_Editor_Plugin\Assets\Resources\HabrSerialisText»); UpdateList ();//Обновляем список } /////////////////////////////////////////////////////////////////////////////////// private void listBox1_SelectedIndexChanged (object sender, EventArgs e) //Выбор элемента в списке { _SelectedNodeID = (short)listBox1.SelectedIndex;
if (Nodes.Name.ContainsKey (_SelectedNodeID))//есть ли такой объект { textBox1.Text = Nodes.Name[_SelectedNodeID];//Выводим имя объекта в текстовое поле 1 textBox2.Text = Nodes.Text[_SelectedNodeID];//Выводим текст объекта в текстовое поле 2 } } /////////////////////////////////////////////////// private void button3_Click (object sender, EventArgs e)//Добавление нового объекта (ноды) { short _NewNodeID = CalcNewItemIndex ();
Nodes.Name.Add (_NewNodeID, «New Node name»);//Добавляем имя в коллекцию Nodes.Text.Add (_NewNodeID, «New Node Text»);//Добавляем в коллекцию Nodes.Perm.Add (_NewNodeID, false);//Добавляем в коллекцию UpdateList ();//Обновляем список объектов listBox1.SelectedIndex = _NewNodeID; } /////////////////////////////////////////////////// private void textBox2_TextChanged (object sender, EventArgs e) { Nodes.Text[_SelectedNodeID] = textBox2.Text;//Изменение текста выбранного объекта в коллекции } /////////////////////////////////////////////////// private void textBox1_TextChanged (object sender, EventArgs e)//Изменение имени { Nodes.Name[_SelectedNodeID] = textBox1.Text;//Изменение имени выбранного объекта listBox1.Items[_SelectedNodeID] = «ID:» + _SelectedNodeID + » » + textBox1.Text;//Изменение текста выбранного объекта в списке } /////////////////////////////////////////////////// short CalcNewItemIndex ()//Находим свободную позицию в коллекции { short Index = -1; while (Nodes.Name.ContainsKey (++Index)); return Index; } /////////////////////////////////////////////////// void UpdateList ()//Обновляем список объектов { listBox1.Items.Clear ();
foreach (KeyValuePair
//////////////////// Проверка версий ////////////////////////////////////
if (versionBack == 1)//Если версия 1
{
NodesV1 _NodesV1 = (NodesV1)bformatter.Deserialize (stream);//используем десериализатор версии 1
stream.Close ();//Закрываем поток
NodesV2 NodeV2ret = new NodesV2();//Создаем клас который будем возвращать
NodeV2ret.Name = _NodesV1.Name; //Копируем имя как есть
NodeV2ret.Text = _NodesV1.Text; //Копируем текст как есть
NodeV2ret.Perm = new Dictionary
foreach (KeyValuePair