📦 Iris 网络复制系统技术分析 - 第八部分:数据流与传输

🎯 本章目标:深入理解 Iris 的数据流系统,掌握数据如何从服务器"旅行"到客户端的完整过程。
📖 本章导读
在多人游戏中,数据传输就像一场精心编排的物流行动 🚚。想象一下:
服务器是总仓库,存储着所有游戏状态
每个客户端是分店,需要实时获取最新库存信息
网络是高速公路,但有时会堵车(延迟)或货物丢失(丢包)
本章将带你深入了解 Iris 如何高效、可靠地完成这场"数据物流"任务!
📚 你将学到
章节 | 内容 | 难度 |
|---|---|---|
8.1 DataStream 概述 | 数据流的基本概念 | ⭐ 入门 |
8.2 UDataStream 基类 | 核心接口详解 | ⭐⭐ 基础 |
8.3 DataStreamManager | 多流协调管理 | ⭐⭐ 基础 |
8.4 ReplicationWriter | 发送端深度解析 | ⭐⭐⭐ 进阶 |
8.5 ReplicationReader | 接收端深度解析 | ⭐⭐⭐ 进阶 |
8.6 投递确认机制 | ACK/NAK 处理 | ⭐⭐⭐ 进阶 |
8.7 巨型对象处理 | 大数据分片传输 | ⭐⭐⭐⭐ 高级 |
8.8 实际游戏案例 | 三个完整场景 | ⭐⭐ 应用 |
8.9 性能优化 | 调优技巧与监控 | ⭐⭐⭐ 进阶 |
🌊 8.1 DataStream 系统概述
📮 什么是 DataStream?—— 快递公司的比喻
想象一下你经营一家快递公司 📦:
现实世界 | Iris DataStream |
|---|---|
快递公司 |
|
不同的配送线路(普通件、加急件、大件物流) | 不同的 |
包裹 | 序列化后的游戏数据 |
快递单号 |
|
签收确认 |
|
PLAINTEXT
┌─────────────────────────────────────────────────────────────┐
│ 🏢 快递公司总部 │
│ (UDataStreamManager) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ 📦 普通件 │ │ 🚀 加急件 │ │ 🚛 大件物流 │ │
│ │ (复制数据) │ │ (NetToken) │ │ (巨型对象) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
⬇️
🌐 网络传输层
⬇️
📱 客户端接收🎮 游戏场景举例
假设你在开发一款多人射击游戏 🔫:
玩家位置更新 → 通过
UReplicationDataStream发送玩家名称/头衔 → 通过
UNetTokenDataStream发送大型地图数据 → 通过巨型对象机制分片发送
🏗️ 8.2 UDataStream 基类详解
📜 核心接口定义
CPP
// 源文件: Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/DataStream/DataStream.h
UCLASS(abstract, MinimalAPI, transient)
class UDataStream : public UObject
{
GENERATED_BODY()
public:
// 🎯 写入结果枚举 —— 告诉系统"我写完了吗?"
enum class EWriteResult
{
NoData, // 📭 没有数据要发送(空手而归)
Ok, // ✅ 数据发送完毕(任务完成)
HasMoreData, // 📦 还有更多数据(我还能再写!)
};
// 📝 写入参数结构
struct FBeginWriteParameters
{
EDataStreamWriteMode WriteMode; // 写入模式
bool bCanWriteMoreData; // 是否允许写更多
};
// 🔧 核心虚函数接口
virtual EWriteResult BeginWrite(const FBeginWriteParameters& Params); // 开始写入
virtual EWriteResult WriteData(FNetSerializationContext& Context,
FDataStreamRecord const*& OutRecord) = 0; // 写数据
virtual void EndWrite(); // 结束写入
virtual void ReadData(FNetSerializationContext& Context) = 0; // 读数据
virtual void ProcessPacketDeliveryStatus(EPacketDeliveryStatus Status,
FDataStreamRecord const* Record) = 0; // 处理投递状态
virtual bool HasAcknowledgedAllReliableData() const = 0; // 可靠数据是否全部确认
};🔄 数据流生命周期
PLAINTEXT
┌──────────────────────────────────────────────────────────────┐
│ 📤 发送端流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ BeginWrite() ──→ WriteData() ──→ EndWrite() │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ "准备开工!" "打包数据中..." "收工!" │
│ │ │
│ ▼ │
│ FDataStreamRecord │
│ (快递单号,用于追踪) │
│ │
└──────────────────────────────────────────────────────────────┘
⬇️ 网络传输
┌──────────────────────────────────────────────────────────────┐
│ 📥 接收端流程 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ReadData() │
│ │ │
│ ▼ │
│ "拆包验货中..." │
│ │
└──────────────────────────────────────────────────────────────┘
⬇️ ACK/NAK 返回
┌──────────────────────────────────────────────────────────────┐
│ 📋 投递确认 │
├──────────────────────────────────────────────────────────────┤
│ │
│ ProcessPacketDeliveryStatus() │
│ │ │
│ ┌───────────┴───────────┐ │
│ ▼ ▼ │
│ Delivered Lost/Discard │
│ "签收成功!✅" "包裹丢失!❌" │
│ │ │
│ ▼ │
│ 重发逻辑 │
│ │
└──────────────────────────────────────────────────────────────┘💡 EWriteResult 的妙用
CPP
// 🎮 游戏场景:假设你有 100 个敌人需要同步// 但每帧只能发送 10 个(带宽限制)
EWriteResult MyDataStream::WriteData(FNetSerializationContext& Context,
FDataStreamRecord const*& OutRecord){
int32 WrittenCount = 0;
const int32 MaxPerFrame = 10;
for (int32 i = CurrentIndex; i < Enemies.Num() && WrittenCount < MaxPerFrame; ++i)
{
SerializeEnemy(Context, Enemies[i]);
WrittenCount++;
CurrentIndex++;
}
if (WrittenCount == 0)
{
return EWriteResult::NoData; // 📭 没敌人要发了
}
else if (CurrentIndex < Enemies.Num())
{
return EWriteResult::HasMoreData; // 📦 还有敌人没发完,下次继续!
}
else
{
CurrentIndex = 0; // 重置
return EWriteResult::Ok; // ✅ 全部发完了
}
}🎛️ 8.3 DataStreamManager —— 数据流调度中心
🏢 管理器的职责
UDataStreamManager 就像一个快递公司总部,负责:
📋 管理多条配送线路(多个 DataStream)
🔀 协调发送顺序
📊 追踪投递状态
🎚️ 控制发送开关
📝 核心实现分析
CPP
// 源文件: Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/DataStream/DataStreamManager.cpp
class UDataStreamManager::FImpl
{
private:
// 🔢 最多支持 32 个数据流
static constexpr uint32 MaxStreamCount = 32U;
// 📦 所有数据流
TArray<TObjectPtr<UDataStream>> Streams;
// 🚦 每个流的发送状态(Send/Pause)
TArray<EDataStreamSendStatus> StreamSendStatus;
// 📋 记录存储(用于追踪已发送的数据)
TArray<FRecord> RecordStorage;
TResizableCircularQueue<FRecord*> Records;
// 🌐 网络导出管理
FNetExports NetExports;
};🔄 WriteData 流程详解
CPP
UDataStreamManager::EWriteResult UDataStreamManager::FImpl::WriteData(
FNetSerializationContext& Context,
FDataStreamRecord const*& OutRecord)
{
// 1️⃣ 检查是否有数据流
if (Streams.Num() <= 0)
return EWriteResult::NoData;
// 2️⃣ 初始化导出记录
NetExports.InitExportRecordForPacket();
// 3️⃣ 创建临时记录
FRecord TempRecord;
TempRecord.DataStreamRecords.SetNumZeroed(StreamCount);
// 4️⃣ 写入头部信息
// - 5 bits: 数据流数量
// - N bits: 数据流掩码(哪些流有数据)
FNetBitStreamWriter ManagerStream = Context.GetBitStreamWriter()->CreateSubstream();
ManagerStream.WriteBits(0U, StreamCountBitCount); // 占位
ManagerStream.WriteBits(0U, StreamCount); // 占位
// 5️⃣ 遍历所有数据流
uint32 DataStreamMask = 0;
for (SIZE_T StreamIt = 0; StreamIt < StreamCount; ++StreamIt)
{
// 跳过暂停的流
if (StreamSendStatus[StreamIt] == EDataStreamSendStatus::Pause)
continue;
// 创建子流并写入数据
FNetBitStreamWriter SubBitStream = ManagerStream.CreateSubstream();
const EWriteResult WriteResult = Stream->WriteData(SubContext, SubRecord);
if (WriteResult != EWriteResult::NoData)
{
DataStreamMask |= (1U << StreamIt); // 标记该流有数据
ManagerStream.CommitSubstream(SubBitStream);
}
}
// 6️⃣ 修正头部并提交
// ... 省略细节
return CombinedWriteResult;
}📊 数据包结构图
PLAINTEXT
┌─────────────────────────────────────────────────────────────┐
│ 📦 DataStream 数据包 │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 📋 Header (头部) │ │
│ │ ├─ StreamCount (5 bits): 数据流数量 │ │
│ │ └─ DataStreamMask (N bits): 哪些流有数据 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 📦 Stream 0 Data (如果 Mask bit 0 = 1) │ │
│ │ └─ [序列化的复制数据] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 📦 Stream 1 Data (如果 Mask bit 1 = 1) │ │
│ │ └─ [序列化的 NetToken 数据] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 📦 Stream N Data ... │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘🎮 实际应用:暂停/恢复数据流
CPP
// 🎮 游戏场景:玩家打开暂停菜单时,暂停复制数据发送void AMyGameMode::OnPlayerPaused(APlayerController* PC){
UDataStreamManager* StreamManager = GetDataStreamManager();
// 暂停复制数据流,但保持 Token 流(用于 UI 显示玩家名称)
StreamManager->SetSendStatus(TEXT("ReplicationDataStream"), EDataStreamSendStatus::Pause);
UE_LOG(LogGame, Log, TEXT("📵 复制数据流已暂停"));
}
void AMyGameMode::OnPlayerResumed(APlayerController* PC){
UDataStreamManager* StreamManager = GetDataStreamManager();
// 恢复发送
StreamManager->SetSendStatus(TEXT("ReplicationDataStream"), EDataStreamSendStatus::Send);
UE_LOG(LogGame, Log, TEXT("📶 复制数据流已恢复"));
}✍️ 8.4 ReplicationWriter —— 发送端的"打包员"
🎯 ReplicationWriter 是什么?
FReplicationWriter 就像一个专业的打包员 📦,负责:
🔍 收集脏对象(哪些东西变了?)
📊 优先级排序(先发谁?)
📝 序列化数据(打包成二进制)
📋 追踪发送状态(记录快递单)
🔄 对象状态机
CPP
// 源文件: Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/ReplicationWriter.h
enum class EReplicatedObjectState : uint8
{
Invalid = 0, // ❌ 无效状态
// 🔧 特殊状态
AttachmentToObjectNotInScope, // 📎 附件目标不在范围内
HugeObject, // 🐘 巨型对象(需要分片)
// 📤 正常生命周期
PendingCreate, // 🆕 等待创建
WaitOnCreateConfirmation, // ⏳ 等待创建确认
Created, // ✅ 已创建(正常复制中)
WaitOnFlush, // 🚿 等待刷新
PendingTearOff, // ✂️ 等待断开
SubObjectPendingDestroy, // 🗑️ 子对象等待销毁
CancelPendingDestroy, // ↩️ 取消销毁
PendingDestroy, // 💀 等待销毁
WaitOnDestroyConfirmation, // ⏳ 等待销毁确认
Destroyed, // 🪦 已销毁
PermanentlyDestroyed, // ⚰️ 永久销毁
};📊 状态转换图
PLAINTEXT
┌─────────────────────────────────────────┐
│ 对象生命周期状态机 │
└─────────────────────────────────────────┘
┌─────────┐ ┌──────────────┐ ┌───────────────────────┐
│ Invalid │────▶│ PendingCreate│────▶│WaitOnCreateConfirmation│
└─────────┘ └──────────────┘ └───────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Created │
│ (正常复制状态) ✅ │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ WaitOnFlush │ │PendingTearOff│ │SubObjectPending │
│ (刷新) │ │ (断开) │ │ Destroy │
└─────────────┘ └─────────────┘ └──────────────────┘
│ │ │
└────────────────────┼────────────────────┘
▼
┌──────────────────┐
│ PendingDestroy │
└──────────────────┘
│
▼
┌───────────────────────┐
│WaitOnDestroyConfirmation│
└───────────────────────┘
│
▼
┌──────────────────┐
│ Destroyed │
└──────────────────┘
│
▼
┌──────────────────────┐
│ PermanentlyDestroyed │
└──────────────────────┘📦 FReplicationInfo —— 每个对象的"档案卡"
CPP
// 每个复制对象只占 16 字节!🎯 内存优化典范struct FReplicationInfo
{
FChangeMaskStorageOrPointer ChangeMaskOrPtr; // 变化掩码(哪些属性变了)
union
{
uint64 Value;
struct
{
uint64 ChangeMaskBitCount : 16; // 变化掩码位数
uint64 State : 5; // 当前状态(上面的枚举)
uint64 HasDirtySubObjects : 1; // 有脏子对象?
uint64 IsSubObject : 1; // 是子对象?
uint64 HasDirtyChangeMask : 1; // 变化掩码是脏的?
uint64 HasAttachments : 1; // 有附件(RPC)?
uint64 HasChangemaskFilter : 1; // 需要过滤变化掩码?
uint64 IsDestructionInfo : 1; // 是销毁信息?
uint64 IsCreationConfirmed : 1; // 创建已确认?
uint64 TearOff : 1; // 需要断开?
uint64 SubObjectPendingDestroy : 1; // 子对象等待销毁?
uint64 IsDeltaCompressionEnabled : 1; // 启用增量压缩?
uint64 LastAckedBaselineIndex : 2; // 最后确认的基线索引
uint64 PendingBaselineIndex : 2; // 等待确认的基线索引
uint64 FlushFlags : 3; // 刷新标志
uint64 HasDirtyConditionals : 1; // 条件复制变脏?
};
};
};
static_assert(sizeof(FReplicationInfo) == 16, "Expected sizeof FReplicationInfo to be 16 bytes");🎮 游戏场景:敌人 AI 状态同步
CPP
// 🎮 假设我们有一个敌人 AI,需要同步以下状态:// - 位置 (FVector)// - 血量 (float)// - 当前行为 (EAIBehavior)// - 目标玩家 (AActor*)
// ReplicationWriter 会这样处理:
// 1️⃣ 检测变化void FReplicationWriter::UpdateDirtyChangeMasks(const FChangeMaskCache& CachedChangeMasks){
// 遍历所有在范围内的对象
for (uint32 Index : ObjectsInScope)
{
FReplicationInfo& Info = GetReplicationInfo(Index);
// 从缓存获取变化掩码
const ChangeMaskStorageType* CachedMask = CachedChangeMasks.GetMask(Index);
if (CachedMask != nullptr && HasAnyBitSet(CachedMask, Info.ChangeMaskBitCount))
{
// 合并变化掩码
OrChangeMask(Info.GetChangeMaskStoragePointer(), CachedMask, Info.ChangeMaskBitCount);
Info.HasDirtyChangeMask = 1;
// 标记对象为脏
ObjectsWithDirtyChanges.SetBit(Index);
}
}
}
// 2️⃣ 调度发送(按优先级排序)uint32 FReplicationWriter::ScheduleObjects(FScheduleObjectInfo* ScheduledObjectIndices){
uint32 ScheduledCount = 0;
// 收集需要发送的对象
for (uint32 Index : ObjectsWithDirtyChanges)
{
FReplicationInfo& Info = GetReplicationInfo(Index);
// 检查状态是否允许发送
if (CanSendObject(Index))
{
ScheduledObjectIndices[ScheduledCount].Index = Index;
ScheduledObjectIndices[ScheduledCount].SortKey = SchedulingPriorities[Index];
ScheduledCount++;
}
}
// 按优先级排序(高优先级先发)
std::partial_sort(ScheduledObjectIndices,
ScheduledObjectIndices + FMath::Min(ScheduledCount, PartialSortObjectCount),
ScheduledObjectIndices + ScheduledCount,
[](const auto& A, const auto& B) { return A.SortKey > B.SortKey; });
return ScheduledCount;
}📦 写入流程详解
PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 📤 ReplicationWriter 写入流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ BeginWrite() │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1️⃣ 准备写入上下文 │ │
│ │ - 初始化 WriteContext │ │
│ │ - 重置 ObjectsWrittenThisPacket │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Write() │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 2️⃣ 写入销毁对象 │ │
│ │ WriteObjectsPendingDestroy() │ │
│ │ - 优先发送销毁信息(确保客户端及时清理) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 3️⃣ 写入 OOB 附件(带外附件) │ │
│ │ WriteOOBAttachments() │ │
│ │ - 发送不属于任何对象的 RPC │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 4️⃣ 写入普通对象 │ │
│ │ WriteObjects() │ │
│ │ - 按优先级顺序写入 │ │
│ │ - 处理父对象和子对象的依赖关系 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ EndWrite() │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 5️⃣ 提交记录 │ │
│ │ - 保存发送记录用于确认追踪 │ │
│ │ - 更新统计信息 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘🔍 深入源码:状态转换验证
CPP
// 源文件: ReplicationWriter.cpp - 状态转换验证逻辑
void FReplicationWriter::FReplicationInfo::SetState(EReplicatedObjectState NewState)
{
EReplicatedObjectState CurrentState = GetState();
// 🔒 严格的状态转换验证 —— 防止非法状态转换
switch (NewState)
{
case EReplicatedObjectState::PendingCreate:
{
// ✅ 只能从 Invalid 或 WaitOnCreateConfirmation 转换
ensureMsgf(CurrentState == EReplicatedObjectState::Invalid ||
CurrentState == EReplicatedObjectState::WaitOnCreateConfirmation,
TEXT("非法状态转换: %s -> %s"),
LexToString(CurrentState), LexToString(NewState));
}
break;
case EReplicatedObjectState::WaitOnCreateConfirmation:
{
// ✅ 只能从 PendingCreate 或 CancelPendingDestroy 转换
ensureMsgf(CurrentState == EReplicatedObjectState::PendingCreate ||
CurrentState == EReplicatedObjectState::CancelPendingDestroy,
TEXT("非法状态转换: %s -> %s"),
LexToString(CurrentState), LexToString(NewState));
}
break;
case EReplicatedObjectState::Created:
{
// ✅ 可以从多个状态转换到 Created
ensureMsgf(CurrentState == EReplicatedObjectState::PendingCreate ||
CurrentState == EReplicatedObjectState::WaitOnCreateConfirmation ||
CurrentState == EReplicatedObjectState::CancelPendingDestroy ||
CurrentState == EReplicatedObjectState::WaitOnFlush,
TEXT("非法状态转换: %s -> %s"),
LexToString(CurrentState), LexToString(NewState));
}
break;
// ... 其他状态验证
}
State = (uint32)NewState;
}📊 对象调度算法详解
CPP
// 源文件: ReplicationWriter.cpp - 对象调度核心算法
uint32 FReplicationWriter::ScheduleObjects(FScheduleObjectInfo* OutScheduledObjectIndices){
IRIS_PROFILER_SCOPE(FReplicationWriter_ScheduleObjects);
uint32 ScheduledObjectCount = 0;
FScheduleObjectInfo* ScheduledObjectIndices = OutScheduledObjectIndices;
// 🔧 特殊索引单独处理
ObjectsWithDirtyChanges.ClearBit(ObjectIndexForOOBAttachment);
const FNetBitArray& UpdatedObjects = ObjectsWithDirtyChanges;
const FNetBitArray& SubObjects = NetRefHandleManager->GetSubObjectInternalIndices();
// 📊 填充调度列表的 Lambda
auto FillIndexListFunc = [&](uint32 Index)
{
const float UpdatedPriority = SchedulingPriorities[Index];
FScheduleObjectInfo& ScheduledObjectInfo = ScheduledObjectIndices[ScheduledObjectCount];
ScheduledObjectInfo.Index = Index;
ScheduledObjectInfo.SortKey = UpdatedPriority;
// 🎯 只有优先级达到阈值的对象才会被调度
if (UpdatedPriority >= FReplicationWriter::SchedulingThresholdPriority)
{
++ScheduledObjectCount;
// 🔗 处理依赖对象
if (NetRefHandleManager->GetObjectsWithDependentObjectsInternalIndices().GetBit(Index))
{
ScheduleDependentObjects(Index, UpdatedPriority,
SchedulingPriorities, ScheduledObjectIndices, ScheduledObjectCount);
}
}
};
// 🔄 遍历所有脏对象(排除子对象,子对象随父对象一起发送)
FNetBitArray::ForAllSetBits(UpdatedObjects, SubObjects, FNetBitArray::AndNotOp, FillIndexListFunc);
return ScheduledObjectCount;
}
// 🔗 依赖对象调度void FReplicationWriter::ScheduleDependentObjects(
uint32 Index,
float ParentPriority,
TArray<float>& LocalPriorities,
FScheduleObjectInfo* ScheduledObjectIndices,
uint32& OutScheduledObjectCount){
const float DependentObjectPriorityBump = UE_KINDA_SMALL_NUMBER;
for (const FDependentObjectInfo& DependentObjectInfo : NetRefHandleManager->GetDependentObjectInfos(Index))
{
const FInternalNetRefIndex DependentInternalIndex = DependentObjectInfo.NetRefIndex;
float UpdatedPriority = ParentPriority;
if (ObjectsWithDirtyChanges.GetBit(DependentInternalIndex))
{
const FReplicationInfo& DependentInfo = GetReplicationInfo(DependentInternalIndex);
// 🎯 判断是否需要在父对象之前复制
const bool bReplicateBeforeParent =
(DependentObjectInfo.SchedulingHint == EDependentObjectSchedulingHint::ScheduleBeforeParent) ||
((DependentObjectInfo.SchedulingHint == EDependentObjectSchedulingHint::ScheduleBeforeParentIfInitialState) &&
IsInitialState(DependentInfo.GetState()));
if (bReplicateBeforeParent)
{
// 📈 提升依赖对象优先级,确保在父对象之前发送
UpdatedPriority = FMath::Max(
std::nextafter(ParentPriority, std::numeric_limits<float>::infinity()),
LocalPriorities[DependentInternalIndex]);
LocalPriorities[DependentInternalIndex] = UpdatedPriority;
// 📋 加入调度列表
FScheduleObjectInfo& ScheduledObjectInfo = ScheduledObjectIndices[OutScheduledObjectCount];
ScheduledObjectInfo.Index = DependentInternalIndex;
ScheduledObjectInfo.SortKey = UpdatedPriority;
++OutScheduledObjectCount;
}
}
// 🔄 递归处理依赖对象的依赖对象
if (NetRefHandleManager->GetObjectsWithDependentObjectsInternalIndices().GetBit(DependentInternalIndex))
{
ScheduleDependentObjects(DependentInternalIndex, UpdatedPriority,
LocalPriorities, ScheduledObjectIndices, OutScheduledObjectCount);
}
}
}🎯 优先级排序:部分排序优化
CPP
// 源文件: ReplicationWriter.cpp - 部分排序优化
uint32 FReplicationWriter::SortScheduledObjects(
FScheduleObjectInfo* ScheduledObjectIndices,
uint32 ScheduledObjectCount,
uint32 StartIndex){
check(ScheduledObjectCount > 0 && StartIndex <= ScheduledObjectCount);
IRIS_PROFILER_SCOPE(FReplicationWriter_SortScheduledObjects);
// 🎯 关键优化:使用部分排序而非完全排序
// 因为每个数据包只能容纳有限数量的对象,
// 我们只需要找出优先级最高的前 N 个对象
FScheduleObjectInfo* StartIt = ScheduledObjectIndices + StartIndex;
FScheduleObjectInfo* EndIt = ScheduledObjectIndices + ScheduledObjectCount;
FScheduleObjectInfo* SortIt = FMath::Min(StartIt + PartialSortObjectCount, EndIt);
// 📊 std::partial_sort 复杂度: O(N * log(K))
// 比完全排序 O(N * log(N)) 更高效
std::partial_sort(StartIt, SortIt, EndIt,
[](const FScheduleObjectInfo& EntryA, const FScheduleObjectInfo& EntryB)
{
return EntryA.SortKey > EntryB.SortKey; // 降序排列
});
return FMath::Min(ScheduledObjectCount - StartIndex, PartialSortObjectCount);
}🐘 巨型对象处理
当一个对象太大,无法放入单个数据包时,Iris 会自动启用巨型对象模式:
CPP
// 源文件中的关键配置static int32 GReplicationWriterMaxHugeObjectsInTransit = 16; // 最多 16 个巨型对象同时传输
class FHugeObjectSendQueue
{
public:
// 检查队列是否已满
bool IsFull() const;
// 入队巨型对象
bool EnqueueHugeObject(const FHugeObjectContext& Context);
// 确认已完成的对象
void AckObjects(TFunctionRef<void (const FHugeObjectContext& Context)> AckHugeObject);
private:
TSet<FInternalNetRefIndex> RootObjectsInTransit; // 正在传输的根对象
TDoubleLinkedList<FHugeObjectContext> SendContexts; // 发送上下文队列
};PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 🐘 巨型对象分片传输 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 原始对象数据(例如:100KB 的地形数据) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ████████████████████████████████████████████████████████│ │
│ └─────────────────────────────────────────────────────────┘ │
│ ⬇️ 分片 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Part 1│ │Part 2│ │Part 3│ │Part 4│ │Part 5│ │Part 6│ │
│ │ 16KB │ │ 16KB │ │ 16KB │ │ 16KB │ │ 16KB │ │ 20KB │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 🌐 网络传输(可能乱序到达) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ⬇️ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 📦 客户端重新组装 │ │
│ │ (FNetBlobAssembler) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘📖 8.5 ReplicationReader —— 接收端的"拆包员"
🎯 ReplicationReader 是什么?
FReplicationReader 就像一个专业的拆包员 📬,负责:
📥 接收数据包
🔍 解析对象数据
🏗️ 创建/更新对象
🔗 解析对象引用
📢 触发 RepNotify
📊 FReplicatedObjectInfo —— 接收端的"档案卡"
CPP
struct FReplicatedObjectInfo
{
// 变化掩码(记录哪些属性有未解析的引用)
FChangeMaskStorageOrPointer UnresolvedChangeMaskOrPointer;
// 未解析的对象引用追踪
// Key: ChangeMask 位偏移, Value: 引用的 NetRefHandle
FObjectReferenceTracker UnresolvedObjectReferences;
FObjectReferenceTracker ResolvedDynamicObjectReferences;
// 引用计数(用于 O(1) 查找)
TMap<FNetRefHandle, int16> UnresolvedHandleCount;
TMap<FNetRefHandle, int16> ResolvedDynamicHandleCount;
// 增量压缩基线存储
uint8* StoredBaselines[2]; // 双缓冲基线
uint32 InternalIndex;
// 状态标志(位域)
uint32 ChangeMaskBitCount : 16;
uint32 bHasUnresolvedReferences : 1; // 有未解析引用?
uint32 bHasUnresolvedInitialReferences : 1; // 有未解析的初始引用?
uint32 bHasAttachments : 1; // 有附件?
uint32 bDestroy : 1; // 需要销毁?
uint32 bTearOff : 1; // 需要断开?
uint32 bIsDeltaCompressionEnabled : 1; // 启用增量压缩?
uint32 LastStoredBaselineIndex : 2; // 最后存储的基线索引
uint32 PrevStoredBaselineIndex : 2; // 之前的基线索引
};🔄 读取流程详解
CPP
void FReplicationReader::Read(FNetSerializationContext& Context){
// 1️⃣ 分配临时内存
TempLinearAllocator.Reset();
// 2️⃣ 准备待分发对象数组
ObjectsToDispatchArray = new (TempLinearAllocator) FObjectsToDispatchArray(...);
// 3️⃣ 读取销毁对象
const uint32 DestroyedObjectCount = ReadObjectsPendingDestroy(Context);
// 4️⃣ 读取对象数据
const uint32 ObjectCount = Reader.ReadBits(16); // 对象数量
ReadObjects(Context, ObjectCount, 0);
// 5️⃣ 处理巨型对象
ProcessHugeObject(Context);
// 6️⃣ 分发状态数据
DispatchStateData(Context);
// 7️⃣ 结束复制(销毁/断开)
DispatchEndReplication(Context);
// 8️⃣ 解析未解析的引用
ResolveAndDispatchUnresolvedReferences();
}🔍 深入源码:对象批次读取
CPP
// 源文件: ReplicationReader.cpp - 读取对象批次
uint32 FReplicationReader::ReadObjectBatch(FNetSerializationContext& Context, uint32 ReadObjectFlags){
FNetBitStreamReader& Reader = *Context.GetBitStreamReader();
UE_NET_TRACE_SCOPE(Batch, Reader, Context.GetTraceCollector(), ENetTraceVerbosity::Trace);
// 1️⃣ 特殊处理:销毁信息
if (const bool bIsDestructionInfo = Reader.ReadBool())
{
FReplicationBridgeSerializationContext BridgeContext(Context, Parameters.ConnectionId, true);
FForceInlineExportScope ForceInlineExportScope(Context.GetInternalContext());
ReplicationBridge->ReadAndExecuteDestructionInfoFromRemote(BridgeContext);
return 1U;
}
// 2️⃣ 读取批次头部
const FNetRefHandle IncompleteHandle = ReadNetRefHandleId(Context, Reader);
// 3️⃣ 读取批次大小
const uint32 NumBitsUsedForBatchSize =
(ReadObjectFlags & EReadObjectFlag::ReadObjectFlag_IsReadingHugeObjectBatch) == 0U
? Parameters.NumBitsUsedForBatchSize
: Parameters.NumBitsUsedForHugeObjectBatchSize;
uint32 BatchSize = Reader.ReadBits(NumBitsUsedForBatchSize);
// 4️⃣ 验证数据
Context.SetErrorHandleContext(IncompleteHandle);
if (Context.HasErrorOrOverflow() || BatchSize > Reader.GetBitsLeft())
{
Context.SetError(GNetError_InvalidValue);
return 0U;
}
const uint32 BatchEndOrStartOfExportsPos = Reader.GetPosBits() + BatchSize;
// 5️⃣ 读取标志
const bool bHasBatchOwnerData = Reader.ReadBool(); // 批次所有者是否有数据
const bool bHasExports = Reader.ReadBool(); // 是否有导出数据
// 6️⃣ 先读取导出数据(在批次末尾)
uint32 BatchEndPos = BatchEndOrStartOfExportsPos;
TempMustBeMappedReferences.Reset();
if (bHasExports)
{
const uint32 ReturnPos = Reader.GetPosBits();
Reader.Seek(BatchEndPos); // 跳到导出数据位置
ObjectReferenceCache->ReadExports(Context, &TempMustBeMappedReferences);
if (Context.HasErrorOrOverflow())
{
UE_LOG(LogIris, Error, TEXT("读取导出数据失败: %s"), *IncompleteHandle.ToString());
return 0U;
}
BatchEndPos = Reader.GetPosBits();
Reader.Seek(ReturnPos); // 返回状态数据位置
}
// 7️⃣ 检查是否是损坏的对象
const bool bIsBroken = BrokenObjects.FindByPredicate(
[IncompleteHandle](const FNetRefHandle& Entry)
{
return Entry.GetId() == IncompleteHandle.GetId();
}) != nullptr;
if (bIsBroken)
{
Reader.Seek(BatchEndPos); // 跳过损坏对象的数据
return 0U;
}
// 8️⃣ 读取对象数据
uint32 ReadObjectCount = ReadObjectsInBatch(Context, IncompleteHandle,
bHasBatchOwnerData, BatchEndOrStartOfExportsPos);
// 9️⃣ 错误处理
if (Context.HasErrorOrOverflow())
{
if (Context.GetError() == GNetError_BrokenNetHandle)
{
ReplicationBridge->SendErrorWithNetRefHandle(
UE::Net::ENetRefHandleError::ReplicationDisabled,
IncompleteHandle,
Parameters.ConnectionId);
BrokenObjects.AddUnique(IncompleteHandle);
Context.ResetErrorContext();
Reader.Seek(BatchEndPos);
}
return 0U;
}
Reader.Seek(BatchEndPos);
return ReadObjectCount;
}📊 FDispatchObjectInfo —— 待分发对象信息
CPP
// 源文件: ReplicationReader.cpp - 分发对象信息结构
struct FReplicationReader::FDispatchObjectInfo
{
FInternalNetRefIndex InternalIndex = FNetRefHandleManager::InvalidInternalIndex;
FChangeMaskStorageOrPointer ChangeMaskOrPointer; // 变化掩码
// 状态标志(位域,节省内存)
uint32 bIsInitialState : 1 = false; // 是否是初始状态
uint32 bHasState : 1 = false; // 是否有状态数据
uint32 bHasAttachments : 1 = false; // 是否有附件(RPC)
uint32 bDestroy : 1 = false; // 是否需要销毁
uint32 bTearOff : 1 = false; // 是否需要断开
uint32 bDeferredEndReplication : 1 = false; // 是否延迟结束复制
uint32 bShouldCallSubObjectCreatedFromReplication : 1 = false; // 是否调用子对象创建回调
uint32 bDynamicObjectCreated : 1 = false; // 是否是动态创建的对象
};
// 📊 待分发对象数组管理class FReplicationReader::FObjectsToDispatchArray
{
public:
FObjectsToDispatchArray(uint32 InitialCapacity, FMemStackBase& Allocator)
: ObjectsToDispatchCount(0U)
, Capacity(InitialCapacity + ObjectsToDispatchSlackCount)
{
ObjectsToDispatch = new (Allocator) FDispatchObjectInfo[Capacity];
}
// 🔄 动态扩容
void Grow(uint32 Count, FMemStackBase& Allocator)
{
if (Capacity < (ObjectsToDispatchCount + Count))
{
Capacity = ObjectsToDispatchCount + Count + ObjectsToDispatchSlackCount;
FDispatchObjectInfo* NewObjectsToDispatch = new (Allocator) FDispatchObjectInfo[Capacity];
if (ObjectsToDispatchCount)
{
FPlatformMemory::Memcpy(NewObjectsToDispatch, ObjectsToDispatch,
ObjectsToDispatchCount * sizeof(FDispatchObjectInfo));
}
ObjectsToDispatch = NewObjectsToDispatch;
}
}
// 📥 添加待分发对象
FDispatchObjectInfo& AddPendingDispatchObjectInfo(FMemStackBase& Allocator)
{
Grow(1, Allocator);
ObjectsToDispatch[ObjectsToDispatchCount] = FDispatchObjectInfo();
return ObjectsToDispatch[ObjectsToDispatchCount];
}
// ✅ 提交待分发对象
void CommitPendingDispatchObjectInfo()
{
checkSlow(ObjectsToDispatchCount < Capacity);
++ObjectsToDispatchCount;
}
private:
FDispatchObjectInfo* ObjectsToDispatch;
uint32 ObjectsToDispatchCount;
uint32 Capacity;
};PLAINTEXT
// 7️⃣ 结束复制(销毁/断开)DispatchEndReplication(Context);
// 8️⃣ 解析未解析的引用ResolveAndDispatchUnresolvedReferences();}
PLAINTEXT
### 🔗 对象引用解析 —— 一个有趣的挑战
想象这个场景:🎮 服务器发送:
玩家 A 的武器指向 → 武器对象 B
📦 数据包到达顺序可能是:
玩家 A 的数据(引用武器 B)
武器 B 的数据(还没到!)
❓ 问题:玩家 A 收到了,但武器 B 还没创建,怎么办?
PLAINTEXT
Iris 的解决方案 —— **热/冷缓存机制**:
```cpp
// 源文件: ReplicationReader.cpp - 配置参数
// 🔥 热缓存生命周期(毫秒)static int32 HotResolvingLifetimeMS = 1000;
static FAutoConsoleVariableRef CVarHotResolvingLifetimeMS(
TEXT("net.Iris.HotResolvingLifetimeMS"),
HotResolvingLifetimeMS,
TEXT("未解析引用在热缓存中的生命周期(毫秒),超时后移入冷缓存"));
// 🧊 冷缓存重试间隔(毫秒)static int32 ColdResolvingRetryTimeMS = 200;
static FAutoConsoleVariableRef CVarColdResolvingRetryTimeMS(
TEXT("net.Iris.ColdResolvingRetryTimeMS"),
ColdResolvingRetryTimeMS,
TEXT("冷缓存中未解析引用的重试间隔(毫秒)"));
// 🔧 是否启用缓存机制static bool bUseResolvingHandleCache = true;
static FAutoConsoleVariableRef CVarUseResolvingHandleCache(
TEXT("net.Iris.UseResolvingHandleCache"),
bUseResolvingHandleCache,
TEXT("是否使用热/冷缓存机制来优化引用解析"));CPP
// 🔥 热缓存:最近遇到的未解析引用(1秒内)
TMap<FNetRefHandle, uint32> HotUnresolvedHandleCache;
// 🧊 冷缓存:长时间未解析的引用
TMap<FNetRefHandle, uint32> ColdUnresolvedHandleCache;
void FReplicationReader::ResolveAndDispatchUnresolvedReferences(){
// 1️⃣ 先尝试解析热缓存中的引用
for (auto& [Handle, Timestamp] : HotUnresolvedHandleCache)
{
if (TryResolve(Handle))
{
// 解析成功!从缓存移除
HotUnresolvedHandleCache.Remove(Handle);
}
else if (IsExpired(Timestamp, HotResolvingLifetimeMS))
{
// 超时了,移到冷缓存
ColdUnresolvedHandleCache.Add(Handle, Timestamp);
HotUnresolvedHandleCache.Remove(Handle);
}
}
// 2️⃣ 定期尝试解析冷缓存(减少 CPU 开销)
if (ShouldProcessColdCache())
{
for (auto& [Handle, Timestamp] : ColdUnresolvedHandleCache)
{
if (TryResolve(Handle))
{
ColdUnresolvedHandleCache.Remove(Handle);
}
}
}
}PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 🔗 对象引用解析流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 收到数据包 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 解析对象引用 │ │
│ │ CollectReferences() │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ├─── 引用已存在 ──→ ✅ 直接使用 │
│ │ │
│ └─── 引用不存在 ──→ 📥 加入热缓存 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 🔥 热缓存 │ │
│ │ (频繁重试) │ │
│ └─────────────────┘ │
│ │ │
│ 1秒后仍未解析 │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ 🧊 冷缓存 │ │
│ │ (偶尔重试) │ │
│ └─────────────────┘ │
│ │ │
│ 引用对象到达 │
│ │ │
│ ▼ │
│ ✅ 解析成功,应用状态 │
│ │
└─────────────────────────────────────────────────────────────────┘🎮 游戏场景:敌人生成与引用解析
CPP
// 🎮 场景:服务器生成一群敌人,每个敌人都有一个队长引用
// 服务器端void AEnemySpawner::SpawnEnemySquad(){
// 1. 先生成队长
AEnemy* Captain = SpawnEnemy(EEnemyType::Captain);
// 2. 生成小兵,引用队长
for (int i = 0; i < 5; i++)
{
AEnemy* Soldier = SpawnEnemy(EEnemyType::Soldier);
Soldier->Captain = Captain; // 引用队长
}
}
// 客户端接收(可能的顺序)// 📦 Packet 1: Soldier 1 (引用 Captain,但 Captain 还没到)// 📦 Packet 2: Soldier 2 (引用 Captain,但 Captain 还没到)// 📦 Packet 3: Captain (终于到了!)// 📦 Packet 4: Soldier 3, 4, 5
// ReplicationReader 处理流程:// 1. 收到 Soldier 1 → Captain 引用未解析 → 加入热缓存// 2. 收到 Soldier 2 → Captain 引用未解析 → 加入热缓存// 3. 收到 Captain → 创建 Captain 对象// 4. 解析循环 → 发现 Captain 已存在 → 更新 Soldier 1, 2 的引用// 5. 收到 Soldier 3, 4, 5 → Captain 引用直接解析成功🔄 8.6 数据投递确认机制
📬 包投递状态
CPP
enum class EPacketDeliveryStatus : uint8
{
Delivered, // ✅ 已送达(客户端确认收到)
Lost, // ❌ 丢失(需要重发)
Discard, // 🗑️ 丢弃(连接关闭,不需要处理)
};🔄 确认处理流程
CPP
void FReplicationWriter::ProcessDeliveryNotification(EPacketDeliveryStatus Status){
// 获取这个数据包的记录
const uint32 RecordInfoCount = ReplicationRecord.PeekRecord();
// 遍历所有记录的对象
for (uint32 i = 0; i < RecordInfoCount; ++i)
{
const FRecordInfo& RecordInfo = ReplicationRecord.PeekInfo();
FReplicationInfo& Info = ReplicatedObjects[RecordInfo.Index];
switch (Status)
{
case EPacketDeliveryStatus::Delivered:
HandleDeliveredRecord(RecordInfo, Info, AttachmentRecord);
break;
case EPacketDeliveryStatus::Lost:
HandleDroppedRecord(RecordInfo, Info, AttachmentRecord);
break;
case EPacketDeliveryStatus::Discard:
HandleDiscardedRecord(RecordInfo, Info, AttachmentRecord);
break;
}
}
}
void FReplicationWriter::HandleDeliveredRecord(const FRecordInfo& RecordInfo,
FReplicationInfo& Info, ...){
// 根据当前状态处理
switch (Info.GetState())
{
case EReplicatedObjectState::WaitOnCreateConfirmation:
// 创建已确认!
Info.IsCreationConfirmed = 1;
SetState(RecordInfo.Index, EReplicatedObjectState::Created);
break;
case EReplicatedObjectState::WaitOnDestroyConfirmation:
// 销毁已确认!
SetState(RecordInfo.Index, EReplicatedObjectState::Destroyed);
break;
// ... 其他状态处理
}
// 更新增量压缩基线
if (Info.IsDeltaCompressionEnabled && RecordInfo.NewBaselineIndex != InvalidBaselineIndex)
{
Info.LastAckedBaselineIndex = RecordInfo.NewBaselineIndex;
}
}
void FReplicationWriter::HandleDroppedRecord(const FRecordInfo& RecordInfo,
FReplicationInfo& Info, ...){
// 数据丢失了!需要重发
// 1. 恢复变化掩码(标记这些属性需要重发)
if (RecordInfo.HasChangeMask)
{
OrChangeMask(Info.GetChangeMaskStoragePointer(),
RecordInfo.ChangeMaskOrPtr.GetPointer(Info.ChangeMaskBitCount),
Info.ChangeMaskBitCount);
Info.HasDirtyChangeMask = 1;
}
// 2. 标记对象为脏
ObjectsWithDirtyChanges.SetBit(RecordInfo.Index);
// 3. 提升优先级(丢包的对象应该优先重发)
SchedulingPriorities[RecordInfo.Index] += LostStatePriorityBump;
// 4. 失效增量压缩基线
if (Info.IsDeltaCompressionEnabled)
{
InvalidateBaseline(RecordInfo.Index, Info);
}
}📊 投递确认时序图
PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 📬 数据包投递确认时序 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 服务器 客户端 │
│ │ │ │
│ │ ──── 📦 Packet 1 (Seq: 100) ────▶ │ │
│ │ │ │
│ │ ──── 📦 Packet 2 (Seq: 101) ────▶ │ ❌ 丢失! │
│ │ │ │
│ │ ──── 📦 Packet 3 (Seq: 102) ────▶ │ │
│ │ │ │
│ │ ◀──── ✅ ACK 100 ──────────────── │ │
│ │ │ │
│ │ ◀──── ❌ NAK 101 ──────────────── │ │
│ │ │ │
│ │ ◀──── ✅ ACK 102 ──────────────── │ │
│ │ │ │
│ │ │ │
│ ProcessDeliveryNotification(Delivered, Packet1) │
│ ProcessDeliveryNotification(Lost, Packet2) ← 触发重发逻辑 │
│ ProcessDeliveryNotification(Delivered, Packet3) │
│ │ │ │
│ │ ──── 📦 Packet 4 (重发 101 的数据) ──▶ │ │
│ │ │ │
└─────────────────────────────────────────────────────────────────┘🐘 8.7 巨型对象与分片传输 —— 深度解析
🎯 什么是巨型对象?
在网络游戏中,有些数据实在太大了,无法塞进一个数据包 📦。想象一下:
PLAINTEXT
📦 普通数据包容量:约 1KB (1024 bytes)
🐘 巨型对象大小:可能 100KB ~ 几 MB
就像你想用一个快递盒寄一台冰箱 🧊 —— 不可能!
解决方案:把冰箱拆成零件,分多个箱子寄送 📦📦📦📊 巨型对象的典型场景
场景 | 数据类型 | 典型大小 | 分片数量 |
|---|---|---|---|
🗺️ 地形数据 | 高度图、材质信息 | 100KB - 1MB | 100-1000 |
🎨 动态纹理 | 玩家自定义涂装 | 50KB - 500KB | 50-500 |
📜 脚本数据 | 任务脚本、对话树 | 10KB - 100KB | 10-100 |
🏗️ 建筑蓝图 | 玩家建造的建筑 | 20KB - 200KB | 20-200 |
🔧 核心实现:FPartialNetBlob
CPP
// 源文件: Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/ReplicationSystem/NetBlob/PartialNetBlob.h
class FPartialNetBlob final : public FNetBlob
{
public:
// 🔪 分片参数
struct FSplitParams
{
uint32 MaxPartBitCount; // 每个分片最大位数(默认 128*8 = 1024 bits)
uint32 MaxPartCount; // 最大分片数量
};
// 🔪 将一个大 Blob 分割成多个小 PartialNetBlob
static bool SplitNetBlob(
FNetSerializationContext& Context,
const FNetBlobCreationInfo& CreationInfo,
const FSplitParams& SplitParams,
const TRefCountPtr<FNetBlob>& Blob,
TArray<TRefCountPtr<FNetBlob>>& OutPartialBlobs);
// 📊 分片信息
uint32 GetPartCount() const; // 总分片数
uint32 GetPartIndex() const; // 当前分片索引
bool IsFirstPart() const; // 是否是第一个分片
bool IsLastPart() const; // 是否是最后一个分片
private:
uint32 PartCount; // 总分片数(只在第一个分片中有效)
uint32 PartIndex; // 当前分片索引 (0, 1, 2, ...)
uint32 PayloadBitCount; // 负载位数
};🔄 分片流程详解
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────┐│ 🔪 巨型对象分片流程 │├─────────────────────────────────────────────────────────────────────┤│ ││ 原始数据 (例如: 50KB 的地形数据) ││ ┌───────────────────────────────────────────────────────────────┐ ││ │ ████████████████████████████████████████████████████████████ │ ││ │ ████████████████████████████████████████████████████████████ │ ││ └───────────────────────────────────────────────────────────────┘ ││ ⬇️ ││ FPartialNetBlob::SplitNetBlob() ││ ⬇️ ││ ┌─────────────────────────────────────────────────────────────┐ ││ │ 📦 Part 0 (First) │ ││ │ ├─ Header: PartCount=50, PartIndex=0, PayloadBitCount=1024 │ ││ │ └─ Payload: [1024 bits of data] │ ││ ├─────────────────────────────────────────────────────────────┤ ││ │ 📦 Part 1 │ ││ │ ├─ Header: PartIndex=1, PayloadBitCount=1024 │ ││ │ └─ Payload: [1024 bits of data] │ ││ ├─────────────────────────────────────────────────────────────┤ ││ │ 📦 Part 2 ... Part 48 │ ││ │ └─ ... │ ││ ├─────────────────────────────────────────────────────────────┤ ││ │ 📦 Part 49 (Last) │ ││ │ ├─ Header: PartIndex=49, PayloadBitCount=512 (剩余数据) │ ││ │ └─ Payload: [512 bits of remaining data] │ ││ └─────────────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────┘🧩 客户端组装:FNetBlobAssembler
CPP
// 源文件: Engine/Source/Runtime/Experimental/Iris/Core/Public/Iris/ReplicationSystem/NetBlob/NetBlobAssembler.h
class FNetBlobAssembler
{
public:
// 📥 添加一个分片
void AddPartialNetBlob(
FNetSerializationContext& Context,
FNetRefHandle RefHandle,
const TRefCountPtr<FPartialNetBlob>& PartialNetBlob);
// ✅ 检查是否所有分片都已到达
bool IsReadyToAssemble() const { return bIsReadyToAssemble; }
// 🔧 组装完整的 Blob
TRefCountPtr<FNetBlob> Assemble(FNetSerializationContext& Context);
// 🔄 重置(清理未完成的组装)
void Reset();
private:
FNetBitStreamWriter BitWriter; // 用于组装数据的写入器
uint32 NextPartIndex = 0; // 期望的下一个分片索引
uint32 ExpectedPartCount = 0; // 期望的总分片数
bool bIsReadyToAssemble = false;
};🎮 实际代码:巨型对象处理流程
CPP
// 📤 发送端:ReplicationWriter 中的巨型对象处理
void FReplicationWriter::WriteHugeObjectBatch(FNetSerializationContext& Context){
// 1️⃣ 检查是否有待发送的巨型对象
if (!HugeObjectSendQueue.HasPendingObjects())
return;
// 2️⃣ 获取当前要发送的巨型对象上下文
FHugeObjectContext& HugeContext = HugeObjectSendQueue.GetCurrentContext();
// 3️⃣ 序列化对象批次
FNetBitStreamWriter& Writer = *Context.GetBitStreamWriter();
// 写入巨型对象头部
FNetObjectBlob::FHeader Header;
Header.ObjectCount = HugeContext.ObjectCount;
FNetObjectBlob::SerializeHeader(Context, Header);
// 4️⃣ 写入所有对象数据
for (const FObjectRecord& Record : HugeContext.BatchRecord.ObjectReplicationRecords)
{
WriteObjectState(Context, Record);
}
// 5️⃣ 将数据分片
TArray<TRefCountPtr<FNetBlob>> PartialBlobs;
FPartialNetBlob::FSplitParams SplitParams;
SplitParams.MaxPartBitCount = PartialNetBlobHandler->GetConfig()->MaxPartBitCount;
FPartialNetBlob::SplitNetBlob(Context, CreationInfo, SplitParams,
HugeContext.NetObjectBlob, PartialBlobs);
// 6️⃣ 将分片加入发送队列
for (const auto& PartialBlob : PartialBlobs)
{
Attachments.Enqueue(ENetObjectAttachmentType::HugeObject,
ObjectIndexForOOBAttachment,
PartialBlob);
}
}
// 📥 接收端:ReplicationReader 中的巨型对象处理
void FReplicationReader::ProcessHugeObjectAttachment(
FNetSerializationContext& Context,
const TRefCountPtr<FNetBlob>& Attachment){
IRIS_PROFILER_SCOPE(FReplicationReader_ProcessHugeObjectAttachment)
// 1️⃣ 验证是否是 NetObjectBlob 类型
if (Attachment->GetCreationInfo().Type != NetObjectBlobType)
{
UE_LOG(LogIris, Error, TEXT("Unexpected blob type in huge object attachment"));
return;
}
// 2️⃣ 获取组装后的数据
const FNetObjectBlob& NetObjectBlob = *static_cast<FNetObjectBlob*>(Attachment.GetReference());
// 3️⃣ 创建读取器
FNetBitStreamReader HugeObjectReader;
HugeObjectReader.InitBits(NetObjectBlob.GetRawData().GetData(),
NetObjectBlob.GetRawDataBitCount());
// 4️⃣ 创建子上下文
FNetSerializationContext HugeObjectSerializationContext = Context.MakeSubContext(&HugeObjectReader);
// 5️⃣ 读取头部
FNetObjectBlob::FHeader HugeObjectHeader = {};
FNetObjectBlob::DeserializeHeader(HugeObjectSerializationContext, HugeObjectHeader);
if (HugeObjectSerializationContext.HasErrorOrOverflow() || HugeObjectHeader.ObjectCount < 1U)
{
Context.SetError(GNetError_BitStreamError);
return;
}
// 6️⃣ 预分配分发数组
ObjectsToDispatchArray->Grow(HugeObjectHeader.ObjectCount + ObjectsToDispatchSlackCount,
TempLinearAllocator);
// 7️⃣ 读取所有对象
const uint32 ReadObjectFlags = EReadObjectFlag::ReadObjectFlag_IsReadingHugeObjectBatch;
ReadObjects(HugeObjectSerializationContext, HugeObjectHeader.ObjectCount, ReadObjectFlags);
if (HugeObjectSerializationContext.HasErrorOrOverflow())
{
Context.SetError(GNetError_BitStreamError);
return;
}
}📊 巨型对象统计指标
CPP
// 源文件: Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/Stats/NetStats.h
struct FNetSendStats
{
// 🐘 巨型对象相关统计
int32 ActiveHugeObjectCount = 0; // 当前活跃的巨型对象数
int32 HugeObjectsWaitingForAckCount = 0; // 等待确认的巨型对象数
int32 HugeObjectsStallingCount = 0; // 阻塞的巨型对象数
double HugeObjectWaitingForAckTimeInSeconds = 0; // 等待确认的总时间
double HugeObjectStallingTimeInSeconds = 0; // 阻塞的总时间
};
// 📊 CSV 统计输出// 可以在 Unreal Insights 或 CSV 文件中查看:// - Iris/ActiveHugeObjectCount// - Iris/HugeObjectsWaitingForAckCount// - Iris/HugeObjectsStallingCount// - Iris/HugeObjectWaitingForAckTimeInSeconds// - Iris/HugeObjectStallingTimeInSeconds⚠️ 巨型对象使用注意事项
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────┐
│ ⚠️ 巨型对象最佳实践 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ✅ 推荐做法: │
│ ├─ 尽量避免使用巨型对象(优化数据结构) │
│ ├─ 如果必须使用,限制同时传输的数量(默认最多 16 个) │
│ ├─ 对巨型对象使用较低的优先级 │
│ └─ 监控 HugeObjectStallingTime 指标 │
│ │
│ ❌ 避免做法: │
│ ├─ 频繁修改巨型对象(每次修改都要重传整个对象) │
│ ├─ 在巨型对象传输期间删除对象 │
│ └─ 同时传输大量巨型对象(会阻塞普通对象复制) │
│ │
│ 💡 优化建议: │
│ ├─ 将大对象拆分成多个小对象 │
│ ├─ 使用流式加载代替一次性传输 │
│ └─ 考虑使用独立的下载通道(HTTP)传输大数据 │
│ │
└─────────────────────────────────────────────────────────────────────┘🎮 游戏场景:玩家自定义涂装
CPP
// 🎨 场景:玩家可以自定义车辆涂装,涂装数据约 100KB
UCLASS()
class ACustomizableCar : public AActor
{
// ❌ 不推荐:直接复制大数据
// UPROPERTY(Replicated)
// TArray<uint8> PaintData; // 100KB 的涂装数据
// ✅ 推荐:使用引用 + 异步加载
UPROPERTY(Replicated)
FGuid PaintDataId; // 只复制 16 字节的 ID
// 涂装数据通过独立系统加载
void OnRep_PaintDataId()
{
// 通过 ID 从缓存或服务器异步加载涂装数据
PaintDataManager->LoadPaintDataAsync(PaintDataId,
[this](const TArray<uint8>& Data)
{
ApplyPaintData(Data);
});
}
};
// 或者使用 Iris 的巨型对象机制(适合一次性传输)UCLASS()
class APaintDataBlob : public AActor
{
UPROPERTY(Replicated)
TArray<uint8> PaintData; // Iris 会自动分片传输
// 配置:降低优先级,避免阻塞其他复制
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 设置较低的静态优先级
DOREPLIFETIME_WITH_PARAMS(APaintDataBlob, PaintData,
FDoRepLifetimeParams{
.Priority = ENetPriority::Low // 低优先级
});
}
};🎮 8.8 实际游戏场景案例
🎯 案例 1:FPS 游戏 —— 玩家射击同步
CPP
// 场景:玩家 A 向玩家 B 射击
// 🔫 服务器端void AWeapon::Fire(){
// 1. 创建子弹(需要复制)
ABullet* Bullet = GetWorld()->SpawnActor<ABullet>(...);
// 2. 设置子弹属性
Bullet->Shooter = GetOwner();
Bullet->Velocity = GetAimDirection() * BulletSpeed;
Bullet->Damage = WeaponDamage;
// Iris 自动处理:
// - 检测到新对象 → PendingCreate 状态
// - 序列化初始状态
// - 发送到所有相关客户端
}
// 📦 ReplicationWriter 处理流程:// 1. UpdateScope() → 检测到新对象 Bullet// 2. StartReplication(BulletIndex) → 初始化复制状态// 3. GetInitialChangeMask() → 所有属性都是脏的// 4. WriteObjectBatch() → 序列化 Bullet 数据// - 写入 Handle ID// - 写入创建信息(类型、位置等)// - 写入属性数据(Shooter 引用、Velocity、Damage)
// 📥 ReplicationReader 处理流程:// 1. ReadObjectBatch() → 读取 Bullet 数据// 2. 检测到是新对象 → 调用 Bridge 创建// 3. 应用属性数据// 4. 解析 Shooter 引用// 5. 触发 OnRep_Velocity() 等 RepNotify🏎️ 案例 2:赛车游戏 —— 车辆状态高频更新
CPP
// 场景:100 辆赛车,每帧都有位置变化
// 🏎️ 服务器端配置UCLASS()
class ARaceCar : public AActor
{
UPROPERTY(Replicated)
FVector Location; // 位置(高频变化)
UPROPERTY(Replicated)
FRotator Rotation; // 旋转(高频变化)
UPROPERTY(Replicated)
float Speed; // 速度(高频变化)
UPROPERTY(Replicated)
FString DriverName; // 车手名(低频变化)
};
// Iris 优化策略:// 1. 增量压缩:只发送变化的部分// 2. 优先级调度:靠近玩家的车优先发送// 3. 带宽分配:高速变化的属性可能被截断
// 📊 带宽分析// 假设每帧带宽限制:10KB// 100 辆车,每辆车完整状态:100 bytes// // 不优化:100 * 100 = 10KB(刚好用完)// // Iris 优化后:// - 只发送变化的属性(~30 bytes/车)// - 远处的车降低更新频率// - 实际使用:~5KB(节省 50%)🗺️ 案例 3:开放世界 —— 巨型对象流送
CPP
// 场景:玩家进入新区域,需要加载大型地形数据
// 🗺️ 服务器端void AWorldStreamer::StreamTerrainToPlayer(APlayerController* PC, UTerrainData* Terrain){
// 地形数据可能有 500KB
// 单个数据包最大约 1KB
// 需要分成 500+ 个分片
// Iris 自动处理:
// 1. 检测到对象太大 → 进入 HugeObject 状态
// 2. 分片序列化
// 3. 按顺序发送分片
// 4. 等待确认后继续发送
}
// 📦 巨型对象发送流程:// // Frame 1: 发送 Part 1-10// Frame 2: 等待 ACK...// Frame 3: 收到 ACK 1-8, NAK 9-10// 重发 Part 9-10// 发送 Part 11-20// ...// Frame N: 所有分片确认完成// 客户端组装完整对象📊 8.9 性能优化技巧
🎯 优化建议清单
优化项 | 说明 | 效果 |
|---|---|---|
🔄 使用增量压缩 | 只发送变化的部分 | 带宽减少 50-80% |
📊 合理设置优先级 | 重要对象优先发送 | 延迟降低 |
🎚️ 调整轮询频率 | 远处对象降低更新频率 | CPU 减少 30% |
📦 批量处理 | 父子对象一起发送 | 减少包头开销 |
🔗 预加载引用 | 提前发送依赖对象 | 减少引用解析等待 |
🔧 关键配置参数
CPP
// 源文件: ReplicationWriter.cpp - 关键配置参数
// 🐘 巨型对象同时传输数量限制static int32 GReplicationWriterMaxHugeObjectsInTransit = 16;
static FAutoConsoleVariableRef CVarReplicationWriterMaxHugeObjectsInTransit(
TEXT("net.Iris.ReplicationWriterMaxHugeObjectsInTransit"),
GReplicationWriterMaxHugeObjectsInTransit,
TEXT("允许同时传输的巨型对象数量。需要至少为 1。\n"
"权衡:值越大,高延迟/丢包场景下体验越好,但会延迟普通对象复制。"));
// 📦 允许的额外数据包数量(非巨型对象)static int32 GReplicationWriterMaxAllowedPacketsIfNotHugeObject = 3;
static FAutoConsoleVariableRef CVarReplicationWriterMaxAllowedPacketsIfNotHugeObject(
TEXT("net.Iris.ReplicationWriterMaxAllowedPacketsIfNotHugeObject"),
GReplicationWriterMaxAllowedPacketsIfNotHugeObject,
TEXT("如果有更多数据要写入,允许 ReplicationWriter 超额提交数据。"));
// ⚠️ 警告:丢弃不在范围内对象的附件static bool bWarnAboutDroppedAttachmentsToObjectsNotInScope = false;
static FAutoConsoleVariableRef CVarWarnAboutDroppedAttachmentsToObjectsNotInScope(
TEXT("net.Iris.WarnAboutDroppedAttachmentsToObjectsNotInScope"),
bWarnAboutDroppedAttachmentsToObjectsNotInScope,
TEXT("当附件因目标对象不在范围内而被丢弃时发出警告。默认 false。"));
// 🔍 验证脏对象static bool bValidateObjectsWithDirtyChanges = true;
static FAutoConsoleVariableRef CvarValidateObjectsWithDirtyChanges(
TEXT("net.Iris.ReplicationWriter.ValidateObjectsWithDirtyChanges"),
bValidateObjectsWithDirtyChanges,
TEXT("确保不会将无效对象标记为脏。"));CPP
// 源文件: ReplicationReader.cpp - 关键配置参数
// 🔄 是否在应用状态前执行可靠 RPCstatic bool bExecuteReliableRPCsBeforeApplyState = true;
static FAutoConsoleVariableRef CVarExecuteReliableRPCsBeforeApplyState(
TEXT("net.Iris.ExecuteReliableRPCsBeforeApplyState"),
bExecuteReliableRPCsBeforeApplyState,
TEXT("如果为 true 且 Iris 运行在向后兼容模式,\n"
"可靠 RPC 将在应用状态数据之前执行(除非需要先生成对象)。"));
// 📥 是否延迟结束复制static bool bDeferEndReplication = true;
static FAutoConsoleVariableRef CVarDeferEndReplication(
TEXT("net.Iris.DeferEndReplication"),
bDeferEndReplication,
TEXT("如果为 true,EndReplication 调用将延迟到应用状态数据之后。默认 true。"));
// 🔗 是否分发之前收到的未解析变更static bool bDispatchUnresolvedPreviouslyReceivedChanges = false;
static FAutoConsoleVariableRef CvarDispatchUnresolvedPreviouslyReceivedChanges(
TEXT("net.Iris.DispatchUnresolvedPreviouslyReceivedChanges"),
bDispatchUnresolvedPreviouslyReceivedChanges,
TEXT("是否在应用状态数据时包含之前收到的带未解析引用的变更。\n"
"这可能导致 RepNotify 函数被调用,即使值未变化。默认 false。"));
// 🔄 是否重映射动态对象static bool bRemapDynamicObjects = true;
static FAutoConsoleVariableRef CvarRemapDynamicObjects(
TEXT("net.Iris.RemapDynamicObjects"),
bRemapDynamicObjects,
TEXT("允许在接收端重映射动态对象。\n"
"如果对象被重新创建,之前指向该对象的属性会被更新。默认 true。"));📈 监控指标
CPP
// 关键性能指标struct FDataStreamStats
{
uint32 PacketsSent; // 发送的数据包数
uint32 PacketsLost; // 丢失的数据包数
uint32 BytesSent; // 发送的字节数
uint32 ObjectsReplicated; // 复制的对象数
uint32 HugeObjectsInTransit; // 传输中的巨型对象数
float AverageLatency; // 平均延迟
float PacketLossRate; // 丢包率
};
// 📊 CSV 统计指标(可在 Unreal Insights 中查看)// Iris/ScheduledForReplicationRootObjectCount - 计划复制的根对象数// Iris/ReplicatedRootObjectCount - 已复制的根对象数// Iris/ActiveHugeObjectCount - 活跃的巨型对象数// Iris/HugeObjectsWaitingForAckCount - 等待确认的巨型对象数// Iris/HugeObjectsStallingCount - 阻塞的巨型对象数// Iris/ReplicatingConnectionCount - 正在复制的连接数// Iris/HugeObjectWaitingForAckTimeInSeconds - 等待确认的时间// Iris/HugeObjectStallingTimeInSeconds - 阻塞的时间🔍 调试命令大全
CPP
// 🔍 调试命令// net.Iris.LogReplicationWriter 1 // 启用 Writer 日志// net.Iris.LogReplicationReader 1 // 启用 Reader 日志// net.Iris.UseResolvingHandleCache 0 // 禁用引用解析缓存// net.Iris.HotResolvingLifetimeMS 2000 // 调整热缓存生命周期// net.Iris.ColdResolvingRetryTimeMS 500 // 调整冷缓存重试间隔// net.Iris.ReplicationWriterMaxHugeObjectsInTransit 8 // 减少巨型对象并发数// net.Iris.WarnAboutDroppedAttachmentsToObjectsNotInScope 1 // 启用丢弃警告📊 性能分析流程
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────┐
│ 📊 性能分析工作流程 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 启用统计收集 │
│ ├─ 打开 Unreal Insights │
│ └─ 或使用 CSV Profiler │
│ │
│ 2️⃣ 运行游戏并收集数据 │
│ ├─ 关注 Iris/ 开头的统计指标 │
│ └─ 特别注意 HugeObjectsStallingCount │
│ │
│ 3️⃣ 分析瓶颈 │
│ ├─ 带宽不足 → 检查 BytesSent, 考虑增量压缩 │
│ ├─ 延迟高 → 检查优先级设置, 调整 SchedulingThresholdPriority │
│ ├─ 巨型对象阻塞 → 减少 MaxHugeObjectsInTransit │
│ └─ 引用解析慢 → 调整 HotResolvingLifetimeMS │
│ │
│ 4️⃣ 应用优化 │
│ ├─ 调整配置参数 │
│ ├─ 优化对象结构 │
│ └─ 重新测试验证 │
│ │
└─────────────────────────────────────────────────────────────────────┘🎮 优化案例:大型多人游戏
CPP
// 🎮 场景:100 人大逃杀游戏,需要优化网络性能
// ❌ 优化前的问题:// - 所有玩家都以相同频率更新// - 远处玩家占用大量带宽// - 巨型对象阻塞普通复制
// ✅ 优化后的配置:
// 1. 使用空间过滤器,只复制附近玩家void AMyGameMode::ConfigureIris(){
// 配置网格过滤器
UNetObjectGridFilterConfig* GridConfig = GetMutableDefault<UNetObjectGridFilterConfig>();
GridConfig->CellSizeX = 5000.0f; // 50 米的网格
GridConfig->CellSizeY = 5000.0f;
GridConfig->MaxCullDistance = 50000.0f; // 500 米最大距离
}
// 2. 使用球形优先级器,近处玩家优先// 在 DefaultIris.ini 中配置:// [/Script/IrisCore.SphereNetObjectPrioritizer]// InnerRadius=1000.0// OuterRadius=10000.0// InnerPriority=1.0// OuterPriority=0.1// OutsidePriority=0.01
// 3. 限制巨型对象// net.Iris.ReplicationWriterMaxHugeObjectsInTransit 4
// 4. 对大型数据使用独立传输UCLASS()
class APlayerCustomization : public AActor
{
// 不复制大型数据,使用 ID 引用
UPROPERTY(Replicated)
int32 SkinId;
// 皮肤数据通过 HTTP 下载
void OnRep_SkinId()
{
SkinManager->LoadSkinAsync(SkinId);
}
};📚 8.10 小结
🎯 核心概念回顾
PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐│ 📦 数据流系统核心组件 │├─────────────────────────────────────────────────────────────────┤│ ││ UDataStreamManager ││ ├── 管理多个 DataStream ││ ├── 协调发送顺序 ││ ├── 追踪投递状态 ││ └── 最多支持 32 个数据流 ││ ││ UDataStream (基类) ││ ├── BeginWrite / WriteData / EndWrite ││ ├── ReadData ││ ├── ProcessPacketDeliveryStatus ││ └── EWriteResult: NoData / Ok / HasMoreData ││ ││ FReplicationWriter (发送端) ││ ├── 对象状态机管理 (14 种状态) ││ ├── 优先级调度 (部分排序优化) ││ ├── 序列化数据 ││ ├── 巨型对象处理 ││ └── FReplicationInfo (16 字节/对象) ││ ││ FReplicationReader (接收端) ││ ├── 解析数据包 ││ ├── 创建/更新对象 ││ ├── 引用解析(热/冷缓存) ││ ├── RepNotify 触发 ││ └── FReplicatedObjectInfo ││ ││ FPartialNetBlob (分片系统) ││ ├── 将大数据分割成小片段 ││ ├── 默认每片 1024 bits ││ └── FNetBlobAssembler 负责组装 ││ │└─────────────────────────────────────────────────────────────────┘📊 关键数据结构对比
结构 | 所属端 | 大小 | 主要用途 |
|---|---|---|---|
FReplicationInfo | Writer | 16 bytes | 发送端对象状态追踪 |
FReplicatedObjectInfo | Reader | ~100 bytes | 接收端对象状态追踪 |
FDispatchObjectInfo | Reader | ~16 bytes | 待分发对象临时信息 |
FDataStreamRecord | Manager | 可变 | 数据包追踪记录 |
FPartialNetBlob | 分片 | 可变 | 巨型对象分片 |
🔄 状态机转换速查
PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 📊 对象状态转换速查表 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 正常生命周期: │
│ Invalid → PendingCreate → WaitOnCreateConfirmation → Created │
│ │
│ 销毁流程: │
│ Created → WaitOnFlush → PendingDestroy → │
│ WaitOnDestroyConfirmation → Destroyed → Invalid │
│ │
│ 子对象销毁: │
│ Created → SubObjectPendingDestroy → PendingDestroy → ... │
│ │
│ 断开(TearOff): │
│ Created → WaitOnFlush → PendingTearOff → Destroyed │
│ │
│ 取消销毁: │
│ WaitOnDestroyConfirmation → CancelPendingDestroy → Created │
│ │
└─────────────────────────────────────────────────────────────────┘📁 关键源文件索引
文件 | 路径 | 说明 |
|---|---|---|
DataStream.h |
| DataStream 基类定义 |
DataStreamManager.h/cpp |
| 数据流管理器 |
ReplicationWriter.h/cpp |
| 发送端实现(~1400 行) |
ReplicationReader.h/cpp |
| 接收端实现(~2500 行) |
ReplicationDataStream.h/cpp |
| 复制数据流 |
NetTokenDataStream.h/cpp |
| Token 数据流 |
PartialNetBlob.h/cpp |
| 分片 Blob |
NetBlobAssembler.h/cpp |
| Blob 组装器 |
NetStats.h/cpp |
| 统计信息 |
🎓 知识点检查清单
PLAINTEXT
✅ 理解 DataStream 的三种写入结果:NoData / Ok / HasMoreData
✅ 掌握 DataStreamManager 的数据包结构
✅ 理解 ReplicationWriter 的 14 种对象状态
✅ 掌握 FReplicationInfo 的 16 字节设计
✅ 理解优先级调度和部分排序优化
✅ 掌握 ReplicationReader 的读取流程
✅ 理解热/冷缓存机制处理未解析引用
✅ 掌握 ACK/NAK 投递确认机制
✅ 理解巨型对象的分片和组装流程
✅ 掌握关键配置参数和调试命令🎮 下一步学习建议
深入序列化系统:了解数据如何被打包成二进制(第七部分)
学习 NetBlob 系统:理解 RPC 和大数据块的传输(第九部分)
掌握增量压缩:优化带宽使用(第十部分)
实践调试:使用日志和统计工具分析网络性能(第十五部分)
阅读源码:从
ReplicationWriter::Write()和ReplicationReader::Read()开始
💡 常见问题 FAQ
Q: 为什么我的对象没有被复制?
A: 检查以下几点:
对象是否在 Scope 内(过滤器配置)
对象优先级是否达到阈值
对象状态是否为 Created
是否有足够的带宽
Q: 引用解析失败怎么办?
A: 检查:
被引用对象是否已复制
热缓存生命周期是否足够(
net.Iris.HotResolvingLifetimeMS)是否有循环引用
Q: 巨型对象传输太慢?
A: 优化建议:
减少巨型对象大小
增加
MaxHugeObjectsInTransit使用独立下载通道传输大数据
本文档基于 Unreal Engine 5.5.0 Iris 源代码分析(源码目录:Engine/Source/Runtime/Experimental/Iris/)
