Files
HelloVulkan/App/editor/Editor.cpp
T
atdunbg f327f44269 新增 GameFlow 运行时状态机:PIE 与独立运行统一生命周期
Stopped→Loading→Running↔Paused 单一状态源,编辑器 Play/Stop
与 main.cpp 独立运行共用同一套加载/恢复/暂停语义
2026-10-08 17:02:09 +08:00

582 lines
21 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 "panels/AssetBrowserPanel.h"
#include "panels/HierarchyPanel.h"
#include "panels/InspectorPanel.h"
#include "panels/MaterialEditorPanel.h"
#include "panels/ControllerEditorPanel.h"
#include "panels/ViewportPanel.h"
#include "panels/StatsPanel.h"
#include <Engine/Core/Log.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h>
#include <imgui_internal.h> // DockBuilder*(内部 API)
#include <ImGuizmo.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 拷贝后补接,面板只读;
// 权威 = GameFlow,镜像在 Draw 开头同步——面板接口保持 const bool*)
m_Ctx.playState = &m_PlayingMirror;
m_Ctx.pausedState = &m_PausedMirror;
// PIE 转换副作用挂监听(GameFlow 不知场景;快照/恢复是编辑器职责)
if (m_Ctx.flow)
{
m_Ctx.flow->AddListener(
[this](GameFlowState, GameFlowState to) {
if (to == GameFlowState::Loading) OnFlowEnterLoading();
else if (to == GameFlowState::Stopped) OnFlowEnterStopped();
// →Running/→Paused:timeScale 由 GameFlow 权威处理,
// 编辑器无额外副作用
});
}
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>();
m_MaterialEditor = std::make_unique<MaterialEditorPanel>();
m_Ctx.materialEditor = m_MaterialEditor.get();
m_ControllerEditor = std::make_unique<ControllerEditorPanel>();
m_Ctx.controllerEditor = m_ControllerEditor.get();
m_Ctx.selected = &m_Selected;
// 旧布局一次性迁移:面板改名(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)----
// →Loading 监听器:Play 半程。当前场景 → 内存 YAML 快照 → 切 gameView
//(Viewport 面板内嵌 primary 相机渲染,UE PIE 语义——编辑器 UI 保持
// 可用,不切全屏)→ 同步加载完成即 CompleteLoading(快照是同步操作,
// 不完成的 Loading 会卡死主循环的物理/脚本门控)。快照失败 → RequestStop
// 回滚(现场不动)
void Editor::OnFlowEnterLoading()
{
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer || !m_Ctx.flow) return;
m_PlaySnapshot = Serialization::SceneSerializer::SerializeToString(
*m_Ctx.scene, *m_Ctx.assets, "PlaySnapshot");
if (m_PlaySnapshot.empty())
{
ENGINE_ERROR("PIE: snapshot failed, play aborted");
m_Ctx.flow->RequestStop(); // 回滚 → 触发 →Stopped 监听器(空快照跳过恢复)
return;
}
m_Gizmo.dragging = false; // 编辑态拖杆中点 Play:杆运行态不出,复位
m_Ctx.renderer->SetGameView(true);
m_Ctx.flow->CompleteLoading(); // 同步加载完成 → Running(解冻)
ENGINE_INFO("PIE: playing (scene snapshot {} bytes)",
m_PlaySnapshot.size());
}
// →Stopped 监听器:Stop 半程。快照原子换血恢复 → 回编辑器渲染。
// 换血后所有 Entity 句柄悬空:清选中(描边同步清)、Gizmo 拖拽态
// 复位。物理 body/约束/角色由对账系统随实体重建/清理,无需干预。
// 恢复失败(快照损坏,理论罕见)也必须回编辑态,现场留给用户
// File→Load 或退出前 Save。空快照 = 快照失败回滚(未真 Play 过),跳过恢复
void Editor::OnFlowEnterStopped()
{
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return;
if (!m_PlaySnapshot.empty())
{
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_Ctx.renderer->SetGameView(false);
SelectEntity(entt::null);
m_Gizmo.dragging = false;
ENGINE_INFO("PIE: stopped, editor scene restored");
}
void Editor::TogglePlay()
{
if (!m_Ctx.flow) return;
IsPlaying() ? m_Ctx.flow->RequestStop() : m_Ctx.flow->RequestStart();
}
// PIE 暂停(UE Pause 语义):Running ↔ Paused。运行态才有意义
//(编辑器态本来就冻结,Pause 按钮不出现)
void Editor::TogglePause()
{
if (!m_Ctx.flow) return;
IsPaused() ? m_Ctx.flow->RequestResume() : m_Ctx.flow->RequestPause();
}
void Editor::Draw()
{
// PIE 镜像同步(面板只读指针消费;权威 = GameFlow,此处适配)
if (m_Ctx.flow)
{
m_PlayingMirror = m_Ctx.flow->IsIn(GameFlowState::Running) ||
m_Ctx.flow->IsIn(GameFlowState::Paused);
m_PausedMirror = m_Ctx.flow->IsIn(GameFlowState::Paused);
}
// 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();
DrawToolbar();
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);
if (m_ShowMaterialEditor) m_MaterialEditor->Draw(m_Ctx);
m_ControllerEditor->Draw(m_Ctx); // 面板内部 m_Open 门控
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;
}
if (ImGui::BeginMenu("File"))
{
// 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘,
// Load 会撕掉快照恢复链(UE 运行态同样禁)
ImGui::BeginDisabled(IsPlaying());
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::MenuItem("Material Editor", nullptr, &m_ShowMaterialEditor);
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
// 主工具栏(UE Main Toolbar 语义):菜单栏下方独立一行,Play 组水平
// 居中。编辑态 = [Play](绿);PIE 运行态 = [Pause][Stop](暂停高亮黄 /
// 停止橙红)。kDisplayDirect 全屏预览是发布形态预览,不在此处
void Editor::DrawToolbar()
{
const ImGuiIO& io = ImGui::GetIO();
// Ctrl+P 全局热键:编辑态进 PIE,运行态停回编辑器(双向)。
// 文本输入中(改名/路径框)不触发,防打字误切
if (io.KeyCtrl && !io.WantTextInput &&
ImGui::IsKeyPressed(ImGuiKey_P, false))
{
TogglePlay();
}
// 按钮加大触感(工具栏行高 = 一个按钮高;DockSpace avail 自适应)
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 4.0f));
const ImGuiStyle& st = ImGui::GetStyle();
const char* pauseLabel = IsPaused() ? "Resume" : "Pause";
// Play 组宽度 → 水平居中(编辑态 1 钮 / 运行态 2 钮)
float groupW = 0.0f;
if (IsPlaying())
{
groupW = ImGui::CalcTextSize(pauseLabel).x +
ImGui::CalcTextSize("Stop").x +
4.0f * st.FramePadding.x + st.ItemSpacing.x;
}
else
{
groupW = ImGui::CalcTextSize("Play").x + 2.0f * st.FramePadding.x;
}
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - groupW) * 0.5f);
if (IsPlaying())
{
// Pause/Resume:暂停中亮黄常驻(UE 按下态),运行中暗黄
ImGui::PushStyleColor(ImGuiCol_Button,
IsPaused() ? ImVec4(0.85f, 0.62f, 0.12f, 1.0f)
: ImVec4(0.55f, 0.42f, 0.10f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
IsPaused() ? ImVec4(0.92f, 0.70f, 0.18f, 1.0f)
: ImVec4(0.65f, 0.50f, 0.14f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.95f, 0.76f, 0.24f, 1.0f));
if (ImGui::Button(pauseLabel))
{
TogglePause();
}
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(IsPaused() ? "Resume simulation"
: "Pause simulation");
}
ImGui::SameLine();
// Stop:橙红(沿用视口 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::Button("Stop"))
{
TogglePlay();
}
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Stop and restore editor scene (Ctrl+P)");
}
}
else
{
// Play:绿(UE Play 按钮语义)
ImGui::PushStyleColor(ImGuiCol_Button,
ImVec4(0.22f, 0.48f, 0.26f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(0.28f, 0.58f, 0.32f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.34f, 0.66f, 0.38f, 1.0f));
if (ImGui::Button("Play"))
{
TogglePlay();
}
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip("Play in editor (Ctrl+P)");
}
}
ImGui::PopStyleVar();
}
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::DockBuilderDockWindow("Material Editor", dockRight); // Details 旁 tab
ImGui::DockBuilderDockWindow("Controller Editor", dockRight); // Details 旁 tab
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);
}