UGC 客户端架构:面试问答与白板稿
这篇只解决一件事:面试官让你描述 UGC 客户端架构时,能够先给结论,再画图,最后落到自己的工作。正文中的回答可以直接说;图和追问用于继续展开。
面试官:请描述一下 UGC 的客户端软件架构
30 秒回答
UGC 客户端不只是一个编辑器界面,它要解决内容从创作到运行的完整闭环。我通常分成五层:界面层负责对象树、属性面板和 Gizmo;编辑领域层负责命令、事务、撤销和编辑服务;内容模型层定义对象、组件、属性、引用和版本;运行时层负责脚本 VM、C++ Binding、World 和 Actor;工程交付层负责保存、构建、发布、加载和诊断。核心问题有三个:状态由谁拥有,编辑数据怎样变成运行对象,作者源怎样变成可发布制品。
记忆顺序:界面 -> 编辑领域 -> 内容模型 -> 运行时 -> 工程交付。
90 秒回答
UGC 客户端同时包含创作工具和玩法运行时,所以我不会按页面目录介绍,而会按内容生命周期拆分。
第一层是界面,包括对象树、属性、Gizmo、资源和脚本编辑器。它只负责交互和展示,不应该成为业务状态的唯一来源。第二层是编辑领域,负责选择集、命令、事务、撤销重做和编辑服务。第三层是内容模型,用稳定 ID 表达对象、组件、属性、父子关系、资源引用和 Schema 版本,这是保存和迁移的基础。
第四层是运行时。脚本通过 C++ Binding 使用引擎能力,内容模型在试玩或正式运行时被转换成 World、Actor、组件和网络对象。编辑、客户端和服务端的权限及生命周期必须隔离。第五层是工程交付,负责作者源保存、快照冻结、依赖检查、构建、Manifest、发布、下载和诊断。
这几层最终形成两条主链:一条是“编辑操作如何修改内容”,另一条是“内容如何变成客户端和服务端能够运行的版本”。我直接参与较深的是页面协议和原生宿主、JS/C++/Lua 通信、制品接入、多端构建和自动化验证。
白板图
flowchart TB
UI["界面层<br/>对象树 / 属性 / Gizmo / 资源 / 脚本"]
EDITOR["编辑领域层<br/>命令 / 事务 / 撤销 / 编辑服务 / 页面生命周期"]
MODEL["内容模型层<br/>对象 / 组件 / 属性 / 引用 / Schema / 稳定 ID"]
RUNTIME["运行时层<br/>脚本 VM / C++ Binding / World / Actor / 网络"]
DELIVERY["工程交付层<br/>保存 / 快照 / 构建 / Manifest / 发布 / 诊断"]
UI -->|用户操作| EDITOR
EDITOR -->|命令与事务| MODEL
MODEL -->|运行时投影| RUNTIME
MODEL -->|冻结作者数据| DELIVERY
DELIVERY -->|加载制品| RUNTIME画图时按这个顺序说:
- UI 不拥有最终业务状态。
- 编辑操作通过命令和事务修改内容模型。
- 内容模型与运行时 Actor 不是同一个实例。
- 保存作者源与发布运行制品是两件事。
面试官:一份 UGC 内容怎样从编辑器进入游戏?
直接回答
创作者的操作先被转换成可撤销命令,命令修改带稳定身份和版本的内容模型。保存得到的是可继续编辑的作者源;试玩或发布前,系统冻结一份确定快照,再进行依赖解析、Schema、资源、脚本、性能和安全检查。构建成功后生成制品、Manifest 和版本,客户端与服务端加载同一语义版本。失败时只返回可定位错误,不能覆盖上一个成功版本。
白板图
sequenceDiagram
participant U as 创作者
participant E as 编辑器
participant M as 内容模型
participant B as 构建发布
participant C as Client
participant S as Server
U->>E: 编辑对象、属性、脚本和资源
E->>M: 提交命令与事务
M->>M: 更新对象、引用和版本
E->>M: 保存作者源
U->>E: 请求试玩或发布
E->>B: 冻结快照与依赖清单
B->>B: 校验并生成制品
alt 构建失败
B-->>E: 返回结构化错误
else 构建成功
B-->>C: 加载制品与版本
B-->>S: 加载同一语义版本
C->>S: 输入和 Gameplay 请求
S-->>C: 权威状态与复制结果
end必须主动说出三组区别:
保存 != 发布作者源 != 运行制品客户端表现 != 服务端权威
常见追问
为什么要冻结快照?
构建期间用户可能继续编辑。直接读取变化中的工作区,会让一次构建混入两个版本,结果无法复现,也无法可靠重试。
为什么作者源和制品要分开?
作者源追求可编辑、可迁移和可诊断;运行制品追求紧凑、加载快和不可变,两者目标不同。
发布失败怎样恢复?
临时产物与正式版本隔离,只有全部检查通过后才更新线上版本指针。失败保留上一成功版本,并返回具体阶段和对象的错误。
面试官:Edit、Client、Server 三种 VM 是什么关系?
直接回答
三种 VM 表示三种执行角色,不代表一个进程里永远同时常驻三个 VM。Edit VM 拥有工程读写、撤销和编辑服务;Client VM 负责本机输入和表现;Server VM 负责公共规则和权威状态。它们可以共享对象语义和 API 设计,但不能共享运行实例、World 指针或脚本引用。进入试玩本质上是执行域切换:冻结编辑数据,释放编辑态引用,再用同一份制品建立客户端和服务端运行环境。
白板图
flowchart TB
subgraph AUTHOR["编辑阶段"]
direction LR
WEB["TS/TSX 页面"] --> HOST["C++ 页面宿主"]
HOST --> EDIT["Edit VM"]
EDIT --> EWORLD["Editor World / 编辑服务"]
end
SNAP["纯值快照 / Artifact"]
subgraph PLAY["试玩阶段"]
direction LR
CLIENT["Client VM<br/>输入 / 表现"]
SERVER["Server VM<br/>规则 / 权威状态"]
CLIENT <-->|RPC / Replication| SERVER
end
EDIT -->|冻结数据| SNAP
SNAP --> CLIENT
SNAP --> SERVER常见追问
为什么不能共用一个 VM?
三个角色的权限、生命周期和性能预算不同。共用 VM 容易让编辑权限泄漏到游戏,也会让客户端脚本接触服务端权威状态。
跨 VM 能传什么?
传稳定 ID、版本和纯值数据,不传旧 Lua 引用、UObject 指针或 World 内实例。
进程拓扑可以变化吗?
可以。试玩客户端可能与编辑器同进程,也可能独立;DS 通常是独立进程。进程可以变化,但角色和权限边界不能变化。
面试官:TypeScript、C++、Lua 是怎么通信的?
直接回答
TypeScript 只存在于开发阶段,运行时执行的是编译后的 JavaScript。页面把命令编码成类型化消息,通过 PixUI 的消息通道进入 C++ 页面宿主;C++ 根据当前页面和 VM 生命周期做路由,再调用 Edit VM 中注册的 Lua 回调。反向链路由 Lua 调用 C++ 注册接口,再由 C++ 把消息投递给当前 JS Context。Lua 使用引擎能力是另一条 Binding 链路;Client Lua 与 Server Lua 的 RPC 或复制又是联机链路,不能把这几条链混成一句“TS 直接调用 Lua”。
画四条箭头
JS -> C++ -> Edit Lua
Edit Lua -> C++ -> JS
Lua <-> C++ Binding
Client Lua <-> Server Lua RPC / Replication常见追问
C++ 为什么不能只做透传?
C++ 拥有原生 View、JS Context 和 VM 生命周期,必须负责目标校验、销毁隔离和线程边界,否则旧页面回调很容易写入新页面。
怎样处理旧消息和乱序?
用 pageId + generation 区分页面实例,用 domain + revision 区分状态版本;异步回调返回时重新校验目标是否仍然有效。
高频消息怎样优化?
首次传快照,后续传增量;拖拽等高频输入按帧合并。性能分析要分开测 Lua 生成、序列化、C++ 转发、JS 解析和页面渲染。
面试官:元件封装式 UGC 与 Roblox 有什么区别?
直接回答
元件式方案把成熟玩法能力封装成可配置对象,创作者通过属性、事件和组合完成逻辑。它的优势是上手快、质量和性能边界容易控制;问题是新需求经常需要平台继续增加元件,组合复杂后类型和连接规则会膨胀。
Roblox 的底座更接近统一的 DataModel/Instance 层级加 Luau 运行时。世界对象、Service 和脚本处于同一套对象模型中,行为通过属性、事件、模块和服务组合。它的表达上限更高,但平台要承担更重的 API 稳定性、沙箱、安全、复制、兼容和性能治理。
一句话总结:前者从受控领域能力出发,后者从统一对象模型和通用脚本能力出发。更现实的演进通常不是删除元件,而是让元件、模板和可视化逻辑都建立在统一对象与脚本底座上。
面试官:绿洲启元的开发模型与 Roblox 有什么区别?
直接回答
根据绿洲启元官方文档,它是在 UE4 Gameplay Framework 上开放玩法创作。场景实体以 Actor 为基础,DS 是权威端;GameMode 负责服务端玩法流程,GameState 负责全局状态复制,PlayerController 处理玩家与服务端交互,PlayerPawn 承载角色能力,PlayerState 保存队伍可见数据,UI 只存在于 Client。
在逻辑表达上,绿洲既提供模式编辑器的 Event、Condition、Action,也允许可编程蓝图和自定义 Condition、Action 绑定 Lua;GamePart 再提供可配置启用的模块化 Gameplay 能力。Roblox 则用 DataModel/Instance、Service 和 Luau 组织平台。
所以两者的主要区别是:绿洲复用 UE 的 Actor、Gameplay Framework 和网络模型,在上面叠加领域化编辑器与 Lua;Roblox 从统一对象树和通用脚本运行时出发。这里不能根据“类绿洲”这个产品称呼,自行推导跨世界、社交或经济系统。
对比表
| 维度 | 绿洲启元公开模型 | Roblox 公开模型 |
|---|---|---|
| 对象基础 | UE Actor 与 Gameplay Framework | DataModel / Instance |
| 玩法角色 | GameMode、GameState、PlayerController、PlayerPawn、PlayerState | Instance 类型、Service、脚本上下文 |
| 逻辑表达 | 模式编辑器 ECA、Actor 绑定 Lua、自定义脚本 | Luau、属性、事件、ModuleScript、Service |
| 网络边界 | DS 权威、Actor 属性复制、RPC | Client/Server 分域、复制与远程通信 |
| 功能复用 | GamePart、模板、GlobalActor API | Model、Package、ModuleScript、资产生态 |
面试官:UGC1.0 到 UGC2.0 的核心变化是什么?
直接回答
我理解 1.0 更偏功能仓和元件供给,目标是快速提供可用能力;随着对象、页面和团队增加,状态、协议、生命周期和交付链容易分散。2.0 更偏平台化:先统一对象身份、脚本执行域、页面协议和制品规范,再让业务能力在这些边界上扩展。
所以 2.0 的重点不是把 UI 换成 PixUI,也不是简单地引入脚本,而是把过去依赖人工约定的事情变成可验证机制,例如页面代际、状态版本、VM 权限、制品身份、失败清理和多端自动化。
不要这样回答
- “1.0 是元件,2.0 是脚本。”两代系统都可能同时存在元件和脚本。
- “2.0 就是换了 Web UI。”这只描述了表现技术。
- “整个 2.0 架构都是我设计的。”无法通过职责追问。
面试官:你在 UGC2.0 中具体做了什么?
直接回答
我主要负责四类工作。第一是 PixUI 页面基础设施,包括页面协议、页面栈、生命周期和 C++ 原生宿主;第二是 JavaScript、C++、Lua 之间的双向通信和状态版本;第三是作者数据到制品链路中的处理器、Manifest、二进制格式接入和失败清理;第四是 Android、iOS 构建、试玩恢复和 UI 自动化。
完整对象模型、三 VM 和网络运行时不是我一个人设计的,它们是我的上下游。我能够讲清接口、生命周期和故障边界,但面试时会把个人实现、团队架构和复盘后的设计理解分开。
继续追问时怎么展开
| 追问方向 | 首选案例 |
|---|---|
| 页面为什么会重复回调 | pageId + generation、统一销毁、旧回调隔离 |
| 状态为什么会乱序 | domain + revision、快照与增量、缺口重拉 |
| JS/C++/Lua 怎样通信 | 正向命令、反向状态、Binding、联机 RPC 四条链 |
| 制品失败怎样处理 | 输入快照、阶段记录、临时产物隔离、失败清理 |
| 多端怎样保证一致 | 同一制品身份、构建矩阵、自动化证据和回归 |
面试官:如果让你重新设计一套 UGC 客户端,你会怎么开始?
直接回答
我不会先堆页面和功能,而会先确认目标创作者、内容类型、运行平台和开放边界。然后定义统一对象身份、Schema、权限和性能预算,打通一条最小闭环:创建对象、编辑、保存、冻结、构建、发布、加载和联机运行。最小闭环可验证后,再增加元件、脚本、资产、协作和商业化能力。
评审时我会重点检查四件事:谁拥有状态,谁拥有生命周期,跨边界传什么,失败后如何恢复。UGC 的长期复杂度通常不是来自某个页面,而是来自对象身份、版本和执行域没有在早期定义清楚。
面试卡壳时怎样重新起头?
先说:
我先按一份内容从创作到运行的生命周期讲,不按页面目录讲。
然后写下四个词:
编辑 -> 模型 -> 运行 -> 交付
如果仍然卡住,就逐个回答:
- 状态由谁拥有?
- 生命周期由谁管理?
- 跨边界传什么?
- 失败后怎样恢复?
最后用这句话收尾:
UGC 客户端的核心,是把用户持续变化的创作数据,转换成有身份、有版本、可校验,并且能在客户端和服务端安全运行的内容。
事实与职责口径
| 内容 | 面试中的主语 |
|---|---|
| 页面协议、页面栈、原生宿主、跨语言生命周期 | “我负责或深度参与” |
| 制品接入、多端构建、试玩恢复、UI 自动化 | “我负责其中的具体链路” |
| UGC1.0 地图交付、编辑模式、部分元件、AI 创作、ASTC、四足生物 | “我直接做过” |
| 完整对象模型、三 VM、网络运行时 | “团队架构和我的上下游;我能解释,但不是我独立设计” |
| 绿洲启元与 Roblox | “依据公开官方文档做外部对照” |
官方资料
绿洲启元:
Roblox:
相关材料:绿洲启元、Roblox 与 UGC1.0/2.0:封装粒度与脚本运行时、UGC2.0 三 VM、PixUI 与跨语言通信、UGC 客户端架构深度复盘、UGC1.0 事实底座、UGC2.0 事实底座、高级客户端高频问答。