Fix editor scene persistence and XC scene workflow

This commit is contained in:
2026-03-26 01:26:26 +08:00
parent 39edb0b497
commit 0651666d8c
35 changed files with 1958 additions and 256 deletions

View File

@@ -8,15 +8,69 @@
#include <imgui_impl_win32.h> #include <imgui_impl_win32.h>
#include <imgui_impl_dx12.h> #include <imgui_impl_dx12.h>
#include <imgui_internal.h> #include <imgui_internal.h>
#include <filesystem>
#include <stdio.h> #include <stdio.h>
#include <windows.h> #include <windows.h>
#include <string>
extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
static LONG WINAPI GlobalExceptionFilter(EXCEPTION_POINTERS* exceptionPointers) { namespace {
const char* logPath = "D:\\Xuanchi\\Main\\XCEngine\\editor\\bin\\Release\\crash.log";
FILE* f = fopen(logPath, "a"); 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 std::string logPath = GetExecutableLogPath("crash.log");
FILE* f = nullptr;
fopen_s(&f, logPath.c_str(), "a");
if (f) { if (f) {
fprintf(f, "[CRASH] ExceptionCode=0x%08X, Address=0x%p\n", fprintf(f, "[CRASH] ExceptionCode=0x%08X, Address=0x%p\n",
exceptionPointers->ExceptionRecord->ExceptionCode, exceptionPointers->ExceptionRecord->ExceptionCode,
@@ -45,20 +99,7 @@ bool Application::Initialize(HWND hwnd) {
// Redirect stderr to log file to capture ImGui errors // Redirect stderr to log file to capture ImGui errors
{ {
wchar_t exePath[MAX_PATH]; const std::string stderrPath = GetExecutableLogPath("stderr.log");
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";
freopen(stderrPath.c_str(), "w", stderr); freopen(stderrPath.c_str(), "w", stderr);
fprintf(stderr, "[TEST] stderr redirection test - this should appear in stderr.log\n"); 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<Debug::EditorConsoleSink>()); Debug::Logger::Get().AddSink(std::make_unique<Debug::EditorConsoleSink>());
// Get exe directory for log file path // Get exe directory for log file path
wchar_t exePath[MAX_PATH]; const std::string exeDir = GetExecutableDirectoryUtf8();
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 logPath = exeDir + "\\editor.log"; std::string logPath = exeDir + "\\editor.log";
Debug::Logger::Get().AddSink(std::make_unique<Debug::FileLogSink>(logPath.c_str())); Debug::Logger::Get().AddSink(std::make_unique<Debug::FileLogSink>(logPath.c_str()));
Debug::Logger::Get().Info(Debug::LogCategory::General, "Editor Application starting..."); Debug::Logger::Get().Info(Debug::LogCategory::General, "Editor Application starting...");
@@ -154,6 +183,7 @@ void Application::Render() {
ImGui::NewFrame(); ImGui::NewFrame();
m_layerStack.onImGuiRender(); m_layerStack.onImGuiRender();
UpdateWindowTitle();
ImGui::Render(); 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) { void Application::OnResize(int width, int height) {
if (width <= 0 || height <= 0) return; if (width <= 0 || height <= 0) return;

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <string>
#include <imgui.h> #include <imgui.h>
#include <d3d12.h> #include <d3d12.h>
#include <dxgi1_6.h> #include <dxgi1_6.h>
@@ -23,6 +24,7 @@ public:
void Shutdown(); void Shutdown();
void Render(); void Render();
void OnResize(int width, int height); void OnResize(int width, int height);
HWND GetWindowHandle() const { return m_hwnd; }
IEditorContext& GetEditorContext() const { return *m_editorContext; } IEditorContext& GetEditorContext() const { return *m_editorContext; }
@@ -33,6 +35,7 @@ private:
bool CreateDevice(); bool CreateDevice();
bool CreateRenderTarget(); bool CreateRenderTarget();
void CleanupRenderTarget(); void CleanupRenderTarget();
void UpdateWindowTitle();
HWND m_hwnd = nullptr; HWND m_hwnd = nullptr;
int m_width = 1280; int m_width = 1280;
@@ -54,6 +57,7 @@ private:
Core::LayerStack m_layerStack; Core::LayerStack m_layerStack;
EditorLayer* m_editorLayer = nullptr; EditorLayer* m_editorLayer = nullptr;
std::shared_ptr<IEditorContext> m_editorContext; std::shared_ptr<IEditorContext> m_editorContext;
std::wstring m_lastWindowTitle;
}; };
} }

View File

@@ -0,0 +1,108 @@
#pragma once
#include "IComponentEditor.h"
#include "UI/UI.h"
#include <XCEngine/Components/CameraComponent.h>
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<int>(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

View File

@@ -0,0 +1,35 @@
#pragma once
#include <XCEngine/Components/Component.h>
#include <XCEngine/Components/GameObject.h>
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

View File

@@ -0,0 +1,98 @@
#pragma once
#include "IComponentEditor.h"
#include "UI/UI.h"
#include <XCEngine/Components/LightComponent.h>
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<int>(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

View File

@@ -0,0 +1,67 @@
#pragma once
#include "IComponentEditor.h"
#include "UI/UI.h"
#include <XCEngine/Components/TransformComponent.h>
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

View File

@@ -3,14 +3,22 @@
namespace XCEngine { namespace XCEngine {
namespace Debug { namespace Debug {
EditorConsoleSink* EditorConsoleSink::s_instance = nullptr;
EditorConsoleSink* EditorConsoleSink::GetInstance() { EditorConsoleSink* EditorConsoleSink::GetInstance() {
static EditorConsoleSink instance; static EditorConsoleSink fallbackInstance;
return &instance; 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) { void EditorConsoleSink::Log(const LogEntry& entry) {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
@@ -26,7 +34,8 @@ void EditorConsoleSink::Log(const LogEntry& entry) {
void EditorConsoleSink::Flush() { void EditorConsoleSink::Flush() {
} }
const std::vector<LogEntry>& EditorConsoleSink::GetLogs() const { std::vector<LogEntry> EditorConsoleSink::GetLogs() const {
std::lock_guard<std::mutex> lock(m_mutex);
return m_logs; return m_logs;
} }

View File

@@ -19,7 +19,7 @@ public:
void Log(const LogEntry& entry) override; void Log(const LogEntry& entry) override;
void Flush() override; void Flush() override;
const std::vector<LogEntry>& GetLogs() const; std::vector<LogEntry> GetLogs() const;
void Clear(); void Clear();
void SetCallback(std::function<void()> callback); void SetCallback(std::function<void()> callback);
@@ -27,6 +27,7 @@ private:
mutable std::mutex m_mutex; mutable std::mutex m_mutex;
std::vector<LogEntry> m_logs; std::vector<LogEntry> m_logs;
std::function<void()> m_callback; std::function<void()> m_callback;
static EditorConsoleSink* s_instance;
static constexpr size_t MAX_LOGS = 1000; static constexpr size_t MAX_LOGS = 1000;
}; };

View File

@@ -19,7 +19,7 @@ public:
EditorContext() EditorContext()
: m_eventBus(std::make_unique<EventBus>()) : m_eventBus(std::make_unique<EventBus>())
, m_selectionManager(std::make_unique<SelectionManager>(*m_eventBus)) , m_selectionManager(std::make_unique<SelectionManager>(*m_eventBus))
, m_sceneManager(std::make_unique<SceneManager>()) , m_sceneManager(std::make_unique<SceneManager>(m_eventBus.get()))
, m_projectManager(std::make_unique<ProjectManager>()) { , m_projectManager(std::make_unique<ProjectManager>()) {
m_entityDeletedHandlerId = m_eventBus->Subscribe<EntityDeletedEvent>([this](const EntityDeletedEvent& event) { m_entityDeletedHandlerId = m_eventBus->Subscribe<EntityDeletedEvent>([this](const EntityDeletedEvent& event) {

View File

@@ -6,24 +6,30 @@
#include <algorithm> #include <algorithm>
#include <cassert> #include <cassert>
#include <cstdint> #include <cstdint>
#include <atomic>
#include <mutex> #include <mutex>
#include <shared_mutex> #include <shared_mutex>
namespace XCEngine { namespace XCEngine {
namespace Editor { namespace Editor {
template<typename T> class EventTypeRegistry {
struct EventTypeId { public:
static uint32_t Get() { static uint32_t NextId() {
static const uint32_t id = s_nextId++; return s_nextId.fetch_add(1, std::memory_order_relaxed);
return id;
} }
private: private:
static uint32_t s_nextId; inline static std::atomic<uint32_t> s_nextId{0};
}; };
template<typename T> template<typename T>
uint32_t EventTypeId<T>::s_nextId = 0; struct EventTypeId {
static uint32_t Get() {
static const uint32_t id = EventTypeRegistry::NextId();
return id;
}
};
class EventBus { class EventBus {
public: public:

View File

@@ -22,6 +22,16 @@ public:
virtual ::XCEngine::Components::GameObject::ID DuplicateEntity(::XCEngine::Components::GameObject::ID id) = 0; 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 void MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEngine::Components::GameObject::ID newParent) = 0;
virtual bool HasClipboardData() const = 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; virtual void CreateDemoScene() = 0;
}; };

View File

@@ -8,6 +8,7 @@
#include "panels/ProjectPanel.h" #include "panels/ProjectPanel.h"
#include "Core/IEditorContext.h" #include "Core/IEditorContext.h"
#include "Core/EditorContext.h" #include "Core/EditorContext.h"
#include <filesystem>
#include <imgui.h> #include <imgui.h>
#include <imgui_internal.h> #include <imgui_internal.h>
@@ -41,6 +42,10 @@ void EditorLayer::onAttach() {
m_consolePanel->SetContext(m_context.get()); m_consolePanel->SetContext(m_context.get());
m_projectPanel->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_menuBar->OnAttach();
m_hierarchyPanel->OnAttach(); m_hierarchyPanel->OnAttach();
m_sceneViewPanel->OnAttach(); m_sceneViewPanel->OnAttach();
@@ -48,11 +53,18 @@ void EditorLayer::onAttach() {
m_inspectorPanel->OnAttach(); m_inspectorPanel->OnAttach();
m_consolePanel->OnAttach(); m_consolePanel->OnAttach();
m_projectPanel->OnAttach(); m_projectPanel->OnAttach();
m_projectPanel->Initialize(m_context->GetProjectPath());
} }
void EditorLayer::onDetach() { 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_menuBar->OnDetach();
m_hierarchyPanel->OnDetach(); m_hierarchyPanel->OnDetach();
m_sceneViewPanel->OnDetach(); m_sceneViewPanel->OnDetach();

View File

@@ -89,7 +89,6 @@ void ProjectManager::Initialize(const std::string& projectPath) {
std::ofstream((assetsPath / L"Textures" / L"Stone.png").wstring()); std::ofstream((assetsPath / L"Textures" / L"Stone.png").wstring());
std::ofstream((assetsPath / L"Models" / L"Character.fbx").wstring()); std::ofstream((assetsPath / L"Models" / L"Character.fbx").wstring());
std::ofstream((assetsPath / L"Scripts" / L"PlayerController.cs").wstring()); std::ofstream((assetsPath / L"Scripts" / L"PlayerController.cs").wstring());
std::ofstream((assetsPath / L"Scenes" / L"Main.unity").wstring());
} }
m_rootFolder = ScanDirectory(assetsPath.wstring()); m_rootFolder = ScanDirectory(assetsPath.wstring());
@@ -227,7 +226,7 @@ AssetItemPtr ProjectManager::CreateAssetItem(const std::wstring& path, const std
item->type = "Script"; item->type = "Script";
} else if (ext == L".mat") { } else if (ext == L".mat") {
item->type = "Material"; 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"; item->type = "Scene";
} else if (ext == L".prefab") { } else if (ext == L".prefab") {
item->type = "Prefab"; item->type = "Prefab";

View File

@@ -1,30 +1,87 @@
#include "SceneManager.h" #include "SceneManager.h"
#include "Core/EventBus.h"
#include "Core/EditorEvents.h"
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/AudioSourceComponent.h>
#include <XCEngine/Components/AudioListenerComponent.h>
#include <algorithm> #include <algorithm>
#include <filesystem>
#include <fstream>
#include <sstream>
namespace XCEngine { namespace XCEngine {
namespace Editor { namespace Editor {
SceneManager::SceneManager() = default; namespace {
std::pair<std::string, std::string> 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) { ::XCEngine::Components::GameObject* SceneManager::CreateEntity(const std::string& name, ::XCEngine::Components::GameObject* parent) {
if (!m_scene) { 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); ::XCEngine::Components::GameObject* entity = m_scene->CreateGameObject(name, parent);
const auto entityId = entity->GetID();
SyncRootEntities();
SetSceneDirty(true);
if (parent == nullptr) { OnEntityCreated.Invoke(entityId);
m_rootEntities.push_back(entity); OnSceneChanged.Invoke();
if (m_eventBus) {
m_eventBus->Publish(EntityCreatedEvent{ entityId });
m_eventBus->Publish(SceneChangedEvent{});
} }
OnEntityCreated.Invoke(entity->GetID());
return entity; return entity;
} }
void SceneManager::DeleteEntity(::XCEngine::Components::GameObject::ID id) { void SceneManager::DeleteEntity(::XCEngine::Components::GameObject::ID id) {
if (!m_scene) return; 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; if (!entity) return;
std::vector<::XCEngine::Components::GameObject*> children = entity->GetChildren(); std::vector<::XCEngine::Components::GameObject*> children = entity->GetChildren();
@@ -32,12 +89,18 @@ void SceneManager::DeleteEntity(::XCEngine::Components::GameObject::ID id) {
DeleteEntity(child->GetID()); DeleteEntity(child->GetID());
} }
if (entity->GetParent() == nullptr) { const auto entityId = entity->GetID();
m_rootEntities.erase(std::remove(m_rootEntities.begin(), m_rootEntities.end(), entity), m_rootEntities.end());
}
m_scene->DestroyGameObject(entity); 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) { SceneManager::ClipboardData SceneManager::CopyEntityRecursive(const ::XCEngine::Components::GameObject* entity) {
@@ -50,6 +113,14 @@ SceneManager::ClipboardData SceneManager::CopyEntityRecursive(const ::XCEngine::
data.localScale = transform->GetLocalScale(); 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()) { for (auto* child : entity->GetChildren()) {
data.children.push_back(CopyEntityRecursive(child)); data.children.push_back(CopyEntityRecursive(child));
} }
@@ -60,7 +131,7 @@ SceneManager::ClipboardData SceneManager::CopyEntityRecursive(const ::XCEngine::
void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) { void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) {
if (!m_scene) return; 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; if (!entity) return;
m_clipboard = CopyEntityRecursive(entity); 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::ID SceneManager::PasteEntityRecursive(const ClipboardData& data, ::XCEngine::Components::GameObject::ID parent) {
::XCEngine::Components::GameObject* parentObj = nullptr; ::XCEngine::Components::GameObject* parentObj = nullptr;
if (parent != 0) { 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); ::XCEngine::Components::GameObject* newEntity = m_scene->CreateGameObject(data.name, parentObj);
@@ -80,8 +151,13 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) {
transform->SetLocalScale(data.localScale); transform->SetLocalScale(data.localScale);
} }
if (parentObj == nullptr) { for (const auto& componentData : data.components) {
m_rootEntities.push_back(newEntity); 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) { 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) { ::XCEngine::Components::GameObject::ID SceneManager::PasteEntity(::XCEngine::Components::GameObject::ID parent) {
if (!m_clipboard || !m_scene) return 0; 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) { ::XCEngine::Components::GameObject::ID SceneManager::DuplicateEntity(::XCEngine::Components::GameObject::ID id) {
if (!m_scene) return 0; 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; if (!entity) return 0;
CopyEntity(id); CopyEntity(id);
@@ -110,54 +198,210 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) {
return PasteEntity(parentId); 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) { void SceneManager::RenameEntity(::XCEngine::Components::GameObject::ID id, const std::string& newName) {
if (!m_scene) return; 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; if (!obj) return;
obj->SetName(newName); obj->SetName(newName);
SetSceneDirty(true);
OnEntityChanged.Invoke(id); OnEntityChanged.Invoke(id);
if (m_eventBus) {
m_eventBus->Publish(EntityChangedEvent{ id });
}
} }
void SceneManager::MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEngine::Components::GameObject::ID newParentId) { void SceneManager::MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEngine::Components::GameObject::ID newParentId) {
if (!m_scene) return; 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; if (!obj) return;
const auto oldParentId = obj->GetParent() ? obj->GetParent()->GetID() : 0;
::XCEngine::Components::GameObject* newParent = nullptr; ::XCEngine::Components::GameObject* newParent = nullptr;
if (newParentId != 0) { if (newParentId != 0) {
newParent = m_scene->Find(std::to_string(newParentId)); newParent = m_scene->FindByID(newParentId);
} }
obj->SetParent(newParent); obj->SetParent(newParent);
SyncRootEntities();
SetSceneDirty(true);
OnEntityChanged.Invoke(id); 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() { void SceneManager::CreateDemoScene() {
if (m_scene) { m_scene = std::make_unique<::XCEngine::Components::Scene>("Main Scene");
delete m_scene;
}
m_scene = new ::XCEngine::Components::Scene("DemoScene");
m_rootEntities.clear(); m_rootEntities.clear();
m_clipboard.reset(); m_clipboard.reset();
m_currentScenePath.clear();
m_currentSceneName = m_scene->GetName();
SetSceneDirty(true);
::XCEngine::Components::GameObject* camera = CreateEntity("Main Camera", nullptr); ::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); ::XCEngine::Components::GameObject* light = CreateEntity("Directional Light", nullptr);
light->AddComponent<::XCEngine::Components::LightComponent>();
::XCEngine::Components::GameObject* cube = CreateEntity("Cube", nullptr); SyncRootEntities();
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);
OnSceneChanged.Invoke(); 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;
} }
} }

View File

@@ -6,6 +6,7 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <cstdint> #include <cstdint>
#include <vector>
#include <XCEngine/Core/Event.h> #include <XCEngine/Core/Event.h>
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
@@ -18,9 +19,11 @@
namespace XCEngine { namespace XCEngine {
namespace Editor { namespace Editor {
class EventBus;
class SceneManager : public ISceneManager { class SceneManager : public ISceneManager {
public: public:
SceneManager(); explicit SceneManager(EventBus* eventBus = nullptr);
::XCEngine::Components::GameObject* CreateEntity(const std::string& name, ::XCEngine::Components::GameObject* parent = 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); ::XCEngine::Components::GameObject::ID DuplicateEntity(::XCEngine::Components::GameObject::ID id);
void MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEngine::Components::GameObject::ID newParent); 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(); } bool HasClipboardData() const { return m_clipboard.has_value(); }
@@ -60,15 +73,25 @@ private:
Math::Vector3 localPosition = Math::Vector3::Zero(); Math::Vector3 localPosition = Math::Vector3::Zero();
Math::Quaternion localRotation = Math::Quaternion::Identity(); Math::Quaternion localRotation = Math::Quaternion::Identity();
Math::Vector3 localScale = Math::Vector3::One(); Math::Vector3 localScale = Math::Vector3::One();
std::vector<std::pair<std::string, std::string>> components;
std::vector<ClipboardData> children; std::vector<ClipboardData> children;
}; };
ClipboardData CopyEntityRecursive(const ::XCEngine::Components::GameObject* entity); ClipboardData CopyEntityRecursive(const ::XCEngine::Components::GameObject* entity);
::XCEngine::Components::GameObject::ID PasteEntityRecursive(const ClipboardData& data, ::XCEngine::Components::GameObject::ID parent); ::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::vector<::XCEngine::Components::GameObject*> m_rootEntities;
std::optional<ClipboardData> m_clipboard; std::optional<ClipboardData> m_clipboard;
EventBus* m_eventBus = nullptr;
std::string m_currentScenePath;
std::string m_currentSceneName = "Untitled Scene";
bool m_isSceneDirty = false;
}; };
} }

View File

@@ -0,0 +1,196 @@
#pragma once
#include "Core/IEditorContext.h"
#include "Core/IProjectManager.h"
#include "Core/ISceneManager.h"
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <commdlg.h>
#include <array>
#include <filesystem>
#include <string>
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<unsigned char>(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<wchar_t, 1024> 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<DWORD>(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<wchar_t, 1024> 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<DWORD>(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

View File

@@ -12,8 +12,10 @@ ConsolePanel::ConsolePanel() : Panel("Console") {
void ConsolePanel::Render() { void ConsolePanel::Render() {
ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None);
auto* sink = Debug::EditorConsoleSink::GetInstance();
if (ImGui::Button("Clear")) { if (ImGui::Button("Clear")) {
Debug::EditorConsoleSink::GetInstance()->Clear(); sink->Clear();
} }
ImGui::SameLine(); ImGui::SameLine();
@@ -52,7 +54,7 @@ void ConsolePanel::Render() {
ImGui::BeginChild("LogScroll", ImVec2(0, 0), false, ImGuiWindowFlags_HorizontalScrollbar); 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; size_t logIndex = 0;
for (const auto& log : logs) { for (const auto& log : logs) {
bool shouldShow = false; bool shouldShow = false;

View File

@@ -6,6 +6,8 @@
#include "Core/EventBus.h" #include "Core/EventBus.h"
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Core/Math/Quaternion.h> #include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
#include <imgui.h> #include <imgui.h>
#include <cstring> #include <cstring>
@@ -22,9 +24,6 @@ HierarchyPanel::~HierarchyPanel() {
} }
void HierarchyPanel::OnAttach() { void HierarchyPanel::OnAttach() {
auto& sceneManager = m_context->GetSceneManager();
sceneManager.CreateDemoScene();
m_selectionHandlerId = m_context->GetEventBus().Subscribe<SelectionChangedEvent>( m_selectionHandlerId = m_context->GetEventBus().Subscribe<SelectionChangedEvent>(
[this](const SelectionChangedEvent& event) { [this](const SelectionChangedEvent& event) {
OnSelectionChanged(event); OnSelectionChanged(event);
@@ -74,7 +73,16 @@ void HierarchyPanel::Render() {
::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data; ::XCEngine::Components::GameObject* sourceGameObject = *(::XCEngine::Components::GameObject**)payload->Data;
if (sourceGameObject && sourceGameObject->GetParent() != nullptr) { if (sourceGameObject && sourceGameObject->GetParent() != nullptr) {
auto& sceneManager = m_context->GetSceneManager(); 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); sceneManager.MoveEntity(sourceGameObject->GetID(), 0);
srcTransform->SetPosition(worldPos);
srcTransform->SetRotation(worldRot);
srcTransform->SetScale(worldScale);
} }
} }
ImGui::EndDragDropTarget(); ImGui::EndDragDropTarget();
@@ -195,7 +203,7 @@ void HierarchyPanel::RenderContextMenu(::XCEngine::Components::GameObject* gameO
if (gameObject != nullptr && gameObject->GetParent() != nullptr) { if (gameObject != nullptr && gameObject->GetParent() != nullptr) {
if (ImGui::MenuItem("Detach from Parent")) { 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")) { if (ImGui::MenuItem("Camera")) {
auto* newEntity = sceneManager.CreateEntity("Camera", parent); auto* newEntity = sceneManager.CreateEntity("Camera", parent);
newEntity->AddComponent<::XCEngine::Components::TransformComponent>(); newEntity->AddComponent<::XCEngine::Components::CameraComponent>();
selectionManager.SetSelectedEntity(newEntity->GetID()); selectionManager.SetSelectedEntity(newEntity->GetID());
} }
if (ImGui::MenuItem("Light")) { if (ImGui::MenuItem("Light")) {
auto* newEntity = sceneManager.CreateEntity("Light", parent); auto* newEntity = sceneManager.CreateEntity("Light", parent);
newEntity->AddComponent<::XCEngine::Components::LightComponent>();
selectionManager.SetSelectedEntity(newEntity->GetID()); selectionManager.SetSelectedEntity(newEntity->GetID());
} }
@@ -255,19 +264,16 @@ void HierarchyPanel::RenderCreateMenu(::XCEngine::Components::GameObject* parent
if (ImGui::MenuItem("Cube")) { if (ImGui::MenuItem("Cube")) {
auto* newEntity = sceneManager.CreateEntity("Cube", parent); auto* newEntity = sceneManager.CreateEntity("Cube", parent);
newEntity->AddComponent<::XCEngine::Components::TransformComponent>();
selectionManager.SetSelectedEntity(newEntity->GetID()); selectionManager.SetSelectedEntity(newEntity->GetID());
} }
if (ImGui::MenuItem("Sphere")) { if (ImGui::MenuItem("Sphere")) {
auto* newEntity = sceneManager.CreateEntity("Sphere", parent); auto* newEntity = sceneManager.CreateEntity("Sphere", parent);
newEntity->AddComponent<::XCEngine::Components::TransformComponent>();
selectionManager.SetSelectedEntity(newEntity->GetID()); selectionManager.SetSelectedEntity(newEntity->GetID());
} }
if (ImGui::MenuItem("Plane")) { if (ImGui::MenuItem("Plane")) {
auto* newEntity = sceneManager.CreateEntity("Plane", parent); auto* newEntity = sceneManager.CreateEntity("Plane", parent);
newEntity->AddComponent<::XCEngine::Components::TransformComponent>();
selectionManager.SetSelectedEntity(newEntity->GetID()); selectionManager.SetSelectedEntity(newEntity->GetID());
} }
} }

View File

@@ -1,8 +1,13 @@
#include "InspectorPanel.h" #include "InspectorPanel.h"
#include "Core/EditorContext.h" #include "Core/IEditorContext.h"
#include "Core/ISceneManager.h" #include "Core/ISceneManager.h"
#include "UI/UI.h" #include "Core/ISelectionManager.h"
#include <XCEngine/Debug/Logger.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 <imgui.h> #include <imgui.h>
#include <string> #include <string>
@@ -10,7 +15,7 @@ namespace XCEngine {
namespace Editor { namespace Editor {
InspectorPanel::InspectorPanel() : Panel("Inspector") { InspectorPanel::InspectorPanel() : Panel("Inspector") {
Debug::Logger::Get().Debug(Debug::LogCategory::General, "InspectorPanel constructed"); RegisterDefaultComponentEditors();
} }
InspectorPanel::~InspectorPanel() { InspectorPanel::~InspectorPanel() {
@@ -23,8 +28,35 @@ void InspectorPanel::OnSelectionChanged(const SelectionChangedEvent& event) {
m_selectedEntityId = event.primarySelection; m_selectedEntityId = event.primarySelection;
} }
void InspectorPanel::RegisterDefaultComponentEditors() {
RegisterComponentEditor(std::make_unique<TransformComponentEditor>());
RegisterComponentEditor(std::make_unique<CameraComponentEditor>());
RegisterComponentEditor(std::make_unique<LightComponentEditor>());
}
void InspectorPanel::RegisterComponentEditor(std::unique_ptr<IComponentEditor> 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() { void InspectorPanel::Render() {
Debug::Logger::Get().Debug(Debug::LogCategory::General, "InspectorPanel::Render START");
ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None); ImGui::Begin(m_name.c_str(), nullptr, ImGuiWindowFlags_None);
if (!m_selectionHandlerId && m_context) { if (!m_selectionHandlerId && m_context) {
@@ -51,21 +83,18 @@ void InspectorPanel::Render() {
} }
ImGui::End(); ImGui::End();
Debug::Logger::Get().Debug(Debug::LogCategory::General, "InspectorPanel::Render END");
} }
void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameObject) { void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameObject) {
Debug::Logger::Get().Debug(Debug::LogCategory::General, "RenderGameObject START");
char nameBuffer[256]; char nameBuffer[256];
strcpy_s(nameBuffer, gameObject->GetName().c_str()); strcpy_s(nameBuffer, gameObject->GetName().c_str());
ImGui::InputText("##Name", nameBuffer, sizeof(nameBuffer)); ImGui::InputText("##Name", nameBuffer, sizeof(nameBuffer));
if (ImGui::IsItemDeactivatedAfterEdit()) { if (ImGui::IsItemDeactivatedAfterEdit()) {
gameObject->SetName(nameBuffer); m_context->GetSceneManager().RenameEntity(gameObject->GetID(), nameBuffer);
} }
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::Button("Add Component")) { if (ImGui::Button("Add Component")) {
Debug::Logger::Get().Debug(Debug::LogCategory::General, "Add Component BUTTON CLICKED");
ImGui::OpenPopup("AddComponent"); ImGui::OpenPopup("AddComponent");
} }
@@ -75,53 +104,54 @@ void InspectorPanel::RenderGameObject(::XCEngine::Components::GameObject* gameOb
for (auto* component : components) { for (auto* component : components) {
RenderComponent(component, gameObject); RenderComponent(component, gameObject);
} }
Debug::Logger::Get().Debug(Debug::LogCategory::General, "RenderGameObject END");
} }
void InspectorPanel::RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject) { 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")) { if (!ImGui::BeginPopup("AddComponent")) {
Debug::Logger::Get().Debug(Debug::LogCategory::General, "BeginPopup returned false");
return; return;
} }
Debug::Logger::Get().Debug(Debug::LogCategory::General, "BeginPopup succeeded");
ImGui::Text("Components"); ImGui::Text("Components");
ImGui::Separator(); ImGui::Separator();
bool hasTransform = gameObject->GetComponent<::XCEngine::Components::TransformComponent>() != nullptr; bool drewAnyEntry = false;
Debug::Logger::Get().Debug(Debug::LogCategory::General, hasTransform ? "Has Transform: yes" : "Has Transform: no"); for (const auto& editor : m_componentEditors) {
if (!editor || !editor->ShowInAddComponentMenu()) {
Debug::Logger::Get().Debug(Debug::LogCategory::General, "About to check MenuItem condition"); continue;
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)");
} }
ImGui::Separator(); drewAnyEntry = true;
ImGui::TextDisabled("No more components available"); 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();
}
}
}
if (!drewAnyEntry) {
ImGui::TextDisabled("No registered component editors");
}
Debug::Logger::Get().Debug(Debug::LogCategory::General, "About to EndPopup - calling EndPopup");
ImGui::EndPopup(); ImGui::EndPopup();
Debug::Logger::Get().Debug(Debug::LogCategory::General, "Popup closed");
} }
void InspectorPanel::RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject) { void InspectorPanel::RenderComponent(::XCEngine::Components::Component* component, ::XCEngine::Components::GameObject* gameObject) {
if (!component) return; if (!component) return;
IComponentEditor* editor = GetEditorFor(component);
const char* name = component->GetName().c_str(); const char* name = component->GetName().c_str();
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 2}); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2{4, 2});
@@ -138,8 +168,9 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen
ImGui::PopStyleVar(); ImGui::PopStyleVar();
bool removeComponent = false; bool removeComponent = false;
const bool canRemoveComponent = editor ? editor->CanRemove(component) : false;
if (ImGui::BeginPopupContextItem("ComponentSettings")) { if (ImGui::BeginPopupContextItem("ComponentSettings")) {
if (ImGui::MenuItem("Remove Component")) { if (ImGui::MenuItem("Remove Component", nullptr, false, canRemoveComponent)) {
removeComponent = true; removeComponent = true;
} }
ImGui::EndPopup(); ImGui::EndPopup();
@@ -151,22 +182,12 @@ void InspectorPanel::RenderComponent(::XCEngine::Components::Component* componen
} }
if (open) { if (open) {
if (auto* transform = dynamic_cast<::XCEngine::Components::TransformComponent*>(component)) { if (editor) {
::XCEngine::Math::Vector3 position = transform->GetLocalPosition(); if (editor->Render(component)) {
::XCEngine::Math::Vector3 rotation = transform->GetLocalEulerAngles(); m_context->GetSceneManager().MarkSceneDirty();
::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);
} }
} else {
ImGui::TextDisabled("No registered editor for this component");
} }
ImGui::TreePop(); ImGui::TreePop();
@@ -180,13 +201,8 @@ void InspectorPanel::RemoveComponentByType(::XCEngine::Components::Component* co
return; return;
} }
auto components = gameObject->GetComponents<::XCEngine::Components::Component>(); gameObject->RemoveComponent(component);
for (auto* comp : components) { m_context->GetSceneManager().MarkSceneDirty();
if (comp == component) {
gameObject->RemoveComponent<::XCEngine::Components::Component>();
break;
}
}
} }
} }

View File

@@ -1,11 +1,21 @@
#pragma once #pragma once
#include "Panel.h" #include "Panel.h"
#include <XCEngine/Components/TransformComponent.h>
#include <cstdint>
#include <memory>
#include <vector>
namespace XCEngine { namespace XCEngine {
namespace Components {
class Component;
class GameObject;
}
namespace Editor { namespace Editor {
class IComponentEditor;
class InspectorPanel : public Panel { class InspectorPanel : public Panel {
public: public:
InspectorPanel(); InspectorPanel();
@@ -14,6 +24,9 @@ public:
void Render() override; void Render() override;
private: private:
void RegisterDefaultComponentEditors();
void RegisterComponentEditor(std::unique_ptr<IComponentEditor> editor);
IComponentEditor* GetEditorFor(::XCEngine::Components::Component* component) const;
void RenderGameObject(::XCEngine::Components::GameObject* gameObject); void RenderGameObject(::XCEngine::Components::GameObject* gameObject);
void RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject); void RenderAddComponentPopup(::XCEngine::Components::GameObject* gameObject);
void RenderComponent(::XCEngine::Components::Component* component, ::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_selectionHandlerId = 0;
uint64_t m_selectedEntityId = 0; uint64_t m_selectedEntityId = 0;
std::vector<std::unique_ptr<IComponentEditor>> m_componentEditors;
}; };
} }

View File

@@ -1,4 +1,8 @@
#include "MenuBar.h" #include "MenuBar.h"
#include "Core/IEditorContext.h"
#include "Core/ISceneManager.h"
#include "Utils/SceneEditorUtils.h"
#include <filesystem>
#include <imgui.h> #include <imgui.h>
namespace XCEngine { namespace XCEngine {
@@ -7,20 +11,103 @@ namespace Editor {
MenuBar::MenuBar() : Panel("MenuBar") {} MenuBar::MenuBar() : Panel("MenuBar") {}
void MenuBar::Render() { void MenuBar::Render() {
HandleShortcuts();
if (ImGui::BeginMainMenuBar()) { if (ImGui::BeginMainMenuBar()) {
ShowFileMenu(); ShowFileMenu();
ShowEditMenu(); ShowEditMenu();
ShowViewMenu(); ShowViewMenu();
ShowHelpMenu(); ShowHelpMenu();
RenderSceneStatus();
ImGui::EndMainMenuBar(); 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() { void MenuBar::ShowFileMenu() {
if (ImGui::BeginMenu("File")) { if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("New Scene", "Ctrl+N")) {} if (ImGui::MenuItem("New Scene", "Ctrl+N")) {
if (ImGui::MenuItem("Open Scene", "Ctrl+O")) {} NewScene();
if (ImGui::MenuItem("Save Scene", "Ctrl+S")) {} }
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(); ImGui::Separator();
if (ImGui::MenuItem("Exit", "Alt+F4")) {} if (ImGui::MenuItem("Exit", "Alt+F4")) {}
ImGui::EndMenu(); 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();
}
}
} }
} }

View File

@@ -11,6 +11,12 @@ public:
void Render() override; void Render() override;
private: private:
void HandleShortcuts();
void NewScene();
void OpenScene();
void SaveScene();
void SaveSceneAs();
void RenderSceneStatus();
void ShowFileMenu(); void ShowFileMenu();
void ShowEditMenu(); void ShowEditMenu();
void ShowViewMenu(); void ShowViewMenu();

View File

@@ -1,7 +1,9 @@
#include "ProjectPanel.h" #include "ProjectPanel.h"
#include "Core/IEditorContext.h" #include "Core/IEditorContext.h"
#include "Core/IProjectManager.h" #include "Core/IProjectManager.h"
#include "Core/ISceneManager.h"
#include "Core/AssetItem.h" #include "Core/AssetItem.h"
#include "Utils/SceneEditorUtils.h"
#include <imgui.h> #include <imgui.h>
#include <imgui_internal.h> #include <imgui_internal.h>
@@ -83,7 +85,7 @@ void ProjectPanel::Render() {
auto& items = manager.GetCurrentItems(); auto& items = manager.GetCurrentItems();
std::string searchStr = m_searchBuffer; std::string searchStr = m_searchBuffer;
int itemIndex = 0; int displayedCount = 0;
for (int i = 0; i < (int)items.size(); i++) { for (int i = 0; i < (int)items.size(); i++) {
if (!searchStr.empty()) { if (!searchStr.empty()) {
@@ -92,11 +94,11 @@ void ProjectPanel::Render() {
} }
} }
if (itemIndex > 0 && itemIndex % columns != 0) { if (displayedCount > 0 && displayedCount % columns != 0) {
ImGui::SameLine(); ImGui::SameLine();
} }
RenderAssetItem(items[i], itemIndex); RenderAssetItem(items[i], i);
itemIndex++; displayedCount++;
} }
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -108,9 +110,13 @@ void ProjectPanel::Render() {
if (ImGui::BeginPopup("ItemContextMenu")) { if (ImGui::BeginPopup("ItemContextMenu")) {
if (m_contextMenuIndex >= 0 && m_contextMenuIndex < (int)items.size()) { if (m_contextMenuIndex >= 0 && m_contextMenuIndex < (int)items.size()) {
auto& item = items[m_contextMenuIndex]; auto& item = items[m_contextMenuIndex];
if (item->isFolder) { if (item->isFolder || item->type == "Scene") {
if (ImGui::MenuItem("Open")) { if (ImGui::MenuItem("Open")) {
if (item->isFolder) {
manager.NavigateToFolder(item); manager.NavigateToFolder(item);
} else if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) {
m_context->GetSceneManager().LoadScene(item->fullPath);
}
} }
ImGui::Separator(); ImGui::Separator();
} }
@@ -276,8 +282,14 @@ void ProjectPanel::RenderAssetItem(const AssetItemPtr& item, int index) {
} }
} }
if (doubleClicked && item->isFolder) { if (doubleClicked) {
if (item->isFolder) {
manager.NavigateToFolder(item); manager.NavigateToFolder(item);
} else if (item->type == "Scene") {
if (SceneEditorUtils::ConfirmSceneSwitch(*m_context)) {
m_context->GetSceneManager().LoadScene(item->fullPath);
}
}
} }
ImGui::PopID(); ImGui::PopID();

View File

@@ -243,9 +243,17 @@ add_library(XCEngine STATIC
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h ${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/TransformComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/GameObject.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/Component.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/TransformComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Components/TransformComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Components/GameObject.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 # Scene
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h

View File

@@ -0,0 +1,57 @@
#pragma once
#include <XCEngine/Components/Component.h>
#include <XCEngine/Core/Math/Color.h>
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

View File

@@ -5,10 +5,12 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include <memory> #include <memory>
#include <algorithm>
#include <unordered_map> #include <unordered_map>
#include <istream> #include <istream>
#include <ostream> #include <ostream>
#include <random> #include <random>
#include <type_traits>
namespace XCEngine { namespace XCEngine {
namespace Components { namespace Components {
@@ -38,6 +40,10 @@ public:
template<typename T, typename... Args> template<typename T, typename... Args>
T* AddComponent(Args&&... args) { T* AddComponent(Args&&... args) {
if constexpr (std::is_base_of_v<TransformComponent, T>) {
return dynamic_cast<T*>(m_transform);
}
auto component = std::make_unique<T>(std::forward<Args>(args)...); auto component = std::make_unique<T>(std::forward<Args>(args)...);
component->m_gameObject = this; component->m_gameObject = this;
T* ptr = component.get(); T* ptr = component.get();
@@ -47,6 +53,10 @@ public:
template<typename T> template<typename T>
T* GetComponent() { T* GetComponent() {
if (T* casted = dynamic_cast<T*>(m_transform)) {
return casted;
}
for (auto& comp : m_components) { for (auto& comp : m_components) {
if (T* casted = dynamic_cast<T*>(comp.get())) { if (T* casted = dynamic_cast<T*>(comp.get())) {
return casted; return casted;
@@ -57,6 +67,10 @@ public:
template<typename T> template<typename T>
const T* GetComponent() const { const T* GetComponent() const {
if (const T* casted = dynamic_cast<const T*>(m_transform)) {
return casted;
}
for (auto& comp : m_components) { for (auto& comp : m_components) {
if (T* casted = dynamic_cast<T*>(comp.get())) { if (T* casted = dynamic_cast<T*>(comp.get())) {
return casted; return casted;
@@ -68,6 +82,9 @@ public:
template<typename T> template<typename T>
std::vector<T*> GetComponents() { std::vector<T*> GetComponents() {
std::vector<T*> result; std::vector<T*> result;
if (T* casted = dynamic_cast<T*>(m_transform)) {
result.push_back(casted);
}
for (auto& comp : m_components) { for (auto& comp : m_components) {
if (T* casted = dynamic_cast<T*>(comp.get())) { if (T* casted = dynamic_cast<T*>(comp.get())) {
result.push_back(casted); result.push_back(casted);
@@ -79,6 +96,9 @@ public:
template<typename T> template<typename T>
std::vector<const T*> GetComponents() const { std::vector<const T*> GetComponents() const {
std::vector<const T*> result; std::vector<const T*> result;
if (const T* casted = dynamic_cast<const T*>(m_transform)) {
result.push_back(casted);
}
for (auto& comp : m_components) { for (auto& comp : m_components) {
if (const T* casted = dynamic_cast<const T*>(comp.get())) { if (const T* casted = dynamic_cast<const T*>(comp.get())) {
result.push_back(casted); result.push_back(casted);
@@ -89,6 +109,10 @@ public:
template<typename T> template<typename T>
void RemoveComponent() { void RemoveComponent() {
if (dynamic_cast<T*>(m_transform)) {
return;
}
for (auto it = m_components.begin(); it != m_components.end(); ++it) { for (auto it = m_components.begin(); it != m_components.end(); ++it) {
if (T* casted = dynamic_cast<T*>(it->get())) { if (T* casted = dynamic_cast<T*>(it->get())) {
m_components.erase(it); 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<Component>& entry) {
return entry.get() == component;
});
if (it == m_components.end()) {
return false;
}
m_components.erase(it);
return true;
}
template<typename T> template<typename T>
T* GetComponentInChildren() { T* GetComponentInChildren() {
T* comp = GetComponent<T>(); T* comp = GetComponent<T>();

View File

@@ -0,0 +1,50 @@
#pragma once
#include <XCEngine/Components/Component.h>
#include <XCEngine/Core/Math/Color.h>
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

View File

@@ -0,0 +1,77 @@
#include "Components/CameraComponent.h"
#include <algorithm>
#include <sstream>
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<int>(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<CameraProjectionType>(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

View File

@@ -48,32 +48,30 @@ void GameObject::SetParent(GameObject* parent) {
} }
void GameObject::SetParent(GameObject* parent, bool worldPositionStays) { void GameObject::SetParent(GameObject* parent, bool worldPositionStays) {
if (m_parent == parent) { if (m_parent == parent || parent == this) {
return; 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) { if (m_parent) {
auto& siblings = m_parent->m_children; auto& siblings = m_parent->m_children;
siblings.erase(std::remove(siblings.begin(), siblings.end(), this), siblings.end()); 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; m_parent = parent;
if (m_parent) { if (m_parent) {
m_parent->m_children.push_back(this); 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()->SetParent(parent ? parent->GetTransform() : nullptr, worldPositionStays);
GetTransform()->SetPosition(worldPos);
GetTransform()->SetRotation(worldRot);
GetTransform()->SetScale(worldScale);
}
GetTransform()->SetDirty();
} }
GameObject* GameObject::GetChild(size_t index) const { GameObject* GameObject::GetChild(size_t index) const {
@@ -88,32 +86,17 @@ std::vector<GameObject*> GameObject::GetChildren() const {
} }
void GameObject::DetachChildren() { void GameObject::DetachChildren() {
for (auto* child : m_children) { auto children = m_children;
for (auto* child : children) {
if (child) { if (child) {
child->m_parent = nullptr; child->SetParent(nullptr, true);
} }
} }
m_children.clear();
} }
void GameObject::DetachFromParent() { void GameObject::DetachFromParent() {
if (m_parent) { if (m_parent) {
Math::Vector3 worldPos = GetTransform()->GetPosition(); SetParent(nullptr, true);
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();
} }
} }

View File

@@ -0,0 +1,64 @@
#include "Components/LightComponent.h"
#include <algorithm>
#include <sstream>
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<int>(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<LightType>(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

View File

@@ -1,10 +1,126 @@
#include "Scene/Scene.h" #include "Scene/Scene.h"
#include "Components/GameObject.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 <sstream> #include <sstream>
#include <algorithm>
#include <unordered_map>
namespace XCEngine { namespace XCEngine {
namespace Components { 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<PendingComponentData> 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<CameraComponent>();
}
if (type == "Light") {
return gameObject->AddComponent<LightComponent>();
}
if (type == "AudioSource") {
return gameObject->AddComponent<AudioSourceComponent>();
}
if (type == "AudioListener") {
return gameObject->AddComponent<AudioListenerComponent>();
}
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<Component>();
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() Scene::Scene()
: m_name("Untitled") { : m_name("Untitled") {
} }
@@ -24,14 +140,13 @@ GameObject* Scene::CreateGameObject(const std::string& name, GameObject* parent)
GameObject::GetGlobalRegistry()[ptr->m_id] = ptr; GameObject::GetGlobalRegistry()[ptr->m_id] = ptr;
m_gameObjectIDs.insert(ptr->m_id); m_gameObjectIDs.insert(ptr->m_id);
m_gameObjects.emplace(ptr->m_id, std::move(gameObject)); m_gameObjects.emplace(ptr->m_id, std::move(gameObject));
ptr->m_scene = this;
if (parent) { if (parent) {
ptr->SetParent(parent); ptr->SetParent(parent);
} else { } else {
m_rootGameObjects.push_back(ptr->m_id); m_rootGameObjects.push_back(ptr->m_id);
} }
ptr->m_scene = this;
ptr->Awake(); ptr->Awake();
m_onGameObjectCreated.Invoke(ptr); m_onGameObjectCreated.Invoke(ptr);
@@ -159,26 +274,118 @@ void Scene::Load(const std::string& filePath) {
m_rootGameObjects.clear(); m_rootGameObjects.clear();
m_gameObjectIDs.clear(); m_gameObjectIDs.clear();
std::vector<PendingGameObjectData> pendingObjects;
std::string line; std::string line;
PendingGameObjectData* currentObject = nullptr;
GameObject::ID maxId = 0;
while (std::getline(file, line)) { while (std::getline(file, line)) {
if (line.empty() || line[0] == '#') continue; if (line.empty() || line[0] == '#') continue;
std::istringstream iss(line); if (line == "gameobject_begin") {
std::string token; pendingObjects.emplace_back();
std::getline(iss, token, '='); currentObject = &pendingObjects.back();
continue;
}
if (token == "scene") { if (line == "gameobject_end") {
std::getline(iss, m_name, ';'); currentObject = nullptr;
} else if (token == "gameobject") { continue;
auto go = std::make_unique<GameObject>(); }
go->Deserialize(iss);
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<GameObject::ID>(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<GameObject::ID>(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<GameObject::ID, GameObject*> createdObjects;
createdObjects.reserve(pendingObjects.size());
for (const PendingGameObjectData& pending : pendingObjects) {
auto go = std::make_unique<GameObject>(pending.name);
go->m_id = pending.id;
go->m_activeSelf = pending.active;
go->m_scene = this; 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* ptr = go.get();
GameObject::GetGlobalRegistry()[ptr->m_id] = ptr; GameObject::GetGlobalRegistry()[ptr->m_id] = ptr;
m_gameObjectIDs.insert(ptr->m_id); m_gameObjectIDs.insert(ptr->m_id);
m_rootGameObjects.push_back(ptr->m_id); createdObjects[ptr->m_id] = ptr;
m_gameObjects.emplace(ptr->m_id, std::move(go)); 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,12 +396,11 @@ void Scene::Save(const std::string& filePath) {
} }
file << "# XCEngine Scene File\n"; file << "# XCEngine Scene File\n";
file << "scene=" << m_name << ";\n"; file << "scene=" << EscapeString(m_name) << "\n";
file << "active=" << (m_active ? "1" : "0") << ";\n\n"; file << "active=" << (m_active ? "1" : "0") << "\n\n";
for (auto* go : GetRootGameObjects()) { for (auto* go : GetRootGameObjects()) {
file << "gameobject="; SerializeGameObjectRecursive(file, go);
go->Serialize(file);
file << "\n"; file << "\n";
} }
} }

View File

@@ -6,6 +6,7 @@ set(COMPONENTS_TEST_SOURCES
test_component.cpp test_component.cpp
test_transform_component.cpp test_transform_component.cpp
test_game_object.cpp test_game_object.cpp
test_camera_light_component.cpp
) )
add_executable(components_tests ${COMPONENTS_TEST_SOURCES}) add_executable(components_tests ${COMPONENTS_TEST_SOURCES})

View File

@@ -0,0 +1,57 @@
#include <gtest/gtest.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/LightComponent.h>
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

View File

@@ -142,7 +142,7 @@ TEST(GameObject_Test, SetParent_WithoutWorldPosition) {
child.SetParent(&parent, false); child.SetParent(&parent, false);
Vector3 childWorldPos = child.GetTransform()->GetPosition(); 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) { TEST(GameObject_Test, GetChild_ValidIndex) {

View File

@@ -1,8 +1,11 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <XCEngine/Scene/Scene.h> #include <XCEngine/Scene/Scene.h>
#include <XCEngine/Components/CameraComponent.h>
#include <XCEngine/Components/GameObject.h> #include <XCEngine/Components/GameObject.h>
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/TransformComponent.h> #include <XCEngine/Components/TransformComponent.h>
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
#include <filesystem>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
@@ -36,6 +39,10 @@ protected:
testScene = std::make_unique<Scene>("TestScene"); testScene = std::make_unique<Scene>("TestScene");
} }
std::filesystem::path GetTempScenePath(const char* fileName) const {
return std::filesystem::temp_directory_path() / fileName;
}
std::unique_ptr<Scene> testScene; std::unique_ptr<Scene> testScene;
}; };
@@ -223,29 +230,127 @@ TEST_F(SceneTest, Save_And_Load) {
go->GetTransform()->SetLocalPosition(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); go->GetTransform()->SetLocalPosition(XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
go->SetActive(true); go->SetActive(true);
testScene->Save("test_scene.scene"); const std::filesystem::path scenePath = GetTempScenePath("test_scene.xc");
testScene->Save(scenePath.string());
Scene loadedScene; Scene loadedScene;
loadedScene.Load("test_scene.scene"); loadedScene.Load(scenePath.string());
GameObject* loadedGo = loadedScene.Find("SavedObject"); GameObject* loadedGo = loadedScene.Find("SavedObject");
EXPECT_NE(loadedGo, nullptr); EXPECT_NE(loadedGo, nullptr);
EXPECT_EQ(loadedGo->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f)); EXPECT_EQ(loadedGo->GetTransform()->GetLocalPosition(), XCEngine::Math::Vector3(1.0f, 2.0f, 3.0f));
std::filesystem::remove(scenePath);
} }
TEST_F(SceneTest, Save_ContainsGameObjectData) { TEST_F(SceneTest, Save_ContainsGameObjectData) {
testScene->CreateGameObject("Player"); testScene->CreateGameObject("Player");
testScene->CreateGameObject("Enemy"); testScene->CreateGameObject("Enemy");
testScene->Save("test_scene_multi.scene"); const std::filesystem::path scenePath = GetTempScenePath("test_scene_multi.xc");
testScene->Save(scenePath.string());
std::ifstream file("test_scene_multi.scene"); std::ifstream file(scenePath);
std::stringstream buffer; std::stringstream buffer;
buffer << file.rdbuf(); buffer << file.rdbuf();
std::string content = buffer.str(); std::string content = buffer.str();
file.close();
EXPECT_TRUE(content.find("Player") != std::string::npos); EXPECT_TRUE(content.find("Player") != std::string::npos);
EXPECT_TRUE(content.find("Enemy") != std::string::npos); EXPECT_TRUE(content.find("Enemy") != std::string::npos);
std::filesystem::remove(scenePath);
}
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<LightComponent>();
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<CameraComponent>();
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<LightComponent>();
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<CameraComponent>();
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<CameraComponent>();
parent->AddComponent<LightComponent>();
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 } // namespace