Files
HelloVulkan/App/editor/Editor.cpp
T
atdunbg b7d6f53591 Editor: rework default layout to UE5 arrangement
Panels renamed to UE semantics (docking identity change): Hierarchy ->
Outliner, Inspector -> Details, Asset Browser -> Content Browser.

Default DockBuilder layout now matches UE5 factory layout: Details
docked right (full height), Content Browser docked bottom spanning
left+center, Outliner top-left, Viewport center with Stats as a
side tab.

Legacy imgui.ini (old window names, no new names) is removed once at
Init so the new default rebuilds cleanly; fresh or new-style ini is
untouched.
2026-10-05 00:47:33 +08:00

513 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// 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;
std::string envPath;
if (!Serialization::SceneSerializer::DeserializeFromString(
*m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot, envPath))
{
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);
// 环境路径:引擎序列化器只认字符串,查表由编辑器侧负责
std::string envPath;
if (m_Ctx.getEnvIndex)
{
const int idx = m_Ctx.getEnvIndex();
if (idx >= 0 && idx < kEnvCount) envPath = kEnvPaths[idx];
}
if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets,
path, envPath))
{
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;
}
std::string envPath;
if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene,
*m_Ctx.assets, path,
envPath))
{
ENGINE_ERROR("Editor: failed to load scene '{}'", path);
return;
}
ENGINE_INFO("Editor: scene loaded from '{}'", path);
// 旧实体已销毁:清选中(连带清描边,见 SelectEntity)
SelectEntity(entt::null);
// 环境恢复:场景带环境且命中环境表才切换(镜像 StatsPanel 同步路径)
if (!envPath.empty() && m_Ctx.getEnvIndex && m_Ctx.setEnvIndex)
{
int found = -1;
for (int i = 0; i < kEnvCount; ++i)
{
if (envPath == kEnvPaths[i]) { found = i; break; }
}
if (found >= 0)
{
m_Ctx.setEnvIndex(found);
const auto env = m_Ctx.assets->LoadEnvironment(kEnvPaths[found]);
if (env.Valid())
{
m_Ctx.renderer->SetEnvironment(
m_Ctx.assets->GetEnvironment(env));
}
}
else
{
ENGINE_WARN("Editor: scene env '{}' not in env table, "
"keeping current", envPath);
}
}
}