🎯 Iris 网络复制系统技术分析 - 第六部分:优先级系统 (Prioritization)

想象一下,你是一个忙碌的外卖小哥🛵,手上有50份外卖要送,但每趟只能带10份。你会怎么决定先送哪些?
🔥 快超时的订单?(紧急度)
📍 离你最近的?(距离)
💰 打赏多的土豪?(重要性)
👀 正在盯着APP看配送进度的?(关注度)
Iris 的优先级系统就是帮服务器做这个决定的"智能调度员"📋,它决定:在带宽有限的情况下,哪些对象的数据应该优先发送给玩家。
📚 6.1 优先级系统概述
🎯 优先级的作用
在大型多人游戏中,服务器可能需要同步成千上万个对象给每个玩家。但网络带宽是有限的——就像外卖小哥的电动车后座只能放这么多餐盒🍱。
没有优先级系统会怎样?
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 😱 没有优先级的混乱世界 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 服务器:"我有1000个对象要同步,带宽只够发100个..." │
│ │
│ 发送队列(随机顺序): │
│ [远处的树🌳] [脚下的地雷💣] [天边的云☁️] [敌人的子弹🔫] │
│ [背后的宝箱🎁] [正在攻击你的Boss👹] [装饰花盆🪴]... │
│ │
│ 结果: │
│ • 玩家被"隐形"子弹打死 → "这游戏有挂!" │
│ • Boss 攻击动作卡顿 → "网络太差了!" │
│ • 重要道具延迟出现 → "BUG!道具不刷新!" │
│ │
└────────────────────────────────────────────────────────────────┘有了优先级系统之后:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ ✅ 智能优先级调度 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 优先级排序后: │
│ [正在攻击你的Boss👹] 优先级: 1.0 ← 最近+正在交互 │
│ [敌人的子弹🔫] 优先级: 0.95 ← 飞向你的! │
│ [脚下的地雷💣] 优先级: 0.9 ← 很近很危险 │
│ [背后的宝箱🎁] 优先级: 0.7 ← 近但不紧急 │
│ [远处的树🌳] 优先级: 0.1 ← 装饰,不重要 │
│ [天边的云☁️] 优先级: 0.05 ← 超远,随缘 │
│ │
│ 结果:重要的先发,不重要的等带宽空闲再发 │
│ │
└────────────────────────────────────────────────────────────────┘📊 静态优先级 vs 动态优先级
类型 | 比喻 | 特点 | 适用场景 |
|---|---|---|---|
静态优先级 | 🍽️ 套餐固定价 | 一次设定,永不改变 | 背景装饰、不重要的NPC |
动态优先级 | 📊 实时竞价 | 每帧根据情况重新计算 | 玩家、敌人、重要道具 |
CPP
// 📍 源文件:ReplicationPrioritization.h
class FReplicationPrioritization
{
// 静态优先级:设置一次,永久生效
void SetStaticPriority(uint32 ObjectIndex, float Priority);
// 动态优先级:通过 Prioritizer 每帧计算
bool SetPrioritizer(uint32 ObjectIndex, FNetObjectPrioritizerHandle Prioritizer);
// 默认优先级常量
static constexpr float DefaultPriority = 1.0f;
// 视图目标(玩家控制的角色)获得超高优先级
static constexpr float ViewTargetHighPriority = 1.0E7f; // 一千万!
};⏰ 优先级计算时机
优先级计算发生在 PreSendUpdate 阶段,在过滤之后执行:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔄 PreSendUpdate 执行流程 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ UpdateDirtyObjectList() │
│ └─→ 收集本帧有变化的对象 │
│ │
│ 2️⃣ UpdateWorldLocations() │
│ └─→ 更新对象位置(过滤和优先级都需要) │
│ │
│ 3️⃣ UpdateFiltering() ⬅️ 先过滤! │
│ └─→ 确定每个连接的对象作用域 │
│ │
│ 4️⃣ UpdateConditionals() │
│ └─→ 更新条件复制状态 │
│ │
│ 5️⃣ UpdatePrioritization() ⬅️ 后计算优先级! │
│ └─→ 为过滤后的对象计算优先级 │
│ │
│ 📌 为什么这个顺序? │
│ • 过滤后对象数量大大减少 │
│ • 优先级只需要计算"可能被复制"的对象 │
│ • 节省大量计算开销 │
│ │
└────────────────────────────────────────────────────────────────┘CPP
// 📍 源文件:ReplicationSystem.cppvoid UReplicationSystem::PreSendUpdate(const FSendUpdateParams& Params){
// 1. 更新脏对象列表
Impl->UpdateDirtyObjectList();
// 2. 更新世界位置(过滤和优先级都需要)
Impl->UpdateWorldLocations();
// 3. 先执行过滤,确定每个连接的作用域
Impl->UpdateFiltering();
// 4. 调用 PreSendUpdate 回调
Impl->CallPreSendUpdate(Params.DeltaSeconds);
// 5. 更新条件复制
Impl->UpdateConditionals();
// 6. 最后执行优先级计算(在过滤之后)
Impl->UpdatePrioritization(ReplicatingConnections);
}🔢 优先级值的含义
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📊 优先级数值含义 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 优先级值 含义 │
│ ─────────────────────────────────────────────────────────────│
│ 1.0E7f (1000万) 视图目标(玩家角色)- 绝对最高 │
│ 1.0f 标准优先级 - 考虑复制的阈值 │
│ 0.5f 中等优先级 - 可能被复制 │
│ 0.1f 低优先级 - 带宽充足时才复制 │
│ 0.0f 不复制 - 完全跳过 │
│ │
│ 📌 重要机制:优先级会累积! │
│ 帧1: 对象A优先级 = 0.3 (未被复制,累积) │
│ 帧2: 对象A优先级 = 0.6 (仍未复制) │
│ 帧3: 对象A优先级 = 0.9 (仍未复制) │
│ 帧4: 对象A优先级 = 1.2 (超过1.0,被复制!然后重置) │
│ │
│ 这个机制防止低优先级对象永远"饿死"! │
│ │
└────────────────────────────────────────────────────────────────┘🧱 6.2 UNetObjectPrioritizer 基类
📋 Prioritizer 接口定义
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Prioritization/NetObjectPrioritizer.h
/**
* 优先级专用的每对象数据结构
* 大小固定为 8 字节,用于存储优先级特定的信息
* 子类通过继承此结构来存储自己需要的数据
*/struct alignas(8) FNetObjectPrioritizationInfo
{
uint16 Data[4]; // 4 个 uint16 = 4 × 2 字节 = 8 字节 可被子类重新解释使用
};
/**
* 优先级配置基类
* 子类通过 UPROPERTY(Config) 暴露配置参数到 ini 文件
*/UCLASS(Transient, MinimalAPI)
class UNetObjectPrioritizerConfig : public UObject
{
GENERATED_BODY()
};
/**
* 优先级抽象基类
* 定义了优先级的完整生命周期和核心接口
*/UCLASS(Abstract)
class UNetObjectPrioritizer : public UObject
{
GENERATED_BODY()
public:
// ═══════════════════════════════════════════════════════════════
// 🎬 生命周期管理
// ═══════════════════════════════════════════════════════════════
/**
* 初始化优先级
* @param Params 包含 ReplicationSystem、Config、最大对象数等信息
* 在 FReplicationPrioritization::InitPrioritizers() 中调用
*/
IRISCORE_API virtual void Init(FNetObjectPrioritizerInitParams& Params) PURE_VIRTUAL(Init,);
/**
* 清理优先级
* 在复制系统关闭时调用,释放所有资源
*/
IRISCORE_API virtual void Deinit() PURE_VIRTUAL(Deinit,);
/**
* 当最大内部索引增加时调用
* 用于重新分配内部数组以容纳更多对象
* @param NewMaxInternalIndex 新的最大内部索引
*/
IRISCORE_API virtual void OnMaxInternalNetRefIndexIncreased(uint32 NewMaxInternalIndex)
PURE_VIRTUAL(OnMaxInternalNetRefIndexIncreased,);
// ═══════════════════════════════════════════════════════════════
// 🔌 连接管理
// ═══════════════════════════════════════════════════════════════
/**
* 新连接添加时调用
* 用于初始化每连接的数据(如 Fill 模式的帧计数数组)
* 基类实现为空,子类按需重写
*/
IRISCORE_API virtual void AddConnection(uint32 ConnectionId);
/**
* 连接移除时调用
* 用于清理每连接的数据
*/
IRISCORE_API virtual void RemoveConnection(uint32 ConnectionId);
// ═══════════════════════════════════════════════════════════════
// 📦 对象管理
// ═══════════════════════════════════════════════════════════════
/**
* 新对象要使用此优先级时调用
* @param ObjectIndex 对象的内部索引
* @param Params 包含 Protocol、InstanceProtocol、StateBuffer 等
* @return true 表示成功添加,false 表示此优先级不支持该对象
*/
IRISCORE_API virtual bool AddObject(uint32 ObjectIndex, FNetObjectPrioritizerAddObjectParams& Params)
PURE_VIRTUAL(AddObject, return false;);
/**
* 对象不再使用此优先级时调用
* 用于释放对象相关的资源(如位置索引)
*/
IRISCORE_API virtual void RemoveObject(uint32 ObjectIndex, const FNetObjectPrioritizationInfo& Info)
PURE_VIRTUAL(RemoveObject,);
/**
* 一组对象已更新(脏)时调用
* 用于更新对象的位置等信息
* 在 NotifyPrioritizersOfDirtyObjects() 中批量调用
*/
IRISCORE_API virtual void UpdateObjects(FNetObjectPrioritizerUpdateParams&) PURE_VIRTUAL(UpdateObjects,);
// ═══════════════════════════════════════════════════════════════
// 🎯 优先级计算(核心!)
// ═══════════════════════════════════════════════════════════════
/**
* 在所有 Prioritize() 调用之前调用一次
* 用于准备工作,如 RoundRobin 模式选择本帧要考虑的对象
* 基类实现为空
*/
IRISCORE_API virtual void PrePrioritize(FNetObjectPrePrioritizationParams&);
/**
* ⭐ 核心方法:批量计算优先级
* 可能对同一连接多次调用(如果对象数量超过批大小)
* @param Params 包含对象列表、优先级输出数组、视图信息等
*/
IRISCORE_API virtual void Prioritize(FNetObjectPrioritizationParams&) PURE_VIRTUAL(Prioritize,);
/**
* 在所有 Prioritize() 调用之后调用一次
* 用于清理工作
* 基类实现为空
*/
IRISCORE_API virtual void PostPrioritize(FNetObjectPostPrioritizationParams&);
};
// 特殊句柄常量constexpr FNetObjectPrioritizerHandle InvalidNetObjectPrioritizerHandle = ~FNetObjectPrioritizerHandle(0);
constexpr FNetObjectPrioritizerHandle DefaultSpatialNetObjectPrioritizerHandle = FNetObjectPrioritizerHandle(0);🔑 关键设计解析
为什么 FNetObjectPrioritizationInfo 只有 8 字节?
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📦 FNetObjectPrioritizationInfo 设计原理 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 设计约束: │
│ • 每个可复制对象都需要一个 Info 结构 │
│ • 大型游戏可能有 10000+ 对象 │
│ • 内存占用 = 对象数 × Info大小 │
│ │
│ 8字节设计: │
│ uint16 Data[4] = { 状态偏移, 状态索引, 位置索引低16位, 位置索引高16位 }│
│ │
│ 内存计算: │
│ 10000对象 × 8字节 = 80KB(可接受) │
│ 10000对象 × 64字节 = 640KB(太大!) │
│ │
│ 子类通过 static_cast 重新解释这 8 字节: │
│ • FObjectLocationInfo: 存储位置相关信息 │
│ • FObjectInfo: 存储内部索引和所有者连接 │
│ │
└────────────────────────────────────────────────────────────────┘🎯 Prioritize 参数详解
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Prioritization/NetObjectPrioritizer.h
/**
* 优先级计算的核心参数结构
* 包含了计算优先级所需的所有输入和输出
*/struct FNetObjectPrioritizationParams
{
// ═══════════════════════════════════════════════════════════════
// 📥 输入参数
// ═══════════════════════════════════════════════════════════════
const uint32* ObjectIndices; // 需要计算优先级的对象索引数组
uint32 ObjectCount; // 本批次的对象数量(最多 1024)
const FNetObjectPrioritizationInfo* PrioritizationInfos; // 对象信息数组(全局)
uint32 ConnectionId; // 当前连接ID
UE::Net::FReplicationView View; // ⭐视图信息(位置、方向、FOV)
// ═══════════════════════════════════════════════════════════════
// 📤 输出参数
// ═══════════════════════════════════════════════════════════════
float* Priorities; // ⭐输出:优先级数组(全局索引)
// Priorities[ObjectIndices[i]] = 计算结果
};
/**
* AddObject 的参数结构
*/struct FNetObjectPrioritizerAddObjectParams
{
FNetObjectPrioritizationInfo& OutInfo; // 输出:填充对象信息
const FReplicationInstanceProtocol* InstanceProtocol; // 实例协议
const FReplicationProtocol* Protocol; // 复制协议
const uint8* StateBuffer; // 状态缓冲区
};
/**
* UpdateObjects 的参数结构
*/struct FNetObjectPrioritizerUpdateParams
{
const uint32* ObjectIndices; // 脏对象索引数组
uint32 ObjectCount; // 脏对象数量
const FReplicationInstanceProtocol* const* InstanceProtocols; // 实例协议数组
FNetObjectPrioritizationInfo* PrioritizationInfos; // 对象信息数组(可写)
const TArray<uint8*>* StateBuffers; // 状态缓冲区数组
};调用模式详解:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔄 每帧优先级计算完整流程 │
├────────────────────────────────────────────────────────────────┤
│ │
│ FReplicationPrioritization::Prioritize() │
│ │ │
│ ├─→ UpdatePrioritiesForNewAndDeletedObjects() │
│ │ └─→ 处理新增/删除对象的优先级 │
│ │ │
│ ├─→ NotifyPrioritizersOfDirtyObjects(DirtyObjects) │
│ │ │ │
│ │ └─→ for each Prioritizer: │
│ │ Prioritizer->UpdateObjects(脏对象列表) │
│ │ └─→ 更新位置等信息 │
│ │ │
│ ├─→ for each Prioritizer with objects: │
│ │ Prioritizer->PrePrioritize() ← 调用一次 │
│ │ └─→ RoundRobin: 选择本帧要考虑的对象 │
│ │ └─→ Fill: 无操作(每连接独立处理) │
│ │ │
│ ├─→ for each Connection with View: │
│ │ │ │
│ │ ├─→ PrioritizeForConnection(ConnId, Objects) │
│ │ │ │ │
│ │ │ ├─→ 按优先级分组对象 │
│ │ │ │ │
│ │ │ └─→ for each Prioritizer batch: │
│ │ │ Prioritizer->Prioritize(Params) │
│ │ │ └─→ 计算优先级,写入 Priorities │
│ │ │ │
│ │ └─→ SetHighPriorityOnViewTargets() │
│ │ └─→ Controller/ViewTarget = 1.0E7f │
│ │ │
│ │ └─→ ReplicationWriter->UpdatePriorities() │
│ │ └─→ 累加优先级到 SchedulingPriorities │
│ │ │
│ └─→ for each Prioritizer with objects: │
│ Prioritizer->PostPrioritize() ← 调用一次 │
│ │
│ 📌 关键点: │
│ • PrePrioritize/PostPrioritize 每帧各调用一次 │
│ • Prioritize 每连接可能调用多次(批处理) │
│ • 同一对象对不同连接有不同优先级! │
│ │
└────────────────────────────────────────────────────────────────┘🤝 与过滤系统的协作
优先级系统和过滤系统紧密配合,形成"先筛选后排序"的高效流水线:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔗 过滤系统 → 优先级系统 协作流程 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 原始对象池(10000个对象) │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 🔍 过滤系统 (UpdateFiltering) │ │
│ │ • 空间过滤:剔除视野外对象 │ │
│ │ • 组过滤:剔除不相关关卡 │ │
│ │ • 连接过滤:剔除特定连接对象 │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ 过滤后(500个对象) │
│ ┌──────────────────────────────────────┐ │
│ │ 📊 优先级系统 (UpdatePrioritization)│ │
│ │ • 距离优先级 │ │
│ │ • 视野优先级 │ │
│ │ • 所有者加成 │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ 排序后(按优先级发送) │
│ ┌──────────────────────────────────────┐ │
│ │ 📤 ReplicationWriter │ │
│ │ • 优先级累积 │ │
│ │ • 选择本帧发送的对象 │ │
│ └──────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘关键代码:过滤后对象传递给优先级系统
CPP
// 📍 源文件:ReplicationPrioritization.cpp
void FReplicationPrioritization::Prioritize(
const FNetBitArrayView& ReplicatingConnections,
const FNetBitArrayView& DirtyObjectsThisFrame){
// ...
for (const uint32 ConnId : MakeArrayView(ConnectionIds, ReplicatingConnectionCount))
{
// 检查连接是否有视图
if (Connections->GetReplicationView(ConnId).Views.Num() <= 0)
continue;
FReplicationConnection* Connection = Connections->GetConnection(ConnId);
FReplicationWriter* ReplicationWriter = Connection->ReplicationWriter;
// 🔑 关键:从 ReplicationWriter 获取需要优先级更新的对象
// 这些对象已经通过过滤系统的筛选!
const FNetBitArray& Objects = ReplicationWriter->GetObjectsRequiringPriorityUpdate();
if (Objects.GetNumBits() == 0) continue;
PrioritizeForConnection(ConnId, BatchHelper, MakeNetBitArrayView(Objects));
// 将更新后的优先级传回 ReplicationWriter
ReplicationWriter->UpdatePriorities(ConnInfo.Priorities.GetData());
}
}🎛️ 6.3 内置优先级
Iris 提供了一套完整的优先级继承体系,满足不同游戏类型的需求:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🏗️ 优先级继承层次 │
├────────────────────────────────────────────────────────────────┤
│ │
│ UNetObjectPrioritizer (抽象基类) │
│ │ │
│ ├── ULocationBasedNetObjectPrioritizer (抽象,位置管理) │
│ │ │ │
│ │ ├── USphereNetObjectPrioritizer (球形距离) │
│ │ │ │ │
│ │ │ └── USphereWithOwnerBoostNetObjectPrioritizer│
│ │ │ │
│ │ └── UFieldOfViewNetObjectPrioritizer (视野锥) │
│ │ │
│ └── UNetObjectCountLimiter (数量限制,RoundRobin/Fill) │
│ │
└────────────────────────────────────────────────────────────────┘📍 LocationBasedNetObjectPrioritizer(位置基础类)
这是一个抽象基类,为所有基于位置的优先级提供位置管理基础设施。
"我负责管理位置数据,子类负责计算优先级!" 📍
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Prioritization/LocationBasedNetObjectPrioritizer.h
UCLASS(Transient, MinimalAPI, Abstract)
class ULocationBasedNetObjectPrioritizer : public UNetObjectPrioritizer
{
GENERATED_BODY()
protected:
// ═══════════════════════════════════════════════════════════════
// 📦 位置信息结构(重新解释 FNetObjectPrioritizationInfo 的 8 字节)
// ═══════════════════════════════════════════════════════════════
struct FObjectLocationInfo : public FNetObjectPrioritizationInfo
{
// Data[0]: 位置状态偏移(在状态缓冲区中的偏移)
void SetLocationStateOffset(uint16 Offset) { Data[0] = Offset; }
uint16 GetLocationStateOffset() const { return Data[0]; }
// Data[1]: 位置状态索引(Fragment 索引)
// InvalidStateIndex (0xFFFF) 表示使用 WorldLocations
void SetLocationStateIndex(uint16 Index) { Data[1] = Index; }
uint16 GetLocationStateIndex() const { return Data[1]; }
// Data[2-3]: 位置索引(在 Locations 数组中的索引,32位)
void SetLocationIndex(uint32 Index) {
Data[2] = Index & 65535U;
Data[3] = Index >> 16U;
}
uint32 GetLocationIndex() const {
return (uint32(Data[3]) << 16U) | uint32(Data[2]);
}
// 判断位置来源
bool IsUsingWorldLocations() const {
return GetLocationStateIndex() == InvalidStateIndex;
}
bool IsUsingLocationInState() const {
return GetLocationStateIndex() != InvalidStateIndex;
}
};
// ═══════════════════════════════════════════════════════════════
// 🎯 SIMD 优化的位置操作
// ═══════════════════════════════════════════════════════════════
// 从缓存数组获取位置(VectorRegister = 4个float,SIMD友好)
IRISCORE_API VectorRegister GetLocation(const FObjectLocationInfo& Info) const;
// 设置位置到缓存数组
IRISCORE_API void SetLocation(const FObjectLocationInfo& Info, VectorRegister Location);
// 更新位置(根据来源选择读取方式)
IRISCORE_API void UpdateLocation(const uint32 ObjectIndex,
const FObjectLocationInfo& Info,
const UE::Net::FReplicationInstanceProtocol* InstanceProtocol);
private:
// ═══════════════════════════════════════════════════════════════
// 📊 内部数据结构
// ═══════════════════════════════════════════════════════════════
// 分块存储位置数据,缓存友好
// LocationsChunkSize = 4096 字节,每块约 256 个位置
TChunkedArray<VectorRegister, LocationsChunkSize> Locations;
// 位数组,标记哪些位置索引已被分配
UE::Net::FNetBitArray AssignedLocationIndices;
// 全局位置管理器的引用
const UE::Net::FWorldLocations* WorldLocations = nullptr;
// 常量定义
static constexpr uint16 InvalidStateIndex = 0xFFFF;
static constexpr uint16 InvalidStateOffset = 0xFFFF;
};位置来源的两种方式深入解析:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📍 位置数据来源详解 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 方式1:WorldLocations 系统(推荐) │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 适用场景: │ │
│ │ • 大量对象需要位置信息 │ │
│ │ • 位置更新集中管理 │ │
│ │ • 对象可能没有 RepTag_WorldLocation 标签 │ │
│ │ │ │
│ │ 工作原理: │ │
│ │ 1. 游戏代码调用 WorldLocations->SetWorldLocation() │ │
│ │ 2. LocationBasedPrioritizer 从 WorldLocations 读取 │ │
│ │ 3. 位置存储在 WorldLocations 的内部数组中 │ │
│ │ │ │
│ │ 判断条件: │ │
│ │ WorldLocations->HasInfoForObject(ObjectIndex) == true │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 方式2:状态中的 RepTag_WorldLocation │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 适用场景: │ │
│ │ • 位置是复制属性的一部分 │ │
│ │ • 需要精确同步的位置 │ │
│ │ • 使用 UPROPERTY(Replicated) FVector Location │ │
│ │ │ │
│ │ 工作原理: │ │
│ │ 1. 属性标记为 RepTag_WorldLocation │ │
│ │ 2. 系统通过 FindRepTag() 找到位置在状态中的偏移 │ │
│ │ 3. 直接从状态缓冲区读取位置数据 │ │
│ │ │ │
│ │ 判断条件: │ │
│ │ FindRepTag(Protocol, RepTag_WorldLocation, TagInfo) │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘完整的 AddObject 实现:
CPP
// 📍 源文件:LocationBasedNetObjectPrioritizer.cpp
bool ULocationBasedNetObjectPrioritizer::AddObject(uint32 ObjectIndex,
FNetObjectPrioritizerAddObjectParams& Params){
// 支持两种位置来源:WorldLocations 或状态中的 RepTag_WorldLocation 标签
UE::Net::FRepTagFindInfo TagInfo;
bool bHasWorldLocation = false;
// ═══════════════════════════════════════════════════════════════
// 步骤1:确定位置来源
// ═══════════════════════════════════════════════════════════════
// 优先检查 WorldLocations(更高效)
if (WorldLocations->HasInfoForObject(ObjectIndex))
{
bHasWorldLocation = true;
// 设置特殊标记,表示从 WorldLocations 获取位置
TagInfo.StateIndex = InvalidStateIndex;
TagInfo.ExternalStateOffset = InvalidStateOffset;
}
// 其次检查 RepTag_WorldLocation 标签
else if (!UE::Net::FindRepTag(Params.Protocol, UE::Net::RepTag_WorldLocation, TagInfo))
{
// 两种来源都没有,此优先级不支持该对象
return false;
}
// ═══════════════════════════════════════════════════════════════
// 步骤2:验证偏移范围(uint16 限制)
// ═══════════════════════════════════════════════════════════════
if (!bHasWorldLocation &&
((TagInfo.ExternalStateOffset >= MAX_uint16) || (TagInfo.StateIndex >= MAX_uint16)))
{
// 偏移超出 uint16 范围,无法存储
return false;
}
// ═══════════════════════════════════════════════════════════════
// 步骤3:存储位置信息到 FObjectLocationInfo
// ═══════════════════════════════════════════════════════════════
FObjectLocationInfo& ObjectInfo = static_cast<FObjectLocationInfo&>(Params.OutInfo);
ObjectInfo.SetLocationStateOffset(static_cast<uint16>(TagInfo.ExternalStateOffset));
ObjectInfo.SetLocationStateIndex(static_cast<uint16>(TagInfo.StateIndex));
// ═══════════════════════════════════════════════════════════════
// 步骤4:分配位置索引并初始化位置
// ═══════════════════════════════════════════════════════════════
const uint32 LocationIndex = AllocLocation();
ObjectInfo.SetLocationIndex(LocationIndex);
// 立即更新位置,确保有初始值
UpdateLocation(ObjectIndex, ObjectInfo, Params.InstanceProtocol);
return true;
}UpdateLocation 的完整实现:
CPP
// 📍 源文件:LocationBasedNetObjectPrioritizer.cpp
void ULocationBasedNetObjectPrioritizer::UpdateLocation(
const uint32 ObjectIndex,
const FObjectLocationInfo& Info,
const UE::Net::FReplicationInstanceProtocol* InstanceProtocol){
if (Info.IsUsingWorldLocations())
{
// ═══════════════════════════════════════════════════════════════
// 方式1:从 WorldLocations 系统获取位置
// ═══════════════════════════════════════════════════════════════
const FVector WorldLocation = WorldLocations->GetWorldLocation(ObjectIndex);
// VectorLoadFloat3_W0: 加载 XYZ,W 设为 0(SIMD 友好)
SetLocation(Info, VectorLoadFloat3_W0(&WorldLocation));
}
else
{
// ═══════════════════════════════════════════════════════════════
// 方式2:从状态缓冲区中读取位置(RepTag_WorldLocation)
// ═══════════════════════════════════════════════════════════════
// 获取 Fragment 数据数组
TArrayView<const UE::Net::FReplicationInstanceProtocol::FFragmentData> FragmentDatas =
MakeArrayView(InstanceProtocol->FragmentData, InstanceProtocol->FragmentCount);
// 根据状态索引获取对应的 Fragment
const UE::Net::FReplicationInstanceProtocol::FFragmentData& FragmentData =
FragmentDatas[Info.GetLocationStateIndex()];
// 计算位置在缓冲区中的地址
const uint8* LocationOffset = FragmentData.ExternalSrcBuffer + Info.GetLocationStateOffset();
// 直接读取 FVector(假设内存布局兼容)
SetLocation(Info, VectorLoadFloat3_W0(reinterpret_cast<const FVector*>(LocationOffset)));
}
}位置分配和释放:
CPP
// 📍 源文件:LocationBasedNetObjectPrioritizer.cpp
uint32 ULocationBasedNetObjectPrioritizer::AllocLocation(){
// 查找第一个未使用的位置索引
uint32 Index = AssignedLocationIndices.FindFirstZero();
if (Index >= uint32(Locations.Num()))
{
// 需要扩展 Locations 数组
// NumElementsPerChunk = LocationsChunkSize / sizeof(VectorRegister) = 4096 / 16 = 256
constexpr int32 NumElementsPerChunk = LocationsChunkSize / sizeof(VectorRegister);
Locations.Add(NumElementsPerChunk);
}
// 标记该索引已被使用
AssignedLocationIndices.SetBit(Index);
return Index;
}
void ULocationBasedNetObjectPrioritizer::FreeLocation(uint32 Index){
// 简单地清除标记,位置数据可以被复用
AssignedLocationIndices.ClearBit(Index);
}
// GetLocation/SetLocation 非常简单VectorRegister ULocationBasedNetObjectPrioritizer::GetLocation(const FObjectLocationInfo& Info) const{
return Locations[Info.GetLocationIndex()];
}
void ULocationBasedNetObjectPrioritizer::SetLocation(const FObjectLocationInfo& Info, VectorRegister Location){
Locations[Info.GetLocationIndex()] = Location;
}🔵 SphereNetObjectPrioritizer(球形距离优先级)
"离我越近,越重要!" 🎯
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔵 球形优先级器原理 │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ │
│ │ 外球 (OuterRadius)│ │
│ │ ┌───────────────┐ │ │
│ │ │ 内球 (Inner) │ │ │
│ │ │ ┌─────┐ │ │ │
│ │ │ │ 👤 │ │ │ ← 玩家位置 │
│ │ │ └─────┘ │ │ │
│ │ │ 优先级=1.0 │ │ ← 内球:最高优先级 │
│ │ └───────────────┘ │ │
│ │ 优先级=0.2~1.0 │ ← 外球:线性衰减 │
│ └─────────────────────┘ │
│ 优先级=0.1 ← 球外:最低优先级 │
│ │
│ 📐 优先级计算公式(线性插值): │
│ Distance = |ObjPos - ViewPos| │
│ ClampedDist = Clamp(Distance, InnerRadius, OuterRadius) │
│ Factor = (ClampedDist - InnerRadius) / (OuterRadius - InnerRadius)│
│ Priority = InnerPriority + Factor * (OuterPriority - InnerPriority)│
│ │
└────────────────────────────────────────────────────────────────┘球形优先级系统示意图:内球(绿色)优先级最高,外球(橙色)线性衰减,球外优先级最低
配置类:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Prioritization/SphereNetObjectPrioritizer.h
UCLASS(transient, config=Engine, MinimalAPI)
class USphereNetObjectPrioritizerConfig : public UNetObjectPrioritizerConfig
{
GENERATED_BODY()
public:
UPROPERTY(Config)
float InnerRadius = 2000.0f; // 🔴 内球半径(20米),内部优先级最高
UPROPERTY(Config)
float OuterRadius = 10000.0f; // 🟡 外球半径(100米),边界优先级
UPROPERTY(Config)
float InnerPriority = 1.0f; // 📊 内球内的优先级
UPROPERTY(Config)
float OuterPriority = 0.2f; // 📊 外球边界的优先级
UPROPERTY(Config)
float OutsidePriority = 0.1f; // 📊 球外的优先级
};完整的 Prioritize 实现(批处理架构):
CPP
// 📍 源文件:SphereNetObjectPrioritizer.cpp
void USphereNetObjectPrioritizer::Prioritize(FNetObjectPrioritizationParams& PrioritizationParams){
IRIS_PROFILER_SCOPE(USphereNetObjectPrioritizer_Prioritize);
// 使用线程局部内存栈,避免堆分配
FMemStack& Mem = FMemStack::Get();
FMemMark MemMark(Mem);
// 批处理大小:权衡内存使用和性能
// 1024 是经过调优的值,既能充分利用缓存,又不会占用太多栈空间
constexpr uint32 MaxBatchObjectCount = 1024U;
// 确保批大小是 4 的倍数(SIMD 要求)
uint32 BatchObjectCount = FMath::Min(
(PrioritizationParams.ObjectCount + 3U) & ~3U, // 向上取整到 4 的倍数
MaxBatchObjectCount);
// 设置批处理参数(分配临时数组)
FBatchParams BatchParams;
SetupBatchParams(BatchParams, PrioritizationParams, BatchObjectCount, Mem);
// 分批处理所有对象
for (uint32 ObjectIt = 0, ObjectEndIt = PrioritizationParams.ObjectCount;
ObjectIt < ObjectEndIt; )
{
const uint32 CurrentBatchObjectCount = FMath::Min(
ObjectEndIt - ObjectIt, MaxBatchObjectCount);
BatchParams.ObjectCount = CurrentBatchObjectCount;
// 1. 准备批次:复制优先级和位置到本地数组
PrepareBatch(BatchParams, PrioritizationParams, ObjectIt);
// 2. 计算优先级(根据视图数量选择优化路径)
PrioritizeBatch(BatchParams);
// 3. 完成批次:将结果写回全局数组
FinishBatch(BatchParams, PrioritizationParams, ObjectIt);
ObjectIt += CurrentBatchObjectCount;
}
}批处理参数设置:
CPP
// 📍 源文件:SphereNetObjectPrioritizer.cpp
void USphereNetObjectPrioritizer::SetupBatchParams(
FBatchParams& OutBatchParams,
const FNetObjectPrioritizationParams& PrioritizationParams,
uint32 MaxBatchObjectCount,
FMemStackBase& Mem){
OutBatchParams.View = PrioritizationParams.View;
OutBatchParams.ConnectionId = PrioritizationParams.ConnectionId;
// 从内存栈分配临时数组(比堆分配快得多)
OutBatchParams.Positions = static_cast<VectorRegister*>(
Mem.Alloc(MaxBatchObjectCount * sizeof(VectorRegister), alignof(VectorRegister)));
OutBatchParams.Priorities = static_cast<float*>(
Mem.Alloc(MaxBatchObjectCount * sizeof(float), 16)); // 16字节对齐,SIMD 友好
// 预计算优先级计算常量(避免每次计算时重复)
SetupCalculationConstants(OutBatchParams.PriorityCalculationConstants);
// 清零位置数组(防止未初始化数据影响 SIMD 计算)
FMemory::Memzero(OutBatchParams.Positions, MaxBatchObjectCount * sizeof(VectorRegister));
}
void USphereNetObjectPrioritizer::SetupCalculationConstants(FPriorityCalculationConstants& OutConstants){
// 将配置值转换为 SIMD 向量(每个分量都是相同的值)
const VectorRegister InnerRadius = VectorSetFloat1(Config->InnerRadius);
const VectorRegister OuterRadius = VectorSetFloat1(Config->OuterRadius);
const VectorRegister InnerPriority = VectorSetFloat1(Config->InnerPriority);
const VectorRegister OuterPriority = VectorSetFloat1(Config->OuterPriority);
const VectorRegister OutsidePriority = VectorSetFloat1(Config->OutsidePriority);
// 预计算差值(避免运行时重复计算)
const VectorRegister RadiusDiff = VectorSubtract(OuterRadius, InnerRadius);
const VectorRegister PriorityDiff = VectorSubtract(OuterPriority, InnerPriority);
OutConstants.InnerRadius = InnerRadius;
OutConstants.OuterRadius = OuterRadius;
OutConstants.RadiusDiff = RadiusDiff;
OutConstants.InvRadiusDiff = VectorReciprocalAccurate(RadiusDiff); // 1 / RadiusDiff
OutConstants.InnerPriority = InnerPriority;
OutConstants.OuterPriority = OuterPriority;
OutConstants.OutsidePriority = OutsidePriority;
OutConstants.PriorityDiff = PriorityDiff;
}视图数量分发:
CPP
// 📍 源文件:SphereNetObjectPrioritizer.cpp
void USphereNetObjectPrioritizer::PrioritizeBatch(FBatchParams& BatchParams){
const int32 ViewCount = BatchParams.View.Views.Num();
// 根据视图数量选择最优化的代码路径
if (ViewCount == 1)
{
PrioritizeBatchForSingleView(BatchParams); // 单视图:最常见,最优化
}
else if (ViewCount == 2)
{
PrioritizeBatchForDualView(BatchParams); // 双视图:分屏游戏优化
}
else
{
PrioritizeBatchForMultiView(BatchParams); // 多视图:通用但较慢
}
}单视图 SIMD 优化实现(核心算法):
CPP
// 📍 源文件:SphereNetObjectPrioritizer.cpp
/**
* 优先级线性衰减公式:
* Priority = OuterPriority + (OuterPriority - InnerPriority) *
* (Clamp(Distance(ObjPos, ViewPos), InnerRadius, OuterRadius) / (OuterRadius - InnerRadius))
*/void USphereNetObjectPrioritizer::PrioritizeBatchForSingleView(FBatchParams& BatchParams){
IRIS_PROFILER_SCOPE(USphereNetObjectPrioritizer_PrioritizeBatchForSingleView);
// 加载视图位置到 SIMD 寄存器
const FVector& ViewPosVector = BatchParams.View.Views[0].Pos;
const VectorRegister ViewPos = VectorLoadFloat3_W0(&ViewPosVector);
const VectorRegister* Positions = BatchParams.Positions;
float* Priorities = BatchParams.Priorities;
// ═══════════════════════════════════════════════════════════════
// 每次处理 4 个对象(SIMD 4-way 并行)
// ═══════════════════════════════════════════════════════════════
for (uint32 ObjIt = 0, ObjEndIt = BatchParams.ObjectCount; ObjIt < ObjEndIt; ObjIt += 4)
{
// 步骤1:加载 4 个对象的位置
const VectorRegister Pos0 = Positions[ObjIt + 0];
const VectorRegister Pos1 = Positions[ObjIt + 1];
const VectorRegister Pos2 = Positions[ObjIt + 2];
const VectorRegister Pos3 = Positions[ObjIt + 3];
// 加载原始优先级(用于 max 操作)
const VectorRegister OriginalPriorities0123 = VectorLoadAligned(Priorities + ObjIt);
// 步骤2:计算到视图中心的距离向量
const VectorRegister Dist0 = VectorSubtract(Pos0, ViewPos);
const VectorRegister Dist1 = VectorSubtract(Pos1, ViewPos);
const VectorRegister Dist2 = VectorSubtract(Pos2, ViewPos);
const VectorRegister Dist3 = VectorSubtract(Pos3, ViewPos);
// 步骤3:计算距离平方(点积 = x² + y² + z²)
const VectorRegister ScalarDistSqr0 = VectorDot4(Dist0, Dist0);
const VectorRegister ScalarDistSqr1 = VectorDot4(Dist1, Dist1);
const VectorRegister ScalarDistSqr2 = VectorDot4(Dist2, Dist2);
const VectorRegister ScalarDistSqr3 = VectorDot4(Dist3, Dist3);
// 步骤4:组装 4 个距离到单个向量
// $IRIS TODO: 可用 SSE 4.1 _mm_blend_ps 优化
const VectorRegister ScalarDistSqr0101 = VectorSwizzle(
VectorCombineHigh(ScalarDistSqr0, ScalarDistSqr1), 0, 2, 1, 3);
const VectorRegister ScalarDistSqr2323 = VectorSwizzle(
VectorCombineHigh(ScalarDistSqr2, ScalarDistSqr3), 0, 2, 1, 3);
const VectorRegister ScalarDistSqr0123 = VectorCombineHigh(ScalarDistSqr0101, ScalarDistSqr2323);
// 步骤5:计算实际距离(开方)
const VectorRegister ScalarDist0123 = VectorSqrt(ScalarDistSqr0123);
// 步骤6:Clamp 到 [0, OuterRadius - InnerRadius] 范围
// ClampedDist = max(Distance - InnerRadius, 0)
const VectorRegister ClampedScalarDist0123 = VectorMax(
VectorSubtract(ScalarDist0123, BatchParams.PriorityCalculationConstants.InnerRadius),
VectorZeroVectorRegister());
// 步骤7:计算优先级(假设在球内)
// Factor = ClampedDist / RadiusDiff
const VectorRegister RadiusFactor = VectorMultiply(
ClampedScalarDist0123,
BatchParams.PriorityCalculationConstants.InvRadiusDiff);
// Priority = InnerPriority + Factor * PriorityDiff
VectorRegister Priorities0123 = VectorMultiplyAdd(
RadiusFactor,
BatchParams.PriorityCalculationConstants.PriorityDiff,
BatchParams.PriorityCalculationConstants.InnerPriority);
// 步骤8:处理球外情况
// 如果 ClampedDist > RadiusDiff,则使用 OutsidePriority
const VectorRegister OutsideSphereMask = VectorCompareGT(
ClampedScalarDist0123,
BatchParams.PriorityCalculationConstants.RadiusDiff);
Priorities0123 = VectorSelect(
OutsideSphereMask,
BatchParams.PriorityCalculationConstants.OutsidePriority,
Priorities0123);
// 步骤9:取计算值和原始值的最大值
// 这允许其他系统预设更高的优先级
Priorities0123 = VectorMax(Priorities0123, OriginalPriorities0123);
// 步骤10:存储结果
VectorStoreAligned(Priorities0123, Priorities + ObjIt);
}
}双视图实现(分屏游戏优化):
CPP
// 📍 源文件:SphereNetObjectPrioritizer.cpp
void USphereNetObjectPrioritizer::PrioritizeBatchForDualView(FBatchParams& BatchParams){
IRIS_PROFILER_SCOPE(USphereNetObjectPrioritizer_PrioritizeBatchForDualView);
// 加载两个视图位置
const FVector& ViewPos0Vector = BatchParams.View.Views[0].Pos;
const VectorRegister ViewPos0 = VectorLoadFloat3_W0(&ViewPos0Vector);
const FVector& ViewPos1Vector = BatchParams.View.Views[1].Pos;
const VectorRegister ViewPos1 = VectorLoadFloat3_W0(&ViewPos1Vector);
const VectorRegister* Positions = BatchParams.Positions;
float* Priorities = BatchParams.Priorities;
for (uint32 ObjIt = 0, ObjEndIt = BatchParams.ObjectCount; ObjIt < ObjEndIt; ObjIt += 4)
{
const VectorRegister Pos0 = Positions[ObjIt + 0];
const VectorRegister Pos1 = Positions[ObjIt + 1];
const VectorRegister Pos2 = Positions[ObjIt + 2];
const VectorRegister Pos3 = Positions[ObjIt + 3];
const VectorRegister OriginalPriorities0123 = VectorLoadAligned(Priorities + ObjIt);
// ═══════════════════════════════════════════════════════════════
// 计算到第一个视图的距离
// ═══════════════════════════════════════════════════════════════
const VectorRegister Dist0_0 = VectorSubtract(Pos0, ViewPos0);
const VectorRegister Dist1_0 = VectorSubtract(Pos1, ViewPos0);
const VectorRegister Dist2_0 = VectorSubtract(Pos2, ViewPos0);
const VectorRegister Dist3_0 = VectorSubtract(Pos3, ViewPos0);
VectorRegister ScalarDistSqr0 = VectorDot4(Dist0_0, Dist0_0);
VectorRegister ScalarDistSqr1 = VectorDot4(Dist1_0, Dist1_0);
VectorRegister ScalarDistSqr2 = VectorDot4(Dist2_0, Dist2_0);
VectorRegister ScalarDistSqr3 = VectorDot4(Dist3_0, Dist3_0);
// ═══════════════════════════════════════════════════════════════
// 计算到第二个视图的距离
// ═══════════════════════════════════════════════════════════════
const VectorRegister Dist0_1 = VectorSubtract(Pos0, ViewPos1);
const VectorRegister Dist1_1 = VectorSubtract(Pos1, ViewPos1);
const VectorRegister Dist2_1 = VectorSubtract(Pos2, ViewPos1);
const VectorRegister Dist3_1 = VectorSubtract(Pos3, ViewPos1);
const VectorRegister ScalarDistSqr0_1 = VectorDot4(Dist0_1, Dist0_1);
const VectorRegister ScalarDistSqr1_1 = VectorDot4(Dist1_1, Dist1_1);
const VectorRegister ScalarDistSqr2_1 = VectorDot4(Dist2_1, Dist2_1);
const VectorRegister ScalarDistSqr3_1 = VectorDot4(Dist3_1, Dist3_1);
// ═══════════════════════════════════════════════════════════════
// 🔑 关键:选择最近的视图距离(取最小值)
// ═══════════════════════════════════════════════════════════════
ScalarDistSqr0 = VectorMin(ScalarDistSqr0, ScalarDistSqr0_1);
ScalarDistSqr1 = VectorMin(ScalarDistSqr1, ScalarDistSqr1_1);
ScalarDistSqr2 = VectorMin(ScalarDistSqr2, ScalarDistSqr2_1);
ScalarDistSqr3 = VectorMin(ScalarDistSqr3, ScalarDistSqr3_1);
// 后续计算与单视图相同...
// (组装距离、开方、Clamp、计算优先级、处理球外、取最大值、存储)
}
}多视图实现(通用路径):
CPP
// 📍 源文件:SphereNetObjectPrioritizer.cpp
void USphereNetObjectPrioritizer::PrioritizeBatchForMultiView(FBatchParams& BatchParams){
IRIS_PROFILER_SCOPE(USphereNetObjectPrioritizer_PrioritizeBatchForMultiView);
// 预加载所有视图位置(使用内联分配器,最多 8 个视图不需要堆分配)
TArray<VectorRegister, TInlineAllocator<8>> ViewPositions;
for (const UE::Net::FReplicationView::FView& View : BatchParams.View.Views)
{
const FVector& ViewPosVector = View.Pos;
ViewPositions.Add(VectorLoadFloat3_W0(&ViewPosVector));
}
// 性能警告:超过 8 个视图会触发堆分配
ensureMsgf(ViewPositions.Num() <= 8,
TEXT("Performance warning: Global allocation was needed to accommodate %d views."),
ViewPositions.Num());
const VectorRegister MaxFloatVector = VectorSetFloat1(MAX_flt);
const VectorRegister* Positions = BatchParams.Positions;
float* Priorities = BatchParams.Priorities;
for (uint32 ObjIt = 0, ObjEndIt = BatchParams.ObjectCount; ObjIt < ObjEndIt; ObjIt += 4)
{
const VectorRegister Pos0 = Positions[ObjIt + 0];
const VectorRegister Pos1 = Positions[ObjIt + 1];
const VectorRegister Pos2 = Positions[ObjIt + 2];
const VectorRegister Pos3 = Positions[ObjIt + 3];
const VectorRegister OriginalPriorities0123 = VectorLoadAligned(Priorities + ObjIt);
// 初始化为最大距离
VectorRegister ScalarDistSqr0 = MaxFloatVector;
VectorRegister ScalarDistSqr1 = MaxFloatVector;
VectorRegister ScalarDistSqr2 = MaxFloatVector;
VectorRegister ScalarDistSqr3 = MaxFloatVector;
// ═══════════════════════════════════════════════════════════════
// 遍历所有视图,取最小距离
// ═══════════════════════════════════════════════════════════════
for (VectorRegister ViewPos : ViewPositions)
{
const VectorRegister Dist0 = VectorSubtract(Pos0, ViewPos);
const VectorRegister Dist1 = VectorSubtract(Pos1, ViewPos);
const VectorRegister Dist2 = VectorSubtract(Pos2, ViewPos);
const VectorRegister Dist3 = VectorSubtract(Pos3, ViewPos);
ScalarDistSqr0 = VectorMin(ScalarDistSqr0, VectorDot4(Dist0, Dist0));
ScalarDistSqr1 = VectorMin(ScalarDistSqr1, VectorDot4(Dist1, Dist1));
ScalarDistSqr2 = VectorMin(ScalarDistSqr2, VectorDot4(Dist2, Dist2));
ScalarDistSqr3 = VectorMin(ScalarDistSqr3, VectorDot4(Dist3, Dist3));
}
// 后续计算与单视图相同...
}
}👑 SphereWithOwnerBoostNetObjectPrioritizer(所有者加成)
"自己的东西,当然更重要!" 👑
CPP
// 📍 源文件:SphereWithOwnerBoostNetObjectPrioritizer.h
UCLASS(Transient, Config=Engine)
class USphereWithOwnerBoostNetObjectPrioritizerConfig
: public USphereNetObjectPrioritizerConfig // 继承球形配置
{
public:
UPROPERTY(Config)
float OwnerPriorityBoost = 2.0f; // 👑 所有者优先级加成
};PLAINTEXT
对玩家A来说:
自己的宠物🐕 优先级 = 距离优先级(0.5) + 所有者加成(2.0) = 2.5
对玩家B来说:
别人的宠物🐕 优先级 = 距离优先级(0.5) + 0 = 0.5👁️ FieldOfViewNetObjectPrioritizer(视野优先级)
"我正在看的方向,更重要!" 👁️
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 👁️ 视野优先级器原理 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 视野锥 (Cone) │
│ ╱ ╲ │
│ ╱ 高优先级 ╲ │
│ ╱ 🎯 🎯 ╲ │
│ ╱ ╲ │
│ ╱ 👤 ╲ │
│ ╱ (玩家位置) ╲ │
│ ╱ ┌─────────────┐ ╲ │
│ ╱ │ 内球(高) │ ╲ │
│ ╱ └─────────────┘ ╲ │
│ ╱ 外球(中) ╲ │
│ ╱ ╲ │
│ ╱ 视线胶囊(最高) ╲ │
│ ╱ ════════════ ╲ │
│ │
│ 📊 优先级区域(按优先级从高到低): │
│ 1. 视线胶囊 (Line of Sight) - 准星正对方向 │
│ 2. 内球 (Inner Sphere) - 近距离 360° │
│ 3. 视野锥 (Cone) - 前方视野范围 │
│ 4. 外球 (Outer Sphere) - 中距离 360° │
│ 5. 外部 (Outside) - 所有其他位置 │
│ │
└────────────────────────────────────────────────────────────────┘视野优先级系统示意图:视线胶囊(红色)优先级最高,内球(绿色)次之,视野锥(紫色)中高,外球(橙色)中等
完整配置类:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Prioritization/FieldOfViewNetObjectPrioritizer.h
UCLASS(transient, config=Engine, MinimalAPI)
class UFieldOfViewNetObjectPrioritizerConfig : public UNetObjectPrioritizerConfig
{
GENERATED_BODY()
public:
// ═══════════════════════════════════════════════════════════════
// 🔵 内球参数(近距离 360° 高优先级)
// ═══════════════════════════════════════════════════════════════
UPROPERTY(Config)
float InnerSphereRadius = 3000.0f; // 内球半径(30米)
UPROPERTY(Config)
float InnerSpherePriority = 1.0f; // 内球优先级
// ═══════════════════════════════════════════════════════════════
// 🟡 外球参数(中距离 360° 中等优先级)
// ═══════════════════════════════════════════════════════════════
UPROPERTY(Config)
float OuterSphereRadius = 10000.0f; // 外球半径(100米)
UPROPERTY(Config)
float OuterSpherePriority = 0.2f; // 外球优先级
// ═══════════════════════════════════════════════════════════════
// 📐 视野锥参数(前方视野范围)
// ═══════════════════════════════════════════════════════════════
UPROPERTY(Config)
float ConeFieldOfViewDegrees = 45.0f; // 视野角度(单侧,总角度 90°)
UPROPERTY(Config)
float InnerConeLength = 3000.0f; // 内锥长度(30米内最高优先级)
UPROPERTY(Config)
float ConeLength = 20000.0f; // 锥体总长度(200米)
UPROPERTY(Config)
float MinConePriority = 0.2f; // 锥体边缘优先级
UPROPERTY(Config)
float MaxConePriority = 1.0f; // 锥体中心优先级
// ═══════════════════════════════════════════════════════════════
// 🎯 视线参数(准星正对方向)
// ═══════════════════════════════════════════════════════════════
UPROPERTY(Config)
float LineOfSightWidth = 200.0f; // 视线宽度(2米直径的胶囊)
UPROPERTY(Config)
float LineOfSightPriority = 1.0f; // 视线优先级(最高)
// ═══════════════════════════════════════════════════════════════
// ⬛ 外部优先级
// ═══════════════════════════════════════════════════════════════
UPROPERTY(Config)
float OutsidePriority = 0.1f; // 所有区域外的优先级
};计算常量设置(预计算优化):
CPP
// 📍 源文件:FieldOfViewNetObjectPrioritizer.cpp
void UFieldOfViewNetObjectPrioritizer::SetupCalculationConstants(FPriorityCalculationConstants& OutConstants){
// ═══════════════════════════════════════════════════════════════
// 锥体常量
// ═══════════════════════════════════════════════════════════════
VectorRegister InnerConeLength = VectorSetFloat1(Config->InnerConeLength);
VectorRegister ConeLength = VectorSetFloat1(Config->ConeLength);
VectorRegister ConeLengthDiff = VectorSubtract(ConeLength, InnerConeLength);
VectorRegister InvConeLengthDiff = VectorReciprocalAccurate(ConeLengthDiff);
// 计算锥体半径:ConeLength * tan(FOV/2)
VectorRegister ConeRadius = VectorSetFloat1(
Config->ConeLength * FMath::Tan(0.5f * FMath::DegreesToRadians(Config->ConeFieldOfViewDegrees)));
// 锥体半径因子:用于计算任意距离处的锥体半径
// RadiusAtDist = Dist * ConeRadiusFactor
VectorRegister ConeRadiusFactor = VectorDivide(ConeRadius, ConeLength);
VectorRegister InnerConePriority = VectorSetFloat1(Config->MaxConePriority);
VectorRegister OuterConePriority = VectorSetFloat1(Config->MinConePriority);
VectorRegister ConePriorityDiff = VectorSubtract(OuterConePriority, InnerConePriority);
// ═══════════════════════════════════════════════════════════════
// 球体常量(使用半径平方避免开方)
// ═══════════════════════════════════════════════════════════════
VectorRegister InnerSphereRadiusSqr = VectorSetFloat1(FMath::Square(Config->InnerSphereRadius));
VectorRegister OuterSphereRadiusSqr = VectorSetFloat1(FMath::Square(Config->OuterSphereRadius));
VectorRegister InnerSpherePriority = VectorSetFloat1(Config->InnerSpherePriority);
VectorRegister OuterSpherePriority = VectorSetFloat1(Config->OuterSpherePriority);
// ═══════════════════════════════════════════════════════════════
// 视线常量
// ═══════════════════════════════════════════════════════════════
// 视线是一个胶囊体,半径 = Width/2
VectorRegister LineOfSightRadiusSqr = VectorSetFloat1(FMath::Square(0.5f * Config->LineOfSightWidth));
VectorRegister LineOfSightPriority = VectorSetFloat1(Config->LineOfSightPriority);
// ═══════════════════════════════════════════════════════════════
// 外部优先级
// ═══════════════════════════════════════════════════════════════
VectorRegister OutsidePriority = VectorSetFloat1(Config->OutsidePriority);
// 存储所有常量...
}完整的 PrioritizeBatch 实现:
CPP
// 📍 源文件:FieldOfViewNetObjectPrioritizer.cpp
void UFieldOfViewNetObjectPrioritizer::PrioritizeBatch(FBatchParams& BatchParams){
IRIS_PROFILER_SCOPE(UFieldOfViewNetObjectPrioritizer_PrioritizeBatch);
// 预加载所有视图位置和方向
// 注意:锥体和视线依赖视图方向,无法像球体那样只用最近距离优化
TArray<VectorRegister, TInlineAllocator<16>> ViewPositions;
TArray<VectorRegister, TInlineAllocator<16>> ViewDirs;
for (const UE::Net::FReplicationView::FView& View : BatchParams.View.Views)
{
const FVector& ViewPos = View.Pos;
const FVector& ViewDir = View.Dir;
ViewPositions.Add(VectorLoadFloat3_W0(&ViewPos));
ViewDirs.Add(VectorLoadFloat3_W0(&ViewDir));
}
const VectorRegister* Positions = BatchParams.Positions;
float* Priorities = BatchParams.Priorities;
const int ViewCount = BatchParams.View.Views.Num();
for (uint32 ObjIt = 0, ObjEndIt = BatchParams.ObjectCount; ObjIt < ObjEndIt; ObjIt += 4)
{
// 初始化为外部优先级(取原始值和外部优先级的最大值)
VectorRegister Priorities0123 = VectorMax(
VectorLoadAligned(Priorities + ObjIt),
BatchParams.PriorityCalculationConstants.OutsidePriority);
// ═══════════════════════════════════════════════════════════════
// 遍历所有视图(锥体和视线依赖视图方向,必须逐个处理)
// ═══════════════════════════════════════════════════════════════
for (int ViewIt = 0, ViewEndIt = ViewCount; ViewIt < ViewEndIt; ++ViewIt)
{
const VectorRegister ViewPos = ViewPositions[ViewIt];
const VectorRegister ViewDir = ViewDirs[ViewIt];
// 步骤1:计算对象方向向量(从视图到对象)
const VectorRegister ObjectDir0 = VectorSubtract(Positions[ObjIt + 0], ViewPos);
const VectorRegister ObjectDir1 = VectorSubtract(Positions[ObjIt + 1], ViewPos);
const VectorRegister ObjectDir2 = VectorSubtract(Positions[ObjIt + 2], ViewPos);
const VectorRegister ObjectDir3 = VectorSubtract(Positions[ObjIt + 3], ViewPos);
// 步骤2:计算到视图位置的距离平方
const VectorRegister DistSqrToViewPos0 = VectorDot4(ObjectDir0, ObjectDir0);
const VectorRegister DistSqrToViewPos1 = VectorDot4(ObjectDir1, ObjectDir1);
const VectorRegister DistSqrToViewPos2 = VectorDot4(ObjectDir2, ObjectDir2);
const VectorRegister DistSqrToViewPos3 = VectorDot4(ObjectDir3, ObjectDir3);
// 组装距离向量
const VectorRegister DistSqrToViewPos0101 = VectorSwizzle(
VectorCombineHigh(DistSqrToViewPos0, DistSqrToViewPos1), 0, 2, 1, 3);
const VectorRegister DistSqrToViewPos2323 = VectorSwizzle(
VectorCombineHigh(DistSqrToViewPos2, DistSqrToViewPos3), 0, 2, 1, 3);
const VectorRegister DistSqrToViewPos0123 = VectorCombineHigh(
DistSqrToViewPos0101, DistSqrToViewPos2323);
const VectorRegister DistToViewPos0123 = VectorSqrt(DistSqrToViewPos0123);
// 步骤3:投影对象方向到锥体中心轴(点积 = 沿视线方向的距离)
const VectorRegister ConeDist0 = VectorDot4(ObjectDir0, ViewDir);
const VectorRegister ConeDist1 = VectorDot4(ObjectDir1, ViewDir);
const VectorRegister ConeDist2 = VectorDot4(ObjectDir2, ViewDir);
const VectorRegister ConeDist3 = VectorDot4(ObjectDir3, ViewDir);
// 步骤4:计算到锥体中心轴的距离平方
// 投影点 = ViewPos + ConeDist * ViewDir
// 轴距离向量 = ObjPos - 投影点 = ObjectDir - ConeDist * ViewDir
VectorRegister DistSqrToConeCenterAxis0 = VectorSubtract(
ObjectDir0, VectorMultiply(ConeDist0, ViewDir));
DistSqrToConeCenterAxis0 = VectorDot4(DistSqrToConeCenterAxis0, DistSqrToConeCenterAxis0);
// ... 其他3个类似 ...
// 组装锥体距离和轴距离
const VectorRegister ConeDist0123 = /* 组装 */;
const VectorRegister DistSqrToConeCenterAxis0123 = /* 组装 */;
// 步骤5:验证锥体距离在有效范围 [0, ConeLength]
const VectorRegister ConeDistGEZeroMask = VectorCompareGE(
ConeDist0123, VectorZeroVectorRegister());
const VectorRegister ConeDistLEDistMask = VectorCompareLE(
ConeDist0123, BatchParams.PriorityCalculationConstants.ConeLength);
const VectorRegister ConeDistInRangeMask = VectorBitwiseAnd(
ConeDistGEZeroMask, ConeDistLEDistMask);
// 步骤6:计算该距离处的锥体半径平方
// RadiusAtDist = ConeDist * ConeRadiusFactor
VectorRegister ConeRadiusAtDistSqr = VectorMultiply(
ConeDist0123, BatchParams.PriorityCalculationConstants.ConeRadiusFactor);
ConeRadiusAtDistSqr = VectorMultiply(ConeRadiusAtDistSqr, ConeRadiusAtDistSqr);
// ═══════════════════════════════════════════════════════════════
// 计算各区域优先级
// ═══════════════════════════════════════════════════════════════
// 🔺 锥体优先级
// 条件:在锥体内 && 在有效距离范围内
const VectorRegister InsideConeMask = VectorBitwiseAnd(
VectorCompareLE(DistSqrToConeCenterAxis0123, ConeRadiusAtDistSqr),
ConeDistInRangeMask);
const VectorRegister InsideInnerConeMask = VectorCompareLE(
ConeDist0123, BatchParams.PriorityCalculationConstants.InnerConeLength);
// 锥体优先级线性插值
const VectorRegister ConeLengthFactor = VectorMultiply(
VectorSubtract(DistToViewPos0123,
BatchParams.PriorityCalculationConstants.InnerConeLength),
BatchParams.PriorityCalculationConstants.InvConeLengthDiff);
VectorRegister ConePriorities0123 = VectorMultiplyAdd(
ConeLengthFactor,
BatchParams.PriorityCalculationConstants.ConePriorityDiff,
BatchParams.PriorityCalculationConstants.InnerConePriority);
// 内锥体使用最大优先级
ConePriorities0123 = VectorSelect(
InsideInnerConeMask,
BatchParams.PriorityCalculationConstants.InnerConePriority,
ConePriorities0123);
ConePriorities0123 = VectorBitwiseAnd(ConePriorities0123, InsideConeMask);
// 🎯 视线优先级
// 条件:到中心轴距离 <= 视线半径 && 在有效距离范围内
const VectorRegister InsideLineOfSightMask = VectorBitwiseAnd(
VectorCompareLE(DistSqrToConeCenterAxis0123,
BatchParams.PriorityCalculationConstants.LineOfSightRadiusSqr),
ConeDistInRangeMask);
const VectorRegister LoSPriorities0123 = VectorBitwiseAnd(
InsideLineOfSightMask,
BatchParams.PriorityCalculationConstants.LineOfSightPriority);
// 🟡 外球优先级
const VectorRegister InsideOuterSphereMask = VectorCompareLE(
DistSqrToViewPos0123,
BatchParams.PriorityCalculationConstants.OuterSphereRadiusSqr);
const VectorRegister OuterSpherePriorities0123 = VectorBitwiseAnd(
InsideOuterSphereMask,
BatchParams.PriorityCalculationConstants.OuterSpherePriority);
// 🔵 内球优先级
const VectorRegister InsideInnerSphereMask = VectorCompareLE(
DistSqrToViewPos0123,
BatchParams.PriorityCalculationConstants.InnerSphereRadiusSqr);
const VectorRegister InsideSpherePriorities0123 = VectorBitwiseAnd(
InsideInnerSphereMask,
BatchParams.PriorityCalculationConstants.InnerSpherePriority);
// ═══════════════════════════════════════════════════════════════
// 🔑 关键:取所有优先级的最大值
// ═══════════════════════════════════════════════════════════════
Priorities0123 = VectorMax(Priorities0123,
VectorMax(ConePriorities0123, LoSPriorities0123));
Priorities0123 = VectorMax(Priorities0123,
VectorMax(OuterSpherePriorities0123, InsideSpherePriorities0123));
}
// 存储结果
VectorStoreAligned(Priorities0123, Priorities + ObjIt);
}
}优先级区域判断逻辑图解:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📐 FOV 优先级区域判断 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 对于每个对象,计算以下值: │
│ • DistToViewPos: 到视图位置的距离 │
│ • ConeDist: 沿视线方向的投影距离(点积) │
│ • DistToConeCenterAxis: 到视线中心轴的垂直距离 │
│ │
│ 判断顺序(取最大优先级): │
│ │
│ 1. 内球检查 │
│ if (DistToViewPos² <= InnerSphereRadius²) │
│ Priority = max(Priority, InnerSpherePriority) │
│ │
│ 2. 外球检查 │
│ if (DistToViewPos² <= OuterSphereRadius²) │
│ Priority = max(Priority, OuterSpherePriority) │
│ │
│ 3. 视线检查(在有效距离范围内) │
│ if (ConeDist >= 0 && ConeDist <= ConeLength && │
│ DistToConeCenterAxis² <= LineOfSightRadius²) │
│ Priority = max(Priority, LineOfSightPriority) │
│ │
│ 4. 锥体检查(在有效距离范围内) │
│ ConeRadiusAtDist = ConeDist * tan(FOV/2) │
│ if (ConeDist >= 0 && ConeDist <= ConeLength && │
│ DistToConeCenterAxis² <= ConeRadiusAtDist²) │
│ ConePriority = Lerp(MaxConePriority, MinConePriority, │
│ (DistToViewPos - InnerConeLength) │
│ / (ConeLength - InnerConeLength)) │
│ Priority = max(Priority, ConePriority) │
│ │
│ 📌 注意:所有检查都是独立的,最终取最大值 │
│ │
└────────────────────────────────────────────────────────────────┘🔢 NetObjectCountLimiter(对象数量限制器)
"不管优先级多高,每帧只能处理这么多!" 🔢
这是一个独立的优先级(不继承自 LocationBasedNetObjectPrioritizer),专门用于限制每帧考虑复制的对象数量。
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔢 CountLimiter 应用场景 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 场景:游戏中有 1000 个金币🪙 │
│ │
│ 问题: │
│ • 金币数量太多,全部计算优先级太慢 │
│ • 金币重要性相近,没必要精确排序 │
│ • 玩家不需要同时看到所有金币 │
│ │
│ 解决方案:使用 CountLimiter │
│ • 每帧只考虑 N 个金币(如 10 个) │
│ • 轮流给每个金币"出镜机会" │
│ • 防止任何金币被永久"饿死" │
│ │
└────────────────────────────────────────────────────────────────┘完整配置类:
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/Prioritization/NetObjectCountLimiter.h
UENUM()
enum class ENetObjectCountLimiterMode : uint32
{
/**
* RoundRobin 模式:每次网络更新,下 N 个对象将被允许复制(如果有修改)
* 这意味着即使有很多对象有修改,如果本帧考虑的 N 个对象没有修改,
* 则不会发送任何数据。
*/
RoundRobin,
/**
* Fill 模式:每次网络更新,N 个最久未复制的有修改对象将被允许复制
* 如果一个对象修改频繁而其他对象不修改,该对象可能会被非常频繁地复制
*/
Fill,
};
UCLASS(transient, config=Engine, MinimalAPI)
class UNetObjectCountLimiterConfig : public UNetObjectPrioritizerConfig
{
GENERATED_BODY()
public:
UPROPERTY()
ENetObjectCountLimiterMode Mode = ENetObjectCountLimiterMode::RoundRobin;
/**
* 每帧考虑复制的最大对象数
* 设为 2 时,至少 1 个非连接拥有的对象会被考虑
* 如果优先级不处理连接拥有的对象,可以设为 1
*/
UPROPERTY(Config)
uint32 MaxObjectCount = 2;
/**
* 被考虑复制的对象的优先级
* 优先级会累积直到对象被复制
* 1.0f 是对象可能被复制的阈值
*/
UPROPERTY(Config)
float Priority = 1.0f;
/**
* 如果对象被当前连接拥有,设置的优先级
*/
UPROPERTY(Config)
float OwningConnectionPriority = 1.0f;
/**
* 连接拥有的对象是否总是被考虑复制
* 如果是,这些对象不计入 MaxObjectCount
*/
UPROPERTY(Config)
bool bEnableOwnedObjectsFastLane = true;
};核心数据结构:
CPP
// 📍 源文件:NetObjectCountLimiter.h
UCLASS()
class UNetObjectCountLimiter : public UNetObjectPrioritizer
{
GENERATED_BODY()
protected:
// ═══════════════════════════════════════════════════════════════
// 📦 对象信息结构(重新解释 FNetObjectPrioritizationInfo 的 8 字节)
// ═══════════════════════════════════════════════════════════════
struct FObjectInfo : public FNetObjectPrioritizationInfo
{
// Data[0]: 优先级内部索引
void SetPrioritizerInternalIndex(uint16 Index) { Data[0] = Index; }
uint16 GetPrioritizerInternalIndex() const { return Data[0]; }
// Data[1]: 拥有该对象的连接 ID(用于快速通道判断)
void SetOwningConnection(uint32 ConnectionId) { Data[1] = static_cast<uint16>(ConnectionId); }
uint32 GetOwningConnection() const { return static_cast<uint32>(Data[1]); }
};
// ═══════════════════════════════════════════════════════════════
// 📊 每连接信息(用于 Fill 模式的饥饿追踪)
// ═══════════════════════════════════════════════════════════════
struct FPerConnectionInfo
{
// 每个对象上次被考虑复制的帧号
// 用于计算"饥饿度" = 当前帧 - 上次考虑帧
TArray<uint32> LastConsiderFrames;
};
private:
// ═══════════════════════════════════════════════════════════════
// 🔄 RoundRobin 状态
// ═══════════════════════════════════════════════════════════════
struct FRoundRobinState
{
UE::Net::FNetBitArray InternalObjectIndices; // 本帧要考虑的对象
uint16 NextIndexToConsider = 0; // 下次开始的索引
};
// ═══════════════════════════════════════════════════════════════
// 📥 Fill 状态(用于检测同一帧多次调用)
// ═══════════════════════════════════════════════════════════════
struct FFillState
{
uint32 LastPrioFrame = 0; // 上次优先级计算的帧
uint32 LastConnectionId = 0; // 上次处理的连接 ID
};
// 配置和状态
TStrongObjectPtr<UNetObjectCountLimiterConfig> Config;
TArray<FPerConnectionInfo> PerConnectionInfos; // 每连接的饥饿追踪
UE::Net::FNetBitArray InternalObjectIndices; // 所有对象的位数组
FRoundRobinState RoundRobinState;
FFillState FillState;
uint32 PrioFrame; // 当前帧号(每帧递增)
enum : unsigned { ObjectGrowCount = 64U }; // 每次增长 64 个对象
};🔄 RoundRobin 模式(轮询)
"排好队,一个一个来!" 🔄
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔄 RoundRobin 轮询原理 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 对象池:[🪙0] [🪙1] [🪙2] [🪙3] [🪙4] [🪙5] [🪙6] [🪙7] [🪙8] [🪙9]│
│ │
│ MaxObjectCount = 3 │
│ │
│ 帧1: NextIndex=0 → 选择 [🪙0] [🪙1] [🪙2] → NextIndex=3 │
│ 帧2: NextIndex=3 → 选择 [🪙3] [🪙4] [🪙5] → NextIndex=6 │
│ 帧3: NextIndex=6 → 选择 [🪙6] [🪙7] [🪙8] → NextIndex=9 │
│ 帧4: NextIndex=9 → 选择 [🪙9] [🪙0] [🪙1] → NextIndex=2 │
│ ↑ 回绕到开头 │
│ │
│ 特点: │
│ • 简单高效,O(N) 复杂度 │
│ • 公平轮询,每个对象都有机会 │
│ • 内存开销低(只需一个位数组) │
│ • 所有连接共享同一个轮询状态 │
│ │
└────────────────────────────────────────────────────────────────┘完整的 PrePrioritize 实现:
CPP
// 📍 源文件:NetObjectCountLimiter.cpp
void UNetObjectCountLimiter::PrePrioritize(FNetObjectPrePrioritizationParams& Params){
// 每帧递增帧号(用于 Fill 模式的饥饿计算)
++PrioFrame;
switch (Config->Mode)
{
case ENetObjectCountLimiterMode::RoundRobin:
{
PrePrioritizeForRoundRobin();
break;
};
case ENetObjectCountLimiterMode::Fill:
{
// Fill 模式不需要 PrePrioritize,因为每个连接独立处理
break;
};
};
}
void UNetObjectCountLimiter::PrePrioritizeForRoundRobin(){
IRIS_PROFILER_SCOPE(UNetObjectCountLimiter_PrePrioritizeForRoundRobin);
// 查找下 N 个可用对象。这些对象将被所有连接考虑复制(如果有脏数据)
const uint32 IndexCount = InternalObjectIndices.GetNumBits();
// 重置本帧要考虑的对象位数组
RoundRobinState.InternalObjectIndices.Init(IndexCount);
// 🔄 处理回绕:如果索引超出范围,从头开始
if (RoundRobinState.NextIndexToConsider >= IndexCount)
{
RoundRobinState.NextIndexToConsider = 0;
}
const uint32 MaxObjectCount = Config->MaxObjectCount;
// 遵循用户配置。如果 MaxObjectCount=0,不会优先级化任何对象
// 对象只会在构造时复制一次,之后不再复制
if (MaxObjectCount == 0)
{
return;
}
// 使用栈分配临时数组(避免堆分配)
uint32* Indices = static_cast<uint32*>(FMemory_Alloca(MaxObjectCount * sizeof(uint32)));
// 从当前位置获取 N 个已设置的位
uint32 ObjectCount = InternalObjectIndices.GetSetBitIndices(
RoundRobinState.NextIndexToConsider, ~0U, Indices, MaxObjectCount);
// 如果没找够且不是从头开始,从头继续找
if (RoundRobinState.NextIndexToConsider > 0 && ObjectCount < MaxObjectCount)
{
ObjectCount += InternalObjectIndices.GetSetBitIndices(
0U,
RoundRobinState.NextIndexToConsider - 1U,
Indices + ObjectCount,
MaxObjectCount - ObjectCount);
}
if (ObjectCount)
{
// 更新下次开始位置(最后一个选中对象的下一个位置)
RoundRobinState.NextIndexToConsider = static_cast<uint16>(Indices[ObjectCount - 1U] + 1U);
// 在位数组中标记本帧要考虑的对象
for (uint32 Index : MakeArrayView(Indices, ObjectCount))
{
RoundRobinState.InternalObjectIndices.SetBit(Index);
}
}
}完整的 PrioritizeForRoundRobin 实现:
CPP
// 📍 源文件:NetObjectCountLimiter.cpp
void UNetObjectCountLimiter::PrioritizeForRoundRobin(FNetObjectPrioritizationParams& Params) const{
IRIS_PROFILER_SCOPE(UNetObjectCountLimiter_PrioritizeForRoundRobin);
const bool bEnableOwnedObjectsFastLane = Config->bEnableOwnedObjectsFastLane;
const float StandardPriority = Config->Priority;
const float OwningConnectionPriority = Config->OwningConnectionPriority;
const uint32 MaxConsiderCount = Config->MaxObjectCount;
uint32 ConsiderCount = 0U;
if (bEnableOwnedObjectsFastLane)
{
// ═══════════════════════════════════════════════════════════════
// 🚀 快速通道模式:拥有的对象不计入 MaxObjectCount
// ═══════════════════════════════════════════════════════════════
for (const uint32 ObjectIndex : MakeArrayView(Params.ObjectIndices, Params.ObjectCount))
{
const FObjectInfo& Info = static_cast<const FObjectInfo&>(
Params.PrioritizationInfos[ObjectIndex]);
// 检查是否在本帧轮询列表中
const bool bIsRoundRobin = RoundRobinState.InternalObjectIndices.GetBit(
Info.GetPrioritizerInternalIndex());
// 检查是否被当前连接拥有
const bool bIsOwnedByConnection = Info.GetOwningConnection() == Params.ConnectionId;
// 必须是轮询对象或拥有的对象
if (!(bIsRoundRobin | bIsOwnedByConnection))
{
continue;
}
if (bIsOwnedByConnection)
{
// 🚀 快速通道:拥有的对象总是获得优先级,不计入配额
Params.Priorities[ObjectIndex] = OwningConnectionPriority;
}
else if (ConsiderCount < MaxConsiderCount)
{
// 普通对象:计入配额
++ConsiderCount;
Params.Priorities[ObjectIndex] = StandardPriority;
}
// else: 已达到配额,跳过此对象
}
}
else
{
// ═══════════════════════════════════════════════════════════════
// 无快速通道:所有对象平等对待
// ═══════════════════════════════════════════════════════════════
for (const uint32 ObjectIndex : MakeArrayView(Params.ObjectIndices, Params.ObjectCount))
{
const FObjectInfo& Info = static_cast<const FObjectInfo&>(
Params.PrioritizationInfos[ObjectIndex]);
// 只处理本帧轮询列表中的对象
if (!RoundRobinState.InternalObjectIndices.GetBit(Info.GetPrioritizerInternalIndex()))
{
continue;
}
// 拥有的对象获得更高优先级,但仍计入配额
const float Priority = (Info.GetOwningConnection() == Params.ConnectionId
? OwningConnectionPriority
: StandardPriority);
Params.Priorities[ObjectIndex] = Priority;
// 达到配额后停止
if (++ConsiderCount == MaxConsiderCount)
{
break;
}
}
}
}📥 Fill 模式(填充 + 饥饿预防)
"最久没吃饭的先吃!" 🍽️
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📥 Fill 模式饥饿预防机制 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 核心思想:记录每个对象"上次被考虑"的帧号 │
│ │
│ 对象 上次考虑帧 当前帧=100 饥饿度 │
│ ─────────────────────────────────────────────────────────────│
│ 🪙A 帧 98 100-98=2 低(刚吃过) │
│ 🪙B 帧 50 100-50=50 高(饿了50帧!) │
│ 🪙C 帧 95 100-95=5 中等 │
│ 🪙D 帧 30 100-30=70 很高(饿了70帧!) │
│ │
│ 排序后(饥饿度高的优先): │
│ [🪙D:70] [🪙B:50] [🪙C:5] [🪙A:2] │
│ │
│ MaxObjectCount=2 → 选择 🪙D 和 🪙B │
│ │
│ 📌 效果:没有对象会被永久"饿死"! │
│ │
│ 📌 注意:帧号回绕也能正常工作! │
│ uint32 溢出后,差值计算仍然正确(无符号算术) │
│ │
└────────────────────────────────────────────────────────────────┘完整的 PrioritizeForFill 实现:
CPP
// 📍 源文件:NetObjectCountLimiter.cpp
void UNetObjectCountLimiter::PrioritizeForFill(FNetObjectPrioritizationParams& Params){
IRIS_PROFILER_SCOPE(UNetObjectCountLimiter_PrioritizeForFill);
/*
* ⚠️ 警告:如果同一帧同一连接多次进入此函数,说明对象数量太多被分批处理了。
* Fill 模式要求所有脏对象在同一批次中,以便正确判断哪些对象最久未被复制。
* 如果触发此警告,需要增加优先级系统的批大小,或改用 RoundRobin 模式。
* 当此警告发生时,每个批次可能都会优先级化 N 个对象。
*/
ensureMsgf(FillState.LastPrioFrame != PrioFrame || FillState.LastConnectionId != Params.ConnectionId,
TEXT("UNetObjectCountLimiter::PrioritizeForFill. Too many objects are being prioritized"));
FillState.LastPrioFrame = PrioFrame;
FillState.LastConnectionId = Params.ConnectionId;
const uint32 ConnectionId = Params.ConnectionId;
FPerConnectionInfo& ConnectionInfo = PerConnectionInfos[ConnectionId];
// 确保有足够空间存储所有对象的帧计数
if (static_cast<uint32>(ConnectionInfo.LastConsiderFrames.Num()) < InternalObjectIndices.GetNumBits())
{
/*
* 新对象的 LastConsiderFrame 初始化为 0 是可以的。
* 新对象无论如何都会被复制用于创建,如果它们被此优先级考虑,
* 我们不会浪费带宽添加更多对象。
*/
ConnectionInfo.LastConsiderFrames.SetNum(InternalObjectIndices.GetNumBits());
}
uint32* LastConsideredFrames = ConnectionInfo.LastConsiderFrames.GetData();
// ═══════════════════════════════════════════════════════════════
// 📊 排序信息结构
// ═══════════════════════════════════════════════════════════════
struct FSortInfo
{
uint32 ObjectIndex; // 全局对象索引
uint32 InternalIndex; // 优先级内部索引
uint32 FrameCountSinceConsidered; // 🍽️ 饥饿度:距上次被考虑的帧数
bool bIsOwnedByConnection; // 是否被当前连接拥有
};
// 使用栈分配(最多约 16KB,可接受)
FSortInfo* SortInfosAlloc = static_cast<FSortInfo*>(
FMemory_Alloca(sizeof(FSortInfo) * Params.ObjectCount));
TArrayView<FSortInfo> SortInfos = MakeArrayView(SortInfosAlloc, Params.ObjectCount);
// ═══════════════════════════════════════════════════════════════
// 准备排序数据
// ═══════════════════════════════════════════════════════════════
{
FSortInfo* SortInfoIter = SortInfosAlloc;
for (const uint32 ObjectIndex : MakeArrayView(Params.ObjectIndices, Params.ObjectCount))
{
const FObjectInfo& ObjectInfo = static_cast<const FObjectInfo&>(
Params.PrioritizationInfos[ObjectIndex]);
const uint32 PrioritizerInternalIndex = ObjectInfo.GetPrioritizerInternalIndex();
FSortInfo& SortInfo = *SortInfoIter++;
SortInfo.ObjectIndex = ObjectIndex;
SortInfo.InternalIndex = PrioritizerInternalIndex;
// 🔑 饥饿度计算:当前帧 - 上次被考虑的帧
// 即使帧号溢出回绕,无符号减法仍然正确
SortInfo.FrameCountSinceConsidered = PrioFrame - LastConsideredFrames[PrioritizerInternalIndex];
SortInfo.bIsOwnedByConnection = (ObjectInfo.GetOwningConnection() == ConnectionId);
}
}
// ═══════════════════════════════════════════════════════════════
// 排序并设置优先级
// ═══════════════════════════════════════════════════════════════
{
const float StandardPriority = Config->Priority;
const float OwningConnectionPriority = Config->OwningConnectionPriority;
const uint32 MaxConsiderCount = Config->MaxObjectCount;
uint32 ConsiderCount = 0U;
if (Config->bEnableOwnedObjectsFastLane)
{
// 🚀 快速通道模式:拥有的对象优先,然后按饥饿度排序
auto ByOwnerAndLeastConsidered = [](const FSortInfo& A, const FSortInfo& B)
{
// 1. 拥有的对象优先
if (A.bIsOwnedByConnection != B.bIsOwnedByConnection)
{
return A.bIsOwnedByConnection;
}
// 2. 饥饿度高的优先(更久没被考虑)
if (A.FrameCountSinceConsidered != B.FrameCountSinceConsidered)
{
return A.FrameCountSinceConsidered > B.FrameCountSinceConsidered;
}
// 3. 平局打破器:使用内部索引保证稳定排序
return (A.InternalIndex < B.InternalIndex);
};
Algo::Sort(SortInfos, ByOwnerAndLeastConsidered);
for (const FSortInfo& Info : SortInfos)
{
// 更新最后考虑帧(重置饥饿计数器)
LastConsideredFrames[Info.InternalIndex] = PrioFrame;
const float Priority = (Info.bIsOwnedByConnection
? OwningConnectionPriority
: StandardPriority);
Params.Priorities[Info.ObjectIndex] = Priority;
// 排序保证拥有的对象在前面,所以达到配额时可以直接停止
// 拥有的对象不计入配额
ConsiderCount += !Info.bIsOwnedByConnection;
if (ConsiderCount == MaxConsiderCount)
{
break;
}
}
}
else
{
// 无快速通道:只按饥饿度排序
auto ByLeastConsidered = [](const FSortInfo& A, const FSortInfo& B)
{
if (A.FrameCountSinceConsidered != B.FrameCountSinceConsidered)
{
return A.FrameCountSinceConsidered > B.FrameCountSinceConsidered;
}
return (A.InternalIndex < B.InternalIndex);
};
Algo::Sort(SortInfos, ByLeastConsidered);
for (const FSortInfo& Info : SortInfos)
{
LastConsideredFrames[Info.InternalIndex] = PrioFrame;
const float Priority = (Info.bIsOwnedByConnection
? OwningConnectionPriority
: StandardPriority);
Params.Priorities[Info.ObjectIndex] = Priority;
if (++ConsiderCount == MaxConsiderCount)
{
break;
}
}
}
}
}📊 两种模式对比
特性 | RoundRobin | Fill |
|---|---|---|
对象选择 | 固定轮询顺序 | 最久未复制的优先 |
饥饿预防 | 隐式(轮询保证) | 显式(帧计数排序) |
内存开销 | 低(位数组) | 高(每连接每对象帧计数) |
计算复杂度 | O(N) | O(N log N)(排序) |
适用场景 | 对象修改频率均匀 | 对象修改频率差异大 |
公平性 | 绝对公平 | 按需公平(饿的先吃) |
🚀 快速通道(OwnedObjectsFastLane)
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🚀 快速通道机制 │
├────────────────────────────────────────────────────────────────┤
│ │
│ bEnableOwnedObjectsFastLane = true 时: │
│ │
│ 玩家A的连接: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 🚀 快速通道(不受 MaxObjectCount 限制) │ │
│ │ • 玩家A拥有的宠物🐕 │ │
│ │ • 玩家A拥有的武器🗡️ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ 📦 普通通道(受 MaxObjectCount 限制) │ │
│ │ • 其他玩家的对象 │ │
│ │ • 世界中的金币🪙 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 效果:自己的东西永远不会被"饿死"! │
│ │
└────────────────────────────────────────────────────────────────┘👁️ 6.4 ReplicationView(复制视图)
ReplicationView 是优先级系统的"眼睛"👁️,它告诉优先级:玩家在哪里、看向哪里、视野多大。
📋 视图定义
CPP
// 📍 源文件:Core/Public/Iris/ReplicationSystem/ReplicationView.h
struct FReplicationView
{
struct FView
{
FNetHandle Controller; // 🎮 控制器(通常是 PlayerController)
FNetHandle ViewTarget; // 🎯 视图目标(通常是 Pawn)
FVector Pos = FVector::ZeroVector; // 👁️ 观察者位置
FVector Dir = FVector::ForwardVector; // 🎯 观察方向
float FoVRadians = UE_HALF_PI; // 📐 视野角度(弧度,默认90°)
};
// 支持分屏,默认内联4个视图
TArray<FView, TInlineAllocator<UE_IRIS_INLINE_VIEWS_PER_CONNECTION>> Views;
};🔄 视图更新流程
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🔄 ReplicationView 更新流程 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 游戏代码 │
│ │ │
│ ▼ │
│ PlayerController::GetPlayerViewPoint() │
│ │ │
│ ▼ 获取位置和方向 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ FReplicationView View; │ │
│ │ FReplicationView::FView& V = View.Views.AddDefaulted_GetRef();│
│ │ V.Controller = ControllerHandle; │ │
│ │ V.ViewTarget = PawnHandle; │ │
│ │ V.Pos = ViewLocation; │ │
│ │ V.Dir = ViewRotation.Vector(); │ │
│ │ V.FoVRadians = FMath::DegreesToRadians(FOV); │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ReplicationSystem->SetReplicationView(ConnectionId, View); │
│ │ │
│ ▼ 存储到连接信息中 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ // ReplicationConnections.cpp │ │
│ │ void SetReplicationView(uint32 ConnectionId, │ │
│ │ const FReplicationView& View) │ │
│ │ { │ │
│ │ ReplicationViews[ConnectionId] = View; │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ 优先级计算时使用 │
│ Prioritizer->Prioritize(Params); // Params.View = 存储的视图│
│ │
└────────────────────────────────────────────────────────────────┘🎮 与 PlayerController 的关联
CPP
// 典型的游戏代码中设置视图void AMyGameMode::UpdateReplicationViews(){
for (APlayerController* PC : GetWorld()->GetPlayerControllerIterator())
{
if (!PC->IsLocalController()) // 只为远程玩家设置
{
FReplicationView View;
FReplicationView::FView& V = View.Views.AddDefaulted_GetRef();
// 🎮 设置控制器
V.Controller = GetNetHandle(PC);
// 🎯 设置视图目标(通常是 Pawn)
if (APawn* Pawn = PC->GetPawn())
{
V.ViewTarget = GetNetHandle(Pawn);
}
// 👁️ 获取视图位置和方向
FVector ViewLocation;
FRotator ViewRotation;
PC->GetPlayerViewPoint(ViewLocation, ViewRotation);
V.Pos = ViewLocation;
V.Dir = ViewRotation.Vector();
// 📐 设置视野角度
if (APlayerCameraManager* CamMgr = PC->PlayerCameraManager)
{
V.FoVRadians = FMath::DegreesToRadians(CamMgr->GetFOVAngle());
}
// 📤 提交到复制系统
ReplicationSystem->SetReplicationView(PC->GetConnectionId(), View);
}
}
}🎮 多视图支持(分屏游戏)
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🎮 分屏游戏多视图 │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┬─────────────────┐ │
│ │ 玩家A视角 │ 玩家B视角 │ │
│ │ 👤A │ 👤B │ │
│ └─────────────────┴─────────────────┘ │
│ │
│ 同一个连接,两个视图! │
│ Views[0]: PlayerA 的位置和方向 │
│ Views[1]: PlayerB 的位置和方向 │
│ │
│ 优先级计算:取两个视图中的最高优先级 │
│ 例如:对象在A附近(0.8),离B很远(0.1) → 最终 = max(0.8,0.1) │
│ │
└────────────────────────────────────────────────────────────────┘CPP
// 分屏游戏的视图设置void SetupSplitScreenViews(uint32 ConnectionId,
APlayerController* PC1,
APlayerController* PC2){
FReplicationView View;
// 第一个玩家的视图
{
FReplicationView::FView& V = View.Views.AddDefaulted_GetRef();
V.Controller = GetNetHandle(PC1);
V.ViewTarget = GetNetHandle(PC1->GetPawn());
PC1->GetPlayerViewPoint(V.Pos, V.Dir);
}
// 第二个玩家的视图
{
FReplicationView::FView& V = View.Views.AddDefaulted_GetRef();
V.Controller = GetNetHandle(PC2);
V.ViewTarget = GetNetHandle(PC2->GetPawn());
PC2->GetPlayerViewPoint(V.Pos, V.Dir);
}
ReplicationSystem->SetReplicationView(ConnectionId, View);
}👑 视图目标特殊处理
视图目标(Controller 和 ViewTarget)会获得超高优先级,确保玩家自己的角色永远不会延迟同步:
CPP
// 📍 源文件:ReplicationPrioritization.cpp
void FReplicationPrioritization::SetHighPriorityOnViewTargets(
const TArrayView<float>& Priorities,
const FReplicationView& ReplicationView){
// 收集所有视图目标
TArray<FNetHandle, TInlineAllocator<16>> ViewTargets;
for (const FReplicationView::FView& View : ReplicationView.Views)
{
if (View.Controller.IsValid())
{
ViewTargets.Add(View.Controller); // 🎮 控制器
}
if (View.ViewTarget != View.Controller && View.ViewTarget.IsValid())
{
ViewTargets.Add(View.ViewTarget); // 🎯 Pawn
}
}
// 为视图目标设置超高优先级
for (FNetHandle NetHandle : ViewTargets)
{
const FInternalNetRefIndex ViewTargetInternalIndex =
NetRefHandleManager->GetInternalIndexFromNetHandle(NetHandle);
if (ViewTargetInternalIndex != FNetRefHandleManager::InvalidInternalIndex)
{
// 🔥 一千万!绝对最高优先级
Priorities[ViewTargetInternalIndex] = ViewTargetHighPriority; // 1.0E7f
}
}
}⚙️ 6.5 优先级配置
📝 配置文件示例
INI
[/Script/IrisCore.NetObjectPrioritizerDefinitions]; 第一个定义成为默认空间优先级
+NetObjectPrioritizerDefinitions=(PrioritizerName="SphereNetObjectPrioritizer",ClassName="/Script/IrisCore.SphereNetObjectPrioritizer")
[/Script/IrisCore.SphereNetObjectPrioritizerConfig]InnerRadius=3000.0OuterRadius=15000.0InnerPriority=1.0OuterPriority=0.2OutsidePriority=0.1
[/Script/IrisCore.ObjectReplicationBridgeConfig]; 为特定类指定优先级
+PrioritizerConfigs=(ClassName="/Script/MyGame.MyPlayerCharacter",PrioritizerName="FieldOfViewNetObjectPrioritizer")
+PrioritizerConfigs=(ClassName="/Script/MyGame.GoldCoin",PrioritizerName="NetObjectCountLimiter")🔧 6.6 优先级系统内部实现
📊 FReplicationPrioritization 核心数据结构
CPP
// 📍 源文件:ReplicationPrioritization.h
class FReplicationPrioritization
{
private:
// 🔢 优先级常量
static constexpr float DefaultPriority = 1.0f;
static constexpr float ViewTargetHighPriority = 1.0E7f; // 视图目标的超高优先级
// 📦 每连接信息
struct FPerConnectionInfo
{
TArray<float> Priorities; // 每个对象的累积优先级
uint32 NextObjectIndexToProcess; // 下一个要处理的对象索引
uint32 IsValid : 1; // 连接是否有效
};
// 📊 核心数据
TArray<FNetObjectPrioritizationInfo> NetObjectPrioritizationInfos; // 对象优先级信息
TArray<uint8> ObjectIndexToPrioritizer; // 对象 → 优先级映射
TArray<FPrioritizerInfo> PrioritizerInfos; // 优先级信息列表
TArray<FPerConnectionInfo> ConnectionInfos; // 每连接数据
TArray<float> DefaultPriorities; // 默认优先级数组
};🧩 TChunkedArrayWithChunkManagement 分块数组
Iris 使用自定义的分块数组来高效管理批处理数据:
CPP
// 📍 源文件:ReplicationPrioritization.cpp
/**
* 带块管理的分块数组
* 大部分逻辑都围绕批处理展开,因此需要访问块
*/template<class InElementType, uint32 BytesPerChunk>
class TChunkedArrayWithChunkManagement : public ::TChunkedArray<InElementType, BytesPerChunk>
{
private:
using Super = ::TChunkedArray<InElementType, BytesPerChunk>;
public:
/** 移除第一个块及其所有元素(如果存在) */
void PopChunkSafe()
{
if (Super::NumElements > 0)
{
Super::NumElements -= FPlatformMath::Min(
Super::NumElements, static_cast<int32>(Super::NumElementsPerChunk));
constexpr int32 Index = 0;
constexpr int32 Count = 1;
Super::Chunks.RemoveAt(Index, Count, EAllowShrinking::No);
}
}
/** 在数组末尾构造新元素,返回引用 */
InElementType& Emplace_GetRef()
{
if ((static_cast<uint32>(Super::NumElements) % static_cast<uint32>(Super::NumElementsPerChunk)) == 0U)
{
// 需要新块
++Super::NumElements;
typename Super::FChunk* Chunk = new typename Super::FChunk;
Super::Chunks.Add(Chunk);
return Chunk->Elements[0];
}
else
{
return this->operator[](Super::NumElements++);
}
}
/** 返回第一个块中的元素数量 */
int32 GetFirstChunkNum() const
{
return FPlatformMath::Min(Super::NumElements, static_cast<int32>(Super::NumElementsPerChunk));
}
/** 返回第一个块中第一个元素的指针 */
const InElementType* GetFirstChunkData() const
{
if (typename Super::FChunk const** ChunkPtr = Super::Chunks.GetData())
{
return &(*ChunkPtr)->Elements[0];
}
return nullptr;
}
/** 将元素数量设为零但保留分配 */
void Reset()
{
Super::Chunks.Reset(0);
Super::NumElements = 0;
}
};分块数组设计优势:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ 📦 分块数组设计优势 │├────────────────────────────────────────────────────────────────┤│ ││ 传统数组问题: ││ • 大量对象时需要连续大内存 ││ • 扩容时需要复制所有数据 ││ • 内存碎片化严重 ││ ││ 分块数组优势: ││ • 每块 1024 个对象(4KB),适合缓存行 ││ • 扩容只需分配新块,无需复制 ││ • 处理完一块可立即释放(PopChunkSafe) ││ • 内存使用更可预测 ││ ││ 批处理流程: ││ ┌────────┐ ┌────────┐ ┌────────┐ ││ │ Chunk1 │→│ Chunk2 │→│ Chunk3 │ ││ │ 1024个 │ │ 1024个 │ │ 512个 │ ││ └────────┘ └────────┘ └────────┘ ││ ↓ ↓ ↓ ││ 处理后释放 处理后释放 处理后释放 ││ │└────────────────────────────────────────────────────────────────┘🔄 完整的 Prioritize 方法
CPP
// 📍 源文件:ReplicationPrioritization.cpp
/**
* 优先级计算有多种策略,各有不同的性能特点:
*
* 策略1:优先级化本帧所有脏对象 + 每连接因丢包需要的对象
* - 丢包应该很少,丢失状态的对象列表应该很小
* - 适合脏对象数量相对较少的情况
* - 可以节省大量优先级数据设置时间
*
* 策略2:从每个连接获取需要优先级化的对象列表
* - 适合连接多且作用域对象集合差异大的情况
* - 例如:空间过滤后玩家相距很远
*
* 性能优化思路:
* - 世界生成或延迟加入时,可能有大量对象需要复制
* - 可以每帧只考虑一定数量的对象(如 200 个)
* - 下一帧从上次的位置继续
* - 连接会保持对象的最后优先级值直到计算新值
*/void FReplicationPrioritization::Prioritize(
const FNetBitArrayView& ReplicatingConnections,
const FNetBitArrayView& DirtyObjectsThisFrame){
IRIS_PROFILER_SCOPE(FReplicationPrioritization_Prioritize);
// ═══════════════════════════════════════════════════════════════
// 阶段1:更新新增/删除对象的优先级
// ═══════════════════════════════════════════════════════════════
UpdatePrioritiesForNewAndDeletedObjects();
// ═══════════════════════════════════════════════════════════════
// 阶段2:通知优先级有脏对象(用于位置更新等)
// ═══════════════════════════════════════════════════════════════
NotifyPrioritizersOfDirtyObjects(DirtyObjectsThisFrame);
if (!ReplicatingConnections.IsAnyBitSet())
return;
// ═══════════════════════════════════════════════════════════════
// 阶段3:PrePrioritize - 给优先级准备的机会
// 只有当 Prioritize() 可能被调用时才调用
// ═══════════════════════════════════════════════════════════════
{
FNetObjectPrePrioritizationParams PrePrioParams;
for (FPrioritizerInfo& Info : PrioritizerInfos)
{
if (Info.ObjectCount == 0U) continue;
Info.Prioritizer->PrePrioritize(PrePrioParams);
}
}
// ═══════════════════════════════════════════════════════════════
// 阶段4:为每个连接执行优先级计算
// ═══════════════════════════════════════════════════════════════
// 使用栈分配连接ID数组
uint32* ConnectionIds = static_cast<uint32*>(
FMemory_Alloca(ReplicatingConnections.GetNumBits() * 4));
uint32 ReplicatingConnectionCount = 0;
ReplicatingConnections.ForAllSetBits([ConnectionIds, &ReplicatingConnectionCount](uint32 Bit)
{
ConnectionIds[ReplicatingConnectionCount++] = Bit;
});
FPrioritizerBatchHelper BatchHelper(PrioritizerInfos.Num());
for (const uint32 ConnId : MakeArrayView(ConnectionIds, ReplicatingConnectionCount))
{
// 🔑 如果没有视图,不进行优先级计算
if (Connections->GetReplicationView(ConnId).Views.Num() <= 0)
continue;
FReplicationConnection* Connection = Connections->GetConnection(ConnId);
FReplicationWriter* ReplicationWriter = Connection->ReplicationWriter;
// 从 ReplicationWriter 获取需要优先级更新的对象
const FNetBitArray& Objects = ReplicationWriter->GetObjectsRequiringPriorityUpdate();
if (Objects.GetNumBits() == 0) continue;
// 执行优先级计算
PrioritizeForConnection(ConnId, BatchHelper, MakeNetBitArrayView(Objects));
// 🔄 将更新后的优先级传回 ReplicationWriter
// 当前假设优先级是每对象每连接持久存储的
{
FPerConnectionInfo& ConnInfo = ConnectionInfos[ConnId];
const float* Priorities = ConnInfo.Priorities.GetData();
ReplicationWriter->UpdatePriorities(Priorities);
}
}
// ═══════════════════════════════════════════════════════════════
// 阶段5:PostPrioritize - 给优先级清理的机会
// 如果 PrePrioritize() 被调用,则调用此方法
// ═══════════════════════════════════════════════════════════════
{
FNetObjectPostPrioritizationParams PostPrioParams;
for (FPrioritizerInfo& Info : PrioritizerInfos)
{
if (Info.ObjectCount == 0U) continue;
Info.Prioritizer->PostPrioritize(PostPrioParams);
}
}
}🎯 PrioritizeForConnection 完整实现
这是优先级计算的核心方法,负责将对象按优先级分组并批量处理:
CPP
// 📍 源文件:ReplicationPrioritization.cpp
/**
* 批处理辅助类
* 将对象按优先级分组,支持分块处理以限制内存使用
*/class FReplicationPrioritization::FPrioritizerBatchHelper
{
public:
enum EConstants : unsigned
{
MaxObjectCountPerBatch = 1024U, // 每批最多 1024 个对象
};
enum EBatchProcessStatus : unsigned
{
ProcessFullBatchesAndContinue, // 有满批次,处理后继续
ProcessAllBatchesAndStop, // 处理所有批次并停止
NothingToProcess, // 无需处理
};
struct FPerPrioritizerInfo
{
// 使用分块数组,每块 1024 个对象索引
TChunkedArrayWithChunkManagement<uint32, MaxObjectCountPerBatch*sizeof(uint32)> ObjectIndices;
};
TArray<FPerPrioritizerInfo, TInlineAllocator<16>> PerPrioritizerInfos;
/**
* 准备批次:将对象按优先级分组
* @return 处理状态
*/
EBatchProcessStatus PrepareBatch(FPerConnectionInfo& ConnInfo,
const FNetBitArrayView Objects,
const uint8* PrioritizerIndices,
const float* InDefaultPriorities)
{
float* ConnPriorities = ConnInfo.Priorities.GetData();
uint32 ObjectIndices[MaxObjectCountPerBatch];
// 获取最多 1024 个脏对象
for (uint32 ObjectCount = 0;
(ObjectCount = Objects.GetSetBitIndices(BatchInfo.CurrentObjectIndex, ~0U,
ObjectIndices, MaxObjectCountPerBatch)) > 0; )
{
// 更新下次开始位置
if (ObjectCount < MaxObjectCountPerBatch)
{
BatchInfo.CurrentObjectIndex = Objects.GetNumBits();
}
else
{
BatchInfo.CurrentObjectIndex = ObjectIndices[ObjectCount - 1] + 1U;
}
// 将对象按优先级分组
for (const uint32 ObjectIndex : MakeArrayView(ObjectIndices, ObjectCount))
{
const uint8 PrioritizerIndex = PrioritizerIndices[ObjectIndex];
if (PrioritizerIndex == InvalidNetObjectPrioritizerIndex)
{
continue; // 静态优先级对象,跳过
}
FPerPrioritizerInfo& PerPrioritizerInfo = PerPrioritizerInfos[PrioritizerIndex];
PerPrioritizerInfo.ObjectIndices.Emplace_GetRef() = ObjectIndex;
// 重置优先级为默认值(优先级会覆盖)
ConnPriorities[ObjectIndex] = InDefaultPriorities[ObjectIndex];
}
// 判断返回条件
if (ObjectCount < MaxObjectCountPerBatch)
{
return EBatchProcessStatus::ProcessAllBatchesAndStop;
}
// 检查是否有优先级达到满批次
for (const FPerPrioritizerInfo& Info : PerPrioritizerInfos)
{
if (Info.ObjectIndices.Num() >= MaxObjectCountPerBatch)
{
return EBatchProcessStatus::ProcessFullBatchesAndContinue;
}
}
}
return EBatchProcessStatus::ProcessAllBatchesAndStop;
}
};
/**
* 为单个连接计算所有对象的优先级
*/void FReplicationPrioritization::PrioritizeForConnection(
uint32 ConnId,
FPrioritizerBatchHelper& BatchHelper,
const FNetBitArrayView Objects){
IRIS_PROFILER_SCOPE(FReplicationPrioritization_PrioritizeForConnection);
FPerConnectionInfo& ConnInfo = ConnectionInfos[ConnId];
// 设置优先级计算参数
FNetObjectPrioritizationParams PrioParameters;
PrioParameters.Priorities = ConnInfo.Priorities.GetData();
PrioParameters.PrioritizationInfos = NetObjectPrioritizationInfos.GetData();
PrioParameters.ConnectionId = ConnId;
PrioParameters.View = Connections->GetReplicationView(ConnId);
// 初始化批处理辅助器
BatchHelper.InitForConnection();
while (true)
{
// 准备批次:按优先级分组对象
const auto ProcessStatus = BatchHelper.PrepareBatch(
ConnInfo, Objects, ObjectIndexToPrioritizer.GetData(), DefaultPriorities.GetData());
if (ProcessStatus == FPrioritizerBatchHelper::ProcessAllBatchesAndStop)
{
// ═══════════════════════════════════════════════════════════════
// 处理所有批次(最后一批或唯一一批)
// ═══════════════════════════════════════════════════════════════
for (auto& PerPrioritizerInfo : BatchHelper.PerPrioritizerInfos)
{
// 处理该优先级的所有块
for (int32 ObjectCount = PerPrioritizerInfo.ObjectIndices.Num();
ObjectCount > 0;
ObjectCount = PerPrioritizerInfo.ObjectIndices.Num())
{
// 设置本批次的对象列表
PrioParameters.ObjectIndices = PerPrioritizerInfo.ObjectIndices.GetFirstChunkData();
PrioParameters.ObjectCount = PerPrioritizerInfo.ObjectIndices.GetFirstChunkNum();
// 获取对应的优先级并调用
const int32 PrioritizerIndex = static_cast<int32>(
&PerPrioritizerInfo - BatchHelper.PerPrioritizerInfos.GetData());
UNetObjectPrioritizer* Prioritizer = PrioritizerInfos[PrioritizerIndex].Prioritizer.Get();
// 🎯 调用优先级计算优先级
Prioritizer->Prioritize(PrioParameters);
// 移除已处理的块
PerPrioritizerInfo.ObjectIndices.PopChunkSafe();
}
}
break;
}
else if (ProcessStatus == FPrioritizerBatchHelper::ProcessFullBatchesAndContinue)
{
// ═══════════════════════════════════════════════════════════════
// 处理满批次,然后继续
// ═══════════════════════════════════════════════════════════════
for (auto& PerPrioritizerInfo : BatchHelper.PerPrioritizerInfos)
{
// 只处理达到满批次的优先级
if (PerPrioritizerInfo.ObjectIndices.Num() < FPrioritizerBatchHelper::MaxObjectCountPerBatch)
{
continue;
}
PrioParameters.ObjectIndices = PerPrioritizerInfo.ObjectIndices.GetFirstChunkData();
PrioParameters.ObjectCount = PerPrioritizerInfo.ObjectIndices.GetFirstChunkNum();
const int32 PrioritizerIndex = static_cast<int32>(
&PerPrioritizerInfo - BatchHelper.PerPrioritizerInfos.GetData());
UNetObjectPrioritizer* Prioritizer = PrioritizerInfos[PrioritizerIndex].Prioritizer.Get();
Prioritizer->Prioritize(PrioParameters);
PerPrioritizerInfo.ObjectIndices.PopChunkSafe();
}
continue;
}
checkf(false, TEXT("Unexpected BatchProcessStatus %u"), ProcessStatus);
break;
}
// 可选:为视图目标设置超高优先级
if (CVar_ForceConnectionViewerPriority > 0)
{
SetHighPriorityOnViewTargets(MakeArrayView(ConnInfo.Priorities), PrioParameters.View);
}
}🔔 NotifyPrioritizersOfDirtyObjects 完整实现
这个方法负责通知优先级哪些对象在本帧发生了变化,以便更新位置等信息:
CPP
// 📍 源文件:ReplicationPrioritization.cpp
/**
* 脏对象更新批处理辅助类
* 负责将脏对象按优先级分组,并准备 InstanceProtocol 数据
*/class FReplicationPrioritization::FUpdateDirtyObjectsBatchHelper
{
public:
enum Constants : uint32
{
MaxObjectCountPerBatch = 512U, // 每批最多 512 个对象
};
struct FPerPrioritizerInfo
{
uint32* ObjectIndices; // 对象索引数组
uint32 ObjectCount; // 对象数量
FReplicationInstanceProtocol const** InstanceProtocols; // 实例协议数组
};
FUpdateDirtyObjectsBatchHelper(const FNetRefHandleManager* InNetRefHandleManager, uint32 PrioritizerCount)
: NetRefHandleManager(InNetRefHandleManager)
{
PerPrioritizerInfos.SetNumUninitialized(PrioritizerCount);
// 预分配存储空间:每个优先级最多 512 个对象
ObjectIndicesStorage.SetNumUninitialized(PrioritizerCount * MaxObjectCountPerBatch);
InstanceProtocolsStorage.SetNumUninitialized(PrioritizerCount * MaxObjectCountPerBatch);
// 为每个优先级分配存储区域
uint32 PrioritizerIndex = 0;
for (FPerPrioritizerInfo& PerPrioritizerInfo : PerPrioritizerInfos)
{
PerPrioritizerInfo.ObjectIndices = ObjectIndicesStorage.GetData() +
PrioritizerIndex * MaxObjectCountPerBatch;
PerPrioritizerInfo.InstanceProtocols = InstanceProtocolsStorage.GetData() +
PrioritizerIndex * MaxObjectCountPerBatch;
++PrioritizerIndex;
}
}
void PrepareBatch(const uint32* ObjectIndices, uint32 ObjectCount, const uint8* PrioritizerIndices)
{
ResetBatch();
FPerPrioritizerInfo* PerPrioritizerInfosData = PerPrioritizerInfos.GetData();
for (const uint32 ObjectIndex : MakeArrayView(ObjectIndices, ObjectCount))
{
const uint8 PrioritizerIndex = PrioritizerIndices[ObjectIndex];
if (PrioritizerIndex == FReplicationPrioritization_InvalidNetObjectPrioritizerIndex)
{
continue; // 静态优先级对象,跳过
}
// 获取对象的 InstanceProtocol
if (const FReplicationInstanceProtocol* InstanceProtocol =
NetRefHandleManager->GetReplicatedObjectDataNoCheck(ObjectIndex).InstanceProtocol)
{
FPerPrioritizerInfo& PerPrioritizerInfo = PerPrioritizerInfosData[PrioritizerIndex];
PerPrioritizerInfo.ObjectIndices[PerPrioritizerInfo.ObjectCount] = ObjectIndex;
PerPrioritizerInfo.InstanceProtocols[PerPrioritizerInfo.ObjectCount] = InstanceProtocol;
++PerPrioritizerInfo.ObjectCount;
}
}
}
TArray<FPerPrioritizerInfo, TInlineAllocator<16>> PerPrioritizerInfos;
private:
void ResetBatch()
{
for (FPerPrioritizerInfo& PerPrioritizerInfo : PerPrioritizerInfos)
{
PerPrioritizerInfo.ObjectCount = 0U;
}
}
TArray<uint32> ObjectIndicesStorage;
TArray<const FReplicationInstanceProtocol*> InstanceProtocolsStorage;
const FNetRefHandleManager* NetRefHandleManager;
};
/**
* 通知优先级哪些对象在本帧发生了变化
* 优先级可以利用这个信息更新对象的位置等数据
*/void FReplicationPrioritization::NotifyPrioritizersOfDirtyObjects(const FNetBitArrayView& DirtyObjectsThisFrame){
IRIS_PROFILER_SCOPE(FReplicationPrioritization_NotifyPrioritizersOfDirtyObjects);
FUpdateDirtyObjectsBatchHelper BatchHelper(NetRefHandleManager, PrioritizerInfos.Num());
constexpr SIZE_T MaxBatchObjectCount = FUpdateDirtyObjectsBatchHelper::Constants::MaxObjectCountPerBatch;
uint32 ObjectIndices[MaxBatchObjectCount];
const uint32 BitCount = ~0U;
// 分批处理所有脏对象
for (uint32 ObjectCount, StartIndex = 0;
(ObjectCount = DirtyObjectsThisFrame.GetSetBitIndices(StartIndex, BitCount, ObjectIndices, MaxBatchObjectCount)) > 0; )
{
BatchNotifyPrioritizersOfDirtyObjects(BatchHelper, ObjectIndices, ObjectCount);
StartIndex = ObjectIndices[ObjectCount - 1] + 1U;
if ((StartIndex == DirtyObjectsThisFrame.GetNumBits()) | (ObjectCount < MaxBatchObjectCount))
{
break;
}
}
}
void FReplicationPrioritization::BatchNotifyPrioritizersOfDirtyObjects(
FUpdateDirtyObjectsBatchHelper& BatchHelper, uint32* ObjectIndices, uint32 ObjectCount){
BatchHelper.PrepareBatch(ObjectIndices, ObjectCount, ObjectIndexToPrioritizer.GetData());
FNetObjectPrioritizerUpdateParams UpdateParameters;
UpdateParameters.StateBuffers = &NetRefHandleManager->GetReplicatedObjectStateBuffers();
UpdateParameters.PrioritizationInfos = NetObjectPrioritizationInfos.GetData();
// 调用每个优先级的 UpdateObjects 方法
for (const FUpdateDirtyObjectsBatchHelper::FPerPrioritizerInfo& PerPrioritizerInfo : BatchHelper.PerPrioritizerInfos)
{
if (PerPrioritizerInfo.ObjectCount == 0)
{
continue;
}
UpdateParameters.ObjectIndices = PerPrioritizerInfo.ObjectIndices;
UpdateParameters.ObjectCount = PerPrioritizerInfo.ObjectCount;
UpdateParameters.InstanceProtocols = PerPrioritizerInfo.InstanceProtocols;
const int32 PrioritizerIndex = static_cast<int32>(
&PerPrioritizerInfo - BatchHelper.PerPrioritizerInfos.GetData());
UNetObjectPrioritizer* Prioritizer = PrioritizerInfos[PrioritizerIndex].Prioritizer.Get();
// 🔑 调用优先级更新对象(位置等信息)
Prioritizer->UpdateObjects(UpdateParameters);
}
}🆕 UpdatePrioritiesForNewAndDeletedObjects 完整实现
这个方法处理新增和删除对象的优先级更新:
CPP
// 📍 源文件:ReplicationPrioritization.cpp
/**
* 处理新增和删除对象的优先级
* - 新对象:将默认优先级复制到每个连接
* - 删除对象:重置优先级为默认值,从优先级中移除
*/void FReplicationPrioritization::UpdatePrioritiesForNewAndDeletedObjects(){
IRIS_PROFILER_SCOPE(FReplicationPrioritization_UpdatePrioritiesForNewAndDeletedObjects);
// 获取上一帧和当前帧的可作用域对象索引
const FNetBitArrayView PrevScopedIndices = NetRefHandleManager->GetPrevFrameScopableInternalIndices();
const FNetBitArrayView ScopedIndices = NetRefHandleManager->GetCurrentFrameScopableInternalIndices();
// ═══════════════════════════════════════════════════════════════
// 处理删除的对象
// ═══════════════════════════════════════════════════════════════
auto ForEachRemovedObject = [this](uint32 ObjectIndex)
{
uint8& Prioritizer = ObjectIndexToPrioritizer[ObjectIndex];
if (Prioritizer != FReplicationPrioritization_InvalidNetObjectPrioritizerIndex)
{
// 从优先级中移除对象
FPrioritizerInfo& OldPrioritizerInfo = PrioritizerInfos[Prioritizer];
--OldPrioritizerInfo.ObjectCount;
OldPrioritizerInfo.Prioritizer->RemoveObject(ObjectIndex, NetObjectPrioritizationInfos[ObjectIndex]);
}
// 重置为无效优先级和默认优先级
Prioritizer = FReplicationPrioritization_InvalidNetObjectPrioritizerIndex;
DefaultPriorities[ObjectIndex] = DefaultPriority;
};
// ═══════════════════════════════════════════════════════════════
// 收集新增的对象
// ═══════════════════════════════════════════════════════════════
TArray<uint32> NewIndices;
TFunction<void(uint32)> DoNothing = [](uint32 ObjectIndex){};
TFunction<void(uint32)> AddIndexAndClearFromNewPriority = [&NewIndices, this](uint32 ObjectIndex)
{
NewIndices.Add(ObjectIndex);
// 防止同一索引被添加两次
this->ObjectsWithNewStaticPriority.ClearBit(ObjectIndex);
};
TFunction<void(uint32)> ForEachNewObject = (ConnectionCount > 0 ? AddIndexAndClearFromNewPriority : DoNothing);
if (ConnectionCount > 0)
{
NewIndices.Reserve(FMath::Min(1024U, MaxInternalNetRefIndex));
}
// 🔑 比较两帧的位数组,找出新增和删除的对象
FNetBitArrayView::ForAllExclusiveBits(ScopedIndices, PrevScopedIndices, ForEachNewObject, ForEachRemovedObject);
// ═══════════════════════════════════════════════════════════════
// 处理静态优先级变化的对象
// ═══════════════════════════════════════════════════════════════
if (HasNewObjectsWithStaticPriority)
{
HasNewObjectsWithStaticPriority = 0;
if (ConnectionCount > 0)
{
ObjectsWithNewStaticPriority.ForAllSetBits([this, &NewIndices](uint32 ObjectIndex)
{
NewIndices.Add(ObjectIndex);
});
}
ObjectsWithNewStaticPriority.ClearAllBits();
}
// ═══════════════════════════════════════════════════════════════
// 将新对象的优先级复制到每个连接
// ═══════════════════════════════════════════════════════════════
if (NewIndices.Num() > 0 && ConnectionCount > 0)
{
const TArrayView<uint32> ObjectIndices = MakeArrayView(NewIndices);
for (FPerConnectionInfo& ConnectionInfo : ConnectionInfos)
{
if (!ConnectionInfo.IsValid)
{
continue;
}
for (const uint32 ObjectIndex : ObjectIndices)
{
ConnectionInfo.Priorities[ObjectIndex] = DefaultPriorities[ObjectIndex];
}
}
}
}🏭 InitPrioritizers 完整实现
这个方法负责初始化所有配置的优先级:
CPP
// 📍 源文件:ReplicationPrioritization.cpp
void FReplicationPrioritization::InitPrioritizers(){
/**
* $IRIS TODO: 需要确定热修复支持的类型。
* 不同的方式有不同的权衡,取决于如何设置对象的优先级
* 以及用户是否缓存优先级句柄。
*/
// 加载优先级定义
PrioritizerDefinitions = TStrongObjectPtr<UNetObjectPrioritizerDefinitions>(
NewObject<UNetObjectPrioritizerDefinitions>());
TArray<FNetObjectPrioritizerDefinition> Definitions;
PrioritizerDefinitions->GetValidDefinitions(Definitions);
// 🔑 限制:每个对象存储 uint8 索引,最多支持 256 个优先级
check(Definitions.Num() <= 256);
PrioritizerInfos.Reserve(Definitions.Num());
for (FNetObjectPrioritizerDefinition& Definition : Definitions)
{
// 创建优先级实例
TStrongObjectPtr<UNetObjectPrioritizer> Prioritizer(
NewObject<UNetObjectPrioritizer>(
(UObject*)GetTransientPackage(),
Definition.Class,
MakeUniqueObjectName(nullptr, Definition.Class, Definition.PrioritizerName)));
// 准备初始化参数
FNetObjectPrioritizerInitParams InitParams;
InitParams.ReplicationSystem = ReplicationSystem;
InitParams.Config = (Definition.ConfigClass != nullptr
? NewObject<UNetObjectPrioritizerConfig>((UObject*)GetTransientPackage(), Definition.ConfigClass)
: nullptr);
InitParams.AbsoluteMaxNetObjectCount = NetRefHandleManager->GetMaxActiveObjectCount();
InitParams.CurrentMaxInternalIndex = MaxInternalNetRefIndex;
InitParams.MaxConnectionCount = Connections->GetMaxConnectionCount();
// 初始化优先级
Prioritizer->Init(InitParams);
// 注册到优先级列表
FPrioritizerInfo& Info = PrioritizerInfos.Emplace_GetRef();
Info.Prioritizer = Prioritizer;
Info.Name = Definition.PrioritizerName;
Info.ObjectCount = 0;
}
#if UE_GAME || UE_SERVER
UE_CLOG(PrioritizerInfos.Num() == 0, LogIris, Warning, TEXT("%s"),
TEXT("No prioritizers have been registered. This may result in a bad gameplay experience "
"because nearby actors will not have higher priority than actors far away."));
#endif
}批处理流程图解:
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐│ 🔄 PrioritizeForConnection 批处理流程 │├────────────────────────────────────────────────────────────────┤│ ││ 输入:3000 个需要优先级更新的对象 ││ │ ││ ▼ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ PrepareBatch #1 │ ││ │ • 获取前 1024 个对象 │ ││ │ • 按优先级分组: │ ││ │ - Sphere: 800 个 │ ││ │ - FOV: 200 个 │ ││ │ - CountLimiter: 24 个 │ ││ │ • 检查:没有满批次,继续获取 │ ││ └──────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ PrepareBatch #2 │ ││ │ • 获取接下来 1024 个对象 │ ││ │ • 累计分组: │ ││ │ - Sphere: 1600 个 ← 超过 1024! │ ││ │ - FOV: 400 个 │ ││ │ - CountLimiter: 48 个 │ ││ │ • 返回 ProcessFullBatchesAndContinue │ ││ └──────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 处理满批次 │ ││ │ • Sphere->Prioritize(1024 个对象) │ ││ │ • 移除已处理的块 │ ││ └──────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ PrepareBatch #3 │ ││ │ • 获取剩余 952 个对象 │ ││ │ • 返回 ProcessAllBatchesAndStop │ ││ └──────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ ┌──────────────────────────────────────────────────────────┐ ││ │ 处理所有剩余批次 │ ││ │ • Sphere->Prioritize(剩余对象) │ ││ │ • FOV->Prioritize(所有对象) │ ││ │ • CountLimiter->Prioritize(所有对象) │ ││ └──────────────────────────────────────────────────────────┘ ││ │ ││ ▼ ││ SetHighPriorityOnViewTargets() // 视图目标超高优先级 ││ │└────────────────────────────────────────────────────────────────┘🔌 连接管理方法
CPP
// 📍 源文件:ReplicationPrioritization.cpp
void FReplicationPrioritization::AddConnection(uint32 ConnectionId){
// 确保数组足够大
if (ConnectionId >= (uint32)ConnectionInfos.Num())
{
ConnectionInfos.SetNum(ConnectionId + 1U, EAllowShrinking::No);
}
++ConnectionCount;
FPerConnectionInfo& ConnectionInfo = ConnectionInfos[ConnectionId];
// 🔑 新连接继承默认优先级数组
ConnectionInfo.Priorities = DefaultPriorities;
ConnectionInfo.NextObjectIndexToProcess = 0;
ConnectionInfo.IsValid = 1;
// 通知所有优先级有新连接
for (FPrioritizerInfo& Info : PrioritizerInfos)
{
Info.Prioritizer->AddConnection(ConnectionId);
}
}
void FReplicationPrioritization::RemoveConnection(uint32 ConnectionId){
checkSlow(ConnectionId < (uint32)ConnectionInfos.Num());
--ConnectionCount;
FPerConnectionInfo& ConnectionInfo = ConnectionInfos[ConnectionId];
ConnectionInfo.IsValid = 0;
ConnectionInfo.Priorities.Empty(); // 释放内存
// 通知所有优先级连接已移除
for (FPrioritizerInfo& Info : PrioritizerInfos)
{
Info.Prioritizer->RemoveConnection(ConnectionId);
}
}📊 SetStaticPriority 和 SetPrioritizer
CPP
// 📍 源文件:ReplicationPrioritization.cpp
void FReplicationPrioritization::SetStaticPriority(uint32 ObjectIndex, float NewPrio){
if (!ensureMsgf(NewPrio >= 0.0f, TEXT("Trying to set invalid priority %f"), NewPrio))
{
return;
}
uint8& Prioritizer = ObjectIndexToPrioritizer[ObjectIndex];
float& Prio = DefaultPriorities[ObjectIndex];
bool bPrioritizerDiffers = Prioritizer != FReplicationPrioritization_InvalidNetObjectPrioritizerIndex;
bool bPrioDiffers = Prio != NewPrio;
if (bPrioritizerDiffers || bPrioDiffers)
{
Prio = NewPrio;
if (bPrioritizerDiffers)
{
// 从旧优先级中移除
FPrioritizerInfo& PrioritizerInfo = PrioritizerInfos[Prioritizer];
--PrioritizerInfo.ObjectCount;
PrioritizerInfo.Prioritizer->RemoveObject(ObjectIndex, NetObjectPrioritizationInfos[ObjectIndex]);
Prioritizer = FReplicationPrioritization_InvalidNetObjectPrioritizerIndex;
}
// 标记需要更新到各连接
ObjectsWithNewStaticPriority.SetBit(ObjectIndex);
HasNewObjectsWithStaticPriority = 1;
}
}
bool FReplicationPrioritization::SetPrioritizer(uint32 ObjectIndex, FNetObjectPrioritizerHandle NewPrioritizer){
if (!ensureMsgf(NewPrioritizer != InvalidNetObjectPrioritizerHandle,
TEXT("Call SetStaticPriority if you want to use a static priority for the object.")))
{
return false;
}
if (PrioritizerInfos.Num() == 0 ||
!ensureMsgf(NewPrioritizer < FNetObjectPrioritizerHandle(uint32(PrioritizerInfos.Num())),
TEXT("Trying to set invalid prioritizer 0x%08x"), NewPrioritizer))
{
return false;
}
// 不标记为需要复制新优先级到每个连接
// 保持旧优先级值,无论之前使用哪个优先级
// 这在节流优先级计算时应该能正常工作
DefaultPriorities[ObjectIndex] = 0.0f;
// 从旧优先级注销对象
uint8& Prioritizer = ObjectIndexToPrioritizer[ObjectIndex];
FNetObjectPrioritizationInfo& NetObjectPrioritizationInfo = NetObjectPrioritizationInfos[ObjectIndex];
const bool bWasUsingStaticPriority = (Prioritizer == FReplicationPrioritization_InvalidNetObjectPrioritizerIndex);
if (!bWasUsingStaticPriority)
{
FPrioritizerInfo& OldPrioritizerInfo = PrioritizerInfos[Prioritizer];
--OldPrioritizerInfo.ObjectCount;
OldPrioritizerInfo.Prioritizer->RemoveObject(ObjectIndex, NetObjectPrioritizationInfo);
}
// 向新优先级注册对象
{
const FNetRefHandleManager::FReplicatedObjectData& ObjectData =
NetRefHandleManager->GetReplicatedObjectDataNoCheck(ObjectIndex);
NetObjectPrioritizationInfo = FNetObjectPrioritizationInfo{};
FNetObjectPrioritizerAddObjectParams AddParams = {
NetObjectPrioritizationInfo,
ObjectData.InstanceProtocol,
ObjectData.Protocol,
NetRefHandleManager->GetReplicatedObjectStateBufferNoCheck(ObjectIndex)
};
FPrioritizerInfo& PrioritizerInfo = PrioritizerInfos[NewPrioritizer];
if (PrioritizerInfo.Prioritizer->AddObject(ObjectIndex, AddParams))
{
Prioritizer = static_cast<uint8>(NewPrioritizer);
++PrioritizerInfo.ObjectCount;
return true;
}
// 如果设置新优先级失败,默认使用静态优先级
UE_LOG(LogIris, Verbose, TEXT("Prioritizer '%s' does not support prioritizing object %u"),
ToCStr(PrioritizerInfo.Prioritizer->GetFName().GetPlainNameString()), ObjectIndex);
// 如果之前使用静态优先级,无需做任何事;否则强制设置默认优先级
if (!bWasUsingStaticPriority)
{
DefaultPriorities[ObjectIndex] = DefaultPriority;
Prioritizer = FReplicationPrioritization_InvalidNetObjectPrioritizerIndex;
ObjectsWithNewStaticPriority.SetBit(ObjectIndex);
HasNewObjectsWithStaticPriority = 1;
}
}
return false;
}📈 优先级累积机制
这是防止低优先级对象"饿死"的关键机制!
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 📈 优先级累积原理 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 对象A:每帧计算优先级 = 0.3(远处的树) │
│ │
│ 帧1: 累积优先级 = 0.0 + 0.3 = 0.3 ❌ 未被复制(<1.0) │
│ 帧2: 累积优先级 = 0.3 + 0.3 = 0.6 ❌ 未被复制 │
│ 帧3: 累积优先级 = 0.6 + 0.3 = 0.9 ❌ 未被复制 │
│ 帧4: 累积优先级 = 0.9 + 0.3 = 1.2 ✅ 被复制!(≥1.0) │
│ └─→ 复制后重置为 0.0 │
│ 帧5: 累积优先级 = 0.0 + 0.3 = 0.3 ❌ 重新开始累积 │
│ │
│ 📌 效果: │
│ • 高优先级对象(如 1.0)每帧都被复制 │
│ • 低优先级对象(如 0.3)每 4 帧被复制一次 │
│ • 没有对象会被永久"饿死"! │
│ │
└────────────────────────────────────────────────────────────────┘CPP
// 📍 源文件:ReplicationWriter.cpp
void FReplicationWriter::UpdatePriorities(const float* UpdatedPriorities){
IRIS_PROFILER_SCOPE(FReplicationWriter_UpdatePriorities);
auto UpdatePriority = [&LocalPriorities = SchedulingPriorities, UpdatedPriorities](uint32 Index)
{
// 🔑 关键:优先级是累加的,不是替换!
LocalPriorities[Index] += UpdatedPriorities[Index];
};
// 只更新有脏变化的对象
ObjectsWithDirtyChanges.ForAllSetBits(UpdatePriority);
}
// 当对象被成功复制后,重置其优先级void FReplicationWriter::OnObjectReplicated(uint32 ObjectIndex){
SchedulingPriorities[ObjectIndex] = 0.0f; // 重置累积优先级
}👑 视图目标高优先级设置
CPP
// 📍 源文件:ReplicationPrioritization.cpp
void FReplicationPrioritization::SetHighPriorityOnViewTargets(
const TArrayView<float>& Priorities,
const FReplicationView& ReplicationView){
using namespace UE::Net::Private;
// 收集所有视图目标(Controller 和 ViewTarget)
TArray<FNetHandle, TInlineAllocator<16>> ViewTargets;
for (const FReplicationView::FView& View : ReplicationView.Views)
{
if (View.Controller.IsValid())
{
ViewTargets.Add(View.Controller);
}
if (View.ViewTarget != View.Controller && View.ViewTarget.IsValid())
{
ViewTargets.Add(View.ViewTarget);
}
}
// 为视图目标设置超高优先级
for (FNetHandle NetHandle : ViewTargets)
{
const FInternalNetRefIndex ViewTargetInternalIndex =
NetRefHandleManager->GetInternalIndexFromNetHandle(NetHandle);
if (ViewTargetInternalIndex != FNetRefHandleManager::InvalidInternalIndex)
{
Priorities[ViewTargetInternalIndex] = ViewTargetHighPriority; // 1.0E7f
}
}
}🚀 性能优化策略
优化策略 | 说明 | 性能提升 |
|---|---|---|
SIMD向量化 | 每次处理4个对象,使用 VectorRegister | 3-4倍 |
批量处理 | 每批最多1024个对象,提高缓存命中 | 2-3倍 |
视图数量优化 | 单视图/双视图/多视图分别优化路径 | 10-20% |
位置缓存 | TChunkedArray分块存储,避免重复读取 | 20-30% |
预计算 | 距离平方代替距离,避免开方运算 | 5-10% |
分优先级批处理 | 相同优先级的对象一起处理 | 15-25% |
CPP
// SIMD 优化示例(SphereNetObjectPrioritizer.cpp)void USphereNetObjectPrioritizer::Prioritize(FNetObjectPrioritizationParams& Params){
const FReplicationView::FView& View = Params.View.Views[0];
const VectorRegister ViewPos = VectorLoadFloat3(&View.Pos);
// 预计算常量
const VectorRegister InnerRadiusSq = VectorSetFloat1(InnerRadius * InnerRadius);
const VectorRegister OuterRadiusSq = VectorSetFloat1(OuterRadius * OuterRadius);
// 每次处理 4 个对象(SIMD 优化)
for (uint32 ObjIt = 0; ObjIt < BatchSize; ObjIt += 4)
{
// 加载 4 个对象的位置
VectorRegister Pos0 = GetLocation(Infos[ObjIt + 0]);
VectorRegister Pos1 = GetLocation(Infos[ObjIt + 1]);
VectorRegister Pos2 = GetLocation(Infos[ObjIt + 2]);
VectorRegister Pos3 = GetLocation(Infos[ObjIt + 3]);
// 计算距离平方(避免开方)
VectorRegister DistSq0 = VectorDistSquared(Pos0, ViewPos);
// ... 并行计算4个距离
// 线性插值计算优先级
// Priority = Lerp(InnerPriority, OuterPriority, (DistSq - InnerSq) / (OuterSq - InnerSq))
// 存储结果
Params.Priorities[ObjIt + 0] = Priority0;
Params.Priorities[ObjIt + 1] = Priority1;
Params.Priorities[ObjIt + 2] = Priority2;
Params.Priorities[ObjIt + 3] = Priority3;
}
}🎮 6.7 实际应用案例
🎯 FPS射击游戏配置
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🎯 FPS 游戏优先级策略 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 核心需求: │
│ • 准星正对的敌人必须精确同步 │
│ • 视野边缘的对象可以稍微延迟 │
│ • 背后的敌人仍需要同步(脚步声等) │
│ │
│ 推荐配置:FieldOfViewNetObjectPrioritizer │
│ │
└────────────────────────────────────────────────────────────────┘INI
[/Script/IrisCore.FieldOfViewNetObjectPrioritizerConfig]; 近身区域 - 360度高优先级InnerSphereRadius=2000.0 ; 20米内最高优先级InnerSpherePriority=1.0
; 视野锥 - 瞄准方向高优先级ConeFieldOfViewDegrees=30.0 ; 窄视野锥(瞄准状态)ConeLength=30000.0 ; 300米瞄准距离MaxConePriority=1.0
; 准星中心 - 最高优先级LineOfSightWidth=100.0 ; 准星范围LineOfSightPriority=1.0
; 外围区域OuterSpherePriority=0.3 ; 视野外但在范围内OutsidePriority=0.1 ; 超远距离🌍 开放世界RPG配置
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🌍 开放世界 RPG 优先级策略 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 核心需求: │
│ • 大量NPC和道具需要同步 │
│ • 远处的对象可以低频更新 │
│ • 玩家附近的交互对象优先 │
│ │
│ 推荐配置:SphereNetObjectPrioritizer + CountLimiter │
│ │
└────────────────────────────────────────────────────────────────┘INI
[/Script/IrisCore.SphereNetObjectPrioritizerConfig]InnerRadius=5000.0 ; 50米内最高优先级OuterRadius=20000.0 ; 200米外最低优先级InnerPriority=1.0OuterPriority=0.2OutsidePriority=0.05 ; 很远的对象几乎不同步
[/Script/IrisCore.ObjectReplicationBridgeConfig]; 为大量相似对象使用 CountLimiter
+PrioritizerConfigs=(ClassName="/Script/MyGame.GoldCoin",PrioritizerName="NetObjectCountLimiter")
+PrioritizerConfigs=(ClassName="/Script/MyGame.TreeActor",PrioritizerName="NetObjectCountLimiter")
+PrioritizerConfigs=(ClassName="/Script/MyGame.GrassActor",PrioritizerName="NetObjectCountLimiter")
[/Script/IrisCore.NetObjectCountLimiterConfig]Mode=Fill ; 使用填充模式,防止饥饿MaxObjectCount=20 ; 每帧最多考虑20个bEnableOwnedObjectsFastLane=true🏎️ 竞速游戏配置
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🏎️ 竞速游戏优先级策略 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 核心需求: │
│ • 前方的车辆必须精确同步(避免碰撞) │
│ • 后方的车辆可以稍微延迟 │
│ • 所有车辆都需要同步(排名显示) │
│ │
│ 推荐配置:FieldOfViewNetObjectPrioritizer + OwnerBoost │
│ │
└────────────────────────────────────────────────────────────────┘INI
[/Script/IrisCore.FieldOfViewNetObjectPrioritizerConfig]InnerSphereRadius=3000.0 ; 30米内高优先级ConeFieldOfViewDegrees=60.0 ; 宽视野锥ConeLength=50000.0 ; 500米前方视野MaxConePriority=1.0OutsidePriority=0.3 ; 后方车辆也要同步
[/Script/IrisCore.SphereWithOwnerBoostNetObjectPrioritizerConfig]OwnerPriorityBoost=3.0 ; 自己的车辆优先级加成🎪 大逃杀游戏配置
PLAINTEXT
┌────────────────────────────────────────────────────────────────┐
│ 🎪 大逃杀游戏优先级策略 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 核心需求: │
│ • 100人同时游戏,带宽压力大 │
│ • 远处的玩家可以低频更新 │
│ • 近距离战斗必须高频同步 │
│ │
│ 推荐配置:Sphere + 激进的距离衰减 │
│ │
└────────────────────────────────────────────────────────────────┘INI
[/Script/IrisCore.SphereNetObjectPrioritizerConfig]InnerRadius=3000.0 ; 30米内高优先级(近战范围)OuterRadius=15000.0 ; 150米外低优先级InnerPriority=1.0OuterPriority=0.1 ; 激进衰减OutsidePriority=0.02 ; 超远几乎不同步
[/Script/IrisCore.NetObjectCountLimiterConfig]; 限制每帧同步的玩家数量MaxObjectCount=30 ; 每帧最多考虑30个玩家Mode=Fill ; 防止任何玩家被饿死📝 6.8 小结
📊 知识点总结
知识点 | 要点 |
|---|---|
优先级作用 | 在带宽有限时决定"先发谁" |
静态vs动态 | 静态一次设定;动态每帧计算 |
计算时机 | 在过滤之后执行,只计算过滤后的对象 |
优先级累积 | 未被复制的对象优先级会累积,防止"饿死" |
视图目标 | 玩家角色获得超高优先级(1.0E7f) |
内置优先级 | LocationBased(基类)、Sphere(距离)、FOV(视野)、CountLimiter(数量限制) |
配置方式 | ini文件配置 + 类级别指定 |
与过滤协作 | 过滤先筛选,优先级后排序 |
🏗️ 优先级继承关系
PLAINTEXT
UNetObjectPrioritizer (抽象基类)
│
├── ULocationBasedNetObjectPrioritizer (抽象,位置管理)
│ │
│ ├── USphereNetObjectPrioritizer (球形距离)
│ │ │
│ │ └── USphereWithOwnerBoostNetObjectPrioritizer (所有者加成)
│ │
│ └── UFieldOfViewNetObjectPrioritizer (视野锥)
│
└── UNetObjectCountLimiter (数量限制)
│
├── RoundRobin 模式(轮询)
└── Fill 模式(饥饿预防)🎯 选择优先级的建议
游戏类型 | 推荐优先级 | 原因 |
|---|---|---|
通用/RPG | SphereNetObjectPrioritizer | 简单高效,距离衰减直观 |
FPS/TPS | FieldOfViewNetObjectPrioritizer | 准星方向需要高精度 |
有所有权的对象 | SphereWithOwnerBoostNetObjectPrioritizer | 自己的东西优先同步 |
大量相似对象 | NetObjectCountLimiter | 限制数量,防止带宽爆炸 |
竞速游戏 | FOV + OwnerBoost | 前方车辆 + 自己的车 |
大逃杀 | Sphere + CountLimiter | 距离衰减 + 数量限制 |
🚀 性能优化要点
优化点 | 建议 |
|---|---|
选择合适的优先级 | 不需要视野优先级就用 Sphere,更快 |
合理配置距离参数 | 根据游戏地图大小调整 Inner/Outer Radius |
使用 CountLimiter | 大量相似对象(金币、树木)必须限制数量 |
启用快速通道 | 所有者对象走快速通道,减少延迟 |
避免过多视图 | 分屏游戏视图数量影响性能 |
🔧 内部实现关键点
实现细节 | 说明 |
|---|---|
FNetObjectPrioritizationInfo | 8字节固定大小,子类重新解释使用 |
ObjectIndexToPrioritizer | uint8数组,最多支持256个优先级 |
TChunkedArrayWithChunkManagement | 分块数组,每块1024对象,处理后可立即释放 |
FPrioritizerBatchHelper | 按优先级分组对象,支持分批处理 |
FUpdateDirtyObjectsBatchHelper | 通知优先级脏对象,每批512个 |
SIMD优化 | VectorRegister 4-way并行,显著提升性能 |
内存栈分配 | FMemStack避免堆分配,减少内存碎片 |
🔧 调试技巧
CPP
// 打印对象优先级UE_LOG(LogIris, Log, TEXT("Object %d Priority: %f"), ObjectIndex, Priority);
// 检查视图是否正确设置const FReplicationView& View = ReplicationSystem->GetReplicationView(ConnectionId);
UE_LOG(LogIris, Log, TEXT("View Pos: %s, Dir: %s"), *View.Views[0].Pos.ToString(), *View.Views[0].Dir.ToString());
// 检查优先级分配
FNetObjectPrioritizerHandle Handle = ReplicationSystem->GetPrioritizer(ObjectIndex);
UE_LOG(LogIris, Log, TEXT("Prioritizer Handle: %d"), Handle.GetIndex());本文档基于 Unreal Engine 5.5.0 Iris 源代码分析(源码目录:Engine/Source/Runtime/Experimental/Iris/)