From 0651666d8c86b8b50a5801535dfab3b07d64bf11 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Thu, 26 Mar 2026 01:26:26 +0800 Subject: [PATCH] Fix editor scene persistence and XC scene workflow --- editor/src/Application.cpp | 120 +++++-- editor/src/Application.h | 6 +- .../ComponentEditors/CameraComponentEditor.h | 108 ++++++ .../src/ComponentEditors/IComponentEditor.h | 35 ++ .../ComponentEditors/LightComponentEditor.h | 98 ++++++ .../TransformComponentEditor.h | 67 ++++ editor/src/Core/EditorConsoleSink.cpp | 21 +- editor/src/Core/EditorConsoleSink.h | 5 +- editor/src/Core/EditorContext.h | 2 +- editor/src/Core/EventBus.h | 20 +- editor/src/Core/ISceneManager.h | 12 +- editor/src/Layers/EditorLayer.cpp | 16 +- editor/src/Managers/ProjectManager.cpp | 5 +- editor/src/Managers/SceneManager.cpp | 320 +++++++++++++++--- editor/src/Managers/SceneManager.h | 29 +- editor/src/Utils/SceneEditorUtils.h | 196 +++++++++++ editor/src/panels/ConsolePanel.cpp | 8 +- editor/src/panels/HierarchyPanel.cpp | 22 +- editor/src/panels/InspectorPanel.cpp | 146 ++++---- editor/src/panels/InspectorPanel.h | 16 +- editor/src/panels/MenuBar.cpp | 135 +++++++- editor/src/panels/MenuBar.h | 8 +- editor/src/panels/ProjectPanel.cpp | 30 +- engine/CMakeLists.txt | 8 + .../XCEngine/Components/CameraComponent.h | 57 ++++ .../include/XCEngine/Components/GameObject.h | 43 ++- .../XCEngine/Components/LightComponent.h | 50 +++ engine/src/Components/CameraComponent.cpp | 77 +++++ engine/src/Components/GameObject.cpp | 47 +-- engine/src/Components/LightComponent.cpp | 64 ++++ engine/src/Scene/Scene.cpp | 252 ++++++++++++-- tests/Components/CMakeLists.txt | 3 +- .../test_camera_light_component.cpp | 57 ++++ tests/Components/test_game_object.cpp | 4 +- tests/Scene/test_scene.cpp | 127 ++++++- 35 files changed, 1958 insertions(+), 256 deletions(-) create mode 100644 editor/src/ComponentEditors/CameraComponentEditor.h create mode 100644 editor/src/ComponentEditors/IComponentEditor.h create mode 100644 editor/src/ComponentEditors/LightComponentEditor.h create mode 100644 editor/src/ComponentEditors/TransformComponentEditor.h create mode 100644 editor/src/Utils/SceneEditorUtils.h create mode 100644 engine/include/XCEngine/Components/CameraComponent.h create mode 100644 engine/include/XCEngine/Components/LightComponent.h create mode 100644 engine/src/Components/CameraComponent.cpp create mode 100644 engine/src/Components/LightComponent.cpp create mode 100644 tests/Components/test_camera_light_component.cpp diff --git a/editor/src/Application.cpp b/editor/src/Application.cpp index e8ca8ea1..22b02883 100644 --- a/editor/src/Application.cpp +++ b/editor/src/Application.cpp @@ -8,15 +8,69 @@ #include #include #include +#include #include #include +#include extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); +namespace { + +std::string WideToUtf8(const std::wstring& value) { + if (value.empty()) { + return {}; + } + + int len = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (len <= 0) { + return {}; + } + + std::string result(len - 1, '\0'); + WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, &result[0], len, nullptr, nullptr); + return result; +} + +std::wstring Utf8ToWide(const std::string& value) { + if (value.empty()) { + return {}; + } + + int len = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0); + if (len <= 0) { + return {}; + } + + std::wstring result(len - 1, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, &result[0], len); + return result; +} + +std::string GetExecutableDirectoryUtf8() { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(nullptr, exePath, MAX_PATH); + + std::wstring exeDirW(exePath); + const size_t pos = exeDirW.find_last_of(L"\\/"); + if (pos != std::wstring::npos) { + exeDirW = exeDirW.substr(0, pos); + } + + return WideToUtf8(exeDirW); +} + +std::string GetExecutableLogPath(const char* fileName) { + return GetExecutableDirectoryUtf8() + "\\" + fileName; +} + +} // namespace + static LONG WINAPI GlobalExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) { - const char* logPath = "D:\\Xuanchi\\Main\\XCEngine\\editor\\bin\\Release\\crash.log"; - - FILE* f = fopen(logPath, "a"); + const std::string logPath = GetExecutableLogPath("crash.log"); + + FILE* f = nullptr; + fopen_s(&f, logPath.c_str(), "a"); if (f) { fprintf(f, "[CRASH] ExceptionCode=0x%08X, Address=0x%p\n", exceptionPointers->ExceptionRecord->ExceptionCode, @@ -45,20 +99,7 @@ bool Application::Initialize(HWND hwnd) { // Redirect stderr to log file to capture ImGui errors { - wchar_t exePath[MAX_PATH]; - GetModuleFileNameW(nullptr, exePath, MAX_PATH); - std::wstring exeDirW(exePath); - size_t pos = exeDirW.find_last_of(L"\\/"); - if (pos != std::wstring::npos) { - exeDirW = exeDirW.substr(0, pos); - } - std::string exeDir; - int len = WideCharToMultiByte(CP_UTF8, 0, exeDirW.c_str(), -1, nullptr, 0, nullptr, nullptr); - if (len > 0) { - exeDir.resize(len - 1); - WideCharToMultiByte(CP_UTF8, 0, exeDirW.c_str(), -1, &exeDir[0], len, nullptr, nullptr); - } - std::string stderrPath = exeDir + "\\stderr.log"; + const std::string stderrPath = GetExecutableLogPath("stderr.log"); freopen(stderrPath.c_str(), "w", stderr); fprintf(stderr, "[TEST] stderr redirection test - this should appear in stderr.log\n"); @@ -70,19 +111,7 @@ bool Application::Initialize(HWND hwnd) { Debug::Logger::Get().AddSink(std::make_unique()); // Get exe directory for log file path - wchar_t exePath[MAX_PATH]; - GetModuleFileNameW(nullptr, exePath, MAX_PATH); - std::wstring exeDirW(exePath); - size_t pos = exeDirW.find_last_of(L"\\/"); - if (pos != std::wstring::npos) { - exeDirW = exeDirW.substr(0, pos); - } - std::string exeDir; - int len = WideCharToMultiByte(CP_UTF8, 0, exeDirW.c_str(), -1, nullptr, 0, nullptr, nullptr); - if (len > 0) { - exeDir.resize(len - 1); - WideCharToMultiByte(CP_UTF8, 0, exeDirW.c_str(), -1, &exeDir[0], len, nullptr, nullptr); - } + const std::string exeDir = GetExecutableDirectoryUtf8(); std::string logPath = exeDir + "\\editor.log"; Debug::Logger::Get().AddSink(std::make_unique(logPath.c_str())); Debug::Logger::Get().Info(Debug::LogCategory::General, "Editor Application starting..."); @@ -154,6 +183,7 @@ void Application::Render() { ImGui::NewFrame(); m_layerStack.onImGuiRender(); + UpdateWindowTitle(); ImGui::Render(); @@ -198,6 +228,34 @@ void Application::Render() { } } +void Application::UpdateWindowTitle() { + if (!m_hwnd || !m_editorContext) { + return; + } + + auto& sceneManager = m_editorContext->GetSceneManager(); + std::string sceneName = sceneManager.HasActiveScene() ? sceneManager.GetCurrentSceneName() : "No Scene"; + if (sceneName.empty()) { + sceneName = "Untitled Scene"; + } + + if (sceneManager.IsSceneDirty()) { + sceneName += " *"; + } + if (sceneManager.GetCurrentScenePath().empty()) { + sceneName += " (Unsaved)"; + } else { + sceneName += " - "; + sceneName += std::filesystem::path(sceneManager.GetCurrentScenePath()).filename().string(); + } + + const std::wstring title = Utf8ToWide(sceneName + " - XCVolumeRenderer - Unity Style Editor"); + if (title != m_lastWindowTitle) { + SetWindowTextW(m_hwnd, title.c_str()); + m_lastWindowTitle = title; + } +} + void Application::OnResize(int width, int height) { if (width <= 0 || height <= 0) return; @@ -303,4 +361,4 @@ void Application::CleanupRenderTarget() { } } -} \ No newline at end of file +} diff --git a/editor/src/Application.h b/editor/src/Application.h index 0c635b35..259db559 100644 --- a/editor/src/Application.h +++ b/editor/src/Application.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -23,6 +24,7 @@ public: void Shutdown(); void Render(); void OnResize(int width, int height); + HWND GetWindowHandle() const { return m_hwnd; } IEditorContext& GetEditorContext() const { return *m_editorContext; } @@ -33,6 +35,7 @@ private: bool CreateDevice(); bool CreateRenderTarget(); void CleanupRenderTarget(); + void UpdateWindowTitle(); HWND m_hwnd = nullptr; int m_width = 1280; @@ -54,7 +57,8 @@ private: Core::LayerStack m_layerStack; EditorLayer* m_editorLayer = nullptr; std::shared_ptr m_editorContext; + std::wstring m_lastWindowTitle; }; } -} \ No newline at end of file +} diff --git a/editor/src/ComponentEditors/CameraComponentEditor.h b/editor/src/ComponentEditors/CameraComponentEditor.h new file mode 100644 index 00000000..0eddf81f --- /dev/null +++ b/editor/src/ComponentEditors/CameraComponentEditor.h @@ -0,0 +1,108 @@ +#pragma once + +#include "IComponentEditor.h" +#include "UI/UI.h" + +#include + +namespace XCEngine { +namespace Editor { + +class CameraComponentEditor : public IComponentEditor { +public: + const char* GetDisplayName() const override { + return "Camera"; + } + + bool CanEdit(::XCEngine::Components::Component* component) const override { + return dynamic_cast<::XCEngine::Components::CameraComponent*>(component) != nullptr; + } + + bool Render(::XCEngine::Components::Component* component) override { + auto* camera = dynamic_cast<::XCEngine::Components::CameraComponent*>(component); + if (!camera) { + return false; + } + + int projectionType = static_cast(camera->GetProjectionType()); + const char* projectionLabels[] = { "Perspective", "Orthographic" }; + bool changed = false; + if (ImGui::Combo("Projection", &projectionType, projectionLabels, 2)) { + camera->SetProjectionType(static_cast<::XCEngine::Components::CameraProjectionType>(projectionType)); + changed = true; + } + + if (camera->GetProjectionType() == ::XCEngine::Components::CameraProjectionType::Perspective) { + float fieldOfView = camera->GetFieldOfView(); + if (UI::DrawSliderFloat("Field Of View", fieldOfView, 1.0f, 179.0f, 100.0f, "%.1f")) { + camera->SetFieldOfView(fieldOfView); + changed = true; + } + } else { + float orthographicSize = camera->GetOrthographicSize(); + if (UI::DrawFloat("Orthographic Size", orthographicSize, 100.0f, 0.1f, 0.001f)) { + camera->SetOrthographicSize(orthographicSize); + changed = true; + } + } + + float nearClip = camera->GetNearClipPlane(); + if (UI::DrawFloat("Near Clip", nearClip, 100.0f, 0.01f, 0.001f)) { + camera->SetNearClipPlane(nearClip); + changed = true; + } + + float farClip = camera->GetFarClipPlane(); + if (UI::DrawFloat("Far Clip", farClip, 100.0f, 0.1f, nearClip + 0.001f)) { + camera->SetFarClipPlane(farClip); + changed = true; + } + + float depth = camera->GetDepth(); + if (UI::DrawFloat("Depth", depth, 100.0f, 0.1f)) { + camera->SetDepth(depth); + changed = true; + } + + bool primary = camera->IsPrimary(); + if (UI::DrawBool("Primary", primary)) { + camera->SetPrimary(primary); + changed = true; + } + + float clearColor[4] = { + camera->GetClearColor().r, + camera->GetClearColor().g, + camera->GetClearColor().b, + camera->GetClearColor().a + }; + if (UI::DrawColor4("Clear Color", clearColor)) { + camera->SetClearColor(::XCEngine::Math::Color(clearColor[0], clearColor[1], clearColor[2], clearColor[3])); + changed = true; + } + + return changed; + } + + bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override { + return gameObject && !gameObject->GetComponent<::XCEngine::Components::CameraComponent>(); + } + + const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override { + if (!gameObject) { + return "Invalid"; + } + return gameObject->GetComponent<::XCEngine::Components::CameraComponent>() ? "Already Added" : nullptr; + } + + ::XCEngine::Components::Component* AddTo(::XCEngine::Components::GameObject* gameObject) const override { + return gameObject ? gameObject->AddComponent<::XCEngine::Components::CameraComponent>() : nullptr; + } + + bool CanRemove(::XCEngine::Components::Component* component) const override { + return CanEdit(component); + } +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/ComponentEditors/IComponentEditor.h b/editor/src/ComponentEditors/IComponentEditor.h new file mode 100644 index 00000000..81954c2a --- /dev/null +++ b/editor/src/ComponentEditors/IComponentEditor.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace XCEngine { +namespace Editor { + +class IComponentEditor { +public: + virtual ~IComponentEditor() = default; + + virtual const char* GetDisplayName() const = 0; + virtual bool CanEdit(::XCEngine::Components::Component* component) const = 0; + virtual bool Render(::XCEngine::Components::Component* component) = 0; + + virtual bool ShowInAddComponentMenu() const { return true; } + virtual bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const { return false; } + virtual const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const { + (void)gameObject; + return nullptr; + } + virtual ::XCEngine::Components::Component* AddTo(::XCEngine::Components::GameObject* gameObject) const { + (void)gameObject; + return nullptr; + } + + virtual bool CanRemove(::XCEngine::Components::Component* component) const { + (void)component; + return false; + } +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/ComponentEditors/LightComponentEditor.h b/editor/src/ComponentEditors/LightComponentEditor.h new file mode 100644 index 00000000..cbff270f --- /dev/null +++ b/editor/src/ComponentEditors/LightComponentEditor.h @@ -0,0 +1,98 @@ +#pragma once + +#include "IComponentEditor.h" +#include "UI/UI.h" + +#include + +namespace XCEngine { +namespace Editor { + +class LightComponentEditor : public IComponentEditor { +public: + const char* GetDisplayName() const override { + return "Light"; + } + + bool CanEdit(::XCEngine::Components::Component* component) const override { + return dynamic_cast<::XCEngine::Components::LightComponent*>(component) != nullptr; + } + + bool Render(::XCEngine::Components::Component* component) override { + auto* light = dynamic_cast<::XCEngine::Components::LightComponent*>(component); + if (!light) { + return false; + } + + int lightType = static_cast(light->GetLightType()); + const char* lightTypeLabels[] = { "Directional", "Point", "Spot" }; + bool changed = false; + if (ImGui::Combo("Type", &lightType, lightTypeLabels, 3)) { + light->SetLightType(static_cast<::XCEngine::Components::LightType>(lightType)); + changed = true; + } + + float color[4] = { + light->GetColor().r, + light->GetColor().g, + light->GetColor().b, + light->GetColor().a + }; + if (UI::DrawColor4("Color", color)) { + light->SetColor(::XCEngine::Math::Color(color[0], color[1], color[2], color[3])); + changed = true; + } + + float intensity = light->GetIntensity(); + if (UI::DrawFloat("Intensity", intensity, 100.0f, 0.1f, 0.0f)) { + light->SetIntensity(intensity); + changed = true; + } + + if (light->GetLightType() != ::XCEngine::Components::LightType::Directional) { + float range = light->GetRange(); + if (UI::DrawFloat("Range", range, 100.0f, 0.1f, 0.001f)) { + light->SetRange(range); + changed = true; + } + } + + if (light->GetLightType() == ::XCEngine::Components::LightType::Spot) { + float spotAngle = light->GetSpotAngle(); + if (UI::DrawSliderFloat("Spot Angle", spotAngle, 1.0f, 179.0f, 100.0f, "%.1f")) { + light->SetSpotAngle(spotAngle); + changed = true; + } + } + + bool castsShadows = light->GetCastsShadows(); + if (UI::DrawBool("Cast Shadows", castsShadows)) { + light->SetCastsShadows(castsShadows); + changed = true; + } + + return changed; + } + + bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override { + return gameObject && !gameObject->GetComponent<::XCEngine::Components::LightComponent>(); + } + + const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override { + if (!gameObject) { + return "Invalid"; + } + return gameObject->GetComponent<::XCEngine::Components::LightComponent>() ? "Already Added" : nullptr; + } + + ::XCEngine::Components::Component* AddTo(::XCEngine::Components::GameObject* gameObject) const override { + return gameObject ? gameObject->AddComponent<::XCEngine::Components::LightComponent>() : nullptr; + } + + bool CanRemove(::XCEngine::Components::Component* component) const override { + return CanEdit(component); + } +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/ComponentEditors/TransformComponentEditor.h b/editor/src/ComponentEditors/TransformComponentEditor.h new file mode 100644 index 00000000..e94ab88f --- /dev/null +++ b/editor/src/ComponentEditors/TransformComponentEditor.h @@ -0,0 +1,67 @@ +#pragma once + +#include "IComponentEditor.h" +#include "UI/UI.h" + +#include + +namespace XCEngine { +namespace Editor { + +class TransformComponentEditor : public IComponentEditor { +public: + const char* GetDisplayName() const override { + return "Transform"; + } + + bool CanEdit(::XCEngine::Components::Component* component) const override { + return dynamic_cast<::XCEngine::Components::TransformComponent*>(component) != nullptr; + } + + bool Render(::XCEngine::Components::Component* component) override { + auto* transform = dynamic_cast<::XCEngine::Components::TransformComponent*>(component); + if (!transform) { + return false; + } + + ::XCEngine::Math::Vector3 position = transform->GetLocalPosition(); + ::XCEngine::Math::Vector3 rotation = transform->GetLocalEulerAngles(); + ::XCEngine::Math::Vector3 scale = transform->GetLocalScale(); + bool changed = false; + + if (UI::DrawVec3("Position", position, 0.0f, 80.0f, 0.1f)) { + transform->SetLocalPosition(position); + changed = true; + } + + if (UI::DrawVec3("Rotation", rotation, 0.0f, 80.0f, 1.0f)) { + transform->SetLocalEulerAngles(rotation); + changed = true; + } + + if (UI::DrawVec3("Scale", scale, 1.0f, 80.0f, 0.1f)) { + transform->SetLocalScale(scale); + changed = true; + } + + return changed; + } + + bool CanAddTo(::XCEngine::Components::GameObject* gameObject) const override { + (void)gameObject; + return false; + } + + const char* GetAddDisabledReason(::XCEngine::Components::GameObject* gameObject) const override { + (void)gameObject; + return "Built-in"; + } + + bool CanRemove(::XCEngine::Components::Component* component) const override { + (void)component; + return false; + } +}; + +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/Core/EditorConsoleSink.cpp b/editor/src/Core/EditorConsoleSink.cpp index aa77fa28..d0a07309 100644 --- a/editor/src/Core/EditorConsoleSink.cpp +++ b/editor/src/Core/EditorConsoleSink.cpp @@ -3,14 +3,22 @@ namespace XCEngine { namespace Debug { +EditorConsoleSink* EditorConsoleSink::s_instance = nullptr; + EditorConsoleSink* EditorConsoleSink::GetInstance() { - static EditorConsoleSink instance; - return &instance; + static EditorConsoleSink fallbackInstance; + return s_instance ? s_instance : &fallbackInstance; } -EditorConsoleSink::EditorConsoleSink() = default; +EditorConsoleSink::EditorConsoleSink() { + s_instance = this; +} -EditorConsoleSink::~EditorConsoleSink() = default; +EditorConsoleSink::~EditorConsoleSink() { + if (s_instance == this) { + s_instance = nullptr; + } +} void EditorConsoleSink::Log(const LogEntry& entry) { std::lock_guard lock(m_mutex); @@ -26,7 +34,8 @@ void EditorConsoleSink::Log(const LogEntry& entry) { void EditorConsoleSink::Flush() { } -const std::vector& EditorConsoleSink::GetLogs() const { +std::vector EditorConsoleSink::GetLogs() const { + std::lock_guard lock(m_mutex); return m_logs; } @@ -40,4 +49,4 @@ void EditorConsoleSink::SetCallback(std::function callback) { } } // namespace Debug -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/editor/src/Core/EditorConsoleSink.h b/editor/src/Core/EditorConsoleSink.h index 1b0280e1..22befcf9 100644 --- a/editor/src/Core/EditorConsoleSink.h +++ b/editor/src/Core/EditorConsoleSink.h @@ -19,7 +19,7 @@ public: void Log(const LogEntry& entry) override; void Flush() override; - const std::vector& GetLogs() const; + std::vector GetLogs() const; void Clear(); void SetCallback(std::function callback); @@ -27,8 +27,9 @@ private: mutable std::mutex m_mutex; std::vector m_logs; std::function m_callback; + static EditorConsoleSink* s_instance; static constexpr size_t MAX_LOGS = 1000; }; } // namespace Debug -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/editor/src/Core/EditorContext.h b/editor/src/Core/EditorContext.h index 60692433..f5df32be 100644 --- a/editor/src/Core/EditorContext.h +++ b/editor/src/Core/EditorContext.h @@ -19,7 +19,7 @@ public: EditorContext() : m_eventBus(std::make_unique()) , m_selectionManager(std::make_unique(*m_eventBus)) - , m_sceneManager(std::make_unique()) + , m_sceneManager(std::make_unique(m_eventBus.get())) , m_projectManager(std::make_unique()) { m_entityDeletedHandlerId = m_eventBus->Subscribe([this](const EntityDeletedEvent& event) { diff --git a/editor/src/Core/EventBus.h b/editor/src/Core/EventBus.h index 2ffab2bf..89f7d2a4 100644 --- a/editor/src/Core/EventBus.h +++ b/editor/src/Core/EventBus.h @@ -6,24 +6,30 @@ #include #include #include +#include #include #include namespace XCEngine { namespace Editor { -template -struct EventTypeId { - static uint32_t Get() { - static const uint32_t id = s_nextId++; - return id; +class EventTypeRegistry { +public: + static uint32_t NextId() { + return s_nextId.fetch_add(1, std::memory_order_relaxed); } + private: - static uint32_t s_nextId; + inline static std::atomic s_nextId{0}; }; template -uint32_t EventTypeId::s_nextId = 0; +struct EventTypeId { + static uint32_t Get() { + static const uint32_t id = EventTypeRegistry::NextId(); + return id; + } +}; class EventBus { public: diff --git a/editor/src/Core/ISceneManager.h b/editor/src/Core/ISceneManager.h index 94a12b51..ae35d1bf 100644 --- a/editor/src/Core/ISceneManager.h +++ b/editor/src/Core/ISceneManager.h @@ -22,8 +22,18 @@ public: virtual ::XCEngine::Components::GameObject::ID DuplicateEntity(::XCEngine::Components::GameObject::ID id) = 0; virtual void MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEngine::Components::GameObject::ID newParent) = 0; virtual bool HasClipboardData() const = 0; + virtual void NewScene(const std::string& name = "Untitled Scene") = 0; + virtual bool LoadScene(const std::string& filePath) = 0; + virtual bool SaveScene() = 0; + virtual bool SaveSceneAs(const std::string& filePath) = 0; + virtual bool LoadStartupScene(const std::string& projectPath) = 0; + virtual bool HasActiveScene() const = 0; + virtual bool IsSceneDirty() const = 0; + virtual void MarkSceneDirty() = 0; + virtual const std::string& GetCurrentScenePath() const = 0; + virtual const std::string& GetCurrentSceneName() const = 0; virtual void CreateDemoScene() = 0; }; } -} \ No newline at end of file +} diff --git a/editor/src/Layers/EditorLayer.cpp b/editor/src/Layers/EditorLayer.cpp index 33395dce..4b9d6bb9 100644 --- a/editor/src/Layers/EditorLayer.cpp +++ b/editor/src/Layers/EditorLayer.cpp @@ -8,6 +8,7 @@ #include "panels/ProjectPanel.h" #include "Core/IEditorContext.h" #include "Core/EditorContext.h" +#include #include #include @@ -41,6 +42,10 @@ void EditorLayer::onAttach() { m_consolePanel->SetContext(m_context.get()); m_projectPanel->SetContext(m_context.get()); + m_projectPanel->Initialize(m_context->GetProjectPath()); + m_context->GetSceneManager().LoadStartupScene(m_context->GetProjectPath()); + m_context->GetProjectManager().RefreshCurrentFolder(); + m_menuBar->OnAttach(); m_hierarchyPanel->OnAttach(); m_sceneViewPanel->OnAttach(); @@ -48,11 +53,18 @@ void EditorLayer::onAttach() { m_inspectorPanel->OnAttach(); m_consolePanel->OnAttach(); m_projectPanel->OnAttach(); - - m_projectPanel->Initialize(m_context->GetProjectPath()); } void EditorLayer::onDetach() { + auto& sceneManager = m_context->GetSceneManager(); + if (sceneManager.HasActiveScene() && sceneManager.IsSceneDirty()) { + if (!sceneManager.SaveScene()) { + const std::string fallbackPath = + (std::filesystem::path(m_context->GetProjectPath()) / "Assets" / "Scenes" / "Main.xc").string(); + sceneManager.SaveSceneAs(fallbackPath); + } + } + m_menuBar->OnDetach(); m_hierarchyPanel->OnDetach(); m_sceneViewPanel->OnDetach(); diff --git a/editor/src/Managers/ProjectManager.cpp b/editor/src/Managers/ProjectManager.cpp index b17fa718..b91416ef 100644 --- a/editor/src/Managers/ProjectManager.cpp +++ b/editor/src/Managers/ProjectManager.cpp @@ -89,7 +89,6 @@ void ProjectManager::Initialize(const std::string& projectPath) { std::ofstream((assetsPath / L"Textures" / L"Stone.png").wstring()); std::ofstream((assetsPath / L"Models" / L"Character.fbx").wstring()); std::ofstream((assetsPath / L"Scripts" / L"PlayerController.cs").wstring()); - std::ofstream((assetsPath / L"Scenes" / L"Main.unity").wstring()); } m_rootFolder = ScanDirectory(assetsPath.wstring()); @@ -227,7 +226,7 @@ AssetItemPtr ProjectManager::CreateAssetItem(const std::wstring& path, const std item->type = "Script"; } else if (ext == L".mat") { item->type = "Material"; - } else if (ext == L".unity" || ext == L".scene") { + } else if (ext == L".xc" || ext == L".unity" || ext == L".scene") { item->type = "Scene"; } else if (ext == L".prefab") { item->type = "Prefab"; @@ -240,4 +239,4 @@ AssetItemPtr ProjectManager::CreateAssetItem(const std::wstring& path, const std } } -} \ No newline at end of file +} diff --git a/editor/src/Managers/SceneManager.cpp b/editor/src/Managers/SceneManager.cpp index c8011e82..9e47272f 100644 --- a/editor/src/Managers/SceneManager.cpp +++ b/editor/src/Managers/SceneManager.cpp @@ -1,43 +1,106 @@ #include "SceneManager.h" +#include "Core/EventBus.h" +#include "Core/EditorEvents.h" +#include +#include +#include +#include #include +#include +#include +#include namespace XCEngine { namespace Editor { -SceneManager::SceneManager() = default; +namespace { + +std::pair SerializeComponent(const ::XCEngine::Components::Component* component) { + std::ostringstream payload; + component->Serialize(payload); + return { component->GetName(), payload.str() }; +} + +::XCEngine::Components::Component* CreateComponentByName( + ::XCEngine::Components::GameObject* gameObject, + const std::string& componentName) { + if (!gameObject) { + return nullptr; + } + + if (componentName == "Camera") { + return gameObject->AddComponent<::XCEngine::Components::CameraComponent>(); + } + if (componentName == "Light") { + return gameObject->AddComponent<::XCEngine::Components::LightComponent>(); + } + if (componentName == "AudioSource") { + return gameObject->AddComponent<::XCEngine::Components::AudioSourceComponent>(); + } + if (componentName == "AudioListener") { + return gameObject->AddComponent<::XCEngine::Components::AudioListenerComponent>(); + } + + return nullptr; +} + +} // namespace + +SceneManager::SceneManager(EventBus* eventBus) + : m_eventBus(eventBus) {} + +void SceneManager::SetSceneDirty(bool dirty) { + m_isSceneDirty = dirty; +} + +void SceneManager::MarkSceneDirty() { + SetSceneDirty(true); +} ::XCEngine::Components::GameObject* SceneManager::CreateEntity(const std::string& name, ::XCEngine::Components::GameObject* parent) { if (!m_scene) { - m_scene = new ::XCEngine::Components::Scene("EditorScene"); + m_scene = std::make_unique<::XCEngine::Components::Scene>("EditorScene"); } ::XCEngine::Components::GameObject* entity = m_scene->CreateGameObject(name, parent); - - if (parent == nullptr) { - m_rootEntities.push_back(entity); + const auto entityId = entity->GetID(); + SyncRootEntities(); + SetSceneDirty(true); + + OnEntityCreated.Invoke(entityId); + OnSceneChanged.Invoke(); + + if (m_eventBus) { + m_eventBus->Publish(EntityCreatedEvent{ entityId }); + m_eventBus->Publish(SceneChangedEvent{}); } - - OnEntityCreated.Invoke(entity->GetID()); + return entity; } void SceneManager::DeleteEntity(::XCEngine::Components::GameObject::ID id) { if (!m_scene) return; - ::XCEngine::Components::GameObject* entity = m_scene->Find(std::to_string(id)); + ::XCEngine::Components::GameObject* entity = m_scene->FindByID(id); if (!entity) return; std::vector<::XCEngine::Components::GameObject*> children = entity->GetChildren(); for (auto* child : children) { DeleteEntity(child->GetID()); } - - if (entity->GetParent() == nullptr) { - m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), entity), m_rootEntities.end()); - } - + + const auto entityId = entity->GetID(); m_scene->DestroyGameObject(entity); - OnEntityDeleted.Invoke(entity->GetID()); + SyncRootEntities(); + SetSceneDirty(true); + + OnEntityDeleted.Invoke(entityId); + OnSceneChanged.Invoke(); + + if (m_eventBus) { + m_eventBus->Publish(EntityDeletedEvent{ entityId }); + m_eventBus->Publish(SceneChangedEvent{}); + } } SceneManager::ClipboardData SceneManager::CopyEntityRecursive(const ::XCEngine::Components::GameObject* entity) { @@ -50,6 +113,14 @@ SceneManager::ClipboardData SceneManager::CopyEntityRecursive(const ::XCEngine:: data.localScale = transform->GetLocalScale(); } + auto components = entity->GetComponents<::XCEngine::Components::Component>(); + for (auto* component : components) { + if (!component || component == entity->GetTransform()) { + continue; + } + data.components.push_back(SerializeComponent(component)); + } + for (auto* child : entity->GetChildren()) { data.children.push_back(CopyEntityRecursive(child)); } @@ -60,7 +131,7 @@ SceneManager::ClipboardData SceneManager::CopyEntityRecursive(const ::XCEngine:: void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { if (!m_scene) return; - ::XCEngine::Components::GameObject* entity = m_scene->Find(std::to_string(id)); + ::XCEngine::Components::GameObject* entity = m_scene->FindByID(id); if (!entity) return; m_clipboard = CopyEntityRecursive(entity); @@ -69,7 +140,7 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { ::XCEngine::Components::GameObject::ID SceneManager::PasteEntityRecursive(const ClipboardData& data, ::XCEngine::Components::GameObject::ID parent) { ::XCEngine::Components::GameObject* parentObj = nullptr; if (parent != 0) { - parentObj = m_scene->Find(std::to_string(parent)); + parentObj = m_scene->FindByID(parent); } ::XCEngine::Components::GameObject* newEntity = m_scene->CreateGameObject(data.name, parentObj); @@ -79,9 +150,14 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { transform->SetLocalRotation(data.localRotation); transform->SetLocalScale(data.localScale); } - - if (parentObj == nullptr) { - m_rootEntities.push_back(newEntity); + + for (const auto& componentData : data.components) { + if (auto* component = CreateComponentByName(newEntity, componentData.first)) { + if (!componentData.second.empty()) { + std::istringstream payloadStream(componentData.second); + component->Deserialize(payloadStream); + } + } } for (const auto& childData : data.children) { @@ -93,13 +169,25 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { ::XCEngine::Components::GameObject::ID SceneManager::PasteEntity(::XCEngine::Components::GameObject::ID parent) { if (!m_clipboard || !m_scene) return 0; - return PasteEntityRecursive(*m_clipboard, parent); + + const auto newEntityId = PasteEntityRecursive(*m_clipboard, parent); + SyncRootEntities(); + + OnEntityCreated.Invoke(newEntityId); + OnSceneChanged.Invoke(); + + if (m_eventBus) { + m_eventBus->Publish(EntityCreatedEvent{ newEntityId }); + m_eventBus->Publish(SceneChangedEvent{}); + } + + return newEntityId; } ::XCEngine::Components::GameObject::ID SceneManager::DuplicateEntity(::XCEngine::Components::GameObject::ID id) { if (!m_scene) return 0; - ::XCEngine::Components::GameObject* entity = m_scene->Find(std::to_string(id)); + ::XCEngine::Components::GameObject* entity = m_scene->FindByID(id); if (!entity) return 0; CopyEntity(id); @@ -110,54 +198,210 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { return PasteEntity(parentId); } +void SceneManager::NewScene(const std::string& name) { + m_scene = std::make_unique<::XCEngine::Components::Scene>(name); + m_rootEntities.clear(); + m_clipboard.reset(); + m_currentScenePath.clear(); + m_currentSceneName = name; + SetSceneDirty(true); + + OnSceneChanged.Invoke(); + if (m_eventBus) { + m_eventBus->Publish(SceneChangedEvent{}); + } +} + +bool SceneManager::LoadScene(const std::string& filePath) { + namespace fs = std::filesystem; + + if (!fs::exists(filePath) || !fs::is_regular_file(filePath)) { + return false; + } + + auto scene = std::make_unique<::XCEngine::Components::Scene>(); + scene->Load(filePath); + + m_scene = std::move(scene); + m_clipboard.reset(); + SyncRootEntities(); + m_currentScenePath = filePath; + m_currentSceneName = m_scene ? m_scene->GetName() : "Untitled Scene"; + SetSceneDirty(false); + + OnSceneChanged.Invoke(); + if (m_eventBus) { + m_eventBus->Publish(SceneChangedEvent{}); + } + + return true; +} + +bool SceneManager::SaveScene() { + if (!m_scene) { + return false; + } + if (m_currentScenePath.empty()) { + return false; + } + + namespace fs = std::filesystem; + fs::path savePath = fs::path(m_currentScenePath).replace_extension(".xc"); + fs::create_directories(savePath.parent_path()); + + m_scene->Save(savePath.string()); + m_currentScenePath = savePath.string(); + SetSceneDirty(false); + return true; +} + +bool SceneManager::SaveSceneAs(const std::string& filePath) { + if (!m_scene || filePath.empty()) { + return false; + } + + namespace fs = std::filesystem; + fs::path savePath = fs::path(filePath).replace_extension(".xc"); + fs::create_directories(savePath.parent_path()); + + m_scene->Save(savePath.string()); + m_currentScenePath = savePath.string(); + m_currentSceneName = m_scene->GetName(); + SetSceneDirty(false); + return true; +} + +bool SceneManager::LoadStartupScene(const std::string& projectPath) { + const std::string defaultScenePath = ResolveDefaultScenePath(projectPath); + if (IsSceneFileUsable(defaultScenePath) && LoadScene(defaultScenePath)) { + return true; + } + + CreateDemoScene(); + return SaveSceneAs(defaultScenePath); +} + void SceneManager::RenameEntity(::XCEngine::Components::GameObject::ID id, const std::string& newName) { if (!m_scene) return; - ::XCEngine::Components::GameObject* obj = m_scene->Find(std::to_string(id)); + ::XCEngine::Components::GameObject* obj = m_scene->FindByID(id); if (!obj) return; obj->SetName(newName); + SetSceneDirty(true); OnEntityChanged.Invoke(id); + + if (m_eventBus) { + m_eventBus->Publish(EntityChangedEvent{ id }); + } } void SceneManager::MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEngine::Components::GameObject::ID newParentId) { if (!m_scene) return; - ::XCEngine::Components::GameObject* obj = m_scene->Find(std::to_string(id)); + ::XCEngine::Components::GameObject* obj = m_scene->FindByID(id); if (!obj) return; + const auto oldParentId = obj->GetParent() ? obj->GetParent()->GetID() : 0; ::XCEngine::Components::GameObject* newParent = nullptr; if (newParentId != 0) { - newParent = m_scene->Find(std::to_string(newParentId)); + newParent = m_scene->FindByID(newParentId); } obj->SetParent(newParent); + SyncRootEntities(); + SetSceneDirty(true); + OnEntityChanged.Invoke(id); + OnSceneChanged.Invoke(); + + if (m_eventBus) { + m_eventBus->Publish(EntityParentChangedEvent{ id, oldParentId, newParentId }); + m_eventBus->Publish(EntityChangedEvent{ id }); + m_eventBus->Publish(SceneChangedEvent{}); + } } void SceneManager::CreateDemoScene() { - if (m_scene) { - delete m_scene; - } - m_scene = new ::XCEngine::Components::Scene("DemoScene"); + m_scene = std::make_unique<::XCEngine::Components::Scene>("Main Scene"); m_rootEntities.clear(); m_clipboard.reset(); + m_currentScenePath.clear(); + m_currentSceneName = m_scene->GetName(); + SetSceneDirty(true); ::XCEngine::Components::GameObject* camera = CreateEntity("Main Camera", nullptr); - camera->AddComponent<::XCEngine::Components::TransformComponent>(); + camera->AddComponent<::XCEngine::Components::CameraComponent>(); ::XCEngine::Components::GameObject* light = CreateEntity("Directional Light", nullptr); + light->AddComponent<::XCEngine::Components::LightComponent>(); - ::XCEngine::Components::GameObject* cube = CreateEntity("Cube", nullptr); - cube->AddComponent<::XCEngine::Components::TransformComponent>(); - - ::XCEngine::Components::GameObject* sphere = CreateEntity("Sphere", nullptr); - sphere->AddComponent<::XCEngine::Components::TransformComponent>(); - - ::XCEngine::Components::GameObject* player = CreateEntity("Player", nullptr); - ::XCEngine::Components::GameObject* weapon = CreateEntity("Weapon", player); - + SyncRootEntities(); OnSceneChanged.Invoke(); + + if (m_eventBus) { + m_eventBus->Publish(SceneChangedEvent{}); + } +} + +void SceneManager::SyncRootEntities() { + if (!m_scene) { + m_rootEntities.clear(); + m_currentSceneName = "Untitled Scene"; + SetSceneDirty(false); + return; + } + + m_rootEntities = m_scene->GetRootGameObjects(); + m_currentSceneName = m_scene->GetName(); +} + +std::string SceneManager::GetScenesDirectory(const std::string& projectPath) { + return (std::filesystem::path(projectPath) / "Assets" / "Scenes").string(); +} + +std::string SceneManager::ResolveDefaultScenePath(const std::string& projectPath) { + namespace fs = std::filesystem; + + const fs::path scenesDir = GetScenesDirectory(projectPath); + const fs::path mainXC = scenesDir / "Main.xc"; + if (fs::exists(mainXC)) { + return mainXC.string(); + } + + const fs::path mainScene = scenesDir / "Main.scene"; + if (fs::exists(mainScene)) { + return mainScene.string(); + } + + const fs::path mainUnity = scenesDir / "Main.unity"; + if (fs::exists(mainUnity)) { + return mainUnity.string(); + } + + if (fs::exists(scenesDir)) { + for (const auto& entry : fs::directory_iterator(scenesDir)) { + if (!entry.is_regular_file()) { + continue; + } + + const fs::path ext = entry.path().extension(); + if (ext == ".xc" || ext == ".scene" || ext == ".unity") { + return entry.path().string(); + } + } + } + + return mainXC.string(); +} + +bool SceneManager::IsSceneFileUsable(const std::string& filePath) { + namespace fs = std::filesystem; + + return !filePath.empty() && + fs::exists(filePath) && + fs::is_regular_file(filePath) && + fs::file_size(filePath) > 0; } } diff --git a/editor/src/Managers/SceneManager.h b/editor/src/Managers/SceneManager.h index 2c67b81a..9a7e486f 100644 --- a/editor/src/Managers/SceneManager.h +++ b/editor/src/Managers/SceneManager.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -18,9 +19,11 @@ namespace XCEngine { namespace Editor { +class EventBus; + class SceneManager : public ISceneManager { public: - SceneManager(); + explicit SceneManager(EventBus* eventBus = nullptr); ::XCEngine::Components::GameObject* CreateEntity(const std::string& name, ::XCEngine::Components::GameObject* parent = nullptr); @@ -45,7 +48,17 @@ public: ::XCEngine::Components::GameObject::ID DuplicateEntity(::XCEngine::Components::GameObject::ID id); void MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEngine::Components::GameObject::ID newParent); - void CreateDemoScene(); + void NewScene(const std::string& name = "Untitled Scene") override; + bool LoadScene(const std::string& filePath) override; + bool SaveScene() override; + bool SaveSceneAs(const std::string& filePath) override; + bool LoadStartupScene(const std::string& projectPath) override; + bool HasActiveScene() const override { return m_scene != nullptr; } + bool IsSceneDirty() const override { return m_isSceneDirty; } + void MarkSceneDirty() override; + const std::string& GetCurrentScenePath() const override { return m_currentScenePath; } + const std::string& GetCurrentSceneName() const override { return m_currentSceneName; } + void CreateDemoScene() override; bool HasClipboardData() const { return m_clipboard.has_value(); } @@ -60,15 +73,25 @@ private: Math::Vector3 localPosition = Math::Vector3::Zero(); Math::Quaternion localRotation = Math::Quaternion::Identity(); Math::Vector3 localScale = Math::Vector3::One(); + std::vector> components; std::vector children; }; ClipboardData CopyEntityRecursive(const ::XCEngine::Components::GameObject* entity); ::XCEngine::Components::GameObject::ID PasteEntityRecursive(const ClipboardData& data, ::XCEngine::Components::GameObject::ID parent); + void SetSceneDirty(bool dirty); + void SyncRootEntities(); + static std::string GetScenesDirectory(const std::string& projectPath); + static std::string ResolveDefaultScenePath(const std::string& projectPath); + static bool IsSceneFileUsable(const std::string& filePath); - ::XCEngine::Components::Scene* m_scene = nullptr; + std::unique_ptr<::XCEngine::Components::Scene> m_scene; std::vector<::XCEngine::Components::GameObject*> m_rootEntities; std::optional m_clipboard; + EventBus* m_eventBus = nullptr; + std::string m_currentScenePath; + std::string m_currentSceneName = "Untitled Scene"; + bool m_isSceneDirty = false; }; } diff --git a/editor/src/Utils/SceneEditorUtils.h b/editor/src/Utils/SceneEditorUtils.h new file mode 100644 index 00000000..dbedb0dc --- /dev/null +++ b/editor/src/Utils/SceneEditorUtils.h @@ -0,0 +1,196 @@ +#pragma once + +#include "Core/IEditorContext.h" +#include "Core/IProjectManager.h" +#include "Core/ISceneManager.h" + +#ifndef NOMINMAX +#define NOMINMAX +#endif + +#include +#include + +#include +#include +#include + +namespace XCEngine { +namespace Editor { +namespace SceneEditorUtils { + +inline std::wstring Utf8ToWide(const std::string& value) { + if (value.empty()) { + return {}; + } + + const int length = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0); + if (length <= 0) { + return {}; + } + + std::wstring result(length - 1, L'\0'); + MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, &result[0], length); + return result; +} + +inline std::string WideToUtf8(const std::wstring& value) { + if (value.empty()) { + return {}; + } + + const int length = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, nullptr, 0, nullptr, nullptr); + if (length <= 0) { + return {}; + } + + std::string result(length - 1, '\0'); + WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, &result[0], length, nullptr, nullptr); + return result; +} + +inline std::string SanitizeSceneFileName(const std::string& value) { + std::string result = value.empty() ? "Untitled Scene" : value; + for (char& ch : result) { + switch (ch) { + case '\\': + case '/': + case ':': + case '*': + case '?': + case '"': + case '<': + case '>': + case '|': + ch = '_'; + break; + default: + if (static_cast(ch) < 32u) { + ch = '_'; + } + break; + } + } + return result; +} + +inline HWND GetDialogOwnerWindow() { + HWND owner = GetActiveWindow(); + if (!owner) { + owner = GetForegroundWindow(); + } + return owner; +} + +inline const wchar_t* GetSceneFilter() { + static const wchar_t filter[] = + L"XCEngine Scene (*.xc)\0*.xc\0Legacy Scene (*.scene;*.unity)\0*.scene;*.unity\0All Files (*.*)\0*.*\0\0"; + return filter; +} + +inline std::string OpenSceneFileDialog(const std::string& projectPath, const std::string& initialPath = {}) { + namespace fs = std::filesystem; + + const fs::path scenesDir = fs::path(projectPath) / "Assets" / "Scenes"; + const std::wstring initialDir = Utf8ToWide(scenesDir.string()); + + std::array fileBuffer{}; + if (!initialPath.empty()) { + const std::wstring initialFile = Utf8ToWide(initialPath); + wcsncpy_s(fileBuffer.data(), fileBuffer.size(), initialFile.c_str(), _TRUNCATE); + } + + OPENFILENAMEW dialog{}; + dialog.lStructSize = sizeof(dialog); + dialog.hwndOwner = GetDialogOwnerWindow(); + dialog.lpstrFilter = GetSceneFilter(); + dialog.lpstrFile = fileBuffer.data(); + dialog.nMaxFile = static_cast(fileBuffer.size()); + dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str(); + dialog.Flags = OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR; + + if (!GetOpenFileNameW(&dialog)) { + return {}; + } + + return WideToUtf8(fileBuffer.data()); +} + +inline std::string SaveSceneFileDialog( + const std::string& projectPath, + const std::string& currentScenePath, + const std::string& currentSceneName) { + namespace fs = std::filesystem; + + const fs::path scenesDir = fs::path(projectPath) / "Assets" / "Scenes"; + const fs::path suggestedPath = currentScenePath.empty() + ? scenesDir / (SanitizeSceneFileName(currentSceneName) + ".xc") + : fs::path(currentScenePath).replace_extension(".xc"); + + std::array fileBuffer{}; + const std::wstring suggestedWide = Utf8ToWide(suggestedPath.string()); + wcsncpy_s(fileBuffer.data(), fileBuffer.size(), suggestedWide.c_str(), _TRUNCATE); + + const std::wstring initialDir = Utf8ToWide(suggestedPath.parent_path().string()); + OPENFILENAMEW dialog{}; + dialog.lStructSize = sizeof(dialog); + dialog.hwndOwner = GetDialogOwnerWindow(); + dialog.lpstrFilter = GetSceneFilter(); + dialog.lpstrFile = fileBuffer.data(); + dialog.nMaxFile = static_cast(fileBuffer.size()); + dialog.lpstrInitialDir = initialDir.empty() ? nullptr : initialDir.c_str(); + dialog.lpstrDefExt = L"xc"; + dialog.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR; + + if (!GetSaveFileNameW(&dialog)) { + return {}; + } + + return WideToUtf8(fileBuffer.data()); +} + +inline bool SaveCurrentScene(IEditorContext& context) { + auto& sceneManager = context.GetSceneManager(); + if (sceneManager.SaveScene()) { + return true; + } + + const std::string filePath = SaveSceneFileDialog( + context.GetProjectPath(), + sceneManager.GetCurrentScenePath(), + sceneManager.GetCurrentSceneName()); + if (filePath.empty()) { + return false; + } + + const bool saved = sceneManager.SaveSceneAs(filePath); + if (saved) { + context.GetProjectManager().RefreshCurrentFolder(); + } + return saved; +} + +inline bool ConfirmSceneSwitch(IEditorContext& context) { + auto& sceneManager = context.GetSceneManager(); + if (!sceneManager.HasActiveScene() || !sceneManager.IsSceneDirty()) { + return true; + } + + const int result = MessageBoxW( + GetDialogOwnerWindow(), + L"Save changes to the current scene before continuing?", + L"Unsaved Scene Changes", + MB_YESNOCANCEL | MB_ICONWARNING); + + if (result == IDCANCEL) { + return false; + } + if (result == IDYES) { + return SaveCurrentScene(context); + } + return true; +} + +} // namespace SceneEditorUtils +} // namespace Editor +} // namespace XCEngine diff --git a/editor/src/panels/ConsolePanel.cpp b/editor/src/panels/ConsolePanel.cpp index eef70b9c..d03ed8b0 100644 --- a/editor/src/panels/ConsolePanel.cpp +++ b/editor/src/panels/ConsolePanel.cpp @@ -11,9 +11,11 @@ ConsolePanel::ConsolePanel() : Panel("Console") { void ConsolePanel::Render() { ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); + + auto* sink = Debug::EditorConsoleSink::GetInstance(); if (ImGui::Button("Clear")) { - Debug::EditorConsoleSink::GetInstance()->Clear(); + sink->Clear(); } ImGui::SameLine(); @@ -52,7 +54,7 @@ void ConsolePanel::Render() { ImGui::BeginChild("LogScroll", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); - const auto& logs = Debug::EditorConsoleSink::GetInstance()->GetLogs(); + const auto logs = sink->GetLogs(); size_t logIndex = 0; for (const auto& log : logs) { bool shouldShow = false; @@ -117,4 +119,4 @@ void ConsolePanel::Render() { } } -} \ No newline at end of file +} diff --git a/editor/src/panels/HierarchyPanel.cpp b/editor/src/panels/HierarchyPanel.cpp index 80218e93..fa1c9e0b 100644 --- a/editor/src/panels/HierarchyPanel.cpp +++ b/editor/src/panels/HierarchyPanel.cpp @@ -6,6 +6,8 @@ #include "Core/EventBus.h" #include #include +#include +#include #include #include @@ -22,9 +24,6 @@ HierarchyPanel::~HierarchyPanel() { } void HierarchyPanel::OnAttach() { - auto& sceneManager = m_context->GetSceneManager(); - sceneManager.CreateDemoScene(); - m_selectionHandlerId = m_context->GetEventBus().Subscribe( [this](const SelectionChangedEvent& event) { OnSelectionChanged(event); @@ -74,7 +73,16 @@ void HierarchyPanel::Render() { ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; if (sourceGameObject && sourceGameObject->GetParent() != nullptr) { auto& sceneManager = m_context->GetSceneManager(); + auto* srcTransform = sourceGameObject->GetTransform(); + Math::Vector3 worldPos = srcTransform->GetPosition(); + Math::Quaternion worldRot = srcTransform->GetRotation(); + Math::Vector3 worldScale = srcTransform->GetScale(); + sceneManager.MoveEntity(sourceGameObject->GetID(), 0); + + srcTransform->SetPosition(worldPos); + srcTransform->SetRotation(worldRot); + srcTransform->SetScale(worldScale); } } ImGui::EndDragDropTarget(); @@ -195,7 +203,7 @@ void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* gameO if (gameObject != nullptr && gameObject->GetParent() != nullptr) { if (ImGui::MenuItem("Detach from Parent")) { - gameObject->DetachFromParent(); + sceneManager.MoveEntity(gameObject->GetID(), 0); } } @@ -242,12 +250,13 @@ void HierarchyPanel::RenderCreateMenu(::XCEngine::Components::GameObject* parent if (ImGui::MenuItem("Camera")) { auto* newEntity = sceneManager.CreateEntity("Camera", parent); - newEntity->AddComponent<::XCEngine::Components::TransformComponent>(); + newEntity->AddComponent<::XCEngine::Components::CameraComponent>(); selectionManager.SetSelectedEntity(newEntity->GetID()); } if (ImGui::MenuItem("Light")) { auto* newEntity = sceneManager.CreateEntity("Light", parent); + newEntity->AddComponent<::XCEngine::Components::LightComponent>(); selectionManager.SetSelectedEntity(newEntity->GetID()); } @@ -255,19 +264,16 @@ void HierarchyPanel::RenderCreateMenu(::XCEngine::Components::GameObject* parent if (ImGui::MenuItem("Cube")) { auto* newEntity = sceneManager.CreateEntity("Cube", parent); - newEntity->AddComponent<::XCEngine::Components::TransformComponent>(); selectionManager.SetSelectedEntity(newEntity->GetID()); } if (ImGui::MenuItem("Sphere")) { auto* newEntity = sceneManager.CreateEntity("Sphere", parent); - newEntity->AddComponent<::XCEngine::Components::TransformComponent>(); selectionManager.SetSelectedEntity(newEntity->GetID()); } if (ImGui::MenuItem("Plane")) { auto* newEntity = sceneManager.CreateEntity("Plane", parent); - newEntity->AddComponent<::XCEngine::Components::TransformComponent>(); selectionManager.SetSelectedEntity(newEntity->GetID()); } } diff --git a/editor/src/panels/InspectorPanel.cpp b/editor/src/panels/InspectorPanel.cpp index 3af5c999..1214ab81 100644 --- a/editor/src/panels/InspectorPanel.cpp +++ b/editor/src/panels/InspectorPanel.cpp @@ -1,8 +1,13 @@ #include "InspectorPanel.h" -#include "Core/EditorContext.h" +#include "Core/IEditorContext.h" #include "Core/ISceneManager.h" -#include "UI/UI.h" -#include +#include "Core/ISelectionManager.h" +#include "Core/EventBus.h" +#include "Core/EditorEvents.h" +#include "ComponentEditors/CameraComponentEditor.h" +#include "ComponentEditors/IComponentEditor.h" +#include "ComponentEditors/LightComponentEditor.h" +#include "ComponentEditors/TransformComponentEditor.h" #include #include @@ -10,7 +15,7 @@ namespace XCEngine { namespace Editor { InspectorPanel::InspectorPanel() : Panel("Inspector") { - Debug::Logger::Get().Debug(Debug::LogCategory::General, "InspectorPanel constructed"); + RegisterDefaultComponentEditors(); } InspectorPanel::~InspectorPanel() { @@ -23,8 +28,35 @@ void InspectorPanel::OnSelectionChanged(const SelectionChangedEvent& event) { m_selectedEntityId = event.primarySelection; } +void InspectorPanel::RegisterDefaultComponentEditors() { + RegisterComponentEditor(std::make_unique()); + RegisterComponentEditor(std::make_unique()); + RegisterComponentEditor(std::make_unique()); +} + +void InspectorPanel::RegisterComponentEditor(std::unique_ptr editor) { + if (!editor) { + return; + } + + m_componentEditors.push_back(std::move(editor)); +} + +IComponentEditor* InspectorPanel::GetEditorFor(::XCEngine::Components::Component* component) const { + if (!component) { + return nullptr; + } + + for (const auto& editor : m_componentEditors) { + if (editor && editor->CanEdit(component)) { + return editor.get(); + } + } + + return nullptr; +} + void InspectorPanel::Render() { - Debug::Logger::Get().Debug(Debug::LogCategory::General, "InspectorPanel::Render START"); ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); if (!m_selectionHandlerId && m_context) { @@ -51,21 +83,18 @@ void InspectorPanel::Render() { } ImGui::End(); - Debug::Logger::Get().Debug(Debug::LogCategory::General, "InspectorPanel::Render END"); } void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameObject) { - Debug::Logger::Get().Debug(Debug::LogCategory::General, "RenderGameObject START"); char nameBuffer[256]; strcpy_s(nameBuffer, gameObject->GetName().c_str()); ImGui::InputText("##Name", nameBuffer, sizeof(nameBuffer)); if (ImGui::IsItemDeactivatedAfterEdit()) { - gameObject->SetName(nameBuffer); + m_context->GetSceneManager().RenameEntity(gameObject->GetID(), nameBuffer); } ImGui::SameLine(); if (ImGui::Button("Add Component")) { - Debug::Logger::Get().Debug(Debug::LogCategory::General, "Add Component BUTTON CLICKED"); ImGui::OpenPopup("AddComponent"); } @@ -75,53 +104,54 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb for (auto* component : components) { RenderComponent(component, gameObject); } - Debug::Logger::Get().Debug(Debug::LogCategory::General, "RenderGameObject END"); } void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject) { - Debug::Logger::Get().Debug(Debug::LogCategory::General, "RenderAddComponentPopup called"); - - if (!gameObject) { - Debug::Logger::Get().Error(Debug::LogCategory::General, "ERROR: gameObject is nullptr!"); - return; - } - if (!ImGui::BeginPopup("AddComponent")) { - Debug::Logger::Get().Debug(Debug::LogCategory::General, "BeginPopup returned false"); return; } - - Debug::Logger::Get().Debug(Debug::LogCategory::General, "BeginPopup succeeded"); - + ImGui::Text("Components"); ImGui::Separator(); - - bool hasTransform = gameObject->GetComponent<::XCEngine::Components::TransformComponent>() != nullptr; - Debug::Logger::Get().Debug(Debug::LogCategory::General, hasTransform ? "Has Transform: yes" : "Has Transform: no"); - - Debug::Logger::Get().Debug(Debug::LogCategory::General, "About to check MenuItem condition"); - if (ImGui::MenuItem("Transform", nullptr, false, !hasTransform)) { - Debug::Logger::Get().Debug(Debug::LogCategory::General, "MenuItem CLICKED! Before AddComponent"); - - auto* newComp = gameObject->AddComponent<::XCEngine::Components::TransformComponent>(); - Debug::Logger::Get().Debug(Debug::LogCategory::General, newComp ? "AddComponent SUCCEEDED" : "AddComponent FAILED"); - - ImGui::CloseCurrentPopup(); - } else { - Debug::Logger::Get().Debug(Debug::LogCategory::General, "MenuItem not clicked (disabled or condition false)"); + + bool drewAnyEntry = false; + for (const auto& editor : m_componentEditors) { + if (!editor || !editor->ShowInAddComponentMenu()) { + continue; + } + + drewAnyEntry = true; + const bool canAdd = editor->CanAddTo(gameObject); + std::string label = editor->GetDisplayName(); + if (!canAdd) { + const char* reason = editor->GetAddDisabledReason(gameObject); + if (reason && reason[0] != '\0') { + label += " ("; + label += reason; + label += ")"; + } + } + + if (ImGui::MenuItem(label.c_str(), nullptr, false, canAdd)) { + if (editor->AddTo(gameObject)) { + m_context->GetSceneManager().MarkSceneDirty(); + ImGui::CloseCurrentPopup(); + } + } } - - ImGui::Separator(); - ImGui::TextDisabled("No more components available"); - - Debug::Logger::Get().Debug(Debug::LogCategory::General, "About to EndPopup - calling EndPopup"); + + if (!drewAnyEntry) { + ImGui::TextDisabled("No registered component editors"); + } + ImGui::EndPopup(); - Debug::Logger::Get().Debug(Debug::LogCategory::General, "Popup closed"); } void InspectorPanel::RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject) { if (!component) return; + IComponentEditor* editor = GetEditorFor(component); + const char* name = component->GetName().c_str(); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 2}); @@ -138,8 +168,9 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen ImGui::PopStyleVar(); bool removeComponent = false; + const bool canRemoveComponent = editor ? editor->CanRemove(component) : false; if (ImGui::BeginPopupContextItem("ComponentSettings")) { - if (ImGui::MenuItem("Remove Component")) { + if (ImGui::MenuItem("Remove Component", nullptr, false, canRemoveComponent)) { removeComponent = true; } ImGui::EndPopup(); @@ -151,22 +182,12 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen } if (open) { - if (auto* transform = dynamic_cast<::XCEngine::Components::TransformComponent*>(component)) { - ::XCEngine::Math::Vector3 position = transform->GetLocalPosition(); - ::XCEngine::Math::Vector3 rotation = transform->GetLocalEulerAngles(); - ::XCEngine::Math::Vector3 scale = transform->GetLocalScale(); - - if (UI::DrawVec3("Position", position, 0.0f, 80.0f, 0.1f)) { - transform->SetLocalPosition(position); - } - - if (UI::DrawVec3("Rotation", rotation, 0.0f, 80.0f, 1.0f)) { - transform->SetLocalEulerAngles(rotation); - } - - if (UI::DrawVec3("Scale", scale, 1.0f, 80.0f, 0.1f)) { - transform->SetLocalScale(scale); + if (editor) { + if (editor->Render(component)) { + m_context->GetSceneManager().MarkSceneDirty(); } + } else { + ImGui::TextDisabled("No registered editor for this component"); } ImGui::TreePop(); @@ -179,14 +200,9 @@ void InspectorPanel::RemoveComponentByType(::XCEngine::Components::Component* co if (dynamic_cast<::XCEngine::Components::TransformComponent*>(component)) { return; } - - auto components = gameObject->GetComponents<::XCEngine::Components::Component>(); - for (auto* comp : components) { - if (comp == component) { - gameObject->RemoveComponent<::XCEngine::Components::Component>(); - break; - } - } + + gameObject->RemoveComponent(component); + m_context->GetSceneManager().MarkSceneDirty(); } } diff --git a/editor/src/panels/InspectorPanel.h b/editor/src/panels/InspectorPanel.h index e579ff81..86e6736f 100644 --- a/editor/src/panels/InspectorPanel.h +++ b/editor/src/panels/InspectorPanel.h @@ -1,11 +1,21 @@ #pragma once #include "Panel.h" -#include + +#include +#include +#include namespace XCEngine { +namespace Components { +class Component; +class GameObject; +} + namespace Editor { +class IComponentEditor; + class InspectorPanel : public Panel { public: InspectorPanel(); @@ -14,6 +24,9 @@ public: void Render() override; private: + void RegisterDefaultComponentEditors(); + void RegisterComponentEditor(std::unique_ptr editor); + IComponentEditor* GetEditorFor(::XCEngine::Components::Component* component) const; void RenderGameObject(::XCEngine::Components::GameObject* gameObject); void RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject); void RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject); @@ -22,6 +35,7 @@ private: uint64_t m_selectionHandlerId = 0; uint64_t m_selectedEntityId = 0; + std::vector> m_componentEditors; }; } diff --git a/editor/src/panels/MenuBar.cpp b/editor/src/panels/MenuBar.cpp index 5b1bc60d..1c75b713 100644 --- a/editor/src/panels/MenuBar.cpp +++ b/editor/src/panels/MenuBar.cpp @@ -1,4 +1,8 @@ #include "MenuBar.h" +#include "Core/IEditorContext.h" +#include "Core/ISceneManager.h" +#include "Utils/SceneEditorUtils.h" +#include #include namespace XCEngine { @@ -7,20 +11,103 @@ namespace Editor { MenuBar::MenuBar() : Panel("MenuBar") {} void MenuBar::Render() { + HandleShortcuts(); + if (ImGui::BeginMainMenuBar()) { ShowFileMenu(); ShowEditMenu(); ShowViewMenu(); ShowHelpMenu(); + RenderSceneStatus(); ImGui::EndMainMenuBar(); } } +void MenuBar::NewScene() { + if (!m_context || !SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { + return; + } + + m_context->GetSceneManager().NewScene(); +} + +void MenuBar::OpenScene() { + if (!m_context || !SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { + return; + } + + const std::string filePath = SceneEditorUtils::OpenSceneFileDialog( + m_context->GetProjectPath(), + m_context->GetSceneManager().GetCurrentScenePath()); + if (!filePath.empty()) { + m_context->GetSceneManager().LoadScene(filePath); + } +} + +void MenuBar::SaveScene() { + if (!m_context) { + return; + } + + if (!SceneEditorUtils::SaveCurrentScene(*m_context)) { + return; + } +} + +void MenuBar::SaveSceneAs() { + if (!m_context) { + return; + } + + const std::string filePath = SceneEditorUtils::SaveSceneFileDialog( + m_context->GetProjectPath(), + m_context->GetSceneManager().GetCurrentScenePath(), + m_context->GetSceneManager().GetCurrentSceneName()); + if (!filePath.empty() && m_context->GetSceneManager().SaveSceneAs(filePath)) { + m_context->GetProjectManager().RefreshCurrentFolder(); + } +} + +void MenuBar::HandleShortcuts() { + if (!m_context) { + return; + } + + ImGuiIO& io = ImGui::GetIO(); + if (!io.KeyCtrl) { + return; + } + + auto& sceneManager = m_context->GetSceneManager(); + if (ImGui::IsKeyPressed(ImGuiKey_N, false)) { + NewScene(); + } + if (ImGui::IsKeyPressed(ImGuiKey_O, false)) { + OpenScene(); + } + if (ImGui::IsKeyPressed(ImGuiKey_S, false)) { + if (io.KeyShift) { + SaveSceneAs(); + } else { + SaveScene(); + } + } +} + void MenuBar::ShowFileMenu() { if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("New Scene", "Ctrl+N")) {} - if (ImGui::MenuItem("Open Scene", "Ctrl+O")) {} - if (ImGui::MenuItem("Save Scene", "Ctrl+S")) {} + if (ImGui::MenuItem("New Scene", "Ctrl+N")) { + NewScene(); + } + if (ImGui::MenuItem("Open Scene", "Ctrl+O")) { + OpenScene(); + } + if (ImGui::MenuItem("Save Scene", "Ctrl+S")) { + SaveScene(); + } + if (ImGui::MenuItem("Save Scene As...", "Ctrl+Shift+S")) { + SaveSceneAs(); + } ImGui::Separator(); if (ImGui::MenuItem("Exit", "Alt+F4")) {} ImGui::EndMenu(); @@ -53,5 +140,45 @@ void MenuBar::ShowHelpMenu() { } } +void MenuBar::RenderSceneStatus() { + if (!m_context) { + return; + } + + auto& sceneManager = m_context->GetSceneManager(); + std::string sceneLabel = sceneManager.HasActiveScene() ? sceneManager.GetCurrentSceneName() : "No Scene"; + if (sceneLabel.empty()) { + sceneLabel = "Untitled Scene"; + } + + std::string fileLabel = sceneManager.GetCurrentScenePath().empty() + ? "Unsaved.xc" + : std::filesystem::path(sceneManager.GetCurrentScenePath()).filename().string(); + + const bool dirty = sceneManager.IsSceneDirty(); + const std::string statusText = std::string("Scene: ") + fileLabel + (dirty ? " Modified" : " Saved"); + const ImVec2 textSize = ImGui::CalcTextSize(statusText.c_str()); + const float targetX = ImGui::GetWindowWidth() - textSize.x - 20.0f; + if (targetX > ImGui::GetCursorPosX()) { + ImGui::SetCursorPosX(targetX); + } + + const ImVec4 accentColor = dirty + ? ImVec4(0.94f, 0.68f, 0.20f, 1.0f) + : ImVec4(0.48f, 0.78f, 0.49f, 1.0f); + ImGui::TextColored(accentColor, "%s", statusText.c_str()); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::Text("Scene"); + ImGui::Separator(); + ImGui::Text("Name: %s", sceneLabel.c_str()); + ImGui::Text("File: %s", fileLabel.c_str()); + ImGui::Text("State: %s", dirty ? "Modified" : "Saved"); + ImGui::Text("Path: %s", sceneManager.GetCurrentScenePath().empty() ? "(not saved yet)" : sceneManager.GetCurrentScenePath().c_str()); + ImGui::EndTooltip(); + } +} + +} } -} \ No newline at end of file diff --git a/editor/src/panels/MenuBar.h b/editor/src/panels/MenuBar.h index 60aea6de..bf0fea7b 100644 --- a/editor/src/panels/MenuBar.h +++ b/editor/src/panels/MenuBar.h @@ -11,6 +11,12 @@ public: void Render() override; private: + void HandleShortcuts(); + void NewScene(); + void OpenScene(); + void SaveScene(); + void SaveSceneAs(); + void RenderSceneStatus(); void ShowFileMenu(); void ShowEditMenu(); void ShowViewMenu(); @@ -18,4 +24,4 @@ private: }; } -} \ No newline at end of file +} diff --git a/editor/src/panels/ProjectPanel.cpp b/editor/src/panels/ProjectPanel.cpp index 2a307ec7..a7c51960 100644 --- a/editor/src/panels/ProjectPanel.cpp +++ b/editor/src/panels/ProjectPanel.cpp @@ -1,7 +1,9 @@ #include "ProjectPanel.h" #include "Core/IEditorContext.h" #include "Core/IProjectManager.h" +#include "Core/ISceneManager.h" #include "Core/AssetItem.h" +#include "Utils/SceneEditorUtils.h" #include #include @@ -83,7 +85,7 @@ void ProjectPanel::Render() { auto& items = manager.GetCurrentItems(); std::string searchStr = m_searchBuffer; - int itemIndex = 0; + int displayedCount = 0; for (int i = 0; i < (int)items.size(); i++) { if (!searchStr.empty()) { @@ -92,11 +94,11 @@ void ProjectPanel::Render() { } } - if (itemIndex > 0 && itemIndex % columns != 0) { + if (displayedCount > 0 && displayedCount % columns != 0) { ImGui::SameLine(); } - RenderAssetItem(items[i], itemIndex); - itemIndex++; + RenderAssetItem(items[i], i); + displayedCount++; } ImGui::PopStyleVar(); @@ -108,9 +110,13 @@ void ProjectPanel::Render() { if (ImGui::BeginPopup("ItemContextMenu")) { if (m_contextMenuIndex >= 0 && m_contextMenuIndex < (int)items.size()) { auto& item = items[m_contextMenuIndex]; - if (item->isFolder) { + if (item->isFolder || item->type == "Scene") { if (ImGui::MenuItem("Open")) { - manager.NavigateToFolder(item); + if (item->isFolder) { + manager.NavigateToFolder(item); + } else if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { + m_context->GetSceneManager().LoadScene(item->fullPath); + } } ImGui::Separator(); } @@ -276,8 +282,14 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) { } } - if (doubleClicked && item->isFolder) { - manager.NavigateToFolder(item); + if (doubleClicked) { + if (item->isFolder) { + manager.NavigateToFolder(item); + } else if (item->type == "Scene") { + if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) { + m_context->GetSceneManager().LoadScene(item->fullPath); + } + } } ImGui::PopID(); @@ -297,4 +309,4 @@ bool ProjectPanel::HandleDrop(const AssetItemPtr& targetFolder) { } } -} \ No newline at end of file +} diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index c9480d51..755d4bfe 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -243,9 +243,17 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/TransformComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/GameObject.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/CameraComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/LightComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioSourceComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/AudioListenerComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/Component.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/TransformComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/GameObject.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/CameraComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/LightComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioSourceComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/AudioListenerComponent.cpp # Scene ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h diff --git a/engine/include/XCEngine/Components/CameraComponent.h b/engine/include/XCEngine/Components/CameraComponent.h new file mode 100644 index 00000000..a29839a0 --- /dev/null +++ b/engine/include/XCEngine/Components/CameraComponent.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +namespace XCEngine { +namespace Components { + +enum class CameraProjectionType { + Perspective = 0, + Orthographic +}; + +class CameraComponent : public Component { +public: + std::string GetName() const override { return "Camera"; } + + CameraProjectionType GetProjectionType() const { return m_projectionType; } + void SetProjectionType(CameraProjectionType type) { m_projectionType = type; } + + float GetFieldOfView() const { return m_fieldOfView; } + void SetFieldOfView(float value); + + float GetOrthographicSize() const { return m_orthographicSize; } + void SetOrthographicSize(float value); + + float GetNearClipPlane() const { return m_nearClipPlane; } + void SetNearClipPlane(float value); + + float GetFarClipPlane() const { return m_farClipPlane; } + void SetFarClipPlane(float value); + + float GetDepth() const { return m_depth; } + void SetDepth(float value) { m_depth = value; } + + bool IsPrimary() const { return m_primary; } + void SetPrimary(bool value) { m_primary = value; } + + const Math::Color& GetClearColor() const { return m_clearColor; } + void SetClearColor(const Math::Color& value) { m_clearColor = value; } + + void Serialize(std::ostream& os) const override; + void Deserialize(std::istream& is) override; + +private: + CameraProjectionType m_projectionType = CameraProjectionType::Perspective; + float m_fieldOfView = 60.0f; + float m_orthographicSize = 5.0f; + float m_nearClipPlane = 0.1f; + float m_farClipPlane = 1000.0f; + float m_depth = 0.0f; + bool m_primary = true; + Math::Color m_clearColor = Math::Color(0.192f, 0.302f, 0.475f, 1.0f); +}; + +} // namespace Components +} // namespace XCEngine diff --git a/engine/include/XCEngine/Components/GameObject.h b/engine/include/XCEngine/Components/GameObject.h index 6974a1d7..30fba2ae 100644 --- a/engine/include/XCEngine/Components/GameObject.h +++ b/engine/include/XCEngine/Components/GameObject.h @@ -5,10 +5,12 @@ #include #include #include +#include #include #include #include #include +#include namespace XCEngine { namespace Components { @@ -38,6 +40,10 @@ public: template T* AddComponent(Args&&... args) { + if constexpr (std::is_base_of_v) { + return dynamic_cast(m_transform); + } + auto component = std::make_unique(std::forward(args)...); component->m_gameObject = this; T* ptr = component.get(); @@ -47,6 +53,10 @@ public: template T* GetComponent() { + if (T* casted = dynamic_cast(m_transform)) { + return casted; + } + for (auto& comp : m_components) { if (T* casted = dynamic_cast(comp.get())) { return casted; @@ -57,6 +67,10 @@ public: template const T* GetComponent() const { + if (const T* casted = dynamic_cast(m_transform)) { + return casted; + } + for (auto& comp : m_components) { if (T* casted = dynamic_cast(comp.get())) { return casted; @@ -68,6 +82,9 @@ public: template std::vector GetComponents() { std::vector result; + if (T* casted = dynamic_cast(m_transform)) { + result.push_back(casted); + } for (auto& comp : m_components) { if (T* casted = dynamic_cast(comp.get())) { result.push_back(casted); @@ -79,6 +96,9 @@ public: template std::vector GetComponents() const { std::vector result; + if (const T* casted = dynamic_cast(m_transform)) { + result.push_back(casted); + } for (auto& comp : m_components) { if (const T* casted = dynamic_cast(comp.get())) { result.push_back(casted); @@ -89,6 +109,10 @@ public: template void RemoveComponent() { + if (dynamic_cast(m_transform)) { + return; + } + for (auto it = m_components.begin(); it != m_components.end(); ++it) { if (T* casted = dynamic_cast(it->get())) { m_components.erase(it); @@ -97,6 +121,23 @@ public: } } + bool RemoveComponent(Component* component) { + if (!component || component == m_transform) { + return false; + } + + auto it = std::find_if(m_components.begin(), m_components.end(), + [component](const std::unique_ptr& entry) { + return entry.get() == component; + }); + if (it == m_components.end()) { + return false; + } + + m_components.erase(it); + return true; + } + template T* GetComponentInChildren() { T* comp = GetComponent(); @@ -187,4 +228,4 @@ private: }; } // namespace Components -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/engine/include/XCEngine/Components/LightComponent.h b/engine/include/XCEngine/Components/LightComponent.h new file mode 100644 index 00000000..3d4f3aba --- /dev/null +++ b/engine/include/XCEngine/Components/LightComponent.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +namespace XCEngine { +namespace Components { + +enum class LightType { + Directional = 0, + Point, + Spot +}; + +class LightComponent : public Component { +public: + std::string GetName() const override { return "Light"; } + + LightType GetLightType() const { return m_lightType; } + void SetLightType(LightType value) { m_lightType = value; } + + const Math::Color& GetColor() const { return m_color; } + void SetColor(const Math::Color& value) { m_color = value; } + + float GetIntensity() const { return m_intensity; } + void SetIntensity(float value); + + float GetRange() const { return m_range; } + void SetRange(float value); + + float GetSpotAngle() const { return m_spotAngle; } + void SetSpotAngle(float value); + + bool GetCastsShadows() const { return m_castsShadows; } + void SetCastsShadows(bool value) { m_castsShadows = value; } + + void Serialize(std::ostream& os) const override; + void Deserialize(std::istream& is) override; + +private: + LightType m_lightType = LightType::Directional; + Math::Color m_color = Math::Color::White(); + float m_intensity = 1.0f; + float m_range = 10.0f; + float m_spotAngle = 30.0f; + bool m_castsShadows = false; +}; + +} // namespace Components +} // namespace XCEngine diff --git a/engine/src/Components/CameraComponent.cpp b/engine/src/Components/CameraComponent.cpp new file mode 100644 index 00000000..614f7465 --- /dev/null +++ b/engine/src/Components/CameraComponent.cpp @@ -0,0 +1,77 @@ +#include "Components/CameraComponent.h" + +#include +#include + +namespace XCEngine { +namespace Components { + +void CameraComponent::SetFieldOfView(float value) { + m_fieldOfView = std::clamp(value, 1.0f, 179.0f); +} + +void CameraComponent::SetOrthographicSize(float value) { + m_orthographicSize = std::max(0.001f, value); +} + +void CameraComponent::SetNearClipPlane(float value) { + m_nearClipPlane = std::max(0.001f, value); + if (m_farClipPlane <= m_nearClipPlane) { + m_farClipPlane = m_nearClipPlane + 0.001f; + } +} + +void CameraComponent::SetFarClipPlane(float value) { + m_farClipPlane = std::max(m_nearClipPlane + 0.001f, value); +} + +void CameraComponent::Serialize(std::ostream& os) const { + os << "projection=" << static_cast(m_projectionType) << ";"; + os << "fov=" << m_fieldOfView << ";"; + os << "orthoSize=" << m_orthographicSize << ";"; + os << "near=" << m_nearClipPlane << ";"; + os << "far=" << m_farClipPlane << ";"; + os << "depth=" << m_depth << ";"; + os << "primary=" << (m_primary ? 1 : 0) << ";"; + os << "clearColor=" << m_clearColor.r << "," << m_clearColor.g << "," << m_clearColor.b << "," << m_clearColor.a << ";"; +} + +void CameraComponent::Deserialize(std::istream& is) { + std::string token; + while (std::getline(is, token, ';')) { + if (token.empty()) { + continue; + } + + const size_t eqPos = token.find('='); + if (eqPos == std::string::npos) { + continue; + } + + const std::string key = token.substr(0, eqPos); + std::string value = token.substr(eqPos + 1); + + if (key == "projection") { + m_projectionType = static_cast(std::stoi(value)); + } else if (key == "fov") { + SetFieldOfView(std::stof(value)); + } else if (key == "orthoSize") { + SetOrthographicSize(std::stof(value)); + } else if (key == "near") { + SetNearClipPlane(std::stof(value)); + } else if (key == "far") { + SetFarClipPlane(std::stof(value)); + } else if (key == "depth") { + m_depth = std::stof(value); + } else if (key == "primary") { + m_primary = (std::stoi(value) != 0); + } else if (key == "clearColor") { + std::replace(value.begin(), value.end(), ',', ' '); + std::istringstream ss(value); + ss >> m_clearColor.r >> m_clearColor.g >> m_clearColor.b >> m_clearColor.a; + } + } +} + +} // namespace Components +} // namespace XCEngine diff --git a/engine/src/Components/GameObject.cpp b/engine/src/Components/GameObject.cpp index cad22f2a..f8dfa7ae 100644 --- a/engine/src/Components/GameObject.cpp +++ b/engine/src/Components/GameObject.cpp @@ -48,32 +48,30 @@ void GameObject::SetParent(GameObject* parent) { } void GameObject::SetParent(GameObject* parent, bool worldPositionStays) { - if (m_parent == parent) { + if (m_parent == parent || parent == this) { return; } - Math::Vector3 worldPos = worldPositionStays ? GetTransform()->GetPosition() : GetTransform()->GetLocalPosition(); - Math::Quaternion worldRot = worldPositionStays ? GetTransform()->GetRotation() : GetTransform()->GetLocalRotation(); - Math::Vector3 worldScale = worldPositionStays ? GetTransform()->GetScale() : GetTransform()->GetLocalScale(); - if (m_parent) { auto& siblings = m_parent->m_children; siblings.erase(std::remove(siblings.begin(), siblings.end(), this), siblings.end()); + } else if (m_scene) { + auto& roots = m_scene->m_rootGameObjects; + roots.erase(std::remove(roots.begin(), roots.end(), m_id), roots.end()); } m_parent = parent; if (m_parent) { m_parent->m_children.push_back(this); + } else if (m_scene) { + auto& roots = m_scene->m_rootGameObjects; + if (std::find(roots.begin(), roots.end(), m_id) == roots.end()) { + roots.push_back(m_id); + } } - if (worldPositionStays) { - GetTransform()->SetPosition(worldPos); - GetTransform()->SetRotation(worldRot); - GetTransform()->SetScale(worldScale); - } - - GetTransform()->SetDirty(); + GetTransform()->SetParent(parent ? parent->GetTransform() : nullptr, worldPositionStays); } GameObject* GameObject::GetChild(size_t index) const { @@ -88,32 +86,17 @@ std::vector GameObject::GetChildren() const { } void GameObject::DetachChildren() { - for (auto* child : m_children) { + auto children = m_children; + for (auto* child : children) { if (child) { - child->m_parent = nullptr; + child->SetParent(nullptr, true); } } - m_children.clear(); } void GameObject::DetachFromParent() { if (m_parent) { - Math::Vector3 worldPos = GetTransform()->GetPosition(); - Math::Quaternion worldRot = GetTransform()->GetRotation(); - Math::Vector3 worldScale = GetTransform()->GetScale(); - - auto& siblings = m_parent->m_children; - siblings.erase(std::remove(siblings.begin(), siblings.end(), this), siblings.end()); - m_parent = nullptr; - - if (m_scene) { - m_scene->m_rootGameObjects.push_back(m_id); - } - - GetTransform()->SetPosition(worldPos); - GetTransform()->SetRotation(worldRot); - GetTransform()->SetScale(worldScale); - GetTransform()->SetDirty(); + SetParent(nullptr, true); } } @@ -260,4 +243,4 @@ void GameObject::Deserialize(std::istream& is) { } } // namespace Components -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/engine/src/Components/LightComponent.cpp b/engine/src/Components/LightComponent.cpp new file mode 100644 index 00000000..e537b555 --- /dev/null +++ b/engine/src/Components/LightComponent.cpp @@ -0,0 +1,64 @@ +#include "Components/LightComponent.h" + +#include +#include + +namespace XCEngine { +namespace Components { + +void LightComponent::SetIntensity(float value) { + m_intensity = std::max(0.0f, value); +} + +void LightComponent::SetRange(float value) { + m_range = std::max(0.001f, value); +} + +void LightComponent::SetSpotAngle(float value) { + m_spotAngle = std::clamp(value, 1.0f, 179.0f); +} + +void LightComponent::Serialize(std::ostream& os) const { + os << "type=" << static_cast(m_lightType) << ";"; + os << "color=" << m_color.r << "," << m_color.g << "," << m_color.b << "," << m_color.a << ";"; + os << "intensity=" << m_intensity << ";"; + os << "range=" << m_range << ";"; + os << "spotAngle=" << m_spotAngle << ";"; + os << "shadows=" << (m_castsShadows ? 1 : 0) << ";"; +} + +void LightComponent::Deserialize(std::istream& is) { + std::string token; + while (std::getline(is, token, ';')) { + if (token.empty()) { + continue; + } + + const size_t eqPos = token.find('='); + if (eqPos == std::string::npos) { + continue; + } + + const std::string key = token.substr(0, eqPos); + std::string value = token.substr(eqPos + 1); + + if (key == "type") { + m_lightType = static_cast(std::stoi(value)); + } else if (key == "color") { + std::replace(value.begin(), value.end(), ',', ' '); + std::istringstream ss(value); + ss >> m_color.r >> m_color.g >> m_color.b >> m_color.a; + } else if (key == "intensity") { + SetIntensity(std::stof(value)); + } else if (key == "range") { + SetRange(std::stof(value)); + } else if (key == "spotAngle") { + SetSpotAngle(std::stof(value)); + } else if (key == "shadows") { + m_castsShadows = (std::stoi(value) != 0); + } + } +} + +} // namespace Components +} // namespace XCEngine diff --git a/engine/src/Scene/Scene.cpp b/engine/src/Scene/Scene.cpp index 95b77471..defd146c 100644 --- a/engine/src/Scene/Scene.cpp +++ b/engine/src/Scene/Scene.cpp @@ -1,10 +1,126 @@ #include "Scene/Scene.h" #include "Components/GameObject.h" +#include "Components/TransformComponent.h" +#include "Components/CameraComponent.h" +#include "Components/LightComponent.h" +#include "Components/AudioSourceComponent.h" +#include "Components/AudioListenerComponent.h" #include +#include +#include namespace XCEngine { namespace Components { +namespace { + +struct PendingComponentData { + std::string type; + std::string payload; +}; + +struct PendingGameObjectData { + GameObject::ID id = GameObject::INVALID_ID; + std::string name = "GameObject"; + bool active = true; + GameObject::ID parentId = GameObject::INVALID_ID; + std::string transformPayload; + std::vector components; +}; + +std::string EscapeString(const std::string& value) { + std::string escaped; + escaped.reserve(value.size()); + for (char ch : value) { + if (ch == '\\' || ch == '\n' || ch == '\r') { + escaped.push_back('\\'); + if (ch == '\n') { + escaped.push_back('n'); + } else if (ch == '\r') { + escaped.push_back('r'); + } else { + escaped.push_back(ch); + } + } else { + escaped.push_back(ch); + } + } + return escaped; +} + +std::string UnescapeString(const std::string& value) { + std::string unescaped; + unescaped.reserve(value.size()); + for (size_t i = 0; i < value.size(); ++i) { + if (value[i] == '\\' && i + 1 < value.size()) { + ++i; + switch (value[i]) { + case 'n': unescaped.push_back('\n'); break; + case 'r': unescaped.push_back('\r'); break; + default: unescaped.push_back(value[i]); break; + } + } else { + unescaped.push_back(value[i]); + } + } + return unescaped; +} + +Component* CreateComponentByType(GameObject* gameObject, const std::string& type) { + if (!gameObject) { + return nullptr; + } + + if (type == "Camera") { + return gameObject->AddComponent(); + } + if (type == "Light") { + return gameObject->AddComponent(); + } + if (type == "AudioSource") { + return gameObject->AddComponent(); + } + if (type == "AudioListener") { + return gameObject->AddComponent(); + } + + return nullptr; +} + +void SerializeGameObjectRecursive(std::ostream& os, GameObject* gameObject) { + if (!gameObject) { + return; + } + + os << "gameobject_begin\n"; + os << "id=" << gameObject->GetID() << "\n"; + os << "name=" << EscapeString(gameObject->GetName()) << "\n"; + os << "active=" << (gameObject->IsActive() ? 1 : 0) << "\n"; + os << "parent=" << (gameObject->GetParent() ? gameObject->GetParent()->GetID() : GameObject::INVALID_ID) << "\n"; + os << "transform="; + gameObject->GetTransform()->Serialize(os); + os << "\n"; + + auto components = gameObject->GetComponents(); + for (Component* component : components) { + if (!component || component == gameObject->GetTransform()) { + continue; + } + + os << "component=" << component->GetName() << ";"; + component->Serialize(os); + os << "\n"; + } + + os << "gameobject_end\n"; + + for (GameObject* child : gameObject->GetChildren()) { + SerializeGameObjectRecursive(os, child); + } +} + +} // namespace + Scene::Scene() : m_name("Untitled") { } @@ -24,14 +140,13 @@ GameObject* Scene::CreateGameObject(const std::string& name, GameObject* parent) GameObject::GetGlobalRegistry()[ptr->m_id] = ptr; m_gameObjectIDs.insert(ptr->m_id); m_gameObjects.emplace(ptr->m_id, std::move(gameObject)); + ptr->m_scene = this; if (parent) { ptr->SetParent(parent); } else { m_rootGameObjects.push_back(ptr->m_id); } - - ptr->m_scene = this; ptr->Awake(); m_onGameObjectCreated.Invoke(ptr); @@ -159,26 +274,118 @@ void Scene::Load(const std::string& filePath) { m_rootGameObjects.clear(); m_gameObjectIDs.clear(); + std::vector pendingObjects; std::string line; + PendingGameObjectData* currentObject = nullptr; + GameObject::ID maxId = 0; + while (std::getline(file, line)) { if (line.empty() || line[0] == '#') continue; - - std::istringstream iss(line); - std::string token; - std::getline(iss, token, '='); - - if (token == "scene") { - std::getline(iss, m_name, ';'); - } else if (token == "gameobject") { - auto go = std::make_unique(); - go->Deserialize(iss); - go->m_scene = this; - GameObject* ptr = go.get(); - GameObject::GetGlobalRegistry()[ptr->m_id] = ptr; - m_gameObjectIDs.insert(ptr->m_id); - m_rootGameObjects.push_back(ptr->m_id); - m_gameObjects.emplace(ptr->m_id, std::move(go)); + + if (line == "gameobject_begin") { + pendingObjects.emplace_back(); + currentObject = &pendingObjects.back(); + continue; } + + if (line == "gameobject_end") { + currentObject = nullptr; + continue; + } + + const size_t eqPos = line.find('='); + if (eqPos == std::string::npos) { + continue; + } + + const std::string key = line.substr(0, eqPos); + const std::string value = line.substr(eqPos + 1); + + if (!currentObject) { + if (key == "scene") { + m_name = UnescapeString(value); + } else if (key == "active") { + m_active = (value == "1"); + } + continue; + } + + if (key == "id") { + currentObject->id = static_cast(std::stoull(value)); + maxId = std::max(maxId, currentObject->id); + } else if (key == "name") { + currentObject->name = UnescapeString(value); + } else if (key == "active") { + currentObject->active = (value == "1"); + } else if (key == "parent") { + currentObject->parentId = static_cast(std::stoull(value)); + } else if (key == "transform") { + currentObject->transformPayload = value; + } else if (key == "component") { + const size_t typeEnd = value.find(';'); + PendingComponentData componentData; + if (typeEnd == std::string::npos) { + componentData.type = value; + } else { + componentData.type = value.substr(0, typeEnd); + componentData.payload = value.substr(typeEnd + 1); + } + currentObject->components.push_back(std::move(componentData)); + } + } + + std::unordered_map createdObjects; + createdObjects.reserve(pendingObjects.size()); + + for (const PendingGameObjectData& pending : pendingObjects) { + auto go = std::make_unique(pending.name); + go->m_id = pending.id; + go->m_activeSelf = pending.active; + go->m_scene = this; + + if (!pending.transformPayload.empty()) { + std::istringstream transformStream(pending.transformPayload); + go->m_transform->Deserialize(transformStream); + } + + for (const PendingComponentData& componentData : pending.components) { + if (Component* component = CreateComponentByType(go.get(), componentData.type)) { + if (!componentData.payload.empty()) { + std::istringstream componentStream(componentData.payload); + component->Deserialize(componentStream); + } + } + } + + GameObject* ptr = go.get(); + GameObject::GetGlobalRegistry()[ptr->m_id] = ptr; + m_gameObjectIDs.insert(ptr->m_id); + createdObjects[ptr->m_id] = ptr; + m_gameObjects.emplace(ptr->m_id, std::move(go)); + } + + m_rootGameObjects.clear(); + for (const PendingGameObjectData& pending : pendingObjects) { + auto it = createdObjects.find(pending.id); + if (it == createdObjects.end()) { + continue; + } + + GameObject* gameObject = it->second; + if (pending.parentId == GameObject::INVALID_ID) { + m_rootGameObjects.push_back(gameObject->GetID()); + } else { + auto parentIt = createdObjects.find(pending.parentId); + if (parentIt != createdObjects.end()) { + gameObject->SetParent(parentIt->second, false); + } else { + m_rootGameObjects.push_back(gameObject->GetID()); + } + } + } + + if (maxId != GameObject::INVALID_ID && GameObject::s_nextID <= maxId) { + GameObject::s_nextID = maxId + 1; } } @@ -189,15 +396,14 @@ void Scene::Save(const std::string& filePath) { } file << "# XCEngine Scene File\n"; - file << "scene=" << m_name << ";\n"; - file << "active=" << (m_active ? "1" : "0") << ";\n\n"; + file << "scene=" << EscapeString(m_name) << "\n"; + file << "active=" << (m_active ? "1" : "0") << "\n\n"; for (auto* go : GetRootGameObjects()) { - file << "gameobject="; - go->Serialize(file); + SerializeGameObjectRecursive(file, go); file << "\n"; } } } // namespace Components -} // namespace XCEngine \ No newline at end of file +} // namespace XCEngine diff --git a/tests/Components/CMakeLists.txt b/tests/Components/CMakeLists.txt index 59c7a772..2c130f54 100644 --- a/tests/Components/CMakeLists.txt +++ b/tests/Components/CMakeLists.txt @@ -6,6 +6,7 @@ set(COMPONENTS_TEST_SOURCES test_component.cpp test_transform_component.cpp test_game_object.cpp + test_camera_light_component.cpp ) add_executable(components_tests ${COMPONENTS_TEST_SOURCES}) @@ -27,4 +28,4 @@ target_include_directories(components_tests PRIVATE ) include(GoogleTest) -gtest_discover_tests(components_tests) \ No newline at end of file +gtest_discover_tests(components_tests) diff --git a/tests/Components/test_camera_light_component.cpp b/tests/Components/test_camera_light_component.cpp new file mode 100644 index 00000000..33f1d56f --- /dev/null +++ b/tests/Components/test_camera_light_component.cpp @@ -0,0 +1,57 @@ +#include + +#include +#include + +using namespace XCEngine::Components; + +namespace { + +TEST(CameraComponent_Test, DefaultValues) { + CameraComponent camera; + + EXPECT_EQ(camera.GetProjectionType(), CameraProjectionType::Perspective); + EXPECT_FLOAT_EQ(camera.GetFieldOfView(), 60.0f); + EXPECT_FLOAT_EQ(camera.GetOrthographicSize(), 5.0f); + EXPECT_FLOAT_EQ(camera.GetNearClipPlane(), 0.1f); + EXPECT_FLOAT_EQ(camera.GetFarClipPlane(), 1000.0f); + EXPECT_TRUE(camera.IsPrimary()); +} + +TEST(CameraComponent_Test, SetterClamping) { + CameraComponent camera; + + camera.SetFieldOfView(500.0f); + camera.SetOrthographicSize(-1.0f); + camera.SetNearClipPlane(-10.0f); + camera.SetFarClipPlane(0.0f); + + EXPECT_FLOAT_EQ(camera.GetFieldOfView(), 179.0f); + EXPECT_FLOAT_EQ(camera.GetOrthographicSize(), 0.001f); + EXPECT_FLOAT_EQ(camera.GetNearClipPlane(), 0.001f); + EXPECT_GT(camera.GetFarClipPlane(), camera.GetNearClipPlane()); +} + +TEST(LightComponent_Test, DefaultValues) { + LightComponent light; + + EXPECT_EQ(light.GetLightType(), LightType::Directional); + EXPECT_FLOAT_EQ(light.GetIntensity(), 1.0f); + EXPECT_FLOAT_EQ(light.GetRange(), 10.0f); + EXPECT_FLOAT_EQ(light.GetSpotAngle(), 30.0f); + EXPECT_FALSE(light.GetCastsShadows()); +} + +TEST(LightComponent_Test, SetterClamping) { + LightComponent light; + + light.SetIntensity(-3.0f); + light.SetRange(-1.0f); + light.SetSpotAngle(500.0f); + + EXPECT_FLOAT_EQ(light.GetIntensity(), 0.0f); + EXPECT_FLOAT_EQ(light.GetRange(), 0.001f); + EXPECT_FLOAT_EQ(light.GetSpotAngle(), 179.0f); +} + +} // namespace diff --git a/tests/Components/test_game_object.cpp b/tests/Components/test_game_object.cpp index f65b8ad3..1665ba26 100644 --- a/tests/Components/test_game_object.cpp +++ b/tests/Components/test_game_object.cpp @@ -142,7 +142,7 @@ TEST(GameObject_Test, SetParent_WithoutWorldPosition) { child.SetParent(&parent, false); Vector3 childWorldPos = child.GetTransform()->GetPosition(); - EXPECT_NEAR(childWorldPos.x, 2.0f, 0.001f); + EXPECT_NEAR(childWorldPos.x, 3.0f, 0.001f); } TEST(GameObject_Test, GetChild_ValidIndex) { @@ -275,4 +275,4 @@ TEST(GameObject_Test, GetChildCount) { EXPECT_EQ(parent.GetChildCount(), 2u); } -} // namespace \ No newline at end of file +} // namespace diff --git a/tests/Scene/test_scene.cpp b/tests/Scene/test_scene.cpp index fb3db8d4..92652275 100644 --- a/tests/Scene/test_scene.cpp +++ b/tests/Scene/test_scene.cpp @@ -1,8 +1,11 @@ #include #include +#include #include +#include #include #include +#include #include #include @@ -36,6 +39,10 @@ protected: testScene = std::make_unique("TestScene"); } + std::filesystem::path GetTempScenePath(const char* fileName) const { + return std::filesystem::temp_directory_path() / fileName; + } + std::unique_ptr testScene; }; @@ -222,30 +229,128 @@ TEST_F(SceneTest, Save_And_Load) { GameObject* go = testScene->CreateGameObject("SavedObject"); go->GetTransform()->SetLocalPosition(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); go->SetActive(true); - - testScene->Save("test_scene.scene"); - + + const std::filesystem::path scenePath = GetTempScenePath("test_scene.xc"); + testScene->Save(scenePath.string()); + Scene loadedScene; - loadedScene.Load("test_scene.scene"); - + loadedScene.Load(scenePath.string()); + GameObject* loadedGo = loadedScene.Find("SavedObject"); EXPECT_NE(loadedGo, nullptr); EXPECT_EQ(loadedGo->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); + + std::filesystem::remove(scenePath); } TEST_F(SceneTest, Save_ContainsGameObjectData) { testScene->CreateGameObject("Player"); testScene->CreateGameObject("Enemy"); - - testScene->Save("test_scene_multi.scene"); - - std::ifstream file("test_scene_multi.scene"); + + const std::filesystem::path scenePath = GetTempScenePath("test_scene_multi.xc"); + testScene->Save(scenePath.string()); + + std::ifstream file(scenePath); std::stringstream buffer; buffer << file.rdbuf(); std::string content = buffer.str(); - + file.close(); + EXPECT_TRUE(content.find("Player") != std::string::npos); EXPECT_TRUE(content.find("Enemy") != std::string::npos); + + std::filesystem::remove(scenePath); } -} // namespace \ No newline at end of file +TEST_F(SceneTest, Save_And_Load_PreservesHierarchyAndComponents) { + testScene->SetName("Serialized Scene"); + testScene->SetActive(false); + + GameObject* parent = testScene->CreateGameObject("Rig Root"); + parent->GetTransform()->SetLocalPosition(Vector3(5.0f, 0.0f, 0.0f)); + parent->SetActive(false); + + auto* light = parent->AddComponent(); + light->SetLightType(LightType::Spot); + light->SetIntensity(3.5f); + light->SetRange(12.0f); + light->SetSpotAngle(45.0f); + light->SetCastsShadows(true); + + GameObject* child = testScene->CreateGameObject("Main Camera", parent); + child->GetTransform()->SetLocalPosition(Vector3(1.0f, 2.0f, 3.0f)); + child->GetTransform()->SetLocalScale(Vector3(2.0f, 2.0f, 2.0f)); + + auto* camera = child->AddComponent(); + camera->SetProjectionType(CameraProjectionType::Orthographic); + camera->SetOrthographicSize(7.5f); + camera->SetNearClipPlane(0.5f); + camera->SetFarClipPlane(250.0f); + camera->SetDepth(2.0f); + camera->SetPrimary(false); + + const std::filesystem::path scenePath = GetTempScenePath("test_scene_hierarchy_components.xc"); + testScene->Save(scenePath.string()); + + Scene loadedScene; + loadedScene.Load(scenePath.string()); + + EXPECT_EQ(loadedScene.GetName(), "Serialized Scene"); + EXPECT_FALSE(loadedScene.IsActive()); + + GameObject* loadedParent = loadedScene.Find("Rig Root"); + GameObject* loadedChild = loadedScene.Find("Main Camera"); + ASSERT_NE(loadedParent, nullptr); + ASSERT_NE(loadedChild, nullptr); + + EXPECT_EQ(loadedChild->GetParent(), loadedParent); + EXPECT_EQ(loadedScene.GetRootGameObjects().size(), 1u); + EXPECT_FALSE(loadedParent->IsActive()); + EXPECT_EQ(loadedParent->GetTransform()->GetLocalPosition(), Vector3(5.0f, 0.0f, 0.0f)); + EXPECT_EQ(loadedChild->GetTransform()->GetLocalPosition(), Vector3(1.0f, 2.0f, 3.0f)); + EXPECT_EQ(loadedChild->GetTransform()->GetPosition(), Vector3(6.0f, 2.0f, 3.0f)); + + auto* loadedLight = loadedParent->GetComponent(); + ASSERT_NE(loadedLight, nullptr); + EXPECT_EQ(loadedLight->GetLightType(), LightType::Spot); + EXPECT_FLOAT_EQ(loadedLight->GetIntensity(), 3.5f); + EXPECT_FLOAT_EQ(loadedLight->GetRange(), 12.0f); + EXPECT_FLOAT_EQ(loadedLight->GetSpotAngle(), 45.0f); + EXPECT_TRUE(loadedLight->GetCastsShadows()); + + auto* loadedCamera = loadedChild->GetComponent(); + ASSERT_NE(loadedCamera, nullptr); + EXPECT_EQ(loadedCamera->GetProjectionType(), CameraProjectionType::Orthographic); + EXPECT_FLOAT_EQ(loadedCamera->GetOrthographicSize(), 7.5f); + EXPECT_FLOAT_EQ(loadedCamera->GetNearClipPlane(), 0.5f); + EXPECT_FLOAT_EQ(loadedCamera->GetFarClipPlane(), 250.0f); + EXPECT_FLOAT_EQ(loadedCamera->GetDepth(), 2.0f); + EXPECT_FALSE(loadedCamera->IsPrimary()); + + std::filesystem::remove(scenePath); +} + +TEST_F(SceneTest, Save_ContainsHierarchyAndComponentEntries) { + GameObject* parent = testScene->CreateGameObject("Parent"); + GameObject* child = testScene->CreateGameObject("Child", parent); + child->AddComponent(); + parent->AddComponent(); + + const std::filesystem::path scenePath = GetTempScenePath("test_scene_format.xc"); + testScene->Save(scenePath.string()); + + std::ifstream file(scenePath); + std::stringstream buffer; + buffer << file.rdbuf(); + const std::string content = buffer.str(); + file.close(); + + EXPECT_TRUE(content.find("gameobject_begin") != std::string::npos); + EXPECT_TRUE(content.find("parent=" + std::to_string(parent->GetID())) != std::string::npos); + EXPECT_TRUE(content.find("component=Camera;") != std::string::npos); + EXPECT_TRUE(content.find("component=Light;") != std::string::npos); + + std::filesystem::remove(scenePath); +} + +} // namespace