From f327f4426918fd48d1252a1ef35100231b72ba9d Mon Sep 17 00:00:00 2001 From: atdunbg Date: Thu, 8 Oct 2026 17:02:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20GameFlow=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E6=97=B6=E7=8A=B6=E6=80=81=E6=9C=BA=EF=BC=9APIE=20?= =?UTF-8?q?=E4=B8=8E=E7=8B=AC=E7=AB=8B=E8=BF=90=E8=A1=8C=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stopped→Loading→Running↔Paused 单一状态源,编辑器 Play/Stop 与 main.cpp 独立运行共用同一套加载/恢复/暂停语义 --- App/editor/Editor.cpp | 91 +++++++++++++--------- App/editor/Editor.h | 43 +++++++---- App/main.cpp | 47 ++++++++++-- Engine/include/Engine/Core/Application.h | 5 ++ Engine/include/Engine/Core/GameFlow.h | 71 +++++++++++++++++ Engine/src/Core/Application.cpp | 37 +++++++-- Engine/src/Core/GameFlow.cpp | 97 ++++++++++++++++++++++++ 7 files changed, 329 insertions(+), 62 deletions(-) create mode 100644 Engine/include/Engine/Core/GameFlow.h create mode 100644 Engine/src/Core/GameFlow.cpp diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp index 5004251..8e26236 100644 --- a/App/editor/Editor.cpp +++ b/App/editor/Editor.cpp @@ -21,7 +21,6 @@ #include "panels/StatsPanel.h" #include -#include #include #include @@ -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(); m_Inspector = std::make_unique(); @@ -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"); } diff --git a/App/editor/Editor.h b/App/editor/Editor.h index 76e1682..cc9623d 100644 --- a/App/editor/Editor.h +++ b/App/editor/Editor.h @@ -17,7 +17,8 @@ #include "EditorContext.h" #include "panels/ViewportPanel.h" // ViewportInputState(成员按值持有) -#include // u32(SelectEntity 位模式转换) +#include // u32(SelectEntity 位模式转换) +#include // PIE 状态权威(IsPlaying 直查) #include @@ -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; }; diff --git a/App/main.cpp b/App/main.cpp index 15c894a..0235249 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -23,9 +23,12 @@ #include #include #include +#include #include +#include + // editor/game 编译期分离(HELLOVULKAN_EDITOR CMake option):editor 形态 // 挂接 docking Editor;game 形态 = Direct 直渲 + primary 相机,零编辑器代码 #ifdef HELLO_EDITOR @@ -66,7 +69,9 @@ namespace InitEditor(); #else // game 形态:全窗口 Direct 直渲(Renderer 默认即 kDisplayDirect), - // 相机由场景 primary camera 提供,无 UI + // 相机由场景 primary camera 提供,无 UI。 + // 游戏流:Stopped → Loading(OnUpdate 泵场景加载)→ Running + GetGameFlow().RequestStart(); #endif } @@ -84,7 +89,31 @@ namespace GetRenderer().GetEditorCamera().Update(frameTime.UnscaledDt()); } #else - (void)frameTime; // game 形态:本沙盒无游戏逻辑 + // Loading 泵(同步示例):场景文件存在则加载,不存在 = + // 空场景照常进 Running(引擎冒烟路径)。加载完成后必须 + // CompleteLoading,否则物理/脚本门控永远跳过(卡死 Loading) + if (GetGameFlow().IsIn(GameFlowState::Loading)) + { + if (std::filesystem::exists(m_ScenePath)) + { + if (Serialization::SceneSerializer::Deserialize( + GetScene(), GetAssets(), m_ScenePath)) + { + ENGINE_INFO("Game: scene loaded from {}", m_ScenePath); + } + else + { + ENGINE_ERROR("Game: scene load failed: {}", + m_ScenePath); + } + } + else + { + ENGINE_WARN("Game: scene file not found ({}), " + "starting empty", m_ScenePath); + } + GetGameFlow().CompleteLoading(); + } #endif } @@ -99,16 +128,14 @@ namespace // ---- 事件 ---- +#ifdef HELLO_EDITOR void OnWindowClose(const Event& event) override { -#ifdef HELLO_EDITOR // 退出确认走 Editor 的模态框(ImGui 必须可见才有 UI) SetImGuiEnabled(true); m_Editor->RequestQuit(); -#else - (void)event; -#endif } +#endif void OnKeyPress(const Event& event) override { @@ -141,9 +168,10 @@ namespace ctx.scene = &GetScene(); ctx.assets = &GetAssets(); ctx.app = this; + ctx.flow = &GetGameFlow(); ctx.editorCamera = &GetRenderer().GetEditorCamera(); - // 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复) - Time::SetTimeScale(0.0f); + // 编辑器态默认冻结:GameFlow 构造即 timeScale=0(Stopped 语义), + // timeScale 权威在 GameFlow,此处不再手设 m_Editor->Init(ctx); // 输入归属:覆盖 EnableImGui 的全局 WantCapture* 谓词 @@ -158,6 +186,9 @@ namespace private: std::unique_ptr m_Editor = std::make_unique(); +#else + // game 形态:启动场景路径(CWD = bin;编辑器 Save 的产物位置) + const char* m_ScenePath = "assets/scenes/sandbox.scene"; #endif }; } diff --git a/Engine/include/Engine/Core/Application.h b/Engine/include/Engine/Core/Application.h index e4b3327..dde01ea 100644 --- a/Engine/include/Engine/Core/Application.h +++ b/Engine/include/Engine/Core/Application.h @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -75,6 +76,7 @@ namespace Engine { Assets::AssetManager& GetAssets() const { return *m_Assets; } PhysicsWorld& GetPhysics() const { return *m_Physics; } Scripting::ScriptRuntime& GetScripts() const { return *m_Scripts; } + GameFlow& GetGameFlow() { return m_GameFlow; } u64 GetFrameCount() const { return m_FrameCount; } // ImGui相关 @@ -127,6 +129,9 @@ namespace Engine { // 其后端销毁需要 Device 与 GLFWwindow 存活) Scope m_ImGuiLayer; + // 游戏流状态机(构造即冻结:Stopped → timeScale=0) + GameFlow m_GameFlow; + bool m_Running = false; u64 m_FrameCount = 0; diff --git a/Engine/include/Engine/Core/GameFlow.h b/Engine/include/Engine/Core/GameFlow.h new file mode 100644 index 0000000..6b1b40f --- /dev/null +++ b/Engine/include/Engine/Core/GameFlow.h @@ -0,0 +1,71 @@ +// +// GameFlow.h:游戏流状态机(引擎层唯一的"游戏在干什么"权威)。 +// +// 状态:Stopped(编辑态/未启动)→ Loading(加载中)→ Running ⇄ Paused, +// 任意活动态可 RequestStop 回 Stopped。转换矩阵外的请求拒绝 + warn。 +// +// 职责边界: +// - timeScale 单一权威:每次转换统一设 Time::SetTimeScale +// (Running/Loading→1,Paused/Stopped→0)。上层(编辑器/游戏) +// 不再自行碰 timeScale——冻结语义由状态推导,不再散落 +// - 状态转换广播:AddListener 注册监听(组合根/编辑器 Init 期注册, +// 非每帧路径)。编辑器在 →Loading 挂场景快照、→Stopped 挂恢复; +// 脚本/系统将来同样可订阅(跨系统解耦) +// - 不知场景不知资产:加载本身(读文件/反序列化)是调用方的事, +// 本类只管状态与转换纪律(CompleteLoading 由加载方显式调) +// +// 与主循环的契约(Application::Run): +// - Input::Poll 仅 Running 激活(Paused/Stopped 动作静默) +// - 物理步进仅 Loading 跳过(编辑态对账照跑,停摆靠 dt=0) +// - 脚本仅 Running 执行(Unity Play Mode / UE PIE 语义——脚本副 +// 作用不依赖 dt,编辑态不得运行);→Stopped 由监听 EndPlay 终 +// 局脚本实例(组合根注册,先于编辑器回滚监听) +// + +#ifndef HELLOVULKAN_GAMEFLOW_H +#define HELLOVULKAN_GAMEFLOW_H + +#include "Engine/Core/Types.h" + +#include +#include + +namespace Engine { + + enum class GameFlowState : u8 { + Stopped, // 编辑态 / 未启动(timeScale=0) + Loading, // 加载中(物理/脚本步进跳过;timeScale=1 无消费方) + Running, // 游戏运行(timeScale=1) + Paused, // 暂停(timeScale=0;编辑器 Pause / 游戏菜单挂起) + }; + + class GameFlow { + public: + // (from, to);注册期 = 组合根/编辑器 Init,非每帧路径 + using Listener = std::function; + + GameFlow(); // 构造即冻结(Stopped 语义:timeScale=0) + + void AddListener(Listener l) { m_Listeners.push_back(std::move(l)); } + + GameFlowState State() const { return m_State; } + bool IsIn(GameFlowState s) const { return m_State == s; } + + // ---- 转换请求(矩阵外拒绝 + warn,返回是否生效)---- + bool RequestStart(); // Stopped → Loading + void CompleteLoading(); // Loading → Running(加载方显式调) + bool RequestPause(); // Running → Paused + bool RequestResume(); // Paused → Running + bool RequestStop(); // Loading/Running/Paused → Stopped + + private: + // 立即生效转换:设 timeScale + 广播(同既有 DoPlay 同步语义) + void Transition(GameFlowState to); + + GameFlowState m_State = GameFlowState::Stopped; + std::vector m_Listeners; + }; + +} // namespace Engine + +#endif //HELLOVULKAN_GAMEFLOW_H diff --git a/Engine/src/Core/Application.cpp b/Engine/src/Core/Application.cpp index 4f51e7b..5480078 100644 --- a/Engine/src/Core/Application.cpp +++ b/Engine/src/Core/Application.cpp @@ -44,6 +44,17 @@ namespace Engine { // 统一输入门面装配:脚本/游戏逻辑经 Input 查询键鼠,不直接 // 依赖 Window 后端(GLFW 解耦) Input::Init(*m_Window); + + // GameFlow →Stopped:脚本终局(Unity 退出 Play 语义——实例与 + // 成员状态不跨局存活,下次 Play 由对账重建)。注册在组合根最 + // 早处:先于编辑器 Init 的监听执行,EndPlay 跑在场景回滚之前, + // OnDestroy 见到的仍是上局实体。m_Scripts 在 Run 里才创建,而 + // →Stopped 转换最早发生于 PIE Stop(届时已存在),空指针兜底 + m_GameFlow.AddListener([this](GameFlowState, GameFlowState to) { + if (to == GameFlowState::Stopped && m_Scripts) { + m_Scripts->EndPlay(); + } + }); } Application::~Application() @@ -132,15 +143,29 @@ namespace Engine { const FrameTime& frame = Time::Current(); m_LastFrameTime = now; + // 输入动作层采样(单点,整帧一致):仅 Running 激活—— + // Paused/Stopped(编辑器态)动作静默清零,编辑器相机继续走 + // 原始 IsKeyDown 通道(自带 UI 捕获门控,两套互不干扰) + Input::Poll(m_GameFlow.IsIn(GameFlowState::Running)); + // 物理步进(OnUpdate 之前,Unity FixedUpdate→Update 语义): // 动态体姿态写回 Transform 并标脏,UpdateTransforms 统一重算; - // 吃 scaled dt → timeScale=0(编辑器暂停)时 accumulator 冻结, - // SyncScene 对账仍跑(拖静态体照常同步) - m_Physics->Step(frame.Dt()); + // 吃 scaled dt → timeScale=0(Paused/Stopped 冻结)时 + // accumulator 冻结,SyncScene 对账仍跑(拖静态体照常同步)。 + // Loading 态整段跳过(加载中不步进) + if (!m_GameFlow.IsIn(GameFlowState::Loading)) { + m_Physics->Step(frame.Dt()); - // 脚本更新(物理之后、业务 OnUpdate 之前,Unity FixedUpdate→Update - // 语义;吃 scaled dt——timeScale=0 暂停时 dt=0 脚本自然停摆) - m_Scripts->Update(frame.Dt()); + // 脚本只在 Running 执行(Unity Play Mode / UE PIE 语义): + // 编辑态/暂停态 gameplay 脚本一律不跑——脚本副作用不依赖 + // dt(读输入/画调试图元/翻转动画状态),"dt=0 自然停摆" + // 不成立,曾致编辑态按 WASD 就画箭头+翻动画状态。实例 + // 生命周期 = 一次 PIE 局:→Stopped 由 GameFlow 监听 + // EndPlay 统一销毁 + if (m_GameFlow.IsIn(GameFlowState::Running)) { + m_Scripts->Update(frame.Dt()); + } + } OnUpdate(frame); diff --git a/Engine/src/Core/GameFlow.cpp b/Engine/src/Core/GameFlow.cpp new file mode 100644 index 0000000..d7b3977 --- /dev/null +++ b/Engine/src/Core/GameFlow.cpp @@ -0,0 +1,97 @@ +// +// GameFlow.cpp:游戏流状态机实现。 +// + +#include "Engine/Core/GameFlow.h" +#include "Engine/Core/Log.h" +#include "Engine/Core/Time.h" + +namespace Engine { + + namespace { + const char* StateName(GameFlowState s) + { + switch (s) { + case GameFlowState::Stopped: return "Stopped"; + case GameFlowState::Loading: return "Loading"; + case GameFlowState::Running: return "Running"; + case GameFlowState::Paused: return "Paused"; + } + return "?"; + } + } // anonymous namespace + + GameFlow::GameFlow() + { + // Stopped 语义即冻结:替代各处散落的初始 SetTimeScale(0) + Time::SetTimeScale(0.0f); + } + + bool GameFlow::RequestStart() + { + if (m_State != GameFlowState::Stopped) { + ENGINE_WARN("GameFlow: RequestStart rejected in {}", + StateName(m_State)); + return false; + } + Transition(GameFlowState::Loading); + return true; + } + + void GameFlow::CompleteLoading() + { + if (m_State != GameFlowState::Loading) { + ENGINE_WARN("GameFlow: CompleteLoading rejected in {}", + StateName(m_State)); + return; + } + Transition(GameFlowState::Running); + } + + bool GameFlow::RequestPause() + { + if (m_State != GameFlowState::Running) { + ENGINE_WARN("GameFlow: RequestPause rejected in {}", + StateName(m_State)); + return false; + } + Transition(GameFlowState::Paused); + return true; + } + + bool GameFlow::RequestResume() + { + if (m_State != GameFlowState::Paused) { + ENGINE_WARN("GameFlow: RequestResume rejected in {}", + StateName(m_State)); + return false; + } + Transition(GameFlowState::Running); + return true; + } + + bool GameFlow::RequestStop() + { + if (m_State == GameFlowState::Stopped) { + ENGINE_WARN("GameFlow: RequestStop rejected in Stopped"); + return false; + } + Transition(GameFlowState::Stopped); + return true; + } + + void GameFlow::Transition(GameFlowState to) + { + const GameFlowState from = m_State; + m_State = to; + + // timeScale 单一权威:Running/Loading 解冻,Paused/Stopped 冻结 + Time::SetTimeScale( + to == GameFlowState::Running || to == GameFlowState::Loading + ? 1.0f : 0.0f); + + ENGINE_INFO("GameFlow: {} -> {}", StateName(from), StateName(to)); + for (const Listener& l : m_Listeners) l(from, to); + } + +} // namespace Engine