重构架构:引入EventSystem、EntityID、SelectionManager

- 新增 Event 系统,支持面板间解耦通信
- 用 EntityID 替代裸指针,解决悬空指针问题
- 拆分 SelectionManager 管理选择状态
- SceneManager 使用 EntityID 和 unordered_map 存储
- HierarchyPanel/InspectorPanel 使用事件系统
This commit is contained in:
2026-03-12 17:43:13 +08:00
parent f8fed72cb7
commit c1473d2d39
9 changed files with 322 additions and 116 deletions

48
ui/src/Core/Event.h Normal file
View File

@@ -0,0 +1,48 @@
#pragma once
#include <functional>
#include <vector>
#include <memory>
namespace UI {
template<typename... Args>
class Event {
public:
using HandlerID = size_t;
using Handler = std::function<void(Args...)>;
HandlerID Subscribe(Handler handler) {
HandlerID id = m_nextId++;
m_handlers.emplace_back(id, std::move(handler));
return id;
}
void Unsubscribe(HandlerID id) {
m_handlers.erase(
std::remove_if(m_handlers.begin(), m_handlers.end(),
[id](const auto& pair) { return pair.first == id; }),
m_handlers.end()
);
}
void Invoke(Args... args) {
for (const auto& pair : m_handlers) {
pair.second(args...);
}
}
void operator()(Args... args) {
Invoke(args...);
}
void Clear() {
m_handlers.clear();
}
private:
HandlerID m_nextId = 0;
std::vector<std::pair<HandlerID, Handler>> m_handlers;
};
}

View File

@@ -3,9 +3,15 @@
#include <string>
#include <vector>
#include <memory>
#include <unordered_map>
#include <functional>
#include <cstdint>
namespace UI {
using EntityID = uint64_t;
constexpr EntityID INVALID_ENTITY = 0;
class Component {
public:
virtual ~Component() = default;
@@ -29,16 +35,14 @@ public:
std::string GetName() const override { return "Mesh Renderer"; }
};
class GameObject {
public:
struct Entity {
EntityID id = INVALID_ENTITY;
std::string name;
std::vector<GameObject> children;
EntityID parent = INVALID_ENTITY;
std::vector<EntityID> 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)...);
@@ -58,4 +62,40 @@ public:
}
};
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;
};
}

View File

@@ -2,46 +2,4 @@
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

@@ -1,25 +1,119 @@
#pragma once
#include "Core/GameObject.h"
#include "SelectionManager.h"
#include <unordered_map>
#include <vector>
namespace UI {
class SceneManager {
public:
static SceneManager& Get();
static SceneManager& Get() {
static SceneManager instance;
return instance;
}
std::vector<GameObject>& GetRootObjects() { return m_rootObjects; }
GameObject* GetSelectedObject() { return m_selectedObject; }
void SetSelectedObject(GameObject* obj);
EntityID CreateEntity(const std::string& name, EntityID parent = INVALID_ENTITY) {
EntityID id = m_nextEntityId++;
Entity entity;
entity.id = id;
entity.name = name;
entity.parent = parent;
m_entities[id] = std::move(entity);
if (parent != INVALID_ENTITY) {
m_entities[parent].children.push_back(id);
} else {
m_rootEntities.push_back(id);
}
OnEntityCreated.Invoke(id);
return id;
}
void CreateDemoScene();
Entity* GetEntity(EntityID id) {
auto it = m_entities.find(id);
if (it != m_entities.end()) {
return &it->second;
}
return nullptr;
}
const Entity* GetEntity(EntityID id) const {
auto it = m_entities.find(id);
if (it != m_entities.end()) {
return &it->second;
}
return nullptr;
}
const std::vector<EntityID>& GetRootEntities() const {
return m_rootEntities;
}
void DeleteEntity(EntityID id) {
auto it = m_entities.find(id);
if (it == m_entities.end()) return;
Entity& entity = it->second;
for (EntityID childId : entity.children) {
DeleteEntity(childId);
}
if (entity.parent != INVALID_ENTITY) {
auto* parent = GetEntity(entity.parent);
if (parent) {
auto& siblings = parent->children;
siblings.erase(std::remove(siblings.begin(), siblings.end(), id), siblings.end());
}
} else {
m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), id), m_rootEntities.end());
}
if (SelectionManager::Get().GetSelectedEntity() == id) {
SelectionManager::Get().ClearSelection();
}
m_entities.erase(it);
OnEntityDeleted.Invoke(id);
}
void CreateDemoScene() {
m_entities.clear();
m_rootEntities.clear();
m_nextEntityId = 1;
EntityID camera = CreateEntity("Main Camera");
GetEntity(camera)->AddComponent<TransformComponent>();
EntityID light = CreateEntity("Directional Light");
EntityID cube = CreateEntity("Cube");
GetEntity(cube)->AddComponent<TransformComponent>();
GetEntity(cube)->AddComponent<MeshRendererComponent>()->meshName = "Cube Mesh";
EntityID sphere = CreateEntity("Sphere");
GetEntity(sphere)->AddComponent<TransformComponent>();
GetEntity(sphere)->AddComponent<MeshRendererComponent>()->meshName = "Sphere Mesh";
EntityID player = CreateEntity("Player");
EntityID weapon = CreateEntity("Weapon", player);
OnSceneChanged.Invoke();
}
Event<EntityID> OnEntityCreated;
Event<EntityID> OnEntityDeleted;
Event<> OnSceneChanged;
private:
SceneManager() = default;
std::vector<GameObject> m_rootObjects;
GameObject* m_selectedObject = nullptr;
EntityID m_nextEntityId = 1;
std::unordered_map<EntityID, Entity> m_entities;
std::vector<EntityID> m_rootEntities;
};
}

View File

@@ -0,0 +1,38 @@
#pragma once
#include "Core/GameObject.h"
#include "Core/Event.h"
#include <unordered_set>
namespace UI {
class SelectionManager {
public:
static SelectionManager& Get() {
static SelectionManager instance;
return instance;
}
EntityID GetSelectedEntity() const { return m_selectedEntity; }
void SetSelectedEntity(EntityID id) {
m_selectedEntity = id;
OnSelectionChanged.Invoke(id);
}
void ClearSelection() {
SetSelectedEntity(INVALID_ENTITY);
}
bool IsSelected(EntityID id) const {
return m_selectedEntity == id;
}
Event<EntityID> OnSelectionChanged;
private:
SelectionManager() = default;
EntityID m_selectedEntity = INVALID_ENTITY;
};
}

View File

@@ -1,44 +1,55 @@
#include "HierarchyPanel.h"
#include "Managers/SceneManager.h"
#include "Core/GameObject.h"
#include "Managers/SelectionManager.h"
#include <imgui.h>
namespace UI {
HierarchyPanel::HierarchyPanel() : Panel("Hierarchy") {
SceneManager::Get().CreateDemoScene();
m_selectionHandlerId = SelectionManager::Get().OnSelectionChanged.Subscribe([this](EntityID) {
});
}
HierarchyPanel::~HierarchyPanel() {
SelectionManager::Get().OnSelectionChanged.Unsubscribe(m_selectionHandlerId);
}
void HierarchyPanel::Render() {
ImGui::Begin(m_name.c_str(), &m_isOpen, ImGuiWindowFlags_None);
for (auto& obj : SceneManager::Get().GetRootObjects()) {
RenderGameObject(obj);
for (EntityID id : SceneManager::Get().GetRootEntities()) {
RenderEntity(id);
}
ImGui::End();
}
void HierarchyPanel::RenderGameObject(GameObject& obj) {
void HierarchyPanel::RenderEntity(EntityID id) {
auto& sceneManager = SceneManager::Get();
const Entity* entity = sceneManager.GetEntity(id);
if (!entity) return;
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth;
if (obj.children.empty()) {
if (entity->children.empty()) {
flags |= ImGuiTreeNodeFlags_Leaf;
}
if (obj.selected) {
if (SelectionManager::Get().IsSelected(id)) {
flags |= ImGuiTreeNodeFlags_Selected;
}
bool isOpen = ImGui::TreeNodeEx(obj.name.c_str(), flags);
bool isOpen = ImGui::TreeNodeEx(entity->name.c_str(), flags);
if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) {
SceneManager::Get().SetSelectedObject(&obj);
SelectionManager::Get().SetSelectedEntity(id);
}
if (isOpen) {
for (auto& child : obj.children) {
RenderGameObject(child);
for (EntityID childId : entity->children) {
RenderEntity(childId);
}
ImGui::TreePop();
}

View File

@@ -1,16 +1,22 @@
#pragma once
#include "Panel.h"
#include "Core/Event.h"
#include "Core/GameObject.h"
namespace UI {
class HierarchyPanel : public Panel {
public:
HierarchyPanel();
~HierarchyPanel();
void Render() override;
private:
void RenderGameObject(class GameObject& obj);
void RenderEntity(EntityID id);
Event<EntityID>::HandlerID m_selectionHandlerId = 0;
};
}

View File

@@ -1,31 +1,27 @@
#include "InspectorPanel.h"
#include "Managers/SceneManager.h"
#include "Core/GameObject.h"
#include "Managers/SelectionManager.h"
#include <imgui.h>
namespace UI {
InspectorPanel::InspectorPanel() : Panel("Inspector") {}
InspectorPanel::InspectorPanel() : Panel("Inspector") {
m_selectionHandlerId = SelectionManager::Get().OnSelectionChanged.Subscribe([this](EntityID) {
});
}
InspectorPanel::~InspectorPanel() {
SelectionManager::Get().OnSelectionChanged.Unsubscribe(m_selectionHandlerId);
}
void InspectorPanel::Render() {
ImGui::Begin(m_name.c_str(), &m_isOpen, ImGuiWindowFlags_None);
GameObject* selected = SceneManager::Get().GetSelectedObject();
EntityID selectedId = SelectionManager::Get().GetSelectedEntity();
Entity* entity = SceneManager::Get().GetEntity(selectedId);
if (selected) {
ImGui::Text("%s", selected->name.c_str());
ImGui::Separator();
auto transform = selected->GetComponent<TransformComponent>();
if (transform) {
RenderTransformSection(transform);
ImGui::Separator();
}
auto meshRenderer = selected->GetComponent<MeshRendererComponent>();
if (meshRenderer) {
RenderMeshRendererSection(meshRenderer);
}
if (entity) {
RenderEntity(entity);
} else {
ImGui::Text("No object selected");
ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Select an object in Hierarchy");
@@ -34,42 +30,51 @@ void InspectorPanel::Render() {
ImGui::End();
}
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", transform->position, 0.1f);
ImGui::Text("Rotation");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::DragFloat3("##Rotation", transform->rotation, 1.0f);
ImGui::Text("Scale");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::DragFloat3("##Scale", transform->scale, 0.1f);
ImGui::Unindent(10.0f);
void InspectorPanel::RenderEntity(Entity* entity) {
ImGui::Text("%s", entity->name.c_str());
ImGui::Separator();
for (auto& component : entity->components) {
RenderComponent(component.get());
ImGui::Separator();
}
}
void InspectorPanel::RenderMeshRendererSection(MeshRendererComponent* meshRenderer) {
if (ImGui::CollapsingHeader("Mesh Renderer", ImGuiTreeNodeFlags_DefaultOpen)) {
void InspectorPanel::RenderComponent(Component* component) {
if (!component) return;
const char* name = component->GetName().c_str();
if (ImGui::CollapsingHeader(name, ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent(10.0f);
ImGui::Text("Material");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::InputText("##Material", meshRenderer->materialName.data(), meshRenderer->materialName.capacity());
ImGui::Text("Mesh");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::InputText("##Mesh", meshRenderer->meshName.data(), meshRenderer->meshName.capacity());
if (auto* transform = dynamic_cast<TransformComponent*>(component)) {
ImGui::Text("Position");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::DragFloat3("##Position", transform->position, 0.1f);
ImGui::Text("Rotation");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::DragFloat3("##Rotation", transform->rotation, 1.0f);
ImGui::Text("Scale");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::DragFloat3("##Scale", transform->scale, 0.1f);
}
else if (auto* meshRenderer = dynamic_cast<MeshRendererComponent*>(component)) {
ImGui::Text("Material");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::InputText("##Material", meshRenderer->materialName.data(), meshRenderer->materialName.capacity());
ImGui::Text("Mesh");
ImGui::SameLine(80);
ImGui::SetNextItemWidth(180);
ImGui::InputText("##Mesh", meshRenderer->meshName.data(), meshRenderer->meshName.capacity());
}
ImGui::Unindent(10.0f);
}

View File

@@ -1,17 +1,23 @@
#pragma once
#include "Panel.h"
#include "Core/Event.h"
#include "Core/GameObject.h"
namespace UI {
class InspectorPanel : public Panel {
public:
InspectorPanel();
~InspectorPanel();
void Render() override;
private:
void RenderTransformSection(class TransformComponent* transform);
void RenderMeshRendererSection(class MeshRendererComponent* meshRenderer);
void RenderEntity(Entity* entity);
void RenderComponent(Component* component);
Event<EntityID>::HandlerID m_selectionHandlerId = 0;
};
}