页面加载中
博客快捷键
按住 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 网络复制系统技术分析 - 第十七部分:高级主题

        January 3, 202644 分钟 阅读177 次阅读

        🚀 Iris 网络复制系统技术分析 - 第十七部分:高级主题

        📖 本章导读:当 Iris 的"标准快递服务"无法满足你的特殊需求时,是时候自己开一家定制快递公司了!本章将手把手教你如何扩展 Iris 系统,打造专属的 Filter、Prioritizer 和 Fragment,让网络复制系统完美适配你的游戏需求。无论你是想实现隐身系统、战斗优先级,还是优化千人同服的大型游戏,这里都有你需要的答案!


        🎯 17.0 为什么需要"高级定制"?

        💡 17.0.1 从"买成品"到"自己造"

        PLAINTEXT
        🏠 日常类比:装修房子
        
        买精装房 vs 自己装修:
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │   🏠 精装房(Iris 内置组件)           🔧 自己装修(自定义组件)      │
        │   ═══════════════════════            ═══════════════════════        │
        │                                                                     │
        │   ┌─────────────────┐                ┌─────────────────┐            │
        │   │  ✅ 拎包入住    │                │  ✅ 独特风格    │            │
        │   │  ✅ 省时省力    │                │  ✅ 完美契合    │            │
        │   │  ❌ 风格固定    │                │  ❌ 需要时间    │            │
        │   │  ❌ 无法定制    │                │  ❌ 需要技能    │            │
        │   └─────────────────┘                └─────────────────┘            │
        │                                                                     │
        │   适合:90% 的普通需求                适合:10% 的特殊需求           │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘
        
        Iris 内置的 Filter、Prioritizer 就像精装房,能满足大多数需求。
        但如果你的游戏有独特的机制(隐身、战争迷雾、特殊优先级),
        就需要自己动手"装修"了!

        🤔 17.0.2 什么时候需要自定义?

        PLAINTEXT
        🎯 决策树:我需要自定义吗?
        
                            你的需求
                                │
                                ▼
                    ┌───────────────────────┐
                    │  内置组件能满足吗?    │
                    └───────────────────────┘
                           │           │
                      能满足 │           │ 不能满足
                           ▼           ▼
                    ┌───────────┐  ┌───────────────────┐
                    │ 直接用!  │  │ 能通过配置解决吗? │
                    │ 别造轮子!│  └───────────────────┘
                    └───────────┘         │           │
                                     能配置 │           │ 不能配置
                                          ▼           ▼
                                   ┌───────────┐  ┌───────────────┐
                                   │ 调整配置!│  │ 需要自定义!  │
                                   │ 省时省力!│  │ 继续往下看 👇 │
                                   └───────────┘  └───────────────┘
        

        📊 17.0.3 自定义难度等级表

        定制类型

        难度

        学习时间

        适用场景

        风险等级

        🟢 配置调参

        ⭐

        1小时

        90% 的情况

        低

        🟡 自定义 Filter

        ⭐⭐

        1-2天

        特殊可见性规则

        中

        🟡 自定义 Prioritizer

        ⭐⭐

        1-2天

        特殊优先级逻辑

        中

        🟠 自定义 Fragment

        ⭐⭐⭐

        3-5天

        非标准数据复制

        中高

        🔴 修改核心系统

        ⭐⭐⭐⭐⭐

        1周+

        极端优化需求

        高

        PLAINTEXT
        💡 建议:
        - 新手:先从配置调参开始,熟悉系统后再尝试自定义- 进阶:优先尝试 Filter 和 Prioritizer,它们相对独立- 高手:Fragment 需要深入理解序列化系统,谨慎尝试

        📂 17.0.4 关键源文件索引

        文件

        路径

        职责

        NetObjectFilter.h

        Iris/Core/Public/Iris/ReplicationSystem/Filtering/

        Filter 基类定义

        NetObjectPrioritizer.h

        Iris/Core/Public/Iris/ReplicationSystem/Prioritization/

        Prioritizer 基类定义

        ReplicationFragment.h

        Iris/Core/Public/Iris/ReplicationSystem/

        Fragment 基类定义

        NetObjectGridFilter.cpp

        Iris/Core/Private/Iris/ReplicationSystem/Filtering/

        空间过滤器参考实现

        SphereNetObjectPrioritizer.cpp

        Iris/Core/Private/Iris/ReplicationSystem/Prioritization/

        球形优先级器参考实现


        🔍 17.1 自定义 Filter:打造你的"VIP 通道"

        💡 17.1.1 Filter 是什么?

        PLAINTEXT
        🎭 日常类比:演唱会的安检口
        
        Filter 就像演唱会入口的安检系统——决定谁能进场、谁被拦在门外。
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                        🎤 演唱会入场流程                             │
        ├─────────────────────────────────────────────────────────────────────┤
        │                                                                     │
        │   观众排队                                                          │
        │   👤👤👤👤👤👤👤👤                                                  │
        │        │                                                            │
        │        ▼                                                            │
        │   ┌─────────────────────────────────────────┐                       │
        │   │           🚪 安检口(Filter)            │                       │
        │   │                                         │                       │
        │   │  检查项目:                              │                       │
        │   │  ✅ 有票?→ 放行                        │                       │
        │   │  ✅ VIP票?→ VIP通道                    │                       │
        │   │  ❌ 没票?→ 拒绝入场                    │                       │
        │   │  ❌ 黑名单?→ 拒绝入场                  │                       │
        │   │                                         │                       │
        │   └─────────────────────────────────────────┘                       │
        │        │                    │                                       │
        │        ▼                    ▼                                       │
        │   ┌─────────┐          ┌─────────┐                                  │
        │   │ 🎉 入场 │          │ 🚫 拒绝 │                                  │
        │   └─────────┘          └─────────┘                                  │
        │                                                                     │
        │   在网络复制中:                                                     │
        │   - 观众 = 游戏对象                                                 │
        │   - 安检口 = Filter                                                 │
        │   - 入场 = 对象对该玩家可见                                          │
        │   - 拒绝 = 对象对该玩家不可见(被过滤)                               │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🎯 17.1.2 什么时候需要自定义 Filter?

        📖 真实案例:小明的"隐身斗篷"困境

        PLAINTEXT
        🧙 游戏需求:魔法对战游戏的隐身系统
        
        小明在做一款魔法对战游戏,需要实现隐身术:
        
        隐身规则:
        ┌─────────────────────────────────────────────────────────────────────┐
        │  隐身等级  │  敌人能看到?  │  队友能看到?  │  自己能看到?         │
        ├─────────────────────────────────────────────────────────────────────┤
        │  等级 0    │     ✅ 是      │     ✅ 是      │     ✅ 是            │
        │ (正常)   │               │               │                       │
        ├─────────────────────────────────────────────────────────────────────┤
        │  等级 1    │     ❌ 否      │     ✅ 是      │     ✅ 是            │
        │(轻度隐身)│  除非有真视    │               │                       │
        ├─────────────────────────────────────────────────────────────────────┤
        │  等级 2    │     ❌ 否      │     ❌ 否      │     ✅ 是            │
        │(完全隐身)│  除非有真视    │  除非有真视    │                       │
        └─────────────────────────────────────────────────────────────────────┘
        
        问题:Iris 内置的 Filter 都不支持这种复杂的可见性逻辑!
        
        内置 Filter 的局限:
        - GridFilter:只考虑距离,不考虑隐身状态- ConnectionFilter:只考虑连接关系,不考虑游戏状态- GroupFilter:只考虑组归属,不够灵活
        
        结论:需要自定义 InvisibilityFilter!

        📊 需要自定义 Filter 的典型场景

        场景

        为什么内置不够用

        解决方案

        🧙 隐身系统

        需要根据技能状态动态过滤

        自定义 InvisibilityFilter

        🌫️ 战争迷雾

        需要结合游戏地图探索数据

        自定义 FogOfWarFilter

        👁️ 侦察技能

        临时"看穿"某些隐藏对象

        自定义 DetectionFilter

        🏰 阵营系统

        复杂的敌我中立关系判定

        自定义 FactionFilter

        🚪 副本隔离

        同地图不同副本实例隔离

        自定义 DungeonInstanceFilter

        🎭 观战模式

        观战者能看到所有/部分信息

        自定义 SpectatorFilter

        🏗️ 17.1.3 Filter 工作原理揭秘

        💡 Filter 生命周期

        PLAINTEXT
        🍽️ 日常类比:餐厅订座系统
        
        Filter 的工作流程就像餐厅的订座系统:
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                        🍽️ 餐厅订座流程                               │
        ├─────────────────────────────────────────────────────────────────────┤
        │                                                                     │
        │  1️⃣ 客人打电话预约                                                  │
        │     ════════════════                                                │
        │     📞 "我想订位"                                                    │
        │         │                                                           │
        │         ▼                                                           │
        │     AddObject()  ← 对象"报到",Filter 记录它的信息                   │
        │                                                                     │
        │  2️⃣ 餐厅每天检查预约                                                │
        │     ════════════════                                                │
        │     📋 检查今天有哪些预约                                            │
        │         │                                                           │
        │         ▼                                                           │
        │     PreFilter()  ← 每帧开始前的准备工作                              │
        │         │                                                           │
        │         ▼                                                           │
        │     Filter()     ← 核心!决定每个客人能否入座                        │
        │         │                                                           │
        │         ▼                                                           │
        │     PostFilter() ← 每帧结束后的清理工作                              │
        │                                                                     │
        │  3️⃣ 客人取消预约                                                    │
        │     ════════════════                                                │
        │     📞 "我不来了"                                                    │
        │         │                                                           │
        │         ▼                                                           │
        │     RemoveObject() ← 对象"告别",Filter 清除记录                     │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🔄 Filter 接口详解

        CPP
        // 源码位置:NetObjectFilter.h
        
        class UNetObjectFilter : public UObject
        {
        public:
            //========================================
            // 🎯 初始化接口
            //========================================
            
            /**
             * 初始化 Filter
             * 调用时机:ReplicationSystem 创建时
             * 职责:分配内部数据结构、读取配置
             */
            virtual void Init(FNetObjectFilterInitParams& Params);
            
            //========================================
            // 📦 对象管理接口
            //========================================
            
            /**
             * 添加对象到 Filter
             * 调用时机:对象开始参与网络复制时
             * 职责:记录对象信息,初始化过滤状态
             * 返回值:true = 成功添加,false = 拒绝添加
             */
            virtual bool AddObject(uint32 ObjectIndex, FNetObjectFilterAddObjectParams& Params);
            
            /**
             * 从 Filter 移除对象
             * 调用时机:对象停止网络复制时(销毁、休眠等)
             * 职责:清理所有与该对象相关的数据
             */
            virtual void RemoveObject(uint32 ObjectIndex, const FNetObjectFilteringInfo& Info);
            
            //========================================
            // 🔍 过滤接口(核心!)
            //========================================
            
            /**
             * 过滤前准备
             * 调用时机:每帧过滤开始前
             * 职责:批量准备工作,如更新缓存、预计算等
             */
            virtual void PreFilter(FNetObjectPreFilteringParams& Params);
            
            /**
             * 执行过滤(最重要的方法!)
             * 调用时机:每帧,对每个需要过滤的对象
             * 职责:决定每个对象对每个连接是否可见
             */
            virtual void Filter(FNetObjectFilteringParams& Params);
            
            /**
             * 过滤后清理
             * 调用时机:每帧过滤结束后
             * 职责:清理临时数据,更新统计等
             */
            virtual void PostFilter(FNetObjectPostFilteringParams& Params);
            
            //========================================
            // 🔌 连接管理接口
            //========================================
            
            /**
             * 添加连接
             * 调用时机:新玩家加入游戏时
             * 职责:为新连接初始化过滤数据
             */
            virtual bool AddConnection(uint32 ConnectionId);
            
            /**
             * 移除连接
             * 调用时机:玩家离开游戏时
             * 职责:清理该连接相关的所有数据
             */
            virtual void RemoveConnection(uint32 ConnectionId);
        };

        📊 Filter 接口调用时序

        PLAINTEXT
        ┌─────────────────────────────────────────────────────────────────────┐
        │                    🔄 Filter 接口调用时序图                          │
        ├─────────────────────────────────────────────────────────────────────┤
        │                                                                     │
        │   游戏启动                                                          │
        │      │                                                              │
        │      ▼                                                              │
        │   Init() ─────────────────────────────────────────────────────────  │
        │      │                                                              │
        │      │  [玩家加入]                                                  │
        │      ├──────────► AddConnection(ConnectionId)                       │
        │      │                                                              │
        │      │  [对象创建]                                                  │
        │      ├──────────► AddObject(ObjectIndex)                            │
        │      │                                                              │
        │      │  ┌─────────────────────────────────────┐                     │
        │      │  │           每帧循环                   │                     │
        │      │  │                                     │                     │
        │      │  │   PreFilter()                       │                     │
        │      │  │       │                             │                     │
        │      │  │       ▼                             │                     │
        │      │  │   Filter() ← 核心过滤逻辑           │                     │
        │      │  │       │                             │                     │
        │      │  │       ▼                             │                     │
        │      │  │   PostFilter()                      │                     │
        │      │  │                                     │                     │
        │      │  └─────────────────────────────────────┘                     │
        │      │                                                              │
        │      │  [对象销毁]                                                  │
        │      ├──────────► RemoveObject(ObjectIndex)                         │
        │      │                                                              │
        │      │  [玩家离开]                                                  │
        │      └──────────► RemoveConnection(ConnectionId)                    │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🛠️ 17.1.4 手把手实现:隐身系统过滤器

        📋 需求分析

        PLAINTEXT
        🧙 隐身系统完整需求:
        
        1. 隐身等级规则:
           - 等级 0:正常状态,所有人可见
           - 等级 1:轻度隐身,敌人不可见,队友可见
           - 等级 2:完全隐身,只有自己可见
        
        2. 真视技能:
           - 拥有"真视"技能的玩家可以看穿隐身
           - 配置项:真视是否能看穿所有等级
        
        3. 性能要求:
           - 支持 100+ 隐身对象
           - 支持 64 个玩家连接
           - 每帧过滤时间 < 0.5ms

        📝 第一步:定义 Filter 类(头文件)

        CPP
        // InvisibilityNetObjectFilter.h// 🧙 隐身系统过滤器——让魔法师真正"消失"
        
        #pragma once
        
        #include "Iris/ReplicationSystem/Filtering/NetObjectFilter.h"#include "InvisibilityNetObjectFilter.generated.h"
        
        /**
         * 隐身过滤器:根据隐身等级决定对象可见性
         * 
         * 💡 设计思路(类比):
         * 想象一个魔法世界的"视觉系统":
         * - 普通人只能看到没隐身的人
         * - 队友有"心灵感应",能看到轻度隐身的队友
         * - 拥有"真视之眼"的人能看穿一切隐身
         */UCLASS()
        class MYGAME_API UInvisibilityNetObjectFilter : public UNetObjectFilter
        {
            GENERATED_BODY()
        
        public:
            //========================================
            // 🎯 配置属性
            //========================================
            
            /** 
             * 真视技能是否能看穿所有隐身等级
             * true = 真视无敌,能看穿等级2隐身
             * false = 真视只能看穿等级1隐身
             */
            UPROPERTY(Config)
            bool bTrueSightSeesAll = true;
        
        protected:
            //========================================
            // 🔧 Filter 接口实现
            //========================================
            
            virtual void Init(FNetObjectFilterInitParams& Params) override;
            virtual bool AddObject(uint32 ObjectIndex, FNetObjectFilterAddObjectParams& Params) override;
            virtual void RemoveObject(uint32 ObjectIndex, const FNetObjectFilteringInfo& Info) override;
            virtual void Filter(FNetObjectFilteringParams& Params) override;
            virtual bool AddConnection(uint32 ConnectionId) override;
            virtual void RemoveConnection(uint32 ConnectionId) override;
        
        public:
            //========================================
            // 🎮 游戏逻辑接口
            //========================================
            
            /** 更新对象的隐身等级 */
            UFUNCTION(BlueprintCallable, Category = "Invisibility")
            void UpdateObjectInvisibility(uint32 ObjectIndex, int32 InvisibilityLevel);
            
            /** 更新玩家的真视状态 */
            UFUNCTION(BlueprintCallable, Category = "Invisibility")
            void UpdatePlayerTrueSight(uint32 ConnectionId, bool bHasTrueSight);
            
            /** 设置对象的队伍ID */
            UFUNCTION(BlueprintCallable, Category = "Invisibility")
            void SetObjectTeam(uint32 ObjectIndex, int32 TeamId);
            
            /** 设置玩家的队伍ID */
            UFUNCTION(BlueprintCallable, Category = "Invisibility")
            void SetPlayerTeam(uint32 ConnectionId, int32 TeamId);
        
        private:
            //========================================
            // 📊 内部数据结构
            //========================================
            
            /** 对象隐身等级表 (只存储隐身的对象,节省内存) */
            TMap<uint32, int32> ObjectInvisibilityLevels;
            
            /** 对象所属队伍表 */
            TMap<uint32, int32> ObjectTeams;
            
            /** 玩家真视状态表 */
            TMap<uint32, bool> PlayerTrueSightStatus;
            
            /** 玩家所属队伍表 */
            TMap<uint32, int32> PlayerTeams;
            
            /** 玩家对应的对象索引 (用于判断"自己") */
            TMap<uint32, uint32> PlayerObjectIndices;
            
            /** 最大支持的连接数 */
            uint32 MaxConnectionCount = 0;
        };

        📝 第二步:实现核心逻辑(源文件)

        CPP
        // InvisibilityNetObjectFilter.cpp
        
        #include "InvisibilityNetObjectFilter.h"
        
        void UInvisibilityNetObjectFilter::Init(FNetObjectFilterInitParams& Params){
            MaxConnectionCount = Params.MaxConnectionCount;
            
            // 💡 性能优化:预分配内存
            ObjectInvisibilityLevels.Reserve(256);
            ObjectTeams.Reserve(1024);
            PlayerTrueSightStatus.Reserve(MaxConnectionCount);
            PlayerTeams.Reserve(MaxConnectionCount);
        }
        
        bool UInvisibilityNetObjectFilter::AddObject(
            uint32 ObjectIndex, 
            FNetObjectFilterAddObjectParams& Params){
            // 获取对象的初始队伍和隐身状态
            // 实际实现需要从游戏对象获取这些信息
            ObjectTeams.Add(ObjectIndex, -1);  // 默认无队伍
            return true;
        }
        
        void UInvisibilityNetObjectFilter::RemoveObject(
            uint32 ObjectIndex, 
            const FNetObjectFilteringInfo& Info){
            // 🧹 清理所有相关数据
            ObjectInvisibilityLevels.Remove(ObjectIndex);
            ObjectTeams.Remove(ObjectIndex);
            
            // 清理玩家-对象映射
            for (auto It = PlayerObjectIndices.CreateIterator(); It; ++It)
            {
                if (It.Value() == ObjectIndex)
                {
                    It.RemoveCurrent();
                    break;
                }
            }
        }
        
        void UInvisibilityNetObjectFilter::Filter(FNetObjectFilteringParams& Params){
            /**
             * 🎯 核心过滤逻辑
             * 
             * 对于每个对象,判断每个玩家能否看到它
             */
            
            // 🚀 性能优化:如果没有隐身对象,直接返回
            if (ObjectInvisibilityLevels.Num() == 0)
            {
                return;
            }
            
            // 遍历需要过滤的对象
            for (uint32 ObjectIndex : Params.ObjectIndices)
            {
                // 获取隐身等级
                const int32* InvisLevelPtr = ObjectInvisibilityLevels.Find(ObjectIndex);
                const int32 InvisLevel = InvisLevelPtr ? *InvisLevelPtr : 0;
                
                // 等级0:所有人可见,跳过
                if (InvisLevel == 0)
                {
                    continue;
                }
                
                // 获取对象队伍
                const int32 ObjectTeam = ObjectTeams.FindRef(ObjectIndex);
                
                // 遍历每个连接
                for (uint32 ConnId = 0; ConnId < MaxConnectionCount; ++ConnId)
                {
                    bool bShouldFilter = false;  // true = 不可见
                    
                    // 检查是否是自己
                    if (const uint32* PlayerObjIdx = PlayerObjectIndices.Find(ConnId))
                    {
                        if (*PlayerObjIdx == ObjectIndex)
                        {
                            continue;  // 自己永远可见
                        }
                    }
                    
                    // 检查真视
                    const bool bHasTrueSight = PlayerTrueSightStatus.FindRef(ConnId);
                    if (bHasTrueSight && bTrueSightSeesAll)
                    {
                        continue;  // 真视无敌
                    }
                    
                    // 根据隐身等级判断
                    if (InvisLevel == 1)
                    {
                        // 等级1:检查是否队友
                        const int32 PlayerTeam = PlayerTeams.FindRef(ConnId);
                        bShouldFilter = (PlayerTeam != ObjectTeam || ObjectTeam == -1);
                        
                        // 真视可以看穿等级1
                        if (bHasTrueSight) bShouldFilter = false;
                    }
                    else if (InvisLevel == 2)
                    {
                        // 等级2:只有真视(无敌模式)可见
                        bShouldFilter = !bHasTrueSight || !bTrueSightSeesAll;
                    }
                    
                    // 设置过滤结果
                    if (bShouldFilter)
                    {
                        Params.OutFilteredOutObjects[ConnId].SetBit(ObjectIndex, true);
                    }
                }
            }
        }
        
        bool UInvisibilityNetObjectFilter::AddConnection(uint32 ConnectionId){
            PlayerTrueSightStatus.Add(ConnectionId, false);
            PlayerTeams.Add(ConnectionId, -1);
            return true;
        }
        
        void UInvisibilityNetObjectFilter::RemoveConnection(uint32 ConnectionId){
            PlayerTrueSightStatus.Remove(ConnectionId);
            PlayerTeams.Remove(ConnectionId);
            PlayerObjectIndices.Remove(ConnectionId);
        }
        
        void UInvisibilityNetObjectFilter::UpdateObjectInvisibility(
            uint32 ObjectIndex, int32 InvisibilityLevel){
            if (InvisibilityLevel > 0)
            {
                ObjectInvisibilityLevels.Add(ObjectIndex, InvisibilityLevel);
            }
            else
            {
                ObjectInvisibilityLevels.Remove(ObjectIndex);
            }
        }
        
        void UInvisibilityNetObjectFilter::UpdatePlayerTrueSight(
            uint32 ConnectionId, bool bHasTrueSight){
            PlayerTrueSightStatus.Add(ConnectionId, bHasTrueSight);
        }
        
        void UInvisibilityNetObjectFilter::SetObjectTeam(uint32 ObjectIndex, int32 TeamId){
            ObjectTeams.Add(ObjectIndex, TeamId);
        }
        
        void UInvisibilityNetObjectFilter::SetPlayerTeam(uint32 ConnectionId, int32 TeamId){
            PlayerTeams.Add(ConnectionId, TeamId);
        }

        📝 第三步:注册和配置

        INI
        ; DefaultGame.ini
        
        [/Script/IrisCore.ObjectReplicationBridgeConfig]; 为隐身角色类配置自定义 Filter
        +FilterConfigs=(ClassName="/Script/MyGame.InvisibleCharacter", FilterName="InvisibilityFilter")
        
        [/Script/IrisCore.NetObjectFilterConfig]; 定义 Filter 类型
        +FilterDefinitions=(FilterName="InvisibilityFilter", FilterClassName="/Script/MyGame.UInvisibilityNetObjectFilter")

        📝 第四步:在游戏代码中使用

        CPP
        // MyCharacter.cpp
        
        void AMyCharacter::ActivateInvisibility(int32 Level){
            CurrentInvisibilityLevel = Level;
            
            // 🎯 通知 Filter 更新
            if (UIrisReplicationSystem* IrisSystem = GetIrisReplicationSystem())
            {
                FNetRefHandle Handle = IrisSystem->GetNetRefHandle(this);
                uint32 ObjectIndex = Handle.GetObjectIndex();
                
                if (auto* Filter = Cast<UInvisibilityNetObjectFilter>(
                    IrisSystem->GetFilter("InvisibilityFilter")))
                {
                    Filter->UpdateObjectInvisibility(ObjectIndex, Level);
                }
            }
            
            // 强制立即网络更新
            ForceNetUpdate();
        }

        ⚡ 17.1.5 性能优化技巧

        PLAINTEXT
        🚀 Filter 性能优化三板斧
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │  1️⃣ 早期退出(Early Out)                                           │
        │  ════════════════════════                                           │
        │                                                                     │
        │  if (ObjectInvisibilityLevels.Num() == 0)                          │
        │  {                                                                  │
        │      return;  // 没有隐身对象,直接返回!                            │
        │  }                                                                  │
        │                                                                     │
        │  效果:无隐身时,耗时从 0.8ms → 0.01ms (80倍提升!)                  │
        │                                                                     │
        ├─────────────────────────────────────────────────────────────────────┤
        │                                                                     │
        │  2️⃣ 批量处理(Batch Processing)                                    │
        │  ════════════════════════════════                                   │
        │                                                                     │
        │  // ❌ 低效:逐个处理                                               │
        │  for (Object) { for (Connection) { ... } }                         │
        │                                                                     │
        │  // ✅ 高效:按连接批量处理                                          │
        │  for (Connection)                                                   │
        │  {                                                                  │
        │      // 预先获取该连接的所有数据                                     │
        │      int32 PlayerTeam = PlayerTeams[Connection];                   │
        │      bool bHasTrueSight = PlayerTrueSightStatus[Connection];       │
        │                                                                     │
        │      for (Object) { ... }  // 批量处理                              │
        │  }                                                                  │
        │                                                                     │
        │  效果:耗时从 2.5ms → 0.8ms (3倍提升)                               │
        │                                                                     │
        ├─────────────────────────────────────────────────────────────────────┤
        │                                                                     │
        │  3️⃣ 缓存计算结果                                                    │
        │  ════════════════                                                   │
        │                                                                     │
        │  // 对于不常变化的数据,缓存计算结果                                  │
        │  struct FCachedVisibility                                           │
        │  {                                                                  │
        │      int32 TeamId;                                                  │
        │      bool bHasTrueSight;                                            │
        │      uint32 LastUpdateFrame;                                        │
        │  };                                                                  │
        │                                                                     │
        │  效果:耗时从 0.8ms → 0.5ms (1.6倍提升)                             │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🐛 17.1.6 常见问题与解决方案

        问题

        症状

        原因

        解决方案

        🔄 对象闪烁

        隐身对象时隐时现

        Filter 结果不稳定

        添加滞后机制(Hysteresis)

        ⏰ 更新延迟

        隐身后敌人还能看到

        状态更新不同步

        调用 ForceNetUpdate()

        💾 内存泄漏

        长时间运行内存增长

        RemoveObject 未清理

        检查所有 Map 的清理

        🐌 性能差

        过滤耗时过长

        未使用批量处理

        重构为批量操作


        ⚡ 17.2 自定义 Prioritizer:打造你的"VIP 排队系统"

        💡 17.2.1 Prioritizer 是什么?

        PLAINTEXT
        🏥 日常类比:医院的分诊台
        
        Prioritizer 就像医院的分诊系统——决定谁先看病。
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                        🏥 医院分诊流程                               │
        ├─────────────────────────────────────────────────────────────────────┤
        │                                                                     │
        │   患者排队                                                          │
        │   🤒🤕😷🤧🤢                                                        │
        │        │                                                            │
        │        ▼                                                            │
        │   ┌─────────────────────────────────────────┐                       │
        │   │         🏥 分诊台(Prioritizer)         │                       │
        │   │                                         │                       │
        │   │  优先级规则:                            │                       │
        │   │  🚨 急诊(心脏病)→ 优先级 1.0          │                       │
        │   │  👴 老人/孕妇    → 优先级 0.8          │                       │
        │   │  💳 VIP 会员     → 优先级 0.7          │                       │
        │   │  👤 普通患者     → 优先级 0.5          │                       │
        │   │  🦷 小病小痛     → 优先级 0.3          │                       │
        │   │                                         │                       │
        │   └─────────────────────────────────────────┘                       │
        │        │                                                            │
        │        ▼                                                            │
        │   按优先级排序后叫号:🚨 → 👴 → 💳 → 👤 → 🦷                        │
        │                                                                     │
        │   在网络复制中:                                                     │
        │   - 患者 = 游戏对象                                                 │
        │   - 分诊台 = Prioritizer                                            │
        │   - 优先级 = 复制顺序(高优先级先复制)                              │
        │   - 带宽有限 = 医生数量有限(不能同时看所有人)                       │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🎯 17.2.2 什么时候需要自定义 Prioritizer?

        📖 真实案例:小红的"吃鸡"困境

        PLAINTEXT
        🎮 游戏需求:大逃杀游戏的优先级系统
        
        小红在做一款大逃杀游戏,100 个玩家同时在线:
        
        问题:带宽有限,不能同时更新所有人的数据
        
        需求:
        ┌─────────────────────────────────────────────────────────────────────┐
        │  对象类型          │  期望优先级  │  原因                           │
        ├─────────────────────────────────────────────────────────────────────┤
        │  正在攻击我的敌人  │     最高     │  生死攸关!必须第一时间知道     │
        │  我正在攻击的敌人  │     很高     │  需要准确的命中判定             │
        │  视野内的敌人      │     高       │  可能随时开火                   │
        │  队友              │     中高     │  需要配合                       │
        │  远处的敌人        │     低       │  暂时威胁不大                   │
        │  地上的物品        │     最低     │  不会动,不着急                 │
        └─────────────────────────────────────────────────────────────────────┘
        
        问题:内置的 SphereNetObjectPrioritizer 只考虑距离,不考虑"战斗状态"!
        
        结论:需要自定义 CombatPrioritizer!

        📊 需要自定义 Prioritizer 的典型场景

        场景

        为什么内置不够用

        解决方案

        🔫 战斗优先

        需要结合战斗系统数据

        自定义 CombatPrioritizer

        🎯 目标锁定

        锁定目标需最高优先级

        自定义 TargetLockPrioritizer

        📢 语音聊天

        说话的人优先级提升

        自定义 VoiceChatPrioritizer

        🏆 任务相关

        任务目标 NPC 优先

        自定义 QuestPrioritizer

        🎪 表演系统

        表演者优先级最高

        自定义 PerformancePrioritizer

        🏗️ 17.2.3 Prioritizer 工作原理

        PLAINTEXT
        📊 优先级值的含义
        
        优先级范围:0.0 ~ 1.0
        
        0.0 ──────────────────────────────────────── 1.0
         │                                            │
        最低优先级                                  最高优先级
        (可能不复制)                                (优先复制)
        
        💡 特殊值:
        - 0.0:完全不复制(相当于被过滤)- 1.0:最高优先级,必须复制- 累积机制:每帧未复制的对象,优先级会累积增加

        🛠️ 17.2.4 手把手实现:战斗优先级器

        CPP
        // CombatNetObjectPrioritizer.h
        
        UCLASS()
        class UCombatNetObjectPrioritizer : public USphereNetObjectPrioritizer
        {
            GENERATED_BODY()
        
        public:
            /** 正在攻击我的敌人加成 */
            UPROPERTY(Config)
            float AttackingMeBoost = 0.3f;
            
            /** 我正在攻击的敌人加成 */
            UPROPERTY(Config)
            float MyTargetBoost = 0.2f;
            
            /** 队友加成 */
            UPROPERTY(Config)
            float TeammateBoost = 0.15f;
            
            /** 目标锁定加成 */
            UPROPERTY(Config)
            float TargetLockBoost = 0.4f;
        
        protected:
            virtual void Prioritize(FNetObjectPrioritizationParams& Params) override;
        
        public:
            /** 记录攻击事件 */
            void RecordAttack(uint32 AttackerIndex, uint32 VictimIndex);
            
            /** 设置目标锁定 */
            void SetTargetLock(uint32 ConnectionId, uint32 TargetIndex);
        
        private:
            /** 战斗关系表 <Attacker, Victim> -> 最后攻击时间 */
            TMap<TPair<uint32, uint32>, double> CombatRelations;
            
            /** 目标锁定表 ConnectionId -> TargetIndex */
            TMap<uint32, uint32> TargetLocks;
        };
        
        // CombatNetObjectPrioritizer.cpp
        
        void UCombatNetObjectPrioritizer::Prioritize(FNetObjectPrioritizationParams& Params){
            // 第一步:调用父类计算基础距离优先级
            Super::Prioritize(Params);
            
            // 第二步:叠加战斗加成
            const double CurrentTime = FPlatformTime::Seconds();
            
            for (uint32 ConnectionId : Params.ConnectionIds)
            {
                TArrayView<float> Priorities = Params.OutPriorities[ConnectionId];
                uint32 PlayerObjectIndex = GetPlayerObjectIndex(ConnectionId);
                
                for (int32 i = 0; i < Params.ObjectIndices.Num(); ++i)
                {
                    uint32 ObjectIndex = Params.ObjectIndices[i];
                    float& Priority = Priorities[i];
                    
                    if (Priority <= 0.0f) continue;  // 已被过滤
                    
                    float Boost = 0.0f;
                    
                    // 检查:对象是否正在攻击我?
                    auto AttackingMeKey = MakeTuple(ObjectIndex, PlayerObjectIndex);
                    if (CombatRelations.Contains(AttackingMeKey))
                    {
                        Boost += AttackingMeBoost;
                    }
                    
                    // 检查:我是否正在攻击对象?
                    auto MyTargetKey = MakeTuple(PlayerObjectIndex, ObjectIndex);
                    if (CombatRelations.Contains(MyTargetKey))
                    {
                        Boost += MyTargetBoost;
                    }
                    
                    // 检查:是否是锁定目标?
                    if (const uint32* LockedTarget = TargetLocks.Find(ConnectionId))
                    {
                        if (*LockedTarget == ObjectIndex)
                        {
                            Boost += TargetLockBoost;
                        }
                    }
                    
                    // 叠加加成(确保不超过 1.0)
                    Priority = FMath::Min(1.0f, Priority + Boost);
                }
            }
        }

        🤝 17.2.5 Filter 和 Prioritizer 的协作

        PLAINTEXT
        🔄 数据包发送流程
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │   所有游戏对象                                                       │
        │   📦📦📦📦📦📦📦📦📦📦                                              │
        │        │                                                            │
        │        ▼                                                            │
        │   ┌─────────────────────────────────────────┐                       │
        │   │         🔍 Filter 过滤                   │                       │
        │   │         决定"谁能进场"                   │                       │
        │   └─────────────────────────────────────────┘                       │
        │        │                                                            │
        │        ▼                                                            │
        │   通过过滤的对象                                                     │
        │   📦📦📦📦📦                                                        │
        │        │                                                            │
        │        ▼                                                            │
        │   ┌─────────────────────────────────────────┐                       │
        │   │         ⚡ Prioritizer 排序              │                       │
        │   │         决定"谁先服务"                   │                       │
        │   └─────────────────────────────────────────┘                       │
        │        │                                                            │
        │        ▼                                                            │
        │   按优先级排序的对象                                                 │
        │   📦(0.9) → 📦(0.7) → 📦(0.5) → 📦(0.3) → 📦(0.1)                  │
        │        │                                                            │
        │        ▼                                                            │
        │   ┌─────────────────────────────────────────┐                       │
        │   │         📡 带宽限制                      │                       │
        │   │         只发送前 N 个                    │                       │
        │   └─────────────────────────────────────────┘                       │
        │        │                                                            │
        │        ▼                                                            │
        │   实际发送的对象                                                     │
        │   📦(0.9) → 📦(0.7) → 📦(0.5)                                       │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🧩 17.3 自定义 ReplicationFragment:打造你的"特殊快递包裹"

        💡 17.3.1 Fragment 是什么?

        PLAINTEXT
        📦 日常类比:快递的包装方式
        
        Fragment 就像快递的"包装方式"——不同的物品需要不同的包装。
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                        📦 快递包装类比                               │
        ├─────────────────────────────────────────────────────────────────────┤
        │                                                                     │
        │   物品类型              包装方式              对应 Fragment          │
        │   ─────────            ─────────            ─────────────          │
        │                                                                     │
        │   📱 手机        →     🧊 防震泡沫      →   PropertyFragment        │
        │   (普通属性)         (标准包装)         (标准属性复制)          │
        │                                                                     │
        │   🍷 红酒        →     📦 特制酒盒      →   自定义 Fragment         │
        │   (易碎品)           (特殊包装)         (特殊数据复制)          │
        │                                                                     │
        │   📚 书籍(多本)  →     📦 批量打包      →   FastArrayFragment       │
        │   (数组数据)         (数组包装)         (快速数组复制)          │
        │                                                                     │
        │   💡 核心思想:                                                      │
        │   - 不同的数据结构需要不同的"包装方式"                               │
        │   - Fragment 定义了数据如何被收集、序列化、传输、应用                 │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🎯 17.3.2 什么时候需要自定义 Fragment?

        PLAINTEXT
        🎮 真实案例:小刚的"技能系统"困境
        
        小刚在做一款 MOBA 游戏:
        - 每个英雄有 4 个技能- 每个技能有:冷却时间、充能数、等级、是否可用- 技能数据存在自定义的技能系统中,不是 UPROPERTY
        
        问题:Iris 默认只能复制 UPROPERTY!
        
        解决方案:自定义 SkillReplicationFragment

        场景

        为什么需要自定义

        解决方案

        🎮 技能系统

        数据不在 UPROPERTY 中

        自定义 SkillFragment

        📦 背包系统

        复杂的嵌套数据结构

        自定义 InventoryFragment

        🗺️ 地图数据

        大量动态生成的数据

        自定义 MapDataFragment

        🎨 外观系统

        需要特殊的压缩方式

        自定义 AppearanceFragment

        🏗️ 17.3.3 Fragment 工作原理

        PLAINTEXT
        📦 Fragment 生命周期
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │   发送方(服务器)                      接收方(客户端)             │
        │   ═══════════════                      ═══════════════             │
        │                                                                     │
        │   1️⃣ CollectPropertyData()                                         │
        │      从游戏对象收集数据                                              │
        │      │                                                              │
        │      ▼                                                              │
        │   2️⃣ PollPropertyData()                                            │
        │      检测哪些数据变了                                                │
        │      │                                                              │
        │      ▼                                                              │
        │   3️⃣ Serialize()                                                   │
        │      序列化成字节流                                                  │
        │      │                                                              │
        │      │  ════════════════════════════►                              │
        │      │         网络传输                                             │
        │      │                                                              │
        │                                        4️⃣ Deserialize()            │
        │                                           反序列化字节流             │
        │                                           │                         │
        │                                           ▼                         │
        │                                        5️⃣ ApplyPropertyData()      │
        │                                           应用到游戏对象             │
        │                                           │                         │
        │                                           ▼                         │
        │                                        6️⃣ CallRepNotifies()        │
        │                                           触发回调通知               │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🛠️ 17.3.4 简化示例:技能系统 Fragment

        CPP
        // SkillReplicationFragment.h
        
        /** 单个技能的数据 */USTRUCT()
        struct FReplicatedSkillData
        {
            GENERATED_BODY()
            
            UPROPERTY()
            float CooldownRemaining = 0.0f;
            
            UPROPERTY()
            uint8 CurrentCharges = 0;
            
            UPROPERTY()
            uint8 SkillLevel = 1;
            
            UPROPERTY()
            bool bIsAvailable = true;
        };
        
        UCLASS()
        class USkillReplicationFragment : public UReplicationFragment
        {
            GENERATED_BODY()
        
        public:
            /** 绑定到技能组件 */
            void BindToSkillComponent(class USkillComponent* InSkillComponent);
        
        protected:
            virtual void CollectPropertyData(FReplicationStateCollectParams& Params) override;
            virtual EPollPropertyDataResult PollPropertyData(FReplicationStatePollParams& Params) override;
            virtual void ApplyPropertyData(FReplicationStateApplyParams& Params) override;
        
        private:
            TWeakObjectPtr<class USkillComponent> SkillComponent;
            FReplicatedSkillData CurrentSkillData[4];
            FReplicatedSkillData LastSentSkillData[4];
        };
        
        // SkillReplicationFragment.cpp
        
        void USkillReplicationFragment::CollectPropertyData(FReplicationStateCollectParams& Params){
            if (!SkillComponent.IsValid()) return;
            
            // 从技能组件读取数据
            for (int32 i = 0; i < 4; ++i)
            {
                if (const FSkillInstance* Skill = SkillComponent->GetSkill(i))
                {
                    CurrentSkillData[i].CooldownRemaining = Skill->GetCooldownRemaining();
                    CurrentSkillData[i].CurrentCharges = Skill->GetCurrentCharges();
                    CurrentSkillData[i].SkillLevel = Skill->GetLevel();
                    CurrentSkillData[i].bIsAvailable = Skill->IsAvailable();
                }
            }
        }
        
        EPollPropertyDataResult USkillReplicationFragment::PollPropertyData(FReplicationStatePollParams& Params){
            CollectPropertyData(Params);
            
            // 检查是否有变化
            for (int32 i = 0; i < 4; ++i)
            {
                if (CurrentSkillData[i] != LastSentSkillData[i])
                {
                    return EPollPropertyDataResult::Dirty;
                }
            }
            return EPollPropertyDataResult::Clean;
        }
        
        void USkillReplicationFragment::ApplyPropertyData(FReplicationStateApplyParams& Params){
            if (!SkillComponent.IsValid()) return;
            
            // 应用数据到技能组件
            for (int32 i = 0; i < 4; ++i)
            {
                if (FSkillInstance* Skill = SkillComponent->GetSkillMutable(i))
                {
                    Skill->SetCooldownRemaining(CurrentSkillData[i].CooldownRemaining);
                    Skill->SetCurrentCharges(CurrentSkillData[i].CurrentCharges);
                    Skill->SetLevel(CurrentSkillData[i].SkillLevel);
                    Skill->SetAvailable(CurrentSkillData[i].bIsAvailable);
                }
            }
        }

        🌍 17.4 大规模多人游戏优化:当 1000 人同时在线

        💡 17.4.1 大规模游戏面临的挑战

        PLAINTEXT
        📊 数字会说话
        
        假设:1000 人同时在线的大逃杀游戏
        
        每个玩家需要知道的信息:┌─────────────────────────────────────────────────────────────────────┐│  数据类型            │  数量    │  每个大小  │  总计                 │├─────────────────────────────────────────────────────────────────────┤│  其他玩家            │  999     │  100 字节  │  99,900 字节          ││  可拾取物品          │  5000    │  20 字节   │  100,000 字节         ││  载具                │  500     │  50 字节   │  25,000 字节          ││  建筑状态            │  1000    │  10 字节   │  10,000 字节          │├─────────────────────────────────────────────────────────────────────┤│  总计/帧             │          │            │  ~235,000 字节        ││  × 60帧              │          │            │  ~14 MB/秒            │└─────────────────────────────────────────────────────────────────────┘
        
        💀 如果不优化:服务器爆炸,玩家卡成 PPT!

        🚀 17.4.2 优化策略一:空间分区

        PLAINTEXT
        🏙️ 日常类比:城市分区管理
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │   ❌ 不优化:市长管理全市 1000 万人                                  │
        │   ═══════════════════════════════════                               │
        │   ┌─────────────────────────────────┐                               │
        │   │         🏛️ 市政府               │                               │
        │   │    管理 1000 万人的所有事务     │  ← 累死!                     │
        │   └─────────────────────────────────┘                               │
        │                                                                     │
        │   ✅ 优化后:每个区长管理自己区的 10 万人                            │
        │   ═══════════════════════════════════════                           │
        │   ┌─────────┐ ┌─────────┐ ┌─────────┐                               │
        │   │ 🏢 A区  │ │ 🏢 B区  │ │ 🏢 C区  │                               │
        │   │ 10万人  │ │ 10万人  │ │ 10万人  │  ← 各管各的!                 │
        │   └─────────┘ └─────────┘ └─────────┘                               │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘
        
        实现:使用 NetObjectGridFilter
        - 把地图分成 100×100 的网格- 每个玩家只接收附近 9 个网格内的对象- 对象数量从 1000 降到 ~100
        INI
        ; 配置空间网格过滤[/Script/IrisCore.NetObjectGridFilterConfig]CellSizeX=10000.0  ; 100米一个格子CellSizeY=10000.0ViewDistance=3     ; 可见范围:3个格子 = 300米

        🚀 17.4.3 优化策略二:自适应更新频率

        PLAINTEXT
        💓 日常类比:心跳监测
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │   ❌ 不优化:所有人每秒检测 60 次心跳                                │
        │                                                                     │
        │   ✅ 优化后:根据状态调整频率                                        │
        │                                                                     │
        │   状态              │  更新频率  │  原因                            │
        │   ─────────────────────────────────────────────────────            │
        │   🏃 运动中         │  60 Hz     │  位置快速变化                    │
        │   🧍 静止           │  10 Hz     │  位置不变,降低频率              │
        │   🌄 远处           │  2 Hz      │  不重要,偶尔更新                │
        │   😴 休眠           │  0 Hz      │  完全停止更新                    │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🚀 17.4.4 优化策略三:增量压缩

        PLAINTEXT
        📹 日常类比:视频压缩
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │   ❌ 全量传输:每帧发送完整数据                                      │
        │   帧1: [完整数据 100字节]                                           │
        │   帧2: [完整数据 100字节]                                           │
        │   帧3: [完整数据 100字节]                                           │
        │   总计: 300 字节                                                    │
        │                                                                     │
        │   ✅ 增量传输:只发送变化的部分                                      │
        │   帧1: [完整数据 100字节]  ← 关键帧                                 │
        │   帧2: [差异数据 10字节]   ← 只有位置变了                           │
        │   帧3: [差异数据 5字节]    ← 只有朝向变了                           │
        │   总计: 115 字节 (节省 62%!)                                        │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        📊 17.4.5 优化效果对比

        指标

        未优化

        优化后

        提升

        带宽/玩家

        14 MB/s

        50 KB/s

        280x

        服务器 CPU

        100%

        30%

        3.3x

        复制延迟

        500ms

        50ms

        10x

        支持玩家数

        64

        1000+

        15x


        🎮 17.5 与 Gameplay 系统集成

        ⚔️ 17.5.1 与 GAS (Gameplay Ability System) 集成

        PLAINTEXT
        🎮 GAS 复制优化要点
        
        1. 使用 Push Model 优化属性复制
        2. 只复制"可见"的 GameplayEffect
        3. 实现技能预测与回滚
        CPP
        // GAS 属性使用 Push ModelUCLASS()
        class UMyAttributeSet : public UAttributeSet
        {
            UPROPERTY(ReplicatedUsing=OnRep_Health)
            FGameplayAttributeData Health;
            
            void SetHealth(float NewValue)
            {
                Health.SetBaseValue(NewValue);
                MARK_PROPERTY_DIRTY_FROM_NAME(UMyAttributeSet, Health, this);
            }
        };

        🎱 17.5.2 物理复制策略

        PLAINTEXT
        🎱 物理复制三种策略
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │  1️⃣ 关键帧同步                                                      │
        │     只在状态变化时同步,不是每帧                                     │
        │     适用:台球、保龄球等回合制物理                                   │
        │                                                                     │
        │  2️⃣ 客户端预测 + 服务器校正                                         │
        │     客户端先预测,服务器验证后校正                                   │
        │     适用:赛车、飞行等实时物理                                       │
        │                                                                     │
        │  3️⃣ 确定性物理                                                      │
        │     使用相同随机种子,确保结果一致                                   │
        │     适用:RTS、格斗等需要精确同步的游戏                              │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        🤖 17.5.3 AI 复制策略

        PLAINTEXT
        🤖 AI 复制三种策略
        
        ┌─────────────────────────────────────────────────────────────────────┐
        │                                                                     │
        │  1️⃣ 只复制"结果",不复制"过程"                                      │
        │     服务器运行 AI,客户端只接收行为结果                              │
        │     优点:带宽最小                                                  │
        │     缺点:客户端 AI 表现可能不流畅                                   │
        │                                                                     │
        │  2️⃣ 分层复制                                                        │
        │     重要 AI(Boss):完整复制                                        │
        │     普通 AI(路人):简化复制                                        │
        │     优点:平衡带宽和表现                                            │
        │                                                                     │
        │  3️⃣ 客户端 AI 代理                                                  │
        │     远处 AI 在客户端运行简化版行为                                   │
        │     优点:表现流畅                                                  │
        │     缺点:可能与服务器不同步                                        │
        │                                                                     │
        └─────────────────────────────────────────────────────────────────────┘

        📋 17.6 总结与最佳实践

        🎯 核心概念回顾

        组件

        作用

        难度

        使用场景

        🔍 Filter

        决定"谁能看到谁"

        ⭐⭐

        隐身、战争迷雾、阵营

        ⚡ Prioritizer

        决定"谁先更新"

        ⭐⭐

        战斗优先、目标锁定

        🧩 Fragment

        决定"怎么打包数据"

        ⭐⭐⭐

        自定义数据结构

        ✅ 最佳实践清单

        🔍 自定义 Filter

        • 继承正确的基类(UNetObjectFilter)

        • 实现所有必需的接口方法

        • 在 RemoveObject 中清理所有相关数据

        • 使用早期退出优化性能

        • 使用批量处理优化性能

        • 添加滞后机制避免闪烁

        ⚡ 自定义 Prioritizer

        • 可以继承内置 Prioritizer(如 USphereNetObjectPrioritizer)

        • 优先级值保持在 0.0-1.0 范围内

        • 考虑使用 SIMD 优化大量对象

        • 与 Filter 正确协作

        🧩 自定义 Fragment

        • 正确实现 CollectPropertyData 和 ApplyPropertyData

        • 实现 PollPropertyData 检测变化

        • 处理动态状态的内存管理

        • 测试序列化/反序列化的正确性

        🌍 大规模优化

        • 使用空间分区(GridFilter)

        • 配置合理的更新频率

        • 使用增量压缩

        • 实现休眠策略

        • 监控带宽使用情况

        🐛 常见问题速查表

        问题

        可能原因

        解决方案

        对象闪烁

        Filter 结果不稳定

        添加滞后机制

        更新延迟

        优先级太低

        调整 Prioritizer

        内存泄漏

        RemoveObject 未清理

        检查所有 Map/Array

        数据不同步

        Fragment 序列化错误

        检查 Serialize/Deserialize

        性能差

        未使用批量处理

        重构为批量操作

        📚 推荐学习路径

        PLAINTEXT
        第一周:理解基础
        ├── 阅读 UNetObjectFilter 源码
        ├── 阅读 UNetObjectPrioritizer 源码
        └── 运行内置组件的调试日志
        
        第二周:动手实践
        ├── 实现一个简单的自定义 Filter
        ├── 实现一个简单的自定义 Prioritizer
        └── 在测试项目中验证
        
        第三周:深入优化
        ├── 学习性能优化技巧
        ├── 实现批量处理
        └── 性能测试与调优
        
        第四周:高级主题
        ├── 实现自定义 Fragment
        ├── 研究大规模优化策略
        └── 与 GAS/物理/AI 集成

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

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