客户端工程决策手册:从问题到契约与证据
自动关联目录:客户端工程决策手册:从问题到契约与证据
这不是另一篇架构总览,而是一张入口表:先把症状归类,再找到必须建立的契约和可以证明结果的证据。具体实现、反例和取舍仍以链接文章为准。
一、先定位问题属于哪一层
客户端问题
├─ 数据不一致:谁是真相?身份和版本是什么?
├─ 生命周期错位:事实应该活过 Session、World、Actor 还是一次请求?
├─ 异步竞态:回调回来时,它是否仍有提交权?
├─ 线程边界:对象存活、数据同步和业务有效性是否被混为一谈?
├─ 半次变更:哪些写入必须作为一次事务提交?
├─ 多端分歧:Client、DS、编辑器分别拥有哪部分权威?
├─ 规模失控:是否把逻辑对象过早物化成 Actor、组件或重资源?
├─ 结果不可证:用什么确定性证据说明功能、性能和恢复正确?
└─ AI 自动化:模型能提出什么,宿主允许什么,谁负责验收?不要从“该用哪个 UE 类”开始。先回答真相、身份、生命周期、提交边界、权威和证据,再选择 Actor、Subsystem、GAS、Mass、ISM/HISM、Slate 或工具框架。
二、症状到决策的路由表
| 现场症状 | 首要问题 | 优先契约 | 深入阅读 |
|---|---|---|---|
| 同一个 ID 在不同 World、VM 或重建后串台 | 身份是否包含运行域和代次 | 稳定内容身份 + 本地句柄 + Domain + Generation | 对象身份、角色重建 |
| 初始化完成却偶发不可用 | 内部构建完成与外部可用是否混为一谈 | Readiness DAG + Session/World Generation | 启动就绪、可玩就绪 |
| 异步加载后对象已销毁或需求已变化 | 回调是否仍有提交权 | Operation Scope + Request/Owner/Generation 校验 | 异步提交、动画生命周期 |
| 后台归档偶发错乱,关闭时又与任务竞争 | 保活是否被误当成线程安全与操作所有权 | Pure C++ Job + Serial Executor + Commit Permit | 归档异步 |
| 退出时偶发丢记录,Pause 后线程无法恢复 | 队列是否缺少关闭、排空和 Join 协议 | Close + Drain/Abort + Event + Backpressure | 后台写盘 |
| 编辑器插件重载后回调重复或打开资产崩溃 | 全局注册是否仍能调用旧模块实例或代码 | Delegate Handle + Registration Ledger | 编辑器模块卸载 |
| 系统选择器的旧结果回到了新页面 | 平台回调是否携带本次请求身份 | RequestId + Caller Scope + Single Flight | 原生回调 |
| 脚本超时保护本身出现竞态或随机崩溃 | 监测线程是否越权修改 VM | Atomic Deadline Signal + Same-Thread Safepoint | 看门狗线程、脚本预算 |
| 一次编辑或导入失败后留下半成品 | 哪些写入属于同一原子意图 | ChangeSet + Preconditions + Prepare/Commit/Compensate | 编辑事务、写入回滚 |
| 超时后不知道远端动作是否发生 | “没有响应”是否被错误当成“没有发生” | Effect Handle + Idempotency Key + Reconcile | 发布恢复、取消语义 |
| Client 看着正确,DS 却不同意 | 表现、预测和权威是否分层 | Command/Receipt/State/Event + Authority Epoch | 交互同步、角色对称 |
| 某类 Actor 偶发不出现或换 Owner 后仍发错连接 | 路由与依赖边是否随实例语义迁移 | Route Revision + Owned Replication Edge | 复制路由、复制依赖 |
| 玩家高速移动、瞬移或跨区后短暂看不到对象 | 空间 Cell 的陈旧窗口是否有运动包络 | Conservative Cell + Event Fast Path + Overflow | 空间Cell |
| 每帧复制数量稳定,DS 网络耗时仍尖峰 | 数量是否被误当成字节和序列化预算 | Per-Connection Cost Budget + Initial Quota + Age | 复制预算 |
| 退组影响其他玩家,或晚加入组看不到既有对象 | 连接订阅是否误用了 Actor 全局生命周期 | Group Snapshot Revision + Connection Channel Close | 组相关性 |
| Fast Array 偶发读错 Key、漏变化或重复触发脚本 | 容器地址、Dirty 与事件语义是否混用 | Stable Key/Index + Typed State + Separate Event Log | FastArray |
| 对象数量增长后 Actor、Tick、复制一起爆炸 | 逻辑身份是否被绑定到最重表示 | 数据事实 + 选择性物化 + 空间工作集 | 海量对象、Actor迁移 |
| Draw Call 降了但 CPU、内存或编辑能力恶化 | 优化是否只看单一渲染指标 | Representation Contract + 能力与成本矩阵 | 网格合批、渲染伸缩 |
| AI/NPC 数量增长后 DS 帧时间失控 | 表现 LOD 是否被误当成模拟 LOD | 模拟分级 + 共享调度预算 + 玩法不变量 | AI规模化、AI选敌 |
| 预览看起来正确,正式内容却不可用 | 预览是否越过导入、平台和预算门禁 | Candidate -> Validate -> Build -> Promote | 生成资产、资产全生命周期 |
| Agent 能调用工具,但影响范围不可控 | Schema 是否被误当成授权 | Capability Grant + Canonical Resource + Effect Set | 工具权限、脚本隔离 |
| 自动化“完成率”提高,人工返工却更多 | 是否只评价最终回复 | 冻结任务 + 确定性 Gate + 修复/审查成本 | Agent评测、自动化验证 |
三、可复用契约
| 契约 | 最小字段 | 解决什么 | 不解决什么 |
|---|---|---|---|
StableHandle | logicalId, domain, generation | 跨重建引用和迟到结果拒绝 | 不证明目标当前可用 |
RevisionedSnapshot | revision, lineage, digest, payload | 冻结一次保存、构建或恢复输入 | 不负责外部副作用提交 |
OperationScope | operationId, ownerGeneration, cancelTree, deadline | 统一异步所有权和取消传播 | 不能撤销已提交效果 |
ClosablePipeline | state, queueBytes, shutdownPolicy, flushFence | 约束生产准入、Drain/Abort 与 Join | 不保证底层介质永不失败 |
RegistrationLedger | owner, removalToken, phase, removed | 保证模块按逆序撤销全局注册 | 不判断回调业务逻辑正确性 |
NativeRequest | requestId, kind, requester, session, deadline | 把跨语言结果交给正确调用方 | 不验证载荷内容安全 |
ReadinessGraph | node, prerequisites, generation, evidence | 区分 Decode、World、角色和可见就绪 | 不替代业务权威判断 |
ChangeSet | mutations, preconditions, verification, compensation | 原子表达跨文件和编辑器意图 | 不能让不可逆外部动作变可逆 |
EffectReceipt | idempotencyKey, handle, state, resultDigest | 对账远端与长任务的未知结果 | 依赖工具提供查询能力 |
AuthorityLease | owner, epoch, deadline, scope | 拒绝旧所有者和迟到释放 | 不等于永久持久状态 |
RuntimeOverlay | baselineRevision, sessionId, overrides | 隔离试玩和运行时变化 | 不自动合并回创作数据 |
ArtifactManifest | logicalAsset, sourceRevision, target, toolchain, digest | 证明制品属于哪个输入和目标 | 路径本身仍不是身份 |
RepresentationBinding | logicalId, representationKind, host, generation | 在 Actor、实例和数据记录之间切换 | 不保证不同表示语义一致 |
CapabilityGrant | principal, effects, targets, budget, expiry | 把授权限定到具体资源与影响 | Schema 正确仍不代表语义正确 |
EvidenceBundle | gates, receipts, runtime traces, environment | 让编译、运行、性能和审查结论可追溯 | 证据缺失时不能靠解释补齐 |
契约之间的依赖关系见 目标架构:按事实、执行、投影与制品划分边界。
四、设计前检查单
4.1 真相与身份
- 哪份数据是创作事实、运行时权威、派生索引和表现投影?
- ID 在跨 World、跨进程、对象复用和重建后是否仍唯一?
- 引用是否携带 Revision、Domain 或 Generation,而不是只存裸指针和名字?
- 缓存、Manifest、分类表能否从权威事实重建?
4.2 生命周期与异步
- 事实应归 GameInstance、World、Actor、组件、播放会话还是一次操作?
- Travel、PIE 重启、对象池复用、热重载和断线重连会改变哪个代次?
- 每个异步结果提交前是否重验需求、所有者、Revision 和运行角色?
- Cancel、Timeout、Failed、Unknown Outcome 和 Rolled Back 是否是不同状态?
- UObject 保活、工作线程访问许可和结果提交权是否分别证明?
- 后台消费者能否被唤醒,并在关闭时明确 Drain 或 Abort 后 Join?
- 模块、页面或 World 结束时,委托、Timer、Registry 和原生请求是否失效?
参考:生命周期归属、归档异步、后台写盘、编辑器模块卸载、原生回调。
4.3 UE 表示选择
- 该对象必须拥有 Actor 生命周期、网络通道、独立碰撞和脚本宿主吗?
- 只有可见、可交互或进入相关性的对象,是否可以按需物化?
- ISM/HISM、合批或代理表示失去了哪些编辑、碰撞、材质和变换能力?
- DS 是否需要渲染资产,Client 是否被迫承载只属于权威模拟的数据?
4.4 网络与玩法权威
- 客户端发送的是意图、预测还是事实?DS 会重新证明哪些资格?
- 状态、瞬时事件、命令和处理回执是否使用了不同通道?
- 晚加入、重连、丢包、乱序和所有权迁移后如何恢复?
- 视觉、动画和音频能否被纠正或取消,而不反向决定玩法结果?
- 路由、每连接相关性、调度优先级和实际传输预算是否分层?
- 连接离开兴趣集合时,是否误用了 TearOff 或全局 Destroy?
- 数量限流是否还验证了 Initial Channel、Bits、CPU 和最大等待年龄?
- Fast Array 承载的是可恢复状态,还是必须逐次消费的业务事件?
参考:交互资格、物理所有权、复制路由、复制预算、组相关性、FastArray。
4.5 性能与证据
- 成本是否按 Decode、Spawn、初始化、Tick、复制、渲染和回收阶段拆开?
- 预算约束的是任务数量,还是主线程时间、队列年龄和不可抢占长尾?
- 工作集是否绑定空间候选、活跃对象和变化集合,而不是每帧全量扫描?
- 指标是否包含 P95/P99、连续慢帧、阶段身份和 Client/DS 真实负载?
4.6 AI 辅助开发
- 模型输出是候选、假设、观察还是确定性证据?
- 上下文事实是否带来源、版本、信任级别、有效期和数据策略?
- 工具权限是否绑定资源、Effect、次数、预算和过期时间?
- 变更是否经过 Schema、编译、Client/DS 运行、性能和人工审批门禁?
- 评测是否记录返工、审查、回滚、取消、未知结果和资源成本?
五、伪代码目录
| 需要实现的机制 | 推荐入口 |
|---|---|
| 跨域对象句柄与代次校验 | 对象身份 |
| 快照保存、代际提交和多文件 Manifest | 保存快照、多文件保存 |
| 多关卡切换与大世界 Cell Prepare/Commit | 切换关卡、多关卡不是大世界 |
| 资产获取、安装、驻留和选择性物化 | 共享下载、缓存安装、运行时驻留 |
| 动画会话、层叠姿势与网络恢复 | 动画生命周期、姿势合成、动画同步 |
| 物理属性重建、权威迁移和快照原子应用 | 物理属性、物理所有权、物理快照 |
| 交互资格、Lease、预测与状态恢复 | 交互资格、交互占用、交互同步 |
| AI 感知、选敌、寻路、攻击和池化 | AI感知、AI选敌、AI寻路、AI战斗、AI池化 |
| 复制路由、空间相关性、连接预算与增量状态 | 复制路由、空间Cell、复制预算、组相关性、FastArray |
| 输入捕获、相机恢复和 UI 增量投影 | 输入路由、相机生命周期、UI物化 |
| 脚本 Capability、预算与热重载清理 | 脚本隔离、脚本预算、脚本热重载 |
| UObject 后台任务、可关闭线程和同线程 VM 中断 | 归档异步、后台写盘、看门狗线程 |
| 编辑器模块注册和原生平台请求关联 | 编辑器模块卸载、原生回调 |
| 导入冻结、质量门禁和可重现工具分发 | 导入提交、质量门禁、工具分发 |
| Agent ChangeSet、权限、取消和评测 | 写入回滚、工具权限、取消语义、Agent评测 |
六、判断方案是否真的更好
一个方案只有同时回答下面五个问题,才算架构改进,而不是换了一组类名:
- 守住了什么不变量:身份唯一、权威唯一、创作数据不被试玩污染,或同一 Revision 的多目标制品不互相覆盖。
- 缩小了什么工作集:从全部对象缩到空间候选、活跃集合、变化集合或明确相关性。
- 把失败变成了什么状态:可重试、可补偿、结果未知、需要对账,还是必须人工裁决。
- 付出了什么成本:更多元数据、状态机、存储、延迟、工具复杂度或内容约束。
- 拿什么证明:静态契约、单元测试、真实 Client/DS 运行、故障注入、平台构建、性能分位数和人工审查。
如果回答只能落在“更优雅”“更通用”或“模型认为正确”,就还没有完成工程决策。
许可协议:CC BY
作者:Davids
本文链接:https://hustjjd.github.io/d1e38495.html
更新于:2026年10月14日