refactor: rename ui_editor to editor for consistency
This commit is contained in:
546
docs/plan/XCEngine输入系统设计.md
Normal file
546
docs/plan/XCEngine输入系统设计.md
Normal file
@@ -0,0 +1,546 @@
|
||||
# XCEngine 输入系统设计与实现
|
||||
|
||||
> **文档信息**
|
||||
> - **版本**: 1.0
|
||||
> - **日期**: 2026-03-22
|
||||
> - **状态**: 设计文档
|
||||
> - **目标**: 设计引擎级输入系统
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
|
||||
XCEngine 输入系统需要提供:
|
||||
1. **统一的跨平台输入抽象** - 支持键盘、鼠标、手柄、触摸
|
||||
2. **与引擎架构无缝集成** - 使用现有的 `Core::Event` 系统
|
||||
3. **轮询 + 事件混合模式** - 既支持 `IsKeyDown()` 轮询,也支持事件回调
|
||||
4. **UI 系统支持** - 为 UI 组件 (Button, Slider 等) 提供指针事件
|
||||
|
||||
### 1.2 当前状态分析
|
||||
|
||||
| 模块 | 状态 | 说明 |
|
||||
|-----|------|------|
|
||||
| Core::Event | ✅ 完备 | 线程安全,Subscribe/Unsubscribe 模式 |
|
||||
| RHI::RHISwapChain | ⚠️ PollEvents 空实现 | 需要填充 Windows 消息泵 |
|
||||
| 现有 Input (mvs) | ❌ 耦合 Windows | 直接处理 HWND 消息,不适合引擎架构 |
|
||||
| Platform/Window | ❌ 不存在 | 需要新建 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 模块结构
|
||||
|
||||
```
|
||||
engine/include/XCEngine/
|
||||
├── Core/
|
||||
│ └── Event.h # 已有,复用
|
||||
├── Input/
|
||||
│ ├── InputTypes.h # 枚举和结构体定义
|
||||
│ ├── InputEvent.h # 输入事件结构体
|
||||
│ ├── InputManager.h # 输入管理器(单例)
|
||||
│ └── InputModule.h # 平台相关输入处理模块接口
|
||||
└── Platform/
|
||||
├── PlatformTypes.h # 平台类型抽象
|
||||
├── Window.h # 窗口抽象接口
|
||||
└── Windows/
|
||||
├── WindowsPlatform.h # Windows 平台实现
|
||||
└── WindowsInputModule.h # Windows 输入模块实现
|
||||
```
|
||||
|
||||
### 2.2 核心设计原则
|
||||
|
||||
1. **事件驱动 + 状态轮询双模式**
|
||||
- 事件:用于 UI 交互、一次性按键响应
|
||||
- 轮询:用于连续输入检测(角色移动、视角控制)
|
||||
|
||||
2. **平台抽象层**
|
||||
- `InputModule` 接口:抽象平台特定的输入处理
|
||||
- `Window` 接口:抽象平台特定的窗口管理
|
||||
|
||||
3. **与现有引擎组件集成**
|
||||
- 使用 `Core::Event` 作为事件系统
|
||||
- 使用 `Math::Vector2` 作为 2D 坐标类型
|
||||
|
||||
---
|
||||
|
||||
## 3. 详细设计
|
||||
|
||||
### 3.1 输入类型定义 (`InputTypes.h`)
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#include "Core/Types.h"
|
||||
#include "Math/Vector2.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Input {
|
||||
|
||||
enum class KeyCode : uint8 {
|
||||
None = 0,
|
||||
A = 4, B = 5, C = 6, D = 7, E = 8, F = 9, G = 10,
|
||||
H = 11, I = 12, J = 13, K = 14, L = 15, M = 16, N = 17,
|
||||
O = 18, P = 19, Q = 20, R = 21, S = 22, T = 23, U = 24,
|
||||
V = 25, W = 26, X = 27, Y = 28, Z = 29,
|
||||
F1 = 58, F2 = 59, F3 = 60, F4 = 61, F5 = 62, F6 = 63,
|
||||
F7 = 64, F8 = 65, F9 = 66, F10 = 67, F11 = 68, F12 = 69,
|
||||
Space = 49, Tab = 48, Enter = 36, Escape = 53,
|
||||
LeftShift = 56, RightShift = 60, LeftCtrl = 59, RightCtrl = 62,
|
||||
LeftAlt = 58, RightAlt = 61,
|
||||
Up = 126, Down = 125, Left = 123, Right = 124,
|
||||
Home = 115, End = 119, PageUp = 116, PageDown = 121,
|
||||
Delete = 51, Backspace = 51
|
||||
};
|
||||
|
||||
enum class MouseButton : uint8 {
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
Middle = 2,
|
||||
Button4 = 3,
|
||||
Button5 = 4
|
||||
};
|
||||
|
||||
enum class JoystickButton : uint8 {
|
||||
Button0 = 0,
|
||||
Button1 = 1,
|
||||
Button2 = 2,
|
||||
// ... Xbox/PlayStation 标准按钮
|
||||
};
|
||||
|
||||
enum class JoystickAxis : uint8 {
|
||||
LeftX = 0,
|
||||
LeftY = 1,
|
||||
RightX = 2,
|
||||
RightY = 3,
|
||||
LeftTrigger = 4,
|
||||
RightTrigger = 5
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace XCEngine
|
||||
```
|
||||
|
||||
### 3.2 输入事件结构体 (`InputEvent.h`)
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#include "InputTypes.h"
|
||||
#include "Math/Vector2.h"
|
||||
#include "Containers/String.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Input {
|
||||
|
||||
struct KeyEvent {
|
||||
KeyCode keyCode;
|
||||
bool alt;
|
||||
bool ctrl;
|
||||
bool shift;
|
||||
bool meta;
|
||||
enum Type { Down, Up, Repeat } type;
|
||||
};
|
||||
|
||||
struct MouseButtonEvent {
|
||||
MouseButton button;
|
||||
Math::Vector2 position;
|
||||
enum Type { Pressed, Released } type;
|
||||
};
|
||||
|
||||
struct MouseMoveEvent {
|
||||
Math::Vector2 position;
|
||||
Math::Vector2 delta; // 相对上一帧的移动量
|
||||
};
|
||||
|
||||
struct MouseWheelEvent {
|
||||
Math::Vector2 position;
|
||||
float delta; // 滚轮滚动量
|
||||
};
|
||||
|
||||
struct TextInputEvent {
|
||||
char character;
|
||||
Containers::String text; // 完整输入文本
|
||||
};
|
||||
|
||||
struct TouchState {
|
||||
int touchId;
|
||||
Math::Vector2 position;
|
||||
Math::Vector2 deltaPosition;
|
||||
float deltaTime;
|
||||
int tapCount;
|
||||
enum Phase { Began, Moved, Stationary, Ended, Canceled } phase;
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace XCEngine
|
||||
```
|
||||
|
||||
### 3.3 输入管理器 (`InputManager.h`)
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#include "Core/Event.h"
|
||||
#include "InputTypes.h"
|
||||
#include "InputEvent.h"
|
||||
#include "Math/Vector2.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Input {
|
||||
|
||||
class InputManager {
|
||||
public:
|
||||
static InputManager& Get();
|
||||
|
||||
void Initialize(void* platformWindowHandle);
|
||||
void Shutdown();
|
||||
void Update(); // 每帧调用,更新输入状态
|
||||
|
||||
// ============ 轮询接口 ============
|
||||
|
||||
// 键盘
|
||||
bool IsKeyDown(KeyCode key) const;
|
||||
bool IsKeyUp(KeyCode key) const;
|
||||
bool IsKeyPressed(KeyCode key) const; // 当前帧按下
|
||||
|
||||
// 鼠标
|
||||
Math::Vector2 GetMousePosition() const;
|
||||
Math::Vector2 GetMouseDelta() const;
|
||||
float GetMouseScrollDelta() const;
|
||||
bool IsMouseButtonDown(MouseButton button) const;
|
||||
bool IsMouseButtonUp(MouseButton button) const;
|
||||
bool IsMouseButtonClicked(MouseButton button) const; // 当前帧点击
|
||||
|
||||
// 手柄
|
||||
int GetJoystickCount() const;
|
||||
bool IsJoystickConnected(int joystick) const;
|
||||
Math::Vector2 GetJoystickAxis(int joystick, JoystickAxis axis) const;
|
||||
bool IsJoystickButtonDown(int joystick, JoystickButton button) const;
|
||||
|
||||
// ============ 事件接口 ============
|
||||
|
||||
Core::Event<const KeyEvent&>& OnKeyEvent() { return m_onKeyEvent; }
|
||||
Core::Event<const MouseButtonEvent&>& OnMouseButton() { return m_onMouseButton; }
|
||||
Core::Event<const MouseMoveEvent&>& OnMouseMove() { return m_onMouseMove; }
|
||||
Core::Event<const MouseWheelEvent&>& OnMouseWheel() { return m_onMouseWheel; }
|
||||
Core::Event<const TextInputEvent&>& OnTextInput() { return m_onTextInput; }
|
||||
|
||||
// 内部方法(供 PlatformInputModule 调用)
|
||||
void ProcessKeyDown(KeyCode key, bool repeat);
|
||||
void ProcessKeyUp(KeyCode key);
|
||||
void ProcessMouseMove(int x, int y, int deltaX, int deltaY);
|
||||
void ProcessMouseButton(MouseButton button, bool pressed, int x, int y);
|
||||
void ProcessMouseWheel(float delta, int x, int y);
|
||||
void ProcessTextInput(char c);
|
||||
|
||||
private:
|
||||
InputManager() = default;
|
||||
~InputManager() = default;
|
||||
|
||||
void* m_platformWindowHandle = nullptr;
|
||||
|
||||
// 键盘状态
|
||||
std::vector<bool> m_keyDownThisFrame;
|
||||
std::vector<bool> m_keyDownLastFrame;
|
||||
std::vector<bool> m_keyDown;
|
||||
|
||||
// 鼠标状态
|
||||
Math::Vector2 m_mousePosition;
|
||||
Math::Vector2 m_mouseDelta;
|
||||
float m_mouseScrollDelta = 0.0f;
|
||||
std::vector<bool> m_mouseButtonDownThisFrame;
|
||||
std::vector<bool> m_mouseButtonDownLastFrame;
|
||||
std::vector<bool> m_mouseButtonDown;
|
||||
|
||||
// 事件
|
||||
Core::Event<const KeyEvent&> m_onKeyEvent;
|
||||
Core::Event<const MouseButtonEvent&> m_onMouseButton;
|
||||
Core::Event<const MouseMoveEvent&> m_onMouseMove;
|
||||
Core::Event<const MouseWheelEvent&> m_onMouseWheel;
|
||||
Core::Event<const TextInputEvent&> m_onTextInput;
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace XCEngine
|
||||
```
|
||||
|
||||
### 3.4 平台输入模块接口 (`InputModule.h`)
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Input {
|
||||
|
||||
class InputModule {
|
||||
public:
|
||||
virtual ~InputModule() = default;
|
||||
|
||||
virtual void Initialize(void* windowHandle) = 0;
|
||||
virtual void Shutdown() = 0;
|
||||
virtual void PumpEvents() = 0; // 抽取并处理平台事件
|
||||
|
||||
protected:
|
||||
InputModule() = default;
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace XCEngine
|
||||
```
|
||||
|
||||
### 3.5 Windows 输入模块实现 (`WindowsInputModule.h`)
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#include "InputModule.h"
|
||||
#include <Windows.h>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Input {
|
||||
namespace Platform {
|
||||
|
||||
class WindowsInputModule : public InputModule {
|
||||
public:
|
||||
void Initialize(void* windowHandle) override;
|
||||
void Shutdown() override;
|
||||
void PumpEvents() override;
|
||||
|
||||
// 供 Window 调用的消息处理
|
||||
void HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
|
||||
|
||||
private:
|
||||
void ProcessKeyDown(WPARAM wParam, LPARAM lParam);
|
||||
void ProcessKeyUp(WPARAM wParam, LPARAM lParam);
|
||||
void ProcessMouseMove(WPARAM wParam, LPARAM lParam);
|
||||
void ProcessMouseButton(WPARAM wParam, LPARAM lParam, bool pressed);
|
||||
void ProcessMouseWheel(WPARAM wParam, LPARAM lParam);
|
||||
void ProcessCharInput(WPARAM wParam, LPARAM lParam);
|
||||
|
||||
HWND m_hwnd = nullptr;
|
||||
bool m_captureMouse = false;
|
||||
};
|
||||
|
||||
} // namespace Platform
|
||||
} // namespace Input
|
||||
} // namespace XCEngine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 与引擎其他模块的集成
|
||||
|
||||
### 4.1 与 RHI/SwapChain 的集成
|
||||
|
||||
`RHISwapChain::PollEvents()` 需要调用 `InputManager::Update()` 和平台输入模块的 `PumpEvents()`:
|
||||
|
||||
```cpp
|
||||
// D3D12SwapChain.cpp
|
||||
void D3D12SwapChain::PollEvents() {
|
||||
// 抽取 Windows 消息
|
||||
if (m_inputModule) {
|
||||
m_inputModule->PumpEvents();
|
||||
}
|
||||
|
||||
// 更新输入管理器状态
|
||||
Input::InputManager::Get().Update();
|
||||
|
||||
// 处理关闭请求
|
||||
MSG msg;
|
||||
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
if (msg.message == WM_QUIT) {
|
||||
m_shouldClose = true;
|
||||
}
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 与 UI 系统的集成
|
||||
|
||||
UI 组件 (Button, Slider 等) 通过订阅 `InputManager` 的事件来响应用户输入:
|
||||
|
||||
```cpp
|
||||
// ButtonComponent.cpp
|
||||
void ButtonComponent::Update(float deltaTime) {
|
||||
if (!IsEnabled()) return;
|
||||
|
||||
auto& input = Input::InputManager::Get();
|
||||
Vector2 mousePos = input.GetMousePosition();
|
||||
|
||||
// 射线检测是否悬停在按钮上
|
||||
if (IsPointInRect(mousePos, m_rect)) {
|
||||
if (input.IsMouseButtonClicked(Input::MouseButton::Left)) {
|
||||
OnClick.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 与场景生命周期的集成
|
||||
|
||||
```cpp
|
||||
// Scene.cpp
|
||||
void Scene::Awake() {
|
||||
// 初始化输入系统
|
||||
Input::InputManager::Get().Initialize(m_windowHandle);
|
||||
}
|
||||
|
||||
void Scene::OnDestroy() {
|
||||
Input::InputManager::Get().Shutdown();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Windows 消息到引擎事件的映射
|
||||
|
||||
| Windows Message | Engine Event |
|
||||
|----------------|---------------|
|
||||
| WM_KEYDOWN | `InputManager::ProcessKeyDown` |
|
||||
| WM_KEYUP | `InputManager::ProcessKeyUp` |
|
||||
| WM_CHAR | `InputManager::ProcessTextInput` |
|
||||
| WM_MOUSEMOVE | `InputManager::ProcessMouseMove` |
|
||||
| WM_LBUTTONDOWN/RBUTTONDOWN/MBUTTONDOWN | `InputManager::ProcessMouseButton` |
|
||||
| WM_LBUTTONUP/RBUTTONUP/MBUTTONUP | `InputManager::ProcessMouseButton` |
|
||||
| WM_MOUSEWHEEL | `InputManager::ProcessMouseWheel` |
|
||||
|
||||
### 5.1 Windows VK 码到 KeyCode 的映射
|
||||
|
||||
```cpp
|
||||
// WindowsVKToKeyCode 映射表
|
||||
static const KeyCode VK_TO_KEYCODE[] = {
|
||||
// VK 0-29 对应 KeyCode A-Z
|
||||
KeyCode::A, KeyCode::B, KeyCode::C, KeyCode::D, KeyCode::E,
|
||||
KeyCode::F, KeyCode::G, KeyCode::H, KeyCode::I, KeyCode::J,
|
||||
KeyCode::K, KeyCode::L, KeyCode::M, KeyCode::N, KeyCode::O,
|
||||
KeyCode::P, KeyCode::Q, KeyCode::R, KeyCode::S, KeyCode::T,
|
||||
KeyCode::U, KeyCode::V, KeyCode::W, KeyCode::X, KeyCode::Y,
|
||||
KeyCode::Z,
|
||||
// ... 其他映射
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 实现计划
|
||||
|
||||
### Phase 1: 核心输入系统
|
||||
|
||||
| 任务 | 文件 | 说明 |
|
||||
|-----|------|------|
|
||||
| 创建 Input 模块目录 | `engine/include/XCEngine/Input/` | |
|
||||
| 实现 InputTypes.h | KeyCode, MouseButton 等枚举 | |
|
||||
| 实现 InputEvent.h | 事件结构体 | |
|
||||
| 实现 InputManager.h/cpp | 单例管理器 | |
|
||||
| 实现 WindowsInputModule.h/cpp | Windows 平台实现 | |
|
||||
|
||||
### Phase 2: 与 RHI 集成
|
||||
|
||||
| 任务 | 文件 | 说明 |
|
||||
|-----|------|------|
|
||||
| 修改 RHISwapChain | 添加 InputModule 成员 | |
|
||||
| 实现 D3D12SwapChain::PollEvents | 填充消息泵逻辑 | |
|
||||
| 实现 OpenGLSwapChain::PollEvents | GL 消息处理 | |
|
||||
|
||||
### Phase 3: UI 输入支持
|
||||
|
||||
| 任务 | 说明 |
|
||||
|-----|------|
|
||||
| 实现 Canvas 组件的射线检测 | 将屏幕坐标转换为 UI 元素 |
|
||||
| Button/Slider 事件集成 | 使用 InputManager 事件 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 使用示例
|
||||
|
||||
### 7.1 轮询模式(角色移动)
|
||||
|
||||
```cpp
|
||||
void PlayerController::Update(float deltaTime) {
|
||||
auto& input = Input::InputManager::Get();
|
||||
|
||||
if (input.IsKeyDown(Input::KeyCode::W)) {
|
||||
m_velocity.z = 1.0f;
|
||||
} else if (input.IsKeyDown(Input::KeyCode::S)) {
|
||||
m_velocity.z = -1.0f;
|
||||
}
|
||||
|
||||
if (input.IsKeyDown(Input::KeyCode::A)) {
|
||||
m_velocity.x = -1.0f;
|
||||
} else if (input.IsKeyDown(Input::KeyCode::D)) {
|
||||
m_velocity.x = 1.0f;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 事件模式(UI 交互)
|
||||
|
||||
```cpp
|
||||
class MyUIButton : public Component {
|
||||
uint64_t m_clickHandlerId = 0;
|
||||
|
||||
void Awake() override {
|
||||
m_clickHandlerId = Input::InputManager::Get().OnMouseButton().Subscribe(
|
||||
[this](const Input::MouseButtonEvent& event) {
|
||||
if (event.type == Input::MouseButtonEvent::Pressed &&
|
||||
event.button == Input::MouseButton::Left &&
|
||||
IsPointInButton(event.position)) {
|
||||
OnClicked();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void OnDestroy() override {
|
||||
Input::InputManager::Get().OnMouseButton().Unsubscribe(m_clickHandlerId);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 7.3 文本输入
|
||||
|
||||
```cpp
|
||||
void InputFieldComponent::Update(float deltaTime) {
|
||||
auto& input = Input::InputManager::Get();
|
||||
|
||||
uint64_t textHandlerId = input.OnTextInput().Subscribe(
|
||||
[this](const Input::TextInputEvent& event) {
|
||||
if (m_isFocused) {
|
||||
m_text += event.character;
|
||||
OnValueChanged.Invoke(m_text);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 清理
|
||||
input.OnTextInput().Unsubscribe(textHandlerId);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 待讨论问题
|
||||
|
||||
1. **多窗口支持**: 引擎是否需要支持多窗口?如果是,输入系统如何路由事件到正确的窗口?
|
||||
|
||||
2. **手柄/游戏手柄支持**: 是否需要实现 XInput 支持?这会增加复杂性。
|
||||
|
||||
3. **原始输入 vs 输入模式**: Unity 有 "Input.GetAxis" vs "Input.GetButtonDown"。是否需要实现类似的轴概念?
|
||||
|
||||
4. **IME/输入法支持**: 对于中文、日文输入,是否需要特殊处理?
|
||||
|
||||
5. **平台模块位置**: `InputModule` 放在 `Input/` 还是 `Platform/` 命名空间下?
|
||||
|
||||
---
|
||||
|
||||
## 9. 结论
|
||||
|
||||
XCEngine 输入系统设计遵循以下核心原则:
|
||||
|
||||
1. **复用现有 Core::Event** - 不重复造轮子
|
||||
2. **平台抽象** - 通过 `InputModule` 接口隔离平台差异
|
||||
3. **双模式支持** - 轮询 + 事件,兼顾性能和响应性
|
||||
4. **与 UI 架构协同** - 为 Canvas/Button 提供必要的事件支持
|
||||
|
||||
该设计参考了 Unity 的 Input 系统架构,同时适配了 XCEngine 现有的组件模式和事件系统。
|
||||
458
docs/plan/end/编辑器与运行时分层架构设计.md
Normal file
458
docs/plan/end/编辑器与运行时分层架构设计.md
Normal file
@@ -0,0 +1,458 @@
|
||||
# 编辑器与运行时分层架构设计
|
||||
|
||||
## 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 分层原则
|
||||
|
||||
编辑器与运行时的分层应遵循以下原则:
|
||||
|
||||
1. **核心不可依赖编辑器**:Engine Core 不应包含任何 UI 相关的代码,确保可以在无界面环境下运行
|
||||
2. **运行时独立最小化**:运行时只包含游戏运行必需的组件,不包含编辑器特有功能
|
||||
3. **接口抽象分离**:通过接口隔离变化,便于未来替换实现
|
||||
4. **数据驱动**:场景、配置等数据与代码逻辑分离
|
||||
|
||||
### 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 时复制的场景 | 运行时逻辑可改 |
|
||||
|
||||
复制过程:
|
||||
|
||||
```cpp
|
||||
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**:
|
||||
1. `m_editorScene` 深复制为 `m_runtimeScene`
|
||||
2. `m_runtimeScene->onRuntimeStart()` 初始化运行时
|
||||
3. 层级面板禁用编辑
|
||||
|
||||
**Edit → Simulate**:
|
||||
1. 与 Play 相同,但使用 EditorCamera
|
||||
2. 物理运行,方便调试物理效果
|
||||
|
||||
**Stop**:
|
||||
1. 运行时场景 `onRuntimeStop()` 清理
|
||||
2. 切回 `m_editorScene`
|
||||
3. 层级面板恢复编辑
|
||||
|
||||
---
|
||||
|
||||
### 4.3 资源管理差异
|
||||
|
||||
编辑器与运行时对资源的访问权限不同:
|
||||
|
||||
| 操作 | EditorAssetManager | RuntimeAssetManager |
|
||||
|------|-------------------|---------------------|
|
||||
| 加载资源 | ✅ | ✅ |
|
||||
| 导入资源 | ✅ | ❌ |
|
||||
| 创建资源 | ✅ | ❌ |
|
||||
| 删除资源 | ✅ | ❌ |
|
||||
|
||||
这种设计确保:
|
||||
- **运行时安全**:玩家无法导入新资源,防止作弊
|
||||
- **编辑器便捷**:开发者可以直接在编辑器内导入、管理资源
|
||||
- **职责清晰**:资源导入是开发流程的一部分,不是运行时必需的
|
||||
|
||||
实现上,两个 AssetManager 继承自同一基类:
|
||||
|
||||
```cpp
|
||||
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*
|
||||
Reference in New Issue
Block a user