[Из песочницы] Бинарная сериализация в 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 Name;//Имя ноды public Dictionary Text;//Текст } fb2296f1feb441bb961eae3340214c19.jpg(код программы в конце статьи)Новая созданная нода должна добавляться в первую свободную позицию в коллекции, для этого я использовал код:

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(); Nodes.Text = new Dictionary(); } private void button1_Click (object sender, EventArgs e)//Сериализация { SaveLoad_Data _SaveNodes = new SaveLoad_Data ();//Создаем экземпляр класс обработки сериализации _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 = (NodesV1)_LoadNodes.Load (@«C:\UnityProjects\Blueprint_Editor_Plugin\Assets\Resources\HabrSerialisText»); //Десериализуем } Сохранять файл будем в папку проекта Unity: Assets\Resources. Именно из папки Resources будет корректно работать на Unity чтение файла на мобильных устройствах и т. д. Теперь шаг второй, решить вопрос с версией десериализатора. В первые два байта бинарного файла мы будем записывать версию сериализатора. При десериализации мы считываем версию, убираем эти два байта и запускаем десериализатор соответствующей версии. Версия сериализатора будет определяться по цифрам в конце имени класса (NodesV1 — версия »1»).

Добавим проверку версии:

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 Name; public Dictionary Text; public Dictionary Perm; } Изменяем в коде программы класс NodesV1 на NodesV2. При запуске так же инициализируем новую переменную: Nodes.Perm = new Dictionary(); Теперь самое интересное. В файле со старой структурой данных нет переменной Perm, а нам нужно десериализовать в соответствии со старой структурой и вернуть в новой.В каждом случае будет происходить своя обработка этой ситуации, но у меня будет просто создаваться эта коллекция со значениями false.

Изменим код проверки версии в десериализаторе:

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(); //Инициализируем не существующую в версии 1 коллекцию Perm foreach (KeyValuePair name in NodeV2ret.Name) { NodeV2ret.Perm.Add (name.Key, false);//Добавляем значения false } return NodeV2ret; //Возвращаем } else if (versionBack == 2)//Если версия 2 — используем текущий (последний на данный момент) десериализатор { NodesV2 _NodesV2 = (NodesV2)bformatter.Deserialize (stream);//десериализуем и записываем stream.Close ();//Закрываем поток return _NodesV2; } После изменений десериализация файла со старой структурой проходит успешно.Unity Создаем C# скрипт, который десериализует бинарник и в GUI будет отображать имя и текст ноды. Так же можно будет изменить эти данные и сериализовать обратно.Код Unity скрипта using UnityEngine; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using System.Runtime.Serialization; using System.Reflection;

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 Name; public Dictionary Text; } [Serializable ()] public class NodesV2 { public Dictionary Name; public Dictionary Text; public Dictionary Perm; } public class SaveLoad_Data { private int Version; BinaryFormatter bformatter = new BinaryFormatter (); public void Save (object data, string filepath)//Функция сериализации { BinaryFormatter bformatter = new BinaryFormatter (); bformatter.Binder = new ClassBinder ();//Здесь мы обучаем сериализатор работать с нашим классом MemoryStream streamReader = new MemoryStream (); bformatter.Serialize (streamReader, data);//Cериализуем Version = Convert.ToInt32(data.GetType ().ToString ().Replace («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 («Assets/Resources/» + filepath + ».txt», result);//пишем в файл streamReader.Close ();//Закрываем поток } public object Load (string filepath)//Функция десериализации { TextAsset asset = Resources.Load (filepath) as TextAsset;//Читаем наш файл из ресурсов byte[] back = asset.bytes; int versionBack = BitConverter.ToInt32(back, 0);//Определяем версию byte[] data = new byte[back.Length — 4]; // вырезаем данные без версии Array.Copy (back, 4, data, 0, back.Length — 4);//копируем данные без версии в новый массив Stream stream = new MemoryStream (data);//Создаем поток с нашими данными bformatter.Binder = new ClassBinder ();//Обучаем десериализатор //////////////////////////////////////////////////////// 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(); //Инициализпуем не существующую в версии 1 коллекцию Perm foreach (KeyValuePair name in NodeV2ret.Name) { NodeV2ret.Perm.Add (name.Key, false);//Добавляем значения } return NodeV2ret;//Возвращаем данные }

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(); Nodes.Text = new Dictionary(); Nodes.Perm = new Dictionary(); } private void button1_Click (object sender, EventArgs e)//Сериализуем { SaveLoad_Data _SaveNodes = new SaveLoad_Data ();//Создаем класс обработки сериализации

_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 node in Nodes.Name) { listBox1.Items.Add («ID:» + node.Key + » » + node.Value); } } } } ////////////////////////////////////////////////// [Serializable ()] public class NodesV1 { public Dictionary Name; public Dictionary Text; } [Serializable ()] public class NodesV2 { public Dictionary Name; public Dictionary Text; public Dictionary Perm; } public class SaveLoad_Data { private int Version; BinaryFormatter bformatter = new BinaryFormatter (); public void Save (object data, string filepath) { BinaryFormatter bformatter = new BinaryFormatter (); bformatter.Binder = new ClassBinder ();//Здесь мы обучаем сериализатор работать с нашим классом MemoryStream streamReader = new MemoryStream (); bformatter.Serialize (streamReader, data);//Cериализуем Version = Convert.ToInt32(data.GetType ().ToString ().Replace («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 ();//Закрываем поток NodesV2 NodeV2ret = new NodesV2();//Создаем клас который будем возвращать NodeV2ret.Name = _NodesV1.Name; //Копируем имя как есть NodeV2ret.Text = _NodesV1.Text; //Копируем текст как есть NodeV2ret.Perm = new Dictionary(); //Инициализпуем не существующую в версии 1 коллекцию Perm

foreach (KeyValuePair name in NodeV2ret.Name) { NodeV2ret.Perm.Add (name.Key, false);//Добавляем значения } return NodeV2ret; } 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; } } Теперь можно изменять и сохранять бинарный файл в программе и в юнити: d1c3e58eca734077af671a0d7c1db284.jpg

© Habrahabr.ru