🔍 Iris 网络复制系统技术分析 - 第五部分:过滤系统 (Filtering)

想象一下,你是一个超级忙碌的快递员,要给一个大型小区的1000户居民送快递。如果你每次都把所有快递都带上,挨家挨户问"这个是你的吗?"——那你一天也送不完几单!聪明的做法是:先筛选出每个区域需要的快递,只带相关的包裹去对应的楼栋。
Iris 的过滤系统就是这样一个"智能快递分拣员"🚀,它帮助服务器决定:哪些游戏对象需要同步给哪些玩家。
📚 5.1 过滤系统概述
🎯 过滤的目的与意义
在多人游戏中,服务器上可能存在成千上万个需要同步的对象(玩家、NPC、道具、子弹等)。如果把所有对象的数据都发送给每个玩家,会导致:
问题 | 后果 |
|---|---|
🔥 带宽爆炸 | 网络拥堵,玩家卡顿 |
💻 客户端过载 | CPU/内存吃不消 |
🔓 安全隐患 | 玩家可能通过作弊看到不该看到的信息 |
⚡ 延迟增加 | 重要数据被不重要的数据挤占 |
过滤系统的核心任务:为每个连接(玩家)筛选出"相关"的对象,只同步真正需要的数据。
PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 🌍 服务器上的所有对象 │
│ [玩家A] [玩家B] [玩家C] [NPC1] [NPC2] [道具1] [道具2] [子弹]... │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ 🔍 过滤系统 │
│ "谁需要什么?" │
└─────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 玩家A连接 │ │ 玩家B连接 │ │ 玩家C连接 │
│ [A][NPC1] │ │ [B][NPC2] │ │ [C][道具1] │
│ [道具2] │ │ [子弹] │ │ [NPC1] │
└─────────────┘ └─────────────┘ └─────────────┘🏷️ 过滤器类型分类
Iris 提供了多种过滤器,就像快递分拣有不同的规则一样:
过滤器类型 | 比喻 | 用途 |
|---|---|---|
空间过滤器 (GridFilter) | 📍 "只送本小区的快递" | 基于距离/位置过滤 |
所有者过滤 (Owner) | 🔐 "只有收件人能收" | 只同步给拥有者 |
连接过滤 (Connection) | 📋 "VIP专属配送" | 针对特定连接的过滤 |
组过滤 (Group) | 🏢 "按楼栋分批送" | 基于分组的批量过滤 |
空操作过滤 (Nop) | ✅ "全部放行" | 不做任何过滤 |
全部过滤 (FilterOut) | ❌ "全部拦截" | 过滤掉所有对象 |
⏱️ 过滤流程时序
每一帧,过滤系统都会按照以下顺序执行:
PLAINTEXT
┌────────────────────────────────────────────────────────────────────┐
│ 📊 每帧过滤流程 │
├────────────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ ResetRemovedConnections() ← 清理已断开的连接 │
│ ↓ │
│ 2️⃣ InitNewConnections() ← 初始化新连接 │
│ ↓ │
│ 3️⃣ UpdateObjectsInScope() ← 更新范围内的对象 │
│ ↓ │
│ 4️⃣ UpdateGroupExclusionFiltering() ← 组排除过滤(优先级最高) │
│ ↓ │
│ 5️⃣ UpdateGroupInclusionFiltering() ← 组包含过滤 │
│ ↓ │
│ 6️⃣ UpdateOwnerAndConnectionFiltering() ← 所有者和连接过滤 │
│ ↓ │
│ 7️⃣ UpdateSubObjectFilters() ← 子对象过滤 │
│ ↓ │
│ 8️⃣ PreUpdateObjectScopeHysteresis() ← 滞后预处理 │
│ ↓ │
│ 9️⃣ UpdateDynamicFilters() ← 动态过滤器(如空间过滤) │
│ ↓ │
│ 🔟 FilterNonRelevantObjects() ← 最终过滤非相关对象 │
│ │
└────────────────────────────────────────────────────────────────────┘💡 小贴士:过滤是有优先级的!组排除过滤优先级最高——如果一个对象被组排除了,后面的过滤器都不会再考虑它。
🧱 5.2 UNetObjectFilter 基类
UNetObjectFilter 是所有过滤器的"老祖宗",定义了过滤器必须遵守的"家规"。理解这个基类是深入掌握整个过滤系统的关键。
📋 Filter 接口定义
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectFilter.h
UCLASS(Abstract, MinimalAPI)
class UNetObjectFilter : public UObject
{
GENERATED_BODY()
public:
// 🎬 生命周期
void Init(const FNetObjectFilterInitParams& Params);
void Deinit();
// 🔌 连接管理
virtual void AddConnection(uint32 ConnectionId);
virtual void RemoveConnection(uint32 ConnectionId);
// 📦 对象管理(子类必须实现)
virtual bool AddObject(uint32 ObjectIndex, FNetObjectFilterAddObjectParams&) PURE_VIRTUAL;
virtual void RemoveObject(uint32 ObjectIndex, const FNetObjectFilteringInfo&) PURE_VIRTUAL;
virtual void UpdateObjects(FNetObjectFilterUpdateParams&);
// 🔍 过滤流程
virtual void PreFilter(FNetObjectPreFilteringParams&);
virtual void Filter(FNetObjectFilteringParams&); // ⭐ 核心方法
virtual void PostFilter(FNetObjectPostFilteringParams&);
// 🏷️ 特性查询
ENetFilterTraits GetFilterTraits() const;
bool HasFilterTrait(ENetFilterTraits FilterTrait) const;
protected:
// 子类可访问的成员
const UE::Net::Private::FNetRefHandleManager* NetRefHandleManager = nullptr;
};🔧 初始化参数详解
过滤器初始化时会收到一个 FNetObjectFilterInitParams 结构,包含了过滤器运行所需的所有上下文信息:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectFilter.h
struct FNetObjectFilterInitParams
{
// 所属的复制系统实例
TObjectPtr<UReplicationSystem> ReplicationSystem = nullptr;
// 可选的配置对象(如 UNetObjectGridFilterConfig)
UNetObjectFilterConfig* Config = nullptr;
// 系统支持的最大复制对象数量(绝对上限)
uint32 AbsoluteMaxNetObjectCount = 0;
// 当前最大内部索引(可能在运行时增长)
uint32 CurrentMaxInternalIndex = 0;
// 系统支持的最大连接数
uint32 MaxConnectionCount = 0;
};为什么需要这些参数?
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ 🔧 初始化参数的作用 │├────────────────────────────────────────────────────────────────┤│ ││ AbsoluteMaxNetObjectCount = 65536 ││ └─→ 过滤器预分配位数组大小,避免运行时频繁扩容 ││ ││ CurrentMaxInternalIndex = 1000 ││ └─→ 当前实际使用的索引范围,优化遍历 ││ ││ MaxConnectionCount = 100 ││ └─→ 为每个连接预分配过滤状态数组 ││ ││ Config = UNetObjectGridFilterConfig* ││ └─→ 读取配置参数(网格大小、剔除距离等) ││ │└────────────────────────────────────────────────────────────────┘➕ AddObject / ➖ RemoveObject
当一个游戏对象需要被某个过滤器管理时,系统会调用 AddObject;当对象销毁或不再需要过滤时,调用 RemoveObject。
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectFilter.h
// 添加对象到过滤器// 返回值:true = 成功添加,false = 添加失败(对象不适合此过滤器)virtual bool AddObject(uint32 ObjectIndex, FNetObjectFilterAddObjectParams& Params) PURE_VIRTUAL;
// 从过滤器移除对象virtual void RemoveObject(uint32 ObjectIndex, const FNetObjectFilteringInfo& Info) PURE_VIRTUAL;AddObject 参数详解:
CPP
struct FNetObjectFilterAddObjectParams
{
// 输出参数:过滤器特定的信息,存储在对象上
// 初始值为零,由过滤器填充(如位置偏移、索引等)
FNetObjectFilteringInfo& OutInfo;
// 配置预设名称(如 "LongRange", "ShortRange")
FName ProfileName;
// 实例协议:描述对象的源状态数据布局
const UE::Net::FReplicationInstanceProtocol* InstanceProtocol;
// 复制协议:描述对象的内部状态数据布局
const UE::Net::FReplicationProtocol* Protocol;
};FNetObjectFilteringInfo 的内部结构:
CPP
// 每个对象存储 8 字节的过滤器专用数据struct alignas(8) FNetObjectFilteringInfo
{
uint16 Data[4]; // 4 个 16 位字段,共 64 位
};
// GridFilter 的使用方式(FObjectLocationInfo):// Data[0] = 位置状态偏移量// Data[1] = 位置状态索引// Data[2] = FPerObjectInfo 索引(低 16 位)// Data[3] = FPerObjectInfo 索引(高 16 位)生活类比 🏪:
AddObject= 新商品上架,需要登记到库存系统,记录货架位置RemoveObject= 商品下架,从库存系统删除,清理货架位置
AddObject 的典型实现流程:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ ➕ AddObject 实现流程 │├────────────────────────────────────────────────────────────────┤│ ││ 1. 检查对象是否适合此过滤器 ││ │ ││ ├─→ GridFilter: 对象必须有位置信息 ││ ├─→ ConnectionFilter: 任何对象都可以 ││ └─→ 如果不适合,返回 false ││ ││ 2. 从协议中提取需要的信息 ││ │ ││ ├─→ 查找 RepTag_WorldLocation 获取位置偏移 ││ ├─→ 查找 RepTag_CullDistanceSqr 获取剔除距离 ││ └─→ 存储到 OutInfo 中 ││ ││ 3. 分配内部资源 ││ │ ││ ├─→ GridFilter: 分配 FPerObjectInfo 槽位 ││ ├─→ 将对象添加到相关网格单元 ││ └─→ 初始化帧计数器 ││ ││ 4. 返回 true 表示成功 ││ │└────────────────────────────────────────────────────────────────┘🔄 PreFilter / Filter / PostFilter
过滤流程分三个阶段,就像做菜有"备菜→烹饪→装盘"三步:
阶段 | 方法 | 作用 |
|---|---|---|
🥬 准备阶段 |
| 准备过滤所需的数据,每帧调用一次 |
🍳 执行阶段 |
| 核心! 为每个连接执行过滤逻辑 |
🍽️ 收尾阶段 |
| 清理临时数据,统计信息,每帧调用一次 |
Filter 参数详解:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectFilter.h
struct FNetObjectFilteringParams
{
// ⭐ 核心输出:位数组,设置哪些对象允许复制
// 初始值未定义!过滤器必须显式设置每个相关对象的位
UE::Net::FNetBitArrayView OutAllowedObjects;
// 所有对象的过滤信息(由 AddObject 时填充)
TArrayView<const FNetObjectFilteringInfo> FilteringInfos;
// 所有对象的状态缓冲区(可用于读取位置等数据)
const UE::Net::TNetChunkedArray<uint8*>* StateBuffers = nullptr;
// 当前正在过滤的连接 ID
uint32 ConnectionId = 0;
// 该连接的视图信息(位置、方向、FOV)
UE::Net::FReplicationView View;
};FReplicationView 结构:
CPP
struct FReplicationView
{
// 支持多视图(如分屏游戏)
struct FView
{
FVector Pos; // 视图位置
FVector Dir; // 视图方向
float FoVRadians; // 视野角度(弧度)
};
TArray<FView, TInlineAllocator<2>> Views; // 通常 1-2 个视图
};Filter 方法的调用时序:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔄 每帧过滤调用时序 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 帧开始 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PreFilter() - 所有过滤器各调用一次 │ │
│ │ ├─→ GridFilter: 增加帧计数器,重置统计 │ │
│ │ ├─→ ConnectionFilter: 准备连接状态 │ │
│ │ └─→ 其他过滤器: 各自的准备工作 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ for each Connection: │ │
│ │ for each Filter: │ │
│ │ Filter(ConnectionId, View) ← 核心过滤逻辑 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ PostFilter() - 所有过滤器各调用一次 │ │
│ │ ├─→ GridFilter: 输出 CSV 统计 │ │
│ │ ├─→ 清理临时数据 │ │
│ │ └─→ 性能指标收集 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 帧结束 │
│ │
└────────────────────────────────────────────────────────────────┘Filter 实现的关键点(基于真实源码):
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.cpp:134-258
void UNetObjectGridFilter::Filter(FNetObjectFilteringParams& Params){
IRIS_PROFILER_SCOPE(UNetObjectGridFilter_Filter);
// ═══════════════════════════════════════════════════════════════
// 阶段1:更新连接的活跃单元格列表
// ═══════════════════════════════════════════════════════════════
FPerConnectionInfo& ConnectionInfo = PerConnectionInfos[Params.ConnectionId];
TArray<FCellAndTimestamp, TInlineAllocator<32>> PrevCells = ConnectionInfo.RecentCells;
ConnectionInfo.RecentCells.Reset();
// 🎯 为每个视图计算当前所在的单元格
TArray<FCellAndTimestamp, TInlineAllocator<32>> NewCells;
for (FReplicationView::FView& View : Params.View.Views)
{
FCellAndTimestamp CellAndTimestamp;
CellAndTimestamp.Timestamp = FrameIndex; // 记录帧时间戳
CalculateCellCoord(CellAndTimestamp.Cell, View.Pos); // 世界坐标转单元格坐标
NewCells.AddUnique(CellAndTimestamp);
// 🔄 从 PrevCells 中移除仍然有效的单元格(避免重复处理)
for (const FCellAndTimestamp& PrevCell : PrevCells)
{
if ((PrevCell.Cell.X == CellAndTimestamp.Cell.X) &
(PrevCell.Cell.Y == CellAndTimestamp.Cell.Y))
{
PrevCells.RemoveAtSwap(static_cast<int32>(&PrevCell - PrevCells.GetData()));
break;
}
}
}
// ⏰ 非精确模式下:保留最近几帧访问过的单元格(ViewPosRelevancyFrameCount)
if (!Config->bUseExactCullDistance)
{
const uint32 MaxFrameCount = Config->ViewPosRelevancyFrameCount;
for (const FCellAndTimestamp& PrevCell : PrevCells)
{
if ((FrameIndex - PrevCell.Timestamp) > MaxFrameCount)
{
continue; // 太旧了,丢弃
}
NewCells.Add(PrevCell); // 仍然相关,保留
}
}
// 💾 保存新的单元格列表
ConnectionInfo.RecentCells = NewCells;
// ═══════════════════════════════════════════════════════════════
// 阶段2:根据单元格设置允许的对象
// ═══════════════════════════════════════════════════════════════
FNetBitArrayView AllowedObjects = Params.OutAllowedObjects;
AllowedObjects.ClearAllBits(); // 🧹 先清空所有位
if (Config->bUseExactCullDistance)
{
// ═══════════════════════════════════════════════════════════
// 精确距离模式:逐对象检查距离
// ═══════════════════════════════════════════════════════════
for (const FCellAndTimestamp& CellAndTimestamp : NewCells)
{
if (FCellObjects* Objects = Cells.Find(CellAndTimestamp.Cell))
{
for (const uint32 ObjectIndex : Objects->ObjectIndices)
{
// 📍 获取对象信息
const FObjectLocationInfo& ObjectLocationInfo =
static_cast<const FObjectLocationInfo&>(Params.FilteringInfos[ObjectIndex]);
const FPerObjectInfo& PerObjectInfo = ObjectInfos[ObjectLocationInfo.GetInfoIndex()];
// 🎯 检查是否有任何视图在剔除距离内
for (const FReplicationView::FView& View : Params.View.Views)
{
const double DistSq = PerObjectInfo.GetCullDistanceSq();
const double ObjectToViewDistSq = FVector::DistSquared(
PerObjectInfo.Position, View.Pos);
if (ObjectToViewDistSq <= DistSq)
{
// ✅ 在范围内:重置帧计数器(滞后机制)
ConnectionInfo.RecentObjectFrameCount.Add(
ObjectIndex,
PerObjectInfo.FrameCountBeforeCulling);
break; // 一个视图满足即可,无需检查其他
}
}
}
}
}
// 🔢 处理帧计数器并设置 AllowedObjects
for (TMap<uint32, uint16>::TIterator It = ConnectionInfo.RecentObjectFrameCount.CreateIterator();
It; ++It)
{
if (It->Value > 0)
{
It->Value--; // 递减计数器
AllowedObjects.SetBit(It->Key); // ✅ 仍然允许复制
}
else
{
It.RemoveCurrent(); // 🗑️ 计数器归零,移除追踪
}
}
}
else
{
// ═══════════════════════════════════════════════════════════
// 网格模式:单元格内所有对象直接允许(更快)
// ═══════════════════════════════════════════════════════════
for (const FCellAndTimestamp& CellAndTimestamp : NewCells)
{
if (FCellObjects* Objects = Cells.Find(CellAndTimestamp.Cell))
{
for (const uint32 ObjectIndex : Objects->ObjectIndices)
{
AllowedObjects.SetBit(ObjectIndex); // ✅ 直接允许
}
}
}
}
}关键数据结构:
CPP
// 单元格坐标(2D网格)struct FCellCoord
{
int32 X;
int32 Y;
};
// 带时间戳的单元格(用于视图位置相关性追踪)struct FCellAndTimestamp
{
FCellCoord Cell;
uint32 Timestamp; // 帧索引
};
// 每连接信息struct FPerConnectionInfo
{
TArray<FCellAndTimestamp> RecentCells; // 最近访问的单元格
TMap<uint32, uint16> RecentObjectFrameCount; // 对象索引 → 剩余帧计数
};两种过滤模式对比:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🎯 GridFilter 两种模式 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 📦 网格模式 (bUseExactCullDistance = false) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • 更快:只检查单元格归属 │ │
│ │ • 粒度粗:单元格内所有对象都允许 │ │
│ │ • 支持 ViewPosRelevancyFrameCount 滞后 │ │
│ │ • 适合:大量对象、对精度要求不高的场景 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 🎯 精确距离模式 (bUseExactCullDistance = true) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • 更精确:逐对象计算距离 │ │
│ │ • 支持 FrameCountBeforeCulling 滞后 │ │
│ │ • 开销更大:需要计算 DistSquared │ │
│ │ • 适合:对象数量适中、需要精确剔除的场景 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘🏷️ FilterTraits 特性
过滤器可以声明自己的"特性",让系统知道它的能力和需求:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectFilter.h
enum class ENetFilterTraits : uint8
{
None = 0x00,
// 🗺️ 空间过滤器标记
// 设置此标记表示过滤器基于 WorldLocation 进行过滤
// 系统会为此类过滤器提供位置相关的优化
Spatial = 0x01,
// 🔄 需要更新标记
// 设置此标记后,系统会每帧调用 UpdateObjects()
// 用于需要追踪对象状态变化的过滤器
NeedsUpdate = 0x02,
};
ENUM_CLASS_FLAGS(ENetFilterTraits);特性的设置与查询:
CPP
class UNetObjectFilter
{
protected:
// 子类在 OnInit 中调用此方法添加特性
void AddFilterTraits(ENetFilterTraits Traits)
{
FilterTraits |= Traits;
}
public:
// 查询过滤器是否具有某特性
bool HasFilterTrait(ENetFilterTraits Trait) const
{
return EnumHasAnyFlags(FilterTraits, Trait);
}
private:
ENetFilterTraits FilterTraits = ENetFilterTraits::None;
};
// GridFilter 的初始化示例void UNetObjectGridFilter::OnInit(const FNetObjectFilterInitParams& Params){
// 声明自己是空间过滤器
AddFilterTraits(ENetFilterTraits::Spatial);
// ...
}为什么需要特性标记?
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🏷️ FilterTraits 的作用 │
├────────────────────────────────────────────────────────────────┤
│ │
│ Spatial 特性的影响: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • 系统知道此过滤器需要位置信息 │ │
│ │ • 对象添加时会查找 RepTag_WorldLocation │ │
│ │ • 可以与 WorldLocations 系统集成 │ │
│ │ • 优先级系统可以复用位置数据 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ NeedsUpdate 特性的影响: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • 系统每帧调用 UpdateObjects() 方法 │ │
│ │ • 用于追踪对象位置变化、状态变化 │ │
│ │ • 不设置此标记可以节省不必要的调用开销 │ │
│ │ • GridFilter 通过轮询机制更新,不需要此标记 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 想象你是快递站调度员: │
│ • Spatial 标记 = "这个快递员只负责固定区域" │
│ • NeedsUpdate 标记 = "这个快递员需要每天更新路线" │
│ │
└────────────────────────────────────────────────────────────────┘🔌 静态过滤器句柄
除了动态过滤器(如 GridFilter),Iris 还定义了几个静态过滤器句柄用于特殊用途:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectFilter.h
// 无效句柄 - 表示对象没有分配过滤器constexpr FNetObjectFilterHandle InvalidNetObjectFilterHandle = 0;
// 所有者过滤 - 只同步给拥有该对象的连接constexpr FNetObjectFilterHandle ToOwnerFilterHandle = 1;
// 连接过滤 - 内部使用,用于每连接的自定义过滤constexpr FNetObjectFilterHandle ConnectionFilterHandle = 2;句柄类型判断:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/ReplicationFiltering.cpp
class FNetObjectFilterHandleUtil
{
public:
// 检查是否为无效句柄
static bool IsInvalidHandle(FNetObjectFilterHandle Handle);
// 检查是否为动态过滤器(如 GridFilter)
static bool IsDynamicFilter(FNetObjectFilterHandle Handle);
// 检查是否为静态过滤器(ToOwner, Connection)
static bool IsStaticFilter(FNetObjectFilterHandle Handle);
private:
// 最高位用于区分动态/静态过滤器
static constexpr FNetObjectFilterHandle DynamicNetObjectFilterHandleFlag =
1U << (sizeof(FNetObjectFilterHandle) * 8U - 1U);
};🎛️ 5.3 内置过滤器
Iris 提供了几个开箱即用的过滤器,覆盖最常见的场景。这些过滤器从最简单到最复杂,展示了过滤器接口的不同使用方式。
✅ NopNetObjectFilter(空操作过滤器)
"来者不拒,全部放行!"
这是最简单的过滤器——它什么都不过滤,所有对象都允许复制。它的实现是理解过滤器接口的最佳起点。
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NopNetObjectFilter.h
// 配置类(空实现,不需要任何配置)UCLASS(transient, MinimalAPI)
class UNopNetObjectFilterConfig final : public UNetObjectFilterConfig
{
GENERATED_BODY()
};
// 过滤器类UCLASS()
class UNopNetObjectFilter final : public UNetObjectFilter
{
GENERATED_BODY()
protected:
// 初始化 - 什么都不做
virtual void OnInit(const FNetObjectFilterInitParams&) override;
virtual void OnDeinit() override {}
// 索引增长 - 不需要处理
virtual void OnMaxInternalNetRefIndexIncreased(uint32 NewMaxInternalIndex) override {}
// 对象管理 - 接受所有对象,不存储任何信息
virtual bool AddObject(uint32 ObjectIndex, FNetObjectFilterAddObjectParams&) override;
virtual void RemoveObject(uint32 ObjectIndex, const FNetObjectFilteringInfo&) override;
// 核心过滤逻辑
virtual void Filter(FNetObjectFilteringParams&) override;
};实现源码:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NopNetObjectFilter.cpp
void UNopNetObjectFilter::OnInit(const FNetObjectFilterInitParams& Params){
// 空实现 - 不需要任何初始化
}
bool UNopNetObjectFilter::AddObject(uint32 ObjectIndex, FNetObjectFilterAddObjectParams& Params){
// 接受所有对象,不需要存储任何信息
return true;
}
void UNopNetObjectFilter::RemoveObject(uint32 ObjectIndex, const FNetObjectFilteringInfo& Info){
// 空实现 - 没有需要清理的资源
}
void UNopNetObjectFilter::Filter(FNetObjectFilteringParams& Params){
// 核心逻辑:设置所有位为1,表示全部允许
// 就像没有应用任何动态过滤一样
Params.OutAllowedObjects.SetAllBits();
}使用场景 🎮:
调试时临时禁用过滤
小规模游戏,所有玩家都需要看到所有对象
作为默认过滤器的占位符
测试网络复制功能时排除过滤器的影响
PLAINTEXT
┌─────────────────────────────────────┐
│ NopFilter: "全部放行!" │
│ │
│ 输入: [A][B][C][D][E][F][G][H] │
│ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ │
│ 输出: [A][B][C][D][E][F][G][H] │
│ ✓ ✓ ✓ ✓ ✓ ✓ ✓ ✓ │
│ │
│ 实现: SetAllBits() - O(N/64) 位操作│
└─────────────────────────────────────┘❌ FilterOutNetObjectFilter(始终过滤)
"一个都别想过!"
与 Nop 相反,这个过滤器拒绝所有对象。它的实现与 NopFilter 几乎相同,只是 Filter 方法的行为相反。
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/FilterOutNetObjectFilter.h
// 配置类(空实现)UCLASS(transient, MinimalAPI)
class UFilterOutNetObjectFilterConfig final : public UNetObjectFilterConfig
{
GENERATED_BODY()
};
// 过滤器类UCLASS()
class UFilterOutNetObjectFilter final : public UNetObjectFilter
{
GENERATED_BODY()
protected:
virtual void OnInit(const FNetObjectFilterInitParams&) override;
virtual void OnDeinit() override {}
virtual void OnMaxInternalNetRefIndexIncreased(uint32 NewMaxInternalIndex) override {}
virtual bool AddObject(uint32 ObjectIndex, FNetObjectFilterAddObjectParams&) override;
virtual void RemoveObject(uint32 ObjectIndex, const FNetObjectFilteringInfo&) override;
virtual void Filter(FNetObjectFilteringParams&) override;
};实现源码:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/FilterOutNetObjectFilter.cpp
void UFilterOutNetObjectFilter::OnInit(const FNetObjectFilterInitParams& Params){
// 空实现
}
bool UFilterOutNetObjectFilter::AddObject(uint32 ObjectIndex, FNetObjectFilterAddObjectParams& Params){
return true; // 接受对象,但在过滤时会全部拒绝
}
void UFilterOutNetObjectFilter::RemoveObject(uint32 ObjectIndex, const FNetObjectFilteringInfo& Info){
// 空实现
}
void UFilterOutNetObjectFilter::Filter(FNetObjectFilteringParams& Params){
// 核心逻辑:清除所有位,表示全部禁止
Params.OutAllowedObjects.ClearAllBits();
}使用场景 🎮:
临时"冻结"某类对象的复制
测试用途(验证对象确实被过滤)
作为"黑名单"过滤器的基础
在特定条件下完全禁用某类对象的同步
PLAINTEXT
┌─────────────────────────────────────┐
│ FilterOut: "全部拦截!" │
│ │
│ 输入: [A][B][C][D][E][F][G][H] │
│ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ │
│ 输出: [ ][ ][ ][ ][ ][ ][ ][ ] │
│ ✗ ✗ ✗ ✗ ✗ ✗ ✗ ✗ │
│ │
│ 实现: ClearAllBits() - O(N/64) │
└─────────────────────────────────────┘NopFilter vs FilterOutFilter 对比:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ 📊 简单过滤器对比 │├────────────────────────────────────────────────────────────────┤│ ││ 特性 NopFilter FilterOutFilter ││ ─────────────────────────────────────────────────────────────││ Filter 行为 SetAllBits() ClearAllBits() ││ 默认状态 全部允许 全部禁止 ││ 内存占用 0 0 ││ CPU 开销 O(N/64) O(N/64) ││ 存储状态 无 无 ││ 适用场景 调试/小规模游戏 测试/临时禁用 ││ ││ 💡 这两个过滤器展示了过滤器接口的最简实现 ││ 实际项目中很少直接使用,但对于理解接口很有帮助 ││ │└────────────────────────────────────────────────────────────────┘🗺️ NetObjectGridFilter(空间网格过滤)
"只同步你附近的东西!"
这是最常用也最复杂的过滤器,基于空间位置进行过滤。想象把游戏世界划分成一个个网格,只同步玩家所在网格及附近网格中的对象。
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.h
UCLASS(transient, config=Engine)
class UNetObjectGridFilterConfig : public UNetObjectFilterConfig
{
GENERATED_BODY()
public:
// 🕐 视点位置相关性帧数
UPROPERTY(Config)
uint32 ViewPosRelevancyFrameCount = 2;
// ⏱️ 剔除前等待帧数(防抖动)
UPROPERTY(Config)
uint16 DefaultFrameCountBeforeCulling = 4;
// 📐 网格单元尺寸
UPROPERTY(Config)
float CellSizeX = 20000.0f; // 200米
UPROPERTY(Config)
float CellSizeY = 20000.0f; // 200米
// 📏 剔除距离
UPROPERTY(Config)
float MaxCullDistance = 0.0f;
UPROPERTY(Config)
float DefaultCullDistance = 15000.0f; // 150米
// 🎯 是否使用精确距离计算
UPROPERTY(Config)
bool bUseExactCullDistance = true;
};工作原理图解:
PLAINTEXT
┌──────────────────────────────────────────────────────────────────┐
│ 🗺️ 游戏世界网格划分 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────┬─────┬─────┬─────┬─────┐ │
│ │ │ │ 🧟 │ │ │ ← 远处的僵尸,不同步 │
│ ├─────┼─────┼─────┼─────┼─────┤ │
│ │ │ 🎁 │ │ │ │ ← 远处的宝箱,不同步 │
│ ├─────┼─────┼─────┼─────┼─────┤ │
│ │ │ │ 🧟 │ 🎁 │ │ ← 这些在范围内,同步! │
│ ├─────┼─────┼─────┼─────┼─────┤ │
│ │ │ 🎁 │ 👤 │ 🧟 │ │ ← 玩家位置 + 附近对象 │
│ ├─────┼─────┼─────┼─────┼─────┤ 都需要同步 │
│ │ │ │ 🧟 │ │ │ │
│ └─────┴─────┴─────┴─────┴─────┘ │
│ │
│ 👤 = 玩家位置 🧟 = 僵尸 🎁 = 宝箱 │
│ ━━ = 剔除距离范围 │
│ │
└──────────────────────────────────────────────────────────────────┘🔌 NetObjectConnectionFilter(连接过滤)
"这个包裹只能送给张三!"
允许为每个对象单独设置:它可以被哪些连接(玩家)看到。这是一个动态预轮询过滤器,支持每连接级别的过滤控制。
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectConnectionFilter.h
// 配置类 - 限制过滤器管理的最大对象数UCLASS(transient, MinimalAPI)
class UNetObjectConnectionFilterConfig : public UNetObjectFilterConfig
{
GENERATED_BODY()
public:
// 最大对象数量(不适合处理大量对象,推荐使用静态连接过滤 API)
UPROPERTY(Config)
uint16 MaxObjectCount = 4096;
};
UCLASS(transient, MinimalAPI)
class UNetObjectConnectionFilter : public UNetObjectFilter
{
GENERATED_BODY()
public:
// 设置对象对特定连接的复制状态
IRISCORE_API void SetReplicateToConnection(
FNetRefHandle RefHandle,
uint32 ConnectionId,
ENetFilterStatus FilterStatus
);
protected:
// 过滤信息 - 存储本地对象索引
struct FFilteringInfo : public FNetObjectFilteringInfo
{
void SetLocalObjectIndex(uint16 Index) { Data[0] = Index; }
uint16 GetLocalObjectIndex() const { return Data[0]; }
};
// 每连接的过滤状态
struct FPerConnectionInfo
{
FNetBitArray ReplicationEnabledObjects; // 每连接启用的对象位图
};
TArray<uint32> LocalToNetRefIndex; // 本地索引 → 网络对象索引
TArray<FPerConnectionInfo> PerConnectionInfos;
FNetBitArray UsedLocalInfoIndices; // 已使用的本地索引
bool bObjectRemoved = false; // 优化标志
};实现细节分析:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Prioritization/NetObjectConnectionFilter.cpp
// SetReplicateToConnection 的核心实现void UNetObjectConnectionFilter::SetReplicateToConnection(
FNetRefHandle RefHandle,
uint32 ConnectionId,
ENetFilterStatus FilterStatus){
// 1. 获取对象的内部索引
const FInternalNetRefIndex ObjectIndex = GetObjectIndex(RefHandle);
// 2. 获取过滤信息中存储的本地索引
const FFilteringInfo* FilteringInfo = static_cast<const FFilteringInfo*>(
GetFilteringInfo(ObjectIndex));
const uint16 LocalIndex = FilteringInfo->GetLocalObjectIndex();
// 3. 设置该连接对该对象的过滤状态
FPerConnectionInfo& PerConnectionInfo = PerConnectionInfos[ConnectionId];
PerConnectionInfo.ReplicationEnabledObjects.SetBitValue(
LocalIndex,
FilterStatus == ENetFilterStatus::Allow
);
}
// Filter 方法 - 核心过滤逻辑void UNetObjectConnectionFilter::Filter(FNetObjectFilteringParams& Params){
FNetBitArrayView& AllowedObjects = Params.OutAllowedObjects;
AllowedObjects.ClearAllBits(); // 默认全部禁止
FPerConnectionInfo& ConnectionInfo = PerConnectionInfos[Params.ConnectionId];
// 遍历该连接允许的对象,设置允许位
ConnectionInfo.ReplicationEnabledObjects.ForAllSetBits(
[&AllowedObjects, &ToNetRefIndex = LocalToNetRefIndex](uint32 LocalObjectIndex)
{
const uint32 ObjectIndex = ToNetRefIndex[LocalObjectIndex];
AllowedObjects.SetBit(ObjectIndex);
}
);
}本地索引映射机制:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ 🔄 本地索引映射 │├────────────────────────────────────────────────────────────────┤│ ││ 为什么需要本地索引? ││ ┌─────────────────────────────────────────────────────────┐ ││ │ 全局对象索引可能很大(0~65535),但 ConnectionFilter │ ││ │ 只管理少量对象(最多 4096 个)。使用本地索引可以: │ ││ │ • 减少每连接位数组的大小 │ ││ │ • 提高缓存效率 │ ││ │ • 降低内存占用 │ ││ └─────────────────────────────────────────────────────────┘ ││ ││ 映射关系: ││ ┌─────────────────────────────────────────────────────────┐ ││ │ LocalIndex (0~4095) ←→ NetRefIndex (0~65535) │ ││ │ │ ││ │ LocalToNetRefIndex[0] = 1234 (对象A) │ ││ │ LocalToNetRefIndex[1] = 5678 (对象B) │ ││ │ LocalToNetRefIndex[2] = 9012 (对象C) │ ││ │ ... │ ││ └─────────────────────────────────────────────────────────┘ ││ ││ 每连接的位数组只需要 4096 位 = 512 字节 ││ 而不是 65536 位 = 8192 字节 ││ │└────────────────────────────────────────────────────────────────┘使用场景 🎮:
私人物品只对拥有者可见
团队专属对象
动态改变对象的可见性
依赖对象(Dependent Object)的过滤
⚠️ 注意:ConnectionFilter 不适合管理大量对象。对于静态的连接过滤需求,推荐使用
UReplicationSystem::SetConnectionFilterAPI。
PLAINTEXT
┌────────────────────────────────────────────────────────────┐
│ 🔌 连接过滤示例 │
├────────────────────────────────────────────────────────────┤
│ │
│ 对象: [玩家A的背包] │
│ │
│ ┌─────────────┬─────────────┬─────────────┐ │
│ │ 玩家A │ 玩家B │ 玩家C │ │
│ │ 连接 │ 连接 │ 连接 │ │
│ ├─────────────┼─────────────┼─────────────┤ │
│ │ ✅ │ ❌ │ ❌ │ │
│ │ 可以看 │ 看不到 │ 看不到 │ │
│ └─────────────┴─────────────┴─────────────┘ │
│ │
│ 代码实现: │
│ ConnectionFilter->SetReplicateToConnection( │
│ BackpackHandle, PlayerA_ConnectionId, Allow); │
│ ConnectionFilter->SetReplicateToConnection( │
│ BackpackHandle, PlayerB_ConnectionId, Disallow); │
│ ConnectionFilter->SetReplicateToConnection( │
│ BackpackHandle, PlayerC_ConnectionId, Disallow); │
│ │
└────────────────────────────────────────────────────────────┘🌐 5.4 空间过滤器详解 (GridFilter)
空间过滤器是 Iris 中最重要也最复杂的过滤器,它基于对象的世界位置进行过滤。让我们深入源码,彻底理解它的工作原理。
🏗️ GridFilter 类层次结构
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.h
// 抽象基类 - 定义网格过滤的核心逻辑UCLASS(abstract)
class UNetObjectGridFilter : public UNetObjectFilter
{
// 网格划分、距离计算、帧计数等核心逻辑
};
// 基于状态数据的实现 - 从对象的复制状态中读取位置UCLASS()
class UNetObjectGridWorldLocStateFilter : public UNetObjectGridFilter
{
// 通过 RepTag_WorldLocation 从状态缓冲区读取位置
};
// 基于 WorldLocations 的实现 - 从全局位置缓存读取位置UCLASS()
class UNetObjectGridWorldLocFilter : public UNetObjectGridFilter
{
// 通过 FWorldLocations 缓存读取位置(更高效)
};为什么有两种实现?
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🏗️ 两种 GridFilter 实现的区别 │
├────────────────────────────────────────────────────────────────┤
│ │
│ UNetObjectGridWorldLocStateFilter(状态读取模式): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 对象状态缓冲区 │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ [Health][Ammo][Position][Rotation][...] │ │ │
│ │ └───────────────────────↑─────────────────────────┘ │ │
│ │ │ │ │
│ │ 通过 RepTag_WorldLocation 找到偏移量,直接读取 │ │
│ │ ✅ 优点:不需要额外内存 │ │
│ │ ❌ 缺点:每次读取需要计算偏移 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ UNetObjectGridWorldLocFilter(缓存读取模式): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FWorldLocations 全局缓存 │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ ObjectIndex → { WorldLocation, CullDistance } │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 直接通过索引查找,O(1) 访问 │ │
│ │ ✅ 优点:访问速度快,缓存友好 │ │
│ │ ❌ 缺点:需要额外维护缓存 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘📐 网格划分算法
GridFilter 将游戏世界划分成规则的网格,每个网格称为一个"单元格"(Cell)。
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.h
// 网格坐标结构struct FCellCoord
{
int32 X;
int32 Y;
// 用于哈希表查找
friend uint32 GetTypeHash(const FCellCoord& Coord)
{
return HashCombine(GetTypeHash(Coord.X), GetTypeHash(Coord.Y));
}
bool operator==(const FCellCoord& Other) const
{
return X == Other.X && Y == Other.Y;
}
};
// 单元格包围盒 - 对象可能跨越多个单元格struct FCellBox
{
int32 MinX = 0;
int32 MaxX = 0;
int32 MinY = 0;
int32 MaxY = 0;
};
// 将世界坐标转换为网格坐标void UNetObjectGridFilter::CalculateCellCoord(FCellCoord& OutCoord, const FVector& Pos){
OutCoord.X = FPlatformMath::FloorToInt(Pos.X / Config->CellSizeX);
OutCoord.Y = FPlatformMath::FloorToInt(Pos.Y / Config->CellSizeY);
}网格划分示意图:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🌐 网格划分原理 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 世界坐标系: 网格坐标系: │
│ │
│ Y ↑ CellY ↑ │
│ │ │ │
│ 60000 ─┼─────────────── 2 ─┼─────┬─────┬───── │
│ │ │ → │(0,2)│(1,2)│(2,2) │
│ 40000 ─┼─────────────── 1 ─┼─────┼─────┼───── │
│ │ │ │(0,1)│(1,1)│(2,1) │
│ 20000 ─┼─────────────── 0 ─┼─────┼─────┼───── │
│ │ │ │(0,0)│(1,0)│(2,0) │
│ 0 ───┼───┬───┬───→ X ───┴─────┴─────┴─────→ CellX│
│ 0 20000 40000 0 1 2 │
│ │
│ CellSize = 20000 (200米) │
│ │
└────────────────────────────────────────────────────────────────┘📦 对象的网格归属
一个对象可能因为剔除距离较大而跨越多个网格单元。GridFilter 使用 FCellBox 来表示对象所属的所有单元格:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.cpp
void UNetObjectGridFilter::CalculateCellBox(
const FPerObjectInfo& PerObjectInfo,
FCellBox& OutCellBox){
const double CullDistance = PerObjectInfo.GetCullDistance();
const FVector Position = PerObjectInfo.Position;
// 计算对象的 AABB 包围盒
FVector MinPosition = Position - CullDistance;
FVector MaxPosition = Position + CullDistance;
// 转换为网格坐标
const int64 MinX = FPlatformMath::FloorToInt(MinPosition.X / Config->CellSizeX);
const int64 MinY = FPlatformMath::FloorToInt(MinPosition.Y / Config->CellSizeY);
const int64 MaxX = FPlatformMath::FloorToInt(MaxPosition.X / Config->CellSizeX);
const int64 MaxY = FPlatformMath::FloorToInt(MaxPosition.Y / Config->CellSizeY);
OutCellBox.MinX = static_cast<int32>(MinX);
OutCellBox.MinY = static_cast<int32>(MinY);
OutCellBox.MaxX = static_cast<int32>(MaxX);
OutCellBox.MaxY = static_cast<int32>(MaxY);
}对象跨越多个单元格的示例:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ 📦 对象的网格归属 │├────────────────────────────────────────────────────────────────┤│ ││ 小剔除距离对象(如装饰物,50m): ││ ┌─────┬─────┬─────┐ ││ │ │ │ │ ││ ├─────┼─────┼─────┤ 对象位置: (25000, 25000) ││ │ │ 🌳 │ │ CellBox: (1,1) → (1,1) ││ ├─────┼─────┼─────┤ 只在一个单元格中 ││ │ │ │ │ ││ └─────┴─────┴─────┘ ││ ││ 大剔除距离对象(如玩家,200m): ││ ┌─────┬─────┬─────┐ ││ │ ┌──┼─────┼──┐ │ 对象位置: (30000, 30000) ││ ├──┼──┼─────┼──┼──┤ 剔除距离: 20000 ││ │ │ │ 👤 │ │ │ CellBox: (0,0) → (2,2) ││ ├──┼──┼─────┼──┼──┤ 跨越 9 个单元格! ││ │ └──┼─────┼──┘ │ ││ └─────┴─────┴─────┘ ││ ││ ⚠️ 大剔除距离对象会显著增加内存和计算开销 ││ Config->MaxCullDistance 可以限制最大剔除距离 ││ │└────────────────────────────────────────────────────────────────┘🗃️ 内部数据结构
GridFilter 使用多个数据结构来高效管理对象:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.h
class UNetObjectGridFilter : public UNetObjectFilter
{
protected:
// 每个对象的详细信息
struct FPerObjectInfo
{
FVector Position = FVector::ZeroVector; // 世界位置
FCellBox CellBox = {}; // 所属单元格范围
uint32 ObjectIndex = 0U; // 对象索引
uint16 FrameCountBeforeCulling = 0U; // 剔除前帧数
private:
float CullDistance = 0.0f; // 剔除距离
float CullDistanceSq = 0.0f; // 剔除距离平方(优化)
public:
float GetCullDistance() const { return CullDistance; }
float GetCullDistanceSq() const { return CullDistanceSq; }
void SetCullDistance(float Distance)
{
CullDistance = Distance;
CullDistanceSq = Distance * Distance; // 预计算平方
}
};
// 每个单元格存储的对象列表
struct FCellObjects
{
TArray<uint32> ObjectIndices; // 该单元格中的对象索引
};
// 每个连接的过滤状态
struct FPerConnectionInfo
{
// 最近在范围内的对象及其帧计数器
TMap<uint32, uint16> RecentObjectFrameCount;
// 最近访问过的单元格(用于视图位置相关性)
TArray<FCellAndTimestamp> RecentCells;
};
// 核心数据结构
TMap<FCellCoord, FCellObjects> Cells; // 网格 → 对象列表
TChunkedArray<FPerObjectInfo> ObjectInfos; // 对象详细信息
TArray<FPerConnectionInfo> PerConnectionInfos; // 每连接状态
FNetBitArray AssignedObjectInfoIndices; // 已分配的槽位
TStrongObjectPtr<UNetObjectGridFilterConfig> Config; // 配置
};数据结构关系图:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🗃️ GridFilter 数据结构关系 │
├────────────────────────────────────────────────────────────────┤
│ │
│ Cells (TMap<FCellCoord, FCellObjects>) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ (0,0) → [Obj1, Obj5, Obj12] │ │
│ │ (0,1) → [Obj3, Obj7] │ │
│ │ (1,0) → [Obj2, Obj5, Obj8] ← Obj5 跨越多个单元格 │ │
│ │ (1,1) → [Obj4, Obj5, Obj9] │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ObjectInfos (TChunkedArray<FPerObjectInfo>) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ [0] Obj1: Pos=(1000,2000), CullDist=15000, CellBox=... │ │
│ │ [1] Obj2: Pos=(5000,3000), CullDist=10000, CellBox=... │ │
│ │ [2] Obj3: Pos=(8000,1000), CullDist=20000, CellBox=... │ │
│ │ ... │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ PerConnectionInfos (每个玩家连接) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Connection[1]: │ │
│ │ RecentObjectFrameCount: {Obj1→4, Obj3→2, Obj5→4} │ │
│ │ RecentCells: [(0,0,Frame100), (0,1,Frame100)] │ │
│ │ Connection[2]: │ │
│ │ RecentObjectFrameCount: {Obj2→3, Obj4→4} │ │
│ │ RecentCells: [(1,1,Frame100)] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘👁️ 视图位置更新
每个玩家都有一个或多个"视图位置"(View Position),通常是摄像机位置。GridFilter 需要知道每个玩家在看哪里。
CPP
// 视图信息结构struct FReplicationView
{
FVector Position; // 视图位置
FVector Direction; // 视图方向
float FieldOfView; // 视野角度
};
// 更新连接的视图位置void UpdateViewPosition(uint32 ConnectionId, const TArray<FReplicationView>& Views);多视图支持(如分屏游戏):
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 👁️ 多视图支持 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 单人游戏: 分屏游戏: │
│ ┌─────────────────┐ ┌────────┬────────┐ │
│ │ │ │ 视图1 │ 视图2 │ │
│ │ 👁️ 视图1 │ │ 👁️ │ 👁️ │ │
│ │ │ │ │ │ │
│ └─────────────────┘ └────────┴────────┘ │
│ │
│ 同步范围 = 视图1附近 同步范围 = 视图1 ∪ 视图2 附近 │
│ │
└────────────────────────────────────────────────────────────────┘📏 剔除距离配置
剔除距离决定了对象在多远时会被过滤掉。Iris 支持多种剔除距离配置:
CPP
// 配置预设 - 为不同类型的对象设置不同的剔除距离USTRUCT()
struct FNetObjectGridFilterProfile
{
UPROPERTY(Config)
FName ProfileName; // 预设名称
UPROPERTY(Config)
float CullDistance = 15000.0f; // 剔除距离
UPROPERTY(Config)
uint16 FrameCountBeforeCulling = 4; // 剔除前帧数
};不同对象的剔除距离示例:
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────┐
│ 📏 剔除距离配置示例 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 对象类型 剔除距离 原因 │
│ ──────────────────────────────────────────────────────────────────│
│ 👤 其他玩家 20000 (200m) 玩家很重要,需要较大范围 │
│ 🧟 普通怪物 15000 (150m) 中等重要性 │
│ 🎁 掉落物品 10000 (100m) 只有靠近才需要看到 │
│ 🌳 装饰物 5000 (50m) 纯视觉效果,可以晚点同步 │
│ 💥 特效粒子 3000 (30m) 很近才能看清 │
│ │
│ 剔除范围示意图(实际使用 AABB 方形包围盒): │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 👤 200m │ │
│ │ ┌───────────────────────────────────────┐ │ │
│ │ │ 🧟 150m │ │ │
│ │ │ ┌───────────────────────────────┐ │ │ │
│ │ │ │ 🎁 100m │ │ │ │
│ │ │ │ ┌───────────────────────┐ │ │ │ │
│ │ │ │ │ 🌳 50m │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ │ 👤 │ │ │ │ │
│ │ │ │ │ (你) │ │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ └───────────────────────┘ │ │ │ │
│ │ │ │ │ │ │ │
│ │ │ └───────────────────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └───────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 📝 源码实现 (NetObjectGridFilter.cpp:498-499): │
│ FVector MinPosition = Position - CullDistance; │
│ FVector MaxPosition = Position + CullDistance; │
│ │
└─────────────────────────────────────────────────────────────────────┘⚡ 性能优化策略
GridFilter 采用了多种优化策略来保证高性能,这些优化对于支持大规模多人游戏至关重要。
1️⃣ 空间哈希(Spatial Hashing)
核心优化:不需要遍历所有对象,只检查相关网格中的对象。
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🚀 空间哈希性能对比 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 传统暴力方法: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ for (每个对象 in 所有10000个对象) │ │
│ │ for (每个视图 in 玩家视图) │ │
│ │ 计算距离,判断是否在范围内 │ │
│ │ │ │
│ │ 时间复杂度: O(N × V) = O(10000 × 2) = 20000次计算 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 网格哈希方法: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 相关单元格 = 玩家所在单元格 + 相邻单元格(约9个) │ │
│ │ for (每个单元格 in 相关单元格) │ │
│ │ 对象列表 = Cells[单元格] // O(1) 哈希查找 │ │
│ │ for (每个对象 in 对象列表) │ │
│ │ 精确距离检查(可选) │ │
│ │ │ │
│ │ 时间复杂度: O(K) 其中 K = 相关单元格中的对象数 │ │
│ │ 假设每个单元格平均50个对象: O(9 × 50) = 450次计算 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 性能提升: 20000 / 450 ≈ 44倍! │
│ │
└────────────────────────────────────────────────────────────────┘2️⃣ 帧计数延迟剔除(Hysteresis)
对象不会立即被剔除,而是等待几帧。这防止了对象在边界处频繁闪烁,同时减少了状态切换的开销。
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.cpp
// Filter 方法中的帧计数器处理(第 232-243 行)for (TMap<uint32, uint16>::TIterator It = ConnectionInfo.RecentObjectFrameCount.CreateIterator(); It; ++It)
{
if (It->Value > 0)
{
It->Value--; // 计数器递减
AllowedObjects.SetBit(It->Key); // 仍然允许复制
}
else
{
It.RemoveCurrent(); // 计数器归零,真正移除
}
}帧计数器的工作流程:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ ⏱️ 帧计数器状态机 │├────────────────────────────────────────────────────────────────┤│ ││ 对象进入范围: ││ ┌─────────────────────────────────────────────────────────┐ ││ │ RecentObjectFrameCount[ObjectIndex] = FrameCountBeforeCulling ││ │ AllowedObjects.SetBit(ObjectIndex) │ ││ └─────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ 每帧检查: ┌──────────────────────────────────────┐ ││ │ 对象仍在范围内? │ ││ └──────────────────────────────────────┘ ││ │ Yes │ No ││ ▼ ▼ ││ ┌─────────────────────────┐ ┌─────────────────────────┐ ││ │ 重置计数器为最大值 │ │ 计数器 > 0? │ ││ │ 保持 AllowedObjects 位 │ └─────────────────────────┘ ││ └─────────────────────────┘ │ Yes │ No ││ ▼ ▼ ││ ┌─────────────────┐ ┌─────────────────┐ ││ │ 计数器-- │ │ 移除对象 │ ││ │ 保持允许状态 │ │ 清除允许位 │ ││ └─────────────────┘ └─────────────────┘ ││ │└────────────────────────────────────────────────────────────────┘3️⃣ 精确距离 vs 网格距离
GridFilter 提供两种距离计算模式,可以根据性能需求选择:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.cpp
void UNetObjectGridFilter::Filter(FNetObjectFilteringParams& Params){
if (Config->bUseExactCullDistance)
{
// 精确模式:使用欧几里得距离
for (uint32 ObjectIndex : Objects->ObjectIndices)
{
const FPerObjectInfo& PerObjectInfo = ObjectInfos[...];
for (const FReplicationView::FView& View : Params.View.Views)
{
// 使用距离平方避免开方运算
const double DistSq = PerObjectInfo.GetCullDistanceSq();
const double ObjectToViewDistSq = FVector::DistSquared(
PerObjectInfo.Position,
View.Pos
);
if (ObjectToViewDistSq <= DistSq)
{
ConnectionInfo.RecentObjectFrameCount.Add(
ObjectIndex,
PerObjectInfo.FrameCountBeforeCulling
);
break;
}
}
}
}
else
{
// 网格模式:单元格内所有对象都允许
for (uint32 ObjectIndex : Objects->ObjectIndices)
{
AllowedObjects.SetBit(ObjectIndex);
}
}
}两种模式的对比:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📊 精确模式 vs 网格模式 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 精确模式 (bUseExactCullDistance = true): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ✅ 优点: │ │
│ │ • 精确的距离判断,不会同步超出范围的对象 │ │
│ │ • 带宽利用更高效 │ │
│ │ ❌ 缺点: │ │
│ │ • 每个对象都需要计算距离(虽然用平方优化) │ │
│ │ • CPU 开销较大 │ │
│ │ 📍 适用场景: 带宽受限、对象数量适中 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 网格模式 (bUseExactCullDistance = false): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ✅ 优点: │ │
│ │ • 极快的过滤速度,只需检查单元格归属 │ │
│ │ • CPU 开销极小 │ │
│ │ ❌ 缺点: │ │
│ │ • 可能同步单元格内超出剔除距离的对象 │ │
│ │ • 带宽利用率略低 │ │
│ │ 📍 适用场景: CPU 受限、对象数量巨大 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘4️⃣ 距离平方优化
避免昂贵的开方运算,使用距离平方进行比较:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.h
struct FPerObjectInfo
{
private:
float CullDistance = 0.0f;
float CullDistanceSq = 0.0f; // 预计算的平方值
public:
void SetCullDistance(float Distance)
{
CullDistance = Distance;
CullDistanceSq = Distance * Distance; // 只在设置时计算一次
}
float GetCullDistanceSq() const { return CullDistanceSq; }
};
// 使用时直接比较平方值,避免开方const double ObjectToViewDistSq = FVector::DistSquared(ObjPos, ViewPos);
if (ObjectToViewDistSq <= PerObjectInfo.GetCullDistanceSq())
{
// 在范围内
}5️⃣ 分块数组(Chunked Array)
使用分块数组存储对象信息,避免大数组重新分配:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.h
enum : unsigned
{
ObjectInfosChunkSize = 64 * 1024, // 64KB 每块
};
// 分块存储,避免单个大数组
TChunkedArray<FPerObjectInfo, ObjectInfosChunkSize> ObjectInfos;6️⃣ 视图位置相关性帧数
玩家移动时,之前所在的单元格不会立即失效:
CPP
// 📍 配置:ViewPosRelevancyFrameCount = 2
// 玩家从单元格 (1,1) 移动到 (1,2)// 帧 N: 当前单元格 (1,2),之前单元格 (1,1) 仍然相关// 帧 N+1: 当前单元格 (1,2),之前单元格 (1,1) 仍然相关// 帧 N+2: 当前单元格 (1,2),之前单元格 (1,1) 不再相关
// 这避免了玩家在单元格边界来回移动时的频繁重新计算🏢 5.5 组过滤 (Group Filtering)
组过滤是一种批量管理对象可见性的机制,特别适合关卡流送(Level Streaming)场景。通过将对象分组,可以一次性控制大量对象的复制状态。
🏗️ 组系统核心数据结构
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGroups.h
// 组特性枚举 - 定义组的过滤行为enum class ENetObjectGroupTraits : uint32
{
None = 0x0000,
IsExclusionFiltering = 0x0001, // 排除过滤:组内对象被排除
IsInclusionFiltering = 0x0002, // 包含过滤:组内对象被强制包含
};
ENUM_CLASS_FLAGS(ENetObjectGroupTraits);
// 组数据结构struct FNetObjectGroup
{
TArray<FInternalNetRefIndex> Members; // 组成员列表
FName GroupName; // 组名称
uint32 GroupId = 0U; // 唯一标识符
ENetObjectGroupTraits Traits = ENetObjectGroupTraits::None; // 组特性
};
// 组句柄 - 用于安全地引用组struct FNetObjectGroupHandle
{
FGroupIndexType Index; // 组在数组中的索引
FGroupIndexType Epoch; // 版本号(用于检测过期句柄)
uint32 UniqueId; // 唯一ID(防止重用冲突)
};组句柄的安全性设计:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔒 组句柄安全性设计 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 问题:组被销毁后,旧的句柄可能被误用 │
│ │
│ 解决方案:三重验证机制 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 1. Index: 组在数组中的位置 │ │
│ │ 2. Epoch: 复制系统的版本号,重启后递增 │ │
│ │ 3. UniqueId: 每个组的唯一ID,永不重复 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 验证逻辑: │
│ bool IsValidGroup(FNetObjectGroupHandle Handle) const │
│ { │
│ // 1. 检查句柄是否有效 │
│ // 2. 检查版本号是否匹配 │
│ // 3. 检查索引是否存在 │
│ // 4. 检查 UniqueId 是否匹配 │
│ return Handle.IsValid() && │
│ Handle.Epoch == CurrentEpoch && │
│ Groups.IsValidIndex(Handle.Index) && │
│ Groups[Handle.Index].GroupId == Handle.UniqueId; │
│ } │
│ │
└────────────────────────────────────────────────────────────────┘📦 组的创建与销毁
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGroups.cpp
class FNetObjectGroups
{
public:
// 创建一个新组
FNetObjectGroupHandle CreateGroup(FName GroupName);
// 销毁组(会自动清空成员)
void DestroyGroup(FNetObjectGroupHandle GroupHandle);
// 清空组(保留组,移除所有成员)
void ClearGroup(FNetObjectGroupHandle GroupHandle);
// 添加对象到组
void AddToGroup(FNetObjectGroupHandle GroupHandle, FInternalNetRefIndex InternalIndex);
// 从组移除对象
void RemoveFromGroup(FNetObjectGroupHandle GroupHandle, FInternalNetRefIndex InternalIndex);
// 查询对象所属的所有组
void GetGroupHandlesOfNetObject(FInternalNetRefIndex InternalIndex,
TArray<FNetObjectGroupHandle>& OutHandles) const;
};CreateGroup 实现详解:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGroups.cpp
FNetObjectGroupHandle FNetObjectGroups::CreateGroup(FName InGroupName){
// 1. 检查是否达到最大组数限制
const bool bCanCreateGroup = (uint32)Groups.Num() < MaxGroupCount;
if (!bCanCreateGroup)
{
UE_LOG(LogIrisGroup, Warning, TEXT("Maximum allowed groups allocated: %u"), MaxGroupCount);
return FNetObjectGroupHandle();
}
// 2. 自动生成名称(如果未提供)
if (InGroupName == NAME_None)
{
InGroupName = FName(TEXT("NetObjectGroup"), AutogeneratedGroupNameId++);
}
// 3. 验证名称唯一性
const FNetObjectGroupHandle ExistingGroup = FindGroupHandle(InGroupName);
if (ExistingGroup.IsValid())
{
UE_LOG(LogIrisGroup, Warning, TEXT("Group name %s is already registered"), *InGroupName.ToString());
return FNetObjectGroupHandle();
}
// 4. 分配唯一ID并创建组
const uint32 NewGroupId = NextGroupUniqueId++;
const uint32 Index = Groups.Emplace(FNetObjectGroup{
.GroupName = InGroupName,
.GroupId = NewGroupId
});
// 5. 构建并返回句柄
return FNetObjectGroupHandle(Index, CurrentEpoch, NewGroupId);
}组的生命周期:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📦 组的生命周期 │
├────────────────────────────────────────────────────────────────┤
│ │
│ CreateGroup("Level_Forest") │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 组: Level_Forest │ │
│ │ 成员: [] │ │
│ │ 特性: None │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ │ AddToGroup(树1, 树2, 怪物1...) │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 组: Level_Forest │ │
│ │ 成员: [🌳树1, 🌳树2, 🧟怪物1...] │ │
│ │ 特性: None │ │
│ └─────────────────────────────────────┘ │
│ │ │
│ │ AddExclusionFilterTrait() │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 组: Level_Forest │ │
│ │ 成员: [🌳树1, 🌳树2, 🧟怪物1...] │ │
│ │ 特性: IsExclusionFiltering ← 变成排除组 │
│ └─────────────────────────────────────┘ │
│ │ │
│ │ 玩家离开森林区域 │
│ │ DestroyGroup() 或 ClearGroup() │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 组已销毁或清空 │ │
│ └─────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘🚫 包含组 vs ✅ 排除组
Iris 支持两种类型的组过滤,它们的行为完全相反:
类型 | 效果 | 使用场景 |
|---|---|---|
排除组 (Exclusion) | 组内对象不会被同步 | 隐藏特定区域的对象 |
包含组 (Inclusion) | 组内对象强制被同步(覆盖其他过滤) | 确保重要对象可见 |
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGroups.cpp
// 设置组为排除组void FNetObjectGroups::AddExclusionFilterTrait(FNetObjectGroupHandle GroupHandle){
if (FNetObjectGroup* Group = GetGroup(GroupHandle))
{
// 只有非过滤组才能添加特性(不能同时是排除和包含)
if (!IsFilterGroup(*Group))
{
Group->Traits |= ENetObjectGroupTraits::IsExclusionFiltering;
// 标记所有成员为可过滤
for (FInternalNetRefIndex MemberIndex : Group->Members)
{
GroupFilteredOutObjects.SetBit(MemberIndex);
}
}
}
}
// 设置组为包含组void FNetObjectGroups::AddInclusionFilterTrait(FNetObjectGroupHandle GroupHandle){
if (FNetObjectGroup* Group = GetGroup(GroupHandle))
{
// 不能同时是排除和包含
if (!IsFilterGroup(*Group))
{
Group->Traits |= ENetObjectGroupTraits::IsInclusionFiltering;
}
}
}
// 查询组类型bool FNetObjectGroups::IsExclusionFilterGroup(FNetObjectGroupHandle GroupHandle) const;
bool FNetObjectGroups::IsInclusionFilterGroup(FNetObjectGroupHandle GroupHandle) const;排除组 vs 包含组示意图:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🚫 排除组 vs ✅ 包含组 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 场景:玩家在城镇,森林关卡未加载 │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ 🏘️ 城镇区域 │ │ 🌲 森林区域 │ │
│ │ (当前区域) │ │ (未加载) │ │
│ │ │ │ │ │
│ │ [商人][铁匠] │ │ [树][怪物] │ │
│ │ [玩家][NPC] │ │ [宝箱][Boss] │ │
│ │ │ │ │ │
│ │ ✅ 正常同步 │ │ 🚫 排除组过滤 │ │
│ └──────────────────┘ └──────────────────┘ │
│ │
│ ─────────────────────────────────────────────────────────────│
│ │
│ 场景:Boss 战斗,确保 Boss 始终可见 │
│ │
│ 即使 Boss 距离很远,使用包含组强制同步: │
│ │
│ ┌─────────────────────────────────────────┐ │
│ │ 正常过滤结果: [玩家附近的对象...] │ │
│ │ + │ │
│ │ 包含组强制添加: [👹Boss] │ │
│ │ = │ │
│ │ 最终结果: [玩家附近对象...] + [👹Boss] │ │
│ └─────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘📊 对象的组成员关系追踪
每个对象可以属于多个组,系统需要高效地追踪这种多对多关系:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/NetObjectGroups.h
class FNetObjectGroups
{
private:
// 每个对象的组成员关系
struct FNetObjectGroupMembership
{
private:
enum { NumInlinedGroupHandles = 2 }; // 内联优化:大多数对象只属于1-2个组
TArray<FGroupIndexType, TInlineAllocator<NumInlinedGroupHandles>> GroupIndexes;
public:
bool ContainsMembership(FNetObjectGroupHandle InGroupHandle) const;
void AddMembership(FNetObjectGroupHandle InGroupHandle);
void RemoveMembership(FNetObjectGroupHandle InGroupHandle);
int32 NumMemberships() const;
};
// 所有对象的组成员关系数组
TArray<FNetObjectGroupMembership> GroupMemberships;
// 属于任何过滤组的对象位图(用于快速查询)
FNetBitArray GroupFilteredOutObjects;
};组成员关系的内存优化:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ 📊 组成员关系内存优化 │├────────────────────────────────────────────────────────────────┤│ ││ 观察:大多数对象只属于 1-2 个组 ││ ││ 优化策略:TInlineAllocator<2> ││ ┌─────────────────────────────────────────────────────────┐ ││ │ FNetObjectGroupMembership 结构: │ ││ │ │ ││ │ ┌────────────────────────────────────────────────────┐ │ ││ │ │ 内联存储 (2 个槽位,无堆分配) │ │ ││ │ │ [GroupIndex1] [GroupIndex2] │ │ ││ │ └────────────────────────────────────────────────────┘ │ ││ │ │ ││ │ 如果对象属于 >2 个组,才会触发堆分配 │ ││ └─────────────────────────────────────────────────────────┘ ││ ││ 控制台变量保护: ││ CVarEnsureIfNumGroupMembershipsExceeds = 128 ││ 如果对象属于超过 128 个组,会触发警告(可能是 bug) ││ │└────────────────────────────────────────────────────────────────┘🔌 连接级别的组状态控制
组过滤可以针对不同的连接设置不同的状态:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/ReplicationFiltering.h
class FReplicationFiltering
{
public:
// 为所有连接设置组状态
void SetGroupFilterStatus(
FNetObjectGroupHandle GroupHandle,
ENetFilterStatus ReplicationStatus
);
// 为特定连接设置组状态
void SetGroupFilterStatus(
FNetObjectGroupHandle GroupHandle,
uint32 ConnectionId,
ENetFilterStatus ReplicationStatus
);
// 添加/移除过滤组
bool AddExclusionFilterGroup(FNetObjectGroupHandle GroupHandle);
bool RemoveExclusionFilterGroup(FNetObjectGroupHandle GroupHandle);
bool AddInclusionFilterGroup(FNetObjectGroupHandle GroupHandle);
bool RemoveInclusionFilterGroup(FNetObjectGroupHandle GroupHandle);
};应用示例:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔌 连接级别组状态控制 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 场景:两个玩家在不同区域 │
│ │
│ 玩家A 在森林 → 森林组: Allow │
│ 玩家B 在城镇 → 森林组: Disallow │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 森林组对象 │ │
│ │ [🌳树1] [🌳树2] [🧟怪物] │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────┴─────────────┐ │
│ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 玩家A连接 │ │ 玩家B连接 │ │
│ │ 状态: Allow │ │ 状态: Disallow │ │
│ │ │ │ │ │
│ │ 收到: 🌳🌳🧟 │ │ 收到: (无) │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 代码实现: │
│ // 玩家A进入森林 │
│ ReplicationFiltering->SetGroupFilterStatus( │
│ ForestGroup, PlayerA_ConnectionId, Allow); │
│ │
│ // 玩家B离开森林 │
│ ReplicationFiltering->SetGroupFilterStatus( │
│ ForestGroup, PlayerB_ConnectionId, Disallow); │
│ │
└────────────────────────────────────────────────────────────────┘🗺️ 关卡流送中的应用
组过滤最典型的应用场景就是关卡流送(Level Streaming):
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🗺️ 关卡流送与组过滤 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 游戏世界划分为多个流送关卡: │
│ │
│ ┌─────────┬─────────┬─────────┐ │
│ │ Level_A │ Level_B │ Level_C │ │
│ │ (城镇) │ (森林) │ (沙漠) │ │
│ ├─────────┼─────────┼─────────┤ │
│ │ Level_D │ Level_E │ Level_F │ │
│ │ (雪山) │ (沼泽) │ (火山) │ │
│ └─────────┴─────────┴─────────┘ │
│ │
│ 每个关卡对应一个组: │
│ - Group_LevelA, Group_LevelB, Group_LevelC... │
│ │
│ 玩家加载关卡时: │
│ 1. 客户端请求加载 Level_B │
│ 2. 服务器设置 Group_LevelB 状态为 Allow │
│ 3. Level_B 中的对象开始同步给该玩家 │
│ │
│ 玩家卸载关卡时: │
│ 1. 客户端请求卸载 Level_A │
│ 2. 服务器设置 Group_LevelA 状态为 Disallow │
│ 3. Level_A 中的对象停止同步给该玩家 │
│ │
│ 实现代码示例: │
│ void OnLevelLoaded(ULevel* Level, uint32 ConnectionId) │
│ { │
│ FNetObjectGroupHandle Group = GetGroupForLevel(Level); │
│ ReplicationFiltering->SetGroupFilterStatus( │
│ Group, ConnectionId, ENetFilterStatus::Allow); │
│ } │
│ │
└────────────────────────────────────────────────────────────────┘⚙️ 5.6 过滤器配置
📄 核心配置类
Iris 的过滤系统通过多个配置类进行控制,这些配置通常在 DefaultEngine.ini 或者 BaseEngine.ini 中设置。
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/ReplicationFilteringConfig.h
// 滞后配置预设 - 为不同类型的对象设置不同的滞后帧数USTRUCT()
struct FObjectScopeHysteresisProfile
{
GENERATED_BODY()
// 配置预设名称(用于在类配置中引用)
UPROPERTY()
FName FilterProfileName;
// 对象被过滤后保持在范围内的帧数
UPROPERTY()
uint8 HysteresisFrameCount = 0;
};
// 主过滤配置类UCLASS(transient, config = Engine)
class UReplicationFilteringConfig final : public UObject
{
GENERATED_BODY()
private:
// 是否启用对象范围滞后
// 启用后,被动态过滤的对象不会立即移出范围
UPROPERTY(Config)
bool bEnableObjectScopeHysteresis = true;
// 默认滞后帧数(可被预设覆盖)
UPROPERTY(Config)
uint8 DefaultHysteresisFrameCount = 0;
// 连接更新节流
// 值为 N 表示每帧只更新 1/N 的连接
// 例如:值为 4 表示每帧更新 25% 的连接
// 范围:1-128,值越大性能越好但响应越慢
UPROPERTY(Config)
uint8 HysteresisUpdateConnectionThrottling = 1;
// 滞后配置预设列表
UPROPERTY(Config)
TArray<FObjectScopeHysteresisProfile> HysteresisProfiles;
public:
bool IsObjectScopeHysteresisEnabled() const { return bEnableObjectScopeHysteresis; }
uint8 GetDefaultHysteresisFrameCount() const { return DefaultHysteresisFrameCount; }
uint8 GetHysteresisUpdateConnectionThrottling() const
{
return FMath::Clamp<uint8>(HysteresisUpdateConnectionThrottling, 1U, 128U);
}
};📄 配置文件格式
过滤器通过配置文件进行设置,通常在 DefaultEngine.ini 中:
INI
; 📍 Config/DefaultEngine.ini
; ═══════════════════════════════════════════════════════════════; 主过滤配置; ═══════════════════════════════════════════════════════════════[/Script/IrisCore.ReplicationFilteringConfig]; 启用对象范围滞后(防止边界闪烁)bEnableObjectScopeHysteresis=true
; 默认滞后帧数DefaultHysteresisFrameCount=4
; 连接节流更新(1=每帧更新所有连接,4=每帧更新25%连接)HysteresisUpdateConnectionThrottling=1
; 清除默认预设
!HysteresisProfiles=ClearArray
; 添加滞后配置预设
+HysteresisProfiles=(FilterProfileName="Default", HysteresisFrameCount=4)
+HysteresisProfiles=(FilterProfileName="Important", HysteresisFrameCount=8)
+HysteresisProfiles=(FilterProfileName="Decoration", HysteresisFrameCount=2)
+HysteresisProfiles=(FilterProfileName="Pawn", HysteresisFrameCount=30)连接节流的工作原理:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ ⚡ 连接节流更新机制 │├────────────────────────────────────────────────────────────────┤│ ││ HysteresisUpdateConnectionThrottling = 4 时: ││ ││ 帧 1: 更新连接 [1, 5, 9, 13, ...] (索引 % 4 == 1) ││ 帧 2: 更新连接 [2, 6, 10, 14, ...] (索引 % 4 == 2) ││ 帧 3: 更新连接 [3, 7, 11, 15, ...] (索引 % 4 == 3) ││ 帧 4: 更新连接 [0, 4, 8, 12, ...] (索引 % 4 == 0) ││ 帧 5: 更新连接 [1, 5, 9, 13, ...] (循环) ││ ││ 优点:减少每帧的计算量,适合大量连接的场景 ││ 缺点:对象可能多停留 N-1 帧才被真正移出范围 ││ ││ ⚠️ 建议保持较低的值(1-4),过高会导致响应延迟 ││ │└────────────────────────────────────────────────────────────────┘🏷️ 类级别过滤器配置
可以为不同的类配置不同的过滤器和预设:
INI
; 📍 Config/DefaultEngine.ini
; ═══════════════════════════════════════════════════════════════; 对象复制桥配置 - 为不同类指定过滤器; ═══════════════════════════════════════════════════════════════[/Script/IrisCore.ObjectReplicationBridgeConfig]
; 清除默认配置
!FilterConfigs=ClearArray
; 为 Pawn 类配置空间过滤器,使用 Pawn 预设(30帧滞后)
+FilterConfigs=(ClassName="/Script/Engine.Pawn", DynamicFilterName="Spatial", FilterProfile="Pawn")
; 为重要 Actor 禁用动态过滤
+FilterConfigs=(ClassName="/Script/MyGame.ImportantActor", DynamicFilterName="Nop", FilterProfile="")
; 为秘密对象使用连接过滤
+FilterConfigs=(ClassName="/Script/MyGame.SecretActor", DynamicFilterName="Connection", FilterProfile="")
; 为装饰物使用较短的滞后
+FilterConfigs=(ClassName="/Script/MyGame.DecorationActor", DynamicFilterName="Spatial", FilterProfile="Decoration")配置结构:
CPP
// 📍 源文件:ObjectReplicationBridgeConfig.h
USTRUCT()
struct FClassFilterConfig
{
GENERATED_BODY()
// 类名(完整路径,如 "/Script/Engine.Pawn")
UPROPERTY(Config)
FName ClassName;
// 使用的动态过滤器名称("Spatial", "Nop", "Connection", "FilterOut")
UPROPERTY(Config)
FName DynamicFilterName;
// 过滤器配置预设名称(对应 HysteresisProfiles 中的 FilterProfileName)
UPROPERTY(Config)
FName FilterProfile;
};📋 GridFilter 配置详解
空间网格过滤器有专门的配置类:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/NetObjectGridFilter.h
UCLASS(transient, config=Engine)
class UNetObjectGridFilterConfig : public UNetObjectFilterConfig
{
GENERATED_BODY()
public:
// ═══════════════════════════════════════════════════════════
// 帧计数相关
// ═══════════════════════════════════════════════════════════
// 视点位置相关性帧数
// 玩家移动后,之前的单元格仍然相关的帧数
UPROPERTY(Config)
uint32 ViewPosRelevancyFrameCount = 2;
// 剔除前等待帧数(防抖动)
UPROPERTY(Config)
uint16 DefaultFrameCountBeforeCulling = 4;
// ═══════════════════════════════════════════════════════════
// 网格尺寸
// ═══════════════════════════════════════════════════════════
// 网格单元 X 方向尺寸(单位:厘米)
UPROPERTY(Config)
float CellSizeX = 20000.0f; // 200米
// 网格单元 Y 方向尺寸(单位:厘米)
UPROPERTY(Config)
float CellSizeY = 20000.0f; // 200米
// ═══════════════════════════════════════════════════════════
// 剔除距离
// ═══════════════════════════════════════════════════════════
// 最大剔除距离(0 = 无限制)
UPROPERTY(Config)
float MaxCullDistance = 0.0f;
// 默认剔除距离
UPROPERTY(Config)
float DefaultCullDistance = 15000.0f; // 150米
// ═══════════════════════════════════════════════════════════
// 距离计算模式
// ═══════════════════════════════════════════════════════════
// 是否使用精确距离计算
// true: 使用欧几里得距离(更精确,CPU 开销更大)
// false: 使用网格归属(更快,但可能同步超出范围的对象)
UPROPERTY(Config)
bool bUseExactCullDistance = true;
// ═══════════════════════════════════════════════════════════
// 配置预设
// ═══════════════════════════════════════════════════════════
// 不同类型对象的剔除距离预设
UPROPERTY(Config)
TArray<FNetObjectGridFilterProfile> FilterProfiles;
};
// 预设结构USTRUCT()
struct FNetObjectGridFilterProfile
{
UPROPERTY(Config)
FName ProfileName; // 预设名称
UPROPERTY(Config)
float CullDistance = 15000.0f; // 剔除距离
UPROPERTY(Config)
uint16 FrameCountBeforeCulling = 4; // 剔除前帧数
};GridFilter 配置示例:
INI
; 📍 Config/DefaultEngine.ini
[/Script/IrisCore.NetObjectGridFilterConfig]; 网格尺寸(200米 x 200米)CellSizeX=20000.0CellSizeY=20000.0
; 默认剔除距离(150米)DefaultCullDistance=15000.0
; 最大剔除距离(0=无限制,建议设置以防止异常大的值)MaxCullDistance=50000.0
; 使用精确距离计算bUseExactCullDistance=true
; 视点位置相关性帧数ViewPosRelevancyFrameCount=2
; 默认剔除前帧数DefaultFrameCountBeforeCulling=4
; 清除默认预设
!FilterProfiles=ClearArray
; 添加预设
+FilterProfiles=(ProfileName="Default", CullDistance=15000.0, FrameCountBeforeCulling=4)
+FilterProfiles=(ProfileName="LongRange", CullDistance=30000.0, FrameCountBeforeCulling=8)
+FilterProfiles=(ProfileName="ShortRange", CullDistance=5000.0, FrameCountBeforeCulling=2)
+FilterProfiles=(ProfileName="VeryLongRange", CullDistance=50000.0, FrameCountBeforeCulling=16)使用示例:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📋 FilterProfile 使用示例 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 类型 FilterProfile 剔除距离 延迟帧数 │
│ ─────────────────────────────────────────────────────────────│
│ APlayerCharacter "LongRange" 300m 8帧 │
│ AEnemy "Default" 150m 4帧 │
│ APickup "ShortRange" 50m 2帧 │
│ ADecoration "ShortRange" 50m 2帧 │
│ ABoss "VeryLongRange" 500m 16帧 │
│ │
│ 这样配置后: │
│ - 玩家角色在很远的地方就能看到 │
│ - Boss 在极远距离也能看到(重要目标) │
│ - 敌人在中等距离可见 │
│ - 拾取物和装饰物只在近距离可见 │
│ │
│ 配置文件中的对应配置: │
│ +FilterConfigs=(ClassName="/Script/MyGame.PlayerCharacter", │
│ DynamicFilterName="Spatial", │
│ FilterProfile="LongRange") │
│ │
└────────────────────────────────────────────────────────────────┘🎮 运行时配置修改
某些配置可以通过控制台变量在运行时修改:
CPP
// 📍 控制台变量
// 是否剔除不相关的对象(调试用)// Net.Iris.CullNonRelevant 0/1bool bCVarRepFilterCullNonRelevant = true;
// 验证子对象过滤一致性(调试用)// Net.Iris.Filtering.ValidateNobSubObjectInScopeWithFilteredOutRootObject 0/1bool bCVarRepFilterValidateNoSubObjectInScopeWithFilteredOutRootObject = false;
// 组成员数量警告阈值// net.Iris.EnsureIfNumGroupMembershipsExceeds N
int32 CVarEnsureIfNumGroupMembershipsExceedsNum = 128;调试命令示例:
PLAINTEXT
; 在控制台中执行
; 临时禁用过滤(所有对象都同步)
Net.Iris.CullNonRelevant 0
; 重新启用过滤
Net.Iris.CullNonRelevant 1
; 启用子对象过滤验证(检测潜在 bug)
Net.Iris.Filtering.ValidateNobSubObjectInScopeWithFilteredOutRootObject 1🔧 5.7 过滤系统内部实现
🧠 ReplicationFiltering(复制过滤核心)
FReplicationFiltering 是整个过滤系统的大脑,协调所有过滤器的工作。它是 Iris 过滤系统中最核心的类,理解它的实现对于深入掌握整个系统至关重要。
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/ReplicationFiltering.h
class FReplicationFiltering
{
public:
// 🎬 初始化
void Init(FReplicationFilteringInitParams& Params);
void Deinit();
// 🔄 主过滤函数 - 每帧调用
void Filter();
// 📊 获取过滤结果
const FNetBitArrayView GetRelevantObjectsInScope(uint32 ConnectionId) const;
const FNetBitArrayView GetGroupFilteredOutObjects(uint32 ConnectionId) const;
// 👤 所有者过滤
void SetOwningConnection(FInternalNetRefIndex ObjectIndex, uint32 ConnectionId);
uint32 GetOwningConnection(FInternalNetRefIndex ObjectIndex) const;
// 🎛️ 动态过滤器管理
bool SetFilter(FInternalNetRefIndex ObjectIndex, FNetObjectFilterHandle Filter, FName FilterConfigProfile);
FNetObjectFilterHandle GetFilterHandle(const FName FilterName) const;
// 🏢 组过滤
bool AddExclusionFilterGroup(FNetObjectGroupHandle GroupHandle);
bool AddInclusionFilterGroup(FNetObjectGroupHandle GroupHandle);
void SetGroupFilterStatus(FNetObjectGroupHandle GroupHandle, ENetFilterStatus Status);
private:
// 状态标志位(用于优化,避免不必要的处理)
uint8 bHasNewConnection : 1;
uint8 bHasRemovedConnection : 1;
uint8 bHasDirtyConnectionFilter : 1;
uint8 bHasDirtyOwner : 1;
uint8 bHasDynamicFilters : 1;
uint8 bHasDirtyExclusionFilterGroup : 1;
uint8 bHasDirtyInclusionFilterGroup : 1;
uint8 bHasDynamicFiltersWithUpdateTrait : 1;
// 每连接维护的数据
struct FPerConnectionInfo
{
FNetBitArray ConnectionFilteredObjects; // 连接过滤结果
FNetBitArray GroupExcludedObjects; // 组排除的对象
FNetBitArray GroupIncludedObjects; // 组包含的对象
FNetBitArray ObjectsInScopeBeforeDynamicFiltering; // 动态过滤前
FNetBitArray ObjectsInScope; // 最终范围
FNetBitArray DynamicFilteredOutObjects; // 动态过滤掉的
FObjectScopeHysteresisUpdater HysteresisUpdater; // 滞后更新器
};
TArray<FPerConnectionInfo> ConnectionInfos;
// 动态过滤器信息
struct FFilterInfo
{
TObjectPtr<UNetObjectFilter> Filter;
FName Name;
uint32 ObjectCount = 0;
};
TArray<FFilterInfo> DynamicFilterInfos;
// 对象到过滤器的映射
TArray<uint8> ObjectIndexToDynamicFilterIndex;
TArray<uint32> ObjectIndexToOwningConnection;
};初始化流程详解:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/ReplicationFiltering.cpp
void FReplicationFiltering::Init(FReplicationFilteringInitParams& Params){
// 1. 加载配置
Config = TStrongObjectPtr(GetDefault<UReplicationFilteringConfig>());
// 2. 保存引用
ReplicationSystem = Params.ReplicationSystem;
Connections = Params.Connections;
NetRefHandleManager = Params.NetRefHandleManager;
Groups = Params.Groups;
// 3. 初始化连接数组
ConnectionInfos.SetNum(Params.Connections->GetMaxConnectionCount() + 1U);
ValidConnections.Init(ConnectionInfos.Num());
NewConnections.Init(ConnectionInfos.Num());
// 4. 初始化对象列表
SetNetObjectListsSize(MaxInternalNetRefIndex);
// 5. 初始化组过滤
GroupInfos.SetNumZeroed(MaxGroupCount);
ExclusionFilterGroups.Init(MaxGroupCount);
InclusionFilterGroups.Init(MaxGroupCount);
// 6. 初始化动态过滤器
InitFilters();
// 7. 初始化滞后系统
InitObjectScopeHysteresis();
}主过滤函数 Filter() 的执行流程:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🧠 FReplicationFiltering::Filter() 执行流程 │
├────────────────────────────────────────────────────────────────┤
│ │
│ void FReplicationFiltering::Filter() │
│ { │
│ // 阶段 1: 连接管理 │
│ ResetRemovedConnections(); // 清理断开的连接 │
│ InitNewConnections(); // 初始化新连接 │
│ │
│ // 阶段 2: 更新对象范围 │
│ UpdateObjectsInScope(); // 更新所有对象的基础范围 │
│ │
│ // 阶段 3: 组过滤(优先级最高) │
│ if (bHasDirtyExclusionFilterGroup) │
│ UpdateGroupExclusionFiltering(); // 排除组 │
│ if (bHasDirtyInclusionFilterGroup) │
│ UpdateGroupInclusionFiltering(); // 包含组 │
│ │
│ // 阶段 4: 所有者和连接过滤 │
│ if (bHasDirtyOwner || bHasDirtyConnectionFilter) │
│ UpdateOwnerAndConnectionFiltering(); │
│ │
│ // 阶段 5: 子对象过滤 │
│ UpdateSubObjectFilters(); │
│ │
│ // 阶段 6: 滞后预处理 │
│ PreUpdateObjectScopeHysteresis(); │
│ │
│ // 阶段 7: 动态过滤器(如 GridFilter) │
│ if (bHasDynamicFilters) │
│ UpdateDynamicFilters(); │
│ │
│ // 阶段 8: 最终过滤 │
│ FilterNonRelevantObjects(); │
│ } │
│ │
└────────────────────────────────────────────────────────────────┘动态过滤器更新的批处理优化:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/ReplicationFiltering.cpp
// 批处理助手类 - 优化脏对象的处理class FReplicationFiltering::FUpdateDirtyObjectsBatchHelper
{
public:
enum Constants : uint32
{
MaxObjectCountPerBatch = 512U, // 每批最多处理 512 个对象
};
// 按过滤器分组对象,减少虚函数调用开销
void PrepareBatch(const uint32* ObjectIndices, uint32 ObjectCount,
const TArray<uint8>& FilterIndices)
{
ResetBatch();
for (const uint32 ObjectIndex : MakeArrayView(ObjectIndices, ObjectCount))
{
const uint8 FilterIndex = FilterIndices[ObjectIndex];
if (FilterIndex == InvalidDynamicFilterIndex)
continue;
FPerFilterInfo& PerFilterInfo = PerFilterInfos[FilterIndex];
PerFilterInfo.ObjectIndices[PerFilterInfo.ObjectCount++] = ObjectIndex;
}
}
};过滤数据流:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🧠 FReplicationFiltering 数据流 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 所有可复制对象 │
│ [████████████████████████████████████] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 1. 组排除过滤 │ │
│ │ 移除被排除组包含的对象 │ │
│ └─────────────────────────────────────┘ │
│ [████████░░░░████████████░░░░████████] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 2. 所有者过滤 │ │
│ │ 只保留属于当前连接的对象 │ │
│ └─────────────────────────────────────┘ │
│ [████████░░░░████░░░░████░░░░████████] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 3. 连接过滤 │ │
│ │ 应用每连接的自定义过滤规则 │ │
│ └─────────────────────────────────────┘ │
│ [████████░░░░████░░░░░░░░░░░░████████] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 4. 动态过滤(空间过滤等) │ │
│ │ 基于位置等动态条件过滤 │ │
│ └─────────────────────────────────────┘ │
│ [████░░░░░░░░████░░░░░░░░░░░░░░░░████] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 5. 组包含过滤 │ │
│ │ 强制添加包含组中的对象 │ │
│ └─────────────────────────────────────┘ │
│ [████░░░░░░░░████░░░░████░░░░░░░░████] │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────┐ │
│ │ 6. 滞后处理 │ │
│ │ 延迟移除刚离开范围的对象 │ │
│ └─────────────────────────────────────┘ │
│ [████░░░░░░░░████░░░░████░░░░░░██████] ← 最终结果 │
│ │
│ ████ = 允许复制 ░░░░ = 禁止复制 │
│ │
└────────────────────────────────────────────────────────────────┘控制台变量(调试用):
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/ReplicationFiltering.cpp
// 是否剔除不相关的对象(可用于调试)bool bCVarRepFilterCullNonRelevant = true;
static FAutoConsoleVariableRef CVarRepFilterCullNonRelevant(
TEXT("Net.Iris.CullNonRelevant"),
bCVarRepFilterCullNonRelevant,
TEXT("When enabled will cull replicated actors that are not relevant to any client."),
ECVF_Default
);
// 验证子对象过滤的一致性bool bCVarRepFilterValidateNoSubObjectInScopeWithFilteredOutRootObject = false;
static FAutoConsoleVariableRef CVarRepFilterValidateNoSubObjectInScopeWithFilteredOutRootObject(
TEXT("Net.Iris.Filtering.ValidateNobSubObjectInScopeWithFilteredOutRootObject"),
bCVarRepFilterValidateNoSubObjectInScopeWithFilteredOutRootObject,
TEXT("Validate there are no subobjects in scope with a filtered out root object."),
ECVF_Default
);⏱️ ObjectScopeHysteresisUpdater(对象范围滞后更新器)
"滞后"(Hysteresis)是一个防抖动机制,防止对象在范围边界频繁进出。这个类是滞后机制的核心实现。
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/ObjectScopeHysteresisUpdater.h
class FObjectScopeHysteresisUpdater
{
public:
void Init(uint32 MaxObjectCount);
void Deinit();
// 索引增长时调用
void OnMaxInternalNetRefIndexIncreased(FInternalNetRefIndex NewMaxInternalIndex);
// 设置对象的滞后帧数(对象进入范围时调用)
void SetHysteresisFrameCount(FInternalNetRefIndex NetRefIndex, uint16 HysteresisFrameCount);
// 移除对象的滞后状态(对象被销毁时调用)
void RemoveHysteresis(FInternalNetRefIndex NetRefIndex);
void RemoveHysteresis(const FNetBitArrayView& ObjectsToRemove);
void RemoveHysteresis(TArrayView<const uint32> ObjectsToRemove);
// 更新滞后状态,返回应该被过滤掉的对象
void Update(uint8 FramesSinceLastUpdate, TArray<FInternalNetRefIndex>& OutObjectsToFilterOut);
// 是否有需要更新的对象
bool HasObjectsToUpdate() const;
// 获取正在更新的对象位图
FNetBitArrayView GetUpdatedObjects() const;
private:
enum : unsigned
{
LocalIndexGrowCount = 256U, // 每次增长 256 个槽位
};
typedef uint32 FLocalIndex;
// 本地索引管理(类似 ConnectionFilter 的优化策略)
FLocalIndex GetOrCreateLocalIndex(FInternalNetRefIndex NetRefIndex);
void FreeLocalIndex(FLocalIndex LocalIndex);
// 每个本地索引的帧计数器
TArray<uint16> FrameCounters;
// 本地索引 → 网络对象索引
TArray<FInternalNetRefIndex> LocalIndexToNetRefIndex;
// 网络对象索引 → 本地索引(用于快速查找)
TMap<FInternalNetRefIndex, FLocalIndex> NetRefIndexToLocalIndex;
// 已使用的本地索引位图
FNetBitArray UsedLocalIndices;
// 正在更新的对象位图(按网络对象索引)
FNetBitArray ObjectsToUpdate;
};Update 方法的 SIMD 优化实现:
CPP
// 📍 源文件:Core/Private/Iris/ReplicationSystem/Filtering/ObjectScopeHysteresisUpdater.cpp
void FObjectScopeHysteresisUpdater::Update(
uint8 FramesSinceLastUpdate,
TArray<FInternalNetRefIndex>& OutObjectsToFilterOut){
IRIS_PROFILER_SCOPE(FObjectScopeHysteresisUpdater_Update);
ensure(FramesSinceLastUpdate > 0 && FramesSinceLastUpdate <= 128);
// 用于检测计数器下溢的比较值
// 当 Counter - FramesSinceLastUpdate 产生下溢时,结果 >= FilterOutCompareValue
const uint16 FilterOutCompareValue = (65536U - FramesSinceLastUpdate) & 65535U;
uint16* CountersData = FrameCounters.GetData();
TArray<FInternalNetRefIndex, TInlineAllocator<32>> ObjectsToRemoveFromUpdate;
// 批量处理:每次处理 4 个计数器(SIMD 友好)
const WordType* LocalIndicesData = UsedLocalIndices.GetData();
for (FLocalIndex ObjectIt = 0; ObjectIt < UsedLocalIndices.GetNumBits();
ObjectIt += WordBitCount, ++LocalIndicesData)
{
WordType LocalIndicesWord = *LocalIndicesData;
if (!LocalIndicesWord)
continue; // 跳过空的字
// 每次处理 4 个索引
for (WordType LocalIndexOffset = 0; LocalIndexOffset < WordBitCount;
LocalIndexOffset += 4U)
{
uint16 Counters[4];
// 批量读取 4 个计数器
Counters[0] = CountersData[IndexOffset + LocalIndexOffset + 0];
Counters[1] = CountersData[IndexOffset + LocalIndexOffset + 1];
Counters[2] = CountersData[IndexOffset + LocalIndexOffset + 2];
Counters[3] = CountersData[IndexOffset + LocalIndexOffset + 3];
// 批量递减
Counters[0] -= FramesSinceLastUpdate;
Counters[1] -= FramesSinceLastUpdate;
Counters[2] -= FramesSinceLastUpdate;
Counters[3] -= FramesSinceLastUpdate;
// 批量写回
CountersData[IndexOffset + LocalIndexOffset + 0] = Counters[0];
CountersData[IndexOffset + LocalIndexOffset + 1] = Counters[1];
CountersData[IndexOffset + LocalIndexOffset + 2] = Counters[2];
CountersData[IndexOffset + LocalIndexOffset + 3] = Counters[3];
// 检查哪些对象应该被过滤
for (uint32 Offset : {0, 1, 2, 3})
{
if (Counters[Offset] >= FilterOutCompareValue)
{
// 计数器下溢 → 对象应该被过滤
ObjectsToRemoveFromUpdate.Add(IndexOffset + LocalIndexOffset + Offset);
}
}
}
// 批量移除
for (FLocalIndex LocalIndex : ObjectsToRemoveFromUpdate)
{
OutObjectsToFilterOut.Add(LocalIndexToNetRefIndex[LocalIndex]);
FreeLocalIndex(LocalIndex);
}
ObjectsToRemoveFromUpdate.Reset();
}
}滞后机制工作原理:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ ⏱️ 滞后机制工作原理 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 场景:对象在范围边界来回移动 │
│ │
│ 没有滞后时: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 帧1: 在范围内 ✅ → 帧2: 离开范围 ❌ → 帧3: 回来 ✅ │ │
│ │ 帧4: 又离开 ❌ → 帧5: 又回来 ✅ → 帧6: 又离开 ❌ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ 结果:对象疯狂闪烁!玩家体验极差 😵 │
│ │
│ 有滞后时(滞后帧数 = 4): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 帧1: 在范围内 ✅ (计数器=4) │ │
│ │ 帧2: 离开范围,但计数器=3,仍然 ✅ │ │
│ │ 帧3: 回来了!计数器重置=4 ✅ │ │
│ │ 帧4: 在范围内 ✅ │ │
│ │ ... │ │
│ │ 只有连续4帧都在范围外,才会真正被过滤掉 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ 结果:对象平滑过渡,没有闪烁 😊 │
│ │
└────────────────────────────────────────────────────────────────┘计数器下溢检测技巧:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔢 计数器下溢检测 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 问题:如何高效检测 Counter - N < 0? │
│ │
│ 传统方法: │
│ if (Counter >= FramesSinceLastUpdate) │
│ Counter -= FramesSinceLastUpdate; │
│ else │
│ // 应该过滤 │
│ │
│ 优化方法(利用无符号整数下溢): │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FilterOutCompareValue = 65536 - FramesSinceLastUpdate │ │
│ │ │ │
│ │ Counter -= FramesSinceLastUpdate; // 可能下溢 │ │
│ │ │ │
│ │ if (Counter >= FilterOutCompareValue) │ │
│ │ // 下溢发生 → 应该过滤 │ │
│ │ │ │
│ │ 示例(FramesSinceLastUpdate = 1): │ │
│ │ FilterOutCompareValue = 65535 │ │
│ │ Counter = 0 → 0 - 1 = 65535 (下溢) ≥ 65535 ✓ 过滤 │ │
│ │ Counter = 1 → 1 - 1 = 0 < 65535 ✗ 不过滤 │ │
│ │ Counter = 4 → 4 - 1 = 3 < 65535 ✗ 不过滤 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 优点:避免分支,SIMD 友好 │
│ │
└────────────────────────────────────────────────────────────────┘滞后时序图:
PLAINTEXT
时间 → 帧1 帧2 帧3 帧4 帧5 帧6 帧7 帧8
─────────────────────────────────────────────────────────────────────
对象位置 范围内 范围外 范围外 范围外 范围外 范围外 范围内 范围内
计数器 4 3 2 1 0 - 4 4
实际状态 ✅ ✅ ✅ ✅ ❌ ❌ ✅ ✅
↑
这里才真正被过滤
Update() 调用流程:
帧2: SetHysteresisFrameCount(obj, 4) → Counter=4
帧3: Update(1) → Counter=3, 仍在范围
帧4: Update(1) → Counter=2, 仍在范围
帧5: Update(1) → Counter=1, 仍在范围
帧6: Update(1) → Counter=0, 下溢检测触发 → OutObjectsToFilterOut.Add(obj)
帧7: 对象回到范围 → SetHysteresisFrameCount(obj, 4) → Counter=4本地索引优化:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ 📊 本地索引优化 │├────────────────────────────────────────────────────────────────┤│ ││ 问题:网络对象索引可能很稀疏(0~65535),但实际需要滞后的 ││ 对象可能只有几百个 ││ ││ 解决方案:使用本地索引映射 ││ ┌─────────────────────────────────────────────────────────┐ ││ │ NetRefIndexToLocalIndex: Map<uint32, uint32> │ ││ │ LocalIndexToNetRefIndex: Array<uint32> │ ││ │ FrameCounters: Array<uint16> │ ││ │ │ ││ │ 网络对象索引 本地索引 帧计数器 │ ││ │ 1234 → 0 → 4 │ ││ │ 5678 → 1 → 8 │ ││ │ 9012 → 2 → 2 │ ││ │ ... │ ││ └─────────────────────────────────────────────────────────┘ ││ ││ 优点: ││ • FrameCounters 数组紧凑,缓存友好 ││ • 批量处理时内存访问连续 ││ • 动态增长(每次 256 个槽位) ││ │└────────────────────────────────────────────────────────────────┘🔗 SharedConnectionFilterStatus(共享连接过滤状态)
用于处理分屏游戏等场景,多个玩家共享同一个网络连接。
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Filtering/SharedConnectionFilterStatus.h
class FSharedConnectionFilterStatus
{
public:
// 设置某个子连接的过滤状态
bool SetFilterStatus(FConnectionHandle ConnectionHandle, ENetFilterStatus FilterStatus);
// 获取组的过滤状态(任一子连接允许则允许)
ENetFilterStatus GetFilterStatus() const;
// 移除子连接
void RemoveConnection(FConnectionHandle ConnectionHandle);
private:
TSet<uint32> AllowConnections; // 允许复制的子连接集合
uint32 ParentConnectionId; // 父连接ID
};分屏游戏场景:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔗 分屏游戏共享连接 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 一台主机,两个玩家分屏游戏 │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 主机 │ │
│ │ ┌─────────────────┬─────────────────┐ │ │
│ │ │ 玩家1视图 │ 玩家2视图 │ │ │
│ │ │ 👤 │ 👤 │ │ │
│ │ │ (子连接1) │ (子连接2) │ │ │
│ │ └─────────────────┴─────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────┴───────┐ │ │
│ │ │ 父连接 │ │ │
│ │ │ (网络连接) │ │ │
│ │ └───────┬───────┘ │ │
│ └────────────────────┼────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 服务器 │
│ │
│ 对于某个对象: │
│ - 子连接1(玩家1): Allow │
│ - 子连接2(玩家2): Disallow │
│ - 父连接的最终状态: Allow(因为有一个子连接允许) │
│ │
└────────────────────────────────────────────────────────────────┘📊 5.8 过滤系统总结
🎯 核心概念回顾
概念 | 说明 |
|---|---|
过滤器 (Filter) | 决定对象是否应该被同步的组件 |
组 (Group) | 批量管理对象可见性的容器 |
滞后 (Hysteresis) | 防止对象在边界闪烁的机制 |
范围 (Scope) | 对某个连接可见的对象集合 |
📋 过滤优先级
PLAINTEXT
优先级从高到低:
1️⃣ 组排除过滤 (Exclusion Group) - 最高优先级,直接排除
2️⃣ 所有者过滤 (Owner) - 只同步给拥有者
3️⃣ 连接过滤 (Connection) - 针对特定连接
4️⃣ 动态过滤 (Dynamic/Spatial) - 基于位置等条件
5️⃣ 组包含过滤 (Inclusion Group) - 强制包含,可覆盖动态过滤
6️⃣ 滞后处理 (Hysteresis) - 延迟移除💡 最佳实践
选择合适的过滤器
大世界游戏 → GridFilter
私有数据 → ConnectionFilter
关卡流送 → Group Filtering
合理配置剔除距离
重要对象(玩家、Boss)→ 大剔除距离
次要对象(装饰物)→ 小剔除距离
使用滞后防止闪烁
快速移动的对象 → 较大滞后帧数
静态对象 → 较小滞后帧数
利用组过滤管理关卡
每个流送关卡一个组
加载时 Allow,卸载时 Disallow
📁 关键源文件
PLAINTEXT
Engine/Source/Runtime/Experimental/Iris/Core/
├── Public/Iris/ReplicationSystem/Filtering/
│ ├── NetObjectFilter.h # 过滤器基类
│ ├── NopNetObjectFilter.h # 空操作过滤器
│ ├── FilterOutNetObjectFilter.h # 全部过滤
│ ├── NetObjectGridFilter.h # 空间网格过滤器
│ ├── NetObjectConnectionFilter.h # 连接过滤器
│ ├── NetObjectFilterDefinitions.h # 过滤器定义
│ ├── ReplicationFilteringConfig.h # 过滤配置
│ └── SharedConnectionFilterStatus.h # 共享连接状态
│
└── Private/Iris/ReplicationSystem/Filtering/
├── ReplicationFiltering.h/.cpp # 核心过滤系统
├── NetObjectFilter.cpp # 基类实现
├── NetObjectGridFilter.cpp # 网格过滤器实现
├── NetObjectGroups.h/.cpp # 对象组管理
├── ObjectScopeHysteresisUpdater.h/.cpp # 滞后更新器
└── SharedConnectionFilterStatus.cpp # 共享状态实现🎉 恭喜! 你已经完成了 Iris 过滤系统的学习。过滤系统是网络复制优化的关键,掌握它能让你的多人游戏更加流畅高效!
📖 下一步:继续学习第六部分「优先级系统」,了解如何在有限带宽下优先同步重要对象。
本文档基于 Unreal Engine 5.5.0 Iris 源代码分析(源码目录:Engine/Source/Runtime/Experimental/Iris/)