diff --git a/.gitmodules b/.gitmodules index 2e9927e9..b015fea3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -37,6 +37,3 @@ path = external/SPIRV-Cross url = https://github.com/flingengine/SPIRV-Cross.git -[submodule "external/imgui_entt_entity_editor"] - path = external/imgui_entt_entity_editor - url = https://github.com/flingengine/imgui_entt_entity_editor.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 4506d7ec..9721b745 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -134,7 +134,6 @@ if( WITH_IMGUI_FLAG ) target_include_directories( ImGui PUBLIC external/imgui ) include_directories( external/imgui ) - include_directories( external/imgui_entt_entity_editor ) endif() include_directories( external/glm ) diff --git a/FlingEngine/Editor/inc/BaseEditor.h b/FlingEngine/Editor/inc/BaseEditor.h index e83fbaf3..d7274dbb 100644 --- a/FlingEngine/Editor/inc/BaseEditor.h +++ b/FlingEngine/Editor/inc/BaseEditor.h @@ -2,7 +2,8 @@ #include #include -#include "imgui_entt_entity_editor.hpp" +#include "WorldOutlinePanel.h" +#include "EntityEditorPanel.h" namespace Fling { @@ -25,9 +26,9 @@ namespace Fling */ virtual void Draw(entt::registry& t_Reg, float DeltaTime); - // #TODO: Init and shutdown functions + // #TODO: Init and shutdown functions - protected: + protected: virtual void OnLoadLevel(std::string t_FileName); @@ -43,9 +44,11 @@ namespace Fling bool m_DisplayWindowOptions = false; bool m_DisplayCameraOptions = false; - /** Component editor so that we can draw our component window */ - entt::entity m_CompEditorEntityType = entt::null; - MM::ImGuiEntityEditor m_ComponentEditor; + /** Lists entities in the world and tracks which one is selected */ + WorldOutlinePanel m_WorldOutline; + + /** Inspects the entity currently selected in m_WorldOutline */ + EntityEditorPanel m_EntityEditor; virtual void DrawFileMenu(); @@ -56,7 +59,7 @@ namespace Fling void DrawWorldOutline(entt::registry& t_Reg); /** assumes that m_DisplayComponentEditor is true */ - void DrawComponentEditor(entt::registry& t_Reg); + void DrawEntityEditor(entt::registry& t_Reg); void DrawWindowOptions(); @@ -70,4 +73,4 @@ namespace Fling // Draw Gizmos, etc // Draw component editor }; -} // namespace Fling \ No newline at end of file +} // namespace Fling diff --git a/FlingEngine/Editor/inc/EntityEditorPanel.h b/FlingEngine/Editor/inc/EntityEditorPanel.h new file mode 100644 index 00000000..9b56a340 --- /dev/null +++ b/FlingEngine/Editor/inc/EntityEditorPanel.h @@ -0,0 +1,26 @@ +#pragma once + +#include "FlingEditorPanel.h" + +#include + +namespace Fling +{ + /** + * Re-usable ImGui panel that inspects a single entity: lists the components + * it has (via ComponentTypeRegistry), lets the user remove them, and offers + * an "Add Component" popup for any registered type the entity doesn't have. + */ + class EntityEditorPanel final : public FlingEditorPanel + { + public: + /** Draws the "Entity Editor" window for the current target entity. Call once per frame. */ + virtual void Draw(entt::registry& t_Reg) override; + + /** Sets which entity this panel inspects. Pass entt::null to show none. */ + void SetTargetEntity(entt::entity t_Entity) { m_TargetEntity = t_Entity; } + + private: + entt::entity m_TargetEntity = entt::null; + }; +} // namespace Fling diff --git a/FlingEngine/Editor/inc/FlingEditorPanel.h b/FlingEngine/Editor/inc/FlingEditorPanel.h new file mode 100644 index 00000000..6789a98a --- /dev/null +++ b/FlingEngine/Editor/inc/FlingEditorPanel.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +namespace Fling +{ + /** + * Base class for the engine's ImGui editor windows. + */ + class FlingEditorPanel + { + public: + virtual ~FlingEditorPanel() = default; + + /** Draws this panel's ImGui window. Call once per frame. */ + virtual void Draw(entt::registry& t_Reg) = 0; + + void Show() { m_ShowWindow = true; } + void Hide() { m_ShowWindow = false; } + bool IsShown() const { return m_ShowWindow; } + + protected: + bool m_ShowWindow = true; + }; +} // namespace Fling diff --git a/FlingEngine/Editor/inc/WorldOutlinePanel.h b/FlingEngine/Editor/inc/WorldOutlinePanel.h new file mode 100644 index 00000000..1b64e956 --- /dev/null +++ b/FlingEngine/Editor/inc/WorldOutlinePanel.h @@ -0,0 +1,27 @@ +#pragma once + +#include "FlingEditorPanel.h" + +#include + +namespace Fling +{ + /** + * Re-usable ImGui panel that lists every entity in a registry. Supports + * creating and deleting entities, and tracks which entity is currently + * selected so other editor windows (e.g. EntityEditorPanel) can follow it. + */ + class WorldOutlinePanel final : public FlingEditorPanel + { + public: + /** Draws the "World Outline" window. Call this once per frame. */ + virtual void Draw(entt::registry& t_Reg) override; + + entt::entity GetSelectedEntity() const { return m_SelectedEntity; } + + void SetSelectedEntity(entt::entity t_Entity) { m_SelectedEntity = t_Entity; } + + private: + entt::entity m_SelectedEntity = entt::null; + }; +} // namespace Fling diff --git a/FlingEngine/Editor/src/BaseEditor.cpp b/FlingEngine/Editor/src/BaseEditor.cpp index 4e086c66..9a71de63 100644 --- a/FlingEngine/Editor/src/BaseEditor.cpp +++ b/FlingEngine/Editor/src/BaseEditor.cpp @@ -8,18 +8,15 @@ // We have to draw the ImGUI stuff somewhere, so we miind as well keep it all here! #include "Components/Transform.h" -#include "Components/Name.hpp" #include "MeshRenderer.h" #include "Lighting/DirectionalLight.hpp" #include "Lighting/PointLight.hpp" #include "ImFileBrowser.hpp" #include "World.h" -#include "EditableComponent.h" +#include "ComponentTypeRegistry.h" -#include -#include -#include -#include +#include +#include namespace Fling { @@ -88,7 +85,7 @@ namespace Fling { MaterialName = t_MeshRend.m_Material->GetGuidString(); } - + const char* m = MaterialName.c_str(); ImGui::LabelText("Material", m, "%s"); @@ -108,11 +105,14 @@ namespace Fling fileDialog.Display(); if(fileDialog.HasSelected()) { - std::string ModelName = t_MeshRend.m_Model->GetGuidString(); std::string SelectedAsset = FlingPaths::ConvertAbsolutePathToRelative(fileDialog.GetSelected().string()); + // Update the material in place. Going through registry.replace() + // here would construct a brand new component and drop its m_UniformBuffer/ + // m_DescriptorSet (only OnMeshRendererAdded, hooked to on_construct, sets + // those up) -- the next frame's render pass would then dereference a null + // uniform buffer for this entity. t_MeshRend.LoadMaterialFromPath(SelectedAsset); - t_Reg.replace(t_Entity, ModelName, SelectedAsset); fileDialog.ClearSelected(); } @@ -145,40 +145,40 @@ namespace Fling void BaseEditor::RegisterComponents(entt::registry& t_Reg) { - m_ComponentEditor.registerTrivial(t_Reg, "Transform"); - m_ComponentEditor.registerComponentWidgetFn( - t_Reg.type(), - [](entt::registry& reg, auto e) + (void)t_Reg; + + // Transform, NameComponent, MeshRenderer, DirectionalLight, and PointLight are + // already known to ComponentTypeRegistry (RegisterGameplayComponents / + // RegisterGraphicsComponents); we're just attaching an ImGui draw callback for + // each so EntityEditorPanel can render them. + ComponentTypeRegistry& Registry = ComponentTypeRegistry::Get(); + + Registry.SetEditorWidget("Transform", + [](entt::registry& reg, entt::entity e) { auto& t = reg.get(e); Widgets::Transform(t); } ); - m_ComponentEditor.registerTrivial(t_Reg, "PointLight"); - m_ComponentEditor.registerComponentWidgetFn( - t_Reg.type(), - [](entt::registry& reg, auto e) + Registry.SetEditorWidget("PointLight", + [](entt::registry& reg, entt::entity e) { auto& t = reg.get(e); Widgets::PointLight(t); } ); - m_ComponentEditor.registerTrivial(t_Reg, "Directional Light"); - m_ComponentEditor.registerComponentWidgetFn( - t_Reg.type(), - [](entt::registry& reg, auto e) + Registry.SetEditorWidget("DirectionalLight", + [](entt::registry& reg, entt::entity e) { auto& t = reg.get(e); Widgets::DirectionalLight(t); } ); - m_ComponentEditor.registerTrivial(t_Reg, "Mesh Renderer"); - m_ComponentEditor.registerComponentWidgetFn( - t_Reg.type(), - [](entt::registry& reg, auto e) + Registry.SetEditorWidget("MeshRenderer", + [](entt::registry& reg, entt::entity e) { auto& t = reg.get(e); Widgets::MeshRenderer(t, reg, e); @@ -187,7 +187,7 @@ namespace Fling } void BaseEditor::Draw(entt::registry& t_Reg, float DeltaTime) - { + { DrawFileMenu(); if (m_DisplayGPUInfo) @@ -202,7 +202,7 @@ namespace Fling if(m_DisplayComponentEditor) { - DrawComponentEditor(t_Reg); + DrawEntityEditor(t_Reg); } if (m_DisplayWindowOptions) @@ -237,95 +237,14 @@ namespace Fling void BaseEditor::DrawWorldOutline(entt::registry& t_Reg) { - ImGui::Begin("World Outline"); - - ImGui::SetWindowSize(ImVec2(250.0f, 400.0f), ImGuiCond_FirstUseEver); - ImGui::SetWindowPos(ImVec2(0.0f, 30.0f), ImGuiCond_FirstUseEver); - - std::vector entities; - t_Reg.each([&](entt::entity entity) - { - entities.push_back(entity); - }); - - entt::entity entityToDestroy = entt::null; - for (entt::entity entity : entities) - { - if (!t_Reg.valid(entity)) - { - continue; - } - - const bool bStartedSelected = (m_CompEditorEntityType == entity); - - std::string label; - if (t_Reg.has(entity) && !t_Reg.get(entity).Name.empty()) - { - label = t_Reg.get(entity).Name; - } - else - { - std::ostringstream os; - os << "Entity " << static_cast(entity); - label = os.str(); - } - - ImGui::PushID(static_cast(entity)); - - if (ImGui::Button(" - ")) - { - F_LOG_TRACE("Delete {}", label); - entityToDestroy = entity; - } - - ImGui::SameLine(); - - if (bStartedSelected) - { - ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(7.0f, 0.6f, 0.6f)); - } - - if (ImGui::Button(label.c_str(), ImVec2(ImGui::GetWindowWidth(), 0.f))) - { - m_CompEditorEntityType = entity; - } - - if (bStartedSelected) - { - ImGui::PopStyleColor(1); - } - - ImGui::PopID(); - } - - if (entityToDestroy != entt::null) - { - if (m_CompEditorEntityType == entityToDestroy) - { - m_CompEditorEntityType = entt::null; - } - t_Reg.destroy(entityToDestroy); - } - - ImGui::End(); + m_WorldOutline.Draw(t_Reg); } - void BaseEditor::DrawComponentEditor(entt::registry& t_Reg) + void BaseEditor::DrawEntityEditor(entt::registry& t_Reg) { - // Set the window options for the component editor - ImGui::SetNextWindowSize(ImVec2(250.0f, 400.0f), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(ImGui::GetWindowWidth(), 30.0f), ImGuiCond_FirstUseEver); - - m_ComponentEditor.renderImGui(t_Reg, m_CompEditorEntityType); - - // Make sure that each entity has a transform so that they show up in the editor window - if(m_CompEditorEntityType != entt::null) - { - if(!t_Reg.has(m_CompEditorEntityType)) - { - t_Reg.assign(m_CompEditorEntityType); - } - } + // Spawn the entity editor for whatever is currently selected in the outline + m_EntityEditor.SetTargetEntity(m_WorldOutline.GetSelectedEntity()); + m_EntityEditor.Draw(t_Reg); } void BaseEditor::DrawWindowOptions() @@ -346,7 +265,7 @@ namespace Fling { CurSelection = WindowOpts[n]; } - + if (is_selected) { ImGui::SetItemDefaultFocus(); @@ -367,7 +286,7 @@ namespace Fling ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_MenuBar | - ImGuiWindowFlags_NoTitleBar | + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoBackground; bool isOpen = true; @@ -489,13 +408,13 @@ namespace Fling { assert(m_OwningWorld); - // File pop up to load the level file + // File pop up to load the level file F_LOG_TRACE("Save to file {}", t_FileName); m_OwningWorld->OutputLevelFile(t_FileName); } - void BaseEditor::DrawGpuInfo() + void BaseEditor::DrawGpuInfo() { Timing& Timing = Timing::Get(); ImGui::Begin("GPU Info"); @@ -530,4 +449,4 @@ namespace Fling } } // namespace Fling -#endif // WITH_EDITOR \ No newline at end of file +#endif // WITH_EDITOR diff --git a/FlingEngine/Editor/src/EntityEditorPanel.cpp b/FlingEngine/Editor/src/EntityEditorPanel.cpp new file mode 100644 index 00000000..4ad60b52 --- /dev/null +++ b/FlingEngine/Editor/src/EntityEditorPanel.cpp @@ -0,0 +1,151 @@ +#include "pch.h" +#include "EntityEditorPanel.h" + +#include "ComponentTypeRegistry.h" +#include "Components/Name.hpp" +#include "EditableComponent.h" + +#include + +#include +#include + +namespace Fling +{ + namespace + { + // NameComponent gets a dedicated rename field up top instead of showing up in + // the generic component list, so it isn't offered twice. + bool IsNameComponent(const ComponentTypeInfo& info) + { + return info.name && std::strcmp(info.name, "NameComponent") == 0; + } + } + + void EntityEditorPanel::Draw(entt::registry& t_Reg) + { + if (!m_ShowWindow) + { + return; + } + + const entt::entity t_Entity = m_TargetEntity; + + if (ImGui::Begin("Entity Editor", &m_ShowWindow)) + { + ImGui::SetNextWindowSize(ImVec2(250.0f, 400.0f), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(ImGui::GetWindowWidth(), 30.0f), ImGuiCond_FirstUseEver); + + ImGui::TextUnformatted("editing:"); + ImGui::SameLine(); + + if (t_Entity != entt::null && t_Reg.valid(t_Entity)) + { + ImGui::Text("id: %u, v: %u", static_cast(t_Reg.entity(t_Entity)), static_cast(t_Reg.version(t_Entity))); + } + else + { + ImGui::TextUnformatted("no entity selected"); + ImGui::End(); + return; + } + + // Rename field. Only touches the registry once the user actually edits it, + // so just opening the editor doesn't silently attach a NameComponent. + { + char NameBuf[128] = {}; + const std::string& CurrentName = t_Reg.has(t_Entity) ? t_Reg.get(t_Entity).Name : std::string(); + std::strncpy(NameBuf, CurrentName.c_str(), sizeof(NameBuf) - 1); + + if (ImGui::InputText("Name", NameBuf, sizeof(NameBuf))) + { + if (!t_Reg.has(t_Entity)) + { + t_Reg.assign(t_Entity); + } + t_Reg.get(t_Entity).Name = NameBuf; + } + } + + ImGui::Separator(); + + const std::vector& types = ComponentTypeRegistry::Get().All(); + + std::vector missing; + for (const ComponentTypeInfo& info : types) + { + if (IsNameComponent(info)) + { + continue; + } + + if (!info.has || !info.has(t_Reg, t_Entity)) + { + missing.push_back(&info); + continue; + } + + ImGui::PushID(info.name); + + if (info.destroy) + { + if (ImGui::Button("-")) + { + info.destroy(t_Reg, t_Entity); + ImGui::PopID(); + continue; // Early out to avoid touching the just-removed component + } + ImGui::SameLine(); + } + + if (ImGui::CollapsingHeader(info.name)) + { + ImGui::Indent(30.f); + + if (info.drawEditorWidget) + { + info.drawEditorWidget(t_Reg, t_Entity); + } + else + { + ImGui::TextDisabled("missing widget to display component!"); + } + + ImGui::Unindent(30.f); + } + + ImGui::PopID(); + } + + if (!missing.empty()) + { + if (ImGui::Button("+ Add Component")) + { + ImGui::OpenPopup("add component"); + } + + if (ImGui::BeginPopup("add component")) + { + ImGui::TextUnformatted("available:"); + ImGui::Separator(); + + for (const ComponentTypeInfo* info : missing) + { + if (info->create && ImGui::Selectable(info->name)) + { + info->create(t_Reg, t_Entity); + } + } + + ImGui::EndPopup(); + } + } + + if (!t_Reg.has(t_Entity)) + { + t_Reg.assign(t_Entity); + } + } + ImGui::End(); + } +} // namespace Fling diff --git a/FlingEngine/Editor/src/WorldOutlinePanel.cpp b/FlingEngine/Editor/src/WorldOutlinePanel.cpp new file mode 100644 index 00000000..c192c531 --- /dev/null +++ b/FlingEngine/Editor/src/WorldOutlinePanel.cpp @@ -0,0 +1,99 @@ +#include "pch.h" +#include "WorldOutlinePanel.h" + +#include "Components/Name.hpp" + +#include + +#include +#include + +namespace Fling +{ + void WorldOutlinePanel::Draw(entt::registry& t_Reg) + { + if (!m_ShowWindow) + { + return; + } + + if (ImGui::Begin("World Outline", &m_ShowWindow)) + { + ImGui::SetWindowSize(ImVec2(250.0f, 400.0f), ImGuiCond_FirstUseEver); + ImGui::SetWindowPos(ImVec2(0.0f, 30.0f), ImGuiCond_FirstUseEver); + + if (ImGui::Button("+ Add Entity", ImVec2(ImGui::GetWindowWidth(), 0.f))) + { + m_SelectedEntity = t_Reg.create(); + } + + ImGui::Separator(); + + std::vector entities; + t_Reg.each([&](entt::entity entity) + { + entities.push_back(entity); + }); + + entt::entity entityToDestroy = entt::null; + for (entt::entity entity : entities) + { + if (!t_Reg.valid(entity)) + { + continue; + } + + const bool bIsSelected = (m_SelectedEntity == entity); + + std::string label; + if (t_Reg.has(entity) && !t_Reg.get(entity).Name.empty()) + { + label = t_Reg.get(entity).Name; + } + else + { + std::ostringstream os; + os << "Entity " << static_cast(entity); + label = os.str(); + } + + ImGui::PushID(static_cast(entity)); + + if (ImGui::Button(" - ")) + { + F_LOG_TRACE("Delete {}", label); + entityToDestroy = entity; + } + + ImGui::SameLine(); + + if (bIsSelected) + { + ImGui::PushStyleColor(ImGuiCol_Button, (ImVec4)ImColor::HSV(7.0f, 0.6f, 0.6f)); + } + + if (ImGui::Button(label.c_str(), ImVec2(ImGui::GetWindowWidth(), 0.f))) + { + m_SelectedEntity = entity; + } + + if (bIsSelected) + { + ImGui::PopStyleColor(1); + } + + ImGui::PopID(); + } + + if (entityToDestroy != entt::null) + { + if (m_SelectedEntity == entityToDestroy) + { + m_SelectedEntity = entt::null; + } + t_Reg.destroy(entityToDestroy); + } + } + ImGui::End(); + } +} // namespace Fling diff --git a/FlingEngine/Gameplay/inc/ComponentTypeRegistry.h b/FlingEngine/Gameplay/inc/ComponentTypeRegistry.h index 6ea2fc24..3fc520ae 100644 --- a/FlingEngine/Gameplay/inc/ComponentTypeRegistry.h +++ b/FlingEngine/Gameplay/inc/ComponentTypeRegistry.h @@ -3,6 +3,7 @@ #include "JsonArchive.h" #include +#include #include #include #include @@ -16,6 +17,18 @@ namespace Fling void (*save)(const entt::registry&, entt::entity, Json& out) = nullptr; void (*load)(entt::registry&, entt::entity, const Json& in) = nullptr; bool (*has)(const entt::registry&, entt::entity) = nullptr; + + /** Default-constructs and assigns this component type to an entity. */ + void (*create)(entt::registry&, entt::entity) = nullptr; + + /** Removes this component type from an entity. */ + void (*destroy)(entt::registry&, entt::entity) = nullptr; + + /** + * Optional ImGui draw callback for editor tooling. Left null for types + * that don't register one via ComponentTypeRegistry::SetEditorWidget. + */ + void (*drawEditorWidget)(entt::registry&, entt::entity) = nullptr; }; /** @@ -48,6 +61,13 @@ namespace Fling /** Store an external registrar to run from World::Init. Asserts/logs if sealed. */ void AddRegistrar(IComponentRegistrar* registrar); + /** + * Attach an ImGui draw callback to an already-registered component type, so + * editor tooling can render it without the registry knowing about ImGui + * itself. No-op (with a warning) if the name hasn't been registered. + */ + void SetEditorWidget(const char* name, void(*fn)(entt::registry&, entt::entity)); + /** Invoke every registrar added via AddRegistrar. */ void RunExternalRegistrars(); @@ -73,7 +93,12 @@ namespace Fling void AddType(const char* name, ComponentTypeInfo info); std::vector m_Types; - std::vector m_NameStorage; + + // A deque (not vector) so that growing it never relocates existing + // elements, which would dangle the const char* pointers ComponentTypeInfo + // entries hold into these strings via .c_str(). + std::deque m_NameStorage; + std::unordered_map m_NameToIndex; std::vector m_Registrars; bool m_Sealed = false; @@ -120,6 +145,14 @@ namespace Fling reg.assign(e, std::move(comp)); } }; + info.create = [](entt::registry& reg, entt::entity e) + { + reg.assign(e); + }; + info.destroy = [](entt::registry& reg, entt::entity e) + { + reg.remove(e); + }; AddType(name, std::move(info)); return true; diff --git a/FlingEngine/Gameplay/src/ComponentTypeRegistry.cpp b/FlingEngine/Gameplay/src/ComponentTypeRegistry.cpp index b94d6108..0c1884d3 100644 --- a/FlingEngine/Gameplay/src/ComponentTypeRegistry.cpp +++ b/FlingEngine/Gameplay/src/ComponentTypeRegistry.cpp @@ -56,6 +56,18 @@ namespace Fling } } + void ComponentTypeRegistry::SetEditorWidget(const char* name, void(*fn)(entt::registry&, entt::entity)) + { + const auto it = m_NameToIndex.find(name); + if (it == m_NameToIndex.end()) + { + F_LOG_WARN("ComponentTypeRegistry::SetEditorWidget('{}') called before the type was registered", name); + return; + } + + m_Types[it->second].drawEditorWidget = fn; + } + void ComponentTypeRegistry::RunExternalRegistrars() { if (m_Sealed) diff --git a/external/imgui_entt_entity_editor b/external/imgui_entt_entity_editor deleted file mode 160000 index f09af05c..00000000 --- a/external/imgui_entt_entity_editor +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f09af05c66dafa28c50f1c8e8a2c6524c495eaed