Skybox/SkyLight are now Actor-carried components instead of resident global settings, matching UE where SkyAtmosphere/SkyLight/fog are Actors added from Place Actors and configured in Details. - New SkyboxComponent (active + envPath asset slot) and SkyLightComponent (active + ambientColor); first-active entity wins per frame - SceneRenderer collects env from components into FrameSceneData (DrawSkybox/envPath/ambient); Renderer performs delta sync - env reloads only when the resolved path changes, neutral fallback when empty or unloadable - App-side env plumbing removed: kEnvPaths table and getEnvIndex/ setEnvIndex callbacks deleted; StatsPanel keeps ClearColor + debug view, env params move to Details - Serializer: scene-level Settings stores ClearColor only; env components serialize per entity; legacy v1 scenes (Environment header + Settings.AmbientColor/DrawSkybox) auto-migrate into an Environment entity on load for visual parity - Inspector: Skybox/Sky Light sections with .hdr asset slot dropdown and Add Component entries; Outliner "+ Create" popup offers Empty/Light/Camera/Environment
477 lines
16 KiB
C++
477 lines
16 KiB
C++
//
|
||
// Editor.cpp:docking 骨架实现。
|
||
//
|
||
// 默认布局 = UE5 出厂布局(首帧 DockBuilder 一次,之后 imgui.ini 接管):
|
||
// ┌──────────┬──────────────────────┬───────────┐
|
||
// │ Outliner │ │ │
|
||
// │ │ Viewport │ Details │
|
||
// │ │ (+Stats tab) │ │
|
||
// ├──────────┴──────────────────────┤ │
|
||
// │ Content Browser(左+中通栏) │ │
|
||
// └─────────────────────────────────┴───────────┘
|
||
//
|
||
|
||
#include "Editor.h"
|
||
#include "AssetBrowserPanel.h"
|
||
#include "HierarchyPanel.h"
|
||
#include "InspectorPanel.h"
|
||
#include "ViewportPanel.h"
|
||
#include "StatsPanel.h"
|
||
|
||
#include <Engine/Core/Log.h>
|
||
#include <Engine/Core/Time.h>
|
||
#include <Engine/Serialization/SceneSerializer.h>
|
||
|
||
#include <imgui.h>
|
||
#include <imgui_internal.h> // DockBuilder*(内部 API)
|
||
#include <ImGuizmo.h>
|
||
|
||
#include <GLFW/glfw3.h>
|
||
|
||
#include <filesystem>
|
||
#include <fstream>
|
||
|
||
using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h)
|
||
|
||
Editor::Editor() = default;
|
||
|
||
Editor::~Editor() = default;
|
||
|
||
void Editor::Init(const EditorContext& ctx)
|
||
{
|
||
m_Ctx = ctx;
|
||
// PIE 运行态指针:指向本对象成员(ctx 拷贝后补接,面板只读)
|
||
m_Ctx.playState = &m_Playing;
|
||
|
||
m_Hierarchy = std::make_unique<HierarchyPanel>();
|
||
m_Inspector = std::make_unique<InspectorPanel>();
|
||
m_Viewport = std::make_unique<ViewportPanel>();
|
||
m_Stats = std::make_unique<StatsPanel>();
|
||
m_AssetBrowser = std::make_unique<AssetBrowserPanel>();
|
||
|
||
// 旧布局一次性迁移:面板改名(Hierarchy→Outliner / Inspector→Details /
|
||
// Asset Browser→Content Browser)= docking 身份全变,旧 imgui.ini 对
|
||
// 新窗口名失效(新窗口首启浮动,而默认布局又因 dock node 非空不再
|
||
// 重建)。检测"有旧名无新名" → 删 ini 让 UE 默认布局重建一次。
|
||
// 此刻 ImGui 首帧 NewFrame 还未读盘(NewFrame 内才 LoadIniSettings),
|
||
// 删文件干净无副作用;新式 ini 或无 ini 不动
|
||
if (ImGui::GetCurrentContext() && ImGui::GetIO().IniFilename)
|
||
{
|
||
const char* path = ImGui::GetIO().IniFilename;
|
||
std::ifstream file(path);
|
||
if (file)
|
||
{
|
||
const std::string content((std::istreambuf_iterator<char>(file)),
|
||
std::istreambuf_iterator<char>());
|
||
file.close();
|
||
if (content.find("[Window][Hierarchy]") != std::string::npos &&
|
||
content.find("[Window][Outliner]") == std::string::npos)
|
||
{
|
||
std::error_code ec;
|
||
std::filesystem::remove(path, ec);
|
||
ENGINE_INFO("Editor: legacy imgui.ini removed, "
|
||
"UE5 default layout will be rebuilt");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- PIE(Play-In-Editor)----
|
||
|
||
// Play:当前场景 → 内存 YAML 快照 → 切游戏渲染(primary 相机实体)
|
||
// → 解冻时间(物理/脚本/动画开始跑)。快照失败不进 Play(现场不动)
|
||
void Editor::DoPlay()
|
||
{
|
||
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return;
|
||
|
||
m_PlaySnapshot = Serialization::SceneSerializer::SerializeToString(
|
||
*m_Ctx.scene, *m_Ctx.assets, "PlaySnapshot");
|
||
if (m_PlaySnapshot.empty())
|
||
{
|
||
ENGINE_ERROR("PIE: snapshot failed, play aborted");
|
||
return;
|
||
}
|
||
|
||
m_Playing = true;
|
||
m_Ctx.renderer->SetDisplayMode(kDisplayDirect);
|
||
Time::SetTimeScale(1.0f);
|
||
ENGINE_INFO("PIE: playing (scene snapshot {} bytes)",
|
||
m_PlaySnapshot.size());
|
||
}
|
||
|
||
// Stop:快照原子换血恢复 → 回编辑器渲染 → 冻结时间。
|
||
// 换血后所有 Entity 句柄悬空:清选中(描边同步清)、Gizmo 拖拽态
|
||
// 复位。物理 body/约束/角色由对账系统随实体重建/清理,无需干预。
|
||
// 恢复失败(快照损坏,理论罕见)也必须回编辑态,现场留给用户
|
||
// File→Load 或退出前 Save
|
||
void Editor::DoStop()
|
||
{
|
||
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return;
|
||
|
||
if (!Serialization::SceneSerializer::DeserializeFromString(
|
||
*m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot))
|
||
{
|
||
ENGINE_ERROR("PIE: snapshot restore failed (scene is now the "
|
||
"played-out state)");
|
||
}
|
||
m_PlaySnapshot.clear();
|
||
|
||
m_Playing = false;
|
||
m_Ctx.renderer->SetDisplayMode(kDisplayViewport);
|
||
Time::SetTimeScale(0.0f);
|
||
SelectEntity(entt::null);
|
||
m_Gizmo.dragging = false;
|
||
ENGINE_INFO("PIE: stopped, editor scene restored");
|
||
}
|
||
|
||
void Editor::TogglePlay()
|
||
{
|
||
m_Playing ? DoStop() : DoPlay();
|
||
}
|
||
|
||
void Editor::Draw()
|
||
{
|
||
// ImGuizmo 每帧首调(官方约定):内部会清 mbOverGizmoHotspot 等
|
||
// 帧级状态——不调则 hover 过一次杆后 hotspot 永久置位,GetMoveType
|
||
// 恒返 MT_NONE → 杆可见但永远抓不住(只能看不能拖)
|
||
ImGuizmo::BeginFrame();
|
||
|
||
// host 窗口:铺满主视口,承载菜单栏 + DockSpace
|
||
ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||
ImGui::SetNextWindowPos(viewport->Pos);
|
||
ImGui::SetNextWindowSize(viewport->Size);
|
||
ImGui::SetNextWindowViewport(viewport->ID);
|
||
|
||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
|
||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
|
||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
|
||
ImGui::Begin("##EditorDockHost", nullptr,
|
||
ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking |
|
||
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse |
|
||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
|
||
ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||
ImGuiWindowFlags_NoNavFocus);
|
||
ImGui::PopStyleVar(3);
|
||
|
||
DrawMenuBar();
|
||
DrawDockSpace();
|
||
|
||
ImGui::End();
|
||
|
||
// 常规面板窗口(窗口名 = docking 身份)
|
||
if (m_ShowHierarchy)
|
||
{
|
||
const entt::entity prev = m_Selected;
|
||
m_Hierarchy->Draw(m_Ctx, m_Selected);
|
||
// 树点选同步描边(面板直写 m_Selected,不走 Viewport 的
|
||
// SelectEntity 返回值通道;对比前后变化触发)
|
||
if (m_Selected != prev) {
|
||
SelectEntity(m_Selected);
|
||
}
|
||
}
|
||
if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected);
|
||
if (m_ShowViewport)
|
||
{
|
||
if (auto picked = m_Viewport->Draw(m_Ctx, m_VpInput, m_Gizmo,
|
||
m_Selected)) {
|
||
SelectEntity(*picked); // Viewport 点击拾取 → 选中联动 Inspector/Hierarchy
|
||
}
|
||
}
|
||
if (m_ShowStats) m_Stats->Draw(m_Ctx);
|
||
if (m_ShowAssetBrowser) m_AssetBrowser->Draw(m_Ctx);
|
||
|
||
DrawQuitModal();
|
||
DrawSceneModals();
|
||
}
|
||
|
||
// 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里
|
||
// 只决定"剩余输入给不给游戏侧"。UI 被隐藏(反引号)时不会走到
|
||
// Draw → m_VpInput 停在旧值,谓词调用方(SandboxApp)须先查
|
||
// GetImGuiEnabled() 再问这里。
|
||
bool Editor::BlockingKeyboard() const
|
||
{
|
||
const ImGuiIO& io = ImGui::GetIO();
|
||
if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键
|
||
|
||
// 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入
|
||
if (m_Ctx.renderer &&
|
||
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
// 编辑器形态:Viewport 面板持焦才拥有键盘(UE Scene view 语义)
|
||
return !m_VpInput.focused;
|
||
}
|
||
|
||
bool Editor::BlockingMouse() const
|
||
{
|
||
// Gizmo 拖拽中:鼠标归操纵杆,不给游戏侧相机(松手自动恢复)
|
||
if (m_Gizmo.dragging) return true;
|
||
|
||
// 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false)
|
||
if (m_Ctx.renderer &&
|
||
m_Ctx.renderer->GetDisplayMode() == kDisplayDirect)
|
||
{
|
||
return ImGui::GetIO().WantCaptureMouse;
|
||
}
|
||
|
||
// 编辑器形态:悬停 Viewport 才放行(模态框遮挡时 hovered=false)
|
||
return !m_VpInput.hovered;
|
||
}
|
||
|
||
void Editor::DrawMenuBar()
|
||
{
|
||
if (!ImGui::BeginMenuBar())
|
||
{
|
||
return;
|
||
}
|
||
|
||
// ---- PIE 工具栏(UE 主工具栏语义):Play/Stop 切换按钮 +
|
||
// Ctrl+P 全局热键。运行态按钮变 Stop(黄底提示在视口内)----
|
||
{
|
||
const ImGuiIO& io = ImGui::GetIO();
|
||
// Ctrl+P:文本输入中(改名/路径框)不触发,防打字误切 PIE
|
||
if (!m_Playing && io.KeyCtrl && !io.WantTextInput &&
|
||
ImGui::IsKeyPressed(ImGuiKey_P, false))
|
||
{
|
||
TogglePlay();
|
||
}
|
||
|
||
if (m_Playing)
|
||
{
|
||
ImGui::PushStyleColor(ImGuiCol_Button,
|
||
ImVec4(0.75f, 0.45f, 0.10f, 1.0f));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
|
||
ImVec4(0.85f, 0.55f, 0.15f, 1.0f));
|
||
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
|
||
ImVec4(0.90f, 0.62f, 0.20f, 1.0f));
|
||
if (ImGui::SmallButton("Stop (Ctrl+P)"))
|
||
{
|
||
TogglePlay();
|
||
}
|
||
ImGui::PopStyleColor(3);
|
||
ImGui::SameLine();
|
||
ImGui::TextColored(ImVec4(1.0f, 0.72f, 0.25f, 1.0f),
|
||
"PLAYING - editor input disabled");
|
||
}
|
||
else if (ImGui::SmallButton("Play (Ctrl+P)"))
|
||
{
|
||
TogglePlay();
|
||
}
|
||
ImGui::SameLine();
|
||
ImGui::Separator();
|
||
}
|
||
|
||
if (ImGui::BeginMenu("File"))
|
||
{
|
||
// 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘,
|
||
// Load 会撕掉快照恢复链(UE 运行态同样禁)
|
||
ImGui::BeginDisabled(m_Playing);
|
||
if (ImGui::MenuItem("Save Scene...", nullptr))
|
||
{
|
||
m_ShowSaveModal = true;
|
||
}
|
||
if (ImGui::MenuItem("Load Scene...", nullptr))
|
||
{
|
||
m_ShowLoadModal = true;
|
||
}
|
||
ImGui::EndDisabled();
|
||
ImGui::Separator();
|
||
if (ImGui::MenuItem("Quit", "Alt+F4"))
|
||
{
|
||
RequestQuit();
|
||
}
|
||
ImGui::EndMenu();
|
||
}
|
||
|
||
if (ImGui::BeginMenu("View"))
|
||
{
|
||
ImGui::MenuItem("Outliner", nullptr, &m_ShowHierarchy);
|
||
ImGui::MenuItem("Details", nullptr, &m_ShowInspector);
|
||
ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport);
|
||
ImGui::MenuItem("Stats", nullptr, &m_ShowStats);
|
||
ImGui::MenuItem("Content Browser", nullptr, &m_ShowAssetBrowser);
|
||
ImGui::EndMenu();
|
||
}
|
||
|
||
ImGui::EndMenuBar();
|
||
}
|
||
|
||
void Editor::DrawDockSpace()
|
||
{
|
||
const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace");
|
||
|
||
// 默认布局:仅当 dock node 不存在时构建一次
|
||
//(imgui.ini 已保存过布局 → DockBuilderGetNode 非空 → 跳过)
|
||
if (!m_LayoutBuilt && ImGui::DockBuilderGetNode(dockspaceId) == nullptr)
|
||
{
|
||
m_LayoutBuilt = true;
|
||
|
||
ImGui::DockBuilderRemoveNode(dockspaceId);
|
||
ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace);
|
||
ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->Size);
|
||
|
||
// UE5 出厂布局:Details 右列通栏 → Content Browser 底部通栏
|
||
//(左+中)→ Outliner 左列 → 剩余全给 Viewport(Stats 作其 tab)
|
||
ImGuiID dockRight = 0, dockBottom = 0, dockLeft = 0, dockCenter = 0;
|
||
ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Right, 0.26f,
|
||
&dockRight, &dockCenter);
|
||
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Down, 0.30f,
|
||
&dockBottom, &dockCenter);
|
||
ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Left, 0.22f,
|
||
&dockLeft, &dockCenter);
|
||
|
||
ImGui::DockBuilderDockWindow("Outliner", dockLeft);
|
||
ImGui::DockBuilderDockWindow("Viewport", dockCenter);
|
||
ImGui::DockBuilderDockWindow("Stats", dockCenter); // Viewport 旁 tab
|
||
ImGui::DockBuilderDockWindow("Content Browser", dockBottom);
|
||
ImGui::DockBuilderDockWindow("Details", dockRight);
|
||
|
||
ImGui::DockBuilderFinish(dockspaceId);
|
||
}
|
||
|
||
ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f),
|
||
ImGuiDockNodeFlags_None);
|
||
}
|
||
|
||
void Editor::DrawQuitModal()
|
||
{
|
||
if (m_ShowQuitConfirm)
|
||
{
|
||
ImGui::OpenPopup("Should quit?");
|
||
m_ShowQuitConfirm = false;
|
||
}
|
||
|
||
ImGui::SetNextWindowPos(
|
||
ImGui::GetMainViewport()->GetCenter(),
|
||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||
|
||
if (ImGui::BeginPopupModal("Should quit?", nullptr,
|
||
ImGuiWindowFlags_AlwaysAutoResize))
|
||
{
|
||
ImGui::TextUnformatted("Quit the application?");
|
||
ImGui::Separator();
|
||
if (ImGui::Button("Yes", ImVec2(120, 0)))
|
||
{
|
||
m_QuitConfirmed = true;
|
||
ImGui::CloseCurrentPopup();
|
||
}
|
||
ImGui::SameLine();
|
||
if (ImGui::Button("No", ImVec2(120, 0)))
|
||
{
|
||
ImGui::CloseCurrentPopup();
|
||
}
|
||
ImGui::EndPopup();
|
||
}
|
||
|
||
if (m_QuitConfirmed)
|
||
{
|
||
m_QuitConfirmed = false;
|
||
m_Ctx.app->Close();
|
||
}
|
||
}
|
||
|
||
void Editor::DrawSceneModals()
|
||
{
|
||
// 保存/加载共用一个路径输入(UE 同款:默认路径记住上次输入)
|
||
if (m_ShowSaveModal)
|
||
{
|
||
ImGui::OpenPopup("Save Scene");
|
||
m_ShowSaveModal = false;
|
||
}
|
||
if (m_ShowLoadModal)
|
||
{
|
||
ImGui::OpenPopup("Load Scene");
|
||
m_ShowLoadModal = false;
|
||
}
|
||
|
||
ImGui::SetNextWindowPos(
|
||
ImGui::GetMainViewport()->GetCenter(),
|
||
ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
|
||
|
||
if (ImGui::BeginPopupModal("Save Scene", nullptr,
|
||
ImGuiWindowFlags_AlwaysAutoResize))
|
||
{
|
||
ImGui::TextUnformatted("Scene file path (relative to working dir):");
|
||
ImGui::SetNextItemWidth(400.0f);
|
||
ImGui::InputText("##SavePath", m_ScenePathBuf, sizeof(m_ScenePathBuf));
|
||
ImGui::Separator();
|
||
if (ImGui::Button("Save", ImVec2(120, 0)))
|
||
{
|
||
DoSaveScene();
|
||
ImGui::CloseCurrentPopup();
|
||
}
|
||
ImGui::SameLine();
|
||
if (ImGui::Button("Cancel", ImVec2(120, 0)))
|
||
{
|
||
ImGui::CloseCurrentPopup();
|
||
}
|
||
ImGui::EndPopup();
|
||
}
|
||
|
||
if (ImGui::BeginPopupModal("Load Scene", nullptr,
|
||
ImGuiWindowFlags_AlwaysAutoResize))
|
||
{
|
||
ImGui::TextUnformatted("Scene file path (relative to working dir):");
|
||
ImGui::SetNextItemWidth(400.0f);
|
||
ImGui::InputText("##LoadPath", m_ScenePathBuf, sizeof(m_ScenePathBuf));
|
||
ImGui::Separator();
|
||
if (ImGui::Button("Load", ImVec2(120, 0)))
|
||
{
|
||
DoLoadScene();
|
||
ImGui::CloseCurrentPopup();
|
||
}
|
||
ImGui::SameLine();
|
||
if (ImGui::Button("Cancel", ImVec2(120, 0)))
|
||
{
|
||
ImGui::CloseCurrentPopup();
|
||
}
|
||
ImGui::EndPopup();
|
||
}
|
||
}
|
||
|
||
void Editor::DoSaveScene()
|
||
{
|
||
const std::string path = m_ScenePathBuf;
|
||
if (path.empty() || !m_Ctx.scene || !m_Ctx.assets)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// scenes/ 目录不存在时自动创建(空父目录/已存在均合法)
|
||
std::error_code ec;
|
||
std::filesystem::create_directories(
|
||
std::filesystem::path(path).parent_path(), ec);
|
||
|
||
if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets,
|
||
path))
|
||
{
|
||
ENGINE_INFO("Editor: scene saved to '{}'", path);
|
||
}
|
||
else
|
||
{
|
||
ENGINE_ERROR("Editor: failed to save scene to '{}'", path);
|
||
}
|
||
}
|
||
|
||
void Editor::DoLoadScene()
|
||
{
|
||
const std::string path = m_ScenePathBuf;
|
||
if (path.empty() || !m_Ctx.scene || !m_Ctx.assets)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene,
|
||
*m_Ctx.assets, path))
|
||
{
|
||
ENGINE_ERROR("Editor: failed to load scene '{}'", path);
|
||
return;
|
||
}
|
||
ENGINE_INFO("Editor: scene loaded from '{}'", path);
|
||
|
||
// 旧实体已销毁:清选中(连带清描边,见 SelectEntity)
|
||
SelectEntity(entt::null);
|
||
}
|