Binary serialization in Unity3d

Binary serialization in Unity3d

606
ПОДЕЛИТЬСЯ

Сериализовать и десерилизовать данные. Столкнулся с достаточно-таки очевидной неувязкой.

Задачка
Требуется синхронизировать состояние модели и, может быть, доп классы. Есть приложение, клиент-сервер. Клиент — Unity3d сервер PhotonServer. Есть модель , которая и на клиенте и на сервере обязана быть эквивалентной.

Решение
Protobuf
В этом явный победитель — ptotobuf (употреблял proto-net 668). Разметил требуемые классы. Роскошно. Проверяю. Но! Он не поддерживает интернет-сборку, но это допустимая жертва. Все работает, маленький размер и стремительный в работе. Самое логичное решение — это применять бинарный протокол.

Как это? В один красивый момент Protobuf выплюнул екзепшен, дескать, таковой класс не найден.
Баг тщательно с примером кода.

Есть вариант скармливать Protobuf типы. Начал разными методами решать эту делему. Наиболее того, Protobuf не поддерживает многомерные массивы. Можно допустить довольно много ошибок либо запамятовать указать тот либо другой тип. Что уже не отлично.

Как ни прискорбно, но Protobuf придется в сторону. Это сработало, поэтому что меж php и Unity прогуливались достаточно-таки обыкновенные структуры данных. К слову, в один прекрасный момент пробовал применять Protobuf в связке php и Unity. Со стороны php реализация Protobuf оказалась довольно баганутой. В итоге в php и Unity употреблял json.

Message pack
Хороший итог. Размер 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;

}

Но непростой пример, который будет, дальше не осилил message pack и protobuf.

Примеры ошибок.

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[]’. Use pre-generated serializer instead. Message packPlatformNotSupportedException: On-the-fly enum serializer generation is not supported in Unity iOS. 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-netNotSupportedException: 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
Употреблял сборку JSON .NET For Unity, который есть в ассетмаркете. Но при десериализации это поле почему-то нул. Выходит тот же костыль, что и с Protobuf. Json? Здесь 2-ая неувязка: джейсон не умеет сериализовать поля типа интерфейс и абстрактные классы. В итоговом файле возникли данные о типе поля и его данные (с указанием сборки, это принципиально; почему — написано дальше). Почему бы и нет. Что применять? Не беда, воспользовавшись гуглом отыскал, как «научить» его делать это. Опять гуглю. Ведь раз обучил сериализовать, означает можно и десериализовать. Итак, Protobuf не подступает. Таковой вариант не подступает.

Результат: Json неплох для не сложных структур. Но когда есть поля типа абстрактный класс либо интерфейс, с ним появляются трудности.

XML
В хоть какой варианты xml — довольно массивный. Хоть и часть проекта на xml. К примеру, система локализации. Потому решил не разглядывать.

BinaryFormatter
Выиграл незначительно в размере, но проиграл по скорости работы. Большой размер файла, правда, не есть отлично. Употреблял LZMA. Барабанная дробь. Решил обратится к обычным средствам. Success! Разметил код, сериализуем. Не беда, пройдемся еще и компрессией. Интернет не поддерживается, беда… Допустимая жертва. Сейчас сборки.

И… Очередной FAIL. Забиндить сборки и вручную их переименовать, но сборка попадает в бинарный файл. Сейчас устроим обмен меж клиентом и сервером. В юнити своя сборка на фотоне своя. Для чего она там нужна? Можно решить через костыльный метод. Дело в том, что сборки у классов различные, хоть классы одни и те же.

Тогда я решил поначалу сформировать требования к сериализатору. Один из их Шарп сериализатор. Решил что к этому способу вернусь, просмотрел еще парочку сериализаторов. Сумел сериализовать поля типа интерфейс, но тоже прописывает сборку и не поддерживается в интернете.

Тест простого типа 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);

Требования к сериализатору
Требуемый сериализатор должен уметь:

Сериализовать в бинарный формат;
Сериализовать в маленький размер;
Сериализовать классы используя определенные сборки (при этом информация о сборке необязательно нужна в файле);
Сериализовать пользовательские классы;
Поддерживать массивы;
Работать в интернет версией юнити;
Работать с фотоном.

Сиим требованиям наиболее наименее соответствовал лишь Json и protobuf ранешней версии. Из тех сериализаторов, что я попробовал.

Свой сериализатор
Начал гуглить. Что же делать? Но безрезультатно.
Большой размер. Применять обычное решение — не чрезвычайно неплохой вариант. Трудности с платформами.
Тогда я решил, под выше написанные требования написать свой сериализатор. Почему бы и нет. Это оказалось еще эффективней, ежели в пустую разбирать и тестировать тот либо другой сериализатор.

С чего же лучше начать?

Как сохранить объекты и как их загрузить. Также маркировать и способы, которые будут вызваны до сериализации и опосля десериализации. В этом плане мне приглянулся подход protobuf-net 668. А конкретно — маркировать требуемые поля и характеристики.

Карта
Для обычных типов значение < 0 и для пользовательских соответственно > 0. Для начала необходимо сохранить карту. А конкретно — ключ и тип. Чтоб по данной карте можно было позже вернуть объект. Размер ключа int16.

} Mappublic 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);
}

Сейчас все это склеим в один класс. Но пока это не необходимо. Может быть, позже у их покажутся доп способы писать в поток и читать из потока.

T Serialization 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);
}

Видео теста:

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

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

Заключение
Для примитивов я все же предпочел бы Protobuf. Но для сложных типов данных готовые решения подходят не постоянно. Раз вы используете примитивные типы, то для вас подойдет хоть какой из рассмотренных сериализаторов.

Ссылки
Protobuf
Unity3d Json
MSDN binary formatter
Message pack
Бечмарки habrahabr.ru