页面加载中
博客快捷键
按住 Shift 键查看可用快捷键
ShiftK
开启/关闭快捷键功能
ShiftA
打开/关闭中控台
ShiftD
深色/浅色显示模式
ShiftS
站内搜索
ShiftR
随机访问
ShiftH
返回首页
ShiftL
友链页面
ShiftP
关于本站
ShiftI
原版/本站右键菜单
松开 Shift 键或点击外部区域关闭
互动
最近评论
暂无评论
标签
寻找感兴趣的领域
暂无标签
    0
    文章
    0
    标签
    8
    分类
    10
    评论
    128
    功能
    深色模式
    标签
    JavaScript12TypeScript8React15Next.js6Vue10Node.js7CSS5前端20
    互动
    最近评论
    暂无评论
    标签
    寻找感兴趣的领域
    暂无标签
      0
      文章
      0
      标签
      8
      分类
      10
      评论
      128
      功能
      深色模式
      标签
      JavaScript12TypeScript8React15Next.js6Vue10Node.js7CSS5前端20
      随便逛逛
      博客分类
      文章标签
      复制地址
      深色模式
      AnHeYuAnHeYu
      Search⌘K
      博客
        暂无其他文档

        Iris 网络复制系统技术分析 - 第十一部分:轮询与脏数据检测

        December 28, 202540 分钟 阅读309 次阅读

        🔍 Iris 网络复制系统技术分析 - 第十一部分:轮询与脏数据检测

        📖 本章导读:想象你是一位繁忙餐厅的服务员,需要照顾100桌客人。你有两种工作方式:一种是每隔几分钟挨个检查每一桌(轮询模式),另一种是让客人需要服务时主动举手示意(推送模式)。Iris 的脏数据检测系统正是这两种策略的完美结合,本章将带你深入理解这个高效的"服务员"是如何工作的!


        🎯 11.1 脏数据检测机制概述

        💡 11.1.1 什么是"脏数据"?

        在网络复制的世界里,"脏数据"(Dirty Data) 指的是自上次同步以来发生了变化的数据。

        PLAINTEXT
        🎮 日常类比:快递追踪系统
        
        想象你网购了一件商品,快递公司需要实时更新物流信息:
        
        📦 商品状态变化历程:
           ┌─────────────────────────────────────────────────────────┐
           │  时间点    │    状态      │   是否需要通知客户(脏?)    │
           ├─────────────────────────────────────────────────────────┤
           │  Day 1     │   已发货     │      ✅ 是(状态变了)        │
           │  Day 1.5   │   已发货     │      ❌ 否(状态没变)        │
           │  Day 2     │   运输中     │      ✅ 是(状态变了)        │
           │  Day 3     │   派送中     │      ✅ 是(状态变了)        │
           │  Day 3.5   │   派送中     │      ❌ 否(状态没变)        │
           │  Day 4     │   已签收     │      ✅ 是(状态变了)        │
           └─────────────────────────────────────────────────────────┘
        
        只有状态发生变化时,才需要发送通知 = 只有数据变"脏"时,才需要网络同步!

        🔄 11.1.2 Poll 模式 vs Push 模式

        Iris 支持两种脏数据检测策略,各有优劣:

        PLAINTEXT
        ┌──────────────────────────────────────────────────────────────────────┐
        │                    🔍 脏数据检测的两种模式                            │
        ├──────────────────────────────────────────────────────────────────────┤
        │                                                                      │
        │   📊 Poll 模式(轮询模式)              📢 Push 模式(推送模式)       │
        │   ════════════════════                 ════════════════════          │
        │                                                                      │
        │   🏠 类比:保安巡逻                     🔔 类比:门铃报警              │
        │                                                                      │
        │   ┌─────────────┐                      ┌─────────────┐               │
        │   │   保安室    │                      │   监控室    │               │
        │   │  ┌─────┐   │                      │  ┌─────┐   │               │
        │   │  │ 👮  │   │ 定时巡逻             │  │ 👨‍💻 │   │ 等待报警        │
        │   │  └─────┘   │ ────────►           │  └─────┘   │ ◄────────      │
        │   └─────────────┘   每帧检查           └─────────────┘  主动通知      │
        │         │                                    ▲                       │
        │         ▼                                    │                       │
        │   ┌─────────────┐                      ┌─────────────┐               │
        │   │ 🏠🏠🏠🏠🏠 │                      │ 🔔🔔🔔🔔🔔 │               │
        │   │ 挨个检查    │                      │ 有变化才响  │               │
        │   └─────────────┘                      └─────────────┘               │
        │                                                                      │
        │   ✅ 优点:简单可靠                    ✅ 优点:高效省资源             │
        │   ❌ 缺点:CPU 开销大                  ❌ 缺点:需要代码配合           │
        │                                                                      │
        └──────────────────────────────────────────────────────────────────────┘

        特性

        🔍 Poll 模式

        📢 Push 模式

        工作方式

        系统主动检查每个属性

        代码主动通知系统

        CPU 开销

        较高(需要比较所有属性)

        较低(只处理变化的)

        代码侵入性

        无(自动工作)

        需要手动标记脏属性

        适用场景

        属性变化频繁的对象

        属性变化稀少的对象

        可靠性

        100% 可靠

        依赖开发者正确标记

        🏗️ 11.1.3 Iris 的混合策略

        Iris 采用智能混合策略,结合两种模式的优点:

        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐│                      🧠 Iris 智能脏数据检测流程                         │├────────────────────────────────────────────────────────────────────────┤│                                                                        ││   游戏对象属性变化                                                       ││         │                                                              ││         ▼                                                              ││   ┌─────────────────────────────────────┐                              ││   │  是否启用 Push Model?               │                              ││   │  (net.Iris.PushModelMode)           │                              ││   └─────────────────────────────────────┘                              ││         │                     │                                        ││    Yes  │                     │  No                                    ││         ▼                     ▼                                        ││   ┌───────────────┐    ┌───────────────┐                               ││   │ Push Model    │    │ Force Poll    │                               ││   │ 检查脏标记    │    │ 强制比较属性   │                               ││   └───────────────┘    └───────────────┘                               ││         │                     │                                        ││         │     ┌───────────────┘                                        ││         ▼     ▼                                                        ││   ┌─────────────────────────────────────┐                              ││   │        对象被标记为脏?              │                              ││   └─────────────────────────────────────┘                              ││         │                     │                                        ││    Yes  │                     │  No                                    ││         ▼                     ▼                                        ││   ┌───────────────┐    ┌───────────────┐                               ││   │ 加入脏对象    │    │ 跳过本次复制   │                               ││   │ 列表等待复制  │    │ 节省带宽      │                               ││   └───────────────┘    └───────────────┘                               ││                                                                        │└────────────────────────────────────────────────────────────────────────┘

        📂 11.1.4 关键源文件索引

        文件

        路径

        职责

        DirtyNetObjectTracker.h/cpp

        Iris/Core/Private/Iris/ReplicationSystem/

        Iris 脏对象追踪器

        GlobalDirtyNetObjectTracker.h/cpp

        Net/Core/Public/Net/Core/DirtyNetObjectTracker/

        全局脏对象追踪器

        ObjectPoller.h/cpp

        Iris/Core/Private/Iris/ReplicationSystem/Polling/

        对象轮询器

        ObjectPollFrequencyLimiter.h/cpp

        Iris/Core/Private/Iris/ReplicationSystem/

        轮询频率限制器


        📊 11.2 DirtyNetObjectTracker(脏对象追踪器)

        💡 11.2.1 追踪器的职责

        FDirtyNetObjectTracker 是 Iris 系统中负责追踪哪些对象需要复制的核心组件。

        PLAINTEXT
        🎮 日常类比:待办事项清单
        
        想象你有一个智能待办事项 App:
        - 📝 当你添加新任务时,它会自动出现在"待处理"列表- ✅ 当你完成任务时,它会从列表中移除- 🔄 每天早上,App 会告诉你今天需要处理哪些任务
        
        DirtyNetObjectTracker 就是网络复制的"待办事项清单":
        - 📝 对象变脏 → 加入脏对象列表- ✅ 复制完成 → 从列表中移除- 🔄 每帧开始 → 告诉系统哪些对象需要复制

        🏗️ 11.2.2 核心数据结构

        CPP
        // 源码位置:DirtyNetObjectTracker.hclass FDirtyNetObjectTracker
        {
        private:
            // 🎯 三个关键的位数组(BitArray)
            
            // 1️⃣ 累积脏对象:记录自上次更新以来所有变脏的对象
            FNetBitArray AccumulatedDirtyNetObjects;
            
            // 2️⃣ 当前帧脏对象:本帧需要处理的脏对象
            FNetBitArray DirtyNetObjects;
            
            // 3️⃣ 强制更新对象:被 ForceNetUpdate 标记的对象
            FNetBitArray ForceNetUpdateObjects;
            
            // 📊 全局脏对象追踪器的引用
            FGlobalDirtyNetObjectTracker* GlobalDirtyNetObjectTracker;
            
            // 🆔 复制系统 ID(支持 PIE 多实例)
            uint32 ReplicationSystemId;
        };
        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    📊 三个位数组的关系图解                               │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │   AccumulatedDirtyNetObjects(累积脏对象)                               │
        │   ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐                                   │
        │   │1│0│1│1│0│0│1│0│0│1│0│0│1│0│1│0│  ← 持续累积,直到被消费             │
        │   └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘                                   │
        │                    │                                                   │
        │                    │ Update() 时合并                                   │
        │                    ▼                                                   │
        │   DirtyNetObjects(当前帧脏对象)                                        │
        │   ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐                                   │
        │   │1│0│1│1│0│0│1│0│0│1│0│0│1│0│1│0│  ← 本帧需要处理的对象               │
        │   └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘                                   │
        │                    │                                                   │
        │                    │ OR 运算合并                                        │
        │                    ▼                                                   │
        │   ForceNetUpdateObjects(强制更新对象)                                  │
        │   ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐                                   │
        │   │0│0│0│0│1│0│0│0│0│0│0│1│0│0│0│0│  ← ForceNetUpdate() 标记的对象     │
        │   └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘                                   │
        │                                                                        │
        │   最终需要复制的对象 = DirtyNetObjects | ForceNetUpdateObjects          │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        📝 11.2.3 核心 API 详解

        标记对象为脏

        CPP
        // 源码位置:DirtyNetObjectTracker.h
        
        /**
         * 🎯 标记网络对象状态为脏
         * 
         * @param NetRefHandle - 网络对象句柄
         * @param ReplicationSystemId - 复制系统 ID
         * 
         * 💡 这是一个全局函数,可以从任何地方调用
         * ⚠️ 线程安全:使用原子操作,支持多线程调用
         */IRISCORE_API void MarkNetObjectStateDirty(
            FNetRefHandle NetRefHandle, 
            uint32 ReplicationSystemId
        );
        
        /**
         * 🎯 强制网络更新
         * 
         * 即使对象没有变脏,也强制在下一帧复制
         * 常用于:休眠唤醒、重要事件触发等场景
         */IRISCORE_API void ForceNetUpdate(
            FNetRefHandle NetRefHandle, 
            uint32 ReplicationSystemId
        );

        实际使用示例

        CPP
        // 🎮 游戏代码示例:角色受伤时标记血量为脏
        
        void AMyCharacter::TakeDamage(float DamageAmount){
            // 更新血量
            Health -= DamageAmount;
            
            // 方式1:使用 Push Model 宏(推荐)
            MARK_PROPERTY_DIRTY_FROM_NAME(AMyCharacter, Health, this);
            
            // 方式2:直接调用 Iris API(底层方式)
            // MarkNetObjectStateDirty(GetNetRefHandle(), GetReplicationSystemId());
        }
        
        // 🎮 强制更新示例:Boss 出现时强制同步所有客户端
        
        void ABossEnemy::OnBossAppear(){
            // 即使没有属性变化,也强制同步
            ForceNetUpdate();  // Actor 的便捷方法
        }

        🔄 11.2.4 更新流程

        CPP
        // 源码位置:DirtyNetObjectTracker.cpp
        
        void FDirtyNetObjectTracker::UpdateDirtyNetObjects(){
            // 1️⃣ 从全局追踪器获取脏对象
            if (GlobalDirtyNetObjectTracker)
            {
                // 获取自上次以来所有变脏的对象
                GlobalDirtyNetObjectTracker->UpdateDirtyNetObjects(
                    ReplicationSystemId, 
                    AccumulatedDirtyNetObjects
                );
            }
            
            // 2️⃣ 合并累积的脏对象到当前帧
            // DirtyNetObjects |= AccumulatedDirtyNetObjects
            DirtyNetObjects.Combine(
                AccumulatedDirtyNetObjects, 
                FNetBitArray::OrOp
            );
            
            // 3️⃣ 清空累积数组,准备下一轮累积
            AccumulatedDirtyNetObjects.Reset();
            
            // 4️⃣ 合并强制更新的对象
            // DirtyNetObjects |= ForceNetUpdateObjects
            DirtyNetObjects.Combine(
                ForceNetUpdateObjects, 
                FNetBitArray::OrOp
            );
            
            // 5️⃣ 清空强制更新数组
            ForceNetUpdateObjects.Reset();
        }
        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    🔄 脏对象更新时序图                                   │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │   帧 N-1                          帧 N                                 │
        │   ─────────────────────────────────────────────────────────────        │
        │                                                                        │
        │   游戏逻辑修改属性                                                       │
        │        │                                                               │
        │        ▼                                                               │
        │   ┌─────────────────┐                                                  │
        │   │ MARK_PROPERTY   │                                                  │
        │   │ _DIRTY          │                                                  │
        │   └────────┬────────┘                                                  │
        │            │                                                           │
        │            ▼                                                           │
        │   ┌─────────────────┐                                                  │
        │   │ GlobalDirty     │  ← 全局追踪器记录                                 │
        │   │ NetObjectTracker│                                                  │
        │   └────────┬────────┘                                                  │
        │            │                                                           │
        │            │  ════════════════ 帧边界 ════════════════                 │
        │            │                                                           │
        │            ▼                                                           │
        │   ┌─────────────────┐                                                  │
        │   │ UpdateDirty     │  ← 复制系统更新阶段                               │
        │   │ NetObjects()    │                                                  │
        │   └────────┬────────┘                                                  │
        │            │                                                           │
        │            ▼                                                           │
        │   ┌─────────────────┐                                                  │
        │   │ 脏对象进入      │                                                   │
        │   │ 复制队列        │                                                   │
        │   └─────────────────┘                                                  │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        🌐 11.2.5 全局脏对象追踪器

        FGlobalDirtyNetObjectTracker 是一个跨 NetDriver 的全局追踪器,支持多个复制系统同时读取脏对象列表。

        CPP
        // 源码位置:GlobalDirtyNetObjectTracker.h
        
        class FGlobalDirtyNetObjectTracker
        {
        public:
            /**
             * 🎯 添加脏对象到全局列表
             * 
             * ⚠️ 线程安全:使用原子操作
             * 📊 支持多个 Poller 同时读取
             */
            void AddDirtyObject(uint32 ObjectIndex);
            
            /**
             * 🎯 获取脏对象列表并更新到目标数组
             * 
             * @param PollerId - 轮询器 ID
             * @param OutDirtyObjects - 输出的脏对象位数组
             */
            void UpdateDirtyNetObjects(
                uint32 PollerId, 
                FNetBitArray& OutDirtyObjects
            );
            
        private:
            // 🔒 原子位数组,支持多线程写入
            FNetBitArrayAtomic DirtyObjects;
            
            // 📊 每个 Poller 的读取状态
            TArray<FPollerState> PollerStates;
        };
        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    🌐 全局追踪器多 Poller 支持                          │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │                     GlobalDirtyNetObjectTracker                        │
        │                    ┌─────────────────────────┐                         │
        │                    │   DirtyObjects (原子)   │                         │
        │                    │ ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐  │                         │
        │                    │ │1│0│1│1│0│0│1│0│0│1│  │                         │
        │                    │ └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘  │                         │
        │                    └───────────┬─────────────┘                         │
        │                                │                                       │
        │           ┌────────────────────┼────────────────────┐                  │
        │           │                    │                    │                  │
        │           ▼                    ▼                    ▼                  │
        │   ┌───────────────┐   ┌───────────────┐   ┌───────────────┐           │
        │   │  Poller 0     │   │  Poller 1     │   │  Poller 2     │           │
        │   │  (主游戏)     │   │  (回放录制)   │   │  (观战系统)   │           │
        │   └───────────────┘   └───────────────┘   └───────────────┘           │
        │                                                                        │
        │   💡 每个 Poller 独立维护自己的"已读取"状态                              │
        │   💡 支持不同系统以不同频率消费脏对象                                    │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        📢 11.3 Push Model(推送模型)

        💡 11.3.1 什么是 Push Model?

        Push Model 是一种主动通知的脏数据检测机制。开发者在修改复制属性时,主动调用宏来通知网络系统"这个属性变了"。

        PLAINTEXT
        🎮 日常类比:微信消息 vs 邮件检查
        
        📱 Push Model = 微信消息
           - 有新消息时,手机立即推送通知
           - 你不需要反复打开 App 检查
           - 高效、省电、实时
        
        📧 Poll Model = 定时检查邮件
           - 每隔几分钟打开邮箱看看有没有新邮件
           - 即使没有新邮件也要检查
           - 简单但浪费资源

        ⚙️ 11.3.2 启用 Push Model

        CPP
        // 方式1:通过控制台变量// 在 DefaultEngine.ini 或运行时设置
        net.IsPushModelEnabled=true
        
        // 方式2:Iris 特定的 Push Model 模式// 在 DefaultEngine.ini 中
        [/Script/IrisCore.IrisSettings]
        PushModelMode=Enabled  // Disabled, Enabled, ForcePoll
        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    ⚙️ Push Model 模式对比                               │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │   模式              │ 行为                    │ 适用场景               │
        │   ─────────────────────────────────────────────────────────────────   │
        │   Disabled          │ 不使用 Push Model       │ 传统项目迁移           │
        │                     │ 所有对象强制轮询        │                        │
        │   ─────────────────────────────────────────────────────────────────   │
        │   Enabled (默认)    │ 智能混合模式            │ 大多数项目             │
        │                     │ 支持 Push 的用 Push     │                        │
        │                     │ 不支持的用 Poll         │                        │
        │   ─────────────────────────────────────────────────────────────────   │
        │   ForcePoll         │ 强制所有对象轮询        │ 调试、性能对比         │
        │                     │ 忽略 Push 标记          │                        │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        📝 11.3.3 MARK_PROPERTY_DIRTY 宏家族

        Push Model 提供了一系列宏来标记属性为脏:

        CPP
        // 源码位置:PushModel.h
        
        // ═══════════════════════════════════════════════════════════════════════// 🎯 核心宏:标记单个属性为脏// ═══════════════════════════════════════════════════════════════════════
        
        /**
         * MARK_PROPERTY_DIRTY_FROM_NAME
         * 
         * 最常用的宏,通过类名和属性名标记属性为脏
         * ✅ 编译时检查:类名或属性名错误会编译失败
         * ✅ 高效:使用编译时常量而非运行时查找
         */#define MARK_PROPERTY_DIRTY_FROM_NAME(ClassName, PropertyName, Object)
        
        // 使用示例void AMyCharacter::SetHealth(float NewHealth){
            Health = NewHealth;
            MARK_PROPERTY_DIRTY_FROM_NAME(AMyCharacter, Health, this);
        }
        
        // ═══════════════════════════════════════════════════════════════════════// 🎯 静态数组宏:标记数组单个元素// ═══════════════════════════════════════════════════════════════════════
        
        /**
         * MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY_INDEX
         * 
         * 用于静态数组(C风格数组)的单个元素
         */#define MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY_INDEX(
            ClassName, PropertyName, ArrayIndex, Object)
        
        // 使用示例void AMyCharacter::SetInventorySlot(int32 Index, UItem* Item){
            InventorySlots[Index] = Item;
            MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY_INDEX(
                AMyCharacter, InventorySlots, Index, this);
        }
        
        // ═══════════════════════════════════════════════════════════════════════// 🎯 静态数组宏:标记整个数组// ═══════════════════════════════════════════════════════════════════════
        
        /**
         * MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY
         * 
         * 用于标记整个静态数组为脏
         */#define MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY(
            ClassName, PropertyName, Object)
        
        // 使用示例void AMyCharacter::ResetInventory(){
            FMemory::Memzero(InventorySlots, sizeof(InventorySlots));
            MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY(
                AMyCharacter, InventorySlots, this);
        }
        
        // ═══════════════════════════════════════════════════════════════════════// 🎯 便捷宏:比较并标记// ═══════════════════════════════════════════════════════════════════════
        
        /**
         * COMPARE_ASSIGN_AND_MARK_PROPERTY_DIRTY
         * 
         * 只有值真的变化时才标记为脏
         * 适用于简单类型(int, float, pointer)
         */#define COMPARE_ASSIGN_AND_MARK_PROPERTY_DIRTY(
            ClassName, PropertyName, NewValue, Object)
        
        // 使用示例void AMyCharacter::SetLevel(int32 NewLevel){
            // 只有 Level 真的变化时才标记脏
            COMPARE_ASSIGN_AND_MARK_PROPERTY_DIRTY(
                AMyCharacter, Level, NewLevel, this);
        }

        🏗️ 11.3.4 完整的 Push Model 使用示例

        CPP
        // ═══════════════════════════════════════════════════════════════════════// 📁 MyCharacter.h - 头文件// ═══════════════════════════════════════════════════════════════════════
        
        #pragma once
        
        #include "CoreMinimal.h"#include "GameFramework/Character.h"#include "MyCharacter.generated.h"
        
        USTRUCT()
        struct FCharacterStats
        {
            GENERATED_BODY()
            
            UPROPERTY()
            int32 Strength = 10;
            
            UPROPERTY()
            int32 Agility = 10;
            
            UPROPERTY()
            int32 Intelligence = 10;
        };
        
        UCLASS()
        class MYGAME_API AMyCharacter : public ACharacter
        {
            GENERATED_BODY()
            
        public:
            // ════════════════════════════════════════════════════════════════
            // 🎯 Setter 方法(推荐方式)
            // ════════════════════════════════════════════════════════════════
            
            /** 设置血量 - 简单类型 */
            void SetHealth(float NewHealth);
            
            /** 设置等级 - 使用比较优化 */
            void SetLevel(int32 NewLevel);
            
            /** 设置角色属性 - 结构体类型 */
            void SetStats(const FCharacterStats& NewStats);
            
            /** 修改单个属性值 */
            void ModifyStrength(int32 Delta);
            
            /** 设置装备槽 - 静态数组单元素 */
            void SetEquipmentSlot(int32 SlotIndex, UItem* Item);
            
            /** 清空所有装备 - 静态数组整体 */
            void ClearAllEquipment();
            
            // ════════════════════════════════════════════════════════════════
            // 🎯 Getter 方法(返回引用时的特殊处理)
            // ════════════════════════════════════════════════════════════════
            
            /** 获取属性的可变引用 - ⚠️ 需要预先标记脏 */
            FCharacterStats& GetStats_Mutable();
            
            /** 获取属性的只读引用 - ✅ 安全 */
            const FCharacterStats& GetStats() const { return Stats; }
            
        protected:
            virtual void GetLifetimeReplicatedProps(
                TArray<FLifetimeProperty>& OutLifetimeProps) const override;
            
        private:
            // ════════════════════════════════════════════════════════════════
            // 📊 复制属性
            // ════════════════════════════════════════════════════════════════
            
            UPROPERTY(Replicated)
            float Health = 100.0f;
            
            UPROPERTY(Replicated)
            int32 Level = 1;
            
            UPROPERTY(Replicated)
            FCharacterStats Stats;
            
            UPROPERTY(Replicated)
            UItem* EquipmentSlots[6];  // 静态数组:头、胸、手、腿、脚、武器
        };
        
        // ═══════════════════════════════════════════════════════════════════════// 📁 MyCharacter.cpp - 实现文件// ═══════════════════════════════════════════════════════════════════════
        
        #include "MyCharacter.h"#include "Net/UnrealNetwork.h"#include "Net/Core/PushModel/PushModel.h"
        
        void AMyCharacter::GetLifetimeReplicatedProps(
            TArray<FLifetimeProperty>& OutLifetimeProps) const{
            Super::GetLifetimeReplicatedProps(OutLifetimeProps);
            
            // 🎯 关键:设置 bIsPushBased = true 启用 Push Model
            FDoRepLifetimeParams Params;
            Params.bIsPushBased = true;
            
            DOREPLIFETIME_WITH_PARAMS_FAST(AMyCharacter, Health, Params);
            DOREPLIFETIME_WITH_PARAMS_FAST(AMyCharacter, Level, Params);
            DOREPLIFETIME_WITH_PARAMS_FAST(AMyCharacter, Stats, Params);
            DOREPLIFETIME_WITH_PARAMS_FAST(AMyCharacter, EquipmentSlots, Params);
        }
        
        // ════════════════════════════════════════════════════════════════════════// 🎯 简单类型 Setter// ════════════════════════════════════════════════════════════════════════
        
        void AMyCharacter::SetHealth(float NewHealth){
            Health = NewHealth;
            
            // ✅ 标记 Health 属性为脏
            MARK_PROPERTY_DIRTY_FROM_NAME(AMyCharacter, Health, this);
        }
        
        // ════════════════════════════════════════════════════════════════════════// 🎯 使用比较优化的 Setter// ════════════════════════════════════════════════════════════════════════
        
        void AMyCharacter::SetLevel(int32 NewLevel){
            // ✅ 只有值变化时才标记脏,避免不必要的网络流量
            COMPARE_ASSIGN_AND_MARK_PROPERTY_DIRTY(AMyCharacter, Level, NewLevel, this);
        }
        
        // ════════════════════════════════════════════════════════════════════════// 🎯 结构体类型 Setter// ════════════════════════════════════════════════════════════════════════
        
        void AMyCharacter::SetStats(const FCharacterStats& NewStats){
            Stats = NewStats;
            
            // ✅ 标记整个结构体为脏
            // ⚠️ 注意:即使只改了一个字段,也要标记整个结构体
            MARK_PROPERTY_DIRTY_FROM_NAME(AMyCharacter, Stats, this);
        }
        
        void AMyCharacter::ModifyStrength(int32 Delta){
            Stats.Strength += Delta;
            
            // ✅ 修改结构体内部字段,仍然标记整个结构体
            MARK_PROPERTY_DIRTY_FROM_NAME(AMyCharacter, Stats, this);
        }
        
        // ════════════════════════════════════════════════════════════════════════// 🎯 静态数组 Setter// ════════════════════════════════════════════════════════════════════════
        
        void AMyCharacter::SetEquipmentSlot(int32 SlotIndex, UItem* Item){
            if (SlotIndex >= 0 && SlotIndex < 6)
            {
                EquipmentSlots[SlotIndex] = Item;
                
                // ✅ 只标记改变的那个槽位
                MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY_INDEX(
                    AMyCharacter, EquipmentSlots, SlotIndex, this);
            }
        }
        
        void AMyCharacter::ClearAllEquipment(){
            for (int32 i = 0; i < 6; ++i)
            {
                EquipmentSlots[i] = nullptr;
            }
            
            // ✅ 标记整个数组为脏
            MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY(
                AMyCharacter, EquipmentSlots, this);
        }
        
        // ════════════════════════════════════════════════════════════════════════// 🎯 返回可变引用的 Getter(特殊处理)// ════════════════════════════════════════════════════════════════════════
        
        FCharacterStats& AMyCharacter::GetStats_Mutable(){
            // ⚠️ 重要:返回引用前先标记脏
            // 因为调用者可能会修改返回的引用
            MARK_PROPERTY_DIRTY_FROM_NAME(AMyCharacter, Stats, this);
            return Stats;
        }

        ⚠️ 11.3.5 Push Model 使用注意事项

        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    ⚠️ Push Model 常见陷阱与解决方案                     │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │   ❌ 陷阱 1:忘记标记脏                                                 │
        │   ──────────────────────────────────────────────────────────────────  │
        │   void SetHealth(float NewHealth)                                      │
        │   {                                                                    │
        │       Health = NewHealth;                                              │
        │       // 💀 忘记调用 MARK_PROPERTY_DIRTY!                              │
        │       // 结果:客户端永远收不到新血量                                    │
        │   }                                                                    │
        │                                                                        │
        │   ✅ 解决:始终在 Setter 中调用 MARK_PROPERTY_DIRTY                     │
        │                                                                        │
        │   ──────────────────────────────────────────────────────────────────  │
        │                                                                        │
        │   ❌ 陷阱 2:直接访问属性绕过 Setter                                    │
        │   ──────────────────────────────────────────────────────────────────  │
        │   // 在某处代码中...                                                    │
        │   MyCharacter->Health = 50.0f;  // 💀 直接赋值,没有标记脏!            │
        │                                                                        │
        │   ✅ 解决:将属性设为 private,强制使用 Setter                          │
        │                                                                        │
        │   ──────────────────────────────────────────────────────────────────  │
        │                                                                        │
        │   ❌ 陷阱 3:持有引用后修改                                             │
        │   ──────────────────────────────────────────────────────────────────  │
        │   FCharacterStats& Stats = Character->GetStats_Mutable();              │
        │   // ... 很久之后 ...                                                   │
        │   Stats.Strength = 100;  // 💀 此时修改不会被检测到!                   │
        │                                                                        │
        │   ✅ 解决:                                                             │
        │   1. 避免持有引用                                                       │
        │   2. 每次修改后手动调用 MARK_PROPERTY_DIRTY                             │
        │   3. 使用 Setter 方法而非直接修改引用                                   │
        │                                                                        │
        │   ──────────────────────────────────────────────────────────────────  │
        │                                                                        │
        │   ❌ 陷阱 4:在非服务器端标记脏                                         │
        │   ──────────────────────────────────────────────────────────────────  │
        │   void AMyCharacter::ClientOnlyFunction()                              │
        │   {                                                                    │
        │       Health = 100.0f;                                                 │
        │       MARK_PROPERTY_DIRTY_FROM_NAME(...);  // 💀 客户端调用无意义       │
        │   }                                                                    │
        │                                                                        │
        │   ✅ 解决:只在服务器端(HasAuthority)调用 MARK_PROPERTY_DIRTY         │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        🔄 11.3.6 LegacyPushModel 兼容层

        Iris 提供了 FNetHandleLegacyPushModelHelper 来桥接传统 Push Model 和 Iris 系统:

        CPP
        // 源码位置:LegacyPushModel.h
        
        /**
         * 🔄 Legacy Push Model 兼容层
         * 
         * 将传统的 Push Model 脏标记转换为 Iris 的脏对象追踪
         */class FNetHandleLegacyPushModelHelper
        {
        public:
            /**
             * 初始化 Push Model 支持
             */
            void InitPushModel(uint32 InReplicationSystemId);
            
            /**
             * 设置对象的 Push ID
             */
            void SetNetPushID(
                UObject* Object, 
                FNetRefHandle NetRefHandle,
                const FReplicationProtocol* Protocol
            );
            
            /**
             * 清除对象的 Push ID
             */
            void ClearNetPushID(UObject* Object);
            
        private:
            uint32 ReplicationSystemId = 0;
        };

        📊 11.3.7 性能优势分析

        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐│                    📊 Push Model vs Poll Model 性能对比                 │├────────────────────────────────────────────────────────────────────────┤│                                                                        ││   测试场景:1000 个 Actor,每个有 20 个复制属性                          ││   每帧只有 5% 的属性发生变化                                            ││                                                                        ││   ┌─────────────────────────────────────────────────────────────────┐ ││   │                     CPU 时间对比(微秒/帧)                      │ ││   ├─────────────────────────────────────────────────────────────────┤ ││   │                                                                 │ ││   │   Poll Model:  ████████████████████████████████████  2000 μs   │ ││   │                检查 1000 × 20 = 20000 个属性                    │ ││   │                                                                 │ ││   │   Push Model:  ████                                   200 μs   │ ││   │                只处理 1000 个脏对象标记                          │ ││   │                                                                 │ ││   │   性能提升:    ~10x 🚀                                          │ ││   │                                                                 │ ││   └─────────────────────────────────────────────────────────────────┘ ││                                                                        ││   💡 结论:                                                            ││   - 属性变化越少,Push Model 优势越大                                   ││   - 对象数量越多,Push Model 优势越大                                   ││   - 适合:大世界游戏、MMO、属性相对稳定的对象                            ││                                                                        │└────────────────────────────────────────────────────────────────────────┘

        🔄 11.4 ObjectPoller(对象轮询器)

        💡 11.4.1 轮询器的职责

        FObjectPoller 负责执行对象的轮询阶段,检测哪些对象的属性发生了变化。

        PLAINTEXT
        🎮 日常类比:图书馆管理员
        
        想象一位图书馆管理员需要检查哪些书被借出或归还:
        
        📚 传统方式(强制轮询):
           - 每天检查每一本书的状态
           - 无论书有没有被动过都要检查
           - 工作量大,但绝对可靠
        
        📱 现代方式(Push Model):
           - 借还书时系统自动记录
           - 只检查有记录变动的书
           - 高效,但依赖系统正确记录
        
        🎯 FObjectPoller 就是这位管理员,它会:
           - 根据配置选择检查方式
           - 记录哪些"书"(对象)需要更新
           - 生成变化掩码(ChangeMask)

        🏗️ 11.4.2 核心结构

        CPP
        // 源码位置:ObjectPoller.h
        
        class FObjectPoller
        {
        public:
            /**
             * 🎯 轮询并复制对象
             * 
             * 这是轮询阶段的主入口,处理所有需要轮询的对象
             */
            void PollAndCopyObjects(FPollAndCopyObjectsParams& Params);
            
            /**
             * 🎯 轮询单个对象
             * 
             * 用于需要立即轮询特定对象的场景
             */
            void PollAndCopySingleObject(
                FNetRefHandle RefHandle,
                FReplicationInstanceProtocol* InstanceProtocol
            );
            
        private:
            // 📊 轮询参数
            struct FPollAndCopyObjectsParams
            {
                // 需要轮询的对象列表
                const FNetBitArray& ObjectsToCompare;
                
                // 脏对象追踪器
                FDirtyNetObjectTracker& DirtyNetObjectTracker;
                
                // 复制系统引用
                UReplicationSystem* ReplicationSystem;
            };
        };

        🔄 11.4.3 轮询流程详解

        CPP
        // 源码位置:ObjectPoller.cpp
        
        void FObjectPoller::PollAndCopyObjects(FPollAndCopyObjectsParams& Params){
            // 遍历所有需要轮询的对象
            for (uint32 ObjectIndex : Params.ObjectsToCompare)
            {
                FNetRefHandle RefHandle = GetNetRefHandle(ObjectIndex);
                
                // 获取对象的复制协议实例
                FReplicationInstanceProtocol* InstanceProtocol = 
                    GetInstanceProtocol(RefHandle);
                
                if (!InstanceProtocol)
                {
                    continue;
                }
                
                // 🎯 根据 Push Model 配置选择轮询策略
                if (IsIrisPushModelEnabled())
                {
                    // Push Model 模式:只轮询脏对象或周期到期的对象
                    PushModelPollObject(RefHandle, InstanceProtocol, Params);
                }
                else
                {
                    // 强制轮询模式:比较所有属性
                    ForcePollObject(RefHandle, InstanceProtocol, Params);
                }
            }
        }
        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    🔄 轮询流程图解                                       │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │   PollAndCopyObjects()                                                 │
        │         │                                                              │
        │         ▼                                                              │
        │   ┌─────────────────────────────────────┐                              │
        │   │  遍历 ObjectsToCompare 中的对象     │                              │
        │   └─────────────────┬───────────────────┘                              │
        │                     │                                                  │
        │                     ▼                                                  │
        │   ┌─────────────────────────────────────┐                              │
        │   │  IsIrisPushModelEnabled()?          │                              │
        │   └─────────────────┬───────────────────┘                              │
        │            ┌────────┴────────┐                                         │
        │            │                 │                                         │
        │       Yes  ▼            No   ▼                                         │
        │   ┌─────────────┐    ┌─────────────┐                                   │
        │   │ PushModel   │    │ ForcePoll   │                                   │
        │   │ PollObject  │    │ Object      │                                   │
        │   └──────┬──────┘    └──────┬──────┘                                   │
        │          │                  │                                          │
        │          ▼                  ▼                                          │
        │   ┌─────────────────────────────────────┐                              │
        │   │  检查对象是否有脏属性                │                              │
        │   └─────────────────┬───────────────────┘                              │
        │                     │                                                  │
        │            ┌────────┴────────┐                                         │
        │            │                 │                                         │
        │       有   ▼            无   ▼                                         │
        │   ┌─────────────┐    ┌─────────────┐                                   │
        │   │ 比较属性    │    │ 跳过对象    │                                   │
        │   │ 生成掩码    │    │ 节省 CPU    │                                   │
        │   └──────┬──────┘    └─────────────┘                                   │
        │          │                                                             │
        │          ▼                                                             │
        │   ┌─────────────────────────────────────┐                              │
        │   │  将变化的属性记录到 ChangeMask      │                              │
        │   └─────────────────────────────────────┘                              │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        📝 11.4.4 两种轮询策略实现

        强制轮询(ForcePoll)

        CPP
        // 源码位置:ObjectPoller.cpp
        
        void FObjectPoller::ForcePollObject(
            FNetRefHandle RefHandle,
            FReplicationInstanceProtocol* InstanceProtocol,
            FPollAndCopyObjectsParams& Params){
            // 获取复制片段列表
            const TArray<FReplicationFragment*>& Fragments = 
                InstanceProtocol->GetFragments();
            
            // 🔍 遍历所有复制片段
            for (FReplicationFragment* Fragment : Fragments)
            {
                // 获取源数据(游戏对象的当前状态)
                const uint8* SourceData = Fragment->GetSourceData();
                
                // 获取量化状态(上次发送的状态)
                uint8* QuantizedState = Fragment->GetQuantizedState();
                
                // 🎯 比较并复制变化的数据
                // 这会更新 ChangeMask 来记录哪些属性变化了
                Fragment->PollAndCopy(SourceData, QuantizedState);
            }
            
            // 如果有任何属性变化,标记对象为脏
            if (InstanceProtocol->HasAnyChanges())
            {
                Params.DirtyNetObjectTracker.MarkObjectDirty(RefHandle);
            }
        }

        Push Model 轮询

        CPP
        // 源码位置:ObjectPoller.cpp
        
        void FObjectPoller::PushModelPollObject(
            FNetRefHandle RefHandle,
            FReplicationInstanceProtocol* InstanceProtocol,
            FPollAndCopyObjectsParams& Params){
            // 🎯 检查对象是否被标记为脏
            const bool bIsDirty = Params.DirtyNetObjectTracker.IsObjectDirty(RefHandle);
            
            // 🎯 检查是否到了周期轮询时间
            const bool bPollPeriodExpired = 
                Params.PollFrequencyLimiter.IsPollPeriodExpired(RefHandle);
            
            // 只有脏对象或周期到期的对象才需要轮询
            if (!bIsDirty && !bPollPeriodExpired)
            {
                // ✅ 跳过轮询,节省 CPU
                return;
            }
            
            // 获取复制片段列表
            const TArray<FReplicationFragment*>& Fragments = 
                InstanceProtocol->GetFragments();
            
            // 🔍 只轮询标记为脏的片段
            for (FReplicationFragment* Fragment : Fragments)
            {
                // 检查片段是否支持 Push Model
                if (Fragment->SupportsPushModel())
                {
                    // 获取脏属性掩码
                    const FChangeMask& DirtyMask = Fragment->GetDirtyMask();
                    
                    // 只比较脏属性
                    Fragment->PollAndCopyDirtyProperties(DirtyMask);
                }
                else
                {
                    // 不支持 Push Model 的片段,强制比较所有属性
                    Fragment->PollAndCopy();
                }
            }
        }

        ⏱️ 11.5 ObjectPollFrequencyLimiter(轮询频率限制器)

        💡 11.5.1 频率限制的目的

        FObjectPollFrequencyLimiter 用于控制对象的轮询频率,避免每帧都轮询所有对象。

        PLAINTEXT
        🎮 日常类比:体检频率
        
        不同人群需要不同的体检频率:
        
        👴 老年人(高风险):每年体检 1 次
           → 对应:重要对象,每帧轮询
        
        👨 中年人(中风险):每 2 年体检 1 次
           → 对应:普通对象,每 2 帧轮询
        
        👶 年轻人(低风险):每 3 年体检 1 次
           → 对应:次要对象,每 4 帧轮询
        
        🎯 ObjectPollFrequencyLimiter 就是这个"体检调度系统":
           - 为每个对象设置轮询周期
           - 确保重要对象更频繁地被检查
           - 分散轮询负载,避免帧率波动

        🏗️ 11.5.2 核心数据结构

        CPP
        // 源码位置:ObjectPollFrequencyLimiter.h
        
        class FObjectPollFrequencyLimiter
        {
        public:
            /**
             * 🎯 设置对象的轮询周期
             * 
             * @param ObjectIndex - 对象索引
             * @param PollPeriod - 轮询周期(帧数),0 = 每帧轮询,255 = 最大周期
             */
            void SetPollPeriod(uint32 ObjectIndex, uint8 PollPeriod);
            
            /**
             * 🎯 更新轮询状态
             * 
             * 每帧调用,递增帧计数器,确定哪些对象需要轮询
             */
            void Update(
                const FNetBitArray& DirtyObjects,
                FNetBitArray& OutObjectsToCompare
            );
            
        private:
            // 📊 每个对象的轮询周期(0-255 帧)
            TArray<uint8> PollPeriods;
            
            // 📊 每个对象的帧计数器
            TArray<uint8> FrameCounters;
            
            // 📊 当前帧号
            uint32 CurrentFrame = 0;
        };
        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐│                    ⏱️ 轮询周期工作原理                                   │├────────────────────────────────────────────────────────────────────────┤│                                                                        ││   对象 A: PollPeriod = 0 (每帧轮询)                                     ││   ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐                                               ││   │✓│✓│✓│✓│✓│✓│✓│✓│✓│✓│  每帧都轮询                                   ││   └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘                                               ││    0 1 2 3 4 5 6 7 8 9   帧号                                          ││                                                                        ││   对象 B: PollPeriod = 2 (每 2 帧轮询)                                  ││   ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐                                               ││   │✓│ │✓│ │✓│ │✓│ │✓│ │  每 2 帧轮询一次                              ││   └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘                                               ││    0 1 2 3 4 5 6 7 8 9   帧号                                          ││                                                                        ││   对象 C: PollPeriod = 4 (每 4 帧轮询)                                  ││   ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐                                               ││   │✓│ │ │ │✓│ │ │ │✓│ │  每 4 帧轮询一次                              ││   └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘                                               ││    0 1 2 3 4 5 6 7 8 9   帧号                                          ││                                                                        ││   💡 脏对象优先:即使未到轮询周期,脏对象也会被轮询                       ││                                                                        │└────────────────────────────────────────────────────────────────────────┘

        📊 11.5.3 轮询周期配置建议

        对象类型

        推荐周期

        说明

        玩家角色

        0 (每帧)

        最重要,需要实时同步

        敌人 NPC

        1-2 帧

        较重要,需要较快响应

        环境物体

        4-8 帧

        不太重要,可以延迟

        远处物体

        16-32 帧

        优先级低,大幅延迟

        静态物体

        255 帧

        几乎不变,极少轮询

        CPP
        // 配置示例:根据对象类型设置轮询周期
        
        void UMyReplicationBridge::ConfigurePollFrequency(AActor* Actor){
            uint8 PollPeriod = 0;  // 默认每帧
            
            if (ACharacter* Character = Cast<ACharacter>(Actor))
            {
                if (Character->IsPlayerControlled())
                {
                    PollPeriod = 0;  // 玩家:每帧
                }
                else
                {
                    PollPeriod = 2;  // NPC:每 2 帧
                }
            }
            else if (AStaticMeshActor* StaticMesh = Cast<AStaticMeshActor>(Actor))
            {
                PollPeriod = 255;  // 静态物体:极少轮询
            }
            else
            {
                // 根据距离动态调整
                float Distance = GetDistanceToNearestPlayer(Actor);
                if (Distance < 1000.0f)
                {
                    PollPeriod = 1;
                }
                else if (Distance < 5000.0f)
                {
                    PollPeriod = 4;
                }
                else
                {
                    PollPeriod = 16;
                }
            }
            
            PollFrequencyLimiter->SetPollPeriod(
                Actor->GetNetRefHandle().GetIndex(), 
                PollPeriod
            );
        }

        🎭 11.6 ChangeMask(变化掩码)

        💡 11.6.1 什么是 ChangeMask?

        ChangeMask 是一个位数组,用于记录对象的哪些属性发生了变化。

        PLAINTEXT
        🎮 日常类比:购物清单勾选
        
        想象你有一张购物清单:
        ┌─────────────────────────────────┐
        │        🛒 购物清单              │
        ├─────────────────────────────────┤
        │  [✓] 牛奶      ← 已购买(变化)  │
        │  [ ] 面包      ← 未购买(无变化)│
        │  [✓] 鸡蛋      ← 已购买(变化)  │
        │  [ ] 苹果      ← 未购买(无变化)│
        │  [✓] 香蕉      ← 已购买(变化)  │
        └─────────────────────────────────┘
        
        ChangeMask 就是这个勾选状态:
        位数组: [1, 0, 1, 0, 1]
                牛奶 面包 鸡蛋 苹果 香蕉
        
        只有被勾选(位为1)的属性才需要发送到客户端!

        🏗️ 11.6.2 核心数据结构

        CPP
        // 源码位置:ChangeMaskUtil.h
        
        /**
         * 🎯 变化掩码存储
         * 
         * 智能存储:小掩码内联存储,大掩码堆分配
         */struct FChangeMaskStorageOrPointer
        {
            union
            {
                // 内联存储:最多 64 位(8 字节)
                uint64 InlineStorage;
                
                // 指针存储:超过 64 位时使用堆分配
                uint32* HeapStorage;
            };
            
            // 掩码位数
            uint16 BitCount;
            
            // 是否使用堆存储
            bool bIsHeapAllocated;
        };
        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐│                    🎭 ChangeMask 内存布局                               │├────────────────────────────────────────────────────────────────────────┤│                                                                        ││   小对象(≤ 64 个属性):内联存储                                       ││   ┌────────────────────────────────────────────┐                       ││   │  FChangeMaskStorageOrPointer (16 bytes)    │                       ││   ├────────────────────────────────────────────┤                       ││   │  InlineStorage: 0x00000000_0000001F        │ ← 直接存储位掩码       ││   │  BitCount: 5                               │                       ││   │  bIsHeapAllocated: false                   │                       ││   └────────────────────────────────────────────┘                       ││                                                                        ││   对应属性:[Health, Mana, Level, XP, Gold]                            ││   掩码值:   [  1,     1,    1,   1,   1  ] = 0x1F                     ││                                                                        ││   ─────────────────────────────────────────────────────────────────   ││                                                                        ││   大对象(> 64 个属性):堆分配                                         ││   ┌────────────────────────────────────────────┐                       ││   │  FChangeMaskStorageOrPointer (16 bytes)    │                       ││   ├────────────────────────────────────────────┤                       ││   │  HeapStorage: 0x7FFF_1234_5678 ──────────┐ │                       ││   │  BitCount: 128                           │ │                       ││   │  bIsHeapAllocated: true                  │ │                       ││   └──────────────────────────────────────────┼─┘                       ││                                              │                         ││                                              ▼                         ││                              ┌───────────────────────────┐             ││                              │  堆内存 (16 bytes)        │             ││                              │  [uint32, uint32,         │             ││                              │   uint32, uint32]         │             ││                              └───────────────────────────┘             ││                                                                        │└────────────────────────────────────────────────────────────────────────┘

        📊 11.6.3 ChangeMask 带宽节省示例

        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    📊 ChangeMask 带宽节省示例                           │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │   角色对象:20 个复制属性                                               │
        │   本帧只有 Health 和 Mana 变化                                         │
        │                                                                        │
        │   ❌ 无 ChangeMask(全量发送):                                        │
        │   ┌─────────────────────────────────────────────────────────────────┐ │
        │   │ [Health][Mana][Level][XP][Gold][Str][Agi][Int][...共20个属性]   │ │
        │   │  4字节   4字节  4字节 4字节 4字节 ...                            │ │
        │   │  总计:约 80 字节                                                │ │
        │   └─────────────────────────────────────────────────────────────────┘ │
        │                                                                        │
        │   ✅ 有 ChangeMask(增量发送):                                        │
        │   ┌─────────────────────────────────────────────────────────────────┐ │
        │   │ [ChangeMask: 0x03][Health][Mana]                                │ │
        │   │    3字节           4字节   4字节                                 │ │
        │   │  总计:约 11 字节                                                │ │
        │   └─────────────────────────────────────────────────────────────────┘ │
        │                                                                        │
        │   📊 带宽节省:(80 - 11) / 80 = 86% 🚀                                 │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        📋 11.7 总结与最佳实践

        🎯 11.7.1 核心概念回顾

        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    📋 第十一部分知识点总结                               │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │   🔍 脏数据检测                                                         │
        │   ├── Poll 模式:系统主动检查,简单可靠但 CPU 开销大                     │
        │   ├── Push 模式:代码主动通知,高效但需要开发者配合                      │
        │   └── Iris 混合策略:智能结合两种模式的优点                              │
        │                                                                        │
        │   📊 DirtyNetObjectTracker                                             │
        │   ├── AccumulatedDirtyNetObjects:累积脏对象                            │
        │   ├── DirtyNetObjects:当前帧脏对象                                     │
        │   └── ForceNetUpdateObjects:强制更新对象                               │
        │                                                                        │
        │   📢 Push Model                                                        │
        │   ├── MARK_PROPERTY_DIRTY_FROM_NAME:标记单个属性                       │
        │   ├── MARK_PROPERTY_DIRTY_FROM_NAME_STATIC_ARRAY_INDEX:数组元素        │
        │   ├── COMPARE_ASSIGN_AND_MARK_PROPERTY_DIRTY:比较后标记                │
        │   └── bIsPushBased = true:在 GetLifetimeReplicatedProps 中启用        │
        │                                                                        │
        │   🔄 ObjectPoller                                                      │
        │   ├── ForcePollObject:强制比较所有属性                                 │
        │   └── PushModelPollObject:只比较脏属性                                 │
        │                                                                        │
        │   ⏱️ ObjectPollFrequencyLimiter                                        │
        │   ├── 轮询周期:0-255 帧                                                │
        │   └── 脏对象优先:即使未到周期也会轮询                                   │
        │                                                                        │
        │   🎭 ChangeMask                                                        │
        │   ├── 记录哪些属性变化                                                  │
        │   ├── 智能存储:小掩码内联,大掩码堆分配                                 │
        │   └── 带宽节省:只发送变化的属性                                        │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        ✅ 11.7.2 最佳实践清单

        PLAINTEXT
        ┌────────────────────────────────────────────────────────────────────────┐
        │                    ✅ 轮询与脏数据检测最佳实践                           │
        ├────────────────────────────────────────────────────────────────────────┤
        │                                                                        │
        │   📢 Push Model 使用                                                   │
        │   ─────────────────────────────────────────────────────────────────   │
        │   ✅ 将复制属性设为 private,通过 Setter 访问                           │
        │   ✅ 在 Setter 中始终调用 MARK_PROPERTY_DIRTY                          │
        │   ✅ 使用 COMPARE_ASSIGN_AND_MARK_PROPERTY_DIRTY 避免无效标记          │
        │   ✅ 在 GetLifetimeReplicatedProps 中设置 bIsPushBased = true          │
        │   ❌ 避免直接访问复制属性                                               │
        │   ❌ 避免持有复制属性的引用                                             │
        │   ❌ 避免在客户端调用 MARK_PROPERTY_DIRTY                               │
        │                                                                        │
        │   ⏱️ 轮询频率配置                                                      │
        │   ─────────────────────────────────────────────────────────────────   │
        │   ✅ 玩家角色:每帧轮询(PollPeriod = 0)                               │
        │   ✅ 重要 NPC:每 1-2 帧轮询                                            │
        │   ✅ 环境物体:每 4-8 帧轮询                                            │
        │   ✅ 远处物体:每 16-32 帧轮询                                          │
        │   ✅ 根据距离动态调整轮询频率                                           │
        │                                                                        │
        │   🎭 ChangeMask 优化                                                   │
        │   ─────────────────────────────────────────────────────────────────   │
        │   ✅ 尽量将属性数量控制在 64 个以内(内联存储)                          │
        │   ✅ 将相关属性分组到结构体中                                           │
        │   ✅ 使用条件复制减少不必要的属性                                       │
        │                                                                        │
        │   🔧 调试技巧                                                          │
        │   ─────────────────────────────────────────────────────────────────   │
        │   ✅ 使用 net.Iris.PushModelMode=2 强制轮询进行对比测试                 │
        │   ✅ 监控脏对象数量和轮询耗时                                           │
        │   ✅ 使用可视化工具查看脏对象分布                                       │
        │                                                                        │
        └────────────────────────────────────────────────────────────────────────┘

        📊 11.7.3 性能优化建议

        优化策略

        预期收益

        实现难度

        启用 Push Model

        🚀🚀🚀 高

        ⭐ 简单

        合理配置轮询频率

        🚀🚀 中

        ⭐⭐ 中等

        使用 COMPARE_ASSIGN

        🚀 低

        ⭐ 简单

        减少复制属性数量

        🚀🚀 中

        ⭐⭐⭐ 困难

        自定义轮询策略

        🚀🚀🚀 高

        ⭐⭐⭐ 困难


        本文档基于 Unreal Engine 5.5.0 Iris 源代码分析(源码目录:Engine/Source/Runtime/Experimental/Iris/)

        最后更新于 April 13, 2026
        On this page
        暂无目录