高级客户端高频问答:UGC、Gameplay与跨端

这是一份口头作答卡,不是知识百科。每题先用第一段回答,面试官继续问时再展开“追问锚点”。默认回答时长 30 至 60 秒。

一、UGC 总体架构

1. 请描述一下 UGC 的客户端架构

我不会按 UI 页面罗列模块,而会按内容生命周期分层。最上面是创作交互,包括对象树、属性、Gizmo、资源和脚本编辑;中间是统一创作模型,定义对象身份、父子关系、属性、事件和事务;再下面是序列化、版本迁移、存储、校验和发布;内容进入试玩后,由脚本运行时、Gameplay 服务和服务端权威规则承载;外围还有资源管线、安全治理、性能门禁、自动化和可观测。核心边界是编辑态与运行态隔离、作者源与发布制品分离、对象身份跨环节稳定。

追问锚点:编辑交互 -> 创作模型 -> 保存发布 -> 运行时 -> 工程治理。

2. UGC 架构里最重要的抽象是什么?

我认为是稳定的对象模型和内容身份。编辑器、脚本、序列化、网络如果各用一套对象表示,复制、删除、Prefab、撤销和版本迁移都会变成特判。统一模型至少要有稳定 ObjectId、类型或能力描述、父子所有权、属性和事件接口;运行时可以生成优化后的实例,但不能丢失与作者数据之间的可追踪关系。

追问锚点:引用修复、未知字段透传、运行时实例不是编辑器实例。

3. 编辑器为什么不能直接操作运行时 Actor?

两者目标不同。编辑对象强调可修改、可序列化、可撤销和可迁移;运行时 Actor 强调帧性能、网络角色和稳定生命周期。直接共用实例会让编辑权限、临时状态和回调泄漏到试玩。更稳妥的做法是共享 Schema 与身份,在进入试玩时冻结作者数据并构建运行世界,退出时销毁运行态并恢复编辑上下文。

4. UGC 内容从保存到发布经历什么?

保存面向作者源,应该保留可编辑信息;发布面向运行时,要冻结一致快照,再经过依赖解析、资源处理、脚本检查、Schema 校验、性能与安全门禁,最终产出带版本和 Manifest 的不可变制品。整个过程要可重试、可诊断,失败不能污染上一次成功版本。

追问锚点:snapshot、processor DAG、content hash、atomic publish、rollback。

5. UGC 如何处理版本兼容?

我会把 Schema 版本、协议版本和制品版本分开。新增字段提供默认值,删除字段保留读取窗口,未知字段尽量透传;迁移过程做成幂等步骤,支持 dry-run、失败清单和源目标版本记录。安全条件和结构损坏不能靠兼容吞掉,必须明确拒绝。

6. UGC 如何保证安全?

安全不能只依赖发布前扫描。创作侧限制可引用资源和能力,脚本侧提供白名单 API、权限与预算,发布侧校验路径、类型、资源和依赖,运行侧仍由服务端校验影响公共状态的请求,并配合限流、超时和隔离。原则是用户输入永远不直接成为引擎权限。

7. UGC 性能怎么治理?

我会把性能预算变成发布门禁,而不是上线前人工体验。预算至少覆盖对象数、脚本时间、内存、DrawCall、资源体积和网络量;固定样例场景采样,结果绑定内容版本。客户端运行时再做可见性、分区加载、对象池和增量更新。没有场景、基线和采样口径的“体感优化”不能算结论。

8. 你做过 UGC 从零到一,具体是什么意思?

准确口径是我参与了平台从零到一,并独立负责若干客户端链路,不是我一个人主导整个平台。我直接做过地图数据与交付、编辑模式和部分元件能力、AI 配色与参考图、ASTC 处理,以及四足生物等内容;同时参与存储、上传下载、发布和长期线上维护。我的价值是把这些点逐步串成对完整内容生命周期的理解。

二、产品形态与 Roblox 对比

9. 元梦老 UGC 基于元件封装逻辑,Roblox 怎么设计?

元件式方案更像把成熟玩法能力封装成可配置积木,优点是上手快、质量和性能容易控制,缺点是表达能力受预制能力边界限制。Roblox 更接近统一实例树加脚本运行时:世界对象、服务和脚本都进入一套对象模型,脚本通过属性、事件和服务组合行为,客户端与服务端按运行上下文分工。它的优势是组合自由度高,代价是 API 治理、沙箱、安全、版本和性能治理更重。两者不是简单的新旧关系,而是“受控模板生产”和“通用可编程平台”的不同取舍。

追问锚点:Instance/DataModel、Property/Signal/Service、脚本沙箱、服务器权威。

10. 绿洲启元的开发模型与 Roblox 有什么区别?

按官方文档,绿洲启元是在 UE4 Gameplay Framework 上开放创作:以 Actor 为场景实体,DS 是权威端,GameMode、GameState、PlayerController、PlayerPawn、PlayerState 分别承载流程、全局复制状态、玩家 RPC、角色和队伍数据;UI 只在 Client。玩法逻辑可以使用模式编辑器的 Event、Condition、Action,也可以绑定 Lua;GamePart 再提供可配置启用的模块化能力。Roblox 则以 DataModel/Instance 统一组织世界对象,通过 Service、Luau、事件和模块组合行为。两者都有脚本与服务器权威,主要区别是“UE Gameplay Framework 加领域化开放”与“统一对象树加通用脚本平台”,不能把产品愿景直接当成软件架构。

追问锚点:Actor、GameMode/GameState、PlayerController、属性复制/RPC、ECA、Lua、GamePart。官方依据:绿洲启元玩法开发框架介绍。

11. 元件式 UGC 应该怎样演进?

不应该直接从元件跳到完全自由脚本,而应逐步把元件背后的共性抽出来:先统一对象身份和属性系统,再把能力拆成可组合组件与事件,然后开放受控脚本和服务 API,最后补齐调试、版本、预算和安全。这样既保留旧内容和低门槛体验,也能让高级创作者突破固定模板。

12. 你会怎样设计一个面向非专业创作者的 UGC?

我会提供三层入口:模板负责快速开局,组件和事件编排覆盖大多数玩法,脚本解决长尾表达。三层最终落到同一个对象模型和发布管线,避免“简单模式”和“高级模式”形成两套不兼容系统。产品上优先保证可发现、可预览、可撤销和错误可解释,而不是一开始开放最多 API。

三、UGC1.0 到 UGC2.0

13. 为什么要做 UGC2.0?

UGC1.0 可以快速交付大量元件和玩法,但随着页面、对象类型和团队增加,容易出现界面与引擎状态耦合、协议分散、编辑态和运行态边界模糊、构建交付依赖人工等问题。UGC2.0 的目标不是换一套 UI,而是把对象模型、脚本运行时、页面协议、制品和多端工程收敛成可扩展的平台边界。

14. 请描述 UGC2.0 的客户端架构

我从创作内容进入运行时来讲。TypeScript/React 承担创作界面;页面协议和 PixUI 原生宿主管理页面栈、生命周期与跨语言消息;编辑领域层管理选择、属性、Gizmo、事务和撤销;核心数据由对象模型、属性、事件和服务表达,Luau 通过 C++ binding 使用引擎能力;试玩阶段隔离 Edit、Client 和 Server 角色;保存发布把作者源冻结并经处理流水线生成可验证制品;外围由多端构建、自动化、性能和诊断体系支撑。核心边界是对象身份、VM 隔离、页面代际和源数据/制品分离。

15. 你在 UGC2.0 具体做了什么?

我主要做四块。第一是 PixUI 页面基础设施,包括页面协议、页面栈、生命周期和原生宿主;第二是 TypeScript、Lua、C++ 的跨语言通信及状态版本;第三是制品生成、二进制化和失败清理接入;第四是 Android/iOS 构建、试玩恢复与 UI 自动化。对象模型、三 VM 和完整运行时不是都由我设计,但它们是我的上下游,我能说明接口和关键边界。

16. UGC1.0 与 2.0 的本质差异是什么?

1.0 更偏功能仓和元件供给,复杂度主要由功能数量带来;2.0 更偏平台内核,先约束对象、协议、生命周期和制品,再让业务扩展。前者容易依赖模块约定和人工联调,后者要把约定变成可验证的机制。2.0 并不意味着全部重写,旧内容迁移和渐进兼容本身就是架构的一部分。

四、PixUI 与跨语言通信

17. 你说做了 PixUI 封装,具体封装了什么?

“PixUI 封装”太泛,我实际做的是页面运行基础设施:页面注册与路由、push/replace/overlay 栈语义、created/active/suspended/disposed 生命周期、原生 View 所有权、跨语言消息结构、状态版本和销毁清理。目标是让业务页面只依赖稳定协议,不直接持有引擎对象或各自处理 PixUI 差异。

18. TypeScript、Lua、C++ 怎么通信?

TS 先调用类型化 transport,把命令编码成带 type、seq 和 data 的 JSON,经 PixUI 官方消息通道进入 C++;C++ 转发到当前 Edit VM 的 Lua callback,由编辑服务分发给领域 handler。回程由 Lua 附加 seq、pageId、generation、domain 和 revision,优先走 PostMessage,同时保留全局队列作为兼容通道;JS 按 seq 去重并确认消费。Lua 调 C++ 是另一层:C++ 向 VM 注册 API,Lua 用它管理原生 View、回调和引擎能力。客户端与服务端 Luau RPC 又是独立链路,不能混为一谈。

追问时先画四条箭头:TS -> C++ -> Lua、Lua -> C++ -> TS、Lua <-> C++ API、Client Lua <-> Server Lua RPC。

19. 为什么不让 TypeScript 直接调用 C++?

直接暴露会把页面、引擎对象和业务能力耦在一起,也不利于浏览器预览、Mock 和协议测试。现在 TS 依赖 transport,Lua 负责编辑领域编排,C++ 负责原生资源和引擎能力。代价是序列化与协议治理成本,所以必须补类型定义、版本、批量、日志和自动化。

20. 为什么同时有 PostMessage 和队列?不会重复吗?

双通道是兼容和时序兜底,不是两套业务语义。它确实可能重复,所以两条通道共享同一 seq,JS 只消费第一次并回写 Ack;队列需要容量和过期策略。若目标平台上的 PostMessage 已经稳定,可以逐步收敛,但迁移期不能假设所有页面和版本同时升级。

21. pageId、generation、revision 分别解决什么?

pageId 标识逻辑页面,generation 标识同一页面的第几次创建,revision 标识某个状态域的版本。页面关闭重开后,旧异步消息会因 generation 过期被拒绝;同一代页面内,只接受更高 revision,发现版本缺口就重拉快照。前者防止旧生命周期污染新页面,后者防止乱序增量覆盖新状态。

22. 页面销毁最容易漏什么?

不是只销毁 React 组件。还要停止订阅、解绑 C++ Delegate、释放 Lua registry 引用、移除 UMG/Viewport 宿主、取消或隔离异步任务,并让旧 generation 的回调失效。否则常见现象是页面重开后收到两次消息、旧 View 写入新状态,或者 VM 关闭后异步回调重入。

23. 高频编辑状态怎样避免跨语言卡顿?

先按选择、属性、网格等 domain 切状态,首次传快照,后续传带 revision 的增量;拖拽一类高频消息按帧和 key 合并,避免每个输入事件都序列化和触发 React 更新。诊断时用同一关联标识分别测 Lua 生成、JSON、C++ 传输、JS 解析、React diff 和 PixUI 绘制,不能只优化一层。

五、Gameplay 与 PVE

24. 请描述一个 PVE 客户端架构

我会按一次对局生命周期来讲。会话层负责组队、匹配、加载和重连;服务端权威的规则层推进阶段、目标、撤离和结算;Gameplay 层承载角色、Ability、武器、Buff、交互和物品;世界层负责房间、机关、怪物、导航和生成;UI 只订阅领域状态;底层由复制、RPC、配置、存档、资源和诊断支撑。关键是分离权威状态与表现状态,用快照加版本增量支持首次进入和重连,并把局内收益写入局外资产做成幂等事务。

25. 你在 PVE 里具体做了什么?

我直接投入最多的是新手流程、智能提示和镜头步骤,局内外时间线与客户端/服务端联调,任务、HUD、小地图和配置接入,以及撤离、装备、仓库、埋点等跨系统问题。此前 PVP 阶段还做过物品道具、交互对象和 Gameplay Ability 接入。地牢生成、完整怪物 AI 和底层战斗框架不是我独立负责的,我对它们的完整理解来自协作接口和后续源码复盘。

26. 新手流程为什么难?

它不是 UI 顺序,而是跨系统、可恢复的状态机。任务、AI、导航、相机、战斗和 UI 都是异步的,任何回调丢失都可能卡死。我会为每一步定义进入条件、完成条件、监听资源、恢复检查、超时和取消;关键里程碑由权威状态保存,重进后根据当前世界重算,而不是等待已经错过的事件。

27. 物品与交互系统如何设计?

先把“可交互对象”“交互请求”和“产生的 Gameplay 结果”分开。客户端负责发现候选、提示和输入,服务端复核距离、权限、占用和对象状态,再执行拾取、开箱或使用事务。对象要有稳定身份和明确状态机,重复请求依靠事务号或幂等检查处理,表现层消费确认结果并允许必要的预测。

28. Gameplay Ability 的一次执行链路是什么?

输入先映射到 Ability 激活请求,系统检查标签、资源、冷却和当前状态;通过后 Commit 成本与冷却,执行目标选择、动画和任务节点,命中结果由权威侧确认,再施加 Effect 并触发 Cue。取消、死亡、切换地图等路径必须能中断任务并释放监听。多人环境下要区分预测表现、服务端确认和纠正。

29. 客户端预测与服务端权威如何平衡?

影响公共结果的命中、伤害、掉落和资产变化由服务端决定;客户端预测输入响应、动画和非关键表现,降低操作延迟。预测必须带可关联标识,确认时去重,拒绝时回滚或平滑纠正。并不是预测越多越好,状态代价大、作弊收益高或难以回滚的行为应减少预测。

30. 重连怎样恢复局内状态?

重连不能靠把历史事件重放一遍。服务端提供带版本的权威快照,包含会话、阶段、角色、关键实体和个人状态;客户端先重建基线,再接收高于快照版本的增量。UI 和表现从领域状态派生,临时动画可以丢弃或重播,但资产和流程状态不能靠本地记忆恢复。

六、工程能力与高级岗位

31. 讲一个最复杂的问题

我会讲 UGC2.0 页面生命周期与消息可靠性。复杂点不在单次通信,而在 JS 页面、C++ View、Lua 服务的生命周期不一致,页面切换和异步回调会制造旧消息、重复订阅和资源泄漏。方案是让原生 host 成为 View 唯一 owner,用 pageId/generation 管实例,用 domain/revision 管状态顺序,统一 dispose 清理,并用序号、去重和 Ack 管双通道。这个案例能同时说明协议设计、生命周期、跨语言定位和测试。

32. 讲一个性能问题

我优先讲移动端 ASTC 或跨语言 UI 卡顿。回答结构固定为:先定义用户场景和性能指标,再分阶段埋点定位瓶颈,说明为什么选择当前方案、付出的内存或复杂度代价,最后说明怎样回归和防止反复。没有同口径数据时,我只说定位方法和已验证行为,不编造提升百分比。

33. 讲一个线上问题

我会选择偶现引导线、页面旧回调或制品失败清理中的一个。先说明为什么难复现,再讲我补了哪些身份、阶段和错误上下文,怎样从日志排除分支,最后落到可恢复状态机、generation gate 或原子提交等机制。高级岗位回答不能停在“加判空修好了”。

34. 你怎样做技术方案?

我先确认目标、非目标和不可变约束,再画状态所有者、生命周期与关键数据流;对候选方案比较一致性、性能、兼容、迁移和诊断成本;实现时先打通最小闭环,再补失败路径、观测、自动化和发布策略。评审材料里会明确未知项和验证计划,不用抽象词掩盖风险。

35. 你如何处理跨团队协作?

跨团队问题通常不是“多沟通”就够,而是接口缺少可验证契约。我会把输入输出、版本、错误码、超时、重试和负责人写清,用最小复现场景或协议测试对齐;联调时让日志带同一个事务或消息标识。这样争议会从“哪边有问题”变成“哪条契约没有满足”。

36. 你做的工作如何体现高级工程师水平?

我不靠功能数量证明高级,而看是否降低系统和团队的不确定性。UGC1.0 让我理解完整内容链路,Gameplay 让我处理权威、表现和跨系统状态,UGC2.0 则让我把页面、协议、制品和自动化收敛成可复用边界。我能独立交付复杂模块,也能把一次修复沉淀成版本、生命周期、工具或质量门禁。

七、现场回答规则

  1. 架构题先给坐标系,再从一条主链路展开,不要直接报模块名。
  2. “你做了什么”只说直接负责和深度参与;团队全貌单独标注。
  3. 每题最多埋两个追问钩子,不主动倾倒全部细节。
  4. 遇到不确定实现,回答“我当时的边界是……;结合源码,我的理解是……”。
  5. 所有指标准备好基线、采集方式和时间范围,否则删掉数字。