UGC2.0 对象模型:Instance、Actor 与 Lua-C++ 边界
这篇回答 UGC2.0 最基础的三道追问:为什么重做,Instance 与 Actor 是什么关系,Lua 和 C++ 到底谁拥有数据与生命周期。
面试官:UGC2.0 为什么要重做?
30 秒回答
UGC1.0 以官方元件和功能仓快速提供能力,低门槛、确定性和治理都比较好;但当创作需求超过预设能力时,每增加一种玩法概念都要补元件类型、编辑 UI、保存结构、运行逻辑和兼容链。UGC2.0 的目标是把扩展单位从“新增一个专用元件”下沉为“组合稳定的 Instance、Class、Service 和脚本能力”,同时支持编辑、客户端和服务端不同运行域。它不是换一套 Web UI,而是在 UE 宿主上建设自己的可编程对象平台。
90 秒回答
旧元件模式把大量语义封装在官方能力里,适合早期快速交付,但对象身份、属性、事件、脚本和运行时映射容易分散在不同模块。UGC2.0 希望先统一底层语义:所有创作对象进入 Instance 树,Class 定义能力与属性,Signal 表达变化,Service 提供跨对象领域能力,Prefab 复用对象子树,Script 在 Edit、Client、Server 对应运行域执行。
统一对象模型并不意味着所有东西都是 Actor。Folder、Service、Script、UI 和纯逻辑节点可以只存在于平台层;真正需要进入 UE World 的对象再通过 Binding 或 Adapter 物化成 Actor、Component 或其他原生资源。这样上层脚本面对稳定平台语义,底层 UE 实现仍可演进。
这套对象模型和三 VM 是团队平台架构,不是我独立设计。我直接做得深的是页面协议、编辑状态投影、原生页面宿主、跨语言链路、制品和自动化;对象模型是这些工作的核心上下游,我能按源码解释边界。
面试官:UGC1.0 和 UGC2.0 的本质区别是什么?
| 维度 | UGC1.0 | UGC2.0 |
|---|---|---|
| 主要扩展单位 | 官方元件、属性和事件连接 | Instance、Class、Service、Prefab、Script |
| 创作自由度 | 在平台预设能力内组合 | 用稳定原语组合更广泛逻辑 |
| 运行逻辑 | 多由元件内部实现 | Edit/Client/Server 脚本按运行域执行 |
| 平台成本 | 新能力要补整条元件链 | 平台要建设沙箱、API、对象、网络和制品治理 |
| 风险控制 | 能力白名单,行为确定 | 权限、配额、服务端权威和发布验证更重要 |
不能简化成“1.0 是元件,2.0 是脚本”。两代都可能有元件和脚本,区别是平台把哪一层抽象交给创作者组合。
面试官:统一对象模型包含什么?
flowchart TB
ROOT["DataModel / Root"]
INST["Instance<br/>稳定身份 / Parent / Children / Class"]
PROP["Property<br/>Schema / 默认值 / 校验 / 变更"]
SIG["Signal<br/>连接 / 触发 / 解绑"]
SVC["Service<br/>领域能力与运行域"]
SCRIPT["Script<br/>Source / RunContext / 权限"]
PREFAB["Prefab / Package<br/>可复用对象子树"]
NATIVE["UE Projection<br/>Actor / Component / Resource"]
ROOT --> INST
INST --> PROP
INST --> SIG
ROOT --> SVC
INST --> SCRIPT
INST --> PREFAB
INST --> NATIVEInstance 至少要统一回答六个问题:我是谁、属于什么 Class、父子关系是什么、有哪些属性、发出哪些 Signal、在哪个运行域存在。编辑器对象树、属性面板、保存、脚本查找和运行时物化都围绕同一身份工作。
面试官:为什么稳定身份这么重要?
内存地址、Lua table 和 Actor 指针只在当前进程与生命周期内有效,不能用于保存、跨 VM、撤销、复制和异步回调。稳定 ID 则可以贯穿:
- 编辑页面定位当前选中对象。
- 序列化父子关系和对象引用。
- 撤销重做恢复同一语义对象。
- Client/Server 对齐网络实体。
- 异步资源和构建错误定位来源。
但稳定 ID 也不能永远复用。对象删除再创建应获得新身份,或者同时携带 generation,避免旧回调误伤同名新对象。
面试官:Class、Instance 和 Service 怎样区分?
Class 定义一类对象允许哪些属性、信号、方法和构造策略;Instance 是具体对象,持有身份、父子关系和属性值;Service 是某个运行域中的领域入口,例如输入、玩家、资源或编辑服务,通常不由创作者任意创建。
把所有能力都做成 Instance 方法会形成巨型基类;把所有东西都做成全局 Service 又会失去对象局部性。判断方式是:能力属于某个对象实例,就放在 Class/Component;能力协调多个对象或代表运行环境,就放 Service。
面试官:Signal 和普通回调有什么区别?
Signal 是对象模型中的可观察事件,需要明确连接身份、触发顺序、运行域和解绑时机。普通函数回调往往只是一次异步结果。Signal 可以多订阅、多次触发;请求 callback 通常只完成一次。
对象销毁、VM 切换或页面释放时,连接必须由 scope 统一断开。否则旧 Instance 的事件可能调用已销毁 Lua 环境或新页面。
面试官:Instance 和 UObject、Actor 是什么关系?
先给结论
Instance 是平台语义对象,UObject/Actor 是 UE 实现对象,两者不是一一对应关系。
| Instance 类型 | 可能的原生形态 |
|---|---|
| Folder、配置节点 | 只存在于脚本/数据层 |
| Service | Lua 对象加 C++ 能力入口 |
| 场景 Part | Actor、Component 或合批后的原生表示 |
| UI 节点 | 平台 UI 对象、原生 Widget 或 PixUI 页面投影 |
| Script | 源码与 VM 中的执行实例 |
| 资源引用 | Asset ID、句柄或按需加载对象 |
是否物化由 Class 元数据、运行域和性能策略决定。大量轻量对象如果全部生成 Actor,会把平台抽象成本直接转成 UE 对象和复制成本;反过来,如果把 Actor 当作者源,又会把 World 生命周期和不可序列化状态带回编辑数据。
面试官:谁是数据权威,Instance 还是 Actor?
要按数据域回答:
- 作者数据:Instance 树、Schema 属性和稳定资源引用是权威,Actor 是编辑或运行投影。
- 编辑交互:命令和事务修改作者模型,再刷新视口投影。
- 正式玩法:服务端运行域拥有公共 Gameplay 真值,客户端 Actor 只是复制与表现。
- 物理瞬态:UE World 在当前运行会话中拥有碰撞、速度等瞬时状态,需要按规则回写或采样,不能自动覆盖作者源。
因此不是全局选一个“唯一权威”,而是给每个状态域指定 owner 和同步方向。
面试官:属性修改怎样从页面走到 Actor?
sequenceDiagram
participant UI as TypeScript 属性面板
participant E as Lua EditorService
participant M as Instance / Property Model
participant B as C++ Binding / Adapter
participant A as UE Actor / Component
UI->>E: SetProperty(instanceId, key, value)
E->>E: 校验页面、对象、Class 与值域
E->>M: 提交编辑事务
M->>B: 属性变化通知
B->>A: 更新原生投影
M-->>E: domain revision 更新
E-->>UI: 新状态或增量页面不能绕过编辑服务直接写 Actor。否则对象树和保存数据不知道发生了变化,撤销、多人编辑和重新加载都会丢失该修改。
面试官:Lua 对象和 C++ 对象怎样绑定?
C++ 向对应 VM 注册受控的构造、查询、属性和方法入口;Lua 只拿到句柄、userdata 或平台包装对象,不持有可以跨生命周期自由使用的裸指针。Binding 层负责类型转换、空值、权限、线程和生命周期校验。
一个典型调用是:
Lua 调用平台 API
-> Binding 检查当前 VM role 和参数
-> 用稳定 ID 或受控句柄定位原生对象
-> 在合法线程执行 UE 操作
-> 把纯值结果或平台对象句柄返回 Lua跨 VM 时不能复用 userdata、闭包或 registry reference,只能重新用稳定身份和序列化数据解析。
面试官:什么数据可以跨 Lua/C++ 边界?
优先传有 Schema 的纯值:布尔、数字、字符串、枚举、向量、颜色、稳定 ID 和受控数组/结构。大对象树传快照或增量,资源传身份和句柄,不传整份原生对象内存。
边界上要限制递归深度、集合大小、字符串长度和未知类型;错误返回结构化 code、stage 和 context。若每次高频属性更新都构造深层 table 和 JSON,会把 GC、序列化和拷贝成本叠加起来,因此还要做批量与帧内合并。
面试官:Lua GC 和 UObject GC 如何协作?
两套 GC 不会自动理解对方的引用图。若 Lua wrapper 强持有 UObject,而 UObject Delegate 又捕获 Lua callback,就可能形成跨系统保活;若两边都只用弱引用,又可能提前回收。
正确做法是定义 owner:原生对象由 World、Subsystem 或明确 host 持有;Lua wrapper 只在有效期内访问;需要回调时由 scope 保存连接 token。销毁顺序通常是先停止新事件,再解绑 Delegate 和 Signal,再释放 Lua 引用,最后销毁原生对象。所有回调入口还要做有效性与 generation 校验。
面试官:为什么要分 Edit、Client、Server 三个 VM?
三者权限和生命周期不同:Edit 可以创建对象、改属性和保存;Client 处理本机输入和表现;Server 承担权威玩法与复制。若共用一个 VM,编辑 API 可能泄露到线上脚本,全局变量和回调也容易跨模式残留。
VM 是逻辑执行域,不必固定等于三个进程。跨域传制品、稳定 ID 和纯值,不传活对象。更完整的 VM 与页面切换回答见独立基建专题。
面试官:开放脚本后怎么保证安全?
安全不是只删掉几个全局函数,而是四层:
- API 面:Class 和 Service 白名单,按 VM role 控制能力。
- 资源面:CPU/指令、内存、协程、网络和日志配额。
- 权威面:伤害、资产、胜负等公共结果由 Server 校验。
- 发布面:语法、依赖、Schema、敏感内容和制品完整性检查。
运行时异常要隔离到脚本或会话,并提供对象 ID、脚本版本和调用栈;不能让一个创作者脚本拖垮整个宿主。
面试官:这套模型最大的代价是什么?
统一对象平台把原来散落在元件中的复杂度收回平台:类型系统、属性兼容、脚本沙箱、跨 VM 网络、调试、包管理、制品和性能预算都要长期维护。开放程度越高,平台治理成本越高。
因此不应为了“像 Roblox”把所有底层 UE 能力直接暴露。正确演进是先建立少量稳定原语,完成端到端保存、运行和发布闭环,再按真实创作需求扩展 Class 与 Service。
面试官:你个人在这里做了什么?
我没有独立设计整套 Instance 对象模型。我的直接工作位于它的上游和下游:页面把用户操作转成领域命令,Lua 编辑服务修改对象并按状态域回推,C++ 页面宿主管理原生 View 和 VM 回调,保存发布侧把作者数据转成运行制品,自动化再通过稳定 UI 和对象身份验证链路。
因为这些工作都依赖对象身份、生命周期和数据权威,我能把对象模型讲清楚;但面试官问 ownership 时,我会明确说这是团队平台架构。
快速背诵版
UGC2.0 的核心不是换 Web UI,而是把扩展单位从专用元件变成统一的 Instance、Class、Service、Prefab 和 Script。Instance 是平台对象,具有稳定身份、父子关系、属性和 Signal;Actor 是 UE 运行投影,二者不是一一对应。作者数据以 Instance/Schema 为权威,正式玩法以 Server 为公共状态权威,物理瞬态由当前 World 管理。Lua 通过 C++ Binding 使用原生能力,跨边界只传纯值、稳定 ID 和受控句柄,不能跨 VM 传 Lua 引用或 UObject 指针。这套对象模型是团队架构,我直接负责的是页面协议、原生宿主、状态投影、制品和自动化。
责任边界
| 内容 | 面试表述 |
|---|---|
| Instance/Class/Service/三 VM 总体设计 | “团队架构,我沿源码和接口掌握” |
| 页面命令、状态投影和 C++ 页面宿主 | 可说“我直接负责关键链路” |
| 某具体 Instance 到 Actor 的全部映射 | 按源码逐类确认,不凭概念推断 |
| Lua/C++ 边界原则 | 可作为架构能力回答 |
| 完整脚本沙箱和网络运行时 | 不说成个人独立实现 |