Unified logging: Replace LogSystem with EditorConsoleSink
- Created EditorConsoleSink (implements ILogSink interface) - EditorConsoleSink stores logs in memory buffer (max 1000 entries) - Added to Debug::Logger in Application::Initialize() - ConsolePanel now reads from EditorConsoleSink via static GetInstance() - Removed separate LogSystem singleton - Removed editor/src/Core/LogEntry.h (no longer needed) Now Editor and Engine share the same Debug::Logger, with ConsolePanel displaying logs via EditorConsoleSink.
This commit is contained in:
@@ -1,403 +0,0 @@
|
||||
# D3D12 Texture 封装架构修复方案
|
||||
|
||||
## 1. 问题分析
|
||||
|
||||
### 1.1 当前问题
|
||||
|
||||
**问题 A:双重 Texture 包装导致悬垂指针**
|
||||
|
||||
```cpp
|
||||
// D3D12SwapChain 内部:
|
||||
std::vector<D3D12Texture> m_backBuffers; // SwapChain 内部的包装
|
||||
|
||||
// minimal/main.cpp:
|
||||
D3D12Texture gColorRTs[2]; // 用户代码又创建了一套包装!
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
ID3D12Resource* buffer = nullptr;
|
||||
gSwapChain.GetSwapChain()->GetBuffer(i, IID_PPV_ARGS(&buffer)); // 获取原生指针
|
||||
gColorRTs[i].InitializeFromExisting(buffer); // 再包装一次
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:
|
||||
- `m_backBuffers[i]` 和 `gColorRTs[i]` 是两个不同的 `D3D12Texture` 对象
|
||||
- 但它们内部都持有指向**同一个** `ID3D12Resource` 的 `ComPtr`
|
||||
- 当其中一个调用 `Shutdown()` → `m_resource.Reset()`,另一个就变成悬垂指针!
|
||||
|
||||
**问题 B:`InitializeFromExisting` 不区分所有权语义**
|
||||
|
||||
```cpp
|
||||
void D3D12Texture::Shutdown() {
|
||||
m_resource.Reset(); // 无条件释放资源
|
||||
}
|
||||
```
|
||||
|
||||
如果通过 `InitializeFromExisting` 包装了 SwapChain 的 back buffer,Shutdown 会错误地释放它!
|
||||
|
||||
**问题 C:`D3D12SwapChain::GetBackBuffer()` 返回内部引用,用户可能误用**
|
||||
|
||||
```cpp
|
||||
D3D12Texture* D3D12SwapChain::GetBackBuffer(uint32_t index) const {
|
||||
return const_cast<D3DTexture*>(&m_backBuffers[index]); // 返回内部成员引用
|
||||
}
|
||||
|
||||
// 用户可能写出:
|
||||
D3D12Texture* rt = gSwapChain.GetBackBuffer(0);
|
||||
gSwapChain.Shutdown(); // rt 变成悬垂指针!
|
||||
```
|
||||
|
||||
### 1.2 问题根因
|
||||
|
||||
| 问题 | 根因 |
|
||||
|------|------|
|
||||
| 双重包装 | 没有阻止用户创建额外的包装 |
|
||||
| 所有权模糊 | `InitializeFromExisting` 不区分"接管"和"借用" |
|
||||
| 悬垂引用 | `GetBackBuffer` 返回内部指针,生命周期不安全 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 修复方案
|
||||
|
||||
### 2.1 方案概述
|
||||
|
||||
引入**所有权语义标记**和**生命周期安全保证**:
|
||||
|
||||
1. **`InitializeFromExisting` 增加 `ownsResource` 参数**
|
||||
2. **`D3D12Texture` 内部根据标记决定是否释放资源**
|
||||
3. **`D3D12SwapChain::GetBackBuffer` 返回安全引用(弱引用或克隆)**
|
||||
4. **提供统一的 Back Buffer 访问接口,避免用户直接调用 `GetSwapChain()->GetBuffer()`**
|
||||
|
||||
### 2.2 详细设计
|
||||
|
||||
#### 2.2.1 修改 `D3D12Texture`
|
||||
|
||||
**文件**: `engine/include/XCEngine/RHI/D3D12/D3D12Texture.h`
|
||||
|
||||
```cpp
|
||||
class D3D12Texture : public RHITexture {
|
||||
public:
|
||||
D3D12Texture();
|
||||
~D3D12Texture() override;
|
||||
|
||||
// 创建并拥有资源
|
||||
bool Initialize(ID3D12Device* device, const D3D12_RESOURCE_DESC& desc,
|
||||
D3D12_RESOURCE_STATES initialState = D3D12_RESOURCE_STATE_COMMON);
|
||||
|
||||
// 包装已有资源,明确所有权语义
|
||||
// ownsResource = true : 获取所有权,Shutdown 时释放
|
||||
// ownsResource = false : 不获取所有权,Shutdown 时不释放
|
||||
bool InitializeFromExisting(ID3D12Resource* resource, bool ownsResource = false);
|
||||
|
||||
bool InitializeFromData(ID3D12Device* device, ID3D12GraphicsCommandList* commandList,
|
||||
const void* pixelData, uint32_t width, uint32_t height, DXGI_FORMAT format);
|
||||
bool InitializeDepthStencil(ID3D12Device* device, uint32_t width, uint32_t height,
|
||||
DXGI_FORMAT format = DXGI_FORMAT_D24_UNORM_S8_UINT);
|
||||
|
||||
void Shutdown() override;
|
||||
|
||||
ID3D12Resource* GetResource() const { return m_resource.Get(); }
|
||||
D3D12_RESOURCE_DESC GetDesc() const { return m_resource->GetDesc(); }
|
||||
|
||||
// 检查是否拥有资源所有权
|
||||
bool OwnsResource() const { return m_ownsResource; }
|
||||
|
||||
// ... 其他现有方法 ...
|
||||
|
||||
private:
|
||||
ComPtr<ID3D12Resource> m_resource;
|
||||
ResourceStates m_state = ResourceStates::Common;
|
||||
std::string m_name;
|
||||
bool m_ownsResource = false; // 新增:所有权标记
|
||||
};
|
||||
```
|
||||
|
||||
**文件**: `engine/src/RHI/D3D12/D3D12Texture.cpp`
|
||||
|
||||
```cpp
|
||||
bool D3D12Texture::InitializeFromExisting(ID3D12Resource* resource, bool ownsResource) {
|
||||
m_resource = resource;
|
||||
m_ownsResource = ownsResource; // 明确设置所有权
|
||||
return true;
|
||||
}
|
||||
|
||||
void D3D12Texture::Shutdown() {
|
||||
if (m_ownsResource) {
|
||||
// 仅当拥有所有权时才释放
|
||||
m_resource.Reset();
|
||||
}
|
||||
// 如果不拥有资源,只是清除引用,不释放底层的 COM 对象
|
||||
m_resource.Reset(); // ComPtr::Reset() 只是减少引用计数,不是释放
|
||||
// 但要注意:如果不拥有所有权,我们需要保留原始指针以防止意外释放
|
||||
// 实际上 ComPtr::Reset() 总是会调用 Release()
|
||||
// 所以我们需要用不同的策略
|
||||
}
|
||||
```
|
||||
|
||||
**等等,`ComPtr::Reset()` 总是会调用 Release()。如果我们要实现"不拥有但不释放",需要用原始指针存储。**
|
||||
|
||||
**修正方案**:
|
||||
|
||||
```cpp
|
||||
class D3D12Texture : public RHITexture {
|
||||
private:
|
||||
ComPtr<ID3D12Resource> m_resource; // 始终持有
|
||||
ID3D12Resource* m_externalResource = nullptr; // 外部资源指针(不拥有)
|
||||
bool m_ownsResource = false;
|
||||
|
||||
public:
|
||||
void Shutdown() override {
|
||||
if (m_ownsResource) {
|
||||
m_resource.Reset(); // 释放拥有的资源
|
||||
}
|
||||
// 如果是外部资源(不拥有),只是清除引用
|
||||
m_externalResource = nullptr;
|
||||
m_resource.Reset(); // 总是 Reset,因为 m_resource 可能持有原始指针的副本
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
**更简洁的方案**:让用户自己决定是否通过 `Initialize` 创建 texture。`InitializeFromExisting` 包装但不拥有,使用者负责保证生命周期。
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.2 修改 `D3D12SwapChain`
|
||||
|
||||
**目标**:提供安全的 BackBuffer 访问接口,阻止用户创建重复包装。
|
||||
|
||||
**文件**: `engine/include/XCEngine/RHI/D3D12/D3D12SwapChain.h`
|
||||
|
||||
```cpp
|
||||
class D3D12SwapChain : public RHISwapChain {
|
||||
public:
|
||||
// ... 现有接口 ...
|
||||
|
||||
// 获取 BackBuffer - 返回引用而非指针,防止悬垂
|
||||
// 返回的引用在 SwapChain 存活期间有效
|
||||
D3D12Texture& GetBackBuffer(uint32_t index);
|
||||
const D3D12Texture& GetBackBuffer(uint32_t index) const;
|
||||
|
||||
// 获取当前 BackBuffer
|
||||
D3D12Texture& GetCurrentBackBuffer();
|
||||
const D3D12Texture& GetCurrentBackBuffer() const;
|
||||
|
||||
// 删除不安全的 GetSwapChain() 暴露方法!
|
||||
// 旧接口:IDXGISwapChain3* GetSwapChain() const { return m_swapChain.Get(); }
|
||||
// 新策略:如果确实需要原生指针,提供 GetNativeHandle() 但不返回具体类型
|
||||
|
||||
private:
|
||||
// 确保 BackBuffer 不能被外部直接修改
|
||||
void SetBackBuffer(uint32_t index, D3D12Texture& texture) = delete;
|
||||
};
|
||||
```
|
||||
|
||||
**修正 `GetBackBuffer` 返回值**:
|
||||
|
||||
```cpp
|
||||
D3D12Texture& D3D12SwapChain::GetBackBuffer(uint32_t index) {
|
||||
assert(index < m_backBuffers.size() && "BackBuffer index out of range");
|
||||
return m_backBuffers[index];
|
||||
}
|
||||
|
||||
const D3D12Texture& D3D12SwapChain::GetBackBuffer(uint32_t index) const {
|
||||
assert(index < m_backBuffers.size() && "BackBuffer index out of range");
|
||||
return m_backBuffers[index];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2.2.3 更新 `minimal/main.cpp`
|
||||
|
||||
```cpp
|
||||
// 修改前(有问题):
|
||||
for (int i = 0; i < 2; i++) {
|
||||
ID3D12Resource* buffer = nullptr;
|
||||
gSwapChain.GetSwapChain()->GetBuffer(i, IID_PPV_ARGS(&buffer)); // ❌ 原生 API
|
||||
gColorRTs[i].InitializeFromExisting(buffer);
|
||||
// ...
|
||||
}
|
||||
|
||||
// 修改后(使用封装):
|
||||
for (int i = 0; i < 2; i++) {
|
||||
// 直接使用封装好的 BackBuffer,不再重复包装
|
||||
D3D12Texture& backBuffer = gSwapChain.GetBackBuffer(i);
|
||||
|
||||
// RTV 创建
|
||||
CPUDescriptorHandle rtvCpuHandle = gRTVHeap.GetCPUDescriptorHandle(i);
|
||||
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle = { rtvCpuHandle.ptr };
|
||||
gRTVs[i].InitializeAt(device, backBuffer.GetResource(), rtvHandle, nullptr);
|
||||
}
|
||||
```
|
||||
|
||||
**但是**:`gColorRTs[i]` 仍然存在且被用于后续渲染。我们需要决定是:
|
||||
- 方案 A:直接使用 `gSwapChain.GetBackBuffer(i)` 替代 `gColorRTs[i]`
|
||||
- 方案 B:让 `gColorRTs[i]` 变成对 `gSwapChain.GetBackBuffer(i)` 的引用
|
||||
|
||||
**推荐方案 A**:移除 `gColorRTs` 数组,直接使用 `gSwapChain.GetBackBuffer()`。这样避免重复引用。
|
||||
|
||||
---
|
||||
|
||||
### 2.3 完整修改清单
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
|------|----------|
|
||||
| `D3D12Texture.h` | 添加 `m_ownsResource` 成员,修改 `InitializeFromExisting` 签名 |
|
||||
| `D3D12Texture.cpp` | 实现所有权语义,`Shutdown` 根据所有权决定是否释放 |
|
||||
| `D3D12SwapChain.h` | 修改 `GetBackBuffer` 返回引用,删除 `GetSwapChain()` 暴露 |
|
||||
| `D3D12SwapChain.cpp` | 实现返回引用的 `GetBackBuffer`,添加断言检查 |
|
||||
| `minimal/main.cpp` | 使用封装后的 `GetBackBuffer()`,移除原生 API 调用 |
|
||||
| `TEST_SPEC.md` | 更新架构说明,移除已知限制 7.2 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 实施步骤
|
||||
|
||||
### Phase 1: 基础修改
|
||||
1. 修改 `D3D12Texture` 添加所有权语义
|
||||
2. 修改 `D3D12SwapChain` 的 `GetBackBuffer` 返回引用
|
||||
3. 运行单元测试确保基础功能正常
|
||||
|
||||
### Phase 2: 更新集成测试
|
||||
1. 修改 `minimal/main.cpp` 使用新的 `GetBackBuffer()` API
|
||||
2. 移除 `gColorRTs` 数组(如果可能)
|
||||
3. 验证截图功能正常
|
||||
|
||||
### Phase 3: 文档和清理
|
||||
1. 更新 `TEST_SPEC.md`
|
||||
2. 删除 `OpenGL_Test_Restructuring_Plan.md` 中对 D3D12 的过时引用
|
||||
3. 提交所有更改
|
||||
|
||||
---
|
||||
|
||||
## 4. 风险和注意事项
|
||||
|
||||
### 4.1 兼容性风险
|
||||
- `InitializeFromExisting` 签名变更会影响所有调用方
|
||||
- 需要检查所有使用此方法的代码
|
||||
|
||||
### 4.2 生命周期风险
|
||||
- `GetBackBuffer()` 返回的引用在 `Shutdown()` 后无效
|
||||
- 用户必须确保在 SwapChain 存活期间使用
|
||||
|
||||
### 4.3 ComPtr 行为澄清
|
||||
|
||||
```cpp
|
||||
// ComPtr::Reset() 调用 Release()
|
||||
// 如果多个 ComPtr 指向同一资源,Reset 只会减少引用计数
|
||||
// 只有最后一个 Reset 才会真正释放
|
||||
|
||||
ComPtr<ID3D12Resource> a = resource; // AddRef
|
||||
ComPtr<ID3D12Resource> b = resource; // AddRef
|
||||
|
||||
a.Reset(); // Release,资源仍未释放(b 还持有)
|
||||
b.Reset(); // 最后一个 Release,资源被释放
|
||||
```
|
||||
|
||||
**关键点**:如果 `D3D12Texture` 和 `m_backBuffers` 都持有同一个 `ID3D12Resource` 的 `ComPtr`,那么:
|
||||
- `gColorRTs[i].InitializeFromExisting(buffer)` 会让 `gColorRTs[i].m_resource` 指向 `buffer`
|
||||
- `m_backBuffers[i].InitializeFromExisting(buffer)` 已经让 `m_backBuffers[i].m_resource` 指向 `buffer`
|
||||
- 现在有两个 `ComPtr` 指向同一个资源
|
||||
|
||||
**问题**:这两个 `ComPtr` 是在不同对象中的,它们各自会增加引用计数。但原始的 `GetBuffer()` 返回的 `buffer` 指针被两个 `ComPtr` 接管了。
|
||||
|
||||
让我重新审视 `D3D12SwapChain::Initialize`:
|
||||
|
||||
```cpp
|
||||
m_backBuffers.resize(m_bufferCount);
|
||||
for (uint32_t i = 0; i < m_bufferCount; ++i) {
|
||||
ID3D12Resource* resource = nullptr;
|
||||
m_swapChain->GetBuffer(i, IID_PPV_ARGS(&resource)); // resource 的引用计数 = 1
|
||||
m_backBuffers[i].InitializeFromExisting(resource); // ComPtr 接管,引用计数 = 2
|
||||
}
|
||||
```
|
||||
|
||||
**问题**:`resource` 被传给了 `InitializeFromExisting`,ComPtr 构造时会 `AddRef`。但 `resource` 是局部变量,函数结束后 `resource` 局部变量销毁但不影响 COM 对象的引用计数。
|
||||
|
||||
等等,这里有个问题:
|
||||
|
||||
```cpp
|
||||
ID3D12Resource* resource = nullptr;
|
||||
m_swapChain->GetBuffer(i, IID_PPV_ARGS(&resource)); // GetBuffer 返回的指针赋给 resource,引用计数 = 1
|
||||
m_backBuffers[i].InitializeFromExisting(resource); // ComPtr 拷贝构造,引用计数 = 2
|
||||
// 函数结束,resource 局部变量销毁(不影响引用计数,因为是指针变量)
|
||||
```
|
||||
|
||||
所以 `m_backBuffers[i].m_resource` 和 `m_swapChain` 内部都持有同一个 COM 对象的引用。
|
||||
|
||||
然后 `minimal/main.cpp` 又做了一次:
|
||||
|
||||
```cpp
|
||||
ID3D12Resource* buffer = nullptr;
|
||||
gSwapChain.GetSwapChain()->GetBuffer(i, IID_PPV_ARGS(&buffer)); // buffer 引用计数 = 3
|
||||
gColorRTs[i].InitializeFromExisting(buffer); // gColorRTs[i].m_resource 引用计数 = 4
|
||||
// buffer 局部变量销毁
|
||||
```
|
||||
|
||||
现在有三个 `ComPtr`(`m_backBuffers[i].m_resource`, `gColorRTs[i].m_resource`, `m_swapChain` 内部)指向同一个对象。引用计数 = 4。
|
||||
|
||||
**这不是问题**!因为 `ComPtr` 的拷贝构造会 `AddRef`。最终:
|
||||
- `m_backBuffers[i]` 销毁 → 引用计数--
|
||||
- `gColorRTs[i]` 销毁 → 引用计数--
|
||||
- `m_swapChain` 销毁 → 引用计数--
|
||||
|
||||
引用计数最终归零,资源被正确释放。
|
||||
|
||||
**那问题是什么?**
|
||||
|
||||
回到 `Shutdown`:
|
||||
|
||||
```cpp
|
||||
void D3D12Texture::Shutdown() {
|
||||
m_resource.Reset(); // ComPtr::Reset() 调用 Release()
|
||||
}
|
||||
```
|
||||
|
||||
如果 `gColorRTs[i]` 先于 `m_swapChain` 销毁:
|
||||
- `gColorRTs[i].Shutdown()` → `m_resource.Reset()` → Release() → 引用计数从 4 变成 3
|
||||
- `m_swapChain` 销毁时 → 内部资源 Release() → 引用计数从 3 变成 2
|
||||
|
||||
**这也应该是正常的...**
|
||||
|
||||
但等等!`m_backBuffers[i]` 是在 `D3D12SwapChain` 内部的 vector 中:
|
||||
|
||||
```cpp
|
||||
std::vector<D3D12Texture> m_backBuffers;
|
||||
```
|
||||
|
||||
如果 `minimal/main.cpp` 中 `gSwapChain.Shutdown()` 被调用:
|
||||
- `D3D12SwapChain::~D3D12SwapChain()` 调用 `Shutdown()`
|
||||
- `Shutdown()` 调用 `m_swapChain.Reset()`(仅重置 swapchain 指针)
|
||||
- 然后 vector `m_backBuffers` 销毁,每个 `D3D12Texture` 析构调用 `Shutdown()`
|
||||
- 每个 `Shutdown()` 调用 `m_resource.Reset()` → 释放 back buffer
|
||||
|
||||
**但是**:`gColorRTs[i]` 是在全局变量中独立创建的:
|
||||
|
||||
```cpp
|
||||
D3D12Texture gColorRTs[2]; // 独立于 SwapChain
|
||||
```
|
||||
|
||||
如果 `gSwapChain.Shutdown()` 先执行,`m_backBuffers` 的 `m_resource` 被释放,那么 `gColorRTs[i].m_resource` 就变成悬垂的 `ComPtr`!
|
||||
|
||||
```cpp
|
||||
// gSwapChain.Shutdown() 执行后:
|
||||
m_backBuffers[0].m_resource.Reset(); // 资源被释放!
|
||||
// 但 gColorRTs[0].m_resource 仍然持有同一个(已释放的)指针!
|
||||
// 调用 gColorRTs[0].GetResource() 会返回已释放的 COM 对象!
|
||||
```
|
||||
|
||||
**这就是 bug!** 当 SwapChain 先 shutdown,用户代码中的 `gColorRTs` 就变成悬垂指针。
|
||||
|
||||
---
|
||||
|
||||
## 5. 最终结论
|
||||
|
||||
### 根因
|
||||
`minimal/main.cpp` 创建了与 `D3D12SwapChain` 内部 `m_backBuffers` **重复包装**的 `gColorRTs` 数组。当任何一个先销毁,另一个就变成悬垂指针。
|
||||
|
||||
### 修复方案
|
||||
1. **最小改动**:直接使用 `gSwapChain.GetBackBuffer(i)` 而非创建新的 `gColorRTs`
|
||||
2. **长期方案**:增强 `D3D12Texture` 的所有权语义,区分拥有和非拥有资源
|
||||
|
||||
### 实施
|
||||
按照第 2.3 节的修改清单执行。
|
||||
@@ -1,731 +0,0 @@
|
||||
# OpenGL 测试架构重构方案
|
||||
|
||||
本文档是 XCEngine 测试规范的 OpenGL 专项补充,旨在将 OpenGL 后端测试体系重构为与 D3D12 同样规范的标准架构。
|
||||
|
||||
**前置阅读**:
|
||||
- [tests/TEST_SPEC.md](../tests/TEST_SPEC.md) - 通用测试规范
|
||||
- [tests/RHI/D3D12/TEST_SPEC.md](./D3D12/TEST_SPEC.md) - D3D12 专项规范(参考模板)
|
||||
|
||||
**规范版本**: 1.0
|
||||
**最后更新**: 2026-03-20
|
||||
|
||||
---
|
||||
|
||||
## 1. 现状分析
|
||||
|
||||
### 1.1 当前目录结构
|
||||
|
||||
```
|
||||
tests/RHI/OpenGL/
|
||||
├── CMakeLists.txt # 混乱的一级配置
|
||||
├── fixtures/
|
||||
│ ├── OpenGLTestFixture.h
|
||||
│ └── OpenGLTestFixture.cpp
|
||||
├── test_device.cpp
|
||||
├── test_buffer.cpp # 被注释,未启用
|
||||
├── test_fence.cpp # 被注释,未启用
|
||||
├── test_texture.cpp
|
||||
├── test_shader.cpp # 被注释,未启用
|
||||
├── test_pipeline_state.cpp
|
||||
├── test_vertex_array.cpp
|
||||
├── test_command_list.cpp
|
||||
├── test_render_target_view.cpp
|
||||
├── test_depth_stencil_view.cpp
|
||||
├── test_swap_chain.cpp
|
||||
├── test_sampler.cpp
|
||||
└── Res/ # 空文件夹,无实际资源
|
||||
├── Data/
|
||||
├── Shader/
|
||||
└── Texture/
|
||||
```
|
||||
|
||||
### 1.2 当前测试统计
|
||||
|
||||
| 组件 | 文件 | 测试数 | 状态 |
|
||||
|------|------|--------|------|
|
||||
| Device | `test_device.cpp` | 6 | ✅ 启用 |
|
||||
| Buffer | `test_buffer.cpp` | 6 | ❌ 被注释 |
|
||||
| Fence | `test_fence.cpp` | 5 | ❌ 被注释 |
|
||||
| Texture | `test_texture.cpp` | 4 | ✅ 启用 |
|
||||
| Shader | `test_shader.cpp` | 4 | ❌ 被注释 |
|
||||
| PipelineState | `test_pipeline_state.cpp` | 3 | ✅ 启用 |
|
||||
| VertexArray | `test_vertex_array.cpp` | 1 | ✅ 启用 |
|
||||
| CommandList | `test_command_list.cpp` | 14 | ✅ 启用 |
|
||||
| Sampler | `test_sampler.cpp` | 2 | ✅ 启用 |
|
||||
| SwapChain | `test_swap_chain.cpp` | 3 | ✅ 启用 |
|
||||
| RTV | `test_render_target_view.cpp` | 2 | ✅ 启用 |
|
||||
| DSV | `test_depth_stencil_view.cpp` | 2 | ✅ 启用 |
|
||||
| **总计** | | **52** | **37 启用, 15 被注释** |
|
||||
|
||||
### 1.3 与 D3D12 对比
|
||||
|
||||
| 方面 | D3D12 (规范) | OpenGL (现状) |
|
||||
|------|-------------|--------------|
|
||||
| **目录分层** | `unit/` + `integration/` 分离 | 全部扁平 |
|
||||
| **CMake 结构** | 顶层 + unit + integration 三级 | 仅一级 |
|
||||
| **Fixture 设计** | 每测试独立设备 | 静态共享上下文 |
|
||||
| **被注释测试** | 无 | 3 个文件被注释 |
|
||||
| **集成测试** | 有 (Python + Golden Image) | **缺失** |
|
||||
| **测试规范文档** | `TEST_SPEC.md` | **缺失** |
|
||||
| **Res 资源** | 按测试隔离 | 空文件夹 |
|
||||
| **资源复制** | POST_BUILD 自动复制 | 未配置 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 问题详解
|
||||
|
||||
### 2.1 Fixture 设计缺陷
|
||||
|
||||
**当前问题**:
|
||||
```cpp
|
||||
// OpenGL - 静态成员所有测试共享,存在状态污染
|
||||
static GLFWwindow* m_window; // 共享窗口
|
||||
static bool m_contextInitialized; // 共享状态
|
||||
static OpenGLDevice* m_device; // 共享设备
|
||||
```
|
||||
|
||||
**D3D12 规范做法**:
|
||||
```cpp
|
||||
// 每个测试独立创建设备
|
||||
void D3D12TestFixture::SetUp() {
|
||||
D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_12_0, ...);
|
||||
}
|
||||
```
|
||||
|
||||
**OpenGL 重构方向**:
|
||||
```cpp
|
||||
// 方案A: 每测试创建独立上下文 (推荐)
|
||||
class OpenGLTestFixture : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
// 创建独立 OpenGL 上下文
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
m_window = glfwCreateWindow(640, 480, "Test", nullptr, nullptr);
|
||||
glfwMakeContextCurrent(m_window);
|
||||
gladLoadGLLoader(...);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 2.2 CMake 配置混乱
|
||||
|
||||
**当前问题**:
|
||||
```cmake
|
||||
# 硬编码绝对路径
|
||||
include_directories(${CMAKE_SOURCE_DIR}/tests/OpenPackage/include/)
|
||||
include_directories(${CMAKE_SOURCE_DIR}/tests/OpenPackage/lib/)
|
||||
link_directories(${CMAKE_SOURCE_DIR}/tests/OpenPackage/lib/)
|
||||
|
||||
# 被注释的测试源文件
|
||||
# test_buffer.cpp
|
||||
# test_fence.cpp
|
||||
# test_shader.cpp
|
||||
```
|
||||
|
||||
**D3D12 规范做法**:
|
||||
```cmake
|
||||
# 顶层 CMakeLists.txt 仅做 add_subdirectory
|
||||
add_subdirectory(unit)
|
||||
add_subdirectory(integration)
|
||||
|
||||
# unit/CMakeLists.txt 独立配置
|
||||
# integration/CMakeLists.txt 独立配置
|
||||
```
|
||||
|
||||
### 2.3 资源目录为空
|
||||
|
||||
当前 `Res/` 下的 `Data/`、`Shader/`、`Texture/` 均为空目录,没有实际测试资源。
|
||||
|
||||
---
|
||||
|
||||
## 3. 重构目标
|
||||
|
||||
### 3.1 目标目录结构
|
||||
|
||||
```
|
||||
tests/RHI/OpenGL/
|
||||
├── CMakeLists.txt # 顶层配置
|
||||
├── TEST_SPEC.md # 本文档 (OpenGL 专项)
|
||||
├── TEST_IMPROVEMENT_PLAN.md # 改进计划
|
||||
├── unit/
|
||||
│ ├── CMakeLists.txt # 单元测试构建
|
||||
│ ├── fixtures/
|
||||
│ │ ├── OpenGLTestFixture.h
|
||||
│ │ └── OpenGLTestFixture.cpp
|
||||
│ ├── test_device.cpp
|
||||
│ ├── test_buffer.cpp
|
||||
│ ├── test_fence.cpp
|
||||
│ ├── test_texture.cpp
|
||||
│ ├── test_shader.cpp
|
||||
│ ├── test_pipeline_state.cpp
|
||||
│ ├── test_vertex_array.cpp
|
||||
│ ├── test_command_list.cpp
|
||||
│ ├── test_render_target_view.cpp
|
||||
│ ├── test_depth_stencil_view.cpp
|
||||
│ ├── test_swap_chain.cpp
|
||||
│ └── test_sampler.cpp
|
||||
└── integration/
|
||||
├── CMakeLists.txt
|
||||
├── run_integration_test.py # 公共脚本
|
||||
├── compare_ppm.py # PPM 图像比对
|
||||
├── run.bat # Windows 启动脚本
|
||||
├── minimal/ # 最小化测试
|
||||
│ ├── main.cpp
|
||||
│ ├── GT_minimal.ppm
|
||||
│ └── Res/
|
||||
│ └── Shader/
|
||||
│ ├── simple.vert
|
||||
│ └── simple.frag
|
||||
└── render_model/ # 模型渲染测试
|
||||
├── main.cpp
|
||||
├── GT.ppm
|
||||
└── Res/
|
||||
├── Image/
|
||||
├── Model/
|
||||
└── Shader/
|
||||
```
|
||||
|
||||
### 3.2 测试数量目标
|
||||
|
||||
| 组件 | 当前 | 重构后 | 变化 |
|
||||
|------|------|--------|------|
|
||||
| Device | 6 | 6 | - |
|
||||
| Buffer | 0 (被注释) | 6 | +6 |
|
||||
| Fence | 0 (被注释) | 5 | +5 |
|
||||
| Texture | 4 | 4 | - |
|
||||
| Shader | 0 (被注释) | 4 | +4 |
|
||||
| PipelineState | 3 | 3 | - |
|
||||
| VertexArray | 1 | 2 | +1 |
|
||||
| CommandList | 14 | 14 | - |
|
||||
| Sampler | 2 | 2 | - |
|
||||
| SwapChain | 3 | 3 | - |
|
||||
| RTV | 2 | 2 | - |
|
||||
| DSV | 2 | 2 | - |
|
||||
| **单元测试总计** | **37** | **53** | **+16** |
|
||||
| **集成测试** | **0** | **2** | **+2** |
|
||||
|
||||
---
|
||||
|
||||
## 4. 分阶段实施计划
|
||||
|
||||
### Phase 1: 目录结构重构
|
||||
|
||||
**目标**: 建立与 D3D12 一致的目录分层
|
||||
|
||||
**步骤**:
|
||||
1. 创建 `unit/` 和 `integration/` 子目录
|
||||
2. 移动现有测试文件到 `unit/`
|
||||
3. 创建顶层 `CMakeLists.txt` 做 `add_subdirectory`
|
||||
4. 重构 `unit/CMakeLists.txt` 独立配置
|
||||
|
||||
**目录变更**:
|
||||
```
|
||||
# 重构前
|
||||
tests/RHI/OpenGL/CMakeLists.txt
|
||||
tests/RHI/OpenGL/test_*.cpp
|
||||
tests/RHI/OpenGL/fixtures/
|
||||
|
||||
# 重构后
|
||||
tests/RHI/OpenGL/CMakeLists.txt # 顶层,仅 add_subdirectory
|
||||
tests/RHI/OpenGL/unit/CMakeLists.txt # 单元测试构建
|
||||
tests/RHI/OpenGL/unit/test_*.cpp # 测试文件
|
||||
tests/RHI/OpenGL/unit/fixtures/ # Fixture
|
||||
tests/RHI/OpenGL/integration/ # 新增
|
||||
tests/RHI/OpenGL/integration/...
|
||||
```
|
||||
|
||||
### Phase 2: Fixture 重构
|
||||
|
||||
**目标**: 消除静态共享状态,避免测试间污染
|
||||
|
||||
**重构内容**:
|
||||
|
||||
```cpp
|
||||
// OpenGLTestFixture.h 重构
|
||||
class OpenGLTestFixture : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override;
|
||||
void TearDown() override;
|
||||
|
||||
GLFWwindow* GetWindow() { return m_window; }
|
||||
void MakeContextCurrent();
|
||||
void DoneContextCurrent();
|
||||
|
||||
void ClearGLErrors();
|
||||
bool CheckGLError(const char* file, int line);
|
||||
void ResetGLState();
|
||||
|
||||
private:
|
||||
GLFWwindow* m_window = nullptr;
|
||||
OpenGLDevice* m_device = nullptr;
|
||||
};
|
||||
|
||||
// OpenGLTestFixture.cpp 重构
|
||||
void OpenGLTestFixture::SetUp() {
|
||||
glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
|
||||
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
|
||||
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
|
||||
|
||||
m_window = glfwCreateWindow(640, 480, "OpenGL Tests", nullptr, nullptr);
|
||||
if (!m_window) {
|
||||
GTEST_SKIP() << "Failed to create GLFW window";
|
||||
return;
|
||||
}
|
||||
|
||||
glfwMakeContextCurrent(m_window);
|
||||
|
||||
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
|
||||
GTEST_SKIP() << "Failed to initialize GLAD";
|
||||
glfwDestroyWindow(m_window);
|
||||
m_window = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
m_device = new OpenGLDevice();
|
||||
m_device->CreateRenderWindow(640, 480, "Test Window", false);
|
||||
|
||||
ClearGLErrors();
|
||||
}
|
||||
|
||||
void OpenGLTestFixture::TearDown() {
|
||||
ResetGLState();
|
||||
|
||||
if (m_device) {
|
||||
delete m_device;
|
||||
m_device = nullptr;
|
||||
}
|
||||
|
||||
if (m_window) {
|
||||
glfwDestroyWindow(m_window);
|
||||
m_window = nullptr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Phase 3: 启用被注释的测试
|
||||
|
||||
**目标**: 激活 `test_buffer.cpp`、`test_fence.cpp`、`test_shader.cpp`
|
||||
|
||||
**步骤**:
|
||||
1. 取消 CMakeLists.txt 中的注释
|
||||
2. 检查并修复可能的编译错误
|
||||
3. 运行测试验证
|
||||
|
||||
**预期新增测试**:
|
||||
- Buffer: 6 tests
|
||||
- Fence: 5 tests
|
||||
- Shader: 4 tests
|
||||
|
||||
### Phase 4: 完善单元测试
|
||||
|
||||
**目标**: 补充缺失的测试用例
|
||||
|
||||
**新增测试计划**:
|
||||
|
||||
| 组件 | 新增测试 | 说明 |
|
||||
|------|---------|------|
|
||||
| VertexArray | 1 | `VertexArray_Bind_MultipleAttributes` |
|
||||
|
||||
### Phase 5: 建立集成测试体系
|
||||
|
||||
**目标**: 建立与 D3D12 一致的集成测试框架
|
||||
|
||||
**步骤**:
|
||||
1. 创建 `integration/` 目录结构
|
||||
2. 复制 `run_integration_test.py` 和 `compare_ppm.py`
|
||||
3. 创建 `minimal/` 集成测试
|
||||
4. 创建 `render_model/` 集成测试
|
||||
5. 配置 CTest 注册
|
||||
|
||||
**minimal/ 集成测试**:
|
||||
```cpp
|
||||
// integration/minimal/main.cpp
|
||||
// 渲染一个简单三角形,输出 PPM 截图
|
||||
int main() {
|
||||
// 1. 初始化 GLFW + OpenGL
|
||||
// 2. 创建窗口
|
||||
// 3. 渲染简单场景
|
||||
// 4. 截图保存为 minimal.ppm
|
||||
// 5. 退出
|
||||
}
|
||||
```
|
||||
|
||||
**Golden Image 生成流程**:
|
||||
1. 在干净硬件环境运行集成测试
|
||||
2. 截图保存为 `GT_<name>.ppm`
|
||||
3. 人工验证截图正确性
|
||||
4. 提交到版本控制
|
||||
|
||||
### Phase 6: 编写测试规范文档
|
||||
|
||||
**目标**: 创建 `TEST_SPEC.md` 和 `TEST_IMPROVEMENT_PLAN.md`
|
||||
|
||||
---
|
||||
|
||||
## 5. CMake 重构详细方案
|
||||
|
||||
### 5.1 顶层 CMakeLists.txt
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
|
||||
project(OpenGLEngineTests)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
add_subdirectory(unit)
|
||||
add_subdirectory(integration)
|
||||
```
|
||||
|
||||
### 5.2 unit/CMakeLists.txt
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
|
||||
get_filename_component(PROJECT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../.. ABSOLUTE)
|
||||
|
||||
find_package(OpenGL REQUIRED)
|
||||
|
||||
include_directories(${PROJECT_ROOT_DIR}/engine/include)
|
||||
include_directories(${PROJECT_ROOT_DIR}/engine/src)
|
||||
|
||||
link_directories(${CMAKE_SOURCE_DIR}/tests/OpenGL/package/lib/)
|
||||
|
||||
find_package(GTest REQUIRED)
|
||||
|
||||
set(TEST_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/tests/OpenGL/package/src/glad.c
|
||||
fixtures/OpenGLTestFixture.cpp
|
||||
test_device.cpp
|
||||
test_buffer.cpp
|
||||
test_fence.cpp
|
||||
test_texture.cpp
|
||||
test_shader.cpp
|
||||
test_pipeline_state.cpp
|
||||
test_vertex_array.cpp
|
||||
test_command_list.cpp
|
||||
test_render_target_view.cpp
|
||||
test_depth_stencil_view.cpp
|
||||
test_swap_chain.cpp
|
||||
test_sampler.cpp
|
||||
)
|
||||
|
||||
add_executable(opengl_engine_tests ${TEST_SOURCES})
|
||||
|
||||
target_link_libraries(opengl_engine_tests PRIVATE
|
||||
opengl32
|
||||
glfw3
|
||||
XCEngine
|
||||
GTest::gtest
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
target_include_directories(opengl_engine_tests PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/fixtures
|
||||
${PROJECT_ROOT_DIR}/engine/include
|
||||
${PROJECT_ROOT_DIR}/engine/src
|
||||
)
|
||||
|
||||
target_compile_definitions(opengl_engine_tests PRIVATE
|
||||
TEST_RESOURCES_DIR="${PROJECT_ROOT_DIR}/tests/RHI/OpenGL/integration/minimal/Res"
|
||||
)
|
||||
|
||||
add_custom_command(TARGET opengl_engine_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${PROJECT_ROOT_DIR}/tests/RHI/OpenGL/integration/minimal/Res
|
||||
$<TARGET_FILE_DIR:opengl_engine_tests>/Res
|
||||
)
|
||||
|
||||
enable_testing()
|
||||
add_test(NAME OpenGLEngineTests COMMAND opengl_engine_tests)
|
||||
```
|
||||
|
||||
### 5.3 integration/CMakeLists.txt
|
||||
|
||||
```cmake
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
|
||||
project(OpenGL_Integration)
|
||||
|
||||
set(ENGINE_ROOT_DIR ${CMAKE_SOURCE_DIR}/engine)
|
||||
|
||||
find_package(Python3 REQUIRED)
|
||||
|
||||
enable_testing()
|
||||
|
||||
# Minimal test
|
||||
add_executable(OpenGL_Minimal
|
||||
WIN32
|
||||
minimal/main.cpp
|
||||
)
|
||||
|
||||
target_include_directories(OpenGL_Minimal PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/minimal
|
||||
${ENGINE_ROOT_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(OpenGL_Minimal PRIVATE
|
||||
opengl32
|
||||
glfw3
|
||||
d3d12
|
||||
dxgi
|
||||
d3dcompiler
|
||||
XCEngine
|
||||
)
|
||||
|
||||
# Copy Res folder
|
||||
add_custom_command(TARGET OpenGL_Minimal POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/minimal/Res
|
||||
$<TARGET_FILE_DIR:OpenGL_Minimal>/Res
|
||||
)
|
||||
|
||||
# Copy test scripts
|
||||
add_custom_command(TARGET OpenGL_Minimal POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/run.bat
|
||||
$<TARGET_FILE_DIR:OpenGL_Minimal>/
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/compare_ppm.py
|
||||
$<TARGET_FILE_DIR:OpenGL_Minimal>/
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/run_integration_test.py
|
||||
$<TARGET_FILE_DIR:OpenGL_Minimal>/
|
||||
)
|
||||
|
||||
# Register integration test with CTest
|
||||
add_test(NAME OpenGL_Minimal_Integration
|
||||
COMMAND ${Python3_EXECUTABLE} $<TARGET_FILE_DIR:OpenGL_Minimal>/run_integration_test.py
|
||||
$<TARGET_FILE:OpenGL_Minimal>
|
||||
minimal.ppm
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/minimal/GT_minimal.ppm
|
||||
5
|
||||
WORKING_DIRECTORY $<TARGET_FILE_DIR:OpenGL_Minimal>
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 测试前缀对应表
|
||||
|
||||
| 类名 | 测试前缀 |
|
||||
|------|---------|
|
||||
| OpenGLDevice | Device |
|
||||
| OpenGLBuffer | Buffer |
|
||||
| OpenGLFence | Fence |
|
||||
| OpenGLTexture | Texture |
|
||||
| OpenGLShader | Shader |
|
||||
| OpenGLPipelineState | PipelineState |
|
||||
| OpenGLVertexArray | VertexArray |
|
||||
| OpenGLCommandList | CommandList |
|
||||
| OpenGLSampler | Sampler |
|
||||
| OpenGLSwapChain | SwapChain |
|
||||
| OpenGLRenderTargetView | RTV |
|
||||
| OpenGLDepthStencilView | DSV |
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试执行
|
||||
|
||||
### 7.1 单元测试
|
||||
|
||||
```bash
|
||||
# 方式 1: 使用统一脚本
|
||||
python scripts/run_tests.py --unit-only
|
||||
|
||||
# 方式 2: 直接使用 CTest
|
||||
cd build/tests/RHI/OpenGL/unit
|
||||
ctest -C Debug --output-on-failure
|
||||
```
|
||||
|
||||
### 7.2 集成测试
|
||||
|
||||
```bash
|
||||
# 方式 1: 使用统一脚本
|
||||
python scripts/run_tests.py --integration
|
||||
|
||||
# 方式 2: 直接使用 CTest
|
||||
cd build/tests/RHI/OpenGL/integration
|
||||
ctest -C Debug --output-on-failure
|
||||
```
|
||||
|
||||
### 7.3 构建和测试
|
||||
|
||||
```bash
|
||||
# 构建
|
||||
cmake --build . --target OpenGL_Minimal --config Debug
|
||||
|
||||
# 运行测试
|
||||
python scripts/run_tests.py --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. CI 集成
|
||||
|
||||
### 8.1 GitHub Actions 配置
|
||||
|
||||
```yaml
|
||||
name: OpenGL Tests
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure CMake
|
||||
run: cmake -B build -DCMAKE_BUILD_TYPE=Debug
|
||||
|
||||
- name: Build OpenGL Tests
|
||||
run: cmake --build build --target opengl_engine_tests OpenGL_Minimal --config Debug
|
||||
|
||||
- name: Run Unit Tests
|
||||
run: cd build/tests/RHI/OpenGL/unit && ctest -C Debug --output-on-failure
|
||||
|
||||
- name: Run Integration Tests
|
||||
run: python scripts/run_tests.py --integration
|
||||
```
|
||||
|
||||
### 8.2 CI 模式
|
||||
|
||||
`--ci` 模式会跳过需要 GUI 的集成测试:
|
||||
```bash
|
||||
python scripts/run_tests.py --ci # 仅运行单元测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 文件清单
|
||||
|
||||
### 9.1 需创建的文件
|
||||
|
||||
| 文件路径 | 说明 |
|
||||
|---------|------|
|
||||
| `tests/RHI/OpenGL/CMakeLists.txt` | 顶层 CMake 配置 |
|
||||
| `tests/RHI/OpenGL/TEST_SPEC.md` | OpenGL 专项规范 |
|
||||
| `tests/RHI/OpenGL/TEST_IMPROVEMENT_PLAN.md` | 改进计划 |
|
||||
| `tests/RHI/OpenGL/unit/CMakeLists.txt` | 单元测试构建配置 |
|
||||
| `tests/RHI/OpenGL/integration/CMakeLists.txt` | 集成测试构建配置 |
|
||||
| `tests/RHI/OpenGL/integration/run_integration_test.py` | 集成测试运行脚本 |
|
||||
| `tests/RHI/OpenGL/integration/compare_ppm.py` | PPM 图像比对脚本 |
|
||||
| `tests/RHI/OpenGL/integration/run.bat` | Windows 启动脚本 |
|
||||
| `tests/RHI/OpenGL/integration/minimal/main.cpp` | 最小化集成测试 |
|
||||
| `tests/RHI/OpenGL/integration/minimal/GT_minimal.ppm` | Golden Image |
|
||||
| `tests/RHI/OpenGL/integration/minimal/Res/Shader/*.vert` | Vertex Shader |
|
||||
| `tests/RHI/OpenGL/integration/minimal/Res/Shader/*.frag` | Fragment Shader |
|
||||
|
||||
### 9.2 需修改的文件
|
||||
|
||||
| 文件路径 | 修改内容 |
|
||||
|---------|---------|
|
||||
| `tests/RHI/OpenGL/fixtures/OpenGLTestFixture.h` | 重构 Fixture 接口 |
|
||||
| `tests/RHI/OpenGL/fixtures/OpenGLTestFixture.cpp` | 重构 Fixture 实现 |
|
||||
| `tests/RHI/OpenGL/CMakeLists.txt` | 简化为 add_subdirectory |
|
||||
|
||||
### 9.3 需移动的文件
|
||||
|
||||
| 原路径 | 新路径 |
|
||||
|-------|-------|
|
||||
| `tests/RHI/OpenGL/test_*.cpp` | `tests/RHI/OpenGL/unit/test_*.cpp` |
|
||||
| `tests/RHI/OpenGL/fixtures/*` | `tests/RHI/OpenGL/unit/fixtures/*` |
|
||||
|
||||
---
|
||||
|
||||
## 10. OpenGL 与 D3D12 测试差异说明
|
||||
|
||||
### 10.1 平台特性差异
|
||||
|
||||
| 方面 | D3D12 | OpenGL |
|
||||
|------|-------|--------|
|
||||
| **设备创建** | `D3D12CreateDevice()` 每测试独立 | 共享 GLFWcontext + Glad |
|
||||
| **上下文管理** | 无 | GLFWwindow 生命周期 |
|
||||
| **错误检查** | `HRESULT` 返回值 | `glGetError()` 状态码 |
|
||||
| **渲染目标** | RTV/DSV descriptor | OpenGL Framebuffer Object |
|
||||
| **同步原语** | `ID3D12Fence` | `glFenceSync` + `glClientWaitSync` |
|
||||
| **管线状态** | PSO 对象 | OpenGL State Machine |
|
||||
| **资源绑定** | CommandList + DescriptorHeap | `glBindBuffer`, `glBindTexture` |
|
||||
|
||||
### 10.2 Fixture 设计差异
|
||||
|
||||
```cpp
|
||||
// D3D12 - 每测试独立 COM 对象
|
||||
class D3D12TestFixture : public ::testing::Test {
|
||||
ComPtr<ID3D12Device> mDevice;
|
||||
ComPtr<ID3D12CommandQueue> mCommandQueue;
|
||||
};
|
||||
|
||||
// OpenGL - 需要共享 context,但每测试独立 window
|
||||
class OpenGLTestFixture : public ::testing::Test {
|
||||
GLFWwindow* m_window; // 每测试独立
|
||||
OpenGLDevice* m_device; // 每测试独立
|
||||
};
|
||||
```
|
||||
|
||||
### 10.3 测试资源差异
|
||||
|
||||
| 方面 | D3D12 | OpenGL |
|
||||
|------|-------|--------|
|
||||
| **Shader 格式** | HLSL (.hlsl) | GLSL (.vert, .frag, .geom) |
|
||||
| **纹理格式** | DDS | PNG/BMP/TGA |
|
||||
| **模型格式** | 自定义 .lhsm | 自定义 .lhsm |
|
||||
|
||||
---
|
||||
|
||||
## 11. 已知问题与待办
|
||||
|
||||
### 11.1 Phase 1 待办
|
||||
|
||||
- [ ] 创建 `unit/` 和 `integration/` 目录
|
||||
- [ ] 移动测试文件到 `unit/`
|
||||
- [ ] 创建顶层 `CMakeLists.txt`
|
||||
- [ ] 重构 `unit/CMakeLists.txt`
|
||||
|
||||
### 11.2 Phase 2 待办
|
||||
|
||||
- [ ] 重构 `OpenGLTestFixture` 消除静态成员
|
||||
- [ ] 验证测试隔离效果
|
||||
|
||||
### 11.3 Phase 3 待办
|
||||
|
||||
- [ ] 启用 `test_buffer.cpp`
|
||||
- [ ] 启用 `test_fence.cpp`
|
||||
- [ ] 启用 `test_shader.cpp`
|
||||
- [ ] 修复编译错误
|
||||
|
||||
### 11.4 Phase 4 待办
|
||||
|
||||
- [ ] 补充 `VertexArray_Bind_MultipleAttributes` 测试
|
||||
|
||||
### 11.5 Phase 5 待办
|
||||
|
||||
- [ ] 创建 `integration/` 目录结构
|
||||
- [ ] 复制并适配 `run_integration_test.py`
|
||||
- [ ] 复制并适配 `compare_ppm.py`
|
||||
- [ ] 创建 `minimal/` 集成测试
|
||||
- [ ] 创建 `render_model/` 集成测试
|
||||
- [ ] 生成 Golden Image
|
||||
|
||||
### 11.6 Phase 6 待办
|
||||
|
||||
- [ ] 编写 `TEST_SPEC.md`
|
||||
- [ ] 编写 `TEST_IMPROVEMENT_PLAN.md`
|
||||
|
||||
---
|
||||
|
||||
## 12. 规范更新记录
|
||||
|
||||
| 版本 | 日期 | 变更 |
|
||||
|------|------|------|
|
||||
| 1.0 | 2026-03-20 | 初始版本,参考 D3D12 TEST_SPEC.md 制定重构方案 |
|
||||
|
||||
---
|
||||
|
||||
**规范版本**: 1.0
|
||||
**最后更新**: 2026-03-20
|
||||
**前置文档**:
|
||||
- [tests/TEST_SPEC.md](../tests/TEST_SPEC.md)
|
||||
- [tests/RHI/D3D12/TEST_SPEC.md](./D3D12/TEST_SPEC.md)
|
||||
@@ -1,609 +0,0 @@
|
||||
# RHI 模块设计问题分析报告(第二版)
|
||||
|
||||
## 1. 项目背景
|
||||
|
||||
本项目 RHI 模块参考 Unity 渲染架构设计,面向 **Direct3D 12** 和 **Vulkan** 等现代图形 API,目标是为引擎上层(SRP/RenderGraph)提供统一的渲染硬件抽象层,屏蔽 API 差异,实现跨后端移植。
|
||||
|
||||
当前已实现两个后端:D3D12 和 OpenGL。基础框架已有雏形(Reset/Close、TransitionBarrier、PipelineState),但存在**被 OpenGL 风格带偏、不符合现代 D3D12/Vulkan 显式设计**的关键问题。
|
||||
|
||||
---
|
||||
|
||||
## 2. 做得好的地方
|
||||
|
||||
1. **有显式生命周期管理**:`Reset()` / `Close()` 是 D3D12/Vulkan 风格,理解"录制-提交"的分离
|
||||
2. **考虑了资源状态转换**:`TransitionBarrier()` 是现代 API 必须的,预留了接口
|
||||
3. **覆盖了图形+计算**:`Draw` / `DrawIndexed` / `Dispatch` 都有,功能完整
|
||||
4. **预留了原生句柄**:`GetNativeHandle()` 方便调试和特殊场景扩展
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心问题(按严重程度排序)
|
||||
|
||||
### 3.1 最大的坑:字符串查找 SetUniform 是 OpenGL 风格
|
||||
|
||||
**问题描述**:当前接口使用字符串名称设置 Uniform/Texture:
|
||||
```cpp
|
||||
virtual void SetUniformInt(const char* name, int value) = 0;
|
||||
virtual void SetGlobalTexture(const char* name, RHIResourceView* texture) = 0;
|
||||
```
|
||||
|
||||
这是**典型的 OpenGL 立即模式风格**,问题极大:
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| **性能差** | 运行时通过字符串查找 uniform 位置,驱动开销大 |
|
||||
| **无法多线程** | 字符串查找不是线程安全的,不利于多线程录制 CommandList |
|
||||
| **接 Vulkan 不可能** | D3D12/Vulkan 用 **DescriptorSet + PipelineLayout**,根本没有"按名字设 uniform"的概念 |
|
||||
|
||||
**根本原因**:这是 OpenGL 风格的设计,没有对齐 D3D12/Vulkan 的显式 Descriptor 绑定模型。
|
||||
|
||||
**D3D12/Vulkan 正确做法**:
|
||||
```cpp
|
||||
// D3D12: 通过 Root Signature + Descriptor Table 绑定
|
||||
commandList->SetGraphicsRootDescriptorTable(rootIndex, gpuDescriptorHandle);
|
||||
|
||||
// Vulkan: 通过 DescriptorSet 绑定
|
||||
vkCmdBindDescriptorSets(commandBuffer, ..., descriptorSet, ...);
|
||||
```
|
||||
|
||||
**修改建议**:
|
||||
```cpp
|
||||
// 新增:DescriptorSet 绑定接口(替代 SetUniform/SetGlobal)
|
||||
virtual void SetGraphicsDescriptorSets(
|
||||
uint32_t firstSet,
|
||||
uint32_t count,
|
||||
RHIDescriptorSet** descriptorSets,
|
||||
RHIPipelineLayout* pipelineLayout) = 0;
|
||||
|
||||
virtual void SetComputeDescriptorSets(
|
||||
uint32_t firstSet,
|
||||
uint32_t count,
|
||||
RHIDescriptorSet** descriptorSets,
|
||||
RHIPipelineLayout* pipelineLayout) = 0;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.1.1 ✅ 已完成:DescriptorSet 抽象
|
||||
|
||||
**实现状态**:2026-03-25 已完成
|
||||
|
||||
**新增抽象**:
|
||||
| 类名 | 说明 |
|
||||
|------|------|
|
||||
| `RHIDescriptorSet` | DescriptorSet 基类,含 Update/UpdateSampler/GetNativeHandle/GetBindingCount/GetBindings |
|
||||
| `RHIDescriptorPool` | DescriptorPool 基类,含 AllocateSet/FreeSet |
|
||||
| `D3D12DescriptorSet` | D3D12 实现,使用 DescriptorHeap 分配 |
|
||||
| `D3D12DescriptorHeap` | 扩展支持 AllocateSet/FreeSet |
|
||||
| `OpenGLDescriptorSet` | OpenGL 实现,使用 TextureUnitAllocator |
|
||||
| `OpenGLDescriptorPool` | OpenGL 实现 |
|
||||
|
||||
**RHICommandList 新增接口**:
|
||||
```cpp
|
||||
virtual void SetGraphicsDescriptorSets(
|
||||
uint32_t firstSet,
|
||||
uint32_t count,
|
||||
RHIDescriptorSet** descriptorSets,
|
||||
RHIPipelineLayout* pipelineLayout) = 0;
|
||||
|
||||
virtual void SetComputeDescriptorSets(
|
||||
uint32_t firstSet,
|
||||
uint32_t count,
|
||||
RHIDescriptorSet** descriptorSets,
|
||||
RHIPipelineLayout* pipelineLayout) = 0;
|
||||
```
|
||||
|
||||
**RHIDevice 新增工厂方法**:
|
||||
```cpp
|
||||
virtual RHIDescriptorPool* CreateDescriptorPool(const DescriptorPoolDesc& desc) = 0;
|
||||
virtual RHIDescriptorSet* CreateDescriptorSet(RHIDescriptorPool* pool, const DescriptorSetDesc& desc) = 0;
|
||||
```
|
||||
|
||||
**实现说明**:
|
||||
- D3D12:使用 `D3D12DescriptorHeap` 的 GPU/CPU descriptor 分配
|
||||
- OpenGL:使用 `TextureUnitAllocator` 分配 texture unit
|
||||
- 旧的 `SetUniform*/SetGlobal*` 仍保留,向后兼容
|
||||
|
||||
---
|
||||
|
||||
### 3.2 缺少显式 RenderPass
|
||||
|
||||
**问题描述**:当前只有 `SetRenderTargets()`,没有 `BeginRenderPass()` / `EndRenderPass()`:
|
||||
|
||||
```cpp
|
||||
// 当前设计
|
||||
virtual void SetRenderTargets(uint32_t count, RHIResourceView** renderTargets, ...) = 0;
|
||||
virtual void ClearRenderTarget(RHIResourceView* renderTarget, const float color[4]) = 0;
|
||||
```
|
||||
|
||||
这是**不符合现代 API 设计**的:
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| **不符合显式设计** | D3D12/Vulkan 要求显式定义渲染通道的开始和结束 |
|
||||
| **清屏逻辑不规范** | 现代 API 推荐用 RenderPass 的 `LoadOp`(Clear/Load/DontCare)清屏 |
|
||||
| **Tile GPU 性能差** | 对移动端 Tile GPU 至关重要,隐式行为无法优化 |
|
||||
|
||||
**根本原因**:OpenGL 的 `glClear()` 是立即模式,没有 RenderPass 概念。
|
||||
|
||||
**D3D12/Vulkan 正确做法**:
|
||||
```cpp
|
||||
// D3D12: Begin/End Render Pass
|
||||
commandList->BeginRenderPass(...);
|
||||
commandList->EndRenderPass();
|
||||
|
||||
// Vulkan: vkCmdBeginRenderPass / vkCmdEndRenderPass
|
||||
vkCmdBeginRenderPass(commandBuffer, &renderPassBeginInfo, ...);
|
||||
vkCmdEndRenderPass(commandBuffer);
|
||||
```
|
||||
|
||||
**修改建议**:
|
||||
```cpp
|
||||
// 新增:显式渲染通道
|
||||
virtual void BeginRenderPass(
|
||||
RHIRenderPass* renderPass,
|
||||
RHIFramebuffer* framebuffer,
|
||||
const Rect& renderArea,
|
||||
uint32_t clearValueCount,
|
||||
const ClearValue* clearValues) = 0;
|
||||
|
||||
virtual void EndRenderPass() = 0;
|
||||
|
||||
// 移除 SetRenderTargets,改由 Framebuffer 定义
|
||||
// 移除或废弃 ClearRenderTarget,改为 BeginRenderPass 的 LoadOp
|
||||
```
|
||||
|
||||
### 3.2.1 ✅ 已完成:显式 RenderPass 实现
|
||||
|
||||
**实现状态**:2026-03-24 已完成
|
||||
|
||||
**新增抽象**:
|
||||
| 类名 | 说明 |
|
||||
|------|------|
|
||||
| `RHIFramebuffer` | Framebuffer 基类,含 Initialize/Bind/GetHandle |
|
||||
| `RHIRenderPass` | RenderPass 基类,含 AttachmentDesc |
|
||||
| `D3D12Framebuffer` | D3D12 Framebuffer 实现 |
|
||||
| `D3D12RenderPass` | D3D12 RenderPass 实现(使用 OMSetRenderTargets) |
|
||||
| `OpenGLFramebuffer` | OpenGL 实现(继承自 RHIFramebuffer) |
|
||||
| `OpenGLRenderPass` | OpenGL RenderPass 实现 |
|
||||
|
||||
**RHICommandList 新增接口**:
|
||||
```cpp
|
||||
virtual void BeginRenderPass(RHIRenderPass* renderPass, RHIFramebuffer* framebuffer,
|
||||
const Rect& renderArea, uint32_t clearValueCount,
|
||||
const ClearValue* clearValues) = 0;
|
||||
virtual void EndRenderPass() = 0;
|
||||
```
|
||||
|
||||
**AttachmentDesc 结构**(定义在 RHIRenderPass.h):
|
||||
```cpp
|
||||
struct AttachmentDesc {
|
||||
Format format = Format::Unknown;
|
||||
LoadAction loadOp = LoadAction::Undefined; // Undefined/Load/Clear
|
||||
StoreAction storeOp = StoreAction::Store; // Store/Resolve/Discard
|
||||
LoadAction stencilLoadOp = LoadAction::Undefined;
|
||||
StoreAction stencilStoreOp = StoreAction::Undefined;
|
||||
ClearValue clearValue;
|
||||
};
|
||||
```
|
||||
|
||||
**实现说明**:
|
||||
- D3D12:使用 `OMSetRenderTargets` + `ClearRenderTargetView`/`ClearDepthStencilView`
|
||||
- OpenGL:使用 `glBindFramebuffer` + `glClearBufferfv` 处理 LoadOp
|
||||
- 旧的 `SetRenderTargets`/`ClearRenderTarget` 仍保留,向后兼容
|
||||
|
||||
---
|
||||
|
||||
### 3.3 动态状态太多,应该收敛到 PipelineState
|
||||
|
||||
**问题描述**:当前把 DepthStencilState、BlendState、PrimitiveTopology 都放在 CommandList 里动态设置:
|
||||
|
||||
```cpp
|
||||
virtual void SetDepthStencilState(const DepthStencilState& state) = 0;
|
||||
virtual void SetBlendState(const BlendState& state) = 0;
|
||||
virtual void SetPrimitiveTopology(PrimitiveTopology topology) = 0;
|
||||
```
|
||||
|
||||
这是**OpenGL 风格的残留**:
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| **驱动开销大** | 现代 API 中这些状态大部分是 PipelineState 的一部分,是不可变的 |
|
||||
| **不符合显式设计** | 动态状态应该只保留极少数(Viewport/Scissor/StencilRef/BlendFactor) |
|
||||
| **状态不一致风险** | CommandList 动态设置的状态可能与 PSO 中定义的状态冲突 |
|
||||
|
||||
**根本原因**:OpenGL 是状态机,可以在任何时候设置任何状态。D3D12/Vulkan 把大部分状态打包到 PSO 中。
|
||||
|
||||
**D3D12/Vulkan 正确做法**:
|
||||
- `DepthStencilState`、`BlendState`、`PrimitiveTopology` 在 **PSO 创建时就确定**
|
||||
- CommandList 只设置**真正的动态状态**:Viewport、Scissor、StencilRef、BlendFactor
|
||||
|
||||
**修改建议**:
|
||||
```cpp
|
||||
// PipelineState 应该包含:
|
||||
// - Shader
|
||||
// - VertexLayout
|
||||
// - RenderPass 兼容
|
||||
// - DepthStencilState(不可变)
|
||||
// - BlendState(不可变)
|
||||
// - PrimitiveTopology(不可变)
|
||||
// - 动态状态掩码
|
||||
|
||||
// CommandList 只保留必要的动态状态:
|
||||
virtual void SetViewports(uint32_t count, const Viewport* viewports) = 0;
|
||||
virtual void SetScissorRects(uint32_t count, const Rect* rects) = 0;
|
||||
virtual void SetStencilRef(uint8_t ref) = 0;
|
||||
virtual void SetBlendFactor(const float factor[4]) = 0;
|
||||
|
||||
// 移除:
|
||||
virtual void SetDepthStencilState(const DepthStencilState& state) = 0; // 移到 PSO
|
||||
virtual void SetBlendState(const BlendState& state) = 0; // 移到 PSO
|
||||
virtual void SetPrimitiveTopology(PrimitiveTopology topology) = 0; // 移到 PSO
|
||||
```
|
||||
|
||||
### 3.3.1 ✅ 已完成:动态状态移至 PSO
|
||||
|
||||
**实现状态**:2026-03-25 已完成
|
||||
|
||||
**移除的无效方法**:
|
||||
| 方法 | 问题 |
|
||||
|------|------|
|
||||
| `SetDepthStencilState` | D3D12 中是空实现(TODO),调用无效;OpenGL 中直接调用 GL 函数但状态应通过 PSO 配置 |
|
||||
| `SetBlendState` | D3D12 中只设置 BlendFactor,其他参数被忽略;OpenGL 中直接调用 GL 函数但状态应通过 PSO 配置 |
|
||||
|
||||
**保留的真正动态状态**:
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `SetViewport/SetViewports` | 真正的动态状态 |
|
||||
| `SetScissorRect/SetScissorRects` | 真正的动态状态 |
|
||||
| `SetStencilRef` | D3D12 动态状态 |
|
||||
| `SetBlendFactor` | D3D12 动态状态 |
|
||||
|
||||
**修改的文件**:
|
||||
- `RHICommandList.h` - 移除 SetDepthStencilState/SetBlendState 纯虚方法
|
||||
- `D3D12CommandList.h/cpp` - 移除 SetDepthStencilState/SetBlendState 实现
|
||||
- `OpenGLCommandList.h/cpp` - 移除 SetDepthStencilState/SetBlendState 实现
|
||||
- `tests/RHI/unit/test_command_list.cpp` - 移除相关测试
|
||||
- `tests/RHI/OpenGL/unit/test_command_list.cpp` - 移除相关测试
|
||||
|
||||
**说明**:
|
||||
- D3D12 中 PSO 是不可变的,SetDepthStencilState/SetBlendState 调用原本就是无效代码
|
||||
- OpenGL 中状态通过 OpenGLPipelineState::Apply() 在绑定 PSO 时应用
|
||||
- PrimitiveTopology 保留在 CommandList,因为 D3D12 允许动态改变 topology type
|
||||
|
||||
---
|
||||
|
||||
### 3.4 ResourceView 类型不明确 ✅ 基本完成
|
||||
|
||||
**问题描述**:当前用 `RHIResourceView*` 代表所有视图:
|
||||
|
||||
```cpp
|
||||
virtual void SetVertexBuffer(uint32_t slot, RHIResourceView* buffer, ...) = 0;
|
||||
virtual void SetRenderTargets(uint32_t count, RHIResourceView** renderTargets, ...) = 0;
|
||||
```
|
||||
|
||||
这会导致:
|
||||
|
||||
| 问题 | 说明 |
|
||||
|------|------|
|
||||
| **类型不安全** | 容易把 SRV 当成 RTV 传,运行时才发现错误 |
|
||||
| **后端处理麻烦** | D3D12/Vulkan 对不同视图有严格区分(RTV/DSV/SRV/UAV/VBV/IBV) |
|
||||
| **语义模糊** | `RHIResourceView` 到底是哪种视图?调用者必须查文档 |
|
||||
|
||||
**根本原因**:没有在类型层面区分不同视图的语义。
|
||||
|
||||
**修改建议**:定义明确的视图类型层次结构
|
||||
```cpp
|
||||
// 基类
|
||||
class RHIResourceView {};
|
||||
|
||||
// 具体视图类型
|
||||
class RHIRenderTargetView : public RHIResourceView {};
|
||||
class RHIDepthStencilView : public RHIResourceView {};
|
||||
class RHIShaderResourceView : public RHIResourceView {};
|
||||
class RHIUnorderedAccessView : public RHIResourceView {};
|
||||
class RHIVertexBufferView : public RHIResourceView {};
|
||||
class RHIIndexBufferView : public RHIResourceView {};
|
||||
class RHIConstantBufferView : public RHIResourceView {};
|
||||
```
|
||||
|
||||
**实现状态**:✅ 基本完成 - `RHIResourceView.h` 中定义了具体视图类型,但 `RHICommandList` 仍使用 `RHIResourceView*` 以保持兼容性
|
||||
|
||||
---
|
||||
|
||||
### 3.5 TransitionBarrier 针对 View 而非 Resource ✅ 已完成
|
||||
|
||||
**问题描述**:当前接口:
|
||||
```cpp
|
||||
virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0;
|
||||
```
|
||||
|
||||
资源状态转换是针对 **Resource(Buffer/Texture 本身)** 的,不是针对 View 的。一张 Texture 可能创建多个 View(SRV、RTV、DSV),但状态转换是对整个 Texture 做的。
|
||||
|
||||
**根本原因**:没有区分 Resource 和 ResourceView 的概念。
|
||||
|
||||
**修改建议**:
|
||||
```cpp
|
||||
// 新增 Resource 基类
|
||||
class RHIResource {};
|
||||
class RHIBuffer : public RHIResource {};
|
||||
class RHITexture : public RHIResource {};
|
||||
|
||||
// 修改 TransitionBarrier
|
||||
struct ResourceBarrier {
|
||||
RHIResource* resource;
|
||||
ResourceStates stateBefore;
|
||||
ResourceStates stateAfter;
|
||||
uint32_t subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
|
||||
};
|
||||
|
||||
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
|
||||
```
|
||||
|
||||
**实现状态**:✅ 已完成 - `RHIResource.h` 已创建,`RHIBuffer` 和 `RHITexture` 已继承自 `RHIResource`
|
||||
|
||||
**问题描述**:当前接口:
|
||||
```cpp
|
||||
virtual void TransitionBarrier(RHIResourceView* resource, ResourceStates stateBefore, ResourceStates stateAfter) = 0;
|
||||
```
|
||||
|
||||
资源状态转换是针对 **Resource(Buffer/Texture 本身)** 的,不是针对 View 的。一张 Texture 可能创建多个 View(SRV、RTV、DSV),但状态转换是对整个 Texture 做的。
|
||||
|
||||
**根本原因**:没有区分 Resource 和 ResourceView 的概念。
|
||||
|
||||
**修改建议**:
|
||||
```cpp
|
||||
// 新增 Resource 基类
|
||||
class RHIResource {};
|
||||
class RHIBuffer : public RHIResource {};
|
||||
class RHITexture : public RHIResource {};
|
||||
|
||||
// 修改 TransitionBarrier
|
||||
struct ResourceBarrier {
|
||||
RHIResource* resource;
|
||||
ResourceStates stateBefore;
|
||||
ResourceStates stateAfter;
|
||||
uint32_t subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
|
||||
};
|
||||
|
||||
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 其他需要修改的问题
|
||||
|
||||
### 4.1 SetGlobal* 是空操作 ✅ 已完成
|
||||
|
||||
**问题**:SetGlobal* 方法(SetGlobalInt/SetGlobalFloat/SetGlobalVec3/SetGlobalVec4/SetGlobalMat4/SetGlobalTexture)在 D3D12 和 OpenGL 中只是缓存值,从不提交到 GPU。
|
||||
|
||||
**实现状态**:2026-03-25 已完成 - 移除所有 SetGlobal* 方法
|
||||
|
||||
**移除的方法**(共6个):
|
||||
| 方法 | 问题 |
|
||||
|------|------|
|
||||
| `SetGlobalInt` | 只缓存到 unordered_map,从未提交到 GPU |
|
||||
| `SetGlobalFloat` | 只缓存到 unordered_map,从未提交到 GPU |
|
||||
| `SetGlobalVec3` | 只缓存到 unordered_map,从未提交到 GPU |
|
||||
| `SetGlobalVec4` | 只缓存到 unordered_map,从未提交到 GPU |
|
||||
| `SetGlobalMat4` | 只缓存到 unordered_map,从未提交到 GPU |
|
||||
| `SetGlobalTexture` | 只缓存到 unordered_map,从未提交到 GPU |
|
||||
|
||||
**移除的缓存成员变量**:
|
||||
- D3D12CommandList: `m_globalIntCache`, `m_globalFloatCache`, `m_globalVec3Cache`, `m_globalVec4Cache`, `m_globalMat4Cache`, `m_globalTextureCache`
|
||||
- OpenGLCommandList: 同上
|
||||
|
||||
**说明**:
|
||||
- `SetGlobal*` 从未被代码库中任何地方调用(死代码)
|
||||
- `SetUniform*` 方法已正常工作,使用 shader reflection + 实际 GPU 绑定
|
||||
- 移除后无功能损失
|
||||
|
||||
### 4.2 PrimitiveType 和 PrimitiveTopology 冲突
|
||||
|
||||
详见第一版文档,此处不再赘述。
|
||||
|
||||
### 4.3 OpenGL 特有方法暴露 ✅ 已完成
|
||||
|
||||
**实现状态**:2026-03-25 已完成
|
||||
|
||||
**问题**:OpenGLDevice 和 OpenGLCommandList 暴露了 OpenGL 特有方法,违反"上层只调用抽象接口"原则。
|
||||
|
||||
**修复内容**:
|
||||
|
||||
1. **OpenGLDevice 内部化方法**:
|
||||
- 移除 `GetTextureUnitAllocator()` 和 `GetUniformBufferManager()` 公开访问(移到 private)
|
||||
- 移除 `SwapBuffers()` 公开方法(OpenGLSwapChain 作为 friend 仍可访问)
|
||||
- `GetPresentationDC()` 和 `GetGLContext()` 保留在 private,通过 friend 访问
|
||||
|
||||
2. **新增抽象接口**:
|
||||
- `MakeContextCurrent()` - 封装 `wglMakeCurrent` 操作
|
||||
- `GetNativeContext()` - 返回 GL Context 供 RenderDoc 使用
|
||||
|
||||
3. **保留的逃生舱方法**:
|
||||
- OpenGLCommandList 中的 `PrimitiveType` 相关方法是显式的 OpenGL 逃生舱
|
||||
- 这些方法使用 OpenGL 特有类型,文档化为"backend-specific escape hatch"
|
||||
- 抽象层正确使用 `PrimitiveTopology` 枚举
|
||||
|
||||
**修改的文件**:
|
||||
- `engine/include/XCEngine/RHI/OpenGL/OpenGLDevice.h` - 内部化特有方法,新增 MakeContextCurrent/GetNativeContext
|
||||
- `engine/src/RHI/OpenGL/OpenGLDevice.cpp` - 实现 MakeContextCurrent
|
||||
- `tests/RHI/OpenGL/unit/test_device.cpp` - 移除 SwapBuffers 测试
|
||||
- `tests/RHI/OpenGL/integration/minimal/main.cpp` - 使用 MakeContextCurrent
|
||||
- `tests/RHI/OpenGL/integration/triangle/main.cpp` - 使用 MakeContextCurrent
|
||||
- `tests/RHI/OpenGL/integration/quad/main.cpp` - 使用 MakeContextCurrent
|
||||
- `tests/RHI/OpenGL/integration/sphere/main.cpp` - 使用 MakeContextCurrent
|
||||
|
||||
**说明**:
|
||||
- `PrimitiveType` 枚举在 OpenGL 专用逃生舱方法中使用是可接受的,因为这些是显式的后端特定接口
|
||||
- 抽象层正确使用 `PrimitiveTopology`,不存在类型泄漏问题
|
||||
|
||||
### 4.4 缺少 Compute Pipeline 抽象
|
||||
|
||||
详见第一版文档,此处不再赘述。
|
||||
|
||||
### 4.5 RHIPipelineLayout 空壳
|
||||
|
||||
详见第一版文档,此处不再赘述。
|
||||
|
||||
---
|
||||
|
||||
## 5. 修改后的 CommandList 核心接口
|
||||
|
||||
```cpp
|
||||
class RHICommandList {
|
||||
public:
|
||||
virtual ~RHICommandList() = default;
|
||||
|
||||
// ========== 生命周期 ==========
|
||||
virtual void Reset(RHICommandPool* pool = nullptr) = 0;
|
||||
virtual void Close() = 0;
|
||||
|
||||
// ========== 资源状态转换 ==========
|
||||
virtual void ResourceBarrier(uint32_t count, const ResourceBarrier* barriers) = 0;
|
||||
|
||||
// ========== 渲染通道(新增)==========
|
||||
virtual void BeginRenderPass(
|
||||
RHIRenderPass* renderPass,
|
||||
RHIFramebuffer* framebuffer,
|
||||
const Rect& renderArea,
|
||||
uint32_t clearValueCount,
|
||||
const ClearValue* clearValues) = 0;
|
||||
virtual void EndRenderPass() = 0;
|
||||
|
||||
// ========== 管线与描述符 ==========
|
||||
virtual void SetPipelineState(RHIPipelineState* pso) = 0;
|
||||
virtual void SetGraphicsDescriptorSets(
|
||||
uint32_t firstSet,
|
||||
uint32_t count,
|
||||
RHIDescriptorSet** descriptorSets,
|
||||
RHIPipelineLayout* pipelineLayout) = 0;
|
||||
|
||||
// ========== 动态状态(精简后)==========
|
||||
virtual void SetViewports(uint32_t count, const Viewport* viewports) = 0;
|
||||
virtual void SetScissorRects(uint32_t count, const Rect* rects) = 0;
|
||||
virtual void SetStencilRef(uint8_t ref) = 0;
|
||||
virtual void SetBlendFactor(const float factor[4]) = 0;
|
||||
|
||||
// ========== 顶点/索引 ==========
|
||||
virtual void SetVertexBuffers(
|
||||
uint32_t startSlot,
|
||||
uint32_t count,
|
||||
RHIVertexBufferView** buffers,
|
||||
const uint64_t* offsets,
|
||||
const uint32_t* strides) = 0;
|
||||
virtual void SetIndexBuffer(
|
||||
RHIIndexBufferView* buffer,
|
||||
uint64_t offset,
|
||||
IndexFormat format) = 0;
|
||||
|
||||
// ========== 绘制 ==========
|
||||
virtual void Draw(uint32_t vertexCount, uint32_t instanceCount = 1,
|
||||
uint32_t startVertex = 0, uint32_t startInstance = 0) = 0;
|
||||
virtual void DrawIndexed(uint32_t indexCount, uint32_t instanceCount = 1,
|
||||
uint32_t startIndex = 0, int32_t baseVertex = 0,
|
||||
uint32_t startInstance = 0) = 0;
|
||||
|
||||
// ========== 计算 ==========
|
||||
virtual void SetComputePipelineState(RHIPipelineState* pso) = 0;
|
||||
virtual void SetComputeDescriptorSets(
|
||||
uint32_t firstSet,
|
||||
uint32_t count,
|
||||
RHIDescriptorSet** descriptorSets,
|
||||
RHIPipelineLayout* pipelineLayout) = 0;
|
||||
virtual void Dispatch(uint32_t x, uint32_t y, uint32_t z) = 0;
|
||||
|
||||
// ========== 复制 ==========
|
||||
virtual void CopyResource(RHIResource* dst, RHIResource* src) = 0;
|
||||
|
||||
// ========== 原生句柄 ==========
|
||||
virtual void* GetNativeHandle() = 0;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 需要同时新增的抽象
|
||||
|
||||
### 6.1 RHIDescriptorSet
|
||||
```cpp
|
||||
class RHIDescriptorSet {
|
||||
public:
|
||||
virtual ~RHIDescriptorSet() = default;
|
||||
virtual void Update(uint32_t offset, RHIResourceView* view) = 0;
|
||||
virtual void UpdateSampler(uint32_t offset, RHISampler* sampler) = 0;
|
||||
virtual void* GetNativeHandle() = 0;
|
||||
};
|
||||
```
|
||||
|
||||
### 6.2 RHIRenderPass
|
||||
```cpp
|
||||
struct AttachmentDesc {
|
||||
Format format;
|
||||
LoadAction loadOp = LoadAction::DontCare;
|
||||
StoreAction storeOp = StoreAction::Store;
|
||||
LoadAction stencilLoadOp = LoadAction::DontCare;
|
||||
StoreAction stencilStoreOp = StoreAction::DontCare;
|
||||
};
|
||||
|
||||
class RHIRenderPass {
|
||||
public:
|
||||
virtual ~RHIRenderPass() = default;
|
||||
virtual bool Initialize(uint32_t colorAttachmentCount, const AttachmentDesc* colorAttachments,
|
||||
const AttachmentDesc* depthStencilAttachment) = 0;
|
||||
virtual void Shutdown() = 0;
|
||||
virtual void* GetNativeHandle() = 0;
|
||||
};
|
||||
```
|
||||
|
||||
### 6.3 RHIFramebuffer
|
||||
```cpp
|
||||
class RHIFramebuffer {
|
||||
public:
|
||||
virtual ~RHIFramebuffer() = default;
|
||||
virtual bool Initialize(RHIRenderPass* renderPass, uint32_t attachmentCount,
|
||||
RHIResourceView** attachments, uint32_t width, uint32_t height) = 0;
|
||||
virtual void Shutdown() = 0;
|
||||
virtual void* GetNativeHandle() = 0;
|
||||
};
|
||||
```
|
||||
|
||||
### 6.4 RHIResource 基类
|
||||
```cpp
|
||||
class RHIResource {
|
||||
public:
|
||||
virtual ~RHIResource() = default;
|
||||
virtual void* GetNativeHandle() = 0;
|
||||
virtual ResourceStates GetState() const = 0;
|
||||
virtual void SetState(ResourceStates state) = 0;
|
||||
};
|
||||
|
||||
class RHIBuffer : public RHIResource { ... };
|
||||
class RHITexture : public RHIResource { ... };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 问题优先级总结
|
||||
|
||||
| 优先级 | 问题 | 严重性 | 修复难度 | 状态 |
|
||||
|--------|------|--------|----------|------|
|
||||
| 1 | 字符串查找 SetUniform 不符合 D3D12/Vulkan | 🔴 致命 | 高 | ✅ 已完成 |
|
||||
| 2 | 缺少显式 RenderPass | 🔴 致命 | 高 | ✅ 已完成 |
|
||||
| 3 | 动态状态太多 | 🔴 高 | 高 | ✅ 已完成 |
|
||||
| 4 | ResourceView 类型不明确 | 🟡 中 | 中 | ✅ 基本完成 |
|
||||
| 5 | TransitionBarrier 针对 View 而非 Resource | 🟡 中 | 中 | ✅ 已完成 |
|
||||
| 6 | SetGlobal* 空操作 | 🟡 中 | 低 | ✅ 已完成 |
|
||||
| 7 | OpenGL 特有方法暴露 | 🟡 中 | 高 | ✅ 已完成 |
|
||||
| 8 | 缺少 Compute Pipeline 抽象 | 🟡 中 | 中 | ✅ 已完成 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 总结
|
||||
|
||||
当前 RHI 模块的基础框架有 D3D12/Vulkan 的影子,但**被 OpenGL 风格带偏**了。最核心的问题是:
|
||||
|
||||
1. **用字符串查找设 uniform** —— 这是 OpenGL 风格,必须改成 DescriptorSet 绑定
|
||||
2. **缺少显式 RenderPass** —— 现代 API 的基础,必须添加
|
||||
3. **动态状态太多** —— 应该收敛到 PSO,只保留必要的动态状态
|
||||
|
||||
修复这些问题后,RHI 抽象层将完全对齐 D3D12/Vulkan 的显式模型,未来接入 Vulkan 将会非常顺利。
|
||||
@@ -1,285 +0,0 @@
|
||||
# XCEngine 测试体系文档
|
||||
|
||||
> **版本**: 1.0
|
||||
> **日期**: 2026-03-13
|
||||
|
||||
---
|
||||
|
||||
## 1. 测试架构
|
||||
|
||||
```
|
||||
tests/
|
||||
├── CMakeLists.txt # 测试构建配置
|
||||
├── run_tests.cmake # 测试运行脚本
|
||||
├── fixtures/ # 测试夹具
|
||||
│ └── MathFixtures.h
|
||||
├── math/ # Math 单元测试
|
||||
│ ├── CMakeLists.txt
|
||||
│ ├── test_vector.cpp
|
||||
│ ├── test_matrix.cpp
|
||||
│ ├── test_quaternion.cpp
|
||||
│ └── test_geometry.cpp
|
||||
├── core/ # Core 测试
|
||||
├── threading/ # 线程测试
|
||||
├── memory/ # 内存测试
|
||||
├── containers/ # 容器测试
|
||||
└── rendering/ # 渲染测试
|
||||
├── unit/ # 单元测试
|
||||
├── integration/ # 集成测试
|
||||
└── screenshots/ # 参考图
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 测试分类
|
||||
|
||||
| 类型 | 目录 | 目的 | 运行频率 |
|
||||
|------|------|------|---------|
|
||||
| **Unit Test** | `tests/*/` | 验证单个函数/类 | 每次提交 |
|
||||
| **Integration Test** | `tests/rendering/integration/` | 验证多模块协作 | 每次提交 |
|
||||
| **Benchmark** | `tests/benchmark/` | 性能回归检测 | 每日/每周 |
|
||||
| **Screenshot Test** | `tests/rendering/screenshots/` | 渲染正确性 | 每次提交 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 测试命名规范
|
||||
|
||||
```cpp
|
||||
// 格式: test_<模块>_<功能>_<场景>
|
||||
TEST(Math_Vector3, Dot_TwoVectors_ReturnsCorrectValue) { }
|
||||
TEST(Math_Vector3, Normalize_ZeroVector_ReturnsZeroVector) { }
|
||||
TEST(Math_Matrix4, Inverse_Identity_ReturnsIdentity) { }
|
||||
TEST(Math_Matrix4, TRS_Decompose_RecoversOriginalValues) { }
|
||||
TEST(Math_Quaternion, Slerp_ShortestPath_InterpolatesCorrectly) { }
|
||||
|
||||
// 边界情况
|
||||
TEST(Math_Vector3, Normalize_ZeroVector_DoesNotCrash) { }
|
||||
TEST(Math_Matrix4, Inverse_SingularMatrix_ReturnsIdentity) { }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 断言规范
|
||||
|
||||
### 4.1 浮点数比较
|
||||
|
||||
```cpp
|
||||
// 必须使用容差
|
||||
EXPECT_NEAR(actual, expected, 1e-5f);
|
||||
ASSERT_FLOAT_EQ(actual, expected); // gtest 内部有容差
|
||||
|
||||
// 数组比较
|
||||
for (int i = 0; i < 4; i++) {
|
||||
EXPECT_NEAR(actual.m[i], expected.m[i], 1e-5f);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 常用断言
|
||||
|
||||
```cpp
|
||||
EXPECT_TRUE(condition);
|
||||
EXPECT_FALSE(condition);
|
||||
EXPECT_EQ(actual, expected);
|
||||
EXPECT_NE(actual, expected);
|
||||
EXPECT_STREQ(actual, expected);
|
||||
EXPECT_THROW(expression, exception_type);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 测试夹具 (Fixture)
|
||||
|
||||
```cpp
|
||||
class MathFixture : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
v1 = Vector3(1, 0, 0);
|
||||
v2 = Vector3(0, 1, 0);
|
||||
v3 = Vector3(1, 1, 1);
|
||||
|
||||
m1 = Matrix4x4::Identity();
|
||||
m2 = Matrix4x4::Translation(Vector3(1, 2, 3));
|
||||
}
|
||||
|
||||
Vector3 v1, v2, v3;
|
||||
Matrix4x4 m1, m2;
|
||||
const float epsilon = 1e-5f;
|
||||
};
|
||||
|
||||
TEST_F(MathFixture, Dot_OrthogonalVectors_ReturnsZero) {
|
||||
EXPECT_FLOAT_EQ(Vector3::Dot(v1, v2), 0.0f);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 参数化测试
|
||||
|
||||
```cpp
|
||||
class MatrixInverseTest : public ::testing::TestWithParam<Matrix4x4> {};
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(
|
||||
InverseCases,
|
||||
MatrixInverseTest,
|
||||
testing::Values(
|
||||
Matrix4x4::Identity(),
|
||||
Matrix4x4::Translation(Vector3(1,2,3)),
|
||||
Matrix4x4::Scale(Vector3(2,2,2))
|
||||
)
|
||||
);
|
||||
|
||||
TEST_P(MatrixInverseTest, InverseOfInverse_EqualsOriginal) {
|
||||
Matrix4x4 original = GetParam();
|
||||
Matrix4x4 inverted = original.Inverse();
|
||||
Matrix4x4 recovered = inverted.Inverse();
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
for (int j = 0; j < 4; j++) {
|
||||
EXPECT_NEAR(original.m[i][j], recovered.m[i][j], 1e-4f);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Math 测试用例
|
||||
|
||||
### 7.1 Vector2/3/4 测试
|
||||
|
||||
| 测试类别 | 测试用例 |
|
||||
|---------|---------|
|
||||
| **构造** | 默认构造、参数构造、从 Vector3 构造 |
|
||||
| **运算** | 加、减、乘、除、点积、叉积 |
|
||||
| **归一化** | Normalize、Normalized、Magnitude、SqrMagnitude |
|
||||
| **插值** | Lerp、MoveTowards |
|
||||
| **投影** | Project、ProjectOnPlane |
|
||||
| **角度** | Angle、Reflect |
|
||||
|
||||
### 7.2 Matrix 测试
|
||||
|
||||
| 测试类别 | 测试用例 |
|
||||
|---------|---------|
|
||||
| **构造** | Identity、Zero |
|
||||
| **变换** | Translation、Rotation、Scale、TRS |
|
||||
| **相机** | LookAt、Perspective、Orthographic |
|
||||
| **运算** | 乘法、点乘、叉乘 |
|
||||
| **分解** | Inverse、Transpose、Determinant、Decompose |
|
||||
|
||||
### 7.3 Quaternion 测试
|
||||
|
||||
| 测试类别 | 测试用例 |
|
||||
|---------|---------|
|
||||
| **构造** | Identity、FromAxisAngle、FromEulerAngles |
|
||||
| **转换** | ToEulerAngles、ToMatrix4x4、FromRotationMatrix |
|
||||
| **插值** | Slerp |
|
||||
| **运算** | 乘法和逆 |
|
||||
|
||||
### 7.4 几何测试
|
||||
|
||||
| 测试类型 | 测试用例 |
|
||||
|---------|---------|
|
||||
| **Ray** | GetPoint、Intersects(Sphere/Box/Plane) |
|
||||
| **Sphere** | Contains、Intersects |
|
||||
| **Box** | Contains、Intersects |
|
||||
| **Plane** | FromPoints、GetDistanceToPoint、Intersects |
|
||||
| **Frustum** | Contains(Point/Sphere/Bounds)、Intersects |
|
||||
| **Bounds** | GetMinMax、Intersects、Contains、Encapsulate |
|
||||
|
||||
---
|
||||
|
||||
## 8. 构建与运行
|
||||
|
||||
### 8.1 构建测试
|
||||
|
||||
```bash
|
||||
# 创建构建目录
|
||||
mkdir build && cd build
|
||||
|
||||
# 配置 CMake
|
||||
cmake .. -G "Visual Studio 17 2022" -A x64
|
||||
|
||||
# 构建测试
|
||||
cmake --build . --config Debug --target xcengine_math_tests
|
||||
```
|
||||
|
||||
### 8.2 运行测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
ctest --output-on-failure
|
||||
|
||||
# 运行 Math 测试
|
||||
./tests/xcengine_math_tests.exe
|
||||
|
||||
# 运行特定测试
|
||||
./tests/xcengine_math_tests.exe --gtest_filter=Math_Vector3.*
|
||||
|
||||
# 运行测试并显示详细信息
|
||||
./tests/xcengine_math_tests.exe --gtest_also_run_disabled_tests --gtest_print_time=1
|
||||
```
|
||||
|
||||
### 8.3 测试过滤器
|
||||
|
||||
```bash
|
||||
# 运行所有 Vector3 测试
|
||||
--gtest_filter=Math_Vector3.*
|
||||
|
||||
# 运行除某测试外的所有测试
|
||||
--gtest_filter=-Math_Matrix4.SingularMatrix*
|
||||
|
||||
# 运行多个测试
|
||||
--gtest_filter=Math_Vector3.*:Math_Matrix4.*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 覆盖率要求
|
||||
|
||||
| 模块 | 最低覆盖率 | 关键测试 |
|
||||
|------|-----------|---------|
|
||||
| Math | 90% | 所有公开 API |
|
||||
| Core | 80% | 智能指针、Event |
|
||||
| Containers | 85% | 边界、迭代器 |
|
||||
| Memory | 90% | 分配/泄漏 |
|
||||
| Threading | 70% | 基本功能 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 持续集成
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Configure
|
||||
run: cmake -B build -DENABLE_COVERAGE=ON
|
||||
|
||||
- name: Build
|
||||
run: cmake --build build --config Debug
|
||||
|
||||
- name: Test
|
||||
run: ctest --test-dir build --output-on-failure
|
||||
|
||||
- name: Coverage
|
||||
run: cmake --build build --target coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 注意事项
|
||||
|
||||
1. **浮点数比较** - 必须使用容差 (通常 1e-5 或 1e-6)
|
||||
2. **边界条件** - 必须测试零向量、奇异矩阵等
|
||||
3. **随机性** - 如需固定 seed 保证确定性
|
||||
4. **线程安全** - 线程测试需设置超时
|
||||
5. **内存泄漏** - 使用 Valgrind 或 CRT 检测
|
||||
@@ -1,550 +0,0 @@
|
||||
# UI-Editor GameObject 缺口分析
|
||||
|
||||
> **基于**:XCEngine渲染引擎架构设计.md
|
||||
> **目标**:识别 UI 编辑器 GameObject.h 相对于 Engine 架构设计文档的缺失功能
|
||||
> **日期**:2026-03-20
|
||||
|
||||
---
|
||||
|
||||
## 架构参考
|
||||
|
||||
XCEngine 组件系统参考 **Unity 传统架构 (GameObject-Component Pattern)**:
|
||||
|
||||
- **GameObject**:场景中的实体(UI Editor 中称 `Entity`)
|
||||
- **Component**:挂载到 GameObject 的功能模块(变换、渲染器等)
|
||||
- **TransformComponent**:每个 GameObject 都有的变换组件,管理位置/旋转/缩放
|
||||
- **Scene/SceneManager**:场景管理和场景切换
|
||||
|
||||
---
|
||||
|
||||
## Engine 模块现状
|
||||
|
||||
| 模块 | 状态 | UI Editor 对齐情况 |
|
||||
|-----|------|------------------|
|
||||
| Core (Event, Types) | ✅ 已实现 | UI 编辑器已复用 `XCEngine::Core::Event<T>` |
|
||||
| Debug (Logger, LogLevel, LogEntry) | ✅ 已实现 | UI 编辑器已复用 `XCEngine::Debug::LogLevel` |
|
||||
| **Math (Vector3, Quaternion, Matrix4x4, Transform, Color)** | ✅ **已实现** | **可直接复用 Engine Math 类型** |
|
||||
| Memory | ✅ 已实现 | |
|
||||
| Threading | ✅ 已实现 | |
|
||||
| Containers | ✅ 已实现 | |
|
||||
| RHI / D3D12 | ✅ 已实现 | UI 编辑器自行实现(与 Engine 技术栈一致) |
|
||||
| Resources | ✅ 已实现 | |
|
||||
| **Components (GameObject, Component, System)** | ❌ **缺失** | UI 编辑器自行实现,待 Engine 实现后迁移 |
|
||||
| **Scene (Scene, SceneManager, SceneLoader, SceneSerializer)** | ❌ **缺失** | UI 编辑器自行实现,待 Engine 实现后迁移 |
|
||||
|
||||
---
|
||||
|
||||
## 一、Component 基类缺口
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `transform()` | `TransformComponent& transform() const` | ❌ 缺失 | 返回所在实体的 TransformComponent 引用 |
|
||||
| `GetScene()` | `Scene* GetScene() const` | ❌ 缺失 | 返回所属 Scene 指针 |
|
||||
| `FixedUpdate()` | `virtual void FixedUpdate()` | ❌ 缺失 | 物理更新(每固定帧) |
|
||||
| `LateUpdate()` | `virtual void LateUpdate(float deltaTime)` | ❌ 缺失 | 晚于 Update 执行 |
|
||||
| `OnEnable()` | `virtual void OnEnable()` | ❌ 缺失 | 组件启用时调用 |
|
||||
| `OnDisable()` | `virtual void OnDisable()` | ❌ 缺失 | 组件禁用时调用 |
|
||||
| `friend class` | `friend class GameObject` | ⚠️ 差异 | UI Editor 是 `friend class Entity` |
|
||||
|
||||
**补充说明**:
|
||||
- UI Editor 的 `Component` 需要增加 `TransformComponent*` 获取方法以支持 Inspector 面板的变换编辑
|
||||
- Engine 的 `m_gameObject` 在 UI Editor 中对应 `m_entity`
|
||||
|
||||
---
|
||||
|
||||
## 二、TransformComponent 缺口
|
||||
|
||||
### 2.1 World 空间方法
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `GetPosition()` | `XCEngine::Math::Vector3 GetPosition() const` | ❌ 缺失 | 获取世界坐标 |
|
||||
| `SetPosition()` | `void SetPosition(const XCEngine::Math::Vector3& position)` | ❌ 缺失 | 设置世界坐标 |
|
||||
| `GetRotation()` | `XCEngine::Math::Quaternion GetRotation() const` | ❌ 缺失 | 获取世界旋转 |
|
||||
| `SetRotation()` | `void SetRotation(const XCEngine::Math::Quaternion& rotation)` | ❌ 缺失 | 设置世界旋转 |
|
||||
| `GetScale()` | `XCEngine::Math::Vector3 GetScale() const` | ❌ 缺失 | 获取世界缩放 |
|
||||
| `SetScale()` | `void SetScale(const XCEngine::Math::Vector3& scale)` | ❌ 缺失 | 设置世界缩放 |
|
||||
|
||||
### 2.2 方向向量
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `GetForward()` | `XCEngine::Math::Vector3 GetForward() const` | ❌ 缺失 | 获取前向向量 |
|
||||
| `GetRight()` | `XCEngine::Math::Vector3 GetRight() const` | ❌ 缺失 | 获取右向量 |
|
||||
| `GetUp()` | `XCEngine::Math::Vector3 GetUp() const` | ❌ 缺失 | 获取上向量 |
|
||||
|
||||
### 2.3 矩阵变换
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `GetLocalToWorldMatrix()` | `const XCEngine::Math::Matrix4x4& GetLocalToWorldMatrix() const` | ❌ 缺失 | 本地到世界矩阵(含缓存) |
|
||||
| `GetWorldToLocalMatrix()` | `XCEngine::Math::Matrix4x4 GetWorldToLocalMatrix() const` | ❌ 缺失 | 世界到本地矩阵 |
|
||||
|
||||
### 2.4 父子层级
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `GetParent()` | `TransformComponent* GetParent() const` | ❌ 缺失 | 获取父变换 |
|
||||
| `SetParent()` | `void SetParent(TransformComponent* parent, bool worldPositionStays = true)` | ❌ 缺失 | 设置父变换 |
|
||||
| `GetChild()` | `TransformComponent* GetChild(int index) const` | ❌ 缺失 | 按索引获取子变换 |
|
||||
| `Find()` | `TransformComponent* Find(const String& name) const` | ❌ 缺失 | 按名称查找子变换 |
|
||||
| `DetachChildren()` | `void DetachChildren()` | ❌ 缺失 | 断开所有子节点 |
|
||||
| `SetAsFirstSibling()` | `void SetAsFirstSibling()` | ❌ 缺失 | 设为第一个同级 |
|
||||
| `SetAsLastSibling()` | `void SetAsLastSibling()` | ❌ 缺失 | 设为最后一个同级 |
|
||||
| `GetSiblingIndex()` | `int GetSiblingIndex() const` | ❌ 缺失 | 获取同级索引 |
|
||||
| `SetSiblingIndex()` | `void SetSiblingIndex(int index)` | ❌ 缺失 | 设置同级索引 |
|
||||
|
||||
### 2.5 变换操作
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `LookAt(target)` | `void LookAt(const XCEngine::Math::Vector3& target)` | ❌ 缺失 | 朝向目标点 |
|
||||
| `LookAt(target, up)` | `void LookAt(const XCEngine::Math::Vector3& target, const XCEngine::Math::Vector3& up)` | ❌ 缺失 | 朝向目标点(指定上向量) |
|
||||
| `Rotate(eulers)` | `void Rotate(const XCEngine::Math::Vector3& eulers)` | ❌ 缺失 | 欧拉角旋转 |
|
||||
| `Rotate(axis, angle)` | `void Rotate(const XCEngine::Math::Vector3& axis, float angle)` | ❌ 缺失 | 轴角旋转 |
|
||||
| `Translate(translation)` | `void Translate(const XCEngine::Math::Vector3& translation)` | ❌ 缺失 | 平移(世界空间) |
|
||||
| `Translate(translation, relativeTo)` | `void Translate(const XCEngine::Math::Vector3& translation, Space relativeTo)` | ❌ 缺失 | 平移(指定空间) |
|
||||
|
||||
### 2.6 点/方向变换
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `TransformPoint()` | `XCEngine::Math::Vector3 TransformPoint(const XCEngine::Math::Vector3& point) const` | ❌ 缺失 | 变换点(含平移旋转) |
|
||||
| `InverseTransformPoint()` | `XCEngine::Math::Vector3 InverseTransformPoint(const XCEngine::Math::Vector3& point) const` | ❌ 缺失 | 逆变换点 |
|
||||
| `TransformDirection()` | `XCEngine::Math::Vector3 TransformDirection(const XCEngine::Math::Vector3& direction) const` | ❌ 缺失 | 变换方向(仅旋转) |
|
||||
| `InverseTransformDirection()` | `XCEngine::Math::Vector3 InverseTransformDirection(const XCEngine::Math::Vector3& direction) const` | ❌ 缺失 | 逆变换方向 |
|
||||
|
||||
### 2.7 脏标记与缓存
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `SetDirty()` | `void SetDirty()` | ⚠️ 局部 | UI Editor 只有简单 flag,缺少完整缓存机制 |
|
||||
| 缓存成员 | `mutable XCEngine::Math::Matrix4x4 m_localToWorldMatrix` 等 | ❌ 缺失 | 矩阵缓存 |
|
||||
| 更新方法 | `void UpdateWorldTransform() const` | ❌ 缺失 | 缓存失效时重新计算 |
|
||||
|
||||
**当前 UI Editor 实现**:
|
||||
```cpp
|
||||
class TransformComponent : public Component {
|
||||
public:
|
||||
float position[3] = {0.0f, 0.0f, 0.0f}; // 仅有本地坐标
|
||||
float rotation[3] = {0.0f, 0.0f, 0.0f}; // 欧拉角
|
||||
float scale[3] = {1.0f, 1.0f, 1.0f}; // 仅有本地缩放
|
||||
// 缺少世界空间方法、矩阵缓存、父子指针等
|
||||
};
|
||||
```
|
||||
|
||||
**架构设计要求**(应使用 Engine Math 类型):
|
||||
```cpp
|
||||
class TransformComponent : public Component {
|
||||
public:
|
||||
// 使用 Engine Math 类型
|
||||
XCEngine::Math::Vector3 GetLocalPosition() const { return m_localPosition; }
|
||||
void SetLocalPosition(const XCEngine::Math::Vector3& pos) { m_localPosition = pos; SetDirty(); }
|
||||
|
||||
XCEngine::Math::Quaternion GetLocalRotation() const { return m_localRotation; }
|
||||
void SetLocalRotation(const XCEngine::Math::Quaternion& rot) { m_localRotation = rot; SetDirty(); }
|
||||
|
||||
XCEngine::Math::Vector3 GetLocalScale() const { return m_localScale; }
|
||||
void SetLocalScale(const XCEngine::Math::Vector3& sc) { m_localScale = sc; SetDirty(); }
|
||||
|
||||
private:
|
||||
XCEngine::Math::Vector3 m_localPosition = XCEngine::Math::Vector3::Zero();
|
||||
XCEngine::Math::Quaternion m_localRotation = XCEngine::Math::Quaternion::Identity();
|
||||
XCEngine::Math::Vector3 m_localScale = XCEngine::Math::Vector3::One();
|
||||
|
||||
TransformComponent* m_parent = nullptr;
|
||||
std::vector<TransformComponent*> m_children;
|
||||
|
||||
mutable XCEngine::Math::Matrix4x4 m_localToWorldMatrix;
|
||||
mutable XCEngine::Math::Matrix4x4 m_worldToLocalMatrix;
|
||||
mutable XCEngine::Math::Vector3 m_worldPosition;
|
||||
mutable XCEngine::Math::Quaternion m_worldRotation;
|
||||
mutable XCEngine::Math::Vector3 m_worldScale;
|
||||
mutable bool m_dirty = true;
|
||||
|
||||
void UpdateWorldTransform() const;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、GameObject缺口
|
||||
|
||||
### 3.1 基础方法
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `GetTransform()` | `TransformComponent& GetTransform()` | ❌ 缺失 | 获取变换组件引用 |
|
||||
| `GetScene()` | `Scene* GetScene() const` | ❌ 缺失 | 获取所属场景 |
|
||||
|
||||
### 3.2 组件查找
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `GetComponentInChildren<T>()` | `T* GetComponentInChildren() const` | ❌ 缺失 | 在子实体中查找组件 |
|
||||
| `GetComponentsInChildren<T>()` | `std::vector<T*> GetComponentsInChildren() const` | ❌ 缺失 | 获取所有子实体中的组件 |
|
||||
| `GetComponentInParent<T>()` | `T* GetComponentInParent() const` | ❌ 缺失 | 在父实体中查找组件 |
|
||||
|
||||
### 3.3 父子层级
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `SetParent(parent)` | `void SetParent(Entity* parent)` | ❌ 缺失 | 2参数版本(含 worldPositionStays) |
|
||||
| `GetChildren()` | `const std::vector<Entity*>& GetChildren() const` | ❌ 缺失 | 获取子实体列表 |
|
||||
| `GetChild()` | `Entity* GetChild(int index) const` | ❌ 缺失 | 按索引获取子实体 |
|
||||
|
||||
### 3.4 激活状态
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `active` 成员 | `bool active = true` | ❌ 缺失 | 实体激活状态 |
|
||||
| `SetActive()` | `void SetActive(bool active)` | ❌ 缺失 | 设置激活状态 |
|
||||
| `IsActive()` | `bool IsActive() const` | ❌ 缺失 | 检查激活状态 |
|
||||
| `IsActiveInHierarchy()` | `bool IsActiveInHierarchy() const` | ❌ 缺失 | 检查层级中的激活状态 |
|
||||
|
||||
### 3.5 静态查找
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `Find()` | `static Entity* Find(const String& name)` | ❌ 缺失 | 按名称查找实体 |
|
||||
| `FindObjectsOfType()` | `static std::vector<Entity*> FindObjectsOfType()` | ❌ 缺失 | 查找所有实体 |
|
||||
| `FindGameObjectsWithTag()` | `static std::vector<Entity*> FindGameObjectsWithTag(const String& tag)` | ❌ 缺失 | 按标签查找 |
|
||||
|
||||
### 3.6 销毁
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `Destroy()` | `void Destroy()` | ❌ 缺失 | 实例方法销毁(通过 Scene) |
|
||||
|
||||
---
|
||||
|
||||
## 四、Scene / SceneManager 缺口
|
||||
|
||||
### 4.1 Scene 类
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `GetName()` | `const String& GetName() const` | ❌ 缺失 | 获取场景名称 |
|
||||
| `SetName()` | `void SetName(const String& name)` | ❌ 缺失 | 设置场景名称 |
|
||||
| `CreateGameObject(name, parent)` | `Entity* CreateGameObject(const String& name, Entity* parent)` | ❌ 缺失 | 带父实体创建 |
|
||||
| `GetRootGameObjects()` | `std::vector<Entity*> GetRootGameObjects() const` | ❌ 缺失 | 获取根实体列表 |
|
||||
| `Find()` | `Entity* Find(const String& name) const` | ❌ 缺失 | 查找实体 |
|
||||
| `FindGameObjectWithTag()` | `Entity* FindGameObjectWithTag(const String& tag) const` | ❌ 缺失 | 按标签查找 |
|
||||
| `FindObjectsOfType<T>()` | `std::vector<T*> FindObjectsOfType() const` | ❌ 缺失 | 模板查找 |
|
||||
| `FindObjectOfType<T>()` | `T* FindObjectOfType() const` | ❌ 缺失 | 查找单个 |
|
||||
| `IsActive()` | `bool IsActive() const` | ❌ 缺失 | 场景激活状态 |
|
||||
| `SetActive()` | `void SetActive(bool active)` | ❌ 缺失 | 设置激活状态 |
|
||||
| `Load()` | `void Load(const String& filePath)` | ❌ 缺失 | 加载场景 |
|
||||
| `Save()` | `void Save(const String& filePath)` | ❌ 缺失 | 保存场景 |
|
||||
| `Update()` | `void Update(float deltaTime)` | ❌ 缺失 | 场景更新 |
|
||||
| `FixedUpdate()` | `void FixedUpdate(float fixedDeltaTime)` | ❌ 缺失 | 物理更新 |
|
||||
| `LateUpdate()` | `void LateUpdate(float deltaTime)` | ❌ 缺失 | 晚更新 |
|
||||
|
||||
**当前 UI Editor 缺失 Scene 概念**,所有实体都在 SceneManager 全局管理。
|
||||
|
||||
### 4.2 SceneManager 类
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| `CreateScene()` | `Scene* CreateScene(const String& name)` | ❌ 缺失 | 创建场景 |
|
||||
| `LoadScene()` | `void LoadScene(const String& filePath)` | ❌ 缺失 | 同步加载 |
|
||||
| `LoadSceneAsync()` | `void LoadSceneAsync(const String& filePath, std::function<void(Scene*)> callback)` | ❌ 缺失 | 异步加载 |
|
||||
| `UnloadScene(scene)` | `void UnloadScene(Scene* scene)` | ❌ 缺失 | 卸载场景 |
|
||||
| `UnloadScene(name)` | `void UnloadScene(const String& sceneName)` | ❌ 缺失 | 按名称卸载 |
|
||||
| `SetActiveScene(scene)` | `void SetActiveScene(Scene* scene)` | ❌ 缺失 | 设置激活场景 |
|
||||
| `SetActiveScene(name)` | `void SetActiveScene(const String& sceneName)` | ❌ 缺失 | 按名称设置 |
|
||||
| `GetActiveScene()` | `Scene* GetActiveScene() const` | ❌ 缺失 | 获取激活场景 |
|
||||
| `GetScene(name)` | `Scene* GetScene(const String& name) const` | ❌ 缺失 | 按名称获取场景 |
|
||||
| `GetAllScenes()` | `std::vector<Scene*> GetAllScenes() const` | ❌ 缺失 | 获取所有场景 |
|
||||
| `OnSceneLoaded` | `Event<Scene*>` | ⚠️ 差异 | UI Editor 是 `OnSceneChanged` |
|
||||
| `OnSceneUnloaded` | `Event<Scene*>` | ❌ 缺失 | 场景卸载事件 |
|
||||
| `OnActiveSceneChanged` | `Event<Scene*>` | ❌ 缺失 | 激活场景变更事件 |
|
||||
|
||||
### 4.3 GameObjectBuilder 类
|
||||
|
||||
| 架构设计接口 | 函数签名 | 状态 | 说明 |
|
||||
|------------|---------|------|------|
|
||||
| 完整类 | `class GameObjectBuilder` | ❌ 缺失 | 实体创建辅助类 |
|
||||
|
||||
---
|
||||
|
||||
## 五、实现优先级
|
||||
|
||||
### P0 - 核心缺失(必须实现)
|
||||
|
||||
| 模块 | 原因 | 涉及文件 |
|
||||
|-----|------|---------|
|
||||
| TransformComponent 世界空间方法 | Editor 变换编辑基础 | GameObject.h |
|
||||
| Entity `GetTransform()` | Inspector 面板需要 | GameObject.h |
|
||||
| Component `transform()` | 组件访问变换 | GameObject.h |
|
||||
|
||||
**依赖**:Engine Math 模块 ✅ 已实现,可直接使用 `XCEngine::Math::Vector3` / `Quaternion` / `Matrix4x4`。
|
||||
|
||||
### P1 - 功能完整(应该实现)
|
||||
|
||||
| 模块 | 原因 | 涉及文件 |
|
||||
|-----|------|---------|
|
||||
| TransformComponent 矩阵缓存 | 性能优化 | GameObject.h |
|
||||
| TransformComponent 父子指针 | 层级操作 | GameObject.h |
|
||||
| Entity 激活状态 | 运行时状态 | GameObject.h |
|
||||
| Entity 父子层级操作 | 层级编辑 | GameObject.h |
|
||||
| Scene 概念 | 多场景支持 | SceneManager.h |
|
||||
| SceneManager 多场景管理 | 场景隔离 | SceneManager.h |
|
||||
|
||||
### P2 - 高级功能(可选实现)
|
||||
|
||||
| 模块 | 原因 | 涉及文件 |
|
||||
|-----|------|---------|
|
||||
| Scene 序列化/反序列化 | 持久化 | SceneManager.h |
|
||||
| TransformComponent 点/方向变换 | Gizmos 操作 | GameObject.h |
|
||||
| SceneManager 异步加载 | 体验优化 | SceneManager.h |
|
||||
| Entity 静态查找 | Editor 操作 | GameObject.h |
|
||||
|
||||
---
|
||||
|
||||
## 六、数据结构对齐表
|
||||
|
||||
### 6.1 当前 UI Editor vs 架构设计
|
||||
|
||||
| 项目 | UI Editor 当前 | 架构设计 | 对齐建议 |
|
||||
|-----|--------------|---------|---------|
|
||||
| 实体内组件存储 | `vector<unique_ptr<Component>>` | `vector<unique_ptr<Component>>` | ✅ 已对齐 |
|
||||
| 变换类型 | `float[3]` 数组 | `XCEngine::Math::Vector3` / `Quaternion` | ⚠️ **应改用 Engine Math** |
|
||||
| 父子关系 | `EntityID` (uint64) | `Entity*` 指针 | ⚠️ UI 用 ID,Engine 用指针 |
|
||||
| 场景管理 | 全局单例 SceneManager | 多 Scene + SceneManager | ❌ 需要重构 |
|
||||
|
||||
### 6.2 命名差异
|
||||
|
||||
| UI Editor | Engine 架构设计 | 说明 |
|
||||
|-----------|---------------|------|
|
||||
| `Entity` | `GameObject` | UI Editor 使用 Entity 避免与 std 冲突 |
|
||||
| `EntityID` | `uint64_t` | UI Editor 自定义类型别名 |
|
||||
| `INVALID_ENTITY_ID` | N/A | UI Editor 自定义常量 |
|
||||
| `ComponentRegistry` | `ComponentTypeRegistry` | 功能相似,命名不同 |
|
||||
|
||||
### 6.3 类型复用情况
|
||||
|
||||
| Engine 类型 | UI Editor 现状 | 建议 |
|
||||
|-----------|--------------|------|
|
||||
| `XCEngine::Math::Vector3` | UI Editor 用 `float[3]` | **直接复用** |
|
||||
| `XCEngine::Math::Quaternion` | UI Editor 用 `float[3]` (欧拉角) | **直接复用**,但欧拉角存储可保留 |
|
||||
| `XCEngine::Math::Matrix4x4` | 无 | **直接复用** |
|
||||
| `XCEngine::Core::Event<T>` | ✅ 已复用 | 保持 |
|
||||
| `XCEngine::Debug::LogLevel` | ✅ 已复用 | 保持 |
|
||||
|
||||
---
|
||||
|
||||
## 七、依赖关系分析
|
||||
|
||||
```
|
||||
实现优先级 - 依赖链:
|
||||
|
||||
P0: TransformComponent 世界空间
|
||||
│
|
||||
├── 需要: XCEngine::Math::Vector3 ✅ 已实现
|
||||
├── 需要: XCEngine::Math::Quaternion ✅ 已实现
|
||||
├── 需要: XCEngine::Math::Matrix4x4 ✅ 已实现
|
||||
└── 影响: Entity::GetTransform()
|
||||
|
||||
P0: Component::transform()
|
||||
│
|
||||
└── 依赖: Entity::GetTransform()
|
||||
|
||||
P1: TransformComponent 父子指针
|
||||
│
|
||||
├── 需要: TransformComponent* parent
|
||||
├── 需要: vector<TransformComponent*> children
|
||||
└── 影响: Entity 父子操作
|
||||
|
||||
P1: Entity 激活状态
|
||||
│
|
||||
├── 需要: active 成员
|
||||
└── 影响: IsActiveInHierarchy()
|
||||
|
||||
P1: Scene 多场景
|
||||
│
|
||||
├── 需要: Scene 类
|
||||
├── 需要: Scene::CreateGameObject()
|
||||
└── 影响: SceneManager 重构
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、与 Unity 架构的对照
|
||||
|
||||
### Unity 传统架构 (GameObject-Component)
|
||||
|
||||
```
|
||||
Scene
|
||||
└── GameObject ("MyEntity")
|
||||
├── Transform
|
||||
├── MeshRenderer
|
||||
└── (其他组件...)
|
||||
|
||||
GameObject.GetComponent<T>() → Entity.GetComponent<T>()
|
||||
GameObject.transform → Entity.GetTransform()
|
||||
GameObject.SetActive(bool) → 缺失
|
||||
GameObject.Find("name") → 缺失
|
||||
Scene.GetRootGameObjects() → 缺失
|
||||
SceneManager.GetActiveScene() → 缺失
|
||||
```
|
||||
|
||||
### UI Editor 当前实现
|
||||
|
||||
```
|
||||
SceneManager (全局单例)
|
||||
└── m_entities (unordered_map<EntityID, Entity>)
|
||||
└── Entity ("MyEntity")
|
||||
├── id, name, parent, children
|
||||
├── components (vector<unique_ptr<Component>>)
|
||||
└── selected
|
||||
|
||||
Entity.AddComponent<T>() → ✅ 已实现
|
||||
Entity.GetComponent<T>() → ✅ 已实现
|
||||
Entity.GetTransform() → ❌ 缺失
|
||||
TransformComponent 世界空间方法 → ❌ 缺失
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、附录:当前 GameObject.h 完整内容
|
||||
|
||||
```cpp
|
||||
// ui_editor/src/Core/GameObject.h 当前实现
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <cstdint>
|
||||
|
||||
#include <XCEngine/Core/Event.h>
|
||||
|
||||
namespace UI {
|
||||
|
||||
using EntityID = uint64_t;
|
||||
constexpr EntityID INVALID_ENTITY_ID = 0;
|
||||
|
||||
class Component {
|
||||
public:
|
||||
virtual ~Component() = default;
|
||||
virtual std::string GetName() const = 0;
|
||||
|
||||
virtual void Awake() {}
|
||||
virtual void Start() {}
|
||||
virtual void Update(float deltaTime) {}
|
||||
virtual void OnDestroy() {}
|
||||
|
||||
class Entity* GetEntity() const { return m_entity; }
|
||||
bool IsEnabled() const { return m_enabled; }
|
||||
void SetEnabled(bool enabled) { m_enabled = enabled; }
|
||||
|
||||
protected:
|
||||
class Entity* m_entity = nullptr;
|
||||
bool m_enabled = true;
|
||||
|
||||
friend class Entity;
|
||||
};
|
||||
|
||||
class TransformComponent : public Component {
|
||||
public:
|
||||
float position[3] = {0.0f, 0.0f, 0.0f};
|
||||
float rotation[3] = {0.0f, 0.0f, 0.0f};
|
||||
float scale[3] = {1.0f, 1.0f, 1.0f};
|
||||
|
||||
std::string GetName() const override { return "Transform"; }
|
||||
};
|
||||
|
||||
class MeshRendererComponent : public Component {
|
||||
public:
|
||||
std::string materialName = "Default-Material";
|
||||
std::string meshName = "";
|
||||
|
||||
std::string GetName() const override { return "Mesh Renderer"; }
|
||||
};
|
||||
|
||||
class Entity {
|
||||
public:
|
||||
EntityID id = INVALID_ENTITY_ID;
|
||||
std::string name;
|
||||
EntityID parent = INVALID_ENTITY_ID;
|
||||
std::vector<EntityID> children;
|
||||
std::vector<std::unique_ptr<Component>> components;
|
||||
bool selected = false;
|
||||
|
||||
template<typename T, typename... Args>
|
||||
T* AddComponent(Args&&... args) {
|
||||
auto comp = std::make_unique<T>(std::forward<Args>(args)...);
|
||||
comp->m_entity = this;
|
||||
T* ptr = comp.get();
|
||||
components.push_back(std::move(comp));
|
||||
return ptr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T* GetComponent() {
|
||||
for (auto& comp : components) {
|
||||
if (auto casted = dynamic_cast<T*>(comp.get())) {
|
||||
return casted;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T*> GetComponents() {
|
||||
std::vector<T*> result;
|
||||
for (auto& comp : components) {
|
||||
if (auto casted = dynamic_cast<T*>(comp.get())) {
|
||||
result.push_back(casted);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
using ComponentInspectorFn = std::function<void(Component*)>;
|
||||
|
||||
struct ComponentInspectorInfo {
|
||||
std::string name;
|
||||
ComponentInspectorFn renderFn;
|
||||
};
|
||||
|
||||
class ComponentRegistry {
|
||||
public:
|
||||
static ComponentRegistry& Get() {
|
||||
static ComponentRegistry instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void RegisterComponent(const std::string& name, ComponentInspectorFn inspectorFn) {
|
||||
m_inspectors[name] = {name, inspectorFn};
|
||||
m_factories[name] = []() -> std::unique_ptr<Component> {
|
||||
return std::make_unique<T>();
|
||||
};
|
||||
}
|
||||
|
||||
ComponentInspectorInfo* GetInspector(const std::string& name) {
|
||||
auto it = m_inspectors.find(name);
|
||||
if (it != m_inspectors.end()) {
|
||||
return &it->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
ComponentRegistry() = default;
|
||||
std::unordered_map<std::string, ComponentInspectorInfo> m_inspectors;
|
||||
std::unordered_map<std::string, std::function<std::unique_ptr<Component>()>> m_factories;
|
||||
};
|
||||
|
||||
} // namespace UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**文档结束**
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,546 +0,0 @@
|
||||
# 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 现有的组件模式和事件系统。
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,151 +0,0 @@
|
||||
# XCEngine 游戏引擎 - 第一阶段计划
|
||||
|
||||
> **目标**: 构建核心基础层,为上层渲染系统提供底层依赖
|
||||
> **版本**: 1.0
|
||||
> **日期**: 2026-03-13
|
||||
|
||||
---
|
||||
|
||||
## 阶段目标
|
||||
|
||||
第一阶段聚焦于引擎底层基础设施的建设,确保后续渲染系统开发有稳定的基础。
|
||||
|
||||
---
|
||||
|
||||
## 模块规划
|
||||
|
||||
### 1.1 数学库 (Math Library)
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **优先级** | P0 |
|
||||
| **预计工作量** | 5天 |
|
||||
| **包含类型** | `Vector2`, `Vector3`, `Vector4`, `Matrix3x3`, `Matrix4x4`, `Quaternion`, `Transform`, `Color`, `Rect`, `RectInt`, `Viewport`, `Ray`, `Sphere`, `Box`, `Plane`, `Frustum`, `Bounds`, `AABB`, `OBB` |
|
||||
| **功能要求** | 向量运算、矩阵变换、四元数、欧拉角转换、视锥体剔除基础 |
|
||||
|
||||
### 1.2 Core 基础类型
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **优先级** | P0 |
|
||||
| **预计工作量** | 2天 |
|
||||
| **包含类型** | 基础类型别名 (`int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `int64`, `uint64`, `byte`)、`RefCounted`、`Ref<T>`、`UniqueRef<T>`、`Event<T>` |
|
||||
| **功能要求** | 基础类型别名、引用计数、智能指针、事件系统 |
|
||||
| **依赖** | 无 |
|
||||
|
||||
### 1.3 线程系统 (Threading)
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **优先级** | P0 |
|
||||
| **预计工作量** | 4天 |
|
||||
| **包含类型** | `ITask`, `LambdaTask`, `TaskGroup`, `TaskSystem`, `TaskSystemConfig`, `Mutex`, `SpinLock`, `ReadWriteLock`, `Thread` |
|
||||
| **功能要求** | 任务调度、依赖管理、并行计算、同步原语 |
|
||||
| **依赖** | Core基础类型 |
|
||||
|
||||
### 1.4 内存管理 (Memory Management)
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **优先级** | P0 |
|
||||
| **预计工作量** | 3天 |
|
||||
| **包含类型** | `IAllocator`, `LinearAllocator`, `PoolAllocator`, `ProxyAllocator`, `MemoryManager` |
|
||||
| **功能要求** | 内存分配、追踪、泄漏检测、线性/池化分配策略 |
|
||||
| **依赖** | 线程系统(ProxyAllocator需要Mutex) |
|
||||
|
||||
### 1.5 容器库 (Containers)
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **优先级** | P0 |
|
||||
| **预计工作量** | 3天 |
|
||||
| **包含类型** | `Array<T>`, `String`, `String::npos`, `HashMap<K,V>` |
|
||||
| **功能要求** | 动态数组、字符串操作(含npos常量)、哈希映射 |
|
||||
| **依赖** | Core基础类型, 内存管理 |
|
||||
|
||||
### 1.6 日志与调试系统
|
||||
|
||||
| 项目 | 内容 |
|
||||
|------|------|
|
||||
| **优先级** | P1 |
|
||||
| **预计工作量** | 2天 |
|
||||
| **包含类型** | `Logger`, `ConsoleLogSink`, `FileLogSink`, `FileWriter`, `Profiler`, `Assert` |
|
||||
| **功能要求** | 分级日志、分类输出、文件写入、性能分析、断言 |
|
||||
| **依赖** | Core基础类型, 容器库(String) |
|
||||
|
||||
---
|
||||
|
||||
## 时间安排
|
||||
|
||||
| 周次 | 内容 |
|
||||
|------|------|
|
||||
| 第1周 | 数学库 + Core基础类型 |
|
||||
| 第2周 | 线程系统 + 内存管理 |
|
||||
| 第3周 | 容器库 + 日志系统 |
|
||||
|
||||
> 注:内存管理依赖线程系统完成(ProxyAllocator需要Mutex),因此调整顺序
|
||||
|
||||
---
|
||||
|
||||
## 测试方案
|
||||
|
||||
### 测试框架
|
||||
- **推荐**: Google Test (gtest) 或 Doctest
|
||||
|
||||
### 测试用例设计
|
||||
|
||||
| 模块 | 测试类别 | 测试用例示例 |
|
||||
|------|---------|-------------|
|
||||
| **Math** | 向量运算 | `Vector3::Dot`, `Cross`, `Normalize`, `Lerp` 精度测试 |
|
||||
| **Math** | 矩阵运算 | `Matrix4x4::TRS`, `LookAt`, `Perspective` 结果正确性 |
|
||||
| **Math** | 四元数 | `FromEulerAngles`, `Slerp`, `ToMatrix4x4` 精度验证 |
|
||||
| **Core** | 引用计数 | `RefCounted` 多线程安全释放 |
|
||||
| **Core** | 事件系统 | 订阅/取消订阅、线程安全调用 |
|
||||
| **Threading** | 任务系统 | 依赖链、优先级、并行For、TaskSystemConfig |
|
||||
| **Threading** | 同步原语 | 锁竞争、死锁检测 |
|
||||
| **Memory** | 分配器 | 边界检查、碎片率、线性分配器回滚测试 |
|
||||
| **Memory** | 泄漏检测 | 分配/释放计数、峰值追踪 |
|
||||
| **Containers** | Array | 边界访问、迭代器、内存增长策略 |
|
||||
| **Containers** | String | 子串、查找、大小写转换 |
|
||||
| **Containers** | HashMap | 冲突处理、负载因子重分布 |
|
||||
| **Logger** | 日志级别 | 过滤、分类、格式化 |
|
||||
|
||||
### 执行命令
|
||||
|
||||
```bash
|
||||
# 编译并运行所有单元测试
|
||||
cmake --build build --target xcengine_tests
|
||||
./build/tests/xcengine_tests.exe
|
||||
|
||||
# 性能基准测试
|
||||
./build/tests/xcengine_tests.exe --benchmark
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 数学库通过全部运算正确性测试
|
||||
- [ ] Core基础类型(引用计数、智能指针)工作正常
|
||||
- [ ] 线程系统在高并发下稳定运行
|
||||
- [ ] 内存分配器无内存泄漏
|
||||
- [ ] 容器操作边界安全
|
||||
- [ ] 日志系统输出格式正确
|
||||
|
||||
---
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
Math Library (无依赖)
|
||||
│
|
||||
├──▶ Core 基础类型 (无依赖)
|
||||
│ │
|
||||
│ ├──▶ Threading (依赖 Core)
|
||||
│ │
|
||||
│ ├──▶ Memory Management (依赖 Threading)
|
||||
│ │ │
|
||||
│ │ └──▶ Containers (依赖 Memory, Core)
|
||||
│ │
|
||||
│ └──▶ Logging & Debug (依赖 Core, Containers)
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,922 +0,0 @@
|
||||
# XCEngine 输入系统设计与实现
|
||||
|
||||
> **文档信息**
|
||||
> - **版本**: 1.1
|
||||
> - **日期**: 2026-03-22
|
||||
> - **状态**: 设计文档
|
||||
> - **目标**: 设计引擎级输入系统
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 设计目标
|
||||
|
||||
XCEngine 输入系统需要提供:
|
||||
1. **统一的跨平台输入抽象** - 支持键盘、鼠标、触摸(手柄暂不考虑)
|
||||
2. **与引擎架构无缝集成** - 使用现有的 `Core::Event` 系统
|
||||
3. **轮询 + 事件混合模式** - 既支持 `IsKeyDown()` 轮询,也支持事件回调
|
||||
4. **UI 系统支持** - 为 UI 组件 (Button, Slider 等) 提供指针事件
|
||||
5. **输入轴支持** - 参考 Unity 的 Input.GetAxis 设计,支持键盘/手柄统一映射
|
||||
|
||||
### 1.2 当前状态分析
|
||||
|
||||
| 模块 | 状态 | 说明 |
|
||||
|-----|------|------|
|
||||
| Core::Event | ✅ 完备 | 线程安全,Subscribe/Unsubscribe 模式 |
|
||||
| RHI::RHISwapChain | ⚠️ PollEvents 空实现 | 需要填充 Windows 消息泵 |
|
||||
| 现有 Input (mvs) | ❌ 耦合 Windows | 直接处理 HWND 消息,不适合引擎架构 |
|
||||
| Platform/Window | ❌ 不存在 | 需要新建 |
|
||||
|
||||
### 1.3 设计决策
|
||||
|
||||
| 问题 | 决策 |
|
||||
|-----|------|
|
||||
| 多窗口 | ❌ 单窗口,简化设计 |
|
||||
| 手柄支持 | ❌ 暂不考虑,预留接口即可 |
|
||||
| 输入轴 | ✅ 参考 Unity 实现 Input.GetAxis |
|
||||
| IME/多语言 | ❌ 暂不考虑,只处理英文字符 |
|
||||
| 模块位置 | ✅ InputModule 放在 Input/ 命名空间 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 架构设计
|
||||
|
||||
### 2.1 模块结构
|
||||
|
||||
```
|
||||
engine/include/XCEngine/
|
||||
├── Core/
|
||||
│ └── Event.h # 已有,复用
|
||||
├── Input/
|
||||
│ ├── InputTypes.h # 枚举和结构体定义
|
||||
│ ├── InputEvent.h # 输入事件结构体
|
||||
│ ├── InputAxis.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. **输入轴映射**
|
||||
- 键盘和手柄可以映射到同一个轴,实现统一输入
|
||||
- 支持在配置文件中定义轴映射关系
|
||||
|
||||
4. **与现有引擎组件集成**
|
||||
- 使用 `Core::Event` 作为事件系统
|
||||
- 使用 `Math::Vector2` 作为 2D 坐标类型
|
||||
|
||||
---
|
||||
|
||||
## 3. 详细设计
|
||||
|
||||
### 3.1 输入类型定义 (`InputTypes.h`)
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#include "Core/Types.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Input {
|
||||
|
||||
enum class KeyCode : uint8 {
|
||||
None = 0,
|
||||
|
||||
// 字母键 A-Z
|
||||
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-F12
|
||||
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,
|
||||
|
||||
// 数字键 0-9
|
||||
Zero = 39, One = 30, Two = 31, Three = 32,
|
||||
Four = 33, Five = 34, Six = 35, Seven = 37,
|
||||
Eight = 38, Nine = 40,
|
||||
|
||||
// 符号键
|
||||
Minus = 43, Equals = 46, BracketLeft = 47, BracketRight = 54,
|
||||
Semicolon = 42, Quote = 40, Comma = 54, Period = 55,
|
||||
Slash = 44, Backslash = 45, Backtick = 41
|
||||
};
|
||||
|
||||
enum class MouseButton : uint8 {
|
||||
Left = 0,
|
||||
Right = 1,
|
||||
Middle = 2,
|
||||
Button4 = 3,
|
||||
Button5 = 4
|
||||
};
|
||||
|
||||
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 输入轴定义 (`InputAxis.h`)
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#include "InputTypes.h"
|
||||
#include "Math/Vector2.h"
|
||||
#include "Containers/String.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Input {
|
||||
|
||||
class InputAxis {
|
||||
public:
|
||||
InputAxis() = default;
|
||||
InputAxis(const Containers::String& name, KeyCode positive, KeyCode negative = KeyCode::None)
|
||||
: m_name(name), m_positiveKey(positive), m_negativeKey(negative) {}
|
||||
|
||||
const Containers::String& GetName() const { return m_name; }
|
||||
KeyCode GetPositiveKey() const { return m_positiveKey; }
|
||||
KeyCode GetNegativeKey() const { return m_negativeKey; }
|
||||
|
||||
void SetKeys(KeyCode positive, KeyCode negative) {
|
||||
m_positiveKey = positive;
|
||||
m_negativeKey = negative;
|
||||
}
|
||||
|
||||
float GetValue() const { return m_value; }
|
||||
void SetValue(float value) { m_value = value; }
|
||||
|
||||
private:
|
||||
Containers::String m_name;
|
||||
KeyCode m_positiveKey = KeyCode::None;
|
||||
KeyCode m_negativeKey = KeyCode::None;
|
||||
float m_value = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace Input
|
||||
} // namespace XCEngine
|
||||
```
|
||||
|
||||
### 3.4 输入管理器 (`InputManager.h`)
|
||||
|
||||
```cpp
|
||||
#pragma once
|
||||
#include "Core/Event.h"
|
||||
#include "InputTypes.h"
|
||||
#include "InputEvent.h"
|
||||
#include "InputAxis.h"
|
||||
#include "Math/Vector2.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Input {
|
||||
|
||||
class InputManager {
|
||||
public:
|
||||
static InputManager& Get();
|
||||
|
||||
void Initialize(void* platformWindowHandle);
|
||||
void Shutdown();
|
||||
void Update(float deltaTime);
|
||||
|
||||
// ============ 轮询接口 ============
|
||||
|
||||
// 键盘
|
||||
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 GetTouchCount() const;
|
||||
TouchState GetTouch(int index) const;
|
||||
|
||||
// ============ 轴接口 (参考 Unity) ============
|
||||
|
||||
float GetAxis(const Containers::String& axisName) const;
|
||||
float GetAxisRaw(const Containers::String& axisName) const;
|
||||
|
||||
bool GetButton(const Containers::String& buttonName) const;
|
||||
bool GetButtonDown(const Containers::String& buttonName) const;
|
||||
bool GetButtonUp(const Containers::String& buttonName) const;
|
||||
|
||||
// 注册轴
|
||||
void RegisterAxis(const InputAxis& axis);
|
||||
void RegisterButton(const Containers::String& name, KeyCode key);
|
||||
void ClearAxes();
|
||||
|
||||
// ============ 事件接口 ============
|
||||
|
||||
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;
|
||||
|
||||
// 触摸状态
|
||||
std::vector<TouchState> m_touches;
|
||||
|
||||
// 轴映射
|
||||
std::unordered_map<Containers::String, InputAxis> m_axes;
|
||||
std::unordered_map<Containers::String, KeyCode> m_buttons;
|
||||
std::vector<bool> m_buttonDownThisFrame;
|
||||
std::vector<bool> m_buttonDownLastFrame;
|
||||
|
||||
// 事件
|
||||
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.5 平台输入模块接口 (`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.6 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;
|
||||
|
||||
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);
|
||||
|
||||
KeyCode VKCodeToKeyCode(int vkCode);
|
||||
|
||||
HWND m_hwnd = nullptr;
|
||||
Math::Vector2 m_lastMousePosition;
|
||||
bool m_captureMouse = false;
|
||||
bool m_isInitialized = false;
|
||||
};
|
||||
|
||||
} // namespace Platform
|
||||
} // namespace Input
|
||||
} // namespace XCEngine
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. InputManager 实现细节
|
||||
|
||||
### 4.1 状态追踪机制
|
||||
|
||||
```
|
||||
每帧 Update() 调用时:
|
||||
1. m_keyDownLastFrame = m_keyDownThisFrame
|
||||
2. 处理 m_keyDownThisFrame = m_keyDown (当前帧按下状态)
|
||||
3. 清零 m_mouseDelta, m_mouseScrollDelta
|
||||
|
||||
IsKeyPressed(key) = m_keyDownThisFrame[key] && !m_keyDownLastFrame[key] // 当前帧按下
|
||||
IsKeyDown(key) = m_keyDown[key] // 当前正按下
|
||||
IsKeyUp(key) = !m_keyDown[key] // 当前已释放
|
||||
```
|
||||
|
||||
### 4.2 轴值计算
|
||||
|
||||
```cpp
|
||||
float InputManager::GetAxis(const Containers::String& axisName) const {
|
||||
auto it = m_axes.find(axisName);
|
||||
if (it == m_axes.end()) return 0.0f;
|
||||
|
||||
const auto& axis = it->second;
|
||||
float value = 0.0f;
|
||||
|
||||
if (axis.GetPositiveKey() != KeyCode::None && IsKeyDown(axis.GetPositiveKey())) {
|
||||
value += 1.0f;
|
||||
}
|
||||
if (axis.GetNegativeKey() != KeyCode::None && IsKeyDown(axis.GetNegativeKey())) {
|
||||
value -= 1.0f;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
float InputManager::GetAxisRaw(const Containers::String& axisName) const {
|
||||
auto it = m_axes.find(axisName);
|
||||
if (it == m_axes.end()) return 0.0f;
|
||||
|
||||
const auto& axis = it->second;
|
||||
float value = 0.0f;
|
||||
|
||||
if (axis.GetPositiveKey() != KeyCode::None && IsKeyPressed(axis.GetPositiveKey())) {
|
||||
value += 1.0f;
|
||||
}
|
||||
if (axis.GetNegativeKey() != KeyCode::None && IsKeyPressed(axis.GetNegativeKey())) {
|
||||
value -= 1.0f;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 默认轴配置
|
||||
|
||||
```cpp
|
||||
void InputManager::Initialize(void* platformWindowHandle) {
|
||||
// 注册默认轴 (类似 Unity)
|
||||
RegisterAxis(InputAxis("Horizontal", KeyCode::D, KeyCode::A));
|
||||
RegisterAxis(InputAxis("Vertical", KeyCode::W, KeyCode::S));
|
||||
RegisterAxis(InputAxis("Mouse X", KeyCode::None, KeyCode::None)); // 鼠标驱动
|
||||
RegisterAxis(InputAxis("Mouse Y", KeyCode::None, KeyCode::None)); // 鼠标驱动
|
||||
|
||||
// 注册默认按钮
|
||||
RegisterButton("Jump", KeyCode::Space);
|
||||
RegisterButton("Fire1", KeyCode::LeftCtrl);
|
||||
RegisterButton("Fire2", KeyCode::LeftAlt);
|
||||
RegisterButton("Fire3", KeyCode::LeftShift);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Windows 消息到引擎事件的映射
|
||||
|
||||
### 5.1 消息映射表
|
||||
|
||||
| Windows Message | Engine Method | 说明 |
|
||||
|----------------|---------------|------|
|
||||
| WM_KEYDOWN | `ProcessKeyDown` | 按键按下,wParam=VK码 |
|
||||
| WM_KEYUP | `ProcessKeyUp` | 按键释放,wParam=VK码 |
|
||||
| WM_CHAR | `ProcessTextInput` | 字符输入,wParam=字符 |
|
||||
| WM_MOUSEMOVE | `ProcessMouseMove` | 鼠标移动 |
|
||||
| WM_LBUTTONDOWN | `ProcessMouseButton(Left, Pressed)` | 左键按下 |
|
||||
| WM_LBUTTONUP | `ProcessMouseButton(Left, Released)` | 左键释放 |
|
||||
| WM_RBUTTONDOWN | `ProcessMouseButton(Right, Pressed)` | 右键按下 |
|
||||
| WM_RBUTTONUP | `ProcessMouseButton(Right, Released)` | 右键释放 |
|
||||
| WM_MBUTTONDOWN | `ProcessMouseButton(Middle, Pressed)` | 中键按下 |
|
||||
| WM_MBUTTONUP | `ProcessMouseButton(Middle, Released)` | 中键释放 |
|
||||
| WM_MOUSEWHEEL | `ProcessMouseWheel` | 滚轮滚动 |
|
||||
| WM_XBUTTONDOWN | `ProcessMouseButton(Button4/5, Pressed)` | 侧键按下 |
|
||||
| WM_XBUTTONUP | `ProcessMouseButton(Button4/5, Released)` | 侧键释放 |
|
||||
|
||||
### 5.2 VK 码到 KeyCode 映射
|
||||
|
||||
```cpp
|
||||
KeyCode WindowsInputModule::VKCodeToKeyCode(int vkCode) {
|
||||
switch (vkCode) {
|
||||
case 'A': return KeyCode::A;
|
||||
case 'B': return KeyCode::B;
|
||||
case 'C': return KeyCode::C;
|
||||
// ... Z
|
||||
case VK_SPACE: return KeyCode::Space;
|
||||
case VK_TAB: return KeyCode::Tab;
|
||||
case VK_RETURN: return KeyCode::Enter;
|
||||
case VK_ESCAPE: return KeyCode::Escape;
|
||||
case VK_SHIFT: return KeyCode::LeftShift;
|
||||
case VK_CONTROL: return KeyCode::LeftCtrl;
|
||||
case VK_MENU: return KeyCode::LeftAlt;
|
||||
case VK_UP: return KeyCode::Up;
|
||||
case VK_DOWN: return KeyCode::Down;
|
||||
case VK_LEFT: return KeyCode::Left;
|
||||
case VK_RIGHT: return KeyCode::Right;
|
||||
case VK_HOME: return KeyCode::Home;
|
||||
case VK_END: return KeyCode::End;
|
||||
case VK_PRIOR: return KeyCode::PageUp;
|
||||
case VK_NEXT: return KeyCode::PageDown;
|
||||
case VK_DELETE: return KeyCode::Delete;
|
||||
case VK_BACK: return KeyCode::Backspace;
|
||||
case VK_F1: return KeyCode::F1;
|
||||
case VK_F2: return KeyCode::F2;
|
||||
// ... F12
|
||||
case '0': return KeyCode::Zero;
|
||||
case '1': return KeyCode::One;
|
||||
// ... 9
|
||||
case VK_OEM_MINUS: return KeyCode::Minus;
|
||||
case VK_OEM_PLUS: return KeyCode::Equals;
|
||||
// ... 其他 OEM 键
|
||||
default: return KeyCode::None;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 与引擎其他模块的集成
|
||||
|
||||
### 6.1 与 RHI/SwapChain 的集成
|
||||
|
||||
`RHISwapChain::PollEvents()` 需要调用 `InputModule::PumpEvents()` 和 `InputManager::Update()`:
|
||||
|
||||
```cpp
|
||||
// D3D12SwapChain.cpp
|
||||
void D3D12SwapChain::PollEvents() {
|
||||
// 抽取 Windows 消息
|
||||
if (m_inputModule) {
|
||||
m_inputModule->PumpEvents();
|
||||
}
|
||||
|
||||
// 处理其他 Windows 消息(关闭请求等)
|
||||
MSG msg;
|
||||
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
|
||||
if (msg.message == WM_QUIT) {
|
||||
m_shouldClose = true;
|
||||
break;
|
||||
}
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
}
|
||||
|
||||
// 在 GameLoop 中每帧调用
|
||||
void GameLoop::Update(float deltaTime) {
|
||||
// 更新输入状态(计算 IsKeyPressed 等)
|
||||
Input::InputManager::Get().Update(deltaTime);
|
||||
|
||||
// 更新场景和组件
|
||||
m_currentScene->Update(deltaTime);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 与 UI 系统的集成
|
||||
|
||||
UI 组件通过订阅 `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 (!m_isHovered) {
|
||||
m_isHovered = true;
|
||||
OnPointerEnter.Invoke();
|
||||
}
|
||||
|
||||
// 检测点击
|
||||
if (input.IsMouseButtonClicked(Input::MouseButton::Left)) {
|
||||
OnClick.Invoke();
|
||||
}
|
||||
|
||||
// 检测按下/释放
|
||||
if (input.IsMouseButtonDown(Input::MouseButton::Left)) {
|
||||
OnPointerDown.Invoke();
|
||||
} else if (m_wasMouseDown && !input.IsMouseButtonDown(Input::MouseButton::Left)) {
|
||||
OnPointerUp.Invoke();
|
||||
}
|
||||
} else if (m_isHovered) {
|
||||
m_isHovered = false;
|
||||
OnPointerExit.Invoke();
|
||||
}
|
||||
|
||||
m_wasMouseDown = input.IsMouseButtonDown(Input::MouseButton::Left);
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 与场景生命周期的集成
|
||||
|
||||
```cpp
|
||||
// Scene.cpp
|
||||
void Scene::Awake() {
|
||||
// 获取窗口句柄并初始化输入系统
|
||||
void* windowHandle = GetEngine()->GetWindowHandle();
|
||||
Input::InputManager::Get().Initialize(windowHandle);
|
||||
}
|
||||
|
||||
void Scene::OnDestroy() {
|
||||
Input::InputManager::Get().Shutdown();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 使用示例
|
||||
|
||||
### 7.1 轮询模式(角色移动)
|
||||
|
||||
```cpp
|
||||
void PlayerController::Update(float deltaTime) {
|
||||
auto& input = Input::InputManager::Get();
|
||||
|
||||
// 使用轴 (推荐方式,兼容手柄)
|
||||
float horizontal = input.GetAxis("Horizontal");
|
||||
float vertical = input.GetAxis("Vertical");
|
||||
|
||||
m_velocity.x = horizontal * m_moveSpeed;
|
||||
m_velocity.z = vertical * m_moveSpeed;
|
||||
|
||||
// 或者使用原始轴(无平滑)
|
||||
float rawH = input.GetAxisRaw("Horizontal");
|
||||
|
||||
// 直接轮询也可以
|
||||
if (input.IsKeyDown(Input::KeyCode::W)) {
|
||||
m_velocity.z = 1.0f;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 事件模式(UI 交互)
|
||||
|
||||
```cpp
|
||||
class MyUIButton : public Component {
|
||||
uint64_t m_clickHandlerId = 0;
|
||||
uint64_t m_hoverHandlerId = 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();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 订阅鼠标移动事件用于悬停检测
|
||||
m_hoverHandlerId = Input::InputManager::Get().OnMouseMove().Subscribe(
|
||||
[this](const Input::MouseMoveEvent& event) {
|
||||
bool isHovering = IsPointInButton(event.position);
|
||||
if (isHovering && !m_isHovered) {
|
||||
m_isHovered = true;
|
||||
OnPointerEnter.Invoke();
|
||||
} else if (!isHovering && m_isHovered) {
|
||||
m_isHovered = false;
|
||||
OnPointerExit.Invoke();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void OnDestroy() override {
|
||||
Input::InputManager::Get().OnMouseButton().Unsubscribe(m_clickHandlerId);
|
||||
Input::InputManager::Get().OnMouseMove().Unsubscribe(m_hoverHandlerId);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 7.3 文本输入
|
||||
|
||||
```cpp
|
||||
void InputFieldComponent::Update(float deltaTime) {
|
||||
static uint64_t textHandlerId = 0;
|
||||
|
||||
if (m_isFocused) {
|
||||
if (textHandlerId == 0) {
|
||||
textHandlerId = Input::InputManager::Get().OnTextInput().Subscribe(
|
||||
[this](const Input::TextInputEvent& event) {
|
||||
if (event.character == '\b') { // Backspace
|
||||
if (!m_text.empty()) {
|
||||
m_text.pop_back();
|
||||
}
|
||||
} else if (event.character == '\r') { // Enter
|
||||
OnSubmit.Invoke();
|
||||
} else if (isprint(event.character)) {
|
||||
if (m_characterLimit == 0 || m_text.length() < m_characterLimit) {
|
||||
m_text += event.character;
|
||||
OnValueChanged.Invoke(m_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
} else if (textHandlerId != 0) {
|
||||
Input::InputManager::Get().OnTextInput().Unsubscribe(textHandlerId);
|
||||
textHandlerId = 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.4 射击游戏开火
|
||||
|
||||
```cpp
|
||||
void WeaponComponent::Update(float deltaTime) {
|
||||
auto& input = Input::InputManager::Get();
|
||||
|
||||
// 方式1: 按钮按下检测
|
||||
if (input.GetButtonDown("Fire1")) {
|
||||
Fire();
|
||||
}
|
||||
|
||||
// 方式2: 轮询检测(适合连发)
|
||||
if (input.GetButton("Fire1") && m_fireRateTimer <= 0) {
|
||||
Fire();
|
||||
m_fireRateTimer = m_fireRate;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 实现计划
|
||||
|
||||
### Phase 1: 核心输入系统
|
||||
|
||||
| 任务 | 文件 | 优先级 |
|
||||
|-----|------|-------|
|
||||
| 创建目录 | `engine/include/XCEngine/Input/` | P0 |
|
||||
| 实现 InputTypes.h | KeyCode, MouseButton 枚举 | P0 |
|
||||
| 实现 InputEvent.h | 事件结构体 | P0 |
|
||||
| 实现 InputAxis.h | 输入轴定义 | P1 |
|
||||
| 实现 InputManager.h/cpp | 单例管理器 | P0 |
|
||||
| 实现 InputModule.h | 平台输入模块接口 | P0 |
|
||||
| 实现 WindowsInputModule.h/cpp | Windows 平台实现 | P0 |
|
||||
|
||||
### Phase 2: 与 RHI 集成
|
||||
|
||||
| 任务 | 文件 | 优先级 |
|
||||
|-----|------|-------|
|
||||
| 修改 RHISwapChain | 添加 InputModule 成员 | P0 |
|
||||
| 实现 D3D12SwapChain::PollEvents | 填充消息泵逻辑 | P0 |
|
||||
| 实现 OpenGLSwapChain::PollEvents | GL 消息处理 | P1 |
|
||||
|
||||
### Phase 3: 轴系统完善
|
||||
|
||||
| 任务 | 说明 | 优先级 |
|
||||
|-----|------|-------|
|
||||
| 默认轴配置 | 注册 Horizontal, Vertical 等默认轴 | P1 |
|
||||
| 轴平滑处理 | GetAxis 的平滑插值 | P2 |
|
||||
| 配置文件支持 | 从 JSON/配置加载轴映射 | P2 |
|
||||
|
||||
### Phase 4: UI 输入支持
|
||||
|
||||
| 任务 | 说明 | 优先级 |
|
||||
|-----|------|-------|
|
||||
| Canvas 射线检测 | 将屏幕坐标转换为 UI 元素 | P1 |
|
||||
| Button 事件 | OnClick, OnPointerDown/Up/Enter/Exit | P1 |
|
||||
| Slider 拖拽 | 使用鼠标事件实现滑块拖拽 | P1 |
|
||||
| InputField 文本输入 | 接收 TextInputEvent | P1 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 测试策略
|
||||
|
||||
### 9.1 单元测试
|
||||
|
||||
```cpp
|
||||
// test_input_manager.cpp
|
||||
TEST(InputManager, KeyDownDetection) {
|
||||
InputManager::Get().Initialize(nullptr);
|
||||
|
||||
// 模拟按下 W 键
|
||||
InputManager::Get().ProcessKeyDown(KeyCode::W, false);
|
||||
|
||||
EXPECT_TRUE(InputManager::Get().IsKeyDown(KeyCode::W));
|
||||
EXPECT_TRUE(InputManager::Get().IsKeyPressed(KeyCode::W)); // 第一帧按下
|
||||
EXPECT_FALSE(InputManager::Get().IsKeyUp(KeyCode::W));
|
||||
|
||||
// 模拟释放
|
||||
InputManager::Get().ProcessKeyUp(KeyCode::W);
|
||||
|
||||
EXPECT_FALSE(InputManager::Get().IsKeyDown(KeyCode::W));
|
||||
EXPECT_TRUE(InputManager::Get().IsKeyUp(KeyCode::W));
|
||||
|
||||
InputManager::Get().Shutdown();
|
||||
}
|
||||
|
||||
TEST(InputManager, AxisRegistration) {
|
||||
InputManager::Get().Initialize(nullptr);
|
||||
|
||||
InputManager::Get().RegisterAxis(InputAxis("TestAxis", KeyCode::W, KeyCode::S));
|
||||
|
||||
EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), 0.0f);
|
||||
|
||||
InputManager::Get().ProcessKeyDown(KeyCode::W, false);
|
||||
EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), 1.0f);
|
||||
|
||||
InputManager::Get().ProcessKeyUp(KeyCode::W);
|
||||
InputManager::Get().ProcessKeyDown(KeyCode::S, false);
|
||||
EXPECT_EQ(InputManager::Get().GetAxis("TestAxis"), -1.0f);
|
||||
|
||||
InputManager::Get().Shutdown();
|
||||
}
|
||||
|
||||
TEST(InputManager, MousePositionTracking) {
|
||||
InputManager::Get().Initialize(nullptr);
|
||||
|
||||
InputManager::Get().ProcessMouseMove(100, 200, 0, 0);
|
||||
EXPECT_EQ(InputManager::Get().GetMousePosition(), Math::Vector2(100, 200));
|
||||
|
||||
InputManager::Get().ProcessMouseMove(105, 205, 5, 5);
|
||||
EXPECT_EQ(InputManager::Get().GetMouseDelta(), Math::Vector2(5, 5));
|
||||
|
||||
InputManager::Get().Shutdown();
|
||||
}
|
||||
```
|
||||
|
||||
### 9.2 集成测试
|
||||
|
||||
- 测试输入系统与 SwapChain 的集成
|
||||
- 测试输入系统与 Scene 的集成
|
||||
- 测试 UI Button 点击响应
|
||||
|
||||
---
|
||||
|
||||
## 10. 文件清单
|
||||
|
||||
### 头文件
|
||||
|
||||
```
|
||||
engine/include/XCEngine/Input/
|
||||
├── InputTypes.h # 48 行
|
||||
├── InputEvent.h # 78 行
|
||||
├── InputAxis.h # 45 行
|
||||
├── InputManager.h # 130 行
|
||||
└── InputModule.h # 20 行
|
||||
|
||||
engine/include/XCEngine/Platform/
|
||||
├── PlatformTypes.h # (新建)
|
||||
├── Window.h # (新建)
|
||||
└── Windows/
|
||||
└── WindowsInputModule.h # 55 行
|
||||
```
|
||||
|
||||
### 源文件
|
||||
|
||||
```
|
||||
engine/src/Input/
|
||||
├── InputManager.cpp # 180 行
|
||||
└── Platform/
|
||||
└── Windows/
|
||||
└── WindowsInputModule.cpp # 200 行
|
||||
```
|
||||
|
||||
### 测试文件
|
||||
|
||||
```
|
||||
tests/Input/
|
||||
├── test_input_types.cpp # KeyCode/MouseButton 枚举测试
|
||||
├── test_input_event.cpp # 事件结构体测试
|
||||
├── test_input_axis.cpp # 轴注册和值计算测试
|
||||
└── test_input_manager.cpp # 核心功能测试
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 设计原则总结
|
||||
|
||||
1. **复用 Core::Event** - 使用现有的线程安全事件系统
|
||||
2. **平台抽象** - InputModule 接口隔离 Windows 消息处理
|
||||
3. **双模式支持** - 轮询 + 事件,兼顾性能和响应性
|
||||
4. **轴系统** - 参考 Unity,支持键盘/手柄统一输入映射
|
||||
5. **单窗口** - 简化设计,满足当前需求
|
||||
6. **UI 协同** - 为 Canvas/Button 提供完整的事件支持
|
||||
Reference in New Issue
Block a user