重构UI架构:分离数据模型和显示逻辑

- 新增 Core 模块:GameObject、LogEntry、AssetItem 数据模型
- 新增 Managers 模块:SceneManager、LogSystem、ProjectManager
- Panel 层只负责显示,不再持有数据
- 解耦 HierarchyPanel 和 InspectorPanel 之间的直接依赖
This commit is contained in:
2026-03-12 16:13:34 +08:00
parent 7d3e3b464d
commit a2f3db8718
19 changed files with 306 additions and 137 deletions

View File

@@ -32,6 +32,9 @@ add_executable(${PROJECT_NAME} WIN32
src/main.cpp
src/Application.cpp
src/Theme.cpp
src/Managers/SceneManager.cpp
src/Managers/LogSystem.cpp
src/Managers/ProjectManager.cpp
src/panels/Panel.cpp
src/panels/MenuBar.cpp
src/panels/HierarchyPanel.cpp

View File

@@ -48,8 +48,6 @@ bool Application::Initialize(HWND hwnd) {
m_consolePanel = std::make_unique<ConsolePanel>();
m_projectPanel = std::make_unique<ProjectPanel>();
m_inspectorPanel->SetHierarchyPanel(m_hierarchyPanel.get());
return true;
}

13
ui/src/Core/AssetItem.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace UI {
struct AssetItem {
std::string name;
std::string type;
bool isFolder;
};
}

61
ui/src/Core/GameObject.h Normal file
View File

@@ -0,0 +1,61 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
namespace UI {
class Component {
public:
virtual ~Component() = default;
virtual std::string GetName() const = 0;
};
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 GameObject {
public:
std::string name;
std::vector<GameObject> children;
std::vector<std::unique_ptr<Component>> components;
bool selected = false;
GameObject() = default;
explicit GameObject(const std::string& n) : name(n) {}
template<typename T, typename... Args>
T* AddComponent(Args&&... args) {
auto comp = std::make_unique<T>(std::forward<Args>(args)...);
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;
}
};
}

13
ui/src/Core/LogEntry.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <string>
namespace UI {
struct LogEntry {
enum class Level { Info, Warning, Error };
Level level;
std::string message;
};
}

View File

@@ -0,0 +1,19 @@
#include "LogSystem.h"
namespace UI {
LogSystem& LogSystem::Get() {
static LogSystem instance;
return instance;
}
void LogSystem::AddLog(LogEntry::Level level, const std::string& message) {
m_logs.push_back({level, message});
if (m_callback) m_callback();
}
void LogSystem::Clear() {
m_logs.clear();
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "Core/LogEntry.h"
#include <vector>
#include <functional>
namespace UI {
class LogSystem {
public:
static LogSystem& Get();
void AddLog(LogEntry::Level level, const std::string& message);
void Clear();
const std::vector<LogEntry>& GetLogs() const { return m_logs; }
void SetCallback(std::function<void()> callback) { m_callback = callback; }
private:
LogSystem() = default;
std::vector<LogEntry> m_logs;
std::function<void()> m_callback;
};
}

View File

@@ -0,0 +1,23 @@
#include "ProjectManager.h"
namespace UI {
ProjectManager& ProjectManager::Get() {
static ProjectManager instance;
return instance;
}
void ProjectManager::CreateDemoAssets() {
m_items = {
{"Cube", "Prefab", false},
{"Sphere", "Prefab", false},
{"Player", "Prefab", false},
{"MainScript", "Script", false},
{"DefaultMat", "Material", false},
{"Scene1", "Scene", false},
{"Textures", "Folder", true},
{"Models", "Folder", true},
};
}
}

View File

@@ -0,0 +1,26 @@
#pragma once
#include "Core/AssetItem.h"
#include <vector>
#include <string>
namespace UI {
class ProjectManager {
public:
static ProjectManager& Get();
std::vector<AssetItem>& GetItems() { return m_items; }
int GetSelectedIndex() const { return m_selectedIndex; }
void SetSelectedIndex(int index) { m_selectedIndex = index; }
void CreateDemoAssets();
private:
ProjectManager() = default;
std::vector<AssetItem> m_items;
int m_selectedIndex = -1;
};
}

View File

@@ -0,0 +1,47 @@
#include "SceneManager.h"
namespace UI {
SceneManager& SceneManager::Get() {
static SceneManager instance;
return instance;
}
void SceneManager::SetSelectedObject(GameObject* obj) {
if (m_selectedObject) {
m_selectedObject->selected = false;
}
m_selectedObject = obj;
if (m_selectedObject) {
m_selectedObject->selected = true;
}
}
void SceneManager::CreateDemoScene() {
m_rootObjects.clear();
GameObject mainCamera("Main Camera");
mainCamera.AddComponent<TransformComponent>();
GameObject directionalLight("Directional Light");
GameObject cube("Cube");
cube.AddComponent<TransformComponent>();
cube.AddComponent<MeshRendererComponent>()->meshName = "Cube Mesh";
GameObject sphere("Sphere");
sphere.AddComponent<TransformComponent>();
sphere.AddComponent<MeshRendererComponent>()->meshName = "Sphere Mesh";
GameObject player("Player");
GameObject weapon("Weapon");
player.children.push_back(std::move(weapon));
m_rootObjects.push_back(std::move(mainCamera));
m_rootObjects.push_back(std::move(directionalLight));
m_rootObjects.push_back(std::move(cube));
m_rootObjects.push_back(std::move(sphere));
m_rootObjects.push_back(std::move(player));
}
}

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Core/GameObject.h"
#include <vector>
namespace UI {
class SceneManager {
public:
static SceneManager& Get();
std::vector<GameObject>& GetRootObjects() { return m_rootObjects; }
GameObject* GetSelectedObject() { return m_selectedObject; }
void SetSelectedObject(GameObject* obj);
void CreateDemoScene();
private:
SceneManager() = default;
std::vector<GameObject> m_rootObjects;
GameObject* m_selectedObject = nullptr;
};
}

View File

@@ -1,40 +1,42 @@
#include "ConsolePanel.h"
#include "Managers/LogSystem.h"
#include "Core/LogEntry.h"
#include <imgui.h>
namespace UI {
ConsolePanel::ConsolePanel() : Panel("Console") {
m_logs.push_back({LogEntry::Level::Info, "Engine initialized successfully"});
m_logs.push_back({LogEntry::Level::Info, "Loading default scene..."});
m_logs.push_back({LogEntry::Level::Warning, "Missing material on object 'Cube'"});
m_logs.push_back({LogEntry::Level::Error, "Failed to load texture: 'Assets/Textures/missing.png'"});
m_logs.push_back({LogEntry::Level::Info, "Scene loaded successfully"});
LogSystem::Get().AddLog(LogEntry::Level::Info, "Engine initialized successfully");
LogSystem::Get().AddLog(LogEntry::Level::Info, "Loading default scene...");
LogSystem::Get().AddLog(LogEntry::Level::Warning, "Missing material on object 'Cube'");
LogSystem::Get().AddLog(LogEntry::Level::Error, "Failed to load texture: 'Assets/Textures/missing.png'");
LogSystem::Get().AddLog(LogEntry::Level::Info, "Scene loaded successfully");
}
void ConsolePanel::Render() {
ImGui::Begin(m_name.c_str(), &m_isOpen, ImGuiWindowFlags_None);
if (ImGui::Button("Clear")) {
Clear();
LogSystem::Get().Clear();
}
ImGui::SameLine();
if (ImGui::Button("Info")) {
AddLog(LogEntry::Level::Info, "Test info message");
LogSystem::Get().AddLog(LogEntry::Level::Info, "Test info message");
}
ImGui::SameLine();
if (ImGui::Button("Warn")) {
AddLog(LogEntry::Level::Warning, "Test warning message");
LogSystem::Get().AddLog(LogEntry::Level::Warning, "Test warning message");
}
ImGui::SameLine();
if (ImGui::Button("Error")) {
AddLog(LogEntry::Level::Error, "Test error message");
LogSystem::Get().AddLog(LogEntry::Level::Error, "Test error message");
}
ImGui::Separator();
ImGui::BeginChild("LogScroll", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar);
for (const auto& log : m_logs) {
for (const auto& log : LogSystem::Get().GetLogs()) {
ImVec4 color;
const char* prefix;
@@ -65,13 +67,4 @@ void ConsolePanel::Render() {
ImGui::End();
}
void ConsolePanel::AddLog(LogEntry::Level level, const std::string& message) {
m_logs.push_back({level, message});
m_scrollToBottom = true;
}
void ConsolePanel::Clear() {
m_logs.clear();
}
}

View File

@@ -1,27 +1,15 @@
#pragma once
#include "Panel.h"
#include <vector>
#include <string>
namespace UI {
struct LogEntry {
enum class Level { Info, Warning, Error };
Level level;
std::string message;
};
class ConsolePanel : public Panel {
public:
ConsolePanel();
void Render() override;
void AddLog(LogEntry::Level level, const std::string& message);
private:
void Clear();
std::vector<LogEntry> m_logs;
bool m_scrollToBottom = false;
};

View File

@@ -1,42 +1,18 @@
#include "HierarchyPanel.h"
#include "Managers/SceneManager.h"
#include "Core/GameObject.h"
#include <imgui.h>
namespace UI {
HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") {
InitDemoData();
}
void HierarchyPanel::InitDemoData() {
GameObject mainCamera;
mainCamera.name = "Main Camera";
GameObject directionalLight;
directionalLight.name = "Directional Light";
GameObject cube;
cube.name = "Cube";
GameObject sphere;
sphere.name = "Sphere";
GameObject player;
player.name = "Player";
GameObject weapon;
weapon.name = "Weapon";
player.children.push_back(weapon);
m_rootObjects.push_back(mainCamera);
m_rootObjects.push_back(directionalLight);
m_rootObjects.push_back(cube);
m_rootObjects.push_back(sphere);
m_rootObjects.push_back(player);
SceneManager::Get().CreateDemoScene();
}
void HierarchyPanel::Render() {
ImGui::Begin(m_name.c_str(), &m_isOpen, ImGuiWindowFlags_None);
for (auto& obj : m_rootObjects) {
for (auto& obj : SceneManager::Get().GetRootObjects()) {
RenderGameObject(obj);
}
@@ -57,11 +33,7 @@ void HierarchyPanel::RenderGameObject(GameObject& obj) {
bool isOpen = ImGui::TreeNodeEx(obj.name.c_str(), flags);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
if (m_selectedObject) {
m_selectedObject->selected = false;
}
obj.selected = true;
m_selectedObject = &obj;
SceneManager::Get().SetSelectedObject(&obj);
}
if (isOpen) {

View File

@@ -1,30 +1,16 @@
#pragma once
#include "Panel.h"
#include <vector>
#include <string>
namespace UI {
struct GameObject {
std::string name;
std::vector<GameObject> children;
bool isOpen = false;
bool selected = false;
};
class HierarchyPanel : public Panel {
public:
HierarchyPanel();
void Render() override;
GameObject* GetSelectedObject() { return m_selectedObject; }
private:
void RenderGameObject(GameObject& obj);
void InitDemoData();
std::vector<GameObject> m_rootObjects;
GameObject* m_selectedObject = nullptr;
void RenderGameObject(class GameObject& obj);
};
}

View File

@@ -1,4 +1,6 @@
#include "InspectorPanel.h"
#include "Managers/SceneManager.h"
#include "Core/GameObject.h"
#include <imgui.h>
namespace UI {
@@ -8,18 +10,22 @@ InspectorPanel::InspectorPanel() : Panel("Inspector") {}
void InspectorPanel::Render() {
ImGui::Begin(m_name.c_str(), &m_isOpen, ImGuiWindowFlags_None);
GameObject* selected = nullptr;
if (m_hierarchyPanel) {
selected = m_hierarchyPanel->GetSelectedObject();
}
GameObject* selected = SceneManager::Get().GetSelectedObject();
if (selected) {
ImGui::Text("%s", selected->name.c_str());
ImGui::Separator();
RenderTransformSection();
ImGui::Separator();
RenderMeshRendererSection();
auto transform = selected->GetComponent<TransformComponent>();
if (transform) {
RenderTransformSection(transform);
ImGui::Separator();
}
auto meshRenderer = selected->GetComponent<MeshRendererComponent>();
if (meshRenderer) {
RenderMeshRendererSection(meshRenderer);
}
} else {
ImGui::Text("No object selected");
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Select an object in Hierarchy");
@@ -28,44 +34,42 @@ void InspectorPanel::Render() {
ImGui::End();
}
void InspectorPanel::RenderTransformSection() {
void InspectorPanel::RenderTransformSection(TransformComponent* transform) {
if (ImGui::CollapsingHeader("Transform", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
ImGui::Text("Position");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::DragFloat3("##Position", m_position, 0.1f);
ImGui::DragFloat3("##Position", transform->position, 0.1f);
ImGui::Text("Rotation");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::DragFloat3("##Rotation", m_rotation, 1.0f);
ImGui::DragFloat3("##Rotation", transform->rotation, 1.0f);
ImGui::Text("Scale");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::DragFloat3("##Scale", m_scale, 0.1f);
ImGui::DragFloat3("##Scale", transform->scale, 0.1f);
ImGui::Unindent(10.0f);
}
}
void InspectorPanel::RenderMeshRendererSection() {
void InspectorPanel::RenderMeshRendererSection(MeshRendererComponent* meshRenderer) {
if (ImGui::CollapsingHeader("Mesh Renderer", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
ImGui::Text("Material");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
static char materialName[64] = "Default-Material";
ImGui::InputText("##Material", materialName, sizeof(materialName));
ImGui::InputText("##Material", meshRenderer->materialName.data(), meshRenderer->materialName.capacity());
ImGui::Text("Mesh");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
static char meshName[64] = "Cube Mesh";
ImGui::InputText("##Mesh", meshName, sizeof(meshName));
ImGui::InputText("##Mesh", meshRenderer->meshName.data(), meshRenderer->meshName.capacity());
ImGui::Unindent(10.0f);
}

View File

@@ -1,7 +1,6 @@
#pragma once
#include "Panel.h"
#include "HierarchyPanel.h"
namespace UI {
@@ -9,17 +8,10 @@ class InspectorPanel : public Panel {
public:
InspectorPanel();
void Render() override;
void SetHierarchyPanel(HierarchyPanel* panel) { m_hierarchyPanel = panel; }
private:
void RenderTransformSection();
void RenderMeshRendererSection();
HierarchyPanel* m_hierarchyPanel = nullptr;
float m_position[3] = {0.0f, 1.0f, -10.0f};
float m_rotation[3] = {0.0f, 0.0f, 0.0f};
float m_scale[3] = {1.0f, 1.0f, 1.0f};
void RenderTransformSection(class TransformComponent* transform);
void RenderMeshRendererSection(class MeshRendererComponent* meshRenderer);
};
}

View File

@@ -1,22 +1,13 @@
#include "ProjectPanel.h"
#include "Managers/ProjectManager.h"
#include "Core/AssetItem.h"
#include <imgui.h>
#include <imgui_internal.h>
namespace UI {
ProjectPanel::ProjectPanel() : Panel("Project") {
m_folders = {"Assets", "Scenes", "Scripts", "Materials", "Prefabs"};
m_items = {
{"Cube", "Prefab", false},
{"Sphere", "Prefab", false},
{"Player", "Prefab", false},
{"MainScript", "Script", false},
{"DefaultMat", "Material", false},
{"Scene1", "Scene", false},
{"Textures", "Folder", true},
{"Models", "Folder", true},
};
ProjectManager::Get().CreateDemoAssets();
}
void ProjectPanel::Render() {
@@ -37,11 +28,12 @@ void ProjectPanel::Render() {
int columns = (int)(panelWidth / (buttonWidth + padding));
if (columns < 1) columns = 1;
for (int i = 0; i < m_items.size(); i++) {
auto& items = ProjectManager::Get().GetItems();
for (int i = 0; i < items.size(); i++) {
if (i > 0 && i % columns != 0) {
ImGui::SameLine();
}
RenderAssetItem(m_items[i], i);
RenderAssetItem(items[i], i);
}
ImGui::EndChild();
@@ -51,14 +43,14 @@ void ProjectPanel::Render() {
void ProjectPanel::RenderAssetItem(const AssetItem& item, int index) {
ImGui::PushID(index);
bool isSelected = (m_selectedIndex == index);
bool isSelected = (ProjectManager::Get().GetSelectedIndex() == index);
if (isSelected) {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.26f, 0.59f, 0.98f, 0.40f));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.40f, 0.40f, 0.40f, 0.50f));
}
ImVec2 buttonSize(80.0f, 90.0f);
if (ImGui::Button("##Asset", buttonSize)) {
m_selectedIndex = index;
ProjectManager::Get().SetSelectedIndex(index);
}
if (isSelected) {

View File

@@ -1,28 +1,16 @@
#pragma once
#include "Panel.h"
#include <vector>
#include <string>
namespace UI {
struct AssetItem {
std::string name;
std::string type;
bool isFolder;
};
class ProjectPanel : public Panel {
public:
ProjectPanel();
void Render() override;
private:
void RenderAssetItem(const AssetItem& item, int index);
std::vector<std::string> m_folders;
std::vector<AssetItem> m_items;
int m_selectedIndex = -1;
void RenderAssetItem(const struct AssetItem& item, int index);
};
}