🔧 Iris 网络复制系统技术分析 - 第七部分:序列化系统 (Serialization)

📍 源码位置:
Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/Serialization/
📁 实现文件:Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/Serialization/(共 57 个序列化器实现文件)
📖 7.1 序列化系统概述
🎯 什么是序列化?
想象你要给远方的朋友寄一个乐高城堡 🏰。你不可能把整个城堡塞进信封,对吧?你需要:
拆解 - 把城堡拆成一块块积木
记录 - 写下每块积木的颜色、形状、位置
打包 - 把说明书和积木装进包裹
寄送 - 通过邮局发送
重建 - 朋友按说明书重新搭建
网络序列化就是这个过程的数字版本! 🎮
🏭 Iris 序列化系统的规模
Iris 提供了一个完整的序列化器生态系统:
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ Iris 内置序列化器分类 (57个文件) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 基础数值类型 📐 几何类型 │
│ ├── IntNetSerializers ├── VectorNetSerializers │
│ ├── UintNetSerializers ├── RotatorNetSerializers │
│ ├── FloatNetSerializers ├── QuatNetSerializers │
│ ├── PackedIntNetSerializers ├── TransformNetSerializers │
│ └── EnumNetSerializers └── BoxNetSerializers │
│ │
│ 📝 字符串类型 🔗 引用类型 │
│ ├── StringNetSerializers ├── ObjectNetSerializer │
│ ├── NameNetSerializer ├── SoftObjectNetSerializers │
│ └── TextNetSerializer ├── WeakObjectNetSerializer │
│ └── NetRoleNetSerializer │
│ │
│ 📦 容器类型 🧬 特殊类型 │
│ ├── ArrayPropertyNetSerializer ├── PolymorphicNetSerializer │
│ ├── SetPropertyNetSerializer ├── GuidNetSerializer │
│ ├── MapPropertyNetSerializer ├── GameplayTagNetSerializer │
│ └── StructPropertyNetSerializer └── LastResortNetSerializer │
│ │
└─────────────────────────────────────────────────────────────────────────────┘PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 序列化的本质 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 游戏对象 (内存中) 网络数据包 (传输中) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Position: │ 序列化 → │ 01101001... │ │
│ │ (100.5,200.3,50)│ ←──────── │ 10110010... │ │
│ │ Health: 75 │ 反序列化 │ 00101110... │ │
│ │ Name: "Hero" │ └─────────────────┘ │
│ │ Rotation: 45° │ │
│ └─────────────────┘ │
│ │
│ 🎯 目标: 用最少的比特数,精确传输游戏状态 │
│ 💡 关键: 不是简单的 memcpy,而是智能压缩 + 位打包 │
└─────────────────────────────────────────────────────────────────────────────┘🆚 序列化 vs 量化:两兄弟的分工
很多新手会混淆这两个概念,让我们用一个生动的例子来区分:
概念 | 类比 | 作用 | 示例 |
|---|---|---|---|
量化 (Quantization) | 压缩照片 | 减少数据精度以节省空间 | FRotator (12 bytes) → 3×uint16 (6 bytes) |
序列化 (Serialization) | 打包快递 | 将数据转换为可传输的格式 | 3×uint16 → 比特流 |
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 量化 vs 序列化 流程图 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ FRotator (Pitch=45.5°, Yaw=90.0°, Roll=0°) ││ ┌─────────────────────────────────────────┐ ││ │ Pitch: 45.5f (4 bytes) │ ││ │ Yaw: 90.0f (4 bytes) │ 共 12 bytes ││ │ Roll: 0.0f (4 bytes) │ ││ └─────────────────────────────────────────┘ ││ │ ││ ▼ Quantize() ││ ┌─────────────────────────────────────────┐ ││ │ X: 8282 (uint16, 2 bytes) │ ││ │ Y: 16384 (uint16, 2 bytes) │ 共 6 bytes ││ │ Z: 0 (uint16, 2 bytes) │ ││ │ XYZIsNotZero: 0b011 (标志位) │ ││ └─────────────────────────────────────────┘ ││ │ ││ ▼ Serialize() ││ ┌─────────────────────────────────────────┐ ││ │ [011][0010000001011010][0100000000000000]│ ││ │ ↑ ↑ ↑ │ 共 35 bits ≈ 5 bytes ││ │ 标志 X值 Y值 │ (Z=0 被跳过!) ││ └─────────────────────────────────────────┘ ││ ││ 🎉 总节省: 12 bytes → 5 bytes = 58% 带宽节省! ││ │└─────────────────────────────────────────────────────────────────────────────┘🏗️ 7.2 FNetSerializer 结构详解
📋 核心结构定义
FNetSerializer 是 Iris 序列化系统的心脏 ❤️,它定义了一个类型如何被网络复制。每个可复制的类型都需要一个对应的序列化器。
CPP
// 源码位置: NetSerializer.h (第376-477行)struct IRISCORE_API FNetSerializer
{
// ═══════════════════════════════════════════════════════════════════════
// 📌 元信息
// ═══════════════════════════════════════════════════════════════════════
uint32 Version; // 版本号 - 修改序列化格式时递增
ENetSerializerTraits Traits; // 特性标志 - 描述序列化器的能力
const TCHAR* Name; // 序列化器名称 (调试用)
// ═══════════════════════════════════════════════════════════════════════
// 📦 核心序列化函数指针 (必须实现)
// ═══════════════════════════════════════════════════════════════════════
// 写入比特流 - 将量化数据转换为比特
NetSerializeFunction Serialize;
// 从比特流读取 - 将比特转换回量化数据
NetDeserializeFunction Deserialize;
// ═══════════════════════════════════════════════════════════════════════
// 📊 增量压缩函数指针 (可选但推荐)
// ═══════════════════════════════════════════════════════════════════════
// 基于前值的增量写入 - 只传输变化部分
NetSerializeDeltaFunction SerializeDelta;
// 增量读取 - 基于前值恢复当前值
NetDeserializeDeltaFunction DeserializeDelta;
// ═══════════════════════════════════════════════════════════════════════
// 📐 量化函数指针 (非 POD 类型必须实现)
// ═══════════════════════════════════════════════════════════════════════
// 源数据 → 量化数据 (如 FRotator → 3×uint16)
NetQuantizeFunction Quantize;
// 量化数据 → 源数据 (如 3×uint16 → FRotator)
NetDequantizeFunction Dequantize;
// ═══════════════════════════════════════════════════════════════════════
// 🔍 辅助函数指针
// ═══════════════════════════════════════════════════════════════════════
// 比较两个值是否相等 (用于脏检测)
NetIsEqualFunction IsEqual;
// 验证数据合法性 (防止作弊/错误数据)
NetValidateFunction Validate;
// ═══════════════════════════════════════════════════════════════════════
// 🧠 动态状态管理 (用于数组、字符串等容器类型)
// ═══════════════════════════════════════════════════════════════════════
// 克隆动态状态 - 深拷贝动态分配的内存
NetCloneDynamicStateFunction CloneDynamicState;
// 释放动态状态 - 释放动态分配的内存
NetFreeDynamicStateFunction FreeDynamicState;
// ═══════════════════════════════════════════════════════════════════════
// 🔗 引用收集 (用于对象引用类型)
// ═══════════════════════════════════════════════════════════════════════
// 收集网络引用 - 用于依赖管理
NetCollectNetReferencesFunction CollectNetReferences;
// ═══════════════════════════════════════════════════════════════════════
// 📝 应用函数 (选择性更新)
// ═══════════════════════════════════════════════════════════════════════
// 选择性更新目标成员 - 只更新变化的部分
NetApplyFunction Apply;
// ═══════════════════════════════════════════════════════════════════════
// ⚙️ 类型信息
// ═══════════════════════════════════════════════════════════════════════
const FNetSerializerConfig* DefaultConfig; // 默认配置
uint16 QuantizedTypeSize; // 量化类型大小 (字节)
uint16 QuantizedTypeAlignment; // 量化类型对齐 (字节)
uint16 ConfigTypeSize; // 配置类型大小
uint16 ConfigTypeAlignment; // 配置类型对齐
};🎨 函数调用流程图解
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ FNetSerializer 完整函数流程图 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────── 发送端 (服务器) ────────────────────────────┐ │
│ │ │ │
│ │ ① 游戏数据变化 │ │
│ │ ↓ │ │
│ │ ② Validate() - 验证数据合法性 │ │
│ │ ↓ (通过) │ │
│ │ ③ Quantize() - 转换为量化格式 │ │
│ │ ↓ │ │
│ │ ④ IsEqual() - 与上次发送的值比较 │ │
│ │ ↓ (不相等) │ │
│ │ ⑤ 选择序列化方式: │ │
│ │ ├─ 有前值? → SerializeDelta() - 增量序列化 │ │
│ │ └─ 无前值? → Serialize() - 全量序列化 │ │
│ │ ↓ │ │
│ │ ⑥ 写入比特流 │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ │ 网络传输 📡 │
│ ▼ │
│ ┌─────────────────────────── 接收端 (客户端) ────────────────────────────┐ │
│ │ │ │
│ │ ⑦ 从比特流读取 │ │
│ │ ↓ │ │
│ │ ⑧ 选择反序列化方式: │ │
│ │ ├─ 增量数据? → DeserializeDelta() - 增量反序列化 │ │
│ │ └─ 全量数据? → Deserialize() - 全量反序列化 │ │
│ │ ↓ │ │
│ │ ⑨ Dequantize() - 转换回游戏格式 │ │
│ │ ↓ │ │
│ │ ⑩ Apply() - 应用到游戏对象 (可能触发 RepNotify) │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🏷️ ENetSerializerTraits 特性标志详解
CPP
// 源码位置: NetSerializer.h (第346-368行)enum class ENetSerializerTraits : uint32
{
None = 0U,
// ═══════════════════════════════════════════════════════════════════════
// 🔄 IsForwardingSerializer (转发序列化器)
// ═══════════════════════════════════════════════════════════════════════
// 表示这是一个包装其他序列化器的序列化器
// 例如: TArray<T> 的序列化器会转发到 T 的序列化器
// 需要实现所有函数,即使只是转发调用
IsForwardingSerializer = 1U << 0U,
// ═══════════════════════════════════════════════════════════════════════
// 🧠 HasDynamicState (有动态状态)
// ═══════════════════════════════════════════════════════════════════════
// 表示量化类型需要动态内存分配
// 必须实现 CloneDynamicState 和 FreeDynamicState
// 典型例子: TArray, TMap, FString, FName
//
// ⚠️ 重要: 如果设置了此标志但忘记实现这两个函数,
// 会导致内存泄漏或 double-free!
HasDynamicState = 1U << 1U,
// ═══════════════════════════════════════════════════════════════════════
// 🔌 HasConnectionSpecificSerialization (连接特定序列化)
// ═══════════════════════════════════════════════════════════════════════
// 表示不同连接可能有不同的序列化结果
// 例如: 对象引用可能在不同客户端有不同的 ID
//
// ⚠️ 尽量避免!会阻止状态共享,增加 CPU 和内存开销
// 因为每个连接都需要独立的序列化缓存
HasConnectionSpecificSerialization = 1U << 2U,
// ═══════════════════════════════════════════════════════════════════════
// 🔗 HasCustomNetReference (自定义网络引用)
// ═══════════════════════════════════════════════════════════════════════
// 表示类型包含对其他网络对象的引用
// 必须实现 CollectNetReferences
// 用于依赖管理,确保引用的对象先被复制
HasCustomNetReference = 1U << 3U,
// ═══════════════════════════════════════════════════════════════════════
// ⚖️ UseSerializerIsEqual (使用序列化器的 IsEqual)
// ═══════════════════════════════════════════════════════════════════════
// 默认情况下,Iris 使用 memcmp 比较量化数据
// 设置此标志表示需要使用自定义的 IsEqual 函数
// 例如: 浮点数需要考虑 -0.0f == +0.0f
UseSerializerIsEqual = 1U << 4U,
// ═══════════════════════════════════════════════════════════════════════
// 📝 HasApply (有 Apply 函数)
// ═══════════════════════════════════════════════════════════════════════
// 表示需要选择性更新目标成员
// 而不是简单地覆盖整个值
// 用于复杂结构体的部分更新
HasApply = 1U << 5U,
};📊 函数参数结构详解
CPP
// ═══════════════════════════════════════════════════════════════════════════// 基础参数结构 - 所有函数共享// ═══════════════════════════════════════════════════════════════════════════struct FNetSerializerBaseArgs
{
// 序列化器配置 (可能为 nullptr,使用默认配置)
const FNetSerializerConfig* NetSerializerConfig;
// 变化掩码信息 (用于结构体成员级别的脏检测)
FNetSerializerChangeMaskParam ChangeMaskInfo;
};
// ═══════════════════════════════════════════════════════════════════════════// 量化参数// ═══════════════════════════════════════════════════════════════════════════struct FNetQuantizeArgs : FNetSerializerBaseArgs
{
// 📥 指向原始源数据的指针 (如 FRotator*)
NetSerializerValuePointer Source;
// 📤 指向量化状态缓冲区的指针
// ⚠️ 缓冲区包含有效但未知的旧数据,需要完全覆盖
// ⚠️ 如果有动态状态,旧的动态状态已被释放
NetSerializerValuePointer Target;
};
// ═══════════════════════════════════════════════════════════════════════════// 序列化参数// ═══════════════════════════════════════════════════════════════════════════struct FNetSerializeArgs : FNetSerializerBaseArgs
{
// 📥 指向量化数据的指针 (要写入比特流的数据)
NetSerializerValuePointer Source;
};
// ═══════════════════════════════════════════════════════════════════════════// 增量序列化参数// ═══════════════════════════════════════════════════════════════════════════struct FNetSerializeDeltaArgs : FNetSerializerBaseArgs
{
// 📥 当前量化数据
NetSerializerValuePointer Source;
// 📥 前一个确认的量化数据 (基线)
NetSerializerValuePointer Prev;
};
// ═══════════════════════════════════════════════════════════════════════════// 相等比较参数// ═══════════════════════════════════════════════════════════════════════════struct FNetIsEqualArgs : FNetSerializerBaseArgs
{
// 📥 第一个值
NetSerializerValuePointer Source0;
// 📥 第二个值
NetSerializerValuePointer Source1;
// 🏷️ 状态是否已量化
// true: Source0/Source1 指向量化数据
// false: Source0/Source1 指向源数据
bool bStateIsQuantized;
};📐 7.3 量化 (Quantization) 深入解析
🎯 量化的目的与原理
量化就像是给数据"减肥" 🏋️ —— 用更少的比特表示相同的信息,代价是损失一些精度。
📊 为什么需要量化?
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 量化的必要性分析 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 🎮 游戏场景: 角色位置 (X, Y, Z) ││ ││ ❓ 问题: 我们真的需要 float 的完整精度吗? ││ ││ IEEE 754 float: ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ 符号位(1) │ 指数(8) │ 尾数(23) │ ││ │ 精度: 约 7 位有效数字 │ ││ │ 范围: ±3.4 × 10^38 │ ││ │ 例: 1234.567890123... 存储为 1234.5679 (精度损失在第 8 位) │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ ││ 🤔 思考: ││ - 游戏地图通常只有几十公里 ││ - 玩家不会注意到 1 厘米的位置误差 ││ - 我们真的需要 ±3.4 × 10^38 的范围吗? ││ ││ ✅ 答案: 不需要!我们可以用更少的位数表示"足够好"的精度 ││ ││ 量化方案 (假设地图 10km × 10km,精度 1cm): ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ X, Y: 0 ~ 1,000,000 cm = 20 bits (2^20 = 1,048,576) │ ││ │ Z: 0 ~ 10,000 cm = 14 bits (2^14 = 16,384) │ ││ │ 总计: 54 bits vs 原始 96 bits │ ││ │ 🎉 节省: 44% 带宽! │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────────┘🔄 Rotator 量化实战案例
让我们深入分析 FRotator 是如何被量化的 —— 这是一个经典的量化案例:
CPP
// 源码位置: RotatorNetSerializers.cpp (第12-49行)template<typename RotatorType>
struct FRotatorAsShortNetSerializerBase
{
// ═══════════════════════════════════════════════════════════════════════
// 📦 量化后的类型定义
// ═══════════════════════════════════════════════════════════════════════
struct FQuantizedType
{
// 🏷️ 3 bits 标志位: 记录哪些分量非零
// bit 0: X (Pitch) 非零
// bit 1: Y (Yaw) 非零
// bit 2: Z (Roll) 非零
uint16 XYZIsNotZero;
// 📐 量化后的角度值
// 将 float 角度 (-180° ~ 180°) 压缩为 uint16 (0 ~ 65535)
// 精度: 360° / 65536 ≈ 0.0055°
uint16 X; // Pitch
uint16 Y; // Yaw
uint16 Z; // Roll
};
// 类型别名
typedef RotatorType SourceType; // FRotator (12 bytes = 3 × float)
typedef FQuantizedType QuantizedType; // 8 bytes = 4 × uint16
// 🎉 节省: 33% 内存!
// 标志位掩码常量
enum Constants : uint16
{
XDiffersMask = 1U, // bit 0: X (Pitch) 非零
YDiffersMask = 2U, // bit 1: Y (Yaw) 非零
ZDiffersMask = 4U, // bit 2: Z (Roll) 非零
};
};量化函数实现详解:
CPP
// 源码位置: RotatorNetSerializers.cpp (第200-217行)template<typename T>
void FRotatorAsShortNetSerializerBase<T>::Quantize(
FNetSerializationContext& Context,
const FNetQuantizeArgs& Args)
{
// ═══════════════════════════════════════════════════════════════════════
// 步骤 1: 获取源数据和目标缓冲区
// ═══════════════════════════════════════════════════════════════════════
const SourceType& Source = *reinterpret_cast<const SourceType*>(Args.Source);
QuantizedType& Target = *reinterpret_cast<QuantizedType*>(Args.Target);
// 使用临时变量,避免部分写入导致的问题
QuantizedType TempValue = {};
// ═══════════════════════════════════════════════════════════════════════
// 步骤 2: 使用 UE 内置的压缩函数进行量化
// ═══════════════════════════════════════════════════════════════════════
// CompressAxisToShort 将 float 角度转换为 uint16:
// - 输入: -180.0° ~ 180.0° (或 0° ~ 360°,会自动归一化)
// - 输出: 0 ~ 65535
// - 算法: ((int)(Angle * 65536.0 / 360.0)) & 0xFFFF
TempValue.X = SourceType::CompressAxisToShort(Source.Pitch);
TempValue.Y = SourceType::CompressAxisToShort(Source.Yaw);
TempValue.Z = SourceType::CompressAxisToShort(Source.Roll);
// ═══════════════════════════════════════════════════════════════════════
// 步骤 3: 设置标志位 - 记录哪些分量非零
// ═══════════════════════════════════════════════════════════════════════
// 💡 为什么要记录?
// 序列化时可以跳过零值分量,节省带宽
TempValue.XYZIsNotZero |= (TempValue.X != 0) ? XDiffersMask : uint16(0);
TempValue.XYZIsNotZero |= (TempValue.Y != 0) ? YDiffersMask : uint16(0);
TempValue.XYZIsNotZero |= (TempValue.Z != 0) ? ZDiffersMask : uint16(0);
// ═══════════════════════════════════════════════════════════════════════
// 步骤 4: 写入目标缓冲区
// ═══════════════════════════════════════════════════════════════════════
Target = TempValue;
}📊 量化精度对照表
原始类型 | 量化类型 | 精度损失 | 适用场景 | 位数节省 |
|---|---|---|---|---|
float (32 bits) | uint32 (32 bits) | 无 | 通用浮点数 | 0% (仅零值优化) |
float 角度 (32 bits) | uint16 (16 bits) | 0.0055° | Rotator 单个分量 | 50% |
FVector (96 bits) | 3×uint16 (48 bits) | 取决于范围 | 有限范围位置 | 50% |
FRotator (96 bits) | 3×uint16 + flags (51 bits) | 0.0055° | 旋转角度 | 47% |
double (64 bits) | float (32 bits) | ~0.0001% | 高精度值 | 50% |
📦 7.4 内置序列化器详解
Iris 提供了丰富的内置序列化器,覆盖了游戏开发中常见的所有数据类型。
🔢 7.4.1 整数序列化器 (IntNetSerializers)
整数序列化器是最基础也是最常用的序列化器之一。Iris 为不同大小的整数提供了优化的实现。
📋 整数序列化器家族
CPP
// 源码位置: IntNetSerializers.cpp// 有符号整数UE_NET_IMPLEMENT_SERIALIZER(FInt8NetSerializer); // int8: -128 ~ 127UE_NET_IMPLEMENT_SERIALIZER(FInt16NetSerializer); // int16: -32768 ~ 32767UE_NET_IMPLEMENT_SERIALIZER(FInt32NetSerializer); // int32: -2^31 ~ 2^31-1UE_NET_IMPLEMENT_SERIALIZER(FInt64NetSerializer); // int64: -2^63 ~ 2^63-1
// 无符号整数UE_NET_IMPLEMENT_SERIALIZER(FUint8NetSerializer); // uint8: 0 ~ 255UE_NET_IMPLEMENT_SERIALIZER(FUint16NetSerializer); // uint16: 0 ~ 65535UE_NET_IMPLEMENT_SERIALIZER(FUint32NetSerializer); // uint32: 0 ~ 2^32-1UE_NET_IMPLEMENT_SERIALIZER(FUint64NetSerializer); // uint64: 0 ~ 2^64-1📤 序列化实现 - 零值优化
CPP
// 源码位置: IntNetSerializerBase.h (第65-115行)template<typename InSourceType, typename InConfigType>
void FIntNetSerializerBase<InSourceType, InConfigType>::Serialize(
FNetSerializationContext& Context,
const FNetSerializeArgs& Args)
{
const ConfigType* Config = static_cast<const ConfigType*>(Args.NetSerializerConfig);
const uint32 BitCount = Config ? Config->BitCount : (sizeof(SourceType) * 8U);
// 获取量化值 (已转换为无符号)
const QuantizedType Value = *reinterpret_cast<const QuantizedType*>(Args.Source);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// ═══════════════════════════════════════════════════════════════════════
// 🎯 零值优化: 对于 >= 16 位的整数
// ═══════════════════════════════════════════════════════════════════════
if (BitCount >= ZeroValueOptimizationBitCount)
{
// 先写一个 bool 表示是否为零
if (Writer->WriteBool(Value == QuantizedType(0)))
{
// 零值只需要 1 bit!
return;
}
// 非零值继续写入实际数据
}
// ═══════════════════════════════════════════════════════════════════════
// 📝 写入实际值
// ═══════════════════════════════════════════════════════════════════════
if constexpr (sizeof(SourceType) <= 4)
{
// 32 位及以下: 直接写入
Writer->WriteBits(static_cast<uint32>(Value), BitCount);
}
else
{
// 64 位: 需要分两次写入 (BitStream 单次最多 32 bits)
Writer->WriteBits(static_cast<uint32>(Value), FMath::Min(BitCount, 32U));
if (BitCount > 32U)
{
Writer->WriteBits(static_cast<uint32>(Value >> 32U), BitCount - 32U);
}
}
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 整数序列化位数分析 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景: int32 Health = 0 (死亡状态) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [1] ← 只需 1 bit!(零值标志 = true) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 场景: int32 Health = 100 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [0][00000000000000000000000001100100] ← 1 + 32 = 33 bits │ │
│ │ ↑ ↑─────────────────────────────── │ │
│ │ 非零标志 实际值 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 场景: int8 Ammo = 30 (配置 BitCount = 8) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [00011110] ← 8 bits (无零值优化,因为 < 16 bits) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📦 7.4.2 打包整数序列化器 (PackedIntNetSerializers)
打包整数序列化器是一种自适应的序列化方式,根据实际值的大小动态选择使用的字节数。
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 打包整数的智能压缩 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 💡 核心思想: 小值用少量字节,大值用更多字节 ││ ││ 传统 int32 序列化: ││ - 值 = 5: [00000000 00000000 00000000 00000101] = 32 bits ││ - 值 = 1000: [00000000 00000000 00000011 11101000] = 32 bits ││ - 值 = 10^9: [00111011 10011010 11001010 00000000] = 32 bits ││ ││ 打包 int32 序列化: ││ - 值 = 5: [00][00000101] = 2 + 8 = 10 bits (节省 69%) ││ - 值 = 1000: [01][00000011 11101000] = 2 + 16 = 18 bits (节省 44%) ││ - 值 = 10^9: [11][完整 32 bits] = 2 + 32 = 34 bits (略微增加) ││ ││ 📊 字节数编码 (2 bits): ││ 00 = 1 字节 (值 < 256) ││ 01 = 2 字节 (值 < 65536) ││ 10 = 3 字节 (值 < 16777216) ││ 11 = 4 字节 (任意值) ││ │└─────────────────────────────────────────────────────────────────────────────┘🔢 7.4.3 浮点数序列化器 (FloatNetSerializers)
CPP
// 源码位置: FloatNetSerializers.cpp (第15-65行)struct FFloatNetSerializer
{
static const uint32 Version = 0;
typedef float SourceType;
typedef uint32 QuantizedType; // 💡 float 和 uint32 大小相同
typedef FNetSerializerConfig ConfigType;
// ═══════════════════════════════════════════════════════════════════════
// 📤 序列化 - 零值优化
// ═══════════════════════════════════════════════════════════════════════
static void Serialize(FNetSerializationContext& Context, const FNetSerializeArgs& Args)
{
const uint32 Value = *reinterpret_cast<const uint32*>(Args.Source);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// 🎯 零值优化: 0.0f 只需要 1 bit
// 注意: IEEE 754 中 +0.0f 的位表示是全零
if (Writer->WriteBool(Value != 0))
{
Writer->WriteBits(Value, 32U);
}
}
};🔄 7.4.4 Rotator 序列化器
CPP
// 源码位置: RotatorNetSerializers.cpp (第85-128行)template<typename T>
void FRotatorAsShortNetSerializerBase<T>::Serialize(
FNetSerializationContext& Context,
const FNetSerializeArgs& Args)
{
const QuantizedType& Value = *reinterpret_cast<const QuantizedType*>(Args.Source);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// ═══════════════════════════════════════════════════════════════════════
// 步骤 1: 写入标志位 (3 bits)
// ═══════════════════════════════════════════════════════════════════════
Writer->WriteBits(Value.XYZIsNotZero, 3U);
// ═══════════════════════════════════════════════════════════════════════
// 步骤 2: 只写入非零分量 (各 16 bits)
// ═══════════════════════════════════════════════════════════════════════
if (Value.XYZIsNotZero & XDiffersMask)
{
Writer->WriteBits(Value.X, 16U);
}
if (Value.XYZIsNotZero & YDiffersMask)
{
Writer->WriteBits(Value.Y, 16U);
}
if (Value.XYZIsNotZero & ZDiffersMask)
{
Writer->WriteBits(Value.Z, 16U);
}
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ Rotator 序列化位数分析 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景: FRotator(0, 90, 0) - 只有 Yaw 非零 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [010][0100000000000000] ← 3 + 16 = 19 bits │ │
│ │ ↑↑↑ ↑─────────────── │ │
│ │ XYZ Y值 (16384 = 90°) │ │
│ │ 标志 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 场景: FRotator(45, 90, 30) - 所有分量非零 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [111][X: 16 bits][Y: 16 bits][Z: 16 bits] ← 3 + 48 = 51 bits │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 场景: FRotator(0, 0, 0) - 所有分量为零 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [000] ← 只需 3 bits! │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 位数统计: │
│ - 最小: 3 bits (全零) │
│ - 最大: 51 bits (全非零) │
│ - 平均: ~35 bits (假设 2 个分量非零) │
│ │
│ vs 原始 float[3]: 96 bits │
│ 🎉 平均节省: 64% │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📝 7.5 自定义 NetSerializer 实现指南
🎯 实现步骤概览
创建自定义序列化器就像做一道菜 🍳:
准备食材 - 定义配置结构体
写菜谱 - 声明序列化器
做菜 - 实现序列化函数
上桌 - 注册序列化器
📋 完整示例:自定义血量序列化器
步骤 1: 定义配置 (头文件)
CPP
// HealthNetSerializer.h#pragma once
#include "Iris/Serialization/NetSerializerConfig.h"#include "HealthNetSerializer.generated.h"
// 📋 配置结构体 - 可以添加自定义参数USTRUCT()
struct FHealthNetSerializerConfig : public FNetSerializerConfig
{
GENERATED_BODY()
// 最大血量值 (用于量化)
UPROPERTY()
float MaxHealth = 1000.0f;
// 量化精度 (bits)
UPROPERTY()
uint8 QuantizationBits = 10; // 0-1023 范围
};
// 📢 声明序列化器UE_NET_DECLARE_SERIALIZER(FHealthNetSerializer, MYGAME_API);步骤 2: 实现序列化器 (源文件)
CPP
// HealthNetSerializer.cpp#include "HealthNetSerializer.h"#include "Iris/Serialization/NetBitStreamReader.h"#include "Iris/Serialization/NetBitStreamWriter.h"
namespace UE::Net
{
struct FHealthNetSerializer
{
// 📌 版本号 - 修改序列化格式时递增
static const uint32 Version = 0;
// 🎯 类型定义
typedef float SourceType; // 原始类型: float 血量
typedef uint16 QuantizedType; // 量化类型: uint16 (足够存 10 bits)
typedef FHealthNetSerializerConfig ConfigType;
// ⚙️ 默认配置
inline static const ConfigType DefaultConfig;
// ═══════════════════════════════════════════════════════════
// 📐 量化函数: float → uint16
// ═══════════════════════════════════════════════════════════
static void Quantize(FNetSerializationContext& Context, const FNetQuantizeArgs& Args)
{
const float& Source = *reinterpret_cast<const float*>(Args.Source);
uint16& Target = *reinterpret_cast<uint16*>(Args.Target);
const auto* Config = static_cast<const ConfigType*>(Args.NetSerializerConfig);
const float MaxHealth = Config ? Config->MaxHealth : 1000.0f;
const uint32 MaxValue = (1U << Config->QuantizationBits) - 1;
// 🔄 将 [0, MaxHealth] 映射到 [0, MaxValue]
const float NormalizedHealth = FMath::Clamp(Source / MaxHealth, 0.0f, 1.0f);
Target = static_cast<uint16>(NormalizedHealth * MaxValue);
}
// ═══════════════════════════════════════════════════════════
// 📦 序列化函数: 写入比特流
// ═══════════════════════════════════════════════════════════
static void Serialize(FNetSerializationContext& Context, const FNetSerializeArgs& Args)
{
const uint16 Value = *reinterpret_cast<const uint16*>(Args.Source);
const auto* Config = static_cast<const ConfigType*>(Args.NetSerializerConfig);
const uint32 BitCount = Config ? Config->QuantizationBits : 10;
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// 🎯 零值优化: 死亡状态只需 1 bit
if (Writer->WriteBool(Value != 0))
{
Writer->WriteBits(Value, BitCount);
}
}
};
// 📢 注册序列化器UE_NET_IMPLEMENT_SERIALIZER(FHealthNetSerializer);
} // namespace UE::Net步骤 3: 在游戏代码中使用
CPP
// MyCharacter.hUCLASS()
class AMyCharacter : public ACharacter
{
GENERATED_BODY()
// 使用自定义序列化器的血量属性
UPROPERTY(Replicated, meta=(NetSerializer="FHealthNetSerializer"))
float Health;
};📡 7.6 BitStream 读写详解
🎯 BitStream 的设计理念
传统网络传输按字节对齐,但 Iris 的 BitStream 按比特对齐:
PLAINTEXT
┌──────────────────────────────────────────────────────────────────┐
│ 字节对齐 vs 比特对齐 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 传统字节对齐 (每个值占整数字节): │
│ ┌────────┬────────┬────────┬────────┬────────┬────────┐ │
│ │ bool │ uint8 │ uint16 │ uint32 │ │
│ │ 8 bits │ 8 bits │ 16 bits │ 32 bits │ │
│ └────────┴────────┴────────┴────────┴────────┴────────┘ │
│ 总计: 8 + 8 + 16 + 32 = 64 bits = 8 bytes │
│ │
│ Iris 比特打包 (紧密排列): │
│ ┌─┬────────┬────────────────┬────────────────────────────────┐ │
│ │1│ 8 │ 16 │ 32 │ │
│ └─┴────────┴────────────────┴────────────────────────────────┘ │
│ 总计: 1 + 8 + 16 + 32 = 57 bits ≈ 8 bytes │
│ │
│ 💡 节省了 7 bits!在大量数据时累积效果显著 │
│ │
└──────────────────────────────────────────────────────────────────┘📝 FNetBitStreamWriter 详解
CPP
// 源码位置: NetBitStreamWriter.h (第10-108行)class FNetBitStreamWriter
{
public:
// ═══════════════════════════════════════════════════════════
// 🔧 初始化
// ═══════════════════════════════════════════════════════════
/**
* 初始化写入器
* @param Buffer 缓冲区 (必须 4 字节对齐)
* @param ByteCount 缓冲区大小 (必须是 4 的倍数)
*/
void InitBytes(void* Buffer, uint32 ByteCount);
// ═══════════════════════════════════════════════════════════
// 📝 写入操作
// ═══════════════════════════════════════════════════════════
/**
* 写入指定位数的值
* @param Value 要写入的值 (只使用低 BitCount 位)
* @param BitCount 要写入的位数
*/
void WriteBits(uint32 Value, uint32 BitCount);
/**
* 写入布尔值并返回该值
* 💡 返回值设计允许这样的用法:
* if (Writer->WriteBool(Value != 0)) { ... }
*/
inline bool WriteBool(bool Value)
{
volatile int8 ValueAsInt8 = Value;
WriteBits(ValueAsInt8 ? 1U : 0U, 1U);
return ValueAsInt8 ? true : false;
}
// ═══════════════════════════════════════════════════════════
// 🔄 提交和定位
// ═══════════════════════════════════════════════════════════
/**
* 提交待写入的数据到缓冲区
* ⚠️ 在完成所有写入后必须调用!
*/
void CommitWrites();
/**
* 定位到指定位置
* 💡 可用于回退和重写
*/
void Seek(uint32 BitPosition);
// ═══════════════════════════════════════════════════════════
// 📊 状态查询
// ═══════════════════════════════════════════════════════════
uint32 GetPosBytes() const; // 当前字节位置
uint32 GetPosBits() const; // 当前位位置
uint32 GetBitsLeft() const; // 剩余可写位数
bool IsOverflown() const; // 是否溢出
};🔄 7.7 增量压缩 (Delta Compression) 深入解析
🎯 什么是增量压缩?
想象你在发短信 📱:
全量发送: "我在北京市朝阳区建国路100号"
增量发送: "100号→101号" (只发变化的部分)
增量压缩的核心思想是:如果接收端已经知道前一个值,我们只需要发送变化的部分。
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 增量压缩原理详解 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 🎮 场景: 玩家位置每帧更新 ││ ││ 帧 N: Position.X = 1000.0f ││ 帧 N+1: Position.X = 1001.5f ← 只移动了 1.5 单位 ││ ││ ═══════════════════════════════════════════════════════════════════════ ││ 全量序列化 (每次都发完整值): ││ ═══════════════════════════════════════════════════════════════════════ ││ 帧 N: [1][0x447A0000] = 33 bits (1000.0f 的 IEEE 754 表示) ││ 帧 N+1: [1][0x447A6000] = 33 bits (1001.5f 的 IEEE 754 表示) ││ 总计: 66 bits ││ ││ ═══════════════════════════════════════════════════════════════════════ ││ 增量序列化 (基于前值): ││ ═══════════════════════════════════════════════════════════════════════ ││ 帧 N: [1][0x447A0000] = 33 bits (第一次,需要完整值) ││ 帧 N+1: [01][delta: 16 bits] = 18 bits (只发送差值!) ││ 总计: 51 bits ││ ││ 🎉 节省: (66-51)/66 ≈ 23% ││ ││ ═══════════════════════════════════════════════════════════════════════ ││ 如果值完全没变: ││ ═══════════════════════════════════════════════════════════════════════ ││ 帧 N: [1][0x447A0000] = 33 bits ││ 帧 N+1: [00] = 2 bits (相同值,只需要索引!) ││ 总计: 35 bits ││ ││ 🎉 节省: (66-35)/66 ≈ 47% ││ │└─────────────────────────────────────────────────────────────────────────────┘📊 Delta 位计数表详解
Iris 使用位计数表来智能选择 Delta 编码的位数:
CPP
// 源码位置: BitPacking.cpp (第15-35行)// 不同位宽的 Delta 压缩位计数表inline static const uint8 DeltaBitCountTable[4][3] =
{
{0, 0, 0}, // <= 8 位: 只支持相同值优化
{0, 4, 10}, // <= 16 位: 0位(相同), 4位(小变化), 10位(中等变化)
{0, 4, 14}, // <= 32 位: 0位, 4位, 14位
{0, 14, 32}, // <= 64 位: 0位, 14位, 32位
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ Delta 位计数表工作原理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 以 32 位整数为例 (使用 {0, 4, 14} 表): │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 表索引 │ Delta 位数 │ Delta 范围 │ 编码格式 │ │
│ ├────────┼───────────┼──────────────────┼──────────────────────────┤ │
│ │ 0 │ 0 bits │ delta = 0 │ [00] │ │
│ │ 1 │ 4 bits │ -8 ~ +7 │ [01][4 bits delta] │ │
│ │ 2 │ 14 bits │ -8192 ~ +8191 │ [10][14 bits delta] │ │
│ │ 超出 │ 32 bits │ 任意值 │ [11][32 bits 完整值] │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📝 编码流程: │
│ 1. 计算 delta = current - previous │
│ 2. 计算表示 delta 所需的位数 │
│ 3. 在表中找到第一个足够大的条目 │
│ 4. 写入表索引 (2 bits) + delta 值 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🎭 7.8 ChangeMask (变化掩码) 系统
ChangeMask 是 Iris 中用于追踪哪些属性发生变化的核心机制,它使得系统只需要序列化真正变化的数据。
🎯 ChangeMask 的作用
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ ChangeMask 工作原理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 🎮 场景: 一个角色有 10 个复制属性 │
│ │
│ struct FCharacterState { │
│ FVector Position; // 属性 0 │
│ FRotator Rotation; // 属性 1 │
│ float Health; // 属性 2 │
│ float MaxHealth; // 属性 3 │
│ int32 Ammo; // 属性 4 │
│ int32 MaxAmmo; // 属性 5 │
│ bool bIsSprinting; // 属性 6 │
│ bool bIsCrouching; // 属性 7 │
│ bool bIsAiming; // 属性 8 │
│ uint8 WeaponSlot; // 属性 9 │
│ }; │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ 帧 N → 帧 N+1: 只有 Position 和 Rotation 变化 │
│ ═══════════════════════════════════════════════════════════════════════ │
│ │
│ ChangeMask (10 bits): │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [1] [1] [0] [0] [0] [0] [0] [0] [0] [0] │ │
│ │ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ │ │
│ │ 0 1 2 3 4 5 6 7 8 9 │ │
│ │ ✓ ✓ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ │ │
│ │ 变化 变化 未变 未变 未变 未变 未变 未变 未变 未变 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 传统方法: 序列化所有 10 个属性 │
│ ChangeMask: 只序列化 Position 和 Rotation │
│ │
│ 💡 带宽节省: 只传输 2/10 = 20% 的数据! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📊 ChangeMask 数据结构
CPP
// 源码位置: ChangeMaskUtil.h (第15-80行)using ChangeMaskStorageType = FNetBitArrayView::StorageWordType; // uint32
// ═══════════════════════════════════════════════════════════════════════════// 🎯 智能存储: 小掩码内联,大掩码堆分配// ═══════════════════════════════════════════════════════════════════════════class FChangeMaskStorageOrPointer
{
public:
// 64 位以内的 ChangeMask 可以内联存储,避免堆分配
static constexpr bool UseInlinedStorage(uint32 BitCount)
{
return BitCount <= 64;
}
// 计算存储所需的字节数
static constexpr uint32 GetStorageSize(uint32 BitCount)
{
return FNetBitArrayView::CalculateRequiredWordCount(BitCount) * sizeof(StorageWordType);
}
// 获取存储指针
inline StorageWordType* GetPointer(uint32 BitCount)
{
// 小于等于 64 位: 直接使用内联存储 (避免堆分配!)
// 大于 64 位: 使用指针指向堆内存
uint64* Ptr = UseInlinedStorage(BitCount) ?
reinterpret_cast<uint64*>(&ChangeMaskOrPointer) :
reinterpret_cast<uint64*>(ChangeMaskOrPointer);
return reinterpret_cast<StorageWordType*>(Ptr);
}
private:
// 这个字段有双重用途:
// - 如果 BitCount <= 64: 直接存储 ChangeMask 数据
// - 如果 BitCount > 64: 存储指向堆内存的指针
uint64 ChangeMaskOrPointer;
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ ChangeMask 内存布局优化 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 小结构体 (≤64 个属性) - 内联存储 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FChangeMaskStorageOrPointer (8 bytes) │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ ChangeMask 数据 (最多 64 bits) │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ 💡 无堆分配,无指针解引用,缓存友好! │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 大结构体 (>64 个属性) - 堆分配 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FChangeMaskStorageOrPointer (8 bytes) │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ 指针 ──────────────────────────────────────────────────────→ │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 堆内存: │ │
│ │ ┌───────────────────────────────────────────────────────────────┐ │ │
│ │ │ ChangeMask 数据 (N bits) │ │ │
│ │ └───────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 💡 大多数游戏结构体属性数 < 64,可以享受内联存储的性能优势! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📦 7.9 数组序列化器深入解析 (FArrayPropertyNetSerializer)
数组是游戏中最常用的容器类型之一,Iris 为数组提供了高度优化的序列化器。
🎯 数组序列化器的设计挑战
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 数组序列化的核心挑战 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 🤔 问题 1: 数组大小可变 ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ 帧 N: [A, B, C] (3 个元素) │ ││ │ 帧 N+1: [A, B, C, D, E] (5 个元素) │ ││ │ 帧 N+2: [A, B] (2 个元素) │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ 💡 解决: 先写入元素数量,再写入元素数据 ││ ││ 🤔 问题 2: 只有部分元素变化 ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ 帧 N: [A, B, C, D, E] │ ││ │ 帧 N+1: [A, B', C, D, E] (只有 B 变化) │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ 💡 解决: 使用 ChangeMask 追踪哪些元素变化 ││ ││ 🤔 问题 3: 元素本身可能有动态状态 ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ TArray<FString> Names; // 每个 FString 都有动态分配的内存 │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ 💡 解决: 递归调用元素的 CloneDynamicState/FreeDynamicState ││ │└─────────────────────────────────────────────────────────────────────────────┘📊 数组量化类型结构
CPP
// 源码位置: ArrayPropertyNetSerializer.cpp (第26-33行)struct FQuantizedType
{
// 当前分配可以容纳的元素数量 (容量)
uint16 ElementCapacityCount;
// 实际有效的元素数量
uint16 ElementCount;
// 指向元素存储区的指针
// 每个元素的大小由 ElementStateDescriptor->InternalSize 决定
void* ElementStorage;
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 数组量化类型内存布局 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ FQuantizedType (8 bytes on 64-bit) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ElementCapacityCount: 5 (uint16) │ │
│ │ ElementCount: 3 (uint16) │ │
│ │ ElementStorage ─────────────────────────────────────────────────→ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ ElementStorage (动态分配) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [Element 0] [Element 1] [Element 2] [未使用] [未使用] │ │
│ │ ↑ ↑ ↑ ↑ ↑ │ │
│ │ 有效 有效 有效 容量内 容量内 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 💡 容量 vs 数量: │
│ - ElementCount = 3 (实际使用的元素) │
│ - ElementCapacityCount = 5 (已分配的空间) │
│ - 当数组增长时,如果 NewCount <= Capacity,无需重新分配 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📤 数组序列化流程详解
CPP
// 源码位置: ArrayPropertyNetSerializer.cpp (第79-134行)void FArrayPropertyNetSerializer::Serialize(FNetSerializationContext& Context, const FNetSerializeArgs& Args){
const ConfigType* Config = static_cast<const ConfigType*>(Args.NetSerializerConfig);
const QuantizedType& Array = *reinterpret_cast<const QuantizedType*>(Args.Source);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// ═══════════════════════════════════════════════════════════════════════
// 步骤 1: 空数组优化 - 只需 1 bit
// ═══════════════════════════════════════════════════════════════════════
if (Array.ElementCount == 0)
{
Writer->WriteBits(1U, 1U); // 写入 "是空数组" 标志
return;
}
// ═══════════════════════════════════════════════════════════════════════
// 步骤 2: 写入元素数量
// ═══════════════════════════════════════════════════════════════════════
Writer->WriteBits(0U, 1U); // 写入 "非空数组" 标志
Writer->WriteBits(Array.ElementCount, Config->ElementCountBitCount);
// ═══════════════════════════════════════════════════════════════════════
// 步骤 3: 获取元素序列化器
// ═══════════════════════════════════════════════════════════════════════
const FReplicationStateDescriptor* ElementStateDescriptor = Config->StateDescriptor;
const FReplicationStateMemberSerializerDescriptor& ElementSerializerDescriptor =
ElementStateDescriptor->MemberSerializerDescriptors[0];
const FNetSerializer* ElementSerializer = ElementSerializerDescriptor.Serializer;
const uint32 ElementSize = ElementStateDescriptor->InternalSize;
// ═══════════════════════════════════════════════════════════════════════
// 步骤 4: 序列化每个元素 (考虑 ChangeMask)
// ═══════════════════════════════════════════════════════════════════════
const FNetBitArrayView* ChangeMask = Args.ChangeMaskInfo.BitCount > 1U ?
Context.GetChangeMask() : nullptr;
if (!ChangeMask)
{
// 无 ChangeMask: 序列化所有元素
FNetSerializeArgs ElementArgs;
ElementArgs.Source = NetSerializerValuePointer(Array.ElementStorage);
for (uint32 ElementIt = 0; ElementIt < Array.ElementCount; ++ElementIt)
{
ElementSerializer->Serialize(Context, ElementArgs);
ElementArgs.Source += ElementSize;
}
}
else
{
// 有 ChangeMask: 只序列化变化的元素 (使用模运算方案)
const uint32 ChangeMaskBitOffset = Args.ChangeMaskInfo.BitOffset + 1U;
const uint32 ChangeMaskBitCount = Args.ChangeMaskInfo.BitCount - 1U;
FNetSerializeArgs ElementArgs;
ElementArgs.Source = NetSerializerValuePointer(Array.ElementStorage);
for (uint32 ElementIt = 0; ElementIt < Array.ElementCount; ++ElementIt)
{
// 模运算: 元素索引 % ChangeMask位数
if (ChangeMask->GetBit(ChangeMaskBitOffset + (ElementIt % ChangeMaskBitCount)))
{
ElementSerializer->Serialize(Context, ElementArgs);
}
ElementArgs.Source += ElementSize;
}
}
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 数组序列化位流格式 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 📊 空数组: ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ [1] ← 只需 1 bit! │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ ││ 📊 非空数组 (假设 ElementCountBitCount = 8): ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ [0][元素数量: 8 bits][元素0数据][元素1数据][元素2数据]... │ ││ │ ↑ ↑ │ ││ │ 非空 数量=3 │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ ││ 📊 增量序列化 (数组大小相同): ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ [1][元素0 delta][元素1 delta][元素2 delta]... │ ││ │ ↑ │ ││ │ 大小相同标志 │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ ││ 📊 增量序列化 (数组大小不同): ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ [0][新元素数量][现有元素 delta...][新元素全量...] │ ││ │ ↑ │ ││ │ 大小不同标志 │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────────┘🔄 数组大小调整逻辑
CPP
// 源码位置: ArrayPropertyNetSerializer.cpp (第814-847行)void FArrayPropertyNetSerializer::AdjustArraySize(
FNetSerializationContext& Context,
QuantizedType& Array,
const ConfigType* Config,
uint16 NewElementCount){
if (NewElementCount < Array.ElementCount)
{
// ═══════════════════════════════════════════════════════════════════
// 情况 1: 数组缩小
// ═══════════════════════════════════════════════════════════════════
if (NewElementCount == 0)
{
// 完全清空: 释放所有内存
FreeDynamicStateInternal(Context, Array, Config);
}
else
{
// 部分缩小: 释放被移除元素的动态状态
ShrinkDynamicStateInternal(Context, Array, Config, NewElementCount);
}
}
else if (NewElementCount > Array.ElementCapacityCount)
{
// ═══════════════════════════════════════════════════════════════════
// 情况 2: 数组增长超出容量 - 需要重新分配
// ═══════════════════════════════════════════════════════════════════
GrowDynamicStateInternal(Context, Array, Config, NewElementCount);
}
else
{
// ═══════════════════════════════════════════════════════════════════
// 情况 3: 数组增长但在容量内 - 只需更新计数
// ═══════════════════════════════════════════════════════════════════
if (NewElementCount > Array.ElementCount)
{
// 将新元素区域清零,确保初始状态一致
const SIZE_T ElementSize = Config->StateDescriptor->InternalSize;
void* ElementsToZeroOut = (void*)(NetSerializerValuePointer(Array.ElementStorage)
+ (Array.ElementCount * ElementSize));
FMemory::Memzero(ElementsToZeroOut, ElementSize * (NewElementCount - Array.ElementCount));
}
Array.ElementCount = NewElementCount;
}
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 数组大小调整策略 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 初始状态: Capacity=5, Count=3 ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ [A] [B] [C] [空] [空] │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ ││ ═══════════════════════════════════════════════════════════════════════ ││ 操作 1: 增长到 4 个元素 (在容量内) ││ ═══════════════════════════════════════════════════════════════════════ ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ [A] [B] [C] [D] [空] │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ 💡 无需重新分配,只更新 Count = 4 ││ ││ ═══════════════════════════════════════════════════════════════════════ ││ 操作 2: 增长到 8 个元素 (超出容量) ││ ═══════════════════════════════════════════════════════════════════════ ││ 旧存储: [A] [B] [C] [D] [空] ││ ↓ 分配新存储,复制数据,释放旧存储 ││ 新存储: [A] [B] [C] [D] [E] [F] [G] [H] ││ 💡 Capacity = 8, Count = 8 ││ ││ ═══════════════════════════════════════════════════════════════════════ ││ 操作 3: 缩小到 2 个元素 ││ ═══════════════════════════════════════════════════════════════════════ ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ [A] [B] [释放] [释放] [释放] [释放] [释放] [释放] │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ 💡 如果元素有动态状态,调用 FreeDynamicState ││ 💡 Count = 2, Capacity 保持不变 (避免频繁重分配) ││ │└─────────────────────────────────────────────────────────────────────────────┘📝 7.10 字符串与名称序列化器详解
🏷️ FName 序列化器 (FNameNetSerializer)
FName 是 UE 中高效的字符串标识符,Iris 为其提供了特殊优化的序列化器。
CPP
// 源码位置: StringNetSerializers.cpp (第35-52行)struct FQuantizedType
{
// 标志位
uint32 bIsString : 1U; // 是否为字符串形式 (vs 硬编码 EName)
uint32 bEncodeNumberFromIntMax : 1U; // Number 编码方式优化
uint32 bIsEncoded : 1U; // 字符串是否为 UTF-8 编码
// 如果 bIsString: 这是 Number 部分
// 如果 !bIsString: 这是 EName 枚举值
int32 ENameOrNumber;
// 字符串存储 (动态分配)
uint16 ElementCapacityCount;
uint16 ElementCount;
void* ElementStorage;
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ FName 序列化优化策略 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 硬编码名称 (EName) - 超级优化 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FName("None"), FName("Actor"), FName("Object")... │ │
│ │ │ │
│ │ 序列化格式: │ │
│ │ [0][EName 索引: ~10 bits] │ │
│ │ ↑ │ │
│ │ 非字符串标志 │ │
│ │ │ │
│ │ 💡 只需 ~11 bits!(vs 完整字符串可能需要几百 bits) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 普通名称 (字符串形式) │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ FName("MyCustomActor_123") │ │
│ │ │ │
│ │ 序列化格式: │ │
│ │ [1][Number编码方式][Number值][编码标志][字符串长度][字符串数据] │ │
│ │ ↑ │ │
│ │ 是字符串标志 │ │
│ │ │ │
│ │ Number 优化: │ │
│ │ - 如果 Number 接近 0: 直接编码 │ │
│ │ - 如果 Number 接近 MAX_int32: 用 (MAX_int32 - Number) 编码 │ │
│ │ - 选择位数更少的方案! │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 ANSI vs Wide 字符串 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ANSI 字符串: 直接存储,每字符 1 byte │ │
│ │ Wide 字符串: UTF-8 编码,每字符 1-3 bytes │ │
│ │ │ │
│ │ 💡 大多数游戏名称是 ANSI,节省 50% 空间 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📜 FString 序列化器 (FStringNetSerializer)
CPP
// FString 使用与 FName 类似的量化结构// 但没有 EName 优化,因为 FString 不支持硬编码名称
// 序列化流程:// 1. 空字符串: [1] (1 bit)// 2. 非空字符串: [0][编码标志][长度][数据]PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 字符串编码优化 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 UTF-8 编码压缩 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 原始 Wide 字符串: L"Hello世界" │ │
│ │ - Wide 存储: 5×2 + 2×2 = 14 bytes (假设每字符 2 bytes) │ │
│ │ │ │
│ │ UTF-8 编码后: │ │
│ │ - "Hello" = 5 bytes (ASCII 字符各 1 byte) │ │
│ │ - "世界" = 6 bytes (中文字符各 3 bytes) │ │
│ │ - 总计: 11 bytes │ │
│ │ │ │
│ │ 💡 对于主要是 ASCII 的字符串,节省约 50% │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 字符串验证 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 反序列化时验证: │ │
│ │ - 检查 UTF-8 编码有效性 │ │
│ │ - 检查字符串长度限制 (NAME_SIZE + 1) × 3 │ │
│ │ - 检查空终止符 │ │
│ │ │ │
│ │ 💡 防止恶意客户端发送畸形字符串导致崩溃 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🔄 7.11 Rotator 序列化器变体详解
Iris 提供了多种 Rotator 序列化器,适用于不同的精度需求:
📊 Rotator 序列化器家族
CPP
// 源码位置: RotatorNetSerializers.cpp
// 1. FRotatorNetSerializer (默认) - 使用 Short 精度struct FRotatorNetSerializer : public FRotatorAsShortNetSerializerBase<FRotator>
{
// 精度: 360° / 65536 ≈ 0.0055°
// 量化类型: 4 × uint16 = 8 bytes
};
// 2. FRotatorAsByteNetSerializer - 低精度版本struct FRotatorAsByteNetSerializer : public FRotatorAsByteNetSerializerBase<FRotator>
{
// 精度: 360° / 256 ≈ 1.41°
// 量化类型: 4 × uint8 = 4 bytes
};
// 3. FRotator3fNetSerializer - 单精度浮点版本struct FRotator3fNetSerializer : public FRotatorAsShortNetSerializerBase<FRotator3f>
{
// 用于 FRotator3f (float 版本)
};
// 4. FRotator3dNetSerializer - 双精度浮点版本struct FRotator3dNetSerializer : public FRotatorAsShortNetSerializerBase<FRotator3d>
{
// 用于 FRotator3d (double 版本)
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ Rotator 序列化器精度对比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 序列化器 │ 精度 │ 最大位数 │ 适用场景 │
│ ──────────────────────────┼──────────┼─────────┼───────────────────── │
│ FRotatorNetSerializer │ 0.0055° │ 51 bits │ 默认,大多数情况 │
│ FRotatorAsByteNetSerializer│ 1.41° │ 27 bits │ 低精度需求,如 NPC │
│ FRotator3fNetSerializer │ 0.0055° │ 51 bits │ 单精度浮点类型 │
│ FRotator3dNetSerializer │ 0.0055° │ 51 bits │ 双精度浮点类型 │
│ │
│ 📊 精度可视化: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Short 精度 (0.0055°): │ │
│ │ ┌───┬───┬───┬───┬───┬───┬───┬───┐ │ │
│ │ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │...│360│ 度 │ │
│ │ └───┴───┴───┴───┴───┴───┴───┴───┘ │ │
│ │ 65536 个离散值,人眼无法察觉差异 │ │
│ │ │ │
│ │ Byte 精度 (1.41°): │ │
│ │ ┌───────┬───────┬───────┬───────┐ │ │
│ │ │ 0 │ 1.4 │ 2.8 │ ... │360│ 度 │ │
│ │ └───────┴───────┴───────┴───────┘ │ │
│ │ 256 个离散值,可能在近距离观察时有轻微跳跃 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🔄 Rotator 增量序列化
CPP
// 源码位置: RotatorNetSerializers.cpp (第129-162行)template<typename T>
void FRotatorAsShortNetSerializerBase<T>::SerializeDelta(
FNetSerializationContext& Context,
const FNetSerializeDeltaArgs& Args)
{
const QuantizedType& Value = *reinterpret_cast<QuantizedType*>(Args.Source);
const QuantizedType& PrevValue = *reinterpret_cast<QuantizedType*>(Args.Prev);
// 计算每个分量的差值
const uint16 DX = Value.X - PrevValue.X; // 利用 uint16 溢出特性
const uint16 DY = Value.Y - PrevValue.Y;
const uint16 DZ = Value.Z - PrevValue.Z;
// 构建差异标志
uint32 XYZDiffers = 0;
XYZDiffers |= (DX != 0) ? uint32(XDiffersMask) : uint32(0);
XYZDiffers |= (DY != 0) ? uint32(YDiffersMask) : uint32(0);
XYZDiffers |= (DZ != 0) ? uint32(ZDiffersMask) : uint32(0);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// 写入差异标志 (3 bits)
Writer->WriteBits(XYZDiffers, 3U);
// 只写入变化的分量
if (XYZDiffers & XDiffersMask) Writer->WriteBits(DX, 16U);
if (XYZDiffers & YDiffersMask) Writer->WriteBits(DY, 16U);
if (XYZDiffers & ZDiffersMask) Writer->WriteBits(DZ, 16U);
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ Rotator 增量序列化示例 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 场景: 角色只转动了 Yaw (左右看) │
│ │
│ 前一帧: FRotator(0°, 90°, 0°) │
│ 当前帧: FRotator(0°, 95°, 0°) │
│ │
│ 量化后: │
│ 前一帧: X=0, Y=16384, Z=0 │
│ 当前帧: X=0, Y=17294, Z=0 │
│ │
│ 差值计算: │
│ DX = 0 - 0 = 0 │
│ DY = 17294 - 16384 = 910 │
│ DZ = 0 - 0 = 0 │
│ │
│ 序列化输出: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ [010][0000001110001110] ← 3 + 16 = 19 bits │ │
│ │ ↑↑↑ ↑─────────────── │ │
│ │ XYZ DY = 910 │ │
│ │ 标志 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ vs 全量序列化: 3 + 16 = 19 bits (只有 Y 非零) │
│ vs 如果所有分量都变化: 3 + 48 = 51 bits │
│ │
│ 💡 增量序列化在分量变化时效果相同,但在值相同时只需 3 bits! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📦 7.12 PackedIntNetSerializers实现细节
🔢 PackedInt的自适应编码
CPP
// 源码位置: PackedIntNetSerializers.cpp (第53-58行)struct FPackedInt32NetSerializerBase
{
static constexpr SIZE_T DeltaBitCountTableEntryCount = 3;
// Delta 位计数表: 用于增量压缩
static constexpr uint8 DeltaBitCountTable[] = {0, 4, 14};
};
// 序列化实现void FPackedInt32NetSerializer::Serialize(FNetSerializationContext& Context, const FNetSerializeArgs& Args){
const int32 Value = *reinterpret_cast<const int32*>(Args.Source);
// 计算表示该值所需的位数
const uint32 BitCountNeeded = GetBitsNeeded(Value);
// 向上取整到字节边界
const uint32 ByteCountNeeded = (BitCountNeeded + 7U) / 8U;
const uint32 BitCountToWrite = ByteCountNeeded * 8U;
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// 写入字节数索引 (2 bits: 0-3 表示 1-4 字节)
Writer->WriteBits(ByteCountNeeded - 1U, 2U);
// 写入实际值
Writer->WriteBits(Value, BitCountToWrite);
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ PackedInt编码详解 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 32 位有符号整数编码: │
│ │
│ 值范围 │ 字节数 │ 索引 │ 总位数 │ 示例 │
│ ────────────────────┼───────┼─────┼───────┼──────────────────────── │
│ -128 ~ 127 │ 1 │ 00 │ 10 │ 值=5: [00][00000101] │
│ -32768 ~ 32767 │ 2 │ 01 │ 18 │ 值=1000: [01][0000001111101000]│
│ -8388608 ~ 8388607 │ 3 │ 10 │ 26 │ 值=100000: [10][...] │
│ 其他 │ 4 │ 11 │ 34 │ 值=10^9: [11][...] │
│ │
│ 📊 有符号整数的位扩展: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 反序列化时需要恢复符号位: │ │
│ │ │ │
│ │ 读取 1 字节 (8 bits): 值 = 0xFF (-1 的补码) │ │
│ │ 符号扩展: Mask = 1 << 7 = 0x80 │ │
│ │ 结果: (0xFF ^ 0x80) - 0x80 = 0x7F - 0x80 = -1 │ │
│ │ │ │
│ │ 💡 这种技巧避免了条件分支,提高性能 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 64 位整数的特殊处理: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ BitStream 单次最多写入 32 bits,所以 64 位需要分两次: │ │
│ │ │ │
│ │ Writer->WriteBits(Value & 0xFFFFFFFF, 32U); // 低 32 位 │ │
│ │ Writer->WriteBits(Value >> 32U, BitCount - 32U); // 高 N 位 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🔄 PackedInt的增量压缩
CPP
// 源码位置: PackedIntNetSerializers.cpp (第251-267行)void FPackedInt32NetSerializer::SerializeDelta(FNetSerializationContext& Context, const FNetSerializeDeltaArgs& Args){
const int32 Value = *reinterpret_cast<const int32*>(Args.Source);
const int32 PrevValue = *reinterpret_cast<const int32*>(Args.Prev);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// 使用通用的整数增量序列化函数
SerializeIntDelta(*Writer, Value, PrevValue, DeltaBitCountTable, DeltaBitCountTableEntryCount, 32U);
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ PackedInt增量压缩策略 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ Delta 位计数表: {0, 4, 14} ││ ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ 索引 │ Delta 位数 │ Delta 范围 │ 编码格式 │ ││ ├─────┼───────────┼──────────────────┼───────────────────────────┤ ││ │ 0 │ 0 bits │ delta = 0 │ [00] │ ││ │ 1 │ 4 bits │ -8 ~ +7 │ [01][4 bits] │ ││ │ 2 │ 14 bits │ -8192 ~ +8191 │ [10][14 bits] │ ││ │ 超出│ 32 bits │ 任意值 │ [11][32 bits] │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ ││ 📊 示例分析: ││ ││ 场景 1: 值未变化 (Score: 1000 → 1000) ││ delta = 0 ││ 编码: [00] = 2 bits ││ ││ 场景 2: 小变化 (Score: 1000 → 1005) ││ delta = 5 (在 -8 ~ +7 范围内) ││ 编码: [01][0101] = 2 + 4 = 6 bits ││ ││ 场景 3: 中等变化 (Score: 1000 → 5000) ││ delta = 4000 (在 -8192 ~ +8191 范围内) ││ 编码: [10][00111110100000] = 2 + 14 = 16 bits ││ ││ 场景 4: 大变化 (Score: 1000 → 1000000) ││ delta = 999000 (超出范围) ││ 编码: [11][完整 32 位值] = 2 + 32 = 34 bits ││ │└─────────────────────────────────────────────────────────────────────────────┘🔧 7.13 序列化器注册与宏系统
📋 序列化器声明宏
CPP
// 源码位置: NetSerializer.h (第455-461行)
// 声明序列化器 - 在头文件中使用#define UE_NET_DECLARE_SERIALIZER(SerializerName, Api) \
struct Api SerializerName ## NetSerializerInfo \
{ \
static const UE::Net::FNetSerializer Serializer; \
static uint32 GetQuantizedTypeSize(); \
static uint32 GetQuantizedTypeAlignment(); \
static const FNetSerializerConfig* GetDefaultConfig(); \
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 序列化器宏展开示例 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📝 声明: UE_NET_DECLARE_SERIALIZER(FHealthNetSerializer, MYGAME_API) │
│ │
│ 展开为: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ struct MYGAME_API FHealthNetSerializerNetSerializerInfo │ │
│ │ { │ │
│ │ static const UE::Net::FNetSerializer Serializer; │ │
│ │ static uint32 GetQuantizedTypeSize(); │ │
│ │ static uint32 GetQuantizedTypeAlignment(); │ │
│ │ static const FNetSerializerConfig* GetDefaultConfig(); │ │
│ │ }; │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📝 实现: UE_NET_IMPLEMENT_SERIALIZER(FHealthNetSerializer) │
│ │
│ 展开为: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ const UE::Net::FNetSerializer │ │
│ │ FHealthNetSerializerNetSerializerInfo::Serializer = │ │
│ │ UE::Net::TNetSerializer<FHealthNetSerializer> │ │
│ │ ::ConstructNetSerializer(TEXT("FHealthNetSerializer")); │ │
│ │ │ │
│ │ uint32 FHealthNetSerializerNetSerializerInfo::GetQuantizedTypeSize() │ │
│ │ { │ │
│ │ return UE::Net::TNetSerializerBuilder<FHealthNetSerializer> │ │
│ │ ::GetQuantizedTypeSize(); │ │
│ │ } │ │
│ │ // ... 其他函数实现 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📝 获取: UE_NET_GET_SERIALIZER(FHealthNetSerializer) │
│ │
│ 展开为: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ static_cast<const UE::Net::FNetSerializer&>( │ │
│ │ FHealthNetSerializerNetSerializerInfo::Serializer) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🏗️ TNetSerializerBuilder 模板
CPP
// 源码位置: NetSerializer.h (第409-450行)template<typename NetSerializerImpl>
class TNetSerializer
{
public:
static constexpr FNetSerializer ConstructNetSerializer(const TCHAR* Name)
{
TNetSerializerBuilder<NetSerializerImpl> Builder;
Builder.Validate(); // 编译时验证序列化器实现
FNetSerializer Serializer = {};
// 从 Builder 获取所有函数指针和配置
Serializer.Version = Builder.GetVersion();
Serializer.Traits = Builder.GetTraits();
Serializer.Serialize = Builder.GetSerializeFunction();
Serializer.Deserialize = Builder.GetDeserializeFunction();
Serializer.SerializeDelta = Builder.GetSerializeDeltaFunction();
Serializer.DeserializeDelta = Builder.GetDeserializeDeltaFunction();
Serializer.Quantize = Builder.GetQuantizeFunction();
Serializer.Dequantize = Builder.GetDequantizeFunction();
Serializer.IsEqual = Builder.GetIsEqualFunction();
Serializer.Validate = Builder.GetValidateFunction();
Serializer.CloneDynamicState = Builder.GetCloneDynamicStateFunction();
Serializer.FreeDynamicState = Builder.GetFreeDynamicStateFunction();
Serializer.CollectNetReferences = Builder.GetCollectNetReferencesFunction();
Serializer.Apply = Builder.GetApplyFunction();
Serializer.DefaultConfig = Builder.GetDefaultConfig();
Serializer.QuantizedTypeSize = Builder.GetQuantizedTypeSize();
Serializer.QuantizedTypeAlignment = Builder.GetQuantizedTypeAlignment();
Serializer.ConfigTypeSize = Builder.GetConfigTypeSize();
Serializer.ConfigTypeAlignment = Builder.GetConfigTypeAlignment();
Serializer.Name = Name;
return Serializer;
}
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 序列化器构建流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 编译时流程: │
│ │
│ ① 用户定义序列化器结构体 │
│ struct FMySerializer { ... }; │
│ │
│ ② 调用 UE_NET_IMPLEMENT_SERIALIZER(FMySerializer) │
│ │
│ ③ TNetSerializer<FMySerializer>::ConstructNetSerializer() 被调用 │
│ │
│ ④ TNetSerializerBuilder<FMySerializer> 检测用户实现: │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ - 检测是否有 Serialize 函数 → 使用用户实现 │ │
│ │ - 检测是否有 Quantize 函数 → 使用用户实现 │ │
│ │ - 检测是否有 IsEqual 函数 → 使用用户实现或默认 memcmp │ │
│ │ - 检测 bHasDynamicState 标志 → 设置 Traits │ │
│ │ - ... │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
│ ⑤ 生成 FNetSerializer 实例 (静态存储) │
│ │
│ 📊 运行时使用: │
│ │
│ const FNetSerializer& Serializer = UE_NET_GET_SERIALIZER(FMySerializer); │
│ Serializer.Serialize(Context, Args); // 调用函数指针 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🎮 数组序列化中的 ChangeMask 使用
ChangeMask 在数组序列化中有特殊的应用 —— 使用模运算方案让多个元素共享 ChangeMask 位:
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 数组 ChangeMask 模运算方案 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 📊 场景: TArray<FItem> Inventory; // 100 个物品 ││ ChangeMask 分配: 1 + 10 = 11 bits ││ - bit 0: 数组本身是否变化 ││ - bits 1-10: 元素变化追踪 (10 bits) ││ ││ ═══════════════════════════════════════════════════════════════════════ ││ 模运算映射: ││ ═══════════════════════════════════════════════════════════════════════ ││ ││ 元素索引 ChangeMask 位 ││ ┌────────┬─────────────┐ ││ │ 0 │ bit 1 │ ││ │ 1 │ bit 2 │ ││ │ 2 │ bit 3 │ ││ │ ... │ ... │ ││ │ 9 │ bit 10 │ ││ │ 10 │ bit 1 │ ← 回到 bit 1 ││ │ 11 │ bit 2 │ ││ │ ... │ ... │ ││ │ 99 │ bit 10 │ ││ └────────┴─────────────┘ ││ ││ ⚠️ 权衡: 可能序列化一些未变化的元素,但节省了 ChangeMask 的存储空间 ││ │└─────────────────────────────────────────────────────────────────────────────┘📊 ChangeMask 序列化优化
CPP
// ChangeMask 本身也需要被序列化,Iris 对此进行了优化
// 源码位置: ChangeMaskUtil.cppvoid SerializeChangeMask(FNetBitStreamWriter& Writer, const FNetBitArrayView& ChangeMask){
const uint32 BitCount = ChangeMask.GetNumBits();
// 小 ChangeMask 优化: 直接写入所有位
if (BitCount <= 8)
{
Writer.WriteBits(ChangeMask.GetWord(0), BitCount);
return;
}
// 大 ChangeMask: 使用游程编码 (RLE)
// 连续的 0 或 1 可以被压缩
// ...
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ ChangeMask 序列化策略 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 小 ChangeMask (≤8 bits): │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ChangeMask: [1][1][0][0][0][0][0][0] │ │
│ │ 序列化: 直接写入 8 bits │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 大 ChangeMask (>8 bits) - 稀疏情况: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ChangeMask: [1][0][0][0][0][0][0][0][0][0][0][0][0][0][0][1] │ │
│ │ │ │
│ │ 游程编码: │ │
│ │ - 位 0: 设置 │ │
│ │ - 位 1-14: 14 个连续的 0 │ │
│ │ - 位 15: 设置 │ │
│ │ │ │
│ │ 编码后: [1][连续0计数=14][1] ≈ 10 bits │ │
│ │ vs 原始: 16 bits │ │
│ │ 💡 节省: 37.5% │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 大 ChangeMask - 密集情况: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ChangeMask: [1][1][1][1][1][1][1][1][1][1][1][1][1][1][1][1] │ │
│ │ │ │
│ │ 游程编码: │ │
│ │ - 16 个连续的 1 │ │
│ │ │ │
│ │ 编码后: [连续1计数=16] ≈ 5 bits │ │
│ │ vs 原始: 16 bits │ │
│ │ 💡 节省: 68.75% │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📋 7.14 序列化器速查表
🔢 内置序列化器完整一览
序列化器 | 源类型 | 量化类型 | 典型位数 | 特点 |
|---|---|---|---|---|
| int8 | uint8 | 8 | 无零值优化 |
| int16 | uint16 | 1-17 | 零值优化 |
| int32 | uint32 | 1-33 | 零值优化 |
| int64 | uint64 | 1-65 | 零值优化 |
| uint8 | uint8 | 8 | 无零值优化 |
| uint16 | uint16 | 1-17 | 零值优化 |
| uint32 | uint32 | 1-33 | 零值优化 |
| uint64 | uint64 | 1-65 | 零值优化 |
| float | uint32 | 1-33 | 零值优化 |
| double | uint64 | 1-65 | 零值优化 |
| FRotator | 4×uint16 | 3-51 | Short 精度 |
| FRotator | 4×uint8 | 3-27 | Byte 精度 |
| FVector | 3×uint32 | 3-99 | 分量零值优化 |
| FVector3f | 3×uint32 | 3-99 | 单精度版本 |
| FVector3d | 3×uint64 | 3-195 | 双精度版本 |
| FQuat | 特殊 | 变长 | 最小三分量编码 |
| int32 | int32 | 10-34 | 自适应位数 |
| int64 | int64 | 11-67 | 自适应位数 |
| uint32 | uint32 | 10-34 | 自适应位数 |
| uint64 | uint64 | 11-67 | 自适应位数 |
| FName | 动态 | 变长 | EName 优化 |
| FString | 动态 | 变长 | UTF-8 编码 |
| TArray | 动态 | 变长 | 元素级脏检测 |
| UObject* | FNetRefHandle | 变长 | 对象引用 |
| FGuid | 4×uint32 | 128 | 固定大小 |
| FGameplayTag | FName | 变长 | 使用 FName 编码 |
🎯 选择序列化器的完整决策树
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 选择序列化器决策树 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 你的数据类型是什么? │
│ │ │
│ ├─ 整数 ───────────────────────────────────────────────────────────────── │
│ │ │ │
│ │ ├─ 值域小且确定 (如 0-100) → 配置 BitCount 的 IntNetSerializer │
│ │ ├─ 值通常较小但偶尔很大 → PackedIntNetSerializer │
│ │ ├─ 枚举类型 → EnumNetSerializer (自动配置位数) │
│ │ └─ 值域大且随机 → 默认 IntNetSerializer │
│ │ │
│ ├─ 浮点数 ─────────────────────────────────────────────────────────────── │
│ │ │ │
│ │ ├─ 需要完整精度 → FloatNetSerializer / DoubleNetSerializer │
│ │ ├─ 有限范围 (如 0-1000) → 自定义量化序列化器 │
│ │ ├─ 归一化值 (0-1) → 配置位数的量化序列化器 (8-16 bits) │
│ │ └─ 百分比 (0-100%) → 7-8 bit 量化 │
│ │ │
│ ├─ 几何类型 ───────────────────────────────────────────────────────────── │
│ │ │ │
│ │ ├─ FRotator (高精度) → FRotatorNetSerializer (Short) │
│ │ ├─ FRotator (低精度, 如 NPC) → FRotatorAsByteNetSerializer │
│ │ ├─ FRotator (只需 Yaw) → 自定义 YawOnlyNetSerializer │
│ │ ├─ FVector (标准) → FVectorNetSerializer │
│ │ ├─ FVector (量化) → FVectorQuantizedNetSerializer │
│ │ ├─ FQuat → FQuatNetSerializer (最小三分量编码) │
│ │ └─ FTransform → FTransformNetSerializer │
│ │ │
│ ├─ 字符串/名称 ────────────────────────────────────────────────────────── │
│ │ │ │
│ │ ├─ FName (资源名、标签) → FNameNetSerializer (有 EName 优化) │
│ │ ├─ FString (用户输入) → FStringNetSerializer │
│ │ └─ FText (本地化文本) → FTextNetSerializer │
│ │ │
│ ├─ 容器类型 ───────────────────────────────────────────────────────────── │
│ │ │ │
│ │ ├─ TArray → FArrayPropertyNetSerializer (自动) │
│ │ ├─ TMap → FMapPropertyNetSerializer (自动) │
│ │ ├─ TSet → FSetPropertyNetSerializer (自动) │
│ │ └─ 固定大小数组 → 静态数组序列化器 │
│ │ │
│ └─ 对象引用 ───────────────────────────────────────────────────────────── │
│ │ │
│ ├─ UObject* (强引用) → FObjectNetSerializer │
│ ├─ TSoftObjectPtr (软引用) → FSoftObjectNetSerializer │
│ ├─ TWeakObjectPtr (弱引用) → FWeakObjectNetSerializer │
│ └─ FNetRefHandle (网络句柄) → FNetRefHandleNetSerializer │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📊 带宽优化效果总结
优化技术 | 适用场景 | 典型节省 | 实现复杂度 |
|---|---|---|---|
零值优化 | 经常为零的值 | 90%+ | 低 |
分量零值优化 | 向量/旋转类型 | 30-60% | 中 |
量化 | 有限范围/精度的值 | 30-50% | 中 |
增量压缩 | 缓慢变化的值 | 20-50% | 高 |
打包整数 | 通常较小的整数 | 40-70% | 低 |
ChangeMask | 多属性对象 | 50-80% | 高 |
EName 优化 | 硬编码名称 | 80-95% | 中 |
UTF-8 编码 | ASCII 字符串 | 50% | 中 |
最小三分量 | 四元数 | 25% | 高 |
🎮 7.15 实际游戏场景案例
🎯 案例 1: FPS 游戏角色状态
CPP
USTRUCT()
struct FFPSCharacterState
{
GENERATED_BODY()
// 位置 - 使用量化 Vector (54 bits vs 96 bits)
UPROPERTY(Replicated, meta=(NetSerializer="FVectorQuantizedNetSerializer"))
FVector Position;
// 旋转 - 使用 Short 量化 (3-51 bits vs 96 bits)
UPROPERTY(Replicated, meta=(NetSerializer="FRotatorAsShortNetSerializer"))
FRotator Rotation;
// 血量 - 使用自定义 10 bit 量化 (1-11 bits vs 32 bits)
UPROPERTY(Replicated, meta=(NetSerializer="FHealthNetSerializer"))
float Health;
// 弹药 - 使用 7 bit 配置 (7 bits vs 32 bits)
UPROPERTY(Replicated, meta=(NetSerializerConfig="Ammo7Bits"))
int32 Ammo;
// 武器槽 - 使用 3 bit 配置 (3 bits vs 8 bits)
UPROPERTY(Replicated, meta=(NetSerializerConfig="Weapon3Bits"))
uint8 WeaponSlot;
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ FPS 角色状态带宽分析 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 未优化 vs 优化后对比: │
│ │
│ 属性 │ 未优化 │ 优化后 (典型) │ 节省 │
│ ─────────────┼────────────┼──────────────┼────────────── │
│ Position │ 96 bits │ 54 bits │ 44% │
│ Rotation │ 96 bits │ 19 bits │ 80% (只有 Yaw 非零) │
│ Health │ 32 bits │ 11 bits │ 66% │
│ Ammo │ 32 bits │ 7 bits │ 78% │
│ WeaponSlot │ 8 bits │ 3 bits │ 63% │
│ ─────────────┼────────────┼──────────────┼────────────── │
│ 总计 │ 264 bits │ 94 bits │ 64% │
│ │ = 33 bytes │ ≈ 12 bytes │ │
│ │
│ 🎉 每个角色每帧节省 21 bytes! │
│ 💡 100 个玩家 × 60 帧 × 21 bytes = 126 KB/秒 带宽节省 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🏎️ 案例 2: 赛车游戏车辆状态
CPP
USTRUCT()
struct FRacingCarState
{
GENERATED_BODY()
// 位置 - 赛道范围内的高精度位置
UPROPERTY(Replicated, meta=(NetSerializer="FVectorQuantizedNetSerializer"))
FVector Position;
// 旋转 - 完整旋转 (车辆可能翻滚)
UPROPERTY(Replicated, meta=(NetSerializer="FRotatorAsShortNetSerializer"))
FRotator Rotation;
// 速度 - 使用增量压缩 (速度变化通常较小)
UPROPERTY(Replicated)
FVector Velocity;
// 档位 - 0-8,使用 4 bits
UPROPERTY(Replicated, meta=(NetSerializerConfig="Gear4Bits"))
uint8 Gear;
// 油门/刹车 - 归一化 0-1,使用 8 bits
UPROPERTY(Replicated, meta=(NetSerializer="FNormalizedFloatNetSerializer"))
float Throttle;
UPROPERTY(Replicated, meta=(NetSerializer="FNormalizedFloatNetSerializer"))
float Brake;
// 方向盘角度 - -1 到 1,使用 10 bits
UPROPERTY(Replicated, meta=(NetSerializer="FSteeringNetSerializer"))
float Steering;
};🌍 案例 3: MMO 游戏 NPC 状态
CPP
USTRUCT()
struct FMMONPCState
{
GENERATED_BODY()
// 位置 - 大世界坐标,使用 double 精度
UPROPERTY(Replicated)
FVector Position;
// 旋转 - 只关心 Yaw (NPC 不会飞)
UPROPERTY(Replicated, meta=(NetSerializer="FYawOnlyRotatorNetSerializer"))
FRotator Rotation;
// 血量 - 使用百分比 (0-100)
UPROPERTY(Replicated, meta=(NetSerializerConfig="Health7Bits"))
uint8 HealthPercent;
// 状态 - 枚举,使用 4 bits
UPROPERTY(Replicated, meta=(NetSerializerConfig="State4Bits"))
ENPCState State;
// 目标 ID - 使用 NetHandle
UPROPERTY(Replicated, meta=(NetSerializer="FNetRefHandleNetSerializer"))
FNetRefHandle TargetHandle;
// 名称 - 使用 FName (有 EName 优化)
UPROPERTY(Replicated)
FName DisplayName;
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ MMO NPC 状态带宽分析 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 场景: 1000 个 NPC 同时在玩家视野内 │
│ │
│ 属性 │ 未优化 │ 优化后 (典型) │ 节省 │
│ ─────────────┼────────────┼──────────────┼────────────── │
│ Position │ 96 bits │ 54 bits │ 44% │
│ Rotation │ 96 bits │ 16 bits │ 83% (只有 Yaw) │
│ HealthPercent│ 8 bits │ 7 bits │ 13% │
│ State │ 32 bits │ 4 bits │ 88% │
│ TargetHandle │ 32 bits │ 32 bits │ 0% │
│ DisplayName │ 变长 │ ~11 bits │ 90%+ (EName 优化) │
│ ─────────────┼────────────┼──────────────┼────────────── │
│ 总计 (典型) │ ~300 bits │ ~124 bits │ 59% │
│ │ ≈ 38 bytes │ ≈ 16 bytes │ │
│ │
│ 🎉 1000 NPC × 16 bytes = 16 KB/帧 vs 38 KB/帧 │
│ 💡 30 帧/秒: 480 KB/s vs 1.14 MB/s,节省 660 KB/s! │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🎲 案例 4: 卡牌游戏手牌状态
CPP
USTRUCT()
struct FCardHandState
{
GENERATED_BODY()
// 手牌数组 - 使用数组序列化器
UPROPERTY(Replicated)
TArray<FCardInfo> Cards;
// 当前选中的卡牌索引 (-1 表示未选中)
UPROPERTY(Replicated, meta=(NetSerializerConfig="CardIndex5Bits"))
int8 SelectedCardIndex;
// 法力值 (0-10)
UPROPERTY(Replicated, meta=(NetSerializerConfig="Mana4Bits"))
uint8 CurrentMana;
// 最大法力值 (0-10)
UPROPERTY(Replicated, meta=(NetSerializerConfig="Mana4Bits"))
uint8 MaxMana;
};
USTRUCT()
struct FCardInfo
{
GENERATED_BODY()
// 卡牌 ID (假设最多 4096 张不同的卡)
UPROPERTY(Replicated, meta=(NetSerializerConfig="CardID12Bits"))
uint16 CardID;
// 卡牌状态标志
UPROPERTY(Replicated)
uint8 StateFlags; // 8 bits: 可打出、已增强、已诅咒等
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 卡牌游戏序列化优化策略 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 📊 手牌变化场景分析: ││ ││ 场景 1: 抽一张牌 ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ 前: [Card1, Card2, Card3] │ ││ │ 后: [Card1, Card2, Card3, Card4] │ ││ │ │ ││ │ 增量序列化: │ ││ │ - 数组大小变化标志: 1 bit │ ││ │ - 新元素数量: 4 bits (假设最多 10 张手牌) │ ││ │ - 新卡牌数据: 12 + 8 = 20 bits │ ││ │ 总计: ~25 bits │ ││ │ │ ││ │ vs 全量序列化: 4 × 20 = 80 bits │ ││ │ 💡 节省: 69% │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ ││ 场景 2: 打出一张牌 ││ ┌─────────────────────────────────────────────────────────────────────┐ ││ │ 前: [Card1, Card2, Card3, Card4] │ ││ │ 后: [Card1, Card3, Card4] (Card2 被打出) │ ││ │ │ ││ │ 增量序列化: │ ││ │ - 数组大小变化标志: 1 bit │ ││ │ - 新元素数量: 4 bits │ ││ │ - 现有元素 delta: 3 × ~2 bits (大多数未变) │ ││ │ 总计: ~11 bits │ ││ │ │ ││ │ 💡 数组序列化器智能处理元素移除 │ ││ └─────────────────────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────────┘🔧 7.16 自定义序列化器开发指南
📝 创建自定义序列化器的完整步骤
CPP
// ═══════════════════════════════════════════════════════════════════════════// 步骤 1: 定义序列化器结构体// ═══════════════════════════════════════════════════════════════════════════
// 示例: 自定义血量序列化器 (0-100 范围,1% 精度)struct FHealthNetSerializer
{
// ═══════════════════════════════════════════════════════════════════════
// 必需: 定义类型别名
// ═══════════════════════════════════════════════════════════════════════
using SourceType = float; // 源类型: 游戏代码中的类型
using QuantizedType = uint8; // 量化类型: 网络传输的类型
using ConfigType = FNetSerializerConfig; // 配置类型
// ═══════════════════════════════════════════════════════════════════════
// 必需: 定义特性标志
// ═══════════════════════════════════════════════════════════════════════
static constexpr bool bHasConnectionSpecificSerialization = false;
static constexpr bool bHasDynamicState = false; // 无动态内存分配
static constexpr bool bIsForwardingSerializer = false;
static constexpr bool bUseSerializerIsEqual = false; // 使用默认 memcmp
// ═══════════════════════════════════════════════════════════════════════
// 必需: 量化函数 - 将源类型转换为量化类型
// ═══════════════════════════════════════════════════════════════════════
static void Quantize(FNetSerializationContext& Context, const FNetQuantizeArgs& Args)
{
const SourceType Value = *reinterpret_cast<const SourceType*>(Args.Source);
QuantizedType& Target = *reinterpret_cast<QuantizedType*>(Args.Target);
// 将 0.0-100.0 映射到 0-100
Target = static_cast<uint8>(FMath::Clamp(Value, 0.0f, 100.0f));
}
// ═══════════════════════════════════════════════════════════════════════
// 必需: 反量化函数 - 将量化类型还原为源类型
// ═══════════════════════════════════════════════════════════════════════
static void Dequantize(FNetSerializationContext& Context, const FNetDequantizeArgs& Args)
{
const QuantizedType Value = *reinterpret_cast<const QuantizedType*>(Args.Source);
SourceType& Target = *reinterpret_cast<SourceType*>(Args.Target);
// 将 0-100 映射回 0.0-100.0
Target = static_cast<float>(Value);
}
// ═══════════════════════════════════════════════════════════════════════
// 必需: 序列化函数 - 将量化类型写入比特流
// ═══════════════════════════════════════════════════════════════════════
static void Serialize(FNetSerializationContext& Context, const FNetSerializeArgs& Args)
{
const QuantizedType Value = *reinterpret_cast<const QuantizedType*>(Args.Source);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
// 零值优化: 如果血量为 0,只需 1 bit
if (Value == 0)
{
Writer->WriteBits(1U, 1U); // 标志: 是零
return;
}
Writer->WriteBits(0U, 1U); // 标志: 非零
Writer->WriteBits(Value, 7U); // 7 bits 可以表示 0-127,足够 0-100
}
// ═══════════════════════════════════════════════════════════════════════
// 必需: 反序列化函数 - 从比特流读取量化类型
// ═══════════════════════════════════════════════════════════════════════
static void Deserialize(FNetSerializationContext& Context, const FNetDeserializeArgs& Args)
{
QuantizedType& Target = *reinterpret_cast<QuantizedType*>(Args.Target);
FNetBitStreamReader* Reader = Context.GetBitStreamReader();
// 读取零值标志
const uint32 IsZero = Reader->ReadBits(1U);
if (IsZero)
{
Target = 0;
return;
}
Target = static_cast<uint8>(Reader->ReadBits(7U));
}
// ═══════════════════════════════════════════════════════════════════════
// 可选: 增量序列化函数 - 基于前值的压缩
// ═══════════════════════════════════════════════════════════════════════
static void SerializeDelta(FNetSerializationContext& Context, const FNetSerializeDeltaArgs& Args)
{
const QuantizedType Value = *reinterpret_cast<const QuantizedType*>(Args.Source);
const QuantizedType PrevValue = *reinterpret_cast<const QuantizedType*>(Args.Prev);
FNetBitStreamWriter* Writer = Context.GetBitStreamWriter();
const int32 Delta = static_cast<int32>(Value) - static_cast<int32>(PrevValue);
// 值未变化: 只需 1 bit
if (Delta == 0)
{
Writer->WriteBits(0U, 1U);
return;
}
Writer->WriteBits(1U, 1U); // 有变化
// 小变化 (-8 ~ +7): 4 bits
if (Delta >= -8 && Delta <= 7)
{
Writer->WriteBits(0U, 1U); // 小变化标志
Writer->WriteBits(static_cast<uint32>(Delta + 8), 4U); // 偏移后写入
return;
}
// 大变化: 完整 7 bits
Writer->WriteBits(1U, 1U); // 大变化标志
Writer->WriteBits(Value, 7U);
}
static void DeserializeDelta(FNetSerializationContext& Context, const FNetDeserializeDeltaArgs& Args)
{
const QuantizedType PrevValue = *reinterpret_cast<const QuantizedType*>(Args.Prev);
QuantizedType& Target = *reinterpret_cast<QuantizedType*>(Args.Target);
FNetBitStreamReader* Reader = Context.GetBitStreamReader();
const uint32 HasChanged = Reader->ReadBits(1U);
if (!HasChanged)
{
Target = PrevValue;
return;
}
const uint32 IsLargeDelta = Reader->ReadBits(1U);
if (!IsLargeDelta)
{
const int32 Delta = static_cast<int32>(Reader->ReadBits(4U)) - 8;
Target = static_cast<uint8>(static_cast<int32>(PrevValue) + Delta);
return;
}
Target = static_cast<uint8>(Reader->ReadBits(7U));
}
};
// ═══════════════════════════════════════════════════════════════════════════// 步骤 2: 在头文件中声明序列化器// ═══════════════════════════════════════════════════════════════════════════// MyNetSerializers.hUE_NET_DECLARE_SERIALIZER(FHealthNetSerializer, MYGAME_API);
// ═══════════════════════════════════════════════════════════════════════════// 步骤 3: 在 cpp 文件中实现序列化器// ═══════════════════════════════════════════════════════════════════════════// MyNetSerializers.cppUE_NET_IMPLEMENT_SERIALIZER(FHealthNetSerializer);PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 自定义序列化器位数分析 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ 📊 FHealthNetSerializer 位数分析: ││ ││ 场景 │ 全量序列化 │ 增量序列化 ││ ───────────────────────┼──────────────┼────────────────────────────── ││ 血量 = 0 │ 1 bit │ 1 bit (未变) / 2 bits (变为0) ││ 血量 = 1-100 │ 8 bits │ 1 bit (未变) / 6 bits (小变化) ││ 血量大变化 │ 8 bits │ 9 bits ││ ││ vs 默认 FFloatNetSerializer: ││ - 零值: 1 bit ││ - 非零值: 33 bits ││ ││ 💡 典型场景 (血量 75%,小变化): ││ - 自定义: 6 bits ││ - 默认: 33 bits ││ - 节省: 82%! ││ │└─────────────────────────────────────────────────────────────────────────────┘🎯 序列化器开发最佳实践
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 序列化器开发检查清单 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 设计阶段: │
│ □ 分析数据的值域范围 │
│ □ 分析数据的变化频率和幅度 │
│ □ 确定可接受的精度损失 │
│ □ 评估零值/常见值的出现频率 │
│ │
│ ✅ 实现阶段: │
│ □ 正确定义所有类型别名 │
│ □ 实现所有必需的函数 │
│ □ 添加零值优化 (如果适用) │
│ □ 添加增量压缩 (如果数据缓慢变化) │
│ □ 处理边界情况 (溢出、下溢) │
│ │
│ ✅ 测试阶段: │
│ □ 测试全范围值的量化/反量化 │
│ □ 测试序列化/反序列化的往返一致性 │
│ □ 测试增量压缩的正确性 │
│ □ 测试边界值 (最小、最大、零) │
│ □ 性能基准测试 │
│ │
│ ✅ 文档阶段: │
│ □ 记录支持的值域范围 │
│ □ 记录精度损失 │
│ □ 记录典型位数消耗 │
│ □ 记录使用场景和限制 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘🐛 7.17 序列化调试与故障排除
🔍 常见问题与解决方案
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐│ 序列化常见问题诊断 │├─────────────────────────────────────────────────────────────────────────────┤│ ││ ❌ 问题 1: 客户端收到的值与服务器不一致 ││ ───────────────────────────────────────────────────────────────────────── ││ 可能原因: ││ • 量化精度损失过大 ││ • 序列化/反序列化不对称 ││ • 字节序问题 (跨平台) ││ ││ 诊断步骤: ││ 1. 启用 Iris 调试日志: net.Iris.LogLevel=Verbose ││ 2. 比较服务器量化值和客户端反量化值 ││ 3. 检查序列化器的 Quantize/Dequantize 实现 ││ ││ ───────────────────────────────────────────────────────────────────────── ││ ││ ❌ 问题 2: 数据包大小异常 ││ ───────────────────────────────────────────────────────────────────────── ││ 可能原因: ││ • ChangeMask 未正确更新 ││ • 增量压缩未生效 ││ • 使用了错误的序列化器 ││ ││ 诊断步骤: ││ 1. 使用 net.Iris.DumpPackets 命令 ││ 2. 分析每个属性的位数消耗 ││ 3. 检查 ChangeMask 位是否正确设置 ││ ││ ───────────────────────────────────────────────────────────────────────── ││ ││ ❌ 问题 3: 动态状态内存泄漏 ││ ───────────────────────────────────────────────────────────────────────── ││ 可能原因: ││ • FreeDynamicState 未正确实现 ││ • 数组元素的动态状态未释放 ││ ││ 诊断步骤: ││ 1. 使用内存分析工具检测泄漏 ││ 2. 检查 bHasDynamicState 标志是否正确设置 ││ 3. 验证 CloneDynamicState/FreeDynamicState 配对调用 ││ │└─────────────────────────────────────────────────────────────────────────────┘📊 调试命令与工具
CPP
// ═══════════════════════════════════════════════════════════════════════════// Iris 真实控制台命令 (CVar)// ═══════════════════════════════════════════════════════════════════════════
// 📊 统计与分析
net.Iris.UseVerboseIrisCsvStats=1 // 输出详细的 per-class CSV 统计
net.Iris.EnableDetailedClientProfiler=1 // 生成详细的客户端 CSV 统计
net.Iris.Stats.ShouldIncludeSubObjectWithRoot=1 // SubObject 与 RootObject 一起报告统计
// 🔄 轮询控制
net.Iris.UseFrequencyBasedPolling=1 // 使用基于频率的轮询
net.Iris.UseDormancyToFilterPolling=1 // 使用休眠过滤轮询
net.Iris.AllowPollPeriodOverrides=1 // 允许轮询周期覆盖
// 🔧 序列化选项
net.Iris.DeltaCompressInitialState=1 // 序列化初始状态时与默认状态比较
net.Iris.OnlyQuantizeDirtyMembers=1 // 只量化脏成员
net.iris.ForceFullCopyAndQuantize=1 // 强制完整拷贝和量化 (调试用)
// 📝 日志
net.Iris.LogReplicationProtocols=1 // 记录所有创建的复制协议
// ⚙️ 其他
net.Iris.EnableFilterMappings=1 // 启用过滤器映射
net.Iris.EnableForceNetUpdate=1 // ForceNetUpdate 只跳过轮询频率
net.iris.AllowAsyncLoading=1 // 允许异步加载📈 7.18 性能优化建议
🚀 序列化性能优化策略
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 序列化性能优化金字塔 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ 高级优化 │ │
│ │ (10-20%) │ │
│ └──────┬──────┘ │
│ ┌───────────┴───────────┐ │
│ │ 中级优化 │ │
│ │ (20-40%) │ │
│ └───────────┬───────────┘ │
│ ┌─────────────────┴─────────────────┐ │
│ │ 基础优化 │ │
│ │ (40-60%) │ │
│ └───────────────────────────────────┘ │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ 基础优化 (必做): │
│ ═══════════════════════════════════════════════════════════════════════ │
│ • 使用适当精度的量化 (不要过度精确) │
│ • 启用零值优化 │
│ • 使用 ChangeMask 跳过未变化的属性 │
│ • 选择正确的序列化器类型 │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ 中级优化 (推荐): │
│ ═══════════════════════════════════════════════════════════════════════ │
│ • 实现增量压缩 (SerializeDelta) │
│ • 使用打包整数替代固定宽度整数 │
│ • 优化数组元素的序列化器 │
│ • 合并相关属性减少 ChangeMask 开销 │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ 高级优化 (可选): │
│ ═══════════════════════════════════════════════════════════════════════ │
│ • 自定义序列化器针对特定数据模式 │
│ • 使用条件复制减少复制频率 │
│ • 实现预测压缩 (基于运动预测) │
│ • 批量序列化相似对象 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📊 带宽预算规划
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 典型游戏带宽预算 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📊 目标: 客户端上行 64 KB/s,下行 256 KB/s │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ FPS 游戏 (64 玩家): │
│ ═══════════════════════════════════════════════════════════════════════ │
│ │
│ 类别 │ 每帧字节 │ 帧率 │ 带宽/秒 │ 占比 │
│ ─────────────────┼─────────┼─────┼─────────┼────────────── │
│ 玩家状态 (64) │ 12 × 64 │ 60 │ 46 KB/s │ 18% │
│ 子弹/投射物 │ 变化 │ 60 │ 30 KB/s │ 12% │
│ 物理对象 │ 变化 │ 30 │ 20 KB/s │ 8% │
│ 游戏事件 │ 变化 │ - │ 15 KB/s │ 6% │
│ RPC 调用 │ 变化 │ - │ 25 KB/s │ 10% │
│ 协议开销 │ - │ - │ 20 KB/s │ 8% │
│ ─────────────────┼─────────┼─────┼─────────┼────────────── │
│ 总计 │ - │ - │ 156 KB/s│ 61% (有余量) │
│ │
│ ═══════════════════════════════════════════════════════════════════════ │
│ MMO 游戏 (1000 NPC 可见): │
│ ═══════════════════════════════════════════════════════════════════════ │
│ │
│ 类别 │ 每帧字节 │ 帧率 │ 带宽/秒 │ 占比 │
│ ─────────────────┼─────────┼─────┼─────────┼────────────── │
│ NPC 状态 (1000) │ 16×1000 │ 10 │ 160 KB/s│ 63% │
│ 其他玩家 (100) │ 20 × 100│ 30 │ 60 KB/s │ 23% │
│ 环境更新 │ 变化 │ 5 │ 15 KB/s │ 6% │
│ 协议开销 │ - │ - │ 20 KB/s │ 8% │
│ ─────────────────┼─────────┼─────┼─────────┼────────────── │
│ 总计 │ - │ - │ 255 KB/s│ 100% (临界) │
│ │
│ 💡 优化建议: NPC 使用更低的更新频率和更激进的量化 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📚 7.19 总结与要点回顾
🎯 核心概念总结
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ Iris 序列化系统核心要点 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 🔑 核心概念: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 1. 量化 (Quantization) │ │
│ │ - 将高精度数据转换为低精度表示 │ │
│ │ - 目的: 减少数据大小 │ │
│ │ - 权衡: 精度 vs 带宽 │ │
│ │ │ │
│ │ 2. 序列化 (Serialization) │ │
│ │ - 将量化数据写入比特流 │ │
│ │ - 目的: 网络传输 │ │
│ │ - 技术: 位打包、变长编码 │ │
│ │ │ │
│ │ 3. 增量压缩 (Delta Compression) │ │
│ │ - 基于前值的差分编码 │ │
│ │ - 目的: 进一步减少带宽 │ │
│ │ - 适用: 缓慢变化的数据 │ │
│ │ │ │
│ │ 4. ChangeMask (变化掩码) │ │
│ │ - 追踪哪些属性发生变化 │ │
│ │ - 目的: 跳过未变化的属性 │ │
│ │ - 优化: 内联存储 (≤64 属性) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 📊 关键序列化器: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 数值类型: Int/Uint/Float/Double NetSerializers │ │
│ │ • 几何类型: Vector/Rotator/Quat NetSerializers │ │
│ │ • 字符串: String/Name NetSerializers │ │
│ │ • 容器: Array/Map/Set PropertyNetSerializers │ │
│ │ • 引用: Object/SoftObject/WeakObject NetSerializers │ │
│ │ • 特殊: Packed/Enum/Guid NetSerializers │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 🚀 优化技术: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ • 零值优化: 1 bit 表示零值 │ │
│ │ • 分量优化: 只序列化非零分量 │ │
│ │ • 打包整数: 自适应位宽 │ │
│ │ • EName 优化: 硬编码名称只需索引 │ │
│ │ • UTF-8 编码: ASCII 字符串节省 50% │ │
│ │ • 最小三分量: 四元数只需 3 个分量 │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘📖 推荐学习路径
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────────┐
│ 序列化系统学习路径 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 📚 入门级: │
│ 1. 理解量化 vs 序列化的区别 │
│ 2. 学习基础数值类型序列化器的使用 │
│ 3. 了解 ChangeMask 的作用 │
│ │
│ 📚 中级: │
│ 1. 深入学习几何类型序列化器 │
│ 2. 理解增量压缩的原理和实现 │
│ 3. 学习数组和容器序列化器 │
│ 4. 掌握序列化器配置选项 │
│ │
│ 📚 高级: │
│ 1. 开发自定义序列化器 │
│ 2. 实现特定数据模式的优化 │
│ 3. 进行带宽分析和性能调优 │
│ 4. 理解序列化器注册和宏系统 │
│ │
│ 📚 专家级: │
│ 1. 深入源码理解 TNetSerializerBuilder │
│ 2. 实现多态序列化器 │
│ 3. 优化大规模对象的序列化 │
│ 4. 贡献新的序列化器到引擎 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘本文档基于 Unreal Engine 5.5.0 Iris 源代码分析(源码目录:Engine/Source/Runtime/Experimental/Iris/)