26 KiB
Unity 式 Tick 系统与 Play Mode 运行时方案
日期:4.2
1. 背景与问题定义
当前仓库已经具备一批运行时生命周期原语:
Application已经能稳定提供 editor 每帧的deltaTimeSceneRuntime已经具备Start / Stop / FixedUpdate / Update / LateUpdateScene与GameObject已经能向原生组件继续分发生命周期ScriptEngine已经能把脚本生命周期接到运行时场景上SceneManager已经具备场景序列化、快照恢复与启动场景加载能力
因此,当前缺的并不是“有没有 Update(float dt) 这个函数”,而是:
- editor 模式下,谁来决定什么时候进入运行时
- Play 时跑的是哪一份场景
- Pause / Step / Stop 的边界由谁维护
- editor 自己的帧循环与 engine 运行时循环如何嵌套
- 运行时对 scene / input / audio / scripting / viewport 的驱动顺序如何统一
如果只是在 editor 每帧里直接调用 Scene::Update(dt),短期看像是“加了 tick”,但长期会立刻引出几个错误方向:
- 编辑场景被运行时修改污染
- Stop 后无法恢复编辑状态
- Hierarchy / Inspector / Undo / Dirty 状态混杂
- GameView 和 SceneView 看到的不是同一个 runtime world
- Pause / Step 无法建立稳定语义
- 后续独立 player 无法复用 editor 中临时写死的调度逻辑
所以这里必须把“tick”提升为一套 Unity 式运行时主循环方案,而不是零散的每帧调用。
2. 目标
本方案的目标是建立一套与 Unity 语义对齐的运行时驱动体系。
2.1 功能目标
- 明确区分
editor tick与engine tick - 在 editor 内支持
Edit / Play / Pause / Step / Simulate - Play 时运行的是 runtime scene 副本,而不是编辑场景本体
- 统一驱动
FixedUpdate / Update / LateUpdate - 让 SceneView / GameView / Inspector / Hierarchy 在 Play 中观察同一份 runtime world
- 为未来独立 player 复用同一套 runtime loop 打基础
2.2 架构目标
- 真正的 runtime loop 逻辑应放在
engine/,而不是散在editor/ - editor 只负责“托管运行时会话”,不复制第二套引擎
- scene 文档管理和 runtime 执行管理要严格分层
- Play/Stop 的数据边界必须显式,不允许隐式回写编辑态
2.3 非目标
本方案第一阶段不解决以下问题:
- 完整物理系统
- 完整 Time API,如
fixedDeltaTime、timeScale、unscaledDeltaTime - 全量输入系统 editor 宿主接线
- 全量音频系统 editor 生命周期接线
- Hot Reload / Script Recompile During Play
- Prefab / Domain Reload / Enter Play Mode Options
这些能力可以在 Tick 体系稳定后继续扩展,但不能反过来阻塞 Tick 方案本身。
3. 核心概念定义
为了避免后续讨论混淆,本方案先固定术语。
3.1 editor tick
editor tick 指编辑器自己的每帧更新。
它始终存在,只要 editor 窗口还在运行,就会继续执行。它负责:
- 面板更新
- 输入焦点与 UI 交互
- 资源异步刷新
- viewport 请求与宿主渲染
- PlaySession 的状态推进
它不等价于 gameplay simulation。
3.2 engine tick
engine tick 指引擎运行时自己的每帧逻辑循环,也就是 Unity 语义里的 PlayerLoop。
它只应在以下场景出现:
- editor 的
Play - editor 的
Simulate - 将来的独立 runtime/player 程序
它负责:
- 固定步长逻辑推进
- 每帧逻辑推进
- 脚本生命周期分发
- 原生组件生命周期分发
- 与运行时相关的系统刷新
3.3 runtime scene
runtime scene 指进入 Play/Simulate 后真正被 engine tick 驱动的场景副本。
它具备以下特征:
- 从编辑场景复制而来
- 允许运行时自由修改
- Stop 后整体丢弃
- 不能直接污染编辑场景
3.4 editor scene
editor scene 指当前用户正在编辑的场景文档。
它具备以下特征:
- 持久化来源是 scene 文件
- 与 Undo / Dirty / Save 强绑定
- 在 Edit 模式中由 Inspector / Hierarchy / SceneView 操作
- Play 期间不应被 runtime 直接修改
3.5 Play Session
Play Session 指 editor 内一次完整的运行时会话。
它应至少覆盖:
- 进入 Play 前快照编辑态
- 构造 runtime scene
- 启动 runtime
- 推进 Pause / Step / Stop 状态
- 退出时恢复编辑态
4. Unity 语义下 editor tick 与 engine tick 的关系
这一点必须讲清楚,因为这是整个方案的基础。
4.1 它们不是同一个东西
如果它们是同一个东西,那么游戏 Pause 时 editor 也应该跟着停掉。
但 Unity 不是这样:
- Pause 游戏后,Hierarchy 还能选对象
- Inspector 还能看 runtime 状态
- SceneView 还能绕相机
- Console 还能操作
- 菜单栏和工具栏仍然活着
因此,暂停的是 gameplay loop,不是 editor host loop。
同理,在本引擎里也应该遵守相同原则:
- editor tick 是宿主
- engine tick 是被宿主管理的一段运行时循环
4.2 正确的嵌套关系
正确关系应为:
Win32 Loop -> Application::Render -> EditorWorkspace::Update(dt) -> PlaySession::Tick(dt) -> RuntimeLoop::Tick(dt)
也就是说:
- 最外层永远是 editor
- editor 每帧判断当前是不是 Play/Simulate
- 若不是,则不推进 runtime loop
- 若是,则由 editor 在本帧内部托管调用 runtime loop
4.3 三种模式下的关系
Edit
- 有 editor tick
- 没有 engine tick
- 当前 active scene 是 editor scene
Play
- 有 editor tick
- 有 engine tick
- 当前 active scene 是 runtime scene
- SceneView 与 GameView 都观察 runtime world
Simulate
- 有 editor tick
- 有 engine tick
- 当前 active scene 仍是 runtime scene
- 与 Play 的主要区别不在“场景是否运行”,而在“使用哪种观察与交互语义”
5. 当前代码状态与缺口归纳
5.1 当前已经具备的条件
editor 外层帧循环
当前 editor 已有稳定主循环:
Win32EditorHost持续调用Application::RenderApplication::Render已计算每帧deltaTimeEditorLayer -> EditorWorkspace已能接收dt
这说明 editor host tick 已存在。
生命周期分发原语
当前 engine 已具备:
SceneRuntime::Start / StopSceneRuntime::FixedUpdate / Update / LateUpdateScene::Update / FixedUpdate / LateUpdateGameObject::Start / Update / FixedUpdate / LateUpdate
这说明 engine tick 的底层生命周期原语已存在。
场景复制与恢复基础
当前 SceneManager 已具备:
- 场景序列化到字符串
- 基于字符串恢复场景
SceneSnapshot抓取与恢复
这说明 runtime scene 副本机制已经有足够基础,不需要从零发明。
5.2 当前明确缺失的部分
缺失 1:运行时会话控制器
当前还没有一个对象负责统一维护:
- 当前 editor mode
- PlaySession 生命周期
- runtime scene 的创建与销毁
- Pause / Step / Stop 状态机
- fixed timestep accumulator
缺失 2:runtime scene 与 editor scene 的显式所有权边界
当前大多数 editor UI 都通过 SceneManager::GetScene() 读取场景。
这意味着如果 Play 期间仍然让 GetScene() 指向编辑场景,就会直接污染编辑态。
缺失 3:真正统一的 runtime tick 调度入口
当前 EditorWorkspace::Update(dt) 只做:
ResourceManager::UpdateAsyncLoads()m_panels.UpdateAll(dt)
还没有推进:
SceneRuntime- fixed-step accumulator
- runtime-specific system flush
缺失 4:Play/Pause/Stop 事件只有定义,没有闭环
当前已有:
PlayModeStartedEventPlayModeStoppedEventPlayModePausedEvent
但缺:
- 谁发布 Started/Stopped
- 谁在 Pause 后阻止 runtime 更新
- 谁在 Step 时只推进一帧或一次 fixed step
缺失 5:脚本后端在 editor 中未完成正式接线
当前 MonoScriptRuntime 实现已存在,但 editor 初始化路径中没有形成明确的 runtime 安装流程。
这意味着即使补上 PlaySession,如果不同时补脚本后端接线,Play 下的脚本生命周期仍可能只落到 NullScriptRuntime。
缺失 6:输入 / 音频 / 任务系统接线不完整
当前代码中虽然有:
InputManager::UpdateAudioSystem::UpdateTaskSystem::Update
但 editor 生命周期中还没有完整初始化和宿主消息接线,因此第一版 Tick 方案必须区分:
- 哪些系统是架构上应该接入的
- 哪些系统是当前阶段先预留挂点
6. 总体架构方案
6.1 分层原则
本方案采用两层控制:
engine/内的 RuntimeLoopeditor/内的 PlaySessionController
其职责必须严格区分。
6.2 engine 层:RuntimeLoop
建议新增一个 runtime loop 控制器,放在 engine/Scene 或 engine/Runtime 相关目录下。
它负责纯运行时行为,不依赖任何 editor 类型。
职责
- 持有
SceneRuntime - 持有
running / paused状态 - 持有 fixed timestep accumulator
- 接受宿主传入的
dt - 统一调度
FixedUpdate / Update / LateUpdate - 提供
Tick(dt)、Pause()、Resume()、StepOnce()
不负责
- 场景文件保存
- editor Undo/Dirty
- 面板 UI
- GameView 工具栏状态
- 项目切换
建议接口
class RuntimeLoop {
public:
struct Settings {
float fixedDeltaTime = 1.0f / 50.0f;
float maxFrameDeltaTime = 0.1f;
uint32_t maxFixedStepsPerFrame = 4;
};
void Start(Components::Scene* scene);
void Stop();
void Tick(float deltaTime);
void Pause();
void Resume();
void StepFrame();
bool IsRunning() const;
bool IsPaused() const;
Components::Scene* GetScene() const;
};
6.3 editor 层:PlaySessionController
建议新增 editor 侧运行时会话控制器,例如:
editor/src/Core/PlaySessionController.h- 或
editor/src/Core/EditorRuntimeController.h
它负责 editor 与 runtime 的边界管理。
职责
- 维护
Edit / Play / Pause / Simulate模式 - 抓取编辑场景快照
- 创建 runtime scene 副本
- 切换
SceneManager当前 active scene - 调用
RuntimeLoop::Start / Stop / Tick - 发布 Play/Pause/Stop 事件
- Stop 时恢复编辑场景与 dirty 状态
不负责
- 不实现具体生命周期分发
- 不直接遍历
GameObject - 不替代
SceneManager
6.4 SceneManager 的角色
SceneManager 仍然是场景文档与当前 active scene 的管理入口。
本方案不建议新增第二个并行 scene manager。
建议保留单一 active scene 语义,但增加“当前 scene 处于 editor 还是 runtime 模式”的管理逻辑。
这样可以保证:
- Hierarchy 读取的是当前正在展示的 scene
- Inspector 读取的是当前正在运行或编辑的对象
- SceneView / GameView 都基于同一份 scene
6.5 为什么不让 GameView 单独持有 runtime scene
这种设计看起来“侵入更小”,但实际会把 editor 撕成两套世界:
- GameView 看 runtime scene
- Hierarchy / Inspector / SceneView 还看 editor scene
这与 Unity Play Mode 的真实使用习惯相反,也会带来以下问题:
- 运行中对象在 GameView 里变了,但 Hierarchy 看不到
- 选中对象后 Inspector 读到的是编辑态,不是运行态
- SceneView 无法自然观察 runtime 世界
因此第一版正确路线应是:
Play 时让 editor 当前 active scene 切到 runtime clone。
7. 场景所有权与数据流方案
7.1 编辑态与运行态必须双轨
在任何时刻,editor 都必须知道自己面对的是哪一类 scene:
editor sceneruntime scene
运行态和编辑态不能混存于同一份对象图中。
7.2 进入 Play 的标准流程
建议流程如下:
- 确认当前有 active scene
- 抓取 editor scene 快照
- 基于快照数据构造新的 runtime scene
- 暂存当前 selection、dirty、scene path 等 editor 上下文
- 将
SceneManager当前 active scene 切到 runtime scene - 启动
RuntimeLoop - 发布
PlayModeStartedEvent
7.3 停止 Play 的标准流程
建议流程如下:
- 调用
RuntimeLoop::Stop() - 销毁 runtime scene
- 用 Play 前保存的快照恢复 editor scene
- 恢复 dirty 状态、scene path、scene name
- 清理 runtime-only 选择状态
- 发布
PlayModeStoppedEvent - 回到
Edit模式
7.4 为什么快照恢复是第一版最合适方案
当前仓库已有:
SerializeToStringDeserializeFromStringSceneSnapshot
这意味着第一版根本不需要发明复杂的“对象级镜像同步层”。
对于 Unity 式 Play 行为来说,最符合直觉的方式就是:
- Play 前冻结编辑态
- Play 中任 runtime 随便改
- Stop 后整份 runtime world 丢弃
- 恢复编辑态
这比尝试在 Stop 时“回滚运行时改动”更稳,也更接近 Unity。
7.5 Dirty 与 Save 语义
Play 期间的 runtime 改动不应自动标记编辑场景 dirty。
需要明确以下规则:
- 进入 Play 前,dirty 状态是多少,就记录多少
- Play 期间 runtime world 的变化不回写 editor dirty
- Stop 后恢复 Play 前 dirty 状态
这样可以避免:
- 只是运行一次游戏,却把场景误判成已修改
- runtime 中脚本动态生成对象导致 editor scene 被误保存
8. 模式状态机设计
8.1 状态定义
建议 editor 模式至少定义如下枚举:
enum class EditorRuntimeMode {
Edit,
Play,
Paused,
Simulate
};
第一阶段即使只正式实现 Edit / Play / Paused,也建议把 Simulate 预留到状态机层,而不是后面另起一套逻辑。
8.2 状态行为语义
Edit
- 当前 active scene 为 editor scene
- 不推进 runtime loop
- 允许完整编辑
Play
- 当前 active scene 为 runtime scene
- 推进 runtime loop
- 对高风险编辑行为进行限制
Paused
- 当前 active scene 仍为 runtime scene
- 不自动推进 runtime loop
- 允许观察运行时状态
- 可通过 Step 手动推进
Simulate
- 当前 active scene 为 runtime scene
- 推进 runtime loop
- 主要差异体现在观察相机和工具行为
8.3 状态转换
建议只允许以下显式转换:
Edit -> PlayPlay -> PausedPaused -> PlayPlay -> EditPaused -> EditEdit -> SimulateSimulate -> Edit
不建议存在:
- 隐式自动恢复
- 运行时错误直接把模式切回 Edit
运行时错误更合理的做法是:
- 触发 Pause
- 在 Console 标记错误
- 保留 runtime world 供观察
这与 Unity 的调试体验更接近。
9. 主循环与 Tick 顺序方案
9.1 顶层顺序
editor 每帧顺序建议为:
- 计算 host
deltaTime - 更新 editor 级输入与宿主状态
- 推进 PlaySession
- 更新面板逻辑
- 渲染 SceneView / GameView
- 提交 ImGui 与窗口交换链
其中第 3 步内部若处于 Play / Simulate,再调用 runtime loop。
9.2 RuntimeLoop 的单帧顺序
建议 RuntimeLoop::Tick(dt) 顺序如下:
- 对
dt做上限钳制 - 把
dt累加到 fixed accumulator - 循环执行若干次
FixedUpdate(fixedDeltaTime) - 执行一次
Update(dt) - 执行一次
LateUpdate(dt) - 运行帧尾系统刷新
伪代码如下:
void RuntimeLoop::Tick(float dt) {
if (!m_running || m_scene == nullptr) {
return;
}
if (m_paused) {
if (!m_stepRequested) {
return;
}
}
dt = Clamp(dt, 0.0f, m_settings.maxFrameDeltaTime);
m_fixedAccumulator += dt;
uint32_t fixedSteps = 0;
while (m_fixedAccumulator >= m_settings.fixedDeltaTime &&
fixedSteps < m_settings.maxFixedStepsPerFrame) {
m_sceneRuntime.FixedUpdate(m_settings.fixedDeltaTime);
m_fixedAccumulator -= m_settings.fixedDeltaTime;
++fixedSteps;
}
m_sceneRuntime.Update(dt);
m_sceneRuntime.LateUpdate(dt);
m_stepRequested = false;
}
9.3 为什么必须有 fixed accumulator
如果没有它:
FixedUpdate就会退化成“和Update一样每帧一次”- Pause / Step 没法建立稳定语义
- 未来物理系统没有可靠接入点
- 帧率波动会直接改变 fixed 逻辑行为
这与 Unity 模型不一致。
9.4 为什么要限制每帧最大 fixed 步数
这是为了避免“螺旋死亡”:
- 某帧卡顿导致
dt很大 - 一次性补太多
FixedUpdate - 补固定步本身又导致更卡
- 进入恶性循环
因此必须有:
maxFrameDeltaTimemaxFixedStepsPerFrame
9.5 Start 与 Update 的顺序
当前 SceneRuntime::Update 内部会先进入脚本更新,再进入 scene/gameobject 的 Start 与原生 Update 分发。
这一行为当前已有测试兜底,但后续在形成正式 runtime loop 后,仍建议把 生命周期顺序作为公开契约 固化下来。
对于第一阶段方案,本文件接受当前行为,后续若要进一步对齐 Unity 的更细语义,可以单独再做生命周期整理,不阻塞 Tick 方案落地。
10. 系统接入顺序
10.1 第一阶段必须接入
第一阶段真正必须接入 runtime tick 的系统只有:
SceneRuntimeScriptEngine(通过SceneRuntime间接接入)Scene / GameObject / Component生命周期
这是形成最小 Unity 式 Play 语义闭环的核心。
10.2 第一阶段建议预留但不强制打通
以下系统建议在 RuntimeLoop 中预留挂点,但可以先不完整接通:
InputManagerAudioSystemTaskSystem
原因不是它们不重要,而是它们当前在 editor 生命周期中还未完整初始化。
因此第一阶段合理策略是:
- 架构上预留顺序和位置
- 实现上只接已初始化的系统
- 未初始化时安全跳过
10.3 建议顺序
如果这些系统后续要正式进入 runtime tick,建议顺序为:
- 宿主输入消息先进入输入模块
- runtime loop 读取上一帧积累的输入状态
- 运行
FixedUpdate - 运行
Update - 运行
LateUpdate - 刷新音频
- 执行主线程任务队列
- 清理本帧输入瞬时态
第一阶段若尚未把 editor 消息正式接到引擎输入模块,则不要伪造“已接通输入”。
11. Play、Pause、Step 的精确定义
11.1 Play
Play 的语义不是“开始渲染 GameView”,而是:
- 构造 runtime scene
- 启动 runtime loop
- 开始推进生命周期
GameView 只是 runtime world 的一个观察窗口。
11.2 Pause
Pause 的语义不是“冻结 editor”,而是:
- 停止自动推进 runtime loop
- 保留 runtime scene 当前状态
- 允许用户继续观察运行态对象图
这点与 Unity 保持一致。
11.3 Step
Step 的语义建议定义为:
- 在
Paused状态下推进 一帧完整 runtime tick - 即允许本次执行固定步补偿、一次
Update、一次LateUpdate
而不是只推进一次 Update。
原因如下:
- 更符合 Unity 使用者对 Step 的直觉
- 对脚本与未来物理更一致
- 避免“Step 看起来动了,但 FixedUpdate 没跑”的语义分裂
11.4 Error Pause
Console 中已有 Error Pause 语义雏形。
建议正式方案中把它收敛为:
- 监听运行时错误
- 当处于 Play 且开启
Error Pause时,自动切换到Paused - 不自动 Stop
这更便于排查 runtime 状态。
12. 对 editor 各模块的影响
12.1 SceneManager
需要增强但不重写。
建议新增能力:
- 进入 runtime scene 的接口
- 恢复 editor scene 的接口
- 查询当前 scene 是否为 runtime scene
不建议让多个模块自行持有 scene 指针并绕开 SceneManager。
12.2 Hierarchy
Play 时 Hierarchy 应展示 runtime scene 中的对象树。
这样才能做到:
- 运行时动态生成对象可见
- 运行时销毁对象可见
- 选中对象后 Inspector 读到的是运行态
12.3 Inspector
Play 时 Inspector 应直接观察 runtime object/component 状态。
同时需要明确编辑策略:
- 第一阶段允许查看与轻量编辑 runtime 值
- 但这些修改默认只作用于 runtime scene
- Stop 后不自动回写 editor scene
12.4 SceneView
Play 时 SceneView 不应继续盯着 editor scene。
它应观察 runtime world,但仍然使用 editor camera 语义。
这正是 Unity 中 SceneView 在 Play 时的常见行为。
12.5 GameView
GameView 在 Play 时应渲染 runtime scene 中的运行时 camera。
这一点当前 viewport host 的基本结构已具备,关键在于它取到的 scene 必须是 runtime clone,而不是 editor scene。
12.6 Undo / Dirty
Play 时 runtime 修改默认不进入 editor undo 历史。
原因:
- Unity 风格下,Play 期间改的是 runtime world
- runtime world 本身是临时态
- 把 runtime 改动混进 editor undo 会破坏编辑语义
因此第一阶段建议:
Edit模式下:正常 Undo/DirtyPlay / Pause / Simulate下:对 editor 文档型命令做限制或隔离
13. 实现方案细化
13.1 建议新增类型
engine 层
RuntimeLoop
editor 层
PlaySessionControllerEditorRuntimeModePlaySessionState
13.2 PlaySessionState 建议字段
struct PlaySessionState {
EditorRuntimeMode mode = EditorRuntimeMode::Edit;
SceneSnapshot editorSnapshot;
std::unique_ptr<Components::Scene> runtimeScene;
bool hasSession = false;
bool stepRequested = false;
std::string sourceScenePath;
std::string sourceSceneName;
bool sourceSceneDirty = false;
};
13.3 RuntimeLoop 建议字段
class RuntimeLoop {
private:
SceneRuntime m_sceneRuntime;
Components::Scene* m_scene = nullptr;
Settings m_settings = {};
float m_fixedAccumulator = 0.0f;
bool m_running = false;
bool m_paused = false;
bool m_stepRequested = false;
};
13.4 EditorWorkspace 中的挂载点
建议在 EditorWorkspace 内持有 PlaySessionController,而不是在各个 panel 分散维护。
理由:
EditorWorkspace已是 editor panel 与 context 的调度层- 它比单个 panel 更适合承接全局运行态
- 它比
Application更贴近 editor 业务层,但又不直接陷入 UI 细节
建议每帧顺序变为:
- 资源异步刷新
PlaySessionController.Update(dt)m_panels.UpdateAll(dt)
13.5 Application 层的责任变化
Application 不应直接拥有 runtime scene 或 runtime loop。
它只应继续负责:
- 计算 host
dt - 调用 layer / workspace update
- 驱动 viewport 渲染
这样未来独立 runtime 程序仍然可以重用 engine runtime loop,而无需依赖 editor 程序对象。
14. 分阶段实施建议
阶段 A:建立 runtime loop 与最小 PlaySession
目标:
- 新增
RuntimeLoop - 新增
PlaySessionController - 支持
Play与Stop - Play 时切到 runtime scene
- Stop 时恢复 editor snapshot
这一阶段先不要求:
- Pause / Step UI 完整
- Simulate 正式可用
- 输入/音频全接通
阶段 B:补 Pause / Step / 状态机闭环
目标:
- 正式接
Paused - 正式实现
Step - 完整发布 Started / Paused / Stopped 事件
- Console 的
Error Pause改为连接正式状态机
阶段 C:补脚本后端 editor 接线
目标:
- editor 启动时根据配置安装
MonoScriptRuntime - Play 时脚本生命周期真实运行
- 运行时错误进入 Console
阶段 D:补输入与 GameView 可操作性
目标:
- editor 宿主窗口消息接入引擎输入模块
- runtime tick 可读取真实输入
- 基础
Time.deltaTime与输入行为在 Play 中可用
阶段 E:扩展 Simulate 与更完整 Time 语义
目标:
Simulate正式与Play分化- 增加
Time.fixedDeltaTime - 增加
timeScale - 为物理系统预留更明确挂点
15. 风险与边界
15.1 最大风险:把 runtime 改动污染 editor scene
这是整个方案最需要防的风险。
只要 runtime scene 与 editor scene 边界不清,就会导致:
- Stop 后场景数据错乱
- Undo/Dirty 语义崩塌
- 用户无法信任 Play 行为
因此第一原则必须是:
Play 永远运行副本,不直接跑编辑场景。
15.2 第二风险:在 editor 里复制第二套 runtime 逻辑
如果把生命周期调度、输入推进、viewport runtime 判断等逻辑分散写在:
- GameViewPanel
- SceneViewPanel
- ConsolePanel
- Application
最终会得到一套 editor 私有 runtime。
这会严重偏离“editor 是宿主,不是第二个引擎”的原则。
因此真正的 runtime 调度逻辑必须尽可能收敛到 engine/RuntimeLoop。
15.3 第三风险:第一阶段试图一次打通所有系统
如果把以下内容强行绑成一个任务:
- PlaySession
- Mono 正式接线
- 输入宿主化
- 音频接线
- Step
- Simulate
- Time API 扩展
实现风险和调试复杂度都会迅速失控。
因此应坚持:
- 第一阶段先形成
Play/Stop + runtime clone + SceneRuntime tick闭环 - 其他系统按阶段接入
16. 测试建议
16.1 engine 层单测
新增或扩展以下测试:
RuntimeLoop在 running / paused / step 下的推进行为- fixed accumulator 在不同
dt下的补偿行为 maxFixedStepsPerFrame的限制行为- Stop 后不再继续分发生命周期
16.2 editor 层单测
新增或扩展以下测试:
- EnterPlay 会创建 runtime scene 副本
- Stop 会恢复 editor snapshot
- Play 期间 active scene 指向 runtime scene
- Play 期间 runtime 改动不污染 editor snapshot
- Pause 后不自动推进
- Step 在 Pause 下能推进一帧
16.3 手工验证清单
第一阶段至少手工验证:
- 进入 Play 后,脚本或原生组件的运行时移动只影响 Play 中对象
- Stop 后场景恢复到 Play 前状态
- Play 中动态创建的对象只在 runtime world 中存在
- Hierarchy / Inspector / SceneView / GameView 看到的是同一份 runtime world
- Play 后保存场景不会把 runtime 临时对象保存进去
17. 最终建议与结论
对于当前仓库,最合理的 tick 建设路线不是“在 editor 每帧里直接调一次 Scene::Update”,而是:
- 在
engine/建立独立的RuntimeLoop - 在
editor/建立PlaySessionController - Play 时把 active scene 切到 runtime scene 副本
- 用
RuntimeLoop统一推进FixedUpdate / Update / LateUpdate - Stop 时恢复 editor snapshot
这条路线的核心收益有三点:
- 它与 Unity 的 Editor/Player 语义一致
- 它能保证 editor scene 与 runtime scene 的边界清晰
- 它为未来独立 runtime/player 复用同一套主循环打下基础
简化成一句话就是:
editor tick 是宿主循环,engine tick 是运行时循环;在 Unity 式架构下,前者在 Play 时托管后者,但两者绝不能被当成同一个东西。