🎭 Iris 网络复制系统技术分析 - 第十三部分:条件复制 (Conditionals) - 智能数据分发系统

📖 本文档详细讲解 Iris 复制系统中的条件复制机制,教你如何精确控制"什么数据发给谁"。
📋 目录
13.1 条件复制概述
💡 什么是条件复制?
想象你是一个情报局长,手下有很多特工(客户端),你需要分发情报(数据)给他们:
PLAINTEXT
🏢 情报局(服务器)
│
├── 📋 机密文件A(仅给局长看) → 只发给 Owner
├── 📋 普通情报B(所有特工都能看) → 发给所有人
├── 📋 行动指令C(只给执行者看) → 只发给 Autonomous
└── 📋 观察报告D(只给旁观者看) → 只发给 Simulated条件复制就是这个"情报分发规则"——决定哪些数据应该发送给哪些客户端。
🎯 为什么需要条件复制?
场景 | 问题 | 解决方案 |
|---|---|---|
🎮 玩家血量 | 敌人不应该看到你的精确血量 |
|
🔫 武器弹药 | 只有持有者需要知道弹药数 |
|
🏃 移动预测 | 本地玩家不需要服务器的位置数据 |
|
🎬 初始状态 | 角色名字只需发送一次 |
|
🎥 回放系统 | 某些数据只在回放时需要 |
|
🏗️ 条件复制的核心组件
PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 条件复制系统架构 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ ELifetimeCondition │ │ EReplicationCondition│ │
│ │ (属性级条件) │ │ (对象级条件) │ │
│ │ • COND_OwnerOnly │ │ • RoleAutonomous │ │
│ │ • COND_SkipOwner │ │ • ReplicatePhysics │ │
│ │ • COND_InitialOnly │ └─────────────────────┘ │
│ │ • ... │ │
│ └─────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ FReplicationConditionals │ │
│ │ • GetLifetimeConditionals() - 计算条件掩码 │ │
│ │ • ApplyConditionalsToChangeMask() - 应用条件 │ │
│ │ • SetConditionConnectionFilter() - 设置连接过滤 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ FConditionalsMask (16位掩码) │ │
│ │ 每个条件对应一个位:bit0=COND_None, bit2=COND_OwnerOnly...│
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘📁 关键源文件索引
文件 | 位置 | 说明 |
|---|---|---|
|
| 对象级条件枚举 |
|
| 条件系统核心类 |
|
| 条件系统实现 |
|
|
|
|
|
|
13.2 ELifetimeCondition 条件类型详解
📊 完整条件类型一览
ELifetimeCondition 是 UE 网络复制的核心枚举,定义在 CoreNetTypes.h 中:
CPP
// Engine/Source/Runtime/CoreUObject/Public/UObject/CoreNetTypes.hUENUM(BlueprintType)
enum ELifetimeCondition : int
{
COND_None = 0, // 无条件,始终复制
COND_InitialOnly = 1, // 仅初始状态复制
COND_OwnerOnly = 2, // 仅复制给拥有者
COND_SkipOwner = 3, // 跳过拥有者
COND_SimulatedOnly = 4, // 仅模拟代理
COND_AutonomousOnly = 5, // 仅自主代理
COND_SimulatedOrPhysics = 6, // 模拟代理或物理复制
COND_InitialOrOwner = 7, // 初始状态或拥有者
COND_Custom = 8, // 自定义条件
COND_ReplayOrOwner = 9, // 回放或拥有者
COND_ReplayOnly = 10, // 仅回放
COND_SimulatedOnlyNoReplay = 11, // 模拟代理(非回放)
COND_SimulatedOrPhysicsNoReplay = 12, // 模拟或物理(非回放)
COND_SkipReplay = 13, // 跳过回放
COND_Dynamic = 14, // 动态条件(运行时决定)
COND_Never = 15, // 永不复制
COND_NetGroup = 16, // 网络组条件(仅用于子对象)
COND_Max = 17
};🎮 日常生活类比
让我们用一个公司内部通讯系统来理解这些条件:
PLAINTEXT
🏢 公司(服务器)发送消息给员工(客户端)
┌────────────────────┬────────────────────────────────────────────┐
│ 条件类型 │ 日常类比 │
├────────────────────┼────────────────────────────────────────────┤
│ COND_None │ 📢 全员通知 - 所有人都能看到 │
│ COND_InitialOnly │ 📋 入职手册 - 只在入职时发一次 │
│ COND_OwnerOnly │ 💰 工资单 - 只有本人能看 │
│ COND_SkipOwner │ 🎂 生日惊喜 - 除了寿星其他人都知道 │
│ COND_SimulatedOnly │ 👀 旁观者报告 - 只给观察员看 │
│ COND_AutonomousOnly│ 🎮 操作指南 - 只给实际操作者看 │
│ COND_Custom │ 🔐 VIP专属 - 根据权限动态决定 │
│ COND_ReplayOnly │ 📹 监控录像 - 只在回放时显示 │
│ COND_Never │ 🚫 内部机密 - 永远不外传 │
└────────────────────┴────────────────────────────────────────────┘📝 详细条件说明
1️⃣ COND_None - 无条件复制
CPP
// 最常用的条件 - 数据变化就复制UPROPERTY(Replicated)
float Health; // 默认就是 COND_None
// 显式指定void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(AMyActor, Health); // 等同于 COND_None
}2️⃣ COND_InitialOnly - 仅初始复制
CPP
// 🎮 适用场景:角色名字、皮肤ID等不会改变的数据UPROPERTY(Replicated)
FString PlayerName;
UPROPERTY(Replicated)
int32 SkinId;
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 只在角色首次复制时发送,之后不再更新
DOREPLIFETIME_CONDITION(AMyCharacter, PlayerName, COND_InitialOnly);
DOREPLIFETIME_CONDITION(AMyCharacter, SkinId, COND_InitialOnly);
}流程图:
PLAINTEXT
服务器创建角色
│
▼┌──────────────────┐│ 首次复制到客户端 ││ PlayerName: "张三"│ ✅ 发送│ SkinId: 5 │ ✅ 发送└──────────────────┘
│
▼ (之后的更新)┌──────────────────┐│ PlayerName 改变? │ ❌ 不发送(InitialOnly)│ SkinId 改变? │ ❌ 不发送(InitialOnly)└──────────────────┘3️⃣ COND_OwnerOnly - 仅拥有者
CPP
// 🔫 适用场景:弹药数、技能冷却等敏感数据UPROPERTY(Replicated)
int32 CurrentAmmo;
UPROPERTY(Replicated)
float AbilityCooldown;
void AMyWeapon::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 只有武器的拥有者能看到弹药数
DOREPLIFETIME_CONDITION(AMyWeapon, CurrentAmmo, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(AMyWeapon, AbilityCooldown, COND_OwnerOnly);
}数据流向图:
PLAINTEXT
服务器
│
CurrentAmmo = 30
│
┌─────┴─────┐
▼ ▼
玩家A 玩家B
(Owner) (非Owner)
│ │
▼ ▼
收到: 30 收不到❌4️⃣ COND_SkipOwner - 跳过拥有者
CPP
// 🏃 适用场景:服务器位置修正(本地玩家有预测,不需要)UPROPERTY(Replicated)
FVector ServerPosition;
UPROPERTY(Replicated)
FRotator ServerRotation;
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 本地玩家用预测位置,不需要服务器位置
// 其他玩家需要服务器位置来显示
DOREPLIFETIME_CONDITION(AMyCharacter, ServerPosition, COND_SkipOwner);
DOREPLIFETIME_CONDITION(AMyCharacter, ServerRotation, COND_SkipOwner);
}为什么要跳过 Owner?
PLAINTEXT
┌─────────────────────────────────────────────────────────┐
│ 移动预测系统 │
├─────────────────────────────────────────────────────────┤
│ │
│ 本地玩家(Owner): │
│ ┌─────────────────────────────────────┐ │
│ │ 输入 → 本地预测 → 立即响应 │ │
│ │ 不需要等服务器位置,体验更流畅 │ │
│ └─────────────────────────────────────┘ │
│ │
│ 其他玩家(非Owner): │
│ ┌─────────────────────────────────────┐ │
│ │ 需要服务器位置来显示其他玩家 │ │
│ │ 没有预测,必须依赖复制数据 │ │
│ └─────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘5️⃣ COND_SimulatedOnly vs COND_AutonomousOnly
CPP
// 理解 Simulated 和 Autonomous 的区别
// Autonomous(自主代理):玩家直接控制的角色// Simulated(模拟代理):其他玩家看到的你的角色
UPROPERTY(Replicated)
FVector InputVector; // 输入向量 - 只有自主代理需要
UPROPERTY(Replicated)
FVector SmoothedPosition; // 平滑位置 - 只有模拟代理需要
void AMyCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 输入向量只发给控制这个角色的客户端
DOREPLIFETIME_CONDITION(AMyCharacter, InputVector, COND_AutonomousOnly);
// 平滑位置只发给其他客户端(用于平滑显示)
DOREPLIFETIME_CONDITION(AMyCharacter, SmoothedPosition, COND_SimulatedOnly);
}角色类型对照表:
PLAINTEXT
┌──────────────────────────────────────────────────────────────┐
│ 角色类型对照 │
├────────────┬─────────────────────┬───────────────────────────┤
│ 视角 │ Autonomous │ Simulated │
├────────────┼─────────────────────┼───────────────────────────┤
│ 玩家A看 │ 玩家A自己的角色 │ 玩家B、C、D的角色 │
│ 玩家A的角色│ │ │
├────────────┼─────────────────────┼───────────────────────────┤
│ 玩家B看 │ 玩家B自己的角色 │ 玩家A、C、D的角色 │
│ 玩家B的角色│ │ │
└────────────┴─────────────────────┴───────────────────────────┘6️⃣ COND_SimulatedOrPhysics - 模拟或物理
CPP
// 🚗 适用场景:物理模拟的对象(如载具)UPROPERTY(Replicated)
FVector PhysicsVelocity;
void AMyVehicle::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 当 bReplicatePhysics 为 true 时,即使是 Owner 也会收到
DOREPLIFETIME_CONDITION(AMyVehicle, PhysicsVelocity, COND_SimulatedOrPhysics);
}7️⃣ COND_Custom - 自定义条件
CPP
// 🔐 适用场景:需要在运行时动态控制的属性UPROPERTY(Replicated)
float SecretData;
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 使用自定义条件
DOREPLIFETIME_CONDITION(AMyActor, SecretData, COND_Custom);
}
// 在运行时控制是否复制void AMyActor::SetSecretDataActive(bool bActive){
// 使用宏来设置自定义条件的激活状态
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyActor, SecretData, bActive);
}8️⃣ COND_Dynamic - 动态条件
CPP
// 🎭 适用场景:条件在运行时可能改变UPROPERTY(Replicated)
float DynamicData;
void AMyActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 声明为动态条件
DOREPLIFETIME_CONDITION(AMyActor, DynamicData, COND_Dynamic);
}
// 在运行时改变条件void AMyActor::ChangeDynamicCondition(ELifetimeCondition NewCondition){
// 通过 PropertyConditionManager 设置新条件
DOREPLIFETIME_CHANGE_CONDITION(AMyActor, DynamicData, NewCondition);
}📊 条件组合速查表
条件 | Owner 收到 | 非Owner 收到 | 初始状态 | 后续更新 | 回放 |
|---|---|---|---|---|---|
COND_None | ✅ | ✅ | ✅ | ✅ | ✅ |
COND_InitialOnly | ✅ | ✅ | ✅ | ❌ | ✅ |
COND_OwnerOnly | ✅ | ❌ | ✅ | ✅ | ❌ |
COND_SkipOwner | ❌ | ✅ | ✅ | ✅ | ✅ |
COND_SimulatedOnly | ❌* | ✅ | ✅ | ✅ | ✅ |
COND_AutonomousOnly | ✅* | ❌ | ✅ | ✅ | ❌ |
COND_InitialOrOwner | ✅ | ✅初始/❌后续 | ✅ | Owner✅ | ❌ |
COND_ReplayOnly | ❌ | ❌ | ❌ | ❌ | ✅ |
COND_SkipReplay | ✅ | ✅ | ✅ | ✅ | ❌ |
COND_Never | ❌ | ❌ | ❌ | ❌ | ❌ |
*注:Simulated/Autonomous 取决于角色的 Role,不是 Owner 关系
13.3 ReplicationConditionals 核心实现
🏗️ FReplicationConditionals 类结构
CPP
// Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/Conditionals/ReplicationConditionals.h
class FReplicationConditionals
{
public:
// 初始化
void Init(FReplicationConditionalsInitParams& Params);
// 连接管理
void AddConnection(uint32 ConnectionId);
void RemoveConnection(uint32 ConnectionId);
// 条件设置 API
bool SetConditionConnectionFilter(FInternalNetRefIndex ObjectIndex,
EReplicationCondition Condition,
uint32 ConnectionId, bool bEnable);
bool SetCondition(FInternalNetRefIndex ObjectIndex,
EReplicationCondition Condition, bool bEnable);
void SetOwningConnection(FInternalNetRefIndex ObjectIndex, uint32 ConnectionId);
// 属性条件
void InitPropertyCustomConditions(FInternalNetRefIndex ObjectIndex);
bool SetPropertyCustomCondition(FInternalNetRefIndex ObjectIndex,
const void* Owner, uint16 RepIndex, bool bIsActive);
bool SetPropertyDynamicCondition(FInternalNetRefIndex ObjectIndex,
const void* Owner, uint16 RepIndex,
ELifetimeCondition Condition);
// 核心方法:应用条件到变化掩码
bool ApplyConditionalsToChangeMask(uint32 ReplicatingConnectionId,
bool bIsInitialState,
FInternalNetRefIndex ParentObjectIndex,
FInternalNetRefIndex ObjectIndex,
uint32* ChangeMaskData,
const uint32* ConditionalChangeMaskData,
const FReplicationProtocol* Protocol);
private:
// 每对象信息
struct FPerObjectInfo
{
uint16 AutonomousConnectionId : 15; // 自主代理的连接ID
uint16 bRepPhysics : 1; // 是否复制物理
};
// 每连接信息
struct FPerConnectionInfo
{
TArray<FConditionalsMask> ObjectConditionals; // 每个对象的条件掩码
};
// 条件掩码结构
struct FConditionalsMask
{
uint16 ConditionalsMask; // 16位,每位对应一个条件
bool IsConditionEnabled(int Condition) const
{
return ConditionalsMask & (uint16(1) << unsigned(Condition));
}
};
// 获取生命周期条件
FConditionalsMask GetLifetimeConditionals(uint32 ReplicatingConnectionId,
FInternalNetRefIndex ParentObjectIndex,
bool bIsInitialState) const;
};🔍 GetLifetimeConditionals 实现详解
这是条件复制的核心方法,决定了哪些条件对特定连接是"启用"的:
CPP
// ReplicationConditionals.cppFReplicationConditionals::FConditionalsMask
FReplicationConditionals::GetLifetimeConditionals(
uint32 ReplicatingConnectionId,
FInternalNetRefIndex ParentObjectIndex,
bool bIsInitialState) const{
FConditionalsMask ConditionalsMask{0};
// 1️⃣ 获取拥有者连接
const uint32 ObjectOwnerConnectionId = ReplicationFiltering->GetOwningConnection(ParentObjectIndex);
const bool bIsReplicatingToOwner = (ReplicatingConnectionId == ObjectOwnerConnectionId);
// 2️⃣ 获取对象信息
const FPerObjectInfo* ObjectInfo = GetPerObjectInfo(ParentObjectIndex);
const bool bRoleSimulated = ReplicatingConnectionId != ObjectInfo->AutonomousConnectionId;
const bool bRoleAutonomous = ReplicatingConnectionId == ObjectInfo->AutonomousConnectionId;
const bool bRepPhysics = ObjectInfo->bRepPhysics;
// 3️⃣ 设置各条件的启用状态
ConditionalsMask.SetConditionEnabled(COND_None, true); // 始终启用
ConditionalsMask.SetConditionEnabled(COND_Custom, true); // 自定义始终启用
ConditionalsMask.SetConditionEnabled(COND_Dynamic, true); // 动态始终启用
// 基于 Owner 的条件
ConditionalsMask.SetConditionEnabled(COND_OwnerOnly, bIsReplicatingToOwner);
ConditionalsMask.SetConditionEnabled(COND_SkipOwner, !bIsReplicatingToOwner);
// 基于 Role 的条件
ConditionalsMask.SetConditionEnabled(COND_SimulatedOnly, bRoleSimulated);
ConditionalsMask.SetConditionEnabled(COND_AutonomousOnly, bRoleAutonomous);
ConditionalsMask.SetConditionEnabled(COND_SimulatedOrPhysics, bRoleSimulated | bRepPhysics);
// 基于初始状态的条件
ConditionalsMask.SetConditionEnabled(COND_InitialOnly, bIsInitialState);
ConditionalsMask.SetConditionEnabled(COND_InitialOrOwner, bIsReplicatingToOwner | bIsInitialState);
// 回放相关条件(非回放连接时的设置)
ConditionalsMask.SetConditionEnabled(COND_ReplayOrOwner, bIsReplicatingToOwner);
ConditionalsMask.SetConditionEnabled(COND_SimulatedOnlyNoReplay, bRoleSimulated);
ConditionalsMask.SetConditionEnabled(COND_SimulatedOrPhysicsNoReplay, bRoleSimulated | bRepPhysics);
ConditionalsMask.SetConditionEnabled(COND_SkipReplay, true);
return ConditionalsMask;
}📊 条件掩码可视化
PLAINTEXT
FConditionalsMask (16位)
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│F│E│D│C│B│A│9│8│7│6│5│4│3│2│1│0│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─ COND_None (0)
│ │ │ │ │ │ │ │ │ │ │ │ │ │ └─── COND_InitialOnly (1)
│ │ │ │ │ │ │ │ │ │ │ │ │ └───── COND_OwnerOnly (2)
│ │ │ │ │ │ │ │ │ │ │ │ └─────── COND_SkipOwner (3)
│ │ │ │ │ │ │ │ │ │ │ └───────── COND_SimulatedOnly (4)
│ │ │ │ │ │ │ │ │ │ └─────────── COND_AutonomousOnly (5)
│ │ │ │ │ │ │ │ │ └───────────── COND_SimulatedOrPhysics (6)
│ │ │ │ │ │ │ │ └─────────────── COND_InitialOrOwner (7)
│ │ │ │ │ │ │ └───────────────── COND_Custom (8)
│ │ │ │ │ │ └─────────────────── COND_ReplayOrOwner (9)
│ │ │ │ │ └───────────────────── COND_ReplayOnly (10)
│ │ │ │ └─────────────────────── COND_SimulatedOnlyNoReplay (11)
│ │ │ └───────────────────────── COND_SimulatedOrPhysicsNoReplay (12)
│ │ └─────────────────────────── COND_SkipReplay (13)
│ └───────────────────────────── COND_Dynamic (14)
└─────────────────────────────── COND_Never (15)
示例:复制给 Owner 的初始状态
ConditionalsMask = 0b0010_0001_1000_0111 = 0x2187
│ │ │ │││
│ │ │ ││└─ COND_None ✅
│ │ │ │└── COND_InitialOnly ✅
│ │ │ └─── COND_OwnerOnly ✅
│ │ └──────── COND_Custom ✅
│ └──────────── COND_Dynamic ✅
└───────────────── COND_SkipReplay ✅13.4 条件评估流程
🔄 完整评估流程图
PLAINTEXT
┌─────────────────────────────────────────────────────────────────────────┐
│ 条件评估流程 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ 1️⃣ 复制开始 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ GetLifetimeConditionals() │ │
│ │ 计算当前连接的条件掩码 │ │
│ │ • 检查 Owner 关系 │ │
│ │ • 检查 Role (Autonomous/Simulated) │ │
│ │ • 检查是否初始状态 │ │
│ │ • 检查物理复制标志 │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 2️⃣ 遍历每个属性 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 获取属性的 ELifetimeCondition │ │
│ │ 从 MemberLifetimeConditionDescriptors │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ 条件是否在掩码中启用? │ │
│ │ IsConditionEnabled(Condition) │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ├─── 是 ──▶ 保留 ChangeMask 位(允许复制) │
│ │ │
│ └─── 否 ──▶ 清除 ChangeMask 位(阻止复制) │
│ │
│ 3️⃣ 应用自定义条件掩码 │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────┐ │
│ │ ChangeMask &= ConditionalChangeMask │ │
│ │ 进一步过滤自定义条件 │ │
│ └──────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 4️⃣ 返回最终的 ChangeMask │
│ 只有位为 1 的属性才会被序列化发送 │
│ │
└─────────────────────────────────────────────────────────────────────────┘📝 ApplyConditionalsToChangeMask 核心逻辑
CPP
// ReplicationConditionals.cppbool FReplicationConditionals::ApplyConditionalsToChangeMask(
uint32 ReplicatingConnectionId,
bool bIsInitialState,
FInternalNetRefIndex ParentObjectIndex,
FInternalNetRefIndex ObjectIndex,
uint32* ChangeMaskData,
const uint32* ConditionalChangeMaskData,
const FReplicationProtocol* Protocol){
bool bMaskWasModified = false;
FNetBitArrayView ChangeMask = MakeNetBitArrayView(ChangeMaskData, Protocol->ChangeMaskBitCount);
// 检查协议是否有生命周期条件
if (EnumHasAnyFlags(Protocol->ProtocolTraits, EReplicationProtocolTraits::HasLifetimeConditionals))
{
// 获取当前连接的条件掩码
const FConditionalsMask LifetimeConditionals =
GetLifetimeConditionals(ReplicatingConnectionId, ParentObjectIndex, bIsInitialState);
// 获取上一次的条件掩码(用于检测条件变化)
FConditionalsMask PrevLifeTimeConditions =
ConnectionInfos[ReplicatingConnectionId].ObjectConditionals[ObjectIndex];
// 遍历每个成员
for (uint32 MemberIt = 0; MemberIt < StateDescriptor->MemberCount; ++MemberIt)
{
// 获取成员的条件
ELifetimeCondition Condition =
static_cast<ELifetimeCondition>(LifetimeConditionDescriptors[MemberIt].Condition);
// 处理动态条件
if (Condition == COND_Dynamic)
{
Condition = GetDynamicCondition(ObjectIndex, Property->RepIndex);
}
// 检查条件是否启用
if (LifetimeConditionals.IsConditionEnabled(Condition))
{
// 条件启用:如果之前被禁用,现在需要标记为脏
if (!PrevLifeTimeConditions.IsConditionEnabled(Condition))
{
// 条件从禁用变为启用,标记属性为脏
ChangeMask.SetBits(ChangeMaskDescriptor.BitOffset, ChangeMaskDescriptor.BitCount);
bMaskWasModified = true;
}
}
else
{
// 条件禁用:清除 ChangeMask 中对应的位
if (ChangeMask.IsAnyBitSet(ChangeMaskDescriptor.BitOffset, ChangeMaskDescriptor.BitCount))
{
ChangeMask.ClearBits(ChangeMaskDescriptor.BitOffset, ChangeMaskDescriptor.BitCount);
bMaskWasModified = true;
}
}
}
// 保存当前条件掩码供下次比较
ConnectionInfos[ReplicatingConnectionId].ObjectConditionals[ObjectIndex] = LifetimeConditionals;
}
// 应用自定义条件掩码
if (ConditionalChangeMaskData != nullptr)
{
// 按字进行 AND 操作
for (uint32 WordIt = 0; WordIt < ChangeMask.GetNumWords(); ++WordIt)
{
const uint32 OldMask = ChangeMaskData[WordIt];
const uint32 ConditionalMask = ConditionalChangeMaskData[WordIt];
ChangeMaskData[WordIt] = OldMask & ConditionalMask;
}
}
return bMaskWasModified;
}🎯 条件变化检测
当条件状态改变时(如 Owner 改变),系统会自动处理:
CPP
// 设置拥有连接时的处理void FReplicationConditionals::SetOwningConnection(FInternalNetRefIndex ObjectIndex, uint32 OwningConnectionId){
const uint32 OldOwningConnectionId = ReplicationFiltering->GetOwningConnection(ObjectIndex);
if (OldOwningConnectionId != OwningConnectionId)
{
// 标记对象的生命周期条件为脏
ObjectsWithDirtyLifetimeConditionals.SetBit(ObjectIndex);
// 使受影响连接的基线失效
InvalidateBaselinesForObjectHierarchy(ObjectIndex, ConnectionsToInvalidate);
}
}13.5 实际应用案例
🎮 案例1:FPS 游戏角色
CPP
// MyFPSCharacter.hUCLASS()
class AMyFPSCharacter : public ACharacter
{
GENERATED_BODY()
public:
// 🩸 血量 - 所有人都能看到(用于显示血条)
UPROPERTY(ReplicatedUsing=OnRep_Health)
float Health;
// 🛡️ 护甲 - 只有自己能看到
UPROPERTY(Replicated)
float Armor;
// 🔫 当前弹药 - 只有自己能看到
UPROPERTY(Replicated)
int32 CurrentAmmo;
// 📦 总弹药 - 只有自己能看到
UPROPERTY(Replicated)
int32 TotalAmmo;
// 🏃 服务器位置 - 其他人看到的位置
UPROPERTY(Replicated)
FVector ServerLocation;
// 👤 玩家名字 - 只发送一次
UPROPERTY(Replicated)
FString PlayerName;
// 🎨 皮肤ID - 只发送一次
UPROPERTY(Replicated)
int32 SkinId;
// 💀 击杀数 - 所有人都能看到(计分板)
UPROPERTY(Replicated)
int32 KillCount;
// 🔒 私密状态 - 可动态控制
UPROPERTY(Replicated)
bool bIsInvisible; // 隐身状态
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyFPSCharacter.cppvoid AMyFPSCharacter::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 📊 无条件复制 - 所有人都需要
DOREPLIFETIME(AMyFPSCharacter, Health);
DOREPLIFETIME(AMyFPSCharacter, KillCount);
// 🔒 仅拥有者 - 敏感数据
DOREPLIFETIME_CONDITION(AMyFPSCharacter, Armor, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(AMyFPSCharacter, CurrentAmmo, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(AMyFPSCharacter, TotalAmmo, COND_OwnerOnly);
// ⏭️ 跳过拥有者 - 本地预测
DOREPLIFETIME_CONDITION(AMyFPSCharacter, ServerLocation, COND_SkipOwner);
// 📋 仅初始 - 不变数据
DOREPLIFETIME_CONDITION(AMyFPSCharacter, PlayerName, COND_InitialOnly);
DOREPLIFETIME_CONDITION(AMyFPSCharacter, SkinId, COND_InitialOnly);
// 🎭 自定义条件 - 运行时控制
DOREPLIFETIME_CONDITION(AMyFPSCharacter, bIsInvisible, COND_Custom);
}
// 动态控制隐身状态的复制void AMyFPSCharacter::SetInvisible(bool bNewInvisible){
bIsInvisible = bNewInvisible;
// 当隐身时,只有队友能看到这个状态
// 敌人不应该知道你隐身了
bool bShouldReplicate = !bNewInvisible; // 取消隐身时复制给所有人
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyFPSCharacter, bIsInvisible, bShouldReplicate);
}🚗 案例2:载具系统
CPP
// MyVehicle.hUCLASS()
class AMyVehicle : public APawn
{
GENERATED_BODY()
public:
// 🚗 载具状态 - 所有人都能看到
UPROPERTY(Replicated)
float CurrentSpeed;
// ⛽ 燃料 - 只有驾驶员能看到
UPROPERTY(Replicated)
float Fuel;
// 🔧 引擎状态 - 只有驾驶员能看到
UPROPERTY(Replicated)
bool bEngineRunning;
// 📍 物理位置 - 模拟或物理复制
UPROPERTY(Replicated)
FVector PhysicsLocation;
// 🔄 物理旋转 - 模拟或物理复制
UPROPERTY(Replicated)
FRotator PhysicsRotation;
// 🎮 输入状态 - 只有驾驶员需要
UPROPERTY(Replicated)
float ThrottleInput;
UPROPERTY(Replicated)
float SteeringInput;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyVehicle.cppvoid AMyVehicle::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 所有人都能看到速度
DOREPLIFETIME(AMyVehicle, CurrentSpeed);
// 只有驾驶员能看到燃料和引擎状态
DOREPLIFETIME_CONDITION(AMyVehicle, Fuel, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(AMyVehicle, bEngineRunning, COND_OwnerOnly);
// 物理数据:模拟代理或物理复制时发送
// 当 bReplicatePhysics=true 时,即使是 Owner 也会收到
DOREPLIFETIME_CONDITION(AMyVehicle, PhysicsLocation, COND_SimulatedOrPhysics);
DOREPLIFETIME_CONDITION(AMyVehicle, PhysicsRotation, COND_SimulatedOrPhysics);
// 输入状态只有自主代理需要
DOREPLIFETIME_CONDITION(AMyVehicle, ThrottleInput, COND_AutonomousOnly);
DOREPLIFETIME_CONDITION(AMyVehicle, SteeringInput, COND_AutonomousOnly);
}🎒 案例3:背包系统
CPP
// MyInventoryComponent.hUCLASS()
class UMyInventoryComponent : public UActorComponent
{
GENERATED_BODY()
public:
// 📦 背包容量 - 只发送一次
UPROPERTY(Replicated)
int32 MaxSlots;
// 🎒 物品列表 - 只有拥有者能看到
UPROPERTY(Replicated)
TArray<FInventoryItem> Items;
// 💰 金币数量 - 只有拥有者能看到
UPROPERTY(Replicated)
int32 Gold;
// ⚖️ 当前重量 - 只有拥有者能看到
UPROPERTY(Replicated)
float CurrentWeight;
// 🔓 是否已解锁扩展槽 - 初始或拥有者
UPROPERTY(Replicated)
bool bExtendedSlotsUnlocked;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyInventoryComponent.cppvoid UMyInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 背包容量只发送一次
DOREPLIFETIME_CONDITION(UMyInventoryComponent, MaxSlots, COND_InitialOnly);
// 物品、金币、重量只有拥有者能看到
DOREPLIFETIME_CONDITION(UMyInventoryComponent, Items, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UMyInventoryComponent, Gold, COND_OwnerOnly);
DOREPLIFETIME_CONDITION(UMyInventoryComponent, CurrentWeight, COND_OwnerOnly);
// 扩展槽状态:初始发送给所有人,之后只发给拥有者
DOREPLIFETIME_CONDITION(UMyInventoryComponent, bExtendedSlotsUnlocked, COND_InitialOrOwner);
}🏆 案例4:比赛系统
CPP
// MyMatchState.hUCLASS()
class AMyMatchState : public AInfo
{
GENERATED_BODY()
public:
// ⏱️ 比赛时间 - 所有人都能看到
UPROPERTY(Replicated)
float MatchTime;
// 📊 比分 - 所有人都能看到
UPROPERTY(Replicated)
int32 TeamAScore;
UPROPERTY(Replicated)
int32 TeamBScore;
// 🎮 比赛阶段 - 所有人都能看到
UPROPERTY(Replicated)
EMatchPhase CurrentPhase;
// 📹 回放数据 - 只在回放时需要
UPROPERTY(Replicated)
TArray<FReplayKeyFrame> ReplayKeyFrames;
// 🔧 调试数据 - 永不复制(仅服务器使用)
UPROPERTY(Replicated)
FString DebugInfo;
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyMatchState.cppvoid AMyMatchState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 基本比赛信息 - 所有人都需要
DOREPLIFETIME(AMyMatchState, MatchTime);
DOREPLIFETIME(AMyMatchState, TeamAScore);
DOREPLIFETIME(AMyMatchState, TeamBScore);
DOREPLIFETIME(AMyMatchState, CurrentPhase);
// 回放数据 - 只在回放时发送
DOREPLIFETIME_CONDITION(AMyMatchState, ReplayKeyFrames, COND_ReplayOnly);
// 调试信息 - 永不复制
DOREPLIFETIME_CONDITION(AMyMatchState, DebugInfo, COND_Never);
}13.6 高级主题:动态条件与自定义条件
🎭 COND_Dynamic - 运行时改变条件
动态条件允许你在运行时改变属性的复制条件:
CPP
// MyDynamicActor.hUCLASS()
class AMyDynamicActor : public AActor
{
GENERATED_BODY()
public:
// 使用动态条件的属性
UPROPERTY(Replicated)
float DynamicData;
// 改变条件的方法
UFUNCTION(BlueprintCallable)
void SetDynamicDataCondition(ELifetimeCondition NewCondition);
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyDynamicActor.cppvoid AMyDynamicActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 声明为动态条件
DOREPLIFETIME_CONDITION(AMyDynamicActor, DynamicData, COND_Dynamic);
}
void AMyDynamicActor::SetDynamicDataCondition(ELifetimeCondition NewCondition){
if (HasAuthority())
{
// 使用 Iris 的 API 改变条件
// 这会触发 SetPropertyDynamicCondition
DOREPLIFETIME_CHANGE_CONDITION(AMyDynamicActor, DynamicData, NewCondition);
}
}Iris 内部实现:
CPP
// ReplicationConditionals.cppbool FReplicationConditionals::SetPropertyDynamicCondition(
FInternalNetRefIndex ObjectIndex,
const void* Owner,
uint16 RepIndex,
ELifetimeCondition Condition){
// 获取旧条件
const ELifetimeCondition OldCondition = GetDynamicCondition(ObjectIndex, RepIndex);
// 设置新条件
SetDynamicCondition(ObjectIndex, RepIndex, Condition);
// 如果条件变化可能导致属性从"不复制"变为"复制"
if (DynamicConditionChangeRequiresBaselineInvalidation(OldCondition, Condition))
{
// 标记属性为脏
MarkDirty(ReplicationStateHeader, MemberChangeMask, ChangeMaskDescriptor);
// 使基线失效(增量压缩需要重新建立基线)
BaselineInvalidationTracker->InvalidateBaselines(ObjectIndex,
BaselineInvalidationTracker->InvalidateBaselineForAllConnections);
}
return true;
}
// 判断条件变化是否需要使基线失效bool FReplicationConditionals::DynamicConditionChangeRequiresBaselineInvalidation(
ELifetimeCondition OldCondition,
ELifetimeCondition NewCondition) const{
// 如果旧条件可能导致属性不被复制
const bool OldConditionMayHaveBeenDisabled =
!(OldCondition == COND_None || OldCondition == COND_Dynamic);
// 如果新条件允许复制
const bool NewConditionMayBeEnabled = (NewCondition != COND_Never);
// 从"可能不复制"变为"可能复制"时需要使基线失效
return OldConditionMayHaveBeenDisabled && NewConditionMayBeEnabled;
}🔧 COND_Custom - 自定义激活控制
自定义条件允许你完全控制属性是否复制:
CPP
// MyCustomConditionActor.hUCLASS()
class AMyCustomConditionActor : public AActor
{
GENERATED_BODY()
public:
// 使用自定义条件的属性
UPROPERTY(Replicated)
float SecretValue;
UPROPERTY(Replicated)
TArray<int32> SecretArray;
// 控制是否激活
void SetSecretValueActive(bool bActive);
void SetSecretArrayActive(bool bActive);
virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};
// MyCustomConditionActor.cppvoid AMyCustomConditionActor::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
// 声明为自定义条件
DOREPLIFETIME_CONDITION(AMyCustomConditionActor, SecretValue, COND_Custom);
DOREPLIFETIME_CONDITION(AMyCustomConditionActor, SecretArray, COND_Custom);
}
void AMyCustomConditionActor::SetSecretValueActive(bool bActive){
// 使用宏设置激活状态
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyCustomConditionActor, SecretValue, bActive);
}
void AMyCustomConditionActor::SetSecretArrayActive(bool bActive){
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyCustomConditionActor, SecretArray, bActive);
}宏展开解析:
CPP
// DOREPLIFETIME_ACTIVE_OVERRIDE 宏展开#define DOREPLIFETIME_ACTIVE_OVERRIDE(c,v,active) \
{ \
static FProperty* sp##v = GetReplicatedProperty(StaticClass(), c::StaticClass(), \
GET_MEMBER_NAME_CHECKED(c,v)); \
UE::Net::Private::FNetPropertyConditionManager& PropertyConditionManager = \
UE::Net::Private::FNetPropertyConditionManager::Get(); \
for (int32 i = 0; i < sp##v->ArrayDim; i++) \
{ \
PropertyConditionManager.SetPropertyActiveOverride(this, sp##v->RepIndex + i, active); \
} \
}📊 Iris 中的自定义条件实现
CPP
// ReplicationConditionals.cppbool FReplicationConditionals::SetPropertyCustomCondition(
FInternalNetRefIndex ObjectIndex,
const void* Owner,
uint16 RepIndex,
bool bIsActive){
// 获取条件变化掩码
FNetBitArrayView ConditionalChangeMask =
GetMemberConditionalChangeMask(Fragment.ExternalSrcBuffer, StateDescriptor);
if (bIsActive)
{
// 激活:设置条件掩码位
ConditionalChangeMask.SetBits(ChangeMaskDescriptor.BitOffset, ChangeMaskDescriptor.BitCount);
// 同时标记属性为脏
FNetBitArrayView MemberChangeMask =
GetMemberChangeMask(Fragment.ExternalSrcBuffer, StateDescriptor);
MarkDirty(ReplicationStateHeader, MemberChangeMask, ChangeMaskDescriptor);
// 使基线失效
BaselineInvalidationTracker->InvalidateBaselines(ObjectIndex,
BaselineInvalidationTracker->InvalidateBaselineForAllConnections);
}
else
{
// 禁用:清除条件掩码位
ConditionalChangeMask.ClearBits(ChangeMaskDescriptor.BitOffset, ChangeMaskDescriptor.BitCount);
}
return true;
}🌐 COND_NetGroup - 子对象网络组条件
网络组条件用于控制子对象的复制,基于连接是否属于特定组:
CPP
// 注册子对象时使用 COND_NetGroupvoid AMyActor::RegisterSubObjectWithNetGroup(UObject* SubObject, FNetObjectGroupHandle GroupHandle){
if (UObjectReplicationBridge* Bridge = GetObjectReplicationBridge())
{
// 使用 COND_NetGroup 注册子对象
Bridge->AddSubObject(GetNetRefHandle(), SubObject, COND_NetGroup);
// 将子对象添加到网络组
Bridge->AddToGroup(GroupHandle, SubObject);
}
}Iris 内部处理:
CPP
// ReplicationConditionals.cpp - GetChildSubObjectsToReplicatevoid FReplicationConditionals::GetChildSubObjectsToReplicate(
uint32 ReplicatingConnectionId,
const FConditionalsMask& LifetimeConditionals,
const FInternalNetRefIndex ParentObjectIndex,
FSubObjectsToReplicateArray& OutSubObjectsToReplicate){
for (uint32 ArrayIndex = 0; ArrayIndex < SubObjectsInfo.NumSubObjects; ++ArrayIndex)
{
const FInternalNetRefIndex SubObjectIndex = SubObjectsInfo.ChildSubObjects[ArrayIndex];
const ELifetimeCondition LifeTimeCondition =
(ELifetimeCondition)SubObjectsInfo.SubObjectLifeTimeConditions[ArrayIndex];
if (LifeTimeCondition == COND_NetGroup)
{
// 检查连接是否属于子对象的网络组
bool bShouldReplicateSubObject = false;
const TArrayView<const FNetObjectGroupHandle::FGroupIndexType> GroupIndexes =
NetObjectGroups->GetGroupIndexesOfNetObject(SubObjectIndex);
for (const FNetObjectGroupHandle::FGroupIndexType GroupIndex : GroupIndexes)
{
const FNetObjectGroupHandle NetGroup = NetObjectGroups->GetHandleFromIndex(GroupIndex);
if (NetGroup.IsNetGroupOwnerNetObjectGroup())
{
// Owner 组
bShouldReplicateSubObject = LifetimeConditionals.IsConditionEnabled(COND_OwnerOnly);
}
else if (NetGroup.IsNetGroupReplayNetObjectGroup())
{
// Replay 组
bShouldReplicateSubObject = LifetimeConditionals.IsConditionEnabled(COND_ReplayOnly);
}
else
{
// 自定义子对象过滤组
ENetFilterStatus ReplicationStatus = ENetFilterStatus::Disallow;
ReplicationFiltering->GetSubObjectFilterStatus(NetGroup, ReplicatingConnectionId,
ReplicationStatus);
bShouldReplicateSubObject = ReplicationStatus != ENetFilterStatus::Disallow;
}
if (bShouldReplicateSubObject)
{
OutSubObjectsToReplicate.Add(SubObjectIndex);
break;
}
}
}
else if (LifetimeConditionals.IsConditionEnabled(LifeTimeCondition))
{
// 普通条件检查
OutSubObjectsToReplicate.Add(SubObjectIndex);
}
}
}13.7 性能优化与最佳实践
⚡ 性能影响分析
PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 条件复制性能影响 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 📊 CPU 开销: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ GetLifetimeConditionals() │ O(1) - 位操作,非常快 │ │
│ │ ApplyConditionalsToChangeMask│ O(n) - n=属性数量 │ │
│ │ SetPropertyCustomCondition │ O(1) - 单属性操作 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 💾 内存开销: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ FPerObjectInfo │ 2 bytes/对象 │ │
│ │ FConditionalsMask │ 2 bytes/对象/连接 │ │
│ │ DynamicConditions │ 仅使用 COND_Dynamic 时分配 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 📡 带宽节省: │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ COND_OwnerOnly │ 节省 (N-1)/N 的带宽(N=连接数) │ │
│ │ COND_InitialOnly │ 节省后续所有更新的带宽 │ │
│ │ COND_SkipOwner │ 节省 1/N 的带宽 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘✅ 最佳实践清单
1️⃣ 选择正确的条件
CPP
// ✅ 好的做法:根据数据特性选择条件DOREPLIFETIME_CONDITION(AMyActor, PrivateData, COND_OwnerOnly); // 敏感数据DOREPLIFETIME_CONDITION(AMyActor, StaticName, COND_InitialOnly); // 不变数据DOREPLIFETIME_CONDITION(AMyActor, PredictedPos, COND_SkipOwner); // 预测数据
// ❌ 不好的做法:所有数据都用 COND_NoneDOREPLIFETIME(AMyActor, PrivateData); // 浪费带宽,可能泄露敏感信息DOREPLIFETIME(AMyActor, StaticName); // 不必要的重复发送2️⃣ 避免频繁改变动态条件
CPP
// ❌ 不好的做法:每帧改变条件void AMyActor::Tick(float DeltaTime){
// 每帧都改变条件会导致频繁的基线失效
DOREPLIFETIME_CHANGE_CONDITION(AMyActor, SomeData,
bSomeCondition ? COND_OwnerOnly : COND_None);
}
// ✅ 好的做法:只在状态真正改变时更新void AMyActor::OnStateChanged(bool bNewState){
if (bNewState != bCachedState)
{
bCachedState = bNewState;
DOREPLIFETIME_CHANGE_CONDITION(AMyActor, SomeData,
bNewState ? COND_OwnerOnly : COND_None);
}
}3️⃣ 合理使用 COND_Custom
CPP
// ✅ 好的做法:在适当时机激活/禁用void AMyActor::OnBecomeRelevant(){
// 变得相关时激活
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyActor, DetailedData, true);
}
void AMyActor::OnBecomeIrrelevant(){
// 不相关时禁用,节省带宽
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyActor, DetailedData, false);
}
// ❌ 不好的做法:频繁切换void AMyActor::Tick(float DeltaTime){
// 每帧切换会产生不必要的开销
DOREPLIFETIME_ACTIVE_OVERRIDE(AMyActor, DetailedData,
FMath::RandBool());
}4️⃣ 考虑子对象的条件
CPP
// ✅ 好的做法:子对象也使用适当的条件void AMyCharacter::RegisterInventorySubObject(UInventoryComponent* Inventory){
if (UObjectReplicationBridge* Bridge = GetObjectReplicationBridge())
{
// 背包只复制给拥有者
Bridge->AddSubObject(GetNetRefHandle(), Inventory, COND_OwnerOnly);
}
}
// ❌ 不好的做法:忽略子对象条件void AMyCharacter::RegisterInventorySubObject(UInventoryComponent* Inventory){
if (UObjectReplicationBridge* Bridge = GetObjectReplicationBridge())
{
// 默认复制给所有人,可能泄露敏感信息
Bridge->AddSubObject(GetNetRefHandle(), Inventory, COND_None);
}
}📊 条件选择决策树
PLAINTEXT
开始选择条件
│
├─ 数据是否敏感(如弹药、金币)?
│ ├─ 是 → COND_OwnerOnly
│ └─ 否 ↓
│
├─ 数据是否永不改变(如名字、皮肤)?
│ ├─ 是 → COND_InitialOnly
│ └─ 否 ↓
│
├─ 本地玩家是否有预测(如位置)?
│ ├─ 是 → COND_SkipOwner
│ └─ 否 ↓
│
├─ 是否只有控制者需要(如输入状态)?
│ ├─ 是 → COND_AutonomousOnly
│ └─ 否 ↓
│
├─ 是否只有旁观者需要(如平滑位置)?
│ ├─ 是 → COND_SimulatedOnly
│ └─ 否 ↓
│
├─ 是否涉及物理复制?
│ ├─ 是 → COND_SimulatedOrPhysics
│ └─ 否 ↓
│
├─ 是否需要运行时控制?
│ ├─ 是,完全控制 → COND_Custom
│ ├─ 是,改变条件 → COND_Dynamic
│ └─ 否 ↓
│
├─ 是否只在回放时需要?
│ ├─ 是 → COND_ReplayOnly
│ └─ 否 ↓
│
├─ 是否永远不需要复制?
│ ├─ 是 → COND_Never
│ └─ 否 → COND_None
🐛 常见问题排查
问题 | 可能原因 | 解决方案 |
|---|---|---|
属性没有复制 | 条件不满足 | 检查 Owner/Role 关系 |
敏感数据泄露 | 使用了 COND_None | 改用 COND_OwnerOnly |
初始数据重复发送 | 未使用 COND_InitialOnly | 添加 InitialOnly 条件 |
预测抖动 | 服务器位置覆盖本地预测 | 使用 COND_SkipOwner |
动态条件不生效 | 未正确设置 | 检查 DOREPLIFETIME_CHANGE_CONDITION |
自定义条件无效 | 未初始化 | 确保调用 InitPropertyCustomConditions |
13.8 总结
🎯 核心概念回顾
PLAINTEXT
┌─────────────────────────────────────────────────────────────────┐
│ 条件复制系统总结 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 📋 ELifetimeCondition - 属性级条件 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • COND_None - 无条件,始终复制 │ │
│ │ • COND_OwnerOnly - 仅拥有者 │ │
│ │ • COND_SkipOwner - 跳过拥有者 │ │
│ │ • COND_InitialOnly - 仅初始状态 │ │
│ │ • COND_SimulatedOnly- 仅模拟代理 │ │
│ │ • COND_AutonomousOnly- 仅自主代理 │ │
│ │ • COND_Custom - 自定义控制 │ │
│ │ • COND_Dynamic - 运行时改变 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 🔧 EReplicationCondition - 对象级条件 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • RoleAutonomous - 自主角色标识 │ │
│ │ • ReplicatePhysics - 物理复制标识 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
│ 🏗️ FReplicationConditionals - 核心管理类 │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ • GetLifetimeConditionals() - 计算条件掩码 │ │
│ │ • ApplyConditionalsToChangeMask() - 应用条件过滤 │ │
│ │ • SetPropertyCustomCondition() - 设置自定义条件 │ │
│ │ • SetPropertyDynamicCondition() - 设置动态条件 │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘✅ 关键要点
选择正确的条件:根据数据特性选择最合适的条件,既保护敏感数据又节省带宽
理解 Owner 和 Role:
Owner:谁拥有这个对象(通常是 PlayerController)
Role:Autonomous(控制者)vs Simulated(旁观者)
善用 InitialOnly:对于不变的数据(名字、皮肤等),使用 COND_InitialOnly 避免重复发送
保护敏感数据:弹药、金币、技能冷却等使用 COND_OwnerOnly
支持本地预测:对于有客户端预测的数据(位置、速度),使用 COND_SkipOwner
谨慎使用动态条件:频繁改变条件会导致基线失效,影响增量压缩效率
📚 相关文档
文档 | 内容 |
|---|---|
Part5_Filtering.md | 过滤系统 - 对象级过滤 |
Part6_Prioritization.md | 优先级系统 - 发送顺序控制 |
Part11_Polling_DirtyDetection.md | 脏数据检测 - 变化追踪 |
Part12_ObjectReference_Dependency.md | 对象引用 - 依赖管理 |
本文档基于 Unreal Engine 5.5.0 Iris 源代码分析
源码目录:Engine/Source/Runtime/Experimental/Iris/Core/Private/Iris/ReplicationSystem/Conditionals/