当前位置: 首页 > article >正文

2025-03-21 Unity 序列化 —— 自定义2进制序列化

文章目录

  • 前言
  • 1 项目结构
    • 1.1 整体
    • 1.2 代码
  • 2 实现
    • 2.1 Processor
      • 2.1.1 BaseType
      • 2.1.2 CollectionType
      • 2.1.3 CustomType
    • 2.2 ByteFormatter
    • 2.3 ByteHelper
  • 3 使用

前言

​ BinaryFormatter 类可以将 C# 类对象快速转换为字节数组数据。

​ 在网络开发时,不会使用 BinaryFormatter 进行数据序列化和反序列化。因为客户端和服务端的开发语言多数情况下不同,BinaryFormatter 序列化的数据无法兼容其它语言。

​ 因此,需要自定义序列化方式。

​ BinaryFormatter 参考链接:2023-05-27 Unity 2进制4——类对象的序列化与反序列化_unity 二进制序列化-CSDN博客。

  • 项目链接:https://github.com/zheliku/ByteHelper。
  • Unity 版本:6000.0.42f1。

1 项目结构

1.1 整体

image-20250321060632469
  • Scenes(示例场景)
  • Scripts(脚本)
    • ByteHelper(工具脚本)
    • Test(测试脚本)

1.2 代码

image-20250321060653045
  • Processor(存放对应类型的 Processor)
    • BaseType(基本类型的 Processor)
    • CollectionType(集合类型的 Processor)
    • CustomType(自定义类型的 Processor)
    • Processor.cs(抽象基类,用于处理对象和字节数组之间的转换)
  • ByteFormatter.cs(存储所有 Processor,并依据类型进行 Write 与 Read)
  • ByteHelper.cs(封装序列化 API)
  • ReflectionExtension.cs(反射方法拓展)

2 实现

2.1 Processor

​ 一个 Processor 用于处理一种类型的序列化,其包含以下 3 种方法:

  • GetBytesLength

    获取对象在字节数组中占用的字节数。

  • Write

    将对象写入 bytes 数组。

  • Read

    从 bytes 数组中读取对象。

// 抽象类 Processor,用于处理对象和字节数组之间的转换
public abstract class Processor
{
    public abstract int GetBytesLength(object value);

    public abstract int Write(byte[] bytes, object value, int index);

    public abstract int Read(byte[] bytes, int index, out object value);
}

​ 使用泛型版本标识每个 Processor 处理的类型:

public abstract class Processor<TValue> : Processor
{
    public abstract int GetBytesLength(TValue value);

    public abstract int Write(byte[] bytes, TValue value, int index);

    public abstract int Read(byte[] bytes, int index, out TValue value);

    public override int GetBytesLength(object value)
    {
        return GetBytesLength((TValue) value);
    }

    public override int Write(byte[] bytes, object value, int index)
    {
        return Write(bytes, (TValue) value, index);
    }

    public override int Read(byte[] bytes, int index, out object value)
    {
        int result = Read(bytes, index, out TValue typedValue);
        value = typedValue;
        return result;
    }
}

2.1.1 BaseType

​ 以 int、string 类型为例:

IntProcessor

  • GetBytesLength

    int 类型使用 4 个子节存储,可直接返回 4,也可使用 sizeof(int)

  • Write

    直接转换为子节,写入 bytes 中的 index 位置。返回值为写入后下一处的位置。

  • Read

    直接将 bytes 中 index 位置的数据读取。返回值为读取后下一处的位置。

public class IntProcessor : Processor<int>
{
    public override int GetBytesLength(int value)
    {
        return sizeof(int);
    }

    public override int Write(byte[] bytes, int value, int index)
    {
        BitConverter.GetBytes(value).CopyTo(bytes, index);
        return index + sizeof(int);
    }

    public override int Read(byte[] bytes, int index, out int value)
    {
        value = BitConverter.ToInt32(bytes, index);
        return index + sizeof(int);
    }
}

StringProcessor

​ string 类型长度可变,因此需要先写入长度(int 类型),再写入内容。

  • GetBytesLength:int 长度 + 字符串长度。
  • Write:先写入长度,后写入内容。
  • Read:先读取长度,后读取内容。
public class StringProcessor : Processor<string>
{
    public override int GetBytesLength(string value)
    {
        return sizeof(int) + value.Length;
    }

    public override int Write(byte[] bytes, string value, int index)
    {
        BitConverter.GetBytes(value.Length).CopyTo(bytes, index);
        Encoding.UTF8.GetBytes(value).CopyTo(bytes, index + sizeof(int));
        return index + sizeof(int) + value.Length;
    }

    public override int Read(byte[] bytes, int index, out string value)
    {
        int length = BitConverter.ToInt32(bytes, index);
        value = Encoding.UTF8.GetString(bytes, index + sizeof(int), length);
        return index + sizeof(int) + length;
    }
}

2.1.2 CollectionType

​ 以 ICollectionProcessor 为例,泛型集合类型需要记录集合本身的 Type 与元素的 Type,因此 ICollectionProcessor 具有 2 个泛型参数。

  • GetBytesLength

    集合长度可变,因此也先写入长度,后顺序写入集合元素。

  • Write

    先写入长度,后顺序写入集合元素。

    使用 ByteFormatter.Write 方法,依据元素类型,自动调用对应的 Processor 写入内容。

  • Read

    先读取长度,后顺序读取集合元素。

    使用 ByteFormatter.Read 方法,依据元素类型,自动调用对应的 Processor 读取内容。

public class ICollectionProcessor<TCollection, TValue> : Processor<TCollection> where TCollection : ICollection<TValue>
{
    public override int GetBytesLength(TCollection value)
    {
        var length = sizeof(int);
        foreach (var item in value)
        {
            length += ByteFormatter.GetBytesLength(item);
        }
        return length;
    }

    public override int Write(byte[] bytes, TCollection value, int index)
    {
        int count = value.Count;

        // 写长度
        BitConverter.GetBytes(count).CopyTo(bytes, index);

        index += sizeof(int); // 留 1 个 int 位置用于写长度

        // 写内容
        foreach (var item in value)
        {
            index = ByteFormatter.Write(bytes, item, index);
        }

        return index;
    }

    public override int Read(byte[] bytes, int index, out TCollection value)
    {
        // 1. 读取长度(元素数量)
        int length = BitConverter.ToInt32(bytes, index);
         index += sizeof(int);

        // 2. 读取内容
        value = (TCollection) Activator.CreateInstance(typeof(TCollection));
        for (int i = 0; i < length; i++)
        {
            index = ByteFormatter.Read(bytes, index, typeof(TValue), out var item);
            value.Add((TValue) item);
        }

        return index;
    }
}

2.1.3 CustomType

​ 自定义类型默认序列化所有的字段,且需要添加 [ByteSerializable] 特性。

  • GetBytesLength

    长度可变,因此也先写入长度,后顺序写入字段。

  • Write

    先写入长度,后使用反射获取所有字段信息,依次写入字段内容。

    反射方法 value.GetFieldValues() 在 ReflectionExtension.cs 中封装,默认获取所有字段的值。

    使用 ByteFormatter.Write 方法,依据字段类型,自动调用对应的 Processor 写入。

  • Read

    先读取长度,后使用反射获取所有字段信息,依次读取字段内容。

    反射方法 obj.GetFieldInfos() 在 ReflectionExtension.cs 中封装,默认获取所有字段的信息。

    使用 ByteFormatter.Read 方法,依据字段类型,自动调用对应的 Processor 读取。

    考虑到值类型,在读取时需要装箱,否则反射赋值时,会将内容赋值给 fieldInfo.SetValue 方法中的临时变量。

public class CustomTypeProcessor<TValue> : Processor<TValue>
{
    public override int GetBytesLength(TValue value)
    {
        return sizeof(int) + value.GetFieldValues().Sum(v => ByteFormatter.GetBytesLength(v));
    }

    public override int Write(byte[] bytes, TValue value, int index)
    {
        // 先写长度,以便读取
        BitConverter.GetBytes(GetBytesLength(value)).CopyTo(bytes, index);
        index += sizeof(int);

        var fieldValues = value.GetFieldValues();
        foreach (var fieldValue in fieldValues)
        {
            index = ByteFormatter.Write(bytes, fieldValue, index);
        }

        return index;
    }

    public override int Read(byte[] bytes, int index, out TValue value)
    {
        var obj = (object) Activator.CreateInstance<TValue>(); // 装箱,以防 TValue 为值类型
        index += sizeof(int);

        var fieldInfos = obj.GetFieldInfos();
        foreach (var fieldInfo in fieldInfos)
        {
            index = ByteFormatter.Read(bytes, index, fieldInfo.FieldType, out var v);
            fieldInfo.SetValue(obj, v);
        }

        value = (TValue) obj; // 拆箱,还原 value

        return index;
    }
}

2.2 ByteFormatter

​ 管理所有类型的 Processor,使用字典存储预置基础类型:

public class ByteFormatter
{
    // 定义字典,用于存储预置类型的处理器
    public static readonly Dictionary<Type, object> PRIMITIVE_PROCESSORS = new Dictionary<Type, object>()
    {
        { typeof(int), new IntProcessor() },
        { typeof(short), new ShortProcessor() },
        { typeof(long), new LongProcessor() },
        { typeof(float), new FloatProcessor() },
        { typeof(double), new DoubleProcessor() },
        { typeof(bool), new BoolProcessor() },
        { typeof(char), new CharProcessor() },
        { typeof(byte), new ByteProcessor() },
        { typeof(string), new StringProcessor() },
    };

    ...
}

​ 依据传入类型,提供对应的 Processor,若不存在则创建。

  1. 在预置类型的字典中,直接读取
public class ByteFormatter
{
    // 根据类型获取对应的处理器
    public static Processor<T> GetProcessor<T>()
    {
        return (Processor<T>) GetProcessor(typeof(T));
    }

    public static Processor GetProcessor(Type type)
    {
        // 在预置类型的字典中,直接读取
        if (PRIMITIVE_PROCESSORS.TryGetValue(type, out object value)) 
        {
            return (Processor) value;
        }
    }
}
  1. 优先处理字典类型,并且先处理 KeyValuePair。

    若字典中存在,则直接读取,否则创建 Processor 添加到字典中再返回。

// 如果类型是 KeyValuePair,则使用 KeyValuePairProcessor
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
    var processorType = typeof(KeyValuePairProcessor<,>).MakeGenericType(type.GetGenericArguments());
    var processor     = (Processor) Activator.CreateInstance(processorType);
    PRIMITIVE_PROCESSORS.Add(type, processor); // 添加到字典中
    return processor;
}

// 如果类型是字典,则使用 IDictionaryProcessor
if (type.IsAssignableToGenericInterface(typeof(IDictionary<,>)))
{
    var processorType = typeof(IDictionaryProcessor<,,>).MakeGenericType(type, type.GetGenericArguments()[0], type.GetGenericArguments()[1]);
    var processor     = (Processor) Activator.CreateInstance(processorType);
    PRIMITIVE_PROCESSORS.Add(type, processor); // 添加到字典中
    return processor;
}
  1. 对于集合类型与用户自定义类型,同理。

    对于用户自定义类型,需要继承 [ByteSerializable] 特性。也可以根据需要自定义其他方式。

// 如果是集合类型,则使用 ICollectionProcessor
if (type.IsAssignableToGenericInterface(typeof(ICollection<>)))
{
    var processorType = typeof(ICollectionProcessor<,>).MakeGenericType(type, type.GetGenericArguments()[0]);
    var processor     = (Processor) Activator.CreateInstance(processorType);
    PRIMITIVE_PROCESSORS.Add(type, processor); // 添加到字典中
    return processor;
}

// 如果拥有 ByteSerializableAttribute 特性,则使用 CustomTypeProcessor
if (type.HasAttribute<ByteSerializableAttribute>())
{
    var processorType = typeof(CustomTypeProcessor<>).MakeGenericType(type);
    var processor     = (Processor) Activator.CreateInstance(processorType);
    PRIMITIVE_PROCESSORS.Add(type, processor); // 添加到字典中
    return processor;
}

​ 提供 Write 和 Read 方法,依据参数类型,自动获取对应 Processor 处理。

public static int Write<T>(byte[] bytes, T value, int index)
{
    return GetProcessor<T>().Write(bytes, value, index);
}

public static int Write(byte[] bytes, object value, int index)
{
    return GetProcessor(value.GetType()).Write(bytes, value, index);
}

public static int Read<T>(byte[] bytes, int index, out T value)
{
    return GetProcessor<T>().Read(bytes, index, out value);
}

public static int Read(byte[] bytes, int index, Type type, out object value)
{
    return GetProcessor(type).Read(bytes, index, out value);
}

2.3 ByteHelper

​ 封装序列化方法。

public class ByteHelper
{
    public const string EXTENSION = ".bytes";

    public static string BinarySavePath { get; set; } = Application.persistentDataPath + "/Binary";

    public static byte[] Serialize<TData>(TData data)
    {
        var processor = ByteFormatter.GetProcessor<TData>();
        var bytes     = new byte[processor.GetBytesLength(data)];
        processor.Write(bytes, data, 0);
        return bytes;
    }
    
    public static TData Deserialize<TData>(byte[] bytes)
    {
        ByteFormatter.Read<TData>(bytes, 0, out var value);
        return value;
    }

    public static void SaveBytes<TData>(string filePath, TData data, string extension = EXTENSION)
    {
        string fullPath = Path.Combine(BinarySavePath, filePath);
        fullPath = Path.ChangeExtension(fullPath, EXTENSION);

        var directory = Path.GetDirectoryName(fullPath);
        if (directory != null && !Directory.Exists(directory))
        {
            Directory.CreateDirectory(directory);
        }

        byte[] bytes = Serialize(data);
        File.WriteAllBytes(fullPath, bytes);

#if UNITY_EDITOR
        UnityEditor.AssetDatabase.Refresh();
#endif
    }

    public static TData LoadBytes<TData>(string filePath, string extension = ByteHelper.EXTENSION)
    {
        string fullPath = Path.Combine(BinarySavePath, filePath);
        fullPath = Path.ChangeExtension(fullPath, EXTENSION);

        if (!File.Exists(fullPath))
        { // 不存在文件,则警告,并返回默认值
            Debug.LogWarning($"ByteHelper: Can't find path \"{fullPath}\"");
            return default(TData);
        }

        byte[] bytes = File.ReadAllBytes(fullPath);
        return Deserialize<TData>(bytes);
    }
}

3 使用

​ 以 CustomType 为例,打开 CustomType 场景,点击 “TestScript” 物体,右侧编辑你想要的数据。

image-20250321064638712

​ 运行场景,会自动序列化数据,对应脚本如下:

[Serializable] [ByteSerializable]
public class CustomData
{
    public  int        Id;
    private string     _name;
    public  List<int>  List = new List<int>();
    public  NestedData NestedData; // Supports nested types

    public string Name
    {
        get => _name;
        set => _name = value;
    }

    public override string ToString() {...}
}

[Serializable] [ByteSerializable]
public struct NestedData
{
    public bool Bool;

    public override string ToString() {...}
}

public class Test_CustomType : MonoBehaviour
{
    public Button Btn;

    public Text Text;

    [Header("Set Your CustomData")]
    public CustomData CustomData;

    public NestedData NestedData;

    private void Start()
    {
        CustomData.Name = "zheliku"; // 设置私有字段

        SerializeData();

        Text.text = "Serialize Data Success!";

        Btn.onClick.AddListener(OnClick);
    }

    // 序列化数据,保存到 CustomType 目录下。
    private void SerializeData()
    {
        ByteHelper.SaveBytes($"CustomType/{nameof(CustomData)}", CustomData);
        ByteHelper.SaveBytes($"CustomType/{nameof(NestedData)}", NestedData);
    }

    // 点击按钮时,显示反序列化数据
    public void OnClick()
    {
        Text.text = "CustomData: " + ByteHelper.LoadBytes<CustomData>($"CustomType/{nameof(CustomData)}") + "\n\n" +
                    "NestedData: " + ByteHelper.LoadBytes<NestedData>($"CustomType/{nameof(NestedData)}");
    }
}

​ 点击按钮,即可显示反序列化数据:

image-20250321065016604

​ 点击 “ByteHelper/Open Binary Folder” 可打开序列化数据存储目录。

image-20250321065044534

​ CustomType 保存在 “CustomType” 目录下。

image-20250321065216116

​ CustomData 的序列化内容如下:

image-20250321065254361

​ 根据需要可自行扩展 Processor。


http://www.kler.cn/a/598483.html

相关文章:

  • 面试常问系列(一)-神经网络参数初始化
  • NLP高频面试题(十一)——RLHF的流程有哪些
  • ModuleNotFoundError: No module named ‘flask‘ 错误
  • 堆的相关要点以及模拟实现
  • 《可爱风格 2048 游戏项目:HTML 实现全解析》
  • 前后端开发概述:架构、技术栈与未来趋势
  • Linux系统移植篇(十)根文件系统构建 V3 - Yocto
  • 第8章:Docker数据持久化与卷管理
  • 基于Matlab的大气湍流光束传输特性的研究
  • Android Compose 层叠布局(ZStack、Surface)源码深度剖析(十三)
  • Android 根据Url使用Retrofit框架进行文件下载
  • 从复杂到集成:APVSG系列多通道相参矢量信号源重塑量子比特(Qubit )信号生成技术
  • qt 对QObject::tr()函数进行重定向
  • Haption Virtuose力反馈设备在CAVE投影系统中提供真实训练交互
  • 基于虚拟知识图谱的语义化决策引擎
  • 机器人前沿技术的发展与展望
  • 跨平台RTSP高性能实时播放器实现思路
  • 使用Pygame构建贪吃蛇游戏:从零开始的Python游戏开发之旅
  • 【Vue3入门2】02-记事本案例
  • ISIS-1 ISIS概述