复制预算:每帧N个Actor不是网络预算
自动关联目录:复制预算:每帧N个Actor不是网络预算
核心判断:Actor 数量可以做保险丝,却不能代表带宽或 CPU。一个首次开 Channel 的复杂 Actor,可能比几十个无变化 Actor 更贵;预算必须作用于每个连接,并同时考虑估算字节、序列化时间、首包、年龄和优先级。
一、业务冲突:峰值要压住,关键状态又不能饿死
大量 Actor 同时进入相关性时,首包需要创建 Channel、发送初始属性和子对象;若全部同帧处理,DS 序列化时间和连接队列会尖峰。若只做硬数量截断,昂贵 Actor 仍能打穿帧预算,低优先级对象也可能长期排不到。
二、实际实现:按类累计数量,并单独限制首次复制
工程在 UE 已完成候选合并和优先级排序后,按连接重置一组类计数器。配置可沿继承层级生效,并区分总 Actor 数与首次开 Channel 数;超过上限的 Actor 本帧跳过。引擎随后真正序列化,写包后再检查连接是否饱和。
值得保留的是两个决定:预算是每连接而非全局的;首次复制有独立上限,因为它通常最贵。类继承配置也能让一个基础类别统一受控。
三、结构性缺陷:计数发生在知道真实成本之前
检查器在序列化前递增计数,统计的是“尝试准入”,不是成功写入;同一 Actor 命中多级类规则时还会同时消耗多个桶。两个 Actor 即使复制字节相差百倍,也都记作 1。真正的包饱和仍由 NetDriver 在写入后发现,因此数量限制只能削峰,无法保证时间和字节上限。
只按类也表达不了状态紧急度:刚受击、正在交互、长时间未同步和纯装饰更新的价值不同。固定首包数若没有等待年龄与保底额度,重连或密集入场时会把首次可见延迟拉长。
四、目标设计:预测准入,真实成本回填
bool Admit(Connection& c, Candidate& x) {
Cost est = Ewma[x.costClass].Estimate(x.isInitial, x.dirtyMask);
Score s = Priority(x) + AgeBoost(x.waitFrames) + Urgency(x);
if (!c.deficit.CanSpend(est, s)) return false;
c.deficit.Reserve(est);
return true;
}
void OnSerialized(Candidate& x, int64 bits, double usec) {
Ewma[x.costClass].Observe(x.isInitial, bits, usec);
CurrentConnection.Reconcile(bits, usec);
}对象数上限保留为灾难保险丝;主预算使用字节、序列化微秒和 Initial Channel 配额。估算不可能完全准确,所以仍保留 UE 的连接饱和硬边界,并用实际 BitsWritten 和 CPU 回填 EWMA。等待年龄必须提高分数,关键状态可借用预算但形成可观测债务。
五、决策与验证
| 维度 | 作用 |
|---|---|
| Actor 数 | 防止候选循环失控 |
| Initial 数 | 限制 Channel 创建与完整快照尖峰 |
| Bits | 约束链路和发送队列 |
| Serialize μs | 约束 DS 帧时间 |
| Wait Age | 防止长期饥饿 |
| Urgency | 保住受击、死亡、交互等关键事实 |
验证看每连接 P95/P99 序列化时间、实际/估算 Bits 偏差、首次可见延迟、最大等待帧、预算债务、饱和次数和关键状态超期率。只看到“每帧复制 Actor 数稳定”不能证明网络预算稳定。