18 KiB
18 KiB
编辑器与运行时分层架构设计
1. 背景与目的
1.1 参考案例:Unity 的 Editor/Player 分离
Unity 是目前最成熟的游戏引擎之一,其架构设计经过多年迭代验证。Unity 的核心设计理念是将游戏开发工具(编辑器)与游戏运行载体(播放器)清晰分离,同时共享同一套引擎核心。
Unity 的整体架构如下:
┌─────────────────────────────────────────────────────────────┐
│ Unity Hub │
│ (项目浏览器、启动器) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────┴───────────────────────────────────┐
│ Unity Editor │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │
│ │Hierarchy │ │Inspector │ │ Scene │ │ Project │ │
│ │ Panel │ │ Panel │ │ View │ │ Browser │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Unity Engine Core (C++) │ │
│ │ Scene System, ECS, Renderer, Physics, Audio, etc. │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ (Build)
↓
┌─────────────────────────────────────────────────────────────┐
│ Unity Player │
│ (独立可执行文件,不含编辑器组件) │
│ - IL2CPP 虚拟机运行 C# 脚本 │
│ - 裁剪过的引擎子集 │
└─────────────────────────────────────────────────────────────┘
这一架构的核心思想是:
- Editor 是开发工具,面向游戏开发者,提供场景编辑、参数调整、资源管理等功能
- Player 是运行载体,面向最终用户,运行游戏逻辑,不包含任何编辑器 UI
- Engine Core 是共享层,Editor 和 Player 都依赖同一套核心逻辑
1.2 分层架构的目标与收益
采用分层架构设计编辑器与运行时,带来以下收益:
| 目标 | 说明 |
|---|---|
| 职责分离 | 编辑器和运行时各司其职,代码边界清晰 |
| 核心复用 | 场景系统、渲染器、物理等核心逻辑只需维护一份 |
| 按需发布 | 发行游戏时只需包含运行时,体积更小 |
| 独立迭代 | 编辑器改进不影响运行时稳定性 |
| 团队协作 | 程序员专注核心,设计师专注编辑器操作 |
2. 整体架构设计
2.1 分层原则
编辑器与运行时的分层应遵循以下原则:
- 核心不可依赖编辑器:Engine Core 不应包含任何 UI 相关的代码,确保可以在无界面环境下运行
- 运行时独立最小化:运行时只包含游戏运行必需的组件,不包含编辑器特有功能
- 接口抽象分离:通过接口隔离变化,便于未来替换实现
- 数据驱动:场景、配置等数据与代码逻辑分离
2.2 模块划分
整体架构分为五个主要模块:
| 模块 | 英文名 | 说明 |
|---|---|---|
| 引擎核心 | Engine Core | 游戏运行时逻辑的核心库 |
| 编辑器应用 | Editor Application | 游戏开发工具 |
| 运行时应用 | Runtime Application | 游戏运行载体 |
| 启动器 | Launcher | 项目浏览器与启动器 |
| 脚本系统 | Script System | 游戏逻辑脚本 |
2.3 整体架构图
┌─────────────────────────────┐
│ <Launcher> │
│ (启动器) │
│ 扫描项目,启动应用 │
└─────────────┬───────────────┘
│
┌──────────────────┴──────────────────┐
│ │
┌─────────▼─────────┐ ┌──────────▼──────────┐
│ <Editor>.exe │ │ <Runtime>.exe │
│ (编辑器) │ │ (运行时) │
│ │ │ │
│ ┌─────────────┐ │ │ ┌───────────────┐ │
│ │ <Editor>Layer │ │ │ │ <Runtime>Layer │ │
│ │ (UI面板) │ │ │ │ (游戏逻辑) │ │
│ └──────┬──────┘ │ │ └───────┬───────┘ │
└─────────┼─────────┘ └──────────┼──────────┘
│ │
┌──────────┴───────────────────────────────────┴──────────┐
│ │
│ <Core> │
│ (引擎核心静态库) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Scene │ │ Renderer │ │ Physics │ │ Script │ │
│ │ (ECS) │ │ │ │ 2D/3D │ │ (C# CLR) │ │
│ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────────────┐ │
│ │ Asset │ │ Event │ │ Project/Settings │ │
│ │ Manager │ │ System │ │ │ │
│ └──────────┘ └──────────┘ └──────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.4 模块间依赖关系
<Launcher> ──启动──> <Editor> / <Runtime>
<Editor> ──链接──> Engine Core
<Runtime> ──链接──> Engine Core
Engine Core(不依赖任何其他模块)
│
├── Scene System
├── Renderer
├── Physics
├── Script Engine
├── Asset Manager
└── Event System
<Script>(C#项目)──编译输出──> .dll 供 <Core> 加载
依赖规则:
- Engine Core 是底层,不被任何其他模块依赖(它依赖谁?)
- Editor 和 Runtime 都依赖 Engine Core,不直接依赖彼此
- Launcher 只依赖文件系统,不依赖 Engine Core
3. 核心模块设计
3.1 Engine Core(引擎核心)
职责
引擎核心是整个架构的基石,负责游戏运行时的一切逻辑:
- 场景管理:Entity-Component-System 架构,场景的创建、销毁、序列化
- 渲染系统:渲染管线、材质、光照、后处理
- 物理系统:2D/3D 物理模拟
- 脚本系统:脚本引擎、脚本绑定
- 资源管理:资源加载、缓存、卸载
- 事件系统:事件分发与响应
- 输入系统:输入事件捕获与分发
- 音频系统:音频播放、音效处理
边界
Engine Core 不包含:
- 任何 UI 逻辑(ImGui 代码、面板布局等)
- 编辑器特有功能(场景大纲、Gizmo 选择等)
- 启动器相关逻辑
设计要点
Engine Core 编译为静态库(.lib),被 Editor 和 Runtime 两个可执行文件链接。这种方式确保:
- 代码真正共享,而非运行时 DLL 加载
- 两个应用使用完全相同的核心逻辑版本
3.2 Editor Application(编辑器应用)
职责
编辑器是游戏开发者的主要工具,提供:
- 场景编辑:场景视图、层级面板、检视面板
- 资源管理:资源导入、浏览、配置
- 游戏预览:Play/Simulate 模式切换
- 项目配置:项目设置、构建选项
与 Engine Core 的关系
编辑器链接 Engine Core,并在此基础上添加编辑器的 UI 层:
Editor Application
│
├── Editor UI Layer(ImGui 面板)
│ │
│ ├── ViewportPanel
│ ├── SceneHierarchyPanel
│ ├── InspectorPanel
│ ├── ContentBrowserPanel
│ └── MenuBarPanel
│
└── Engine Core(场景管理、渲染、物理等)
核心机制:Scene 复制
编辑器在 Play/Simulate 时,会将当前编辑的场景完整复制一份交给运行时:
m_activeScene = Scene::copy(m_editorScene);
m_activeScene->onRuntimeStart();
这样设计的好处是:
- 编辑器场景保持不变,方便运行时修改调试
- 运行时可以自由修改场景对象,不影响编辑器状态
- Stop 时只需切回编辑器场景即可
3.3 Runtime Application(运行时应用)
职责
运行时是游戏的运行载体,面向最终用户:
- 加载游戏项目:读取项目配置、启动场景
- 运行游戏循环:持续执行 Scene 的更新逻辑
- 执行脚本:运行 C# 脚本游戏逻辑
- 渲染画面:调用渲染器输出图像
与 Engine Core 的关系
运行时同样链接 Engine Core,但不包含任何编辑器 UI:
Runtime Application
│
├── Runtime Layer(游戏入口、更新循环)
│
└── Engine Core(与编辑器相同)
特点
- 无 UI:运行时没有任何编辑器界面
- 最小化:只包含游戏运行必需的组件
- 独立发行:可打包为独立的游戏可执行文件
3.4 Launcher(启动器)
职责
启动器是用户进入游戏的第一个界面:
- 项目浏览:扫描并显示本地的游戏项目列表
- 项目管理:创建、打开、删除项目
- 启动选择:启动编辑器或直接运行游戏
设计要点
启动器不链接 Engine Core,而是作为一个独立的小型程序:
Launcher Application
│
├── 项目浏览器 UI
├── 项目文件系统扫描
└── 进程启动(调用 <Editor>.exe 或 <Runtime>.exe)
这种设计使得:
- 启动器可以独立编译、独立发布
- 即使引擎编译失败,启动器也可能正常工作
- 未来可以单独改进启动器的 UI/UX
3.5 Script System(脚本系统)
定位
脚本系统是连接游戏逻辑与引擎的桥梁。游戏开发者编写脚本(当前采用 C#),脚本调用引擎提供的 API。
隔离方式
脚本系统采用独立编译 + 运行时加载的模式:
<Script>(C# 项目)
│
├── 游戏脚本源码(.cs)
│
└── 编译输出 ──> GameScripts.dll
│
↓
<Core>(运行时加载)
│
└── Mono Runtime 执行
设计要点
- 脚本独立编译:不与引擎一起编译,便于快速迭代
- 运行时加载:Engine Core 通过 Mono Runtime 加载编译好的脚本
- 引擎 API 绑定:引擎提供 C++ 函数,脚本通过 InternalCall 调用
4. 编辑器与运行时交互机制
4.1 Scene 复制机制
编辑器维护两种 Scene:
| Scene | 用途 | 修改权限 |
|---|---|---|
m_editorScene |
编辑器中显示的场景 | 编辑器随时可改 |
m_runtimeScene |
Play/Simulate 时复制的场景 | 运行时逻辑可改 |
复制过程:
std::shared_ptr<Scene> Scene::copy(std::shared_ptr<Scene> other) {
std::shared_ptr<Scene> newScene = std::make_shared<Scene>();
// 1. 复制所有 Entity(保留 UUID)
auto idView = srcSceneRegistry.view<IDComponent>();
for (auto e : idView) {
UUID uuid = srcSceneRegistry.get<IDComponent>(e).ID;
Entity newEntity = newScene->createEntityWithUUID(uuid, name);
enttMap[uuid] = (entt::entity)newEntity;
}
// 2. 复制所有 Component
copyComponent(AllComponents{}, dstSceneRegistry, srcSceneRegistry, enttMap);
return newScene;
}
复制确保:
- Entity UUID 不变,便于调试和序列化
- 所有 Component 数据完整复制
- 编辑器场景不受影响
4.2 状态机(Edit / Play / Simulate)
编辑器有三种状态:
| 状态 | Scene | 物理 | 脚本 | 相机 | 层级可编辑 |
|---|---|---|---|---|---|
| Edit | m_editorScene |
❌ | ❌ | EditorCamera | ✅ |
| Play | m_runtimeScene |
✅ | ✅ | MainCamera | ❌ |
| Simulate | m_runtimeScene |
✅ | ✅ | EditorCamera | ❌ |
状态转换:
[Edit Mode]
│
┌─────────┼─────────┐
│ │ │
[Play] [Simulate] [New/Open Scene]
│ │ │
└────┬────┴────┬────┘
│ │
[Stop] ←────┘
│
↓
[Edit Mode](切回编辑器场景)
Edit → Play:
m_editorScene深复制为m_runtimeScenem_runtimeScene->onRuntimeStart()初始化运行时- 层级面板禁用编辑
Edit → Simulate:
- 与 Play 相同,但使用 EditorCamera
- 物理运行,方便调试物理效果
Stop:
- 运行时场景
onRuntimeStop()清理 - 切回
m_editorScene - 层级面板恢复编辑
4.3 资源管理差异
编辑器与运行时对资源的访问权限不同:
| 操作 | EditorAssetManager | RuntimeAssetManager |
|---|---|---|
| 加载资源 | ✅ | ✅ |
| 导入资源 | ✅ | ❌ |
| 创建资源 | ✅ | ❌ |
| 删除资源 | ✅ | ❌ |
这种设计确保:
- 运行时安全:玩家无法导入新资源,防止作弊
- 编辑器便捷:开发者可以直接在编辑器内导入、管理资源
- 职责清晰:资源导入是开发流程的一部分,不是运行时必需的
实现上,两个 AssetManager 继承自同一基类:
class AssetManagerBase { ... };
class EditorAssetManager : public AssetManagerBase {
void importAsset(const std::filesystem::path&) { /* 实现 */ }
};
class RuntimeAssetManager : public AssetManagerBase {
void importAsset(const std::filesystem::path&) = delete; // 禁止
};
5. 总结
5.1 分层架构的核心价值
| 价值 | 说明 |
|---|---|
| 职责清晰 | 编辑器管开发,运行时管执行,各司其职 |
| 代码复用 | Engine Core 被两方共享,维护一份代码 |
| 灵活发布 | 按需构建,只发行运行时 |
| 易于测试 | 核心逻辑可脱离编辑器独立测试 |
5.2 关键设计决策
| 决策 | 选择 | 理由 |
|---|---|---|
| 核心库形式 | 静态库 | 编译时链接,确保版本一致 |
| 脚本隔离 | 独立 C# 项目 | 快速编译迭代,与引擎解耦 |
| 场景复制 | 深复制 | 确保编辑/运行时隔离 |
| 状态机 | Edit/Play/Simulate 三态 | 支持物理调试 |
5.3 架构优势
这种分层架构让引擎具备:
- 类似 Unity 的开发体验
- 可独立迭代的模块
- 可裁剪的运行时体积
- 清晰的代码边界
文档版本:1.0
参考:Unity Editor/Player Architecture