Бинарная Сериализация В Unity3D

Я столкнулся с довольно тривиальной проблемой.

Сериализация и десериализация данных.



Задача
Есть приложение клиент-серверное.

Клиент — сервер Unity3d PhotonServer. Существует модель, которая должна быть эквивалентна как на клиенте, так и на сервере.

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



Решение
Протобуф Наиболее логичным решением является использование двоичного протокола.

Явным фаворитом для этого является ptotobuf (используется протосеть 668).

Он не поддерживает веб-сборку, но это оправданная жертва.

Отметил нужные классы.

Я проверяю.

Все работает, маленький размер и быстрый в работе.

Великолепный.

Но! В какой-то момент Protobuf выплюнул казнь, сказав, что такой класс не найден.

Так? Ошибка подробно с примером кода.

Я начал решать эту проблему разными способами.

Существует возможность подачи типов Protobuf. Что уже нехорошо.

Вы можете допустить довольно много ошибок или забыть указать тот или иной тип.

Более того, Protobuf не поддерживает многомерные массивы.

К сожалению, Protobuf придется отойти в сторону.

Кстати, я когда-то пробовал использовать Protobuf в сочетании с PHP и Unity. Что касается PHP, то реализация Protobuf оказалась довольно глючной.

В результате я использовал json в php и Unity. Это сработало, потому что между PHP и Unity были довольно простые структуры данных.

Пакет сообщений Есть еще один примечательный сериализатор.

Существуют реализации на огромном количестве языков.

Удивительный.

Я решил попробовать.

Примитивный тип сериализуется нормально.

Размер составляет 18 байт против моих 41 байта, 19 байтов protobuf и 44 байтов json. Отличный результат. В чем хитрость? На официальном сайте есть пример того, как он на самом деле все упаковывает. Здесь связь .

Пример

  
  
  
  
  
  
  
  
  
  
   

[Serializable, ProtoContract()] public class TTT { [TDataMember, ProtoMember(1)] public string s = "compact"; [TDataMember, ProtoMember(2)] public bool f = true; [TDataMember, ProtoMember(3)] public string s2 = "schema"; [TDataMember, ProtoMember(4)] public short i = 0; }

Но сложный пример, что будет, дальше не осилил messagepack и protobuf. Примеры ошибок.

Пакет сообщений

PlatformNotSupportedException: On-the-fly enum serializer generation is not supported in Unity iOS. Use pre-generated serializer instead. MsgPack.Serialization.ReflectionSerializers.ReflectionSerializerHelper.CreateReflectionEnuMessagePackSerializer[State] (MsgPack.Serialization.SerializationContext context) MsgPack.Serialization.MessagePackSerializer.CreateReflectionInternal[State] (MsgPack.Serialization.SerializationContext context) MsgPack.Serialization.MessagePackSerializer.CreateReflectionInternal (MsgPack.Serialization.SerializationContext context, System.Type targetType) MsgPack.Serialization.SerializationContext.GetSerializer (System.Type targetType, System.Object providerParameter) MsgPack.Serialization.ReflectionSerializers.ReflectionSerializerHelper.GetMetadata (MsgPack.Serialization.SerializationContext context, System.Type targetType, System.Func`2[]& getters, System.Action`2[]& setters, System.Reflection.MemberInfo[]& memberInfos, MsgPack.Serialization.DataMemberContract[]& contracts, MsgPack.Serialization.IMessagePackSerializer[]& serializers) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].

ctor (MsgPack.Serialization.SerializationContext context) MsgPack.Serialization.MessagePackSerializer.CreateReflectionInternal[TestC] (MsgPack.Serialization.SerializationContext context) MsgPack.Serialization.SerializationContext.GetSerializer[TestC] (System.Object providerParameter) MsgPack.Serialization.SerializationContext.GetSerializer[TestC] () /// [,] string ArgumentException: 'System.String[,]' is not compatible for 'System.String[]'.

Parameter name: objectTree MsgPack.Serialization.MessagePackSerializer`1[System.String[]].

MsgPack.Serialization.IMessagePackSerializer.PackTo (MsgPack.Packer packer, System.Object objectTree) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].

PackMemberValue (MsgPack.Packer packer, .

TestC objectTree, Int32 index) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].

PackToCore (MsgPack.Packer packer, .

TestC objectTree) MsgPack.Serialization.MessagePackSerializer`1[TestS+TestC].

PackTo (MsgPack.Packer packer, .

TestC objectTree) MsgPack.Serialization.MessagePackSerializer`1[TestS+TestC].

Pack (System.IO.Stream stream, .

TestC objectTree) /// etc SerializationException: Non generic collection may contain only MessagePackObject type. MsgPack.Serialization.DefaultSerializers.NonGenericEnumerableSerializerBase`1[T].

PackToCore (MsgPack.Packer packer, .

T objectTree) MsgPack.Serialization.MessagePackSerializer`1[T].

MsgPack.Serialization.IMessagePackSerializer.PackTo (MsgPack.Packer packer, System.Object objectTree) MsgPack.Serialization.ReflectionSerializers.ReflectionCollectionSerializer`1[System.Collections.ArrayList].

PackToCore (MsgPack.Packer packer, System.Collections.ArrayList objectTree) MsgPack.Serialization.MessagePackSerializer`1[System.Collections.ArrayList].

MsgPack.Serialization.IMessagePackSerializer.PackTo (MsgPack.Packer packer, System.Object objectTree) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].

PackMemberValue (MsgPack.Packer packer, .

TestC objectTree, Int32 index) MsgPack.Serialization.ReflectionSerializers.ReflectionObjectMessagePackSerializer`1[TestS+TestC].

PackToCore (MsgPack.Packer packer, .

TestC objectTree) MsgPack.Serialization.MessagePackSerializer`1[TestS+TestC].

PackTo (MsgPack.Packer packer, .

TestC objectTree) MsgPack.Serialization.MessagePackSerializer`1[TestS+TestC].

Pack (System.IO.Stream stream, .

TestC objectTree)

Protobuf-net

NotSupportedException: Multi-dimension arrays are supported ProtoBuf.Meta.MetaType.ResolveListTypes (ProtoBuf.Meta.TypeModel model, System.Type type, System.Type& itemType, System.Type& defaultType) ProtoBuf.Meta.MetaType.ApplyDefaultBehaviour (Boolean isEnum, ProtoBuf.ProtoMemberAttribute normalizedAttribute) ProtoBuf.Meta.MetaType.ApplyDefaultBehaviour () ProtoBuf.Meta.RuntimeTypeModel.FindOrAddAuto (System.Type type, Boolean demand, Boolean addWithContractOnly, Boolean addEvenIfAutoDisabled) ProtoBuf.Meta.RuntimeTypeModel.GetKey (System.Type type, Boolean demand, Boolean getBaseKey)

Json Так что Protobuf не подходит. Что использовать? Джсон? Почему нет. Вот вторая проблема: Джейсон не знает, как сериализовать такие поля, как интерфейс и абстрактные классы.

Нет проблем, с помощью Google я нашел, как его «научить» этому.

В конечном файле появились данные о типе поля и его данные (с указанием сборки, это важно, почему написано ниже).

Но при десериализации это поле почему-то пустое.

Я снова гуглю.

Ведь если вы научили сериализовать, то и десериализовать можно.

Получается тот же костыль, что и с Protobuf. Этот вариант не подходит. Я использовал сборку JSON .

NET For Unity, доступную на рынке активов.

Итог: Json хорош для несложных структур.

Но когда есть поля вроде абстрактного класса или интерфейса, с этим возникают проблемы.

XML В любом варианте xml довольно громоздкий.

Поэтому я решил не рассматривать это.

Хотя часть проекта находится в xml. Например, система локализации.

Бинарныйформаттер Я решил обратиться к стандартным средствам.

Разметил код, приступим к сериализации.

Успех! Однако большой размер файла — это нехорошо.

Не беда, пройдем и сжатие.

Я использовал ЛЗМА.

Немного прибавил в размерах, но потерял в скорости.

Приемлемая жертва.

Теперь сборка.

Барабанная дробь.

Веб не поддерживается, беда.

Теперь организуем обмен между клиентом и сервером.

И.

Очередной ФЕЙЛ.

Дело в том, что у классов разные сборки, хотя классы одинаковые.

У Unity есть своя сборка на фотоне.

Решить ее можно костыльным методом.

Я привязываю сборки и переименовываю их вручную, но сборка оказывается в двоичном файле.

Зачем она там нужна? Я решил, что вернусь к этому методу и присмотрел ещё парочку сериализаторов.

Один из них — сериализатор Sharp. Мне удалось сериализовать поля типа интерфейс, но он тоже прописывает сборку и не поддерживается в вебе.

Тогда я решил сначала сформулировать требования к сериализатору.

Тест примитивного типа

TTT c = new TTT(); TSerizalization serizalization = new TSerizalization(); bytes = serizalization.Serizalize(c, true); System.IO.File.WriteAllBytes("d:\\s.dat", bytes); Debug.LogError("T complete " + bytes.Length ); json = JsonConvert.SerializeObject(c); System.IO.File.WriteAllText("d:\\s.json", json); Debug.LogError("J complete " + json.Length); System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formater = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); formater.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Full; System.IO.MemoryStream mstream = new System.IO.MemoryStream(); formater.Serialize(mstream, c); Debug.LogError("B complete " + mstream.ToArray().

Length); System.IO.File.WriteAllBytes("d:\\s2.dat", mstream.ToArray()); mstream = new System.IO.MemoryStream(); var serializer = MsgPack.Serialization.SerializationContext.Default.GetSerializer<TTT>(); serializer.Pack(mstream, c); System.IO.File.WriteAllBytes("d:\\s3.dat", mstream.ToArray()); Debug.LogError("M complete " + mstream.ToArray().

Length); mstream = new System.IO.MemoryStream(); ProtoBuf.Serializer.Serialize<TTT>(mstream, c); System.IO.File.WriteAllBytes("d:\\s4.dat", mstream.ToArray()); Debug.LogError("P complete " + mstream.ToArray().

Length);



Требования к сериализатору

Требуемый сериализатор должен иметь возможность:
  • Сериализация в двоичный формат;
  • Сериализация до небольшого размера;
  • Сериализовать классы с помощью конкретных сборок (при этом информация о сборке не обязательно должна быть в файле);
  • Сериализация пользовательских классов;
  • Поддержка массивов;
  • Работа в веб-версии Unity;
  • Работа с фотоном.

Из сериализаторов, которые я пробовал.

Только Json и protobuf ранней версии более-менее отвечали этим требованиям.



Пользовательский сериализатор

Я начал гуглить.

Но безрезультатно.

Что делать? Использование стандартного решения – не лучший вариант. Большой размер.

Проблемы с платформами.

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

Почему нет. Это оказалось гораздо эффективнее, чем пустая дизассемблирование и тестирование того или иного сериализатора.

С чего лучше всего начать? Как сохранять объекты и как их загружать.

В этом плане мне понравился подход protobuf-net 668. А именно разметка необходимых полей и свойств.

Также отметьте методы, которые будут вызываться до сериализации и после десериализации.

Карта Для начала необходимо сохранить карту.

А именно, ключ и тип.

Чтобы эту карту впоследствии можно было использовать для реконструкции объекта.

Для стандартных типов значение равно < 0 and for custom types, respectively, > 0. Размер ключа — int16. карта

public class TMap { public Dictionary<Type, short> StandartTypes { get; protected set; } public Dictionary<Type, short> DataBase { get; protected set; } public Dictionary<short, Type> DataBaseTags { get; protected set; } .

}

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

Набор объектов TData Теперь вам нужно получить все поля и свойства.

Упакуйте их в свою структуру, чтобы присвоить им теги.

Множество объектов

public class TData : TContainerBase { public object value; public List<TData> childrens = new List<TData>(); .

}

Поскольку массивы могут быть, я добавляю в контейнер информацию о мере массива.

Описание базового контейнера

public class TContainerBase { public short Tag { get; protected set; } public int ArrayRank { get; protected set; } public List<int> ArrayDimension { get; protected set; } .



Я специально не делил на контейнер типа коллекции и контейнер типа объекта.

Все данные представлены в виде набора.

Это и есть массив, если объект обозначает ранг и размерность нулями.

Теперь нам нужен контейнер, в который будет восстановлен сам объект. Контейнер

public class TContainer : TContainerBase { public int Size { get; protected set; } public List<object> List { get; protected set; } .



Дальше.

Со структурами данных покончено.

Теперь вам нужно их заполнить.

Сначала карта типов и карта объектов.

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

Это на тот случай, если, например, вам нужно добавить еще один формат. Тот же json или xml. Читай пиши

public abstract class TReaderBase { public abstract T Read<T>(byte[] bytes, Assembly assembly); } public abstract class TWriterBase { public abstract byte[] Write(TMap map, TData data); }

Возможно, позже у них появятся дополнительные методы для записи в поток и чтения из потока.

Но пока в этом нет необходимости.

Теперь давайте склеим все это в один класс.

Т-сериализация

public class TSerizalization { protected TMap map; protected TWriterBase writer; protected TReaderBase reader; public TSerizalization() { writer = new TBinaryWriter(); reader = new TBinaryReader(); } public virtual byte[] Serialize(object target, bool callBeforeSerializationMethods = false); public virtual T Deserialize<T>(byte[] bytes, Assembly assembly, bool callAfterDeserializationMethods = false); protected virtual TData Read(object obj) }



Тест

Готовый.

Теперь перейдем к тестам.

Остерегайтесь множества наборов данных.

Тест

public interface IClass { } [System.Serializable ] public class TestC : IClass { [To2dnd.TDataMember] public int a = 10; [To2dnd.TDataMember] public int b = 12; [To2dnd.TDataMember] public string s= "Hello World"; [To2dnd.TDataMember] public State state = State.Close; [To2dnd.TDataMember] public DateTime dt = new DateTime(); [To2dnd.TDataMember] public Type type = typeof(IClass); [To2dnd.TDataMember] public string[,] arr = new string[,] { {"1111", "2222", "3333", "4444" }, {"aaaa", "bbbb", "cccc", "dddd" }, {"321", "32", "2qfs", "12f" } }; [To2dnd.TDataMember] public object classD = new TestC2(); [To2dnd.TDataMember] public TestC1[] array1 = new TestC1[] { new TestC1(), new TestC2(), new TestC2() }; [To2dnd.TDataMember] public ArrayList arr2 = new ArrayList( new string[]{ "list1", "list2" }); [To2dnd.TDataMember] public List<string> list = new List<string>() { "list Item 1", "List Item 2" }; [To2dnd.TDataMember] public Dictionary<string, int> dic = new Dictionary<string, int>() { {"one", 1}, {"two", 2}, {"three", 3}, {"four", 4} }; [To2dnd.TDataMember] public Hashtable ht = new Hashtable() { {"H one", 1}, {"H two", 2}, {"H three", 3}, {"H four", 4} }; [To2dnd.TDataMember] public SortedList<string, int> sl = new SortedList<string, int>() { {"S one", 1}, {"S two", 2}, {"S three", 3}, {"S four", 4} }; [To2dnd.TDataMember] public Dictionary<string, List<string>> dic3 = new Dictionary<string,List<string>>() { {">> 1", new List<string>(){"a1", "a2", "a3"} }, {">> 2", new List<string>(){"b1", "b2", "b3"} }, {">> 3", new List<string>(){"c1", "c2", "c3"} } }; [To2dnd.TDataMember] public List<List<string>> l = new List<List<string>>() { new List<string>(){"a1", "a2", "a3"}, new List<string>(){"b1", "b2", "b3"}, }; [ProtoMember(16)] public Dictionary<string, Dictionary<string, string>> dic4 = new Dictionary<string, Dictionary<string, string>>() { {">> 1", new Dictionary<string, string>() { { "a1", "a2"}, { "a2", "a3"} } }, {">> 2", new Dictionary<string, string>() { { "a1", "a2"}, { "a2", "a3"} } } }; [ProtoMember(17)] public Dictionary<string, Dictionary<string, string>> Dic4 {get; protected set;} [ProtoMember(18)] public Dictionary<string, object> dic333 = new Dictionary<string, object>() { {":@", new List<string>(){"1", "2", "3"}}, {":@2", new TestC2()}, {":@222", "sff"} }; public TestC() { Dic4 = new Dictionary<string,Dictionary<string,string>>() { {">> 1", new Dictionary<string, string>() { { "a1", "a2"}, { "a2", "a3"} } }, {">> 2", new Dictionary<string, string>() { { "a1", "a2"}, { "a2", "a3"} } } }; } } [Serializable] public class TestC1 : IClass { [To2dnd.TDataMember] public float value1 = 10; [To2dnd.TDataMember] public float value2 = 12; } [Serializable ] public class TestC2 : TestC1 { [To2dnd.TDataMember] public float a1 = 10; [To2dnd.TDataMember] public float b2 = 12; [To2dnd.TDataMember] public string str = "Class 1"; [To2dnd.TDataMember] public State state = State.Close; public TestC2() { } [TAfterDeserialization] public void After() { } [TBeforeSerialization] public void Before() { } } public class TestC33 { [To2dnd.TDataMember] public float b2 = 12; [To2dnd.TDataMember] public TestC2 tt = new TestC2(); [ To2dnd.TDataMember] public TestC1[] array1 = new TestC1[] { new TestC1(), new TestC2(), new TestC2() }; [To2dnd.TDataMember] public object classD = new TestC2(); [To2dnd.TDataMember] public Type type = typeof(IClass); }

Тестовое видео:

Нижняя граница

По размеру файла он, конечно, проигрывает Protobuf и messagepack. Ведь я сохраняю карту типов и не использую хитрые приемы со сдвигом битов или преобразованиями строк «byte[] bytes = Encoding.UTF7.GetBytes((string)data.value)».

Это дополнительная нагрузка, возможно позже я ее расширим в виде вариативности.

Протестирован обмен данными между Photon и Unity. Работает так, как ожидалось.

В конце концов, я создаю тип относительно сборки, которая является параметром метода Deserialize. Готовые решения, которых так много в Интернете, не соответствовали требованиям.

Поэтому нам пришлось изобретать велосипед. Что стоило потраченного на него времени.

Его можно расширять и улучшать.



Заключение

Если вы используете примитивные типы, то вам подойдет любой из рассмотренных сериализаторов.

Из примитивов я бы все же предпочел Protobuf. Но для сложных типов данных не всегда подходят готовые решения.



Ссылки

Протобуф Юнити3д Json Программа форматирования двоичных файлов MSDN Пакет сообщений Бечмарки Теги: #unity3d #сериализация #binary #photon #gamDev #Разработка игр #C++ #unity
Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.