🔗 Iris 网络复制系统技术分析 - 第十二部分:对象引用与依赖

📖 本章导读:在网络游戏中,对象之间的关系就像一张复杂的蜘蛛网。玩家持有武器、武器上挂载着瞄准镜、瞄准镜又引用着材质资源...这些"谁引用谁"、"谁依赖谁"的关系,正是本章要深入探讨的核心内容。
🎯 12.1 对象引用与依赖概述
💡 什么是对象引用?—— 快递单号的故事
想象你在网上购物:
PLAINTEXT
📦 你的订单
├── 商品:iPhone 15 Pro
├── 配件:充电器(引用另一个包裹)
├── 赠品:手机壳(引用仓库库存)
└── 快递单号:SF1234567890 ← 这就是"引用"!在网络游戏中,对象引用就像快递单号:
🏷️ 快递单号 =
FNetRefHandle(网络对象句柄)📦 包裹内容 =
UObject(游戏对象)🏭 物流系统 =
ObjectReferenceCache(引用缓存)
当服务器告诉客户端"玩家 A 正在持有武器 B"时,它不会把整个武器对象发过去,而是发送武器的"快递单号",客户端根据单号找到对应的武器对象。
🔗 什么是对象依赖?—— 俄罗斯套娃的秘密
PLAINTEXT
🎮 游戏中的依赖关系示例
玩家角色 (APlayerCharacter)
│
├──→ 武器组件 (UWeaponComponent) [子对象]
│ │
│ └──→ 弹药数据 (UAmmoData) [依赖对象]
│
├──→ 背包组件 (UInventoryComponent) [子对象]
│ │
│ └──→ 物品列表 [...] [依赖对象]
│
└──→ 载具引用 (AVehicle*) [对象引用]依赖关系决定了对象的复制顺序:
📌 子对象 (SubObject):生命周期完全依附于父对象
🔗 依赖对象 (DependentObject):复制时需要保证顺序
📎 对象引用 (Reference):只是"指向"另一个对象
🏗️ 核心组件架构图
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────┐
│ 对象引用与依赖系统架构 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ ObjectReferenceCache │ │ NetDependencyData │ │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ Object→Handle │ │ │ │ SubObjects │ │ │
│ │ │ 正向映射 │ │ │ │ 子对象表 │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ Handle→Object │ │ │ │ ChildSubObjects │ │ │
│ │ │ 反向映射 │ │ │ │ 子子对象表 │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ │ ┌─────────────────┐ │ │ ┌─────────────────┐ │ │
│ │ │ PendingAsync │ │ │ │ DependentObjects│ │ │
│ │ │ 异步加载队列 │ │ │ │ 依赖对象表 │ │ │
│ │ └─────────────────┘ │ │ └─────────────────┘ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │ │ │
│ └──────────────┬───────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ NetTokenStore │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ String │ │ Name │ │ Path │ │ │
│ │ │ Tokens │ │ Tokens │ │ Tokens │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ NetObjectFactory 系统 │ │
│ │ ┌────────────────┐ ┌────────────────────┐ │ │
│ │ │ NetActorFactory│ │ NetSubObjectFactory│ │ │
│ │ │ • 静态 Actor │ │ • 静态子对象 │ │ │
│ │ │ • 动态 Actor │ │ • 动态子对象 │ │ │
│ │ └────────────────┘ └────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘📂 关键源文件索引
组件 | 头文件路径 | 说明 |
|---|---|---|
ObjectReferenceCache |
| 对象引用缓存 |
NetDependencyData |
| 依赖数据管理 |
NetTokenStore |
| 令牌存储系统 |
NetObjectFactory |
| 对象工厂基类 |
NetActorFactory |
| Actor 工厂 |
🗃️ 12.2 ObjectReferenceCache 深度剖析
💡 引用缓存的职责 —— 图书馆的索引系统
想象一个大型图书馆:
PLAINTEXT
📚 图书馆索引系统
┌─────────────────────────────────────────────────────────────┐
│ 图书馆 (游戏世界) │
├─────────────────────────────────────────────────────────────┤
│ │
│ 索引卡片柜 (ObjectReferenceCache) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 索书号 → 书籍位置 │ │
│ │ ───────────────── │ │
│ │ A001 → 科幻区-3排-5层 │ │
│ │ B042 → 历史区-1排-2层 │ │
│ │ C103 → 儿童区-2排-1层 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 书籍位置 → 索书号 (反向索引) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 科幻区-3排-5层 → A001 │ │
│ │ 历史区-1排-2层 → B042 │ │
│ │ 儿童区-2排-1层 → C103 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
🔍 借书流程:
读者说 "我要借 A001"
→ 查索引卡片柜
→ 找到 "科幻区-3排-5层"
→ 取书给读者🏗️ 核心数据结构详解
CPP
// 📁 文件: ObjectReferenceCache.hnamespace UE::Net::Private
{
// 缓存的网络对象引用 - 存储对象的完整引用信息struct FCachedNetObjectReference
{
TWeakObjectPtr<UObject> Object; // 🎯 弱引用指向实际对象(防止阻止GC)
const UObject* ObjectKey = nullptr; // 🔑 用于快速查找的对象指针键
FNetRefHandle NetRefHandle; // 📇 网络引用句柄(对象的身份证)
FNetToken RelativePath; // 📍 相对路径令牌
FNetRefHandle OuterNetRefHandle; // 🔗 外部对象的句柄(父对象)
// 📌 状态标志位(每个只占1bit,节省内存)
uint8 bNoLoad : 1; // 🚫 客户端不需要加载
uint8 bIgnoreWhenMissing : 1; // 🤷 找不到时不报警告
uint8 bIsPackage : 1; // 📦 是否是包
uint8 bIsBroken : 1; // 💔 引用是否已损坏
uint8 bIsPending : 1; // ⏳ 是否正在异步加载
};
class FObjectReferenceCache
{
public:
void Init(UReplicationSystem* ReplicationSystem);
// 🔍 对象类型判断
bool IsDynamicObject(const UObject* Object) const; // 是否为动态对象
bool IsAuthority() const; // 是否有权限创建新Handle(服务器端)
// 📝 Handle 创建与查找
FNetRefHandle CreateObjectReferenceHandle(const UObject* Object);
FNetRefHandle GetObjectReferenceHandleFromObject(const UObject* Object,
EGetRefHandleFlags Flags = EGetRefHandleFlags::None) const;
UObject* GetObjectFromReferenceHandle(FNetRefHandle RefHandle);
// 🔄 引用解析
ENetObjectReferenceResolveResult ResolveObjectReference(
const FNetObjectReference& Reference,
const FNetObjectResolveContext& ResolveContext,
UObject*& OutResolvedObject);
// 📡 远程引用管理
void AddRemoteReference(FNetRefHandle RefHandle, const UObject* Object);
void RemoveReference(FNetRefHandle RefHandle, const UObject* Object);
private:
// 💾 核心数据存储
TMap<const UObject*, FNetRefHandle> ObjectToNetReferenceHandle; // 正向索引
TMap<FNetRefHandle, FCachedNetObjectReference> ReferenceHandleToCachedReference; // 反向索引
TMap<FName, FPendingAsyncLoadRequest> PendingAsyncLoadRequests; // 异步加载队列
bool bIsAuthority; // 是否为服务器
};
}🔄 引用解析流程图
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────┐│ 对象引用解析完整流程 │├─────────────────────────────────────────────────────────────────────────┤│ ││ 输入: FNetRefHandle RefHandle ││ │ ││ ▼ ││ ┌─────────────────────────────────────────────────────────────────┐ ││ │ 步骤 1: 查找缓存 │ ││ │ CacheObjectPtr = ReferenceHandleToCachedReference.Find(RefHandle)│ ││ └───────────────────────────┬─────────────────────────────────────┘ ││ │ ││ ┌───────────────────┴───────────────────┐ ││ │ │ ││ ▼ 未找到 ▼ 找到 ││ ┌───────────────┐ ┌───────────────────────┐ ││ │ 返回 nullptr │ │ 步骤 2: 检查对象有效性 │ ││ │ (未知引用) │ │ Object = CacheObj.Get()│ ││ └───────────────┘ └───────────┬───────────┘ ││ │ ││ ┌─────────────────────────┴─────────────┐ ││ │ │ ││ ▼ Object != nullptr ▼ null ││ ┌──────────────┐ ┌──────────────┐ ││ │ 🎉 直接返回 │ │ 步骤 3: 检查 │ ││ │ Object │ │ 状态标志 │ ││ └──────────────┘ └──────┬───────┘ ││ │ ││ ┌─────────────────────────────────────────┤ ││ │ │ │ ││ ▼ bIsBroken ▼ bIsPending ▼ 其他 ││ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ││ │ 返回 nullptr │ │ 返回 nullptr │ │ 步骤 4: 尝试 │ ││ │ (已损坏) │ │ (等待加载) │ │ 加载对象 │ ││ └──────────────┘ └──────────────┘ └──────┬───────┘ ││ │ ││ ▼ ││ ┌─────────────────────────────────────────────────────────────────┐ ││ │ 步骤 5: 解析外部对象 (Outer) 并加载 │ ││ │ • 静态对象: FindObjectFast / StaticLoadObject │ ││ │ • 包对象: LoadPackage (同步) 或 LoadPackageAsync (异步) │ ││ │ • 更新缓存并返回 │ ││ └─────────────────────────────────────────────────────────────────┘ ││ │└─────────────────────────────────────────────────────────────────────────┘📝 静态对象 vs 动态对象
特性 | 静态对象 🏛️ | 动态对象 ⚡ |
|---|---|---|
来源 | 地图中预放置 | 运行时 SpawnActor |
网络稳定性 |
|
|
标识方式 | 路径 + Handle | 仅 Handle |
客户端加载 | 可独立加载 | 必须等服务器创建 |
Handle 类型 |
|
|
典型例子 | 关卡建筑、触发器 | 玩家角色、子弹、掉落物 |
🔧 异步加载机制
CPP
// 异步加载请求结构struct FPendingAsyncLoadRequest
{
TArray<FNetRefHandle> NetRefHandles; // 等待此包加载的所有 Handle
double RequestStartTime; // 请求开始时间
void Merge(FNetRefHandle InNetRefHandle)
{
NetRefHandles.AddUnique(InNetRefHandle);
}
};
// 启动异步加载void FObjectReferenceCache::StartAsyncLoadingPackage(...){
CacheObject.bIsPending = true;
LoadPackageAsync(PackagePath.ToString(),
FLoadPackageAsyncDelegate::CreateWeakLambda(ReplicationSystem,
[this](const FName& PackageName, UPackage* Package, EAsyncLoadingResult::Type Result)
{
AsyncPackageCallback(PackageName, Package, Result);
}
)
);
}🔗 12.3 NetDependencyData 依赖管理机制
💡 依赖数据的职责 —— 家族族谱管理
PLAINTEXT
👨👩👧👦 家族族谱系统
┌─────────────┐
│ 爷爷 │ ← RootObject (AActor)
└──────┬──────┘
│
┌──────────────┼──────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ 爸爸 │ │ 叔叔 │ │ 姑姑 │ ← SubObjects
│(UComp1) │ │(UComp2) │ │(UComp3) │
└────┬────┘ └─────────┘ └─────────┘
│
┌────▼────┐
│ 孙子 │ ← ChildSubObject
│(UChild) │
└─────────┘
📋 族谱记录的信息:
• 谁是谁的孩子(父子关系)
• 谁依赖谁(复制顺序)
• 特殊条件(只在某些情况下复制)🏗️ 核心数据结构
CPP
// 📁 文件: NetDependencyData.h
// 依赖对象调度提示 - 控制依赖对象的复制顺序enum class EDependentObjectSchedulingHint : uint8
{
Default = 0, // 默认:与父对象同批次复制
ScheduleBeforeParent, // 必须在父对象之前复制
ScheduleBeforeParentIfInitialState, // 首次在前,后续同批次
};
// 依赖对象信息struct FDependentObjectInfo
{
FInternalNetRefIndex NetRefIndex = 0U;
EDependentObjectSchedulingHint SchedulingHint = EDependentObjectSchedulingHint::Default;
};
class FNetDependencyData
{
public:
typedef TArray<FInternalNetRefIndex, TInlineAllocator<8>> FInternalNetRefIndexArray;
typedef TArray<FDependentObjectInfo, TInlineAllocator<8>> FDependentObjectInfoArray;
enum EArrayType { SubObjects = 0U, ChildSubObjects, DependentParentObjects, Count };
// 获取或创建索引数组
FInternalNetRefIndexArray& GetOrCreateInternalIndexArray(
FInternalNetRefIndex OwnerIndex, EArrayType ArrayType);
// 获取或创建依赖对象信息数组
FDependentObjectInfoArray& GetOrCreateDependentObjectInfoArray(FInternalNetRefIndex InternalIndex);
// 释放对象的依赖数据
void FreeStoredDependencyDataForObject(FInternalNetRefIndex InternalIndex);
private:
TMap<FInternalNetRefIndex, FDependencyInfo> DependencyInfos;
TSparseArray<FInternalNetRefIndexArray> DependentObjectsStorage;
TSparseArray<FDependentObjectInfoArray> DependentObjectInfosStorage;
};📊 EDependentObjectSchedulingHint 详解
提示类型 | 复制顺序 | 适用场景 |
|---|---|---|
Default | 与父对象同批次,顺序不保证 | 背包物品、装饰组件 |
ScheduleBeforeParent | 必须在父对象之前 | 武器的弹药数据、配置对象 |
ScheduleBeforeParentIfInitialState | 首次在前,后续同批次 | 技能系统、AI行为树 |
PLAINTEXT
调度示例:
场景:武器组件依赖弹药数据
ScheduleBeforeParent:
[弹药数据] → [武器组件] → [玩家角色]
↑
必须先复制,因为武器的 OnRep 需要读取弹药数据
Default:
[玩家角色] → [武器组件] → [弹药数据] (或其他顺序)
↑
顺序不保证,适用于无依赖关系的情况🏷️ 12.4 NetTokenStore 令牌系统详解
💡 令牌存储的职责 —— 翻译词典系统
PLAINTEXT
🌍 国际会议翻译系统
┌─────────────────────────────────────────────────────────────────────────┐
│ 翻译词典 │
├─────────────────────────────────────────────────────────────────────────┤
│ 完整词汇 (Full String) 令牌 (Token) │
│ ───────────────────── ──────────── │
│ "Hello, how are you?" ←→ T001 │
│ "/Game/Maps/Level01" ←→ T004 │
│ "BP_PlayerCharacter_C" ←→ T005 │
├─────────────────────────────────────────────────────────────────────────┤
│ 🎯 使用场景: │
│ 首次发送:完整字符串 + 令牌(64字节) │
│ 后续发送:只发送令牌(4字节) │
│ 💰 节省:93.75% 带宽! │
└─────────────────────────────────────────────────────────────────────────┘🏗️ 核心数据结构
CPP
// 📁 文件: NetTokenStore.h
class FNetToken
{
public:
enum class ENetTokenAuthority : uint8 { Authority, None };
static constexpr uint32 InvalidTokenIndex = 0U;
static constexpr uint32 MaxNetTokenCount = (1U << 24); // 1600万个令牌
bool IsValid() const { return TokenIndex != InvalidTokenIndex; }
uint32 GetIndex() const { return TokenIndex; }
bool IsAssignedByAuthority() const { return bIsAssignedByAuthority; }
private:
uint32 TokenIndex;
FTypeId TypeId;
bool bIsAssignedByAuthority;
};
class FNetTokenStore
{
public:
void Init(FInitParams& InitParams);
bool IsAuthority() const;
// 数据存储管理
bool RegisterDataStore(TUniquePtr<FNetTokenDataStore> DataStore, FName TokenStoreName);
template<typename T> T* GetDataStore();
// 条件写入:只在接收方不知道时写入完整数据
void ConditionalWriteNetTokenData(FNetSerializationContext& Context,
Private::FNetExportContext* ExportContext, const FNetToken NetToken) const;
void ConditionalReadNetTokenData(FNetSerializationContext& Context, const FNetToken NetToken);
private:
TUniquePtr<FNetTokenStoreState> LocalNetTokenStoreState;
TArray<TUniquePtr<FNetTokenStoreState>> RemoteNetTokenStoreStates;
TArray<TTuple<FName, TUniquePtr<FNetTokenDataStore>>> TokenDataStores;
};📊 令牌类型与带宽节省
令牌类型 | 存储内容 | 使用场景 |
|---|---|---|
Name Token | FName 值 | 类名、属性名、函数名 |
String Token | FString 值 | 玩家名称、聊天消息 |
Path Token | 对象路径 | 资源引用、蓝图类路径 |
数据类型 | 原始大小 | 令牌大小 | 节省比例 |
|---|---|---|---|
FName (短) | 8 字节 | 4 字节 | 50% |
FName (长) | 32 字节 | 4 字节 | 87.5% |
对象路径 (短) | 64 字节 | 4 字节 | 93.75% |
对象路径 (长) | 256 字节 | 4 字节 | 98.4% |
🏭 12.5 NetObjectFactory 对象工厂系统
💡 对象工厂的职责 —— 汽车制造厂
PLAINTEXT
🚗 汽车制造厂系统
┌─────────────────────────────────────────────────────────────────────────┐
│ 📋 订单(创建头信息) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 车型:SUV / 颜色:红色 / 配置:高配 │ │
│ │ 生产线:A线(动态)/ B线(静态) │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 🏭 生产线(工厂) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ A线:动态生产 │ │ B线:静态查找 │ │
│ │ • SpawnActor │ │ • FindObject │ │
│ │ • 设置位置旋转 │ │ • 验证存在 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │ │
│ ▼ │
│ 🚗 成品(实例化的对象) │
└─────────────────────────────────────────────────────────────────────────┘🏗️ 工厂系统架构
PLAINTEXT
┌─────────────────────┐
│ UNetObjectFactory │ ← 抽象基类
└──────────┬──────────┘
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ UNetActorFactory │ │UNetSubObject │ │ 自定义工厂... │
│ • 静态 Actor │ │ Factory │ │ │
│ • 动态 Actor │ │ • 静态子对象 │ │ │
└──────────────────┘ │ • 动态子对象 │ └──────────────────┘
└──────────────┘
🏗️ 核心数据结构
CPP
// 动态 Actor 创建头信息class FDynamicActorNetCreationHeader : public FBaseActorNetCreationHeader
{
public:
virtual bool IsDynamic() const override { return true; }
struct FActorNetSpawnInfo
{
FVector Location; // 生成位置
FRotator Rotation; // 生成旋转
FVector Scale; // 生成缩放
FVector Velocity; // 初始速度
};
FActorNetSpawnInfo SpawnInfo;
FNetObjectReference ArchetypeReference; // 原型引用(蓝图类)
FNetObjectReference LevelReference; // 关卡引用
bool bUsePersistentLevel = false;
};
UCLASS(abstract)
class UNetObjectFactory : public UObject
{
public:
// 创建头信息(服务器端)
TUniquePtr<FNetObjectCreationHeader> CreateHeader(FNetRefHandle Handle, ...);
// 写入/读取头信息
bool WriteHeader(FNetRefHandle Handle, FNetSerializationContext& Context, ...);
TUniquePtr<FNetObjectCreationHeader> ReadHeader(FNetRefHandle Handle, ...);
// 从头信息实例化对象(客户端)
virtual FInstantiateResult InstantiateReplicatedObjectFromHeader(...) PURE_VIRTUAL;
// 生命周期回调
virtual void PostInstantiation(const FPostInstantiationContext& Context) {}
virtual void PostInit(const FPostInitContext& Context) {}
};🔄 对象创建完整流程
PLAINTEXT
═══════════════════ 服务器端 ═══════════════════
1. 游戏代码生成 Actor
AEnemy* Enemy = World->SpawnActor<AEnemy>(...);
│
▼
2. 复制系统注册对象
Bridge->BeginReplication(Enemy);
→ 分配 NetRefHandle
→ 选择工厂 (NetActorFactory)
│
▼
3. 工厂创建头信息
Factory->CreateAndFillHeader(Handle);
→ 填充 SpawnInfo (位置、旋转、缩放)
→ 填充 ArchetypeReference (蓝图类)
│
▼
4. 序列化并发送
Factory->WriteHeader(Handle, Context, Header);
══════════════════ 网络传输 ══════════════════
═══════════════════ 客户端 ═══════════════════
5. 接收并反序列化头信息
Header = Factory->ReadHeader(Handle, Context);
│
▼
6. 实例化对象
Result = Factory->InstantiateReplicatedObjectFromHeader(...);
→ 加载蓝图类
→ SpawnActorAbsolute(...)
→ 设置速度和缩放
│
▼
7. 后处理回调
Factory->PostInstantiation(Context); // OnActorChannelOpen
Factory->PostInit(Context); // PostNetInit🎮 12.6 实际应用案例与代码演练
案例 1:FPS 游戏武器系统
CPP
UCLASS()
class APlayerCharacter : public ACharacter
{
GENERATED_BODY()
public:
// 武器组件 - 作为子对象复制
UPROPERTY(Replicated)
UWeaponComponent* WeaponComponent;
// 当前瞄准的目标 - 作为对象引用复制
UPROPERTY(Replicated)
AActor* CurrentTarget;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(APlayerCharacter, WeaponComponent);
DOREPLIFETIME(APlayerCharacter, CurrentTarget);
}
};
// 服务器端:创建武器void AMyGameMode::SpawnWeaponForPlayer(APlayerCharacter* Player){
AWeapon* Weapon = GetWorld()->SpawnActor<AWeapon>(WeaponClass, ...);
// Iris 自动:
// 1. 为 Weapon 创建 NetRefHandle
// 2. 当复制 Player->EquippedWeapon 时,只发送 Handle
// 3. 客户端根据 Handle 找到/创建对应的 Weapon
Player->EquippedWeapon = Weapon;
}案例 2:处理引用解析失败
CPP
UFUNCTION()
void UMyComponent::OnRep_TargetActor(){
// TargetActor 可能还未在客户端创建
if (TargetActor == nullptr)
{
// 这是正常的!Iris 会在 TargetActor 创建后自动重新解析
UE_LOG(LogGame, Verbose, TEXT("TargetActor pending resolution..."));
return;
}
// 引用已解析,可以安全使用
DoSomethingWithTarget(TargetActor);
}🚀 12.7 高级主题与性能优化
⚡ 异步加载最佳实践
CPP
// 控制异步加载行为// CVar: net.iris.AllowAsyncLoading (默认 true)// CVar: net.AllowAsyncLoading (全局开关)
// 异步加载模式enum class EAsyncLoadMode
{
UseCVar, // 使用 CVar 设置
ForceDisable, // 强制禁用
ForceEnable, // 强制启用
};
void FObjectReferenceCache::SetAsyncLoadMode(EAsyncLoadMode NewMode);🔒 递归限制保护
CPP
// 防止恶意数据包导致栈溢出static const int INTERNAL_READ_REF_RECURSION_LIMIT = 16;
void FObjectReferenceCache::ReadFullReferenceInternal(..., uint32 RecursionCount){
if (RecursionCount > INTERNAL_READ_REF_RECURSION_LIMIT)
{
UE_LOG(LogIris, Warning, TEXT("ReadFullReferenceInternal: Hit recursion limit."));
Reader->DoOverflow();
return;
}
// ...
}📊 性能监控指标
指标 | 说明 | 优化建议 |
|---|---|---|
缓存命中率 | Handle→Object 查找成功率 | 应 > 95% |
异步加载队列长度 | 等待加载的包数量 | 应 < 10 |
令牌表大小 | 已分配的令牌数量 | 监控增长趋势 |
引用解析失败率 | 解析返回 nullptr 的比例 | 应 < 5% |
📋 12.8 总结与最佳实践
🎯 核心概念回顾
概念 | 类比 | 作用 |
|---|---|---|
ObjectReferenceCache | 图书馆索引 | 管理对象与句柄的双向映射 |
NetDependencyData | 家族族谱 | 管理对象间的依赖关系 |
NetTokenStore | 翻译词典 | 压缩字符串/路径传输 |
NetObjectFactory | 汽车工厂 | 创建和实例化网络对象 |
✅ 最佳实践清单
PLAINTEXT
📌 对象引用
✅ 优先使用弱引用 (TWeakObjectPtr) 避免阻止 GC
✅ 静态对象使用路径引用,动态对象使用句柄引用
✅ 处理引用解析失败的情况 (nullptr 检查)
📌 依赖关系
✅ 保持依赖层级简单(最多 2-3 层)
✅ 使用 Default 提示,除非确实需要顺序保证
✅ 避免循环依赖
📌 令牌系统
✅ 对频繁使用的字符串优先使用令牌
✅ 预热常用令牌(连接建立时发送)
✅ 监控令牌表大小
📌 对象工厂
✅ 为特殊对象类型创建自定义工厂
✅ 在 PostInit 中处理依赖初始化
✅ 正确处理异步加载场景⚠️ 常见问题与解决方案
问题 | 原因 | 解决方案 |
|---|---|---|
引用解析返回 nullptr | 对象还未复制到客户端 | 使用 |
循环引用导致死锁 | A 引用 B,B 引用 A | Iris 自动处理,无需担心 |
静态对象找不到 | 客户端缺少资源包 | 确保资源正确打包 |
依赖对象顺序错误 | SchedulingHint 配置不当 | 使用 ScheduleBeforeParent |
异步加载超时 | 包太大或网络慢 | 监控加载时间,优化包大小 |
本文档基于 Unreal Engine 5.5.0 Iris 源代码分析(源码目录:Engine/Source/Runtime/Experimental/Iris/)