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.pswp.cn/diannao/74281.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/74281.shtml
英文地址,請注明出處:http://en.pswp.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;項目概覽 文件結構示…