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; // 拆箱,还原 valuereturn 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_EDITORUnityEditor.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 typespublic 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.mzph.cn/diannao/74281.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

为WordPress自定义一个留言板

要在WordPress中创建一个留言反馈表单&#xff0c;并实现后台管理功能&#xff0c;您可以按照以下步骤进行操作&#xff1a; 1. 创建留言反馈表单 首先&#xff0c;您需要使用一个表单插件来创建表单。推荐使用 Contact Form 7 或 WPForms。以下是使用 Contact Form 7 的示例…

嵌入式项目:利用心知天气获取天气数据实验方案

【实验目的】 1、利用心知天气服务器获取指定位置天气数据 2、将天气数据解析并可视化显示到OLED屏幕 【实验原理】 【实验步骤】 官网注册

go-zero学习笔记

内容不多&#xff0c;只有部分笔记&#xff0c;剩下的没有继续学下去&#xff0c;包括路由与处理器、日志中间件、请求上下文 文章目录 1、go-zero核心库1.1 路由与处理器1.2 日志中间件1.3 请求上下文 1、go-zero核心库 1.1 路由与处理器 package mainimport ("github…

【Go】Go语言继承-多态模拟

继承&#xff08;结构体嵌入&#xff09;多态&#xff08;接口实现和空接口&#xff09; 1. 继承&#xff08;结构体嵌入&#xff09; Go 语言没有传统的面向对象的继承机制&#xff0c;但可以通过“结构体嵌入”实现类似继承的效果。 结构体嵌入&#xff1a;在结构体中嵌入另…

kotlin知识体系(四) : inline、noinline、crossinline 关键字对应编译后的代码是怎样的 ?

kotlin中inline、noinline、crossinline 关键字的作用 在 Kotlin 里&#xff0c;inline、noinline 和 crossinline 这几个关键字和高阶函数紧密相关&#xff0c;它们能够对高阶函数的行为进行优化和控制。下面为你详细阐述它们的作用和原理。 inline 关键字 inline 关键字用…

LabVIEW FPGA与Windows平台数据滤波处理对比

LabVIEW在FPGA和Windows平台均可实现数据滤波处理&#xff0c;但两者的底层架构、资源限制、实时性及应用场景差异显著。FPGA侧重硬件级并行处理&#xff0c;适用于高实时性场景&#xff1b;Windows依赖软件算法&#xff0c;适合复杂数据处理与可视化。本文结合具体案例&#x…

深度解析 Android Matrix 变换(二):组合变换 pre、post

前言 在上一篇文章中&#xff0c;我们讲解了 Canvas 中单个变换的原理和效果&#xff0c;即缩放、旋转和平移。但是单个旋转仅仅是基础&#xff0c;Canvas 变换最重要的是能够随意组合各种变换以实现想要的效果。在这种情况下&#xff0c;就需要了解如何组合变换&#xff0c;以…

Java并发编程之CountDownLatch

1. 基本原理 计数器 CountDownLatch 在创建时需要指定一个初始计数值。这个值通常代表需要等待完成的任务数或线程数。 等待与递减 等待&#xff1a;调用 await() 方法的线程会被阻塞&#xff0c;直到计数器变为 0。递减&#xff1a;每当一个任务完成后&#xff0c;应调用 cou…

C++|GLog开源库的使用 如何实现自定义类型消息日志

参考&#xff1a; C glog使用教程与代码演示 C第三方日志库Glog的安装与使用超详解 GLOG从入门到入门 glog 设置日志级别_glog C版本代码分析 文章目录 日志等级自定义消息创建使用宏定义 日志等级 在 glog 中&#xff0c;日志的严重性是通过 LogSeverity 来区分的&#xff0c…

FAQ - VMware vSphere Web 控制台中鼠标控制不了怎么办?

问题描述 在VMware vSphere vCenter Server 的 Web 控制台中新建了一台 Windows Server 2008 R2 虚拟机&#xff0c;但是鼠标进入控制台后&#xff0c;可以看见鼠标光标&#xff0c;但是移动却没有反应。 根因分析 暂无。 解决方案 选中虚拟机>操作>编辑设置>添加新…

Rust+WebAssembly:开启浏览器3D渲染新时代

引言 在当今的 Web 开发领域&#xff0c;随着用户对网页交互体验的要求日益提高&#xff0c;3D 渲染技术在 Web 应用中的应用愈发广泛。从沉浸式的 Web 游戏&#xff0c;到逼真的虚拟展示场景&#xff0c;3D 渲染引擎承担着将虚拟 3D 世界呈现到用户浏览器中的关键任务。其性能…

在小米AX6000中添加tailscale monitor

经过测试&#xff0c;发现小米路由器中的tailscale可能会因为某种原因状态异常&#xff0c; 为了让tailscale恢复正常&#xff0c;所以又写了monitor用来监控&#xff1a; #!/bin/sh# Define Tailscale related paths TAILSCALED_PATH"/tmp/tailscale/tailscale_1.80.3_a…

表达式括号匹配(stack)(信息学奥赛一本通-1353)

【题目描述】 假设一个表达式有英文字母&#xff08;小写&#xff09;、运算符&#xff08;&#xff0c;—&#xff0c;∗&#xff0c;/&#xff09;和左右小&#xff08;圆&#xff09;括号构成&#xff0c;以“ ”作为表达式的结束符。请编写一个程序检查表达式中的左右圆括号…

IM 基于 WebRtc 视频通信功能

IM&#xff08;即时通讯&#xff09;基于 WebRTC&#xff08;Web Real-Time Communication&#xff0c;网页实时通讯&#xff09; 原理 WebRTC 是一种支持网页浏览器进行实时语音通话或视频通话的技术&#xff0c;它提供了一组 JavaScript API&#xff0c;使得在浏览器之间无…

关于极端场景下,数据库更新与 MQ 消息一致性保障方案的详细总结

目录 一、核心问题场景 二、RocketMQ 事务消息方案 1. 核心机制 2. 执行流程 3. 关键优势 4. 局限性 三、消息表方案 1. 核心机制 2. 执行流程 3. 关键优势 4. 局限性 四、方案对比与选择 五、实施建议 六、总结 一、核心问题场景 当数据库更新后,若 MQ 消息未…

【设计模式】单件模式

七、单件模式 单件(Singleton) 模式也称单例模式/单态模式&#xff0c;是一种创建型模式&#xff0c;用于创建只能产生 一个对象实例 的类。该模式比较特殊&#xff0c;其实现代码中没有用到设计模式中经常提起的抽象概念&#xff0c;而是使用了一种比较特殊的语法结构&#x…

【redis】主从复制:拓扑结构、原理和psync命令解析

文章目录 拓扑一主一从相关问题 一主多从相关问题 树形主从结构相关问题 主从复制原理复制流程 psync 命令命令解析replicatonidoffset总结 运行流程 拓扑 若干个节点之间按照什么样的方式来进行组织连接 一主一从 都可以读&#xff0c;从节点可以帮主节点分担一部分的压力只…

[RoarCTF 2019]Easy Calc-3.23BUUCTF练习day5(2)

[RoarCTF 2019]Easy Calc-3.23BUUCTF练习day5(2) 解题过程 查看源码 发现calc.php页面&#xff0c;访问一下 分析代码 首先获取$_GET[num]的值并赋给变量$str。然后定义了一个黑名单数组$blacklist&#xff0c;包含了一系列被禁止的字符或转义字符&#xff0c;如空格、制表…

阻塞队列:原理、应用及实现

阻塞队列&#xff1a;原理、应用及实现 什么是阻塞队列以生产消费者模型形象地理解阻塞队列阻塞队列实现生产消费者模型模拟实现阻塞队列实现生产消费者模型 什么是阻塞队列 阻塞队列是一种特殊且实用的队列数据结构&#xff0c;它同样遵循 “先进先出” 的原则。与普通队列不…

【开源宝藏】30天学会CSS - DAY5 第五课 脉冲动画

以下是一个完整的渐进式教程&#xff0c;拆解如何用 HTML CSS 构建“Pulsar”水波脉冲动画。通过阅读&#xff0c;你将理解每个核心属性与关键帧如何配合&#xff0c;让一个小圆不断散发动态波纹&#xff0c;并且文字始终停留在圆心。 第 0 步&#xff1a;项目概览 文件结构示…