feat: add runtime play tick and play-mode scene editing semantics
This commit is contained in:
35
docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md
Normal file
35
docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Unity式 Tick 系统与 Play Mode 运行时方案阶段进展
|
||||
|
||||
日期:2026-04-02
|
||||
|
||||
## 已完成
|
||||
|
||||
### 阶段 A
|
||||
|
||||
- 已接入 `RuntimeLoop`,统一承载 `FixedUpdate / Update / LateUpdate`
|
||||
- 已接入 `PlaySessionController`
|
||||
- 已实现 `Play / Stop`
|
||||
- Play 时运行 runtime scene clone
|
||||
- Stop 时恢复 editor scene snapshot
|
||||
- `Run` 菜单与 `F5` 已可切换 `Play / Stop`
|
||||
|
||||
### 阶段 B 当前收口
|
||||
|
||||
- 明确区分“文档级编辑”和“运行态场景对象编辑”
|
||||
- `New/Open/Save Scene` 与 `New/Open/Save Project` 仍只允许在 `Edit` 下执行
|
||||
- `Play / Paused` 下允许对 runtime scene 进行对象级编辑与 `Undo / Redo`
|
||||
- runtime scene 的对象改动默认不再污染场景文档 dirty 状态
|
||||
|
||||
## 当前语义
|
||||
|
||||
- `editor tick` 负责托管运行时会话
|
||||
- `engine tick` 负责推进 runtime world
|
||||
- Play 时 `Hierarchy / Inspector / SceneView / GameView` 面对的是同一份 runtime world
|
||||
- Play 中对对象的改动默认是临时运行态改动,Stop 后回滚
|
||||
- Play 中禁止的是文档切换与文档保存,不是禁止观察或编辑 runtime clone
|
||||
|
||||
## 下一阶段建议
|
||||
|
||||
- 补全 `Pause / Resume / Step` 的完整状态机
|
||||
- 明确 `Paused` 下的 `Undo / Redo / Gizmo / Inspector` 交互语义
|
||||
- 将 `Error Pause` 完整并入正式状态机
|
||||
1054
docs/plan/Unity式Tick系统与PlayMode运行时方案.md
Normal file
1054
docs/plan/Unity式Tick系统与PlayMode运行时方案.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,7 @@ add_executable(${PROJECT_NAME} WIN32
|
||||
src/Application.cpp
|
||||
src/Theme.cpp
|
||||
src/Core/UndoManager.cpp
|
||||
src/Core/PlaySessionController.cpp
|
||||
src/ComponentEditors/ComponentEditorRegistry.cpp
|
||||
src/Managers/SceneManager.cpp
|
||||
src/Managers/ProjectManager.cpp
|
||||
@@ -78,7 +79,9 @@ add_executable(${PROJECT_NAME} WIN32
|
||||
src/Viewport/SceneViewportRotateGizmo.cpp
|
||||
src/Viewport/SceneViewportScaleGizmo.cpp
|
||||
src/Viewport/SceneViewportOrientationGizmo.cpp
|
||||
src/Viewport/SceneViewportOverlayBuilder.cpp
|
||||
src/Viewport/SceneViewportOverlayRenderer.cpp
|
||||
src/Viewport/Passes/SceneViewportEditorOverlayPass.cpp
|
||||
src/panels/GameViewPanel.cpp
|
||||
src/panels/InspectorPanel.cpp
|
||||
src/panels/ConsolePanel.cpp
|
||||
|
||||
@@ -16,48 +16,60 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Actions {
|
||||
|
||||
inline ActionBinding MakeNewProjectAction() {
|
||||
return MakeAction("New Project...");
|
||||
inline ActionBinding MakeNewProjectAction(bool enabled = true) {
|
||||
return MakeAction("New Project...", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeOpenProjectAction() {
|
||||
return MakeAction("Open Project...");
|
||||
inline ActionBinding MakeOpenProjectAction(bool enabled = true) {
|
||||
return MakeAction("Open Project...", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeSaveProjectAction() {
|
||||
return MakeAction("Save Project");
|
||||
inline ActionBinding MakeSaveProjectAction(bool enabled = true) {
|
||||
return MakeAction("Save Project", nullptr, false, enabled);
|
||||
}
|
||||
|
||||
inline ActionBinding MakeNewSceneAction() {
|
||||
return MakeAction("New Scene", "Ctrl+N", false, true, true, Shortcut(ImGuiKey_N, true));
|
||||
inline ActionBinding MakeNewSceneAction(bool enabled = true) {
|
||||
return MakeAction("New Scene", "Ctrl+N", false, enabled, true, Shortcut(ImGuiKey_N, true));
|
||||
}
|
||||
|
||||
inline ActionBinding MakeOpenSceneAction() {
|
||||
return MakeAction("Open Scene", "Ctrl+O", false, true, true, Shortcut(ImGuiKey_O, true));
|
||||
inline ActionBinding MakeOpenSceneAction(bool enabled = true) {
|
||||
return MakeAction("Open Scene", "Ctrl+O", false, enabled, true, Shortcut(ImGuiKey_O, true));
|
||||
}
|
||||
|
||||
inline ActionBinding MakeSaveSceneAction() {
|
||||
return MakeAction("Save Scene", "Ctrl+S", false, true, true, Shortcut(ImGuiKey_S, true));
|
||||
inline ActionBinding MakeSaveSceneAction(bool enabled = true) {
|
||||
return MakeAction("Save Scene", "Ctrl+S", false, enabled, true, Shortcut(ImGuiKey_S, true));
|
||||
}
|
||||
|
||||
inline ActionBinding MakeSaveSceneAsAction() {
|
||||
return MakeAction("Save Scene As...", "Ctrl+Shift+S", false, true, true, Shortcut(ImGuiKey_S, true, true));
|
||||
inline ActionBinding MakeSaveSceneAsAction(bool enabled = true) {
|
||||
return MakeAction(
|
||||
"Save Scene As...",
|
||||
"Ctrl+Shift+S",
|
||||
false,
|
||||
enabled,
|
||||
true,
|
||||
Shortcut(ImGuiKey_S, true, true));
|
||||
}
|
||||
|
||||
inline ActionBinding MakeUndoAction(IEditorContext& context) {
|
||||
auto& undoManager = context.GetUndoManager();
|
||||
const bool enabled =
|
||||
IsEditorSceneUndoRedoAllowed(context.GetRuntimeMode()) &&
|
||||
undoManager.CanUndo();
|
||||
const std::string label = undoManager.CanUndo() ? "Undo " + undoManager.GetUndoLabel() : "Undo";
|
||||
return MakeAction(label, "Ctrl+Z", false, undoManager.CanUndo(), false, Shortcut(ImGuiKey_Z, true));
|
||||
return MakeAction(label, "Ctrl+Z", false, enabled, false, Shortcut(ImGuiKey_Z, true));
|
||||
}
|
||||
|
||||
inline ActionBinding MakeRedoAction(IEditorContext& context) {
|
||||
auto& undoManager = context.GetUndoManager();
|
||||
const bool enabled =
|
||||
IsEditorSceneUndoRedoAllowed(context.GetRuntimeMode()) &&
|
||||
undoManager.CanRedo();
|
||||
const std::string label = undoManager.CanRedo() ? "Redo " + undoManager.GetRedoLabel() : "Redo";
|
||||
return MakeAction(
|
||||
label,
|
||||
"Ctrl+Y",
|
||||
false,
|
||||
undoManager.CanRedo(),
|
||||
enabled,
|
||||
false,
|
||||
Shortcut(ImGuiKey_Y, true),
|
||||
Shortcut(ImGuiKey_Z, true, true));
|
||||
@@ -125,10 +137,22 @@ inline ActionBinding MakeCreateSphereEntityAction() {
|
||||
return MakeAction("Sphere");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeCreateCapsuleEntityAction() {
|
||||
return MakeAction("Capsule");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeCreateCylinderEntityAction() {
|
||||
return MakeAction("Cylinder");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeCreatePlaneEntityAction() {
|
||||
return MakeAction("Plane");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeCreateQuadEntityAction() {
|
||||
return MakeAction("Quad");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeResetLayoutAction() {
|
||||
return MakeAction("Reset Layout");
|
||||
}
|
||||
@@ -141,6 +165,17 @@ inline ActionBinding MakeExitAction() {
|
||||
return MakeAction("Exit", "Alt+F4");
|
||||
}
|
||||
|
||||
inline ActionBinding MakeTogglePlayModeAction(EditorRuntimeMode mode, bool enabled = true) {
|
||||
const bool active = IsEditorRuntimeActive(mode);
|
||||
return MakeAction(
|
||||
active ? "Stop" : "Play",
|
||||
"F5",
|
||||
active,
|
||||
enabled,
|
||||
false,
|
||||
Shortcut(ImGuiKey_F5));
|
||||
}
|
||||
|
||||
inline ActionBinding MakeNavigateBackAction(bool enabled) {
|
||||
return MakeAction("<", "Alt+Left", false, enabled, false, Shortcut(ImGuiKey_LeftArrow, false, false, true));
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Actions {
|
||||
|
||||
inline bool IsDocumentEditingAllowed(const IEditorContext& context) {
|
||||
return IsEditorDocumentEditingAllowed(context.GetRuntimeMode());
|
||||
}
|
||||
|
||||
inline void ExecuteNewScene(IEditorContext& context) {
|
||||
Commands::NewScene(context);
|
||||
}
|
||||
@@ -58,33 +62,53 @@ inline void RequestDockLayoutReset(IEditorContext& context) {
|
||||
context.GetEventBus().Publish(DockLayoutResetRequestedEvent{});
|
||||
}
|
||||
|
||||
inline void RequestTogglePlayMode(IEditorContext& context) {
|
||||
if (context.GetRuntimeMode() == EditorRuntimeMode::Edit) {
|
||||
context.GetEventBus().Publish(PlayModeStartRequestedEvent{});
|
||||
return;
|
||||
}
|
||||
|
||||
context.GetEventBus().Publish(PlayModeStopRequestedEvent{});
|
||||
}
|
||||
|
||||
inline void RequestAboutPopup(UI::DeferredPopupState& aboutPopup) {
|
||||
aboutPopup.RequestOpen();
|
||||
}
|
||||
|
||||
inline void HandleMainMenuShortcuts(IEditorContext& context, const ShortcutContext& shortcutContext) {
|
||||
HandleShortcut(MakeNewSceneAction(), shortcutContext, [&]() { ExecuteNewScene(context); });
|
||||
HandleShortcut(MakeOpenSceneAction(), shortcutContext, [&]() { ExecuteOpenScene(context); });
|
||||
HandleShortcut(MakeSaveSceneAction(), shortcutContext, [&]() { ExecuteSaveScene(context); });
|
||||
HandleShortcut(MakeSaveSceneAsAction(), shortcutContext, [&]() { ExecuteSaveSceneAs(context); });
|
||||
const bool canEditDocuments = IsDocumentEditingAllowed(context);
|
||||
HandleShortcut(MakeTogglePlayModeAction(context.GetRuntimeMode()), shortcutContext, [&]() {
|
||||
RequestTogglePlayMode(context);
|
||||
});
|
||||
HandleShortcut(MakeNewSceneAction(canEditDocuments), shortcutContext, [&]() { ExecuteNewScene(context); });
|
||||
HandleShortcut(MakeOpenSceneAction(canEditDocuments), shortcutContext, [&]() { ExecuteOpenScene(context); });
|
||||
HandleShortcut(MakeSaveSceneAction(canEditDocuments), shortcutContext, [&]() { ExecuteSaveScene(context); });
|
||||
HandleShortcut(MakeSaveSceneAsAction(canEditDocuments), shortcutContext, [&]() { ExecuteSaveSceneAs(context); });
|
||||
HandleShortcut(MakeUndoAction(context), shortcutContext, [&]() { ExecuteUndo(context); });
|
||||
HandleShortcut(MakeRedoAction(context), shortcutContext, [&]() { ExecuteRedo(context); });
|
||||
HandleEditShortcuts(context, shortcutContext);
|
||||
}
|
||||
|
||||
inline void DrawFileMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(MakeNewProjectAction(), [&]() { ExecuteNewProject(context); });
|
||||
DrawMenuAction(MakeOpenProjectAction(), [&]() { ExecuteOpenProject(context); });
|
||||
DrawMenuAction(MakeSaveProjectAction(), [&]() { ExecuteSaveProject(context); });
|
||||
const bool canEditDocuments = IsDocumentEditingAllowed(context);
|
||||
DrawMenuAction(MakeNewProjectAction(canEditDocuments), [&]() { ExecuteNewProject(context); });
|
||||
DrawMenuAction(MakeOpenProjectAction(canEditDocuments), [&]() { ExecuteOpenProject(context); });
|
||||
DrawMenuAction(MakeSaveProjectAction(canEditDocuments), [&]() { ExecuteSaveProject(context); });
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeNewSceneAction(), [&]() { ExecuteNewScene(context); });
|
||||
DrawMenuAction(MakeOpenSceneAction(), [&]() { ExecuteOpenScene(context); });
|
||||
DrawMenuAction(MakeSaveSceneAction(), [&]() { ExecuteSaveScene(context); });
|
||||
DrawMenuAction(MakeSaveSceneAsAction(), [&]() { ExecuteSaveSceneAs(context); });
|
||||
DrawMenuAction(MakeNewSceneAction(canEditDocuments), [&]() { ExecuteNewScene(context); });
|
||||
DrawMenuAction(MakeOpenSceneAction(canEditDocuments), [&]() { ExecuteOpenScene(context); });
|
||||
DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); });
|
||||
DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); });
|
||||
DrawMenuSeparator();
|
||||
DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); });
|
||||
}
|
||||
|
||||
inline void DrawRunMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(MakeTogglePlayModeAction(context.GetRuntimeMode()), [&]() {
|
||||
RequestTogglePlayMode(context);
|
||||
});
|
||||
}
|
||||
|
||||
inline void DrawViewMenuActions(IEditorContext& context) {
|
||||
DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); });
|
||||
}
|
||||
@@ -108,6 +132,9 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo
|
||||
UI::DrawMenuScope("Edit", [&]() {
|
||||
DrawEditActions(context);
|
||||
});
|
||||
UI::DrawMenuScope("Run", [&]() {
|
||||
DrawRunMenuActions(context);
|
||||
});
|
||||
UI::DrawMenuScope("View", [&]() {
|
||||
DrawViewMenuActions(context);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Application.h"
|
||||
#include "Platform/Win32Utf8.h"
|
||||
#include "Core/AssetItem.h"
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/IProjectManager.h"
|
||||
@@ -14,12 +15,17 @@
|
||||
#include <algorithm>
|
||||
#include <cwctype>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Commands {
|
||||
|
||||
inline bool IsProjectDocumentEditingAllowed(const IEditorContext& context) {
|
||||
return IsEditorDocumentEditingAllowed(context.GetRuntimeMode());
|
||||
}
|
||||
|
||||
namespace detail {
|
||||
|
||||
inline std::wstring MakeProjectPathKey(const std::filesystem::path& path) {
|
||||
@@ -75,6 +81,93 @@ inline AssetItemPtr CreateFolder(IProjectManager& projectManager, const std::str
|
||||
return projectManager.CreateFolder(name);
|
||||
}
|
||||
|
||||
inline AssetItemPtr CreateMaterial(IProjectManager& projectManager, const std::string& name) {
|
||||
if (name.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AssetItemPtr currentFolder = projectManager.GetCurrentFolder();
|
||||
const AssetItemPtr rootFolder = projectManager.GetRootFolder();
|
||||
if (!currentFolder || !currentFolder->isFolder || !rootFolder) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
try {
|
||||
const std::string trimmedName = ProjectFileUtils::Trim(name);
|
||||
if (trimmedName.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const fs::path currentFolderPath = fs::path(currentFolder->fullPath);
|
||||
const fs::path rootPath = fs::path(rootFolder->fullPath);
|
||||
if (!fs::exists(currentFolderPath) || !fs::is_directory(currentFolderPath) ||
|
||||
!detail::IsSameOrDescendantProjectPath(currentFolderPath, rootPath)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
fs::path requestedFileName = fs::path(trimmedName);
|
||||
if (!requestedFileName.has_extension()) {
|
||||
requestedFileName += ".mat";
|
||||
}
|
||||
|
||||
fs::path materialPath = currentFolderPath / requestedFileName;
|
||||
for (size_t suffix = 1; fs::exists(materialPath); ++suffix) {
|
||||
materialPath = currentFolderPath /
|
||||
fs::path(requestedFileName.stem().string() + " " + std::to_string(suffix) + requestedFileName.extension().string());
|
||||
}
|
||||
|
||||
std::ofstream output(materialPath, std::ios::out | std::ios::trunc);
|
||||
if (!output.is_open()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
output <<
|
||||
"{\n"
|
||||
" \"renderQueue\": \"geometry\",\n"
|
||||
" \"renderState\": {\n"
|
||||
" \"cull\": \"none\",\n"
|
||||
" \"depthTest\": true,\n"
|
||||
" \"depthWrite\": true,\n"
|
||||
" \"depthFunc\": \"less\",\n"
|
||||
" \"blendEnable\": false,\n"
|
||||
" \"srcBlend\": \"one\",\n"
|
||||
" \"dstBlend\": \"zero\",\n"
|
||||
" \"srcBlendAlpha\": \"one\",\n"
|
||||
" \"dstBlendAlpha\": \"zero\",\n"
|
||||
" \"blendOp\": \"add\",\n"
|
||||
" \"blendOpAlpha\": \"add\",\n"
|
||||
" \"colorWriteMask\": 15\n"
|
||||
" }\n"
|
||||
"}\n";
|
||||
output.close();
|
||||
if (!output.good()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
projectManager.RefreshCurrentFolder();
|
||||
const std::string createdMaterialPath = Platform::WideToUtf8(materialPath.wstring());
|
||||
const int createdIndex = projectManager.FindCurrentItemIndex(createdMaterialPath);
|
||||
if (createdIndex < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto& items = projectManager.GetCurrentItems();
|
||||
if (createdIndex >= static_cast<int>(items.size())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const AssetItemPtr createdMaterial = items[createdIndex];
|
||||
if (createdMaterial) {
|
||||
projectManager.SetSelectedItem(createdMaterial);
|
||||
}
|
||||
return createdMaterial;
|
||||
} catch (...) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool DeleteAsset(IProjectManager& projectManager, const std::string& fullPath) {
|
||||
if (fullPath.empty()) {
|
||||
return false;
|
||||
@@ -204,6 +297,10 @@ inline bool SaveProjectDescriptor(IEditorContext& context) {
|
||||
}
|
||||
|
||||
inline bool SaveProject(IEditorContext& context) {
|
||||
if (!IsProjectDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EnsureProjectStructure(context.GetProjectPath())) {
|
||||
return false;
|
||||
}
|
||||
@@ -218,6 +315,10 @@ inline bool SaveProject(IEditorContext& context) {
|
||||
}
|
||||
|
||||
inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) {
|
||||
if (!IsProjectDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (projectPath.empty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -249,6 +350,10 @@ inline bool SwitchProject(IEditorContext& context, const std::string& projectPat
|
||||
}
|
||||
|
||||
inline bool NewProjectWithDialog(IEditorContext& context) {
|
||||
if (!IsProjectDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string projectPath = FileDialogUtils::PickFolderDialog(
|
||||
L"Select New Project Folder",
|
||||
context.GetProjectPath());
|
||||
@@ -260,6 +365,10 @@ inline bool NewProjectWithDialog(IEditorContext& context) {
|
||||
}
|
||||
|
||||
inline bool OpenProjectWithDialog(IEditorContext& context) {
|
||||
if (!IsProjectDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string projectPath = FileDialogUtils::PickFolderDialog(
|
||||
L"Open Project Folder",
|
||||
context.GetProjectPath());
|
||||
|
||||
@@ -13,12 +13,20 @@ namespace XCEngine {
|
||||
namespace Editor {
|
||||
namespace Commands {
|
||||
|
||||
inline bool IsSceneDocumentEditingAllowed(const IEditorContext& context) {
|
||||
return IsEditorDocumentEditingAllowed(context.GetRuntimeMode());
|
||||
}
|
||||
|
||||
inline void ResetSceneEditingState(IEditorContext& context) {
|
||||
context.GetSelectionManager().ClearSelection();
|
||||
context.GetUndoManager().ClearHistory();
|
||||
}
|
||||
|
||||
inline bool NewScene(IEditorContext& context, const std::string& sceneName = "Untitled Scene") {
|
||||
if (!IsSceneDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SceneEditorUtils::ConfirmSceneSwitch(context)) {
|
||||
return false;
|
||||
}
|
||||
@@ -29,6 +37,10 @@ inline bool NewScene(IEditorContext& context, const std::string& sceneName = "Un
|
||||
}
|
||||
|
||||
inline bool LoadScene(IEditorContext& context, const std::string& filePath, bool confirmSwitch = true) {
|
||||
if (!IsSceneDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
@@ -44,6 +56,10 @@ inline bool LoadScene(IEditorContext& context, const std::string& filePath, bool
|
||||
}
|
||||
|
||||
inline bool OpenSceneWithDialog(IEditorContext& context) {
|
||||
if (!IsSceneDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SceneEditorUtils::ConfirmSceneSwitch(context)) {
|
||||
return false;
|
||||
}
|
||||
@@ -59,10 +75,18 @@ inline bool OpenSceneWithDialog(IEditorContext& context) {
|
||||
}
|
||||
|
||||
inline bool SaveCurrentScene(IEditorContext& context) {
|
||||
if (!IsSceneDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return SceneEditorUtils::SaveCurrentScene(context);
|
||||
}
|
||||
|
||||
inline bool SaveSceneAsWithDialog(IEditorContext& context) {
|
||||
if (!IsSceneDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& sceneManager = context.GetSceneManager();
|
||||
const std::string filePath = SceneEditorUtils::SaveSceneFileDialog(
|
||||
context.GetProjectPath(),
|
||||
@@ -80,6 +104,10 @@ inline bool SaveSceneAsWithDialog(IEditorContext& context) {
|
||||
}
|
||||
|
||||
inline bool LoadStartupScene(IEditorContext& context) {
|
||||
if (!IsSceneDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool loaded = context.GetSceneManager().LoadStartupScene(context.GetProjectPath());
|
||||
context.GetProjectManager().RefreshCurrentFolder();
|
||||
ResetSceneEditingState(context);
|
||||
@@ -87,6 +115,10 @@ inline bool LoadStartupScene(IEditorContext& context) {
|
||||
}
|
||||
|
||||
inline bool SaveDirtySceneWithFallback(IEditorContext& context, const std::string& fallbackPath) {
|
||||
if (!IsSceneDocumentEditingAllowed(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& sceneManager = context.GetSceneManager();
|
||||
if (!sceneManager.HasActiveScene() || !sceneManager.IsSceneDirty()) {
|
||||
return true;
|
||||
|
||||
@@ -72,6 +72,20 @@ public:
|
||||
EditorActionRoute GetActiveActionRoute() const override {
|
||||
return m_activeActionRoute;
|
||||
}
|
||||
|
||||
void SetRuntimeMode(EditorRuntimeMode mode) override {
|
||||
if (m_runtimeMode == mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const EditorRuntimeMode oldMode = m_runtimeMode;
|
||||
m_runtimeMode = mode;
|
||||
m_eventBus->Publish(EditorModeChangedEvent{ oldMode, m_runtimeMode });
|
||||
}
|
||||
|
||||
EditorRuntimeMode GetRuntimeMode() const override {
|
||||
return m_runtimeMode;
|
||||
}
|
||||
|
||||
void SetProjectPath(const std::string& path) override {
|
||||
m_projectPath = path;
|
||||
@@ -89,6 +103,7 @@ private:
|
||||
std::unique_ptr<ProjectManager> m_projectManager;
|
||||
IViewportHostService* m_viewportHostService = nullptr;
|
||||
EditorActionRoute m_activeActionRoute = EditorActionRoute::None;
|
||||
EditorRuntimeMode m_runtimeMode = EditorRuntimeMode::Edit;
|
||||
std::string m_projectPath;
|
||||
uint64_t m_entityDeletedHandlerId;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "EditorRuntimeMode.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
@@ -41,15 +43,24 @@ struct SceneChangedEvent {
|
||||
struct PlayModeStartedEvent {
|
||||
};
|
||||
|
||||
struct PlayModeStartRequestedEvent {
|
||||
};
|
||||
|
||||
struct PlayModeStoppedEvent {
|
||||
};
|
||||
|
||||
struct PlayModeStopRequestedEvent {
|
||||
};
|
||||
|
||||
struct PlayModePausedEvent {
|
||||
};
|
||||
|
||||
struct PlayModePauseRequestedEvent {
|
||||
};
|
||||
|
||||
struct EditorModeChangedEvent {
|
||||
int oldMode;
|
||||
int newMode;
|
||||
EditorRuntimeMode oldMode = EditorRuntimeMode::Edit;
|
||||
EditorRuntimeMode newMode = EditorRuntimeMode::Edit;
|
||||
};
|
||||
|
||||
struct DockLayoutResetRequestedEvent {
|
||||
|
||||
38
editor/src/Core/EditorRuntimeMode.h
Normal file
38
editor/src/Core/EditorRuntimeMode.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
enum class EditorRuntimeMode {
|
||||
Edit = 0,
|
||||
Play,
|
||||
Paused,
|
||||
Simulate
|
||||
};
|
||||
|
||||
inline bool IsEditorRuntimeActive(EditorRuntimeMode mode) {
|
||||
return mode != EditorRuntimeMode::Edit;
|
||||
}
|
||||
|
||||
inline bool IsEditorDocumentEditingAllowed(EditorRuntimeMode mode) {
|
||||
return mode == EditorRuntimeMode::Edit;
|
||||
}
|
||||
|
||||
inline bool IsEditorSceneObjectEditingAllowed(EditorRuntimeMode mode) {
|
||||
switch (mode) {
|
||||
case EditorRuntimeMode::Edit:
|
||||
case EditorRuntimeMode::Play:
|
||||
case EditorRuntimeMode::Paused:
|
||||
case EditorRuntimeMode::Simulate:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline bool IsEditorSceneUndoRedoAllowed(EditorRuntimeMode mode) {
|
||||
return IsEditorSceneObjectEditingAllowed(mode);
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -11,6 +11,22 @@ namespace Editor {
|
||||
|
||||
inline std::string BuildEditorWindowTitle(IEditorContext& context) {
|
||||
auto& sceneManager = context.GetSceneManager();
|
||||
std::string modePrefix;
|
||||
switch (context.GetRuntimeMode()) {
|
||||
case EditorRuntimeMode::Play:
|
||||
modePrefix = "[Play] ";
|
||||
break;
|
||||
case EditorRuntimeMode::Paused:
|
||||
modePrefix = "[Paused] ";
|
||||
break;
|
||||
case EditorRuntimeMode::Simulate:
|
||||
modePrefix = "[Simulate] ";
|
||||
break;
|
||||
case EditorRuntimeMode::Edit:
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
std::string sceneName = sceneManager.HasActiveScene() ? sceneManager.GetCurrentSceneName() : "No Scene";
|
||||
if (sceneName.empty()) {
|
||||
sceneName = "Untitled Scene";
|
||||
@@ -26,7 +42,7 @@ inline std::string BuildEditorWindowTitle(IEditorContext& context) {
|
||||
sceneName += std::filesystem::path(sceneManager.GetCurrentScenePath()).filename().string();
|
||||
}
|
||||
|
||||
return sceneName + " - XCEngine Editor";
|
||||
return modePrefix + sceneName + " - XCEngine Editor";
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "Commands/SceneCommands.h"
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/PlaySessionController.h"
|
||||
#include "Layout/DockLayoutController.h"
|
||||
#include "panels/ConsolePanel.h"
|
||||
#include "panels/GameViewPanel.h"
|
||||
@@ -37,11 +38,13 @@ public:
|
||||
|
||||
m_projectPanel->Initialize(context.GetProjectPath());
|
||||
Commands::LoadStartupScene(context);
|
||||
m_playSessionController.Attach(context);
|
||||
m_dockLayoutController->Attach(context);
|
||||
m_panels.AttachAll();
|
||||
}
|
||||
|
||||
void Detach(IEditorContext& context) {
|
||||
m_playSessionController.Detach(context);
|
||||
Commands::SaveDirtySceneWithFallback(context, BuildFallbackScenePath(context));
|
||||
|
||||
if (m_dockLayoutController) {
|
||||
@@ -56,6 +59,9 @@ public:
|
||||
|
||||
void Update(float dt) {
|
||||
::XCEngine::Resources::ResourceManager::Get().UpdateAsyncLoads();
|
||||
if (IEditorContext* context = m_panels.GetContext()) {
|
||||
m_playSessionController.Update(*context, dt);
|
||||
}
|
||||
m_panels.UpdateAll(dt);
|
||||
}
|
||||
|
||||
@@ -79,6 +85,7 @@ private:
|
||||
PanelCollection m_panels;
|
||||
ProjectPanel* m_projectPanel = nullptr;
|
||||
std::unique_ptr<DockLayoutController> m_dockLayoutController;
|
||||
PlaySessionController m_playSessionController;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "EditorActionRoute.h"
|
||||
#include "EditorRuntimeMode.h"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@@ -27,6 +28,8 @@ public:
|
||||
virtual IViewportHostService* GetViewportHostService() = 0;
|
||||
virtual void SetActiveActionRoute(EditorActionRoute route) = 0;
|
||||
virtual EditorActionRoute GetActiveActionRoute() const = 0;
|
||||
virtual void SetRuntimeMode(EditorRuntimeMode mode) = 0;
|
||||
virtual EditorRuntimeMode GetRuntimeMode() const = 0;
|
||||
|
||||
virtual void SetProjectPath(const std::string& path) = 0;
|
||||
virtual const std::string& GetProjectPath() const = 0;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "SceneSnapshot.h"
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
@@ -34,10 +36,14 @@ public:
|
||||
virtual bool HasActiveScene() const = 0;
|
||||
virtual bool IsSceneDirty() const = 0;
|
||||
virtual void MarkSceneDirty() = 0;
|
||||
virtual void SetSceneDocumentDirtyTrackingEnabled(bool enabled) = 0;
|
||||
virtual bool IsSceneDocumentDirtyTrackingEnabled() const = 0;
|
||||
virtual const std::string& GetCurrentScenePath() const = 0;
|
||||
virtual const std::string& GetCurrentSceneName() const = 0;
|
||||
virtual ::XCEngine::Components::Scene* GetScene() = 0;
|
||||
virtual const ::XCEngine::Components::Scene* GetScene() const = 0;
|
||||
virtual SceneSnapshot CaptureSceneSnapshot() const = 0;
|
||||
virtual bool RestoreSceneSnapshot(const SceneSnapshot& snapshot) = 0;
|
||||
virtual void CreateDemoScene() = 0;
|
||||
};
|
||||
|
||||
|
||||
122
editor/src/Core/PlaySessionController.cpp
Normal file
122
editor/src/Core/PlaySessionController.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "Core/PlaySessionController.h"
|
||||
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Core/EventBus.h"
|
||||
#include "Core/IEditorContext.h"
|
||||
#include "Core/ISceneManager.h"
|
||||
#include "Core/IUndoManager.h"
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
void PlaySessionController::Attach(IEditorContext& context) {
|
||||
if (m_playStartRequestedHandlerId == 0) {
|
||||
m_playStartRequestedHandlerId = context.GetEventBus().Subscribe<PlayModeStartRequestedEvent>(
|
||||
[this, &context](const PlayModeStartRequestedEvent&) {
|
||||
StartPlay(context);
|
||||
});
|
||||
}
|
||||
|
||||
if (m_playStopRequestedHandlerId == 0) {
|
||||
m_playStopRequestedHandlerId = context.GetEventBus().Subscribe<PlayModeStopRequestedEvent>(
|
||||
[this, &context](const PlayModeStopRequestedEvent&) {
|
||||
StopPlay(context);
|
||||
});
|
||||
}
|
||||
|
||||
if (m_playPauseRequestedHandlerId == 0) {
|
||||
m_playPauseRequestedHandlerId = context.GetEventBus().Subscribe<PlayModePauseRequestedEvent>(
|
||||
[this, &context](const PlayModePauseRequestedEvent&) {
|
||||
PausePlay(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PlaySessionController::Detach(IEditorContext& context) {
|
||||
StopPlay(context);
|
||||
|
||||
if (m_playStartRequestedHandlerId != 0) {
|
||||
context.GetEventBus().Unsubscribe<PlayModeStartRequestedEvent>(m_playStartRequestedHandlerId);
|
||||
m_playStartRequestedHandlerId = 0;
|
||||
}
|
||||
|
||||
if (m_playStopRequestedHandlerId != 0) {
|
||||
context.GetEventBus().Unsubscribe<PlayModeStopRequestedEvent>(m_playStopRequestedHandlerId);
|
||||
m_playStopRequestedHandlerId = 0;
|
||||
}
|
||||
|
||||
if (m_playPauseRequestedHandlerId != 0) {
|
||||
context.GetEventBus().Unsubscribe<PlayModePauseRequestedEvent>(m_playPauseRequestedHandlerId);
|
||||
m_playPauseRequestedHandlerId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void PlaySessionController::Update(IEditorContext& context, float deltaTime) {
|
||||
(void)context;
|
||||
if (!m_runtimeLoop.IsRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_runtimeLoop.Tick(deltaTime);
|
||||
}
|
||||
|
||||
bool PlaySessionController::StartPlay(IEditorContext& context) {
|
||||
if (context.GetRuntimeMode() != EditorRuntimeMode::Edit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& sceneManager = context.GetSceneManager();
|
||||
if (!sceneManager.HasActiveScene()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_editorSnapshot = sceneManager.CaptureSceneSnapshot();
|
||||
if (!m_editorSnapshot.hasScene) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sceneManager.RestoreSceneSnapshot(m_editorSnapshot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sceneManager.SetSceneDocumentDirtyTrackingEnabled(false);
|
||||
m_runtimeLoop.Start(sceneManager.GetScene());
|
||||
context.GetUndoManager().ClearHistory();
|
||||
context.SetRuntimeMode(EditorRuntimeMode::Play);
|
||||
context.GetEventBus().Publish(PlayModeStartedEvent{});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaySessionController::StopPlay(IEditorContext& context) {
|
||||
if (!IsEditorRuntimeActive(context.GetRuntimeMode())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto& sceneManager = context.GetSceneManager();
|
||||
m_runtimeLoop.Stop();
|
||||
sceneManager.SetSceneDocumentDirtyTrackingEnabled(true);
|
||||
|
||||
if (!sceneManager.RestoreSceneSnapshot(m_editorSnapshot)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
context.GetUndoManager().ClearHistory();
|
||||
context.SetRuntimeMode(EditorRuntimeMode::Edit);
|
||||
context.GetEventBus().Publish(PlayModeStoppedEvent{});
|
||||
m_editorSnapshot = {};
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlaySessionController::PausePlay(IEditorContext& context) {
|
||||
if (context.GetRuntimeMode() != EditorRuntimeMode::Play || !m_runtimeLoop.IsRunning()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_runtimeLoop.Pause();
|
||||
context.SetRuntimeMode(EditorRuntimeMode::Paused);
|
||||
context.GetEventBus().Publish(PlayModePausedEvent{});
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
35
editor/src/Core/PlaySessionController.h
Normal file
35
editor/src/Core/PlaySessionController.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "EditorRuntimeMode.h"
|
||||
#include "SceneSnapshot.h"
|
||||
|
||||
#include <XCEngine/Scene/RuntimeLoop.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Editor {
|
||||
|
||||
class IEditorContext;
|
||||
|
||||
class PlaySessionController {
|
||||
public:
|
||||
void Attach(IEditorContext& context);
|
||||
void Detach(IEditorContext& context);
|
||||
|
||||
void Update(IEditorContext& context, float deltaTime);
|
||||
|
||||
bool StartPlay(IEditorContext& context);
|
||||
bool StopPlay(IEditorContext& context);
|
||||
bool PausePlay(IEditorContext& context);
|
||||
|
||||
private:
|
||||
uint64_t m_playStartRequestedHandlerId = 0;
|
||||
uint64_t m_playStopRequestedHandlerId = 0;
|
||||
uint64_t m_playPauseRequestedHandlerId = 0;
|
||||
SceneSnapshot m_editorSnapshot = {};
|
||||
XCEngine::Components::RuntimeLoop m_runtimeLoop;
|
||||
};
|
||||
|
||||
} // namespace Editor
|
||||
} // namespace XCEngine
|
||||
@@ -32,6 +32,10 @@ void SceneManager::SetSceneDirty(bool dirty) {
|
||||
}
|
||||
|
||||
void SceneManager::MarkSceneDirty() {
|
||||
if (!m_sceneDocumentDirtyTrackingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
SetSceneDirty(true);
|
||||
}
|
||||
|
||||
@@ -43,7 +47,7 @@ void SceneManager::MarkSceneDirty() {
|
||||
::XCEngine::Components::GameObject* entity = m_scene->CreateGameObject(name, parent);
|
||||
const auto entityId = entity->GetID();
|
||||
SyncRootEntities();
|
||||
SetSceneDirty(true);
|
||||
MarkSceneDirty();
|
||||
|
||||
OnEntityCreated.Invoke(entityId);
|
||||
OnSceneChanged.Invoke();
|
||||
@@ -70,7 +74,7 @@ void SceneManager::DeleteEntity(::XCEngine::Components::GameObject::ID id) {
|
||||
const auto entityId = entity->GetID();
|
||||
m_scene->DestroyGameObject(entity);
|
||||
SyncRootEntities();
|
||||
SetSceneDirty(true);
|
||||
MarkSceneDirty();
|
||||
|
||||
OnEntityDeleted.Invoke(entityId);
|
||||
OnSceneChanged.Invoke();
|
||||
@@ -150,7 +154,7 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) {
|
||||
|
||||
const auto newEntityId = PasteEntityRecursive(*m_clipboard, parent);
|
||||
SyncRootEntities();
|
||||
SetSceneDirty(true);
|
||||
MarkSceneDirty();
|
||||
|
||||
OnEntityCreated.Invoke(newEntityId);
|
||||
OnSceneChanged.Invoke();
|
||||
@@ -320,7 +324,7 @@ void SceneManager::RenameEntity(::XCEngine::Components::GameObject::ID id, const
|
||||
if (!obj) return;
|
||||
|
||||
obj->SetName(newName);
|
||||
SetSceneDirty(true);
|
||||
MarkSceneDirty();
|
||||
OnEntityChanged.Invoke(id);
|
||||
|
||||
if (m_eventBus) {
|
||||
@@ -342,7 +346,7 @@ void SceneManager::MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEng
|
||||
|
||||
obj->SetParent(newParent);
|
||||
SyncRootEntities();
|
||||
SetSceneDirty(true);
|
||||
MarkSceneDirty();
|
||||
|
||||
OnEntityChanged.Invoke(id);
|
||||
OnSceneChanged.Invoke();
|
||||
|
||||
@@ -57,15 +57,21 @@ public:
|
||||
bool HasActiveScene() const override { return m_scene != nullptr; }
|
||||
bool IsSceneDirty() const override { return m_isSceneDirty; }
|
||||
void MarkSceneDirty() override;
|
||||
void SetSceneDocumentDirtyTrackingEnabled(bool enabled) override {
|
||||
m_sceneDocumentDirtyTrackingEnabled = enabled;
|
||||
}
|
||||
bool IsSceneDocumentDirtyTrackingEnabled() const override {
|
||||
return m_sceneDocumentDirtyTrackingEnabled;
|
||||
}
|
||||
const std::string& GetCurrentScenePath() const override { return m_currentScenePath; }
|
||||
const std::string& GetCurrentSceneName() const override { return m_currentSceneName; }
|
||||
::XCEngine::Components::Scene* GetScene() override { return m_scene.get(); }
|
||||
const ::XCEngine::Components::Scene* GetScene() const override { return m_scene.get(); }
|
||||
SceneSnapshot CaptureSceneSnapshot() const override;
|
||||
bool RestoreSceneSnapshot(const SceneSnapshot& snapshot) override;
|
||||
void CreateDemoScene() override;
|
||||
|
||||
bool HasClipboardData() const { return m_clipboard.has_value(); }
|
||||
SceneSnapshot CaptureSceneSnapshot() const;
|
||||
bool RestoreSceneSnapshot(const SceneSnapshot& snapshot);
|
||||
|
||||
::XCEngine::Core::Event<::XCEngine::Components::GameObject::ID> OnEntityCreated;
|
||||
::XCEngine::Core::Event<::XCEngine::Components::GameObject::ID> OnEntityDeleted;
|
||||
@@ -97,6 +103,7 @@ private:
|
||||
std::string m_currentScenePath;
|
||||
std::string m_currentSceneName = "Untitled Scene";
|
||||
bool m_isSceneDirty = false;
|
||||
bool m_sceneDocumentDirtyTrackingEnabled = true;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -511,27 +511,27 @@ bool DrawCompactCheckedMenuItem(const char* label, bool checked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool clicked = ImGui::Selectable(label, false, ImGuiSelectableFlags_SpanAvailWidth);
|
||||
const bool clicked = ImGui::MenuItem(label, nullptr, false, true);
|
||||
const ImRect rect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
if (checked) {
|
||||
const ImRect rect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
const ImU32 color = ImGui::GetColorU32(ImGuiCol_CheckMark);
|
||||
const float height = rect.Max.y - rect.Min.y;
|
||||
const float checkWidth = height * 0.28f;
|
||||
const float checkHeight = height * 0.18f;
|
||||
const float x = rect.Max.x - 12.0f;
|
||||
const float checkWidth = height * 0.24f;
|
||||
const float checkHeight = height * 0.16f;
|
||||
const float x = rect.Max.x - 10.0f;
|
||||
const float y = rect.Min.y + height * 0.52f;
|
||||
|
||||
drawList->AddLine(
|
||||
ImVec2(x - checkWidth, y - checkHeight * 0.15f),
|
||||
ImVec2(x - checkWidth * 0.42f, y + checkHeight),
|
||||
color,
|
||||
1.4f);
|
||||
1.8f);
|
||||
drawList->AddLine(
|
||||
ImVec2(x - checkWidth * 0.42f, y + checkHeight),
|
||||
ImVec2(x + checkWidth, y - checkHeight),
|
||||
color,
|
||||
1.4f);
|
||||
1.8f);
|
||||
}
|
||||
|
||||
return clicked;
|
||||
@@ -973,7 +973,7 @@ void ConsolePanel::Render() {
|
||||
}
|
||||
|
||||
if (shouldPause) {
|
||||
m_context->GetEventBus().Publish(PlayModePausedEvent{});
|
||||
m_context->GetEventBus().Publish(PlayModePauseRequestedEvent{});
|
||||
m_playModePaused = true;
|
||||
}
|
||||
m_lastErrorPauseScanSerial = latestSerial;
|
||||
|
||||
@@ -14,6 +14,10 @@ class IEditorContext;
|
||||
|
||||
class PanelCollection {
|
||||
public:
|
||||
IEditorContext* GetContext() const {
|
||||
return m_context;
|
||||
}
|
||||
|
||||
void SetContext(IEditorContext* context) {
|
||||
m_context = context;
|
||||
|
||||
|
||||
@@ -377,9 +377,11 @@ add_library(XCEngine STATIC
|
||||
# Scene
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneRuntime.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/RuntimeLoop.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/Scene.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneRuntime.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/RuntimeLoop.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneManager.cpp
|
||||
|
||||
# Platform
|
||||
|
||||
45
engine/include/XCEngine/Scene/RuntimeLoop.h
Normal file
45
engine/include/XCEngine/Scene/RuntimeLoop.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include <XCEngine/Scene/SceneRuntime.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
|
||||
class RuntimeLoop {
|
||||
public:
|
||||
struct Settings {
|
||||
float fixedDeltaTime = 1.0f / 50.0f;
|
||||
float maxFrameDeltaTime = 0.1f;
|
||||
uint32_t maxFixedStepsPerFrame = 4;
|
||||
};
|
||||
|
||||
explicit RuntimeLoop(Settings settings = {});
|
||||
|
||||
void SetSettings(const Settings& settings);
|
||||
const Settings& GetSettings() const { return m_settings; }
|
||||
|
||||
void Start(Scene* scene);
|
||||
void Stop();
|
||||
|
||||
void Tick(float deltaTime);
|
||||
void Pause();
|
||||
void Resume();
|
||||
void StepFrame();
|
||||
|
||||
bool IsRunning() const { return m_sceneRuntime.IsRunning(); }
|
||||
bool IsPaused() const { return m_paused; }
|
||||
Scene* GetScene() const { return m_sceneRuntime.GetScene(); }
|
||||
float GetFixedAccumulator() const { return m_fixedAccumulator; }
|
||||
|
||||
private:
|
||||
SceneRuntime m_sceneRuntime;
|
||||
Settings m_settings = {};
|
||||
float m_fixedAccumulator = 0.0f;
|
||||
bool m_paused = false;
|
||||
bool m_stepRequested = false;
|
||||
};
|
||||
|
||||
} // namespace Components
|
||||
} // namespace XCEngine
|
||||
100
engine/src/Scene/RuntimeLoop.cpp
Normal file
100
engine/src/Scene/RuntimeLoop.cpp
Normal file
@@ -0,0 +1,100 @@
|
||||
#include "Scene/RuntimeLoop.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace XCEngine {
|
||||
namespace Components {
|
||||
|
||||
namespace {
|
||||
|
||||
RuntimeLoop::Settings SanitizeSettings(RuntimeLoop::Settings settings) {
|
||||
if (settings.fixedDeltaTime <= 0.0f) {
|
||||
settings.fixedDeltaTime = 1.0f / 50.0f;
|
||||
}
|
||||
if (settings.maxFrameDeltaTime < 0.0f) {
|
||||
settings.maxFrameDeltaTime = 0.0f;
|
||||
}
|
||||
if (settings.maxFixedStepsPerFrame == 0) {
|
||||
settings.maxFixedStepsPerFrame = 1;
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
RuntimeLoop::RuntimeLoop(Settings settings)
|
||||
: m_settings(SanitizeSettings(settings)) {
|
||||
}
|
||||
|
||||
void RuntimeLoop::SetSettings(const Settings& settings) {
|
||||
m_settings = SanitizeSettings(settings);
|
||||
}
|
||||
|
||||
void RuntimeLoop::Start(Scene* scene) {
|
||||
m_fixedAccumulator = 0.0f;
|
||||
m_paused = false;
|
||||
m_stepRequested = false;
|
||||
m_sceneRuntime.Start(scene);
|
||||
}
|
||||
|
||||
void RuntimeLoop::Stop() {
|
||||
m_sceneRuntime.Stop();
|
||||
m_fixedAccumulator = 0.0f;
|
||||
m_paused = false;
|
||||
m_stepRequested = false;
|
||||
}
|
||||
|
||||
void RuntimeLoop::Tick(float deltaTime) {
|
||||
if (!IsRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_paused && !m_stepRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
const float clampedDeltaTime = std::clamp(deltaTime, 0.0f, m_settings.maxFrameDeltaTime);
|
||||
m_fixedAccumulator += clampedDeltaTime;
|
||||
|
||||
uint32_t fixedStepsExecuted = 0;
|
||||
while (m_fixedAccumulator >= m_settings.fixedDeltaTime &&
|
||||
fixedStepsExecuted < m_settings.maxFixedStepsPerFrame) {
|
||||
m_sceneRuntime.FixedUpdate(m_settings.fixedDeltaTime);
|
||||
m_fixedAccumulator -= m_settings.fixedDeltaTime;
|
||||
++fixedStepsExecuted;
|
||||
}
|
||||
|
||||
m_sceneRuntime.Update(clampedDeltaTime);
|
||||
m_sceneRuntime.LateUpdate(clampedDeltaTime);
|
||||
m_stepRequested = false;
|
||||
}
|
||||
|
||||
void RuntimeLoop::Pause() {
|
||||
if (!IsRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_paused = true;
|
||||
m_stepRequested = false;
|
||||
}
|
||||
|
||||
void RuntimeLoop::Resume() {
|
||||
if (!IsRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_paused = false;
|
||||
m_stepRequested = false;
|
||||
}
|
||||
|
||||
void RuntimeLoop::StepFrame() {
|
||||
if (!IsRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_paused = true;
|
||||
m_stepRequested = true;
|
||||
}
|
||||
|
||||
} // namespace Components
|
||||
} // namespace XCEngine
|
||||
@@ -5,6 +5,7 @@ project(XCEngine_SceneTests)
|
||||
set(SCENE_TEST_SOURCES
|
||||
test_scene.cpp
|
||||
test_scene_runtime.cpp
|
||||
test_runtime_loop.cpp
|
||||
test_scene_manager.cpp
|
||||
)
|
||||
|
||||
|
||||
134
tests/Scene/test_runtime_loop.cpp
Normal file
134
tests/Scene/test_runtime_loop.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Components/Component.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Scene/RuntimeLoop.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using namespace XCEngine::Components;
|
||||
|
||||
namespace {
|
||||
|
||||
struct RuntimeLoopCounters {
|
||||
int startCount = 0;
|
||||
int fixedUpdateCount = 0;
|
||||
int updateCount = 0;
|
||||
int lateUpdateCount = 0;
|
||||
};
|
||||
|
||||
class RuntimeLoopObserverComponent : public Component {
|
||||
public:
|
||||
explicit RuntimeLoopObserverComponent(RuntimeLoopCounters* counters)
|
||||
: m_counters(counters) {
|
||||
}
|
||||
|
||||
std::string GetName() const override {
|
||||
return "RuntimeLoopObserver";
|
||||
}
|
||||
|
||||
void Start() override {
|
||||
if (m_counters) {
|
||||
++m_counters->startCount;
|
||||
}
|
||||
}
|
||||
|
||||
void FixedUpdate() override {
|
||||
if (m_counters) {
|
||||
++m_counters->fixedUpdateCount;
|
||||
}
|
||||
}
|
||||
|
||||
void Update(float deltaTime) override {
|
||||
(void)deltaTime;
|
||||
if (m_counters) {
|
||||
++m_counters->updateCount;
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate(float deltaTime) override {
|
||||
(void)deltaTime;
|
||||
if (m_counters) {
|
||||
++m_counters->lateUpdateCount;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
RuntimeLoopCounters* m_counters = nullptr;
|
||||
};
|
||||
|
||||
class RuntimeLoopTest : public ::testing::Test {
|
||||
protected:
|
||||
Scene* CreateScene(const std::string& name = "RuntimeLoopScene") {
|
||||
m_scene = std::make_unique<Scene>(name);
|
||||
return m_scene.get();
|
||||
}
|
||||
|
||||
RuntimeLoopCounters counters;
|
||||
std::unique_ptr<Scene> m_scene;
|
||||
};
|
||||
|
||||
TEST_F(RuntimeLoopTest, AccumulatesFixedUpdatesAcrossFramesAndRunsVariableUpdatesEveryTick) {
|
||||
RuntimeLoop loop({0.02f, 0.1f, 4});
|
||||
Scene* scene = CreateScene();
|
||||
GameObject* host = scene->CreateGameObject("Host");
|
||||
host->AddComponent<RuntimeLoopObserverComponent>(&counters);
|
||||
|
||||
loop.Start(scene);
|
||||
|
||||
loop.Tick(0.01f);
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 0);
|
||||
EXPECT_EQ(counters.startCount, 1);
|
||||
EXPECT_EQ(counters.updateCount, 1);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 1);
|
||||
|
||||
loop.Tick(0.01f);
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 1);
|
||||
EXPECT_EQ(counters.startCount, 1);
|
||||
EXPECT_EQ(counters.updateCount, 2);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 2);
|
||||
}
|
||||
|
||||
TEST_F(RuntimeLoopTest, ClampAndFixedStepLimitPreventExcessiveCatchUp) {
|
||||
RuntimeLoop loop({0.02f, 0.05f, 2});
|
||||
Scene* scene = CreateScene();
|
||||
GameObject* host = scene->CreateGameObject("Host");
|
||||
host->AddComponent<RuntimeLoopObserverComponent>(&counters);
|
||||
|
||||
loop.Start(scene);
|
||||
loop.Tick(1.0f);
|
||||
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 2);
|
||||
EXPECT_EQ(counters.startCount, 1);
|
||||
EXPECT_EQ(counters.updateCount, 1);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 1);
|
||||
EXPECT_NEAR(loop.GetFixedAccumulator(), 0.01f, 1e-4f);
|
||||
}
|
||||
|
||||
TEST_F(RuntimeLoopTest, PauseSkipsAutomaticTicksUntilStepFrameIsRequested) {
|
||||
RuntimeLoop loop({0.02f, 0.1f, 4});
|
||||
Scene* scene = CreateScene();
|
||||
GameObject* host = scene->CreateGameObject("Host");
|
||||
host->AddComponent<RuntimeLoopObserverComponent>(&counters);
|
||||
|
||||
loop.Start(scene);
|
||||
loop.Pause();
|
||||
|
||||
loop.Tick(0.025f);
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 0);
|
||||
EXPECT_EQ(counters.startCount, 0);
|
||||
EXPECT_EQ(counters.updateCount, 0);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 0);
|
||||
|
||||
loop.StepFrame();
|
||||
loop.Tick(0.025f);
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 1);
|
||||
EXPECT_EQ(counters.startCount, 1);
|
||||
EXPECT_EQ(counters.updateCount, 1);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 1);
|
||||
EXPECT_TRUE(loop.IsPaused());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -4,6 +4,7 @@ project(XCEngine_EditorTests)
|
||||
|
||||
set(EDITOR_TEST_SOURCES
|
||||
test_action_routing.cpp
|
||||
test_play_session_controller.cpp
|
||||
test_scene_viewport_camera_controller.cpp
|
||||
test_scene_viewport_move_gizmo.cpp
|
||||
test_scene_viewport_rotate_gizmo.cpp
|
||||
@@ -16,6 +17,7 @@ set(EDITOR_TEST_SOURCES
|
||||
test_viewport_render_flow_utils.cpp
|
||||
test_builtin_icon_layout_utils.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Core/PlaySessionController.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp
|
||||
@@ -30,6 +32,7 @@ if(MSVC)
|
||||
set_target_properties(editor_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
target_compile_options(editor_tests PRIVATE /FS)
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_tests PRIVATE
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "Commands/EntityCommands.h"
|
||||
#include "Commands/SceneCommands.h"
|
||||
#include "Core/EditorContext.h"
|
||||
#include "Core/PlaySessionController.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
@@ -283,6 +284,45 @@ TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsExitResetAndAboutPopup) {
|
||||
m_context.GetEventBus().Unsubscribe<DockLayoutResetRequestedEvent>(resetSubscription);
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked) {
|
||||
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "PlayModeRuntimeEditing.xc";
|
||||
ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string()));
|
||||
ASSERT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
||||
|
||||
PlaySessionController controller;
|
||||
ASSERT_TRUE(controller.StartPlay(m_context));
|
||||
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play);
|
||||
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDocumentDirtyTrackingEnabled());
|
||||
|
||||
EXPECT_FALSE(Commands::NewScene(m_context, "Blocked During Play"));
|
||||
EXPECT_FALSE(Commands::SaveCurrentScene(m_context));
|
||||
|
||||
const size_t entityCountBeforeCreate = CountHierarchyEntities(m_context.GetSceneManager());
|
||||
auto* runtimeEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Runtime Entity", "RuntimeOnly");
|
||||
ASSERT_NE(runtimeEntity, nullptr);
|
||||
const uint64_t runtimeEntityId = runtimeEntity->GetID();
|
||||
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate + 1);
|
||||
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
||||
|
||||
const Actions::ActionBinding undoAction = Actions::MakeUndoAction(m_context);
|
||||
EXPECT_TRUE(undoAction.enabled);
|
||||
Actions::ExecuteUndo(m_context);
|
||||
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate);
|
||||
EXPECT_EQ(m_context.GetSceneManager().GetEntity(runtimeEntityId), nullptr);
|
||||
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
||||
|
||||
const Actions::ActionBinding redoAction = Actions::MakeRedoAction(m_context);
|
||||
EXPECT_TRUE(redoAction.enabled);
|
||||
Actions::ExecuteRedo(m_context);
|
||||
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate + 1);
|
||||
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
||||
|
||||
ASSERT_TRUE(controller.StopPlay(m_context));
|
||||
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit);
|
||||
EXPECT_TRUE(m_context.GetSceneManager().IsSceneDocumentDirtyTrackingEnabled());
|
||||
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, HierarchyRouterRenameHelpersPublishAndCommit) {
|
||||
auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "BeforeRename");
|
||||
ASSERT_NE(entity, nullptr);
|
||||
|
||||
83
tests/editor/test_play_session_controller.cpp
Normal file
83
tests/editor/test_play_session_controller.cpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Core/EditorContext.h"
|
||||
#include "Core/EditorEvents.h"
|
||||
#include "Core/PlaySessionController.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
namespace XCEngine::Editor {
|
||||
namespace {
|
||||
|
||||
class PlaySessionControllerTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
m_context.GetSceneManager().NewScene("Play Session Scene");
|
||||
}
|
||||
|
||||
EditorContext m_context;
|
||||
PlaySessionController m_controller;
|
||||
};
|
||||
|
||||
TEST_F(PlaySessionControllerTest, StartPlayClonesCurrentSceneAndStopRestoresEditorScene) {
|
||||
auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent");
|
||||
ASSERT_NE(editorEntity, nullptr);
|
||||
const uint64_t editorEntityId = editorEntity->GetID();
|
||||
editorEntity->GetTransform()->SetLocalPosition(Math::Vector3(1.0f, 2.0f, 3.0f));
|
||||
|
||||
int startedCount = 0;
|
||||
int stoppedCount = 0;
|
||||
const uint64_t startedSubscription = m_context.GetEventBus().Subscribe<PlayModeStartedEvent>(
|
||||
[&](const PlayModeStartedEvent&) {
|
||||
++startedCount;
|
||||
});
|
||||
const uint64_t stoppedSubscription = m_context.GetEventBus().Subscribe<PlayModeStoppedEvent>(
|
||||
[&](const PlayModeStoppedEvent&) {
|
||||
++stoppedCount;
|
||||
});
|
||||
|
||||
ASSERT_TRUE(m_controller.StartPlay(m_context));
|
||||
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play);
|
||||
EXPECT_EQ(startedCount, 1);
|
||||
|
||||
auto* runtimeEntity = m_context.GetSceneManager().GetEntity(editorEntityId);
|
||||
ASSERT_NE(runtimeEntity, nullptr);
|
||||
runtimeEntity->GetTransform()->SetLocalPosition(Math::Vector3(8.0f, 9.0f, 10.0f));
|
||||
auto* runtimeOnlyEntity = m_context.GetSceneManager().CreateEntity("RuntimeOnly");
|
||||
ASSERT_NE(runtimeOnlyEntity, nullptr);
|
||||
const uint64_t runtimeOnlyId = runtimeOnlyEntity->GetID();
|
||||
|
||||
ASSERT_TRUE(m_controller.StopPlay(m_context));
|
||||
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit);
|
||||
EXPECT_EQ(stoppedCount, 1);
|
||||
|
||||
auto* restoredEntity = m_context.GetSceneManager().GetEntity(editorEntityId);
|
||||
ASSERT_NE(restoredEntity, nullptr);
|
||||
EXPECT_EQ(m_context.GetSceneManager().GetEntity(runtimeOnlyId), nullptr);
|
||||
|
||||
const Math::Vector3 restoredPosition = restoredEntity->GetTransform()->GetLocalPosition();
|
||||
EXPECT_NEAR(restoredPosition.x, 1.0f, 1e-4f);
|
||||
EXPECT_NEAR(restoredPosition.y, 2.0f, 1e-4f);
|
||||
EXPECT_NEAR(restoredPosition.z, 3.0f, 1e-4f);
|
||||
|
||||
m_context.GetEventBus().Unsubscribe<PlayModeStartedEvent>(startedSubscription);
|
||||
m_context.GetEventBus().Unsubscribe<PlayModeStoppedEvent>(stoppedSubscription);
|
||||
}
|
||||
|
||||
TEST_F(PlaySessionControllerTest, StartAndStopRequestsRouteThroughEventBus) {
|
||||
auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent");
|
||||
ASSERT_NE(editorEntity, nullptr);
|
||||
|
||||
m_controller.Attach(m_context);
|
||||
|
||||
m_context.GetEventBus().Publish(PlayModeStartRequestedEvent{});
|
||||
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play);
|
||||
|
||||
m_context.GetEventBus().Publish(PlayModeStopRequestedEvent{});
|
||||
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit);
|
||||
|
||||
m_controller.Detach(m_context);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor
|
||||
Reference in New Issue
Block a user