新增 GameFlow 运行时状态机:PIE 与独立运行统一生命周期

Stopped→Loading→Running↔Paused 单一状态源,编辑器 Play/Stop
与 main.cpp 独立运行共用同一套加载/恢复/暂停语义
This commit is contained in:
2026-10-08 17:02:09 +08:00
parent f73e52926f
commit f327f44269
7 changed files with 329 additions and 62 deletions
+56 -35
View File
@@ -21,7 +21,6 @@
#include "panels/StatsPanel.h"
#include <Engine/Core/Log.h>
#include <Engine/Core/Time.h>
#include <Engine/Serialization/SceneSerializer.h>
#include <imgui.h>
@@ -40,9 +39,22 @@ Editor::~Editor() = default;
void Editor::Init(const EditorContext& ctx)
{
m_Ctx = ctx;
// PIE 运行/暂停态指针:指向本对象成员(ctx 拷贝后补接,面板只读)
m_Ctx.playState = &m_Playing;
m_Ctx.pausedState = &m_Paused;
// 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>();
@@ -84,52 +96,52 @@ void Editor::Init(const EditorContext& ctx)
// ---- PIE(Play-In-Editor)----
// Play:当前场景 → 内存 YAML 快照 → 切 gameView(Viewport 面板内嵌
// primary 相机渲染,UE PIE 语义——编辑器 UI 保持可用,不切全屏;
// kDisplayDirect 全屏预览是后期发布形态,手动经 Stats 切换)
// → 解冻时间(物理/脚本/动画开始跑)。快照失败不进 Play(现场不动)
void Editor::DoPlay()
// →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) return;
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_Playing = true;
m_Paused = false;
m_Gizmo.dragging = false; // 编辑态拖杆中点 Play:杆运行态不出,复位
m_Ctx.renderer->SetGameView(true);
Time::SetTimeScale(1.0f);
m_Ctx.flow->CompleteLoading(); // 同步加载完成 → Running(解冻)
ENGINE_INFO("PIE: playing (scene snapshot {} bytes)",
m_PlaySnapshot.size());
}
// Stop:快照原子换血恢复 → 回编辑器渲染 → 冻结时间。
// →Stopped 监听器:Stop 半程。快照原子换血恢复 → 回编辑器渲染。
// 换血后所有 Entity 句柄悬空:清选中(描边同步清)、Gizmo 拖拽态
// 复位。物理 body/约束/角色由对账系统随实体重建/清理,无需干预。
// 恢复失败(快照损坏,理论罕见)也必须回编辑态,现场留给用户
// File→Load 或退出前 Save
void Editor::DoStop()
// File→Load 或退出前 Save。空快照 = 快照失败回滚(未真 Play 过),跳过恢复
void Editor::OnFlowEnterStopped()
{
if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return;
if (!Serialization::SceneSerializer::DeserializeFromString(
*m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot))
if (!m_PlaySnapshot.empty())
{
ENGINE_ERROR("PIE: snapshot restore failed (scene is now the "
"played-out state)");
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_PlaySnapshot.clear();
m_Playing = false;
m_Paused = false;
m_Ctx.renderer->SetGameView(false);
Time::SetTimeScale(0.0f);
SelectEntity(entt::null);
m_Gizmo.dragging = false;
ENGINE_INFO("PIE: stopped, editor scene restored");
@@ -137,19 +149,28 @@ void Editor::DoStop()
void Editor::TogglePlay()
{
m_Playing ? DoStop() : DoPlay();
if (!m_Ctx.flow) return;
IsPlaying() ? m_Ctx.flow->RequestStop() : m_Ctx.flow->RequestStart();
}
// PIE 暂停(UE Pause 语义):timeScale 0 ↔ 1。运行态才有意义
// PIE 暂停(UE Pause 语义):Running ↔ Paused。运行态才有意义
//(编辑器态本来就冻结,Pause 按钮不出现)
void Editor::TogglePause()
{
m_Paused = !m_Paused;
Time::SetTimeScale(m_Paused ? 0.0f : 1.0f);
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 → 杆可见但永远抓不住(只能看不能拖)
@@ -253,7 +274,7 @@ void Editor::DrawMenuBar()
{
// 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘,
// Load 会撕掉快照恢复链(UE 运行态同样禁)
ImGui::BeginDisabled(m_Playing);
ImGui::BeginDisabled(IsPlaying());
if (ImGui::MenuItem("Save Scene...", nullptr))
{
m_ShowSaveModal = true;
@@ -302,11 +323,11 @@ void Editor::DrawToolbar()
// 按钮加大触感(工具栏行高 = 一个按钮高;DockSpace avail 自适应)
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 4.0f));
const ImGuiStyle& st = ImGui::GetStyle();
const char* pauseLabel = m_Paused ? "Resume" : "Pause";
const char* pauseLabel = IsPaused() ? "Resume" : "Pause";
// Play 组宽度 → 水平居中(编辑态 1 钮 / 运行态 2 钮)
float groupW = 0.0f;
if (m_Playing)
if (IsPlaying())
{
groupW = ImGui::CalcTextSize(pauseLabel).x +
ImGui::CalcTextSize("Stop").x +
@@ -318,14 +339,14 @@ void Editor::DrawToolbar()
}
ImGui::SetCursorPosX((ImGui::GetWindowWidth() - groupW) * 0.5f);
if (m_Playing)
if (IsPlaying())
{
// Pause/Resume:暂停中亮黄常驻(UE 按下态),运行中暗黄
ImGui::PushStyleColor(ImGuiCol_Button,
m_Paused ? ImVec4(0.85f, 0.62f, 0.12f, 1.0f)
IsPaused() ? ImVec4(0.85f, 0.62f, 0.12f, 1.0f)
: ImVec4(0.55f, 0.42f, 0.10f, 1.0f));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
m_Paused ? ImVec4(0.92f, 0.70f, 0.18f, 1.0f)
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));
@@ -336,7 +357,7 @@ void Editor::DrawToolbar()
ImGui::PopStyleColor(3);
if (ImGui::IsItemHovered())
{
ImGui::SetTooltip(m_Paused ? "Resume simulation"
ImGui::SetTooltip(IsPaused() ? "Resume simulation"
: "Pause simulation");
}
+30 -13
View File
@@ -17,7 +17,8 @@
#include "EditorContext.h"
#include "panels/ViewportPanel.h" // ViewportInputState(成员按值持有)
#include <Engine/Core/Types.h> // u32(SelectEntity 位模式转换)
#include <Engine/Core/Types.h> // u32(SelectEntity 位模式转换)
#include <Engine/Core/GameFlow.h> // PIE 状态权威(IsPlaying 直查)
#include <entt/entt.hpp>
@@ -60,19 +61,35 @@ public:
void RequestQuit() { m_ShowQuitConfirm = true; }
// ---- PIE(Play-In-Editor,UE 主工具栏语义)----
bool IsPlaying() const { return m_Playing; }
bool IsPaused() const { return m_Paused; }
// Play:内存快照当前场景 → 切 gameView(Viewport 内嵌 primary 相机
// 渲染,UE PIE 语义;不切全屏)→ 解冻时间;
// Stop:快照原子换血恢复 → 回编辑器渲染 → 冻结时间。
// 编辑器态默认 timeScale=0(物理冻结 = UE 语义),Play 解冻
// 状态权威 = GameFlow(EditorContext::flow);编辑器只是转换的
// 发起者(Request*)与监听器(快照/恢复挂在 →Loading/→Stopped)
bool IsPlaying() const
{
return m_Ctx.flow &&
(m_Ctx.flow->IsIn(Engine::GameFlowState::Running) ||
m_Ctx.flow->IsIn(Engine::GameFlowState::Paused));
}
bool IsPaused() const
{
return m_Ctx.flow &&
m_Ctx.flow->IsIn(Engine::GameFlowState::Paused);
}
// Play:RequestStart → →Loading 监听器做快照 + 切 gameView(Viewport
// 内嵌 primary 相机渲染,UE PIE 语义;不切全屏)→ 同步加载完成
// CompleteLoading → Running 解冻;Stop:RequestStop → →Stopped 监听器
// 快照原子换血恢复 → 回编辑器渲染。编辑器态默认冻结(GameFlow 构造
// 即 timeScale=0),Play 解冻由 GameFlow 权威处理
void TogglePlay();
// PIE 暂停切换(UE Pause 语义):timeScale 0 ↔ 1
// PIE 暂停切换(UE Pause 语义):Running ↔ Paused(timeScale 由
// GameFlow 权威处理)。运行态才有意义(编辑器态本来就冻结,
// Pause 按钮不出现)
void TogglePause();
private:
void DoPlay(); // TogglePlay 的 Play 半程(快照 + 切渲染 + 解冻)
void DoStop(); // TogglePlay 的 Stop 半程(恢复 + 回编辑器 + 冻结)
// GameFlow 监听器(Init 注册):编辑器快照/恢复挂在此处——
// GameFlow 不知场景,转换的副作用由消费端自己挂
void OnFlowEnterLoading(); // →Loading:快照 + 切 gameView + 同步完成加载
void OnFlowEnterStopped(); // →Stopped:快照换血恢复 + 回编辑器渲染
// ---- 选中状态 ----
// 同步描边:Renderer 落帧后 SelectionMask/Outline 只画选中实体。
@@ -135,9 +152,9 @@ private:
bool m_ShowLoadModal = false;
char m_ScenePathBuf[256] = "scenes/sandbox.scene";
// PIE 状态:Play 时场景 YAML 内存快照,Stop 时原子换血恢复
bool m_Playing = false;
bool m_Paused = false; // PIE 暂停(timeScale=0;Stop 时复位)
// PIE 镜像态(面板只读指针消费;权威 = GameFlow,Draw 开头同步)
bool m_PlayingMirror = false;
bool m_PausedMirror = false;
std::string m_PlaySnapshot;
};