滚动限频:平均分桶不能证明最大等待时间

自动关联目录:滚动限频:平均分桶不能证明最大等待时间

核心判断:把全量状态均匀分成若干桶,可以降低平均每帧候选数,却没有自动保证最大等待时间、变更紧急度和成员变化时的公平性。兼容相关性全扫描则只能作为有规模上限的迁移通道。

一、业务冲突:全局玩家状态要可恢复,又不必每帧全发

记分、角色摘要等状态常常对所有连接有意义,但大部分帧没有紧急变化。全部常驻高频复制会浪费候选处理;完全依赖事件又会让晚加入和丢包恢复困难,因此需要可轮转的状态同步。

二、实际实现:每帧重建桶,按全局帧号取一个

当前节点每帧遍历 World 中全部玩家状态,按固定容量顺序切成多个 Actor 列表;每个连接使用 FrameNumber % BucketCount 选择同一桶。重建列表的好处是断线后天然压紧,不需要维护洞;每帧候选数量也有明确上限。

同时,工程保留了一个兼容节点:对其中每个 Actor、每个连接 Viewer 调用 IsNetRelevantFor。代码注释已经把它定位为旧逻辑兼容并要求控制数量,这是正确的迁移态度。

三、结构性缺陷:发送降频了,准备工作没有降频

玩家状态仍被每帧完整迭代和重新分桶,准备成本是 O(N)。玩家增删会改变桶数和数组位置,Frame % BucketCount 随之跳变;“理论上每若干帧轮到一次”不等于有稳定的最大年龄。普通变更与刚发生的关键变更使用同一轮转路径;节点虽然保留强制更新列表,但当前实现中没有看到该列表的填充闭环。

兼容节点更直接:成本是 O(Connection * Actor * Viewer),一旦它从少量例外变成默认逃生口,Replication Graph 只是把全扫描换了位置。

四、目标设计:稳定成员、年龄优先、Dirty 快路

void Gather(Connection& c, Budget b) {
    for (ActorId id : DirtyUrgent.PopVisible(c, b.urgentQuota))
        Emit(id);

    while (b.HasRoom()) {
        ActorId id = StableRing.Next(c.cursor);
        Score s = Age(c, id) + Priority(id);
        if (Eligible(c, id, s)) Emit(id);
        c.cursor.Advance();
    }
}

成员只在加入/离开事件更新,使用稳定 ID 而非每帧 World Iterator 顺序。每连接记录最后成功发送时间;Dirty 进入有上限的紧急队列,普通状态按年龄和优先级轮转。桶仍可用于批量存储,但必须以 max_age 而非“平均每帧 N 个”验收。

兼容相关性节点设置 Actor 数硬上限、逐类迁移清单和成本告警。能表达为空间、Owner、组或依赖关系的类必须离开兼容节点;真正自定义的少数对象才保留逐连接函数。

五、验证指标

记录重建耗时、成员变更数、每连接最大/分位等待时间、Dirty 到发送延迟、轮转重复/跳过次数、兼容节点 Actor 数和 IsNetRelevantFor 调用量。压测要在持续上下线、低服务器帧率、连接拥有多个 Viewer 和 Dirty 风暴下验证;平均吞吐稳定但最大年龄无界,仍是不合格调度器。