feat: add runtime play tick and play-mode scene editing semantics

This commit is contained in:
2026-04-02 19:37:35 +08:00
parent e30f5d5ffa
commit fb15d60be9
28 changed files with 2016 additions and 45 deletions

View 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` 完整并入正式状态机

File diff suppressed because it is too large Load Diff

View File

@@ -65,6 +65,7 @@ add_executable(${PROJECT_NAME} WIN32
src/Application.cpp src/Application.cpp
src/Theme.cpp src/Theme.cpp
src/Core/UndoManager.cpp src/Core/UndoManager.cpp
src/Core/PlaySessionController.cpp
src/ComponentEditors/ComponentEditorRegistry.cpp src/ComponentEditors/ComponentEditorRegistry.cpp
src/Managers/SceneManager.cpp src/Managers/SceneManager.cpp
src/Managers/ProjectManager.cpp src/Managers/ProjectManager.cpp
@@ -78,7 +79,9 @@ add_executable(${PROJECT_NAME} WIN32
src/Viewport/SceneViewportRotateGizmo.cpp src/Viewport/SceneViewportRotateGizmo.cpp
src/Viewport/SceneViewportScaleGizmo.cpp src/Viewport/SceneViewportScaleGizmo.cpp
src/Viewport/SceneViewportOrientationGizmo.cpp src/Viewport/SceneViewportOrientationGizmo.cpp
src/Viewport/SceneViewportOverlayBuilder.cpp
src/Viewport/SceneViewportOverlayRenderer.cpp src/Viewport/SceneViewportOverlayRenderer.cpp
src/Viewport/Passes/SceneViewportEditorOverlayPass.cpp
src/panels/GameViewPanel.cpp src/panels/GameViewPanel.cpp
src/panels/InspectorPanel.cpp src/panels/InspectorPanel.cpp
src/panels/ConsolePanel.cpp src/panels/ConsolePanel.cpp

View File

@@ -16,48 +16,60 @@ namespace XCEngine {
namespace Editor { namespace Editor {
namespace Actions { namespace Actions {
inline ActionBinding MakeNewProjectAction() { inline ActionBinding MakeNewProjectAction(bool enabled = true) {
return MakeAction("New Project..."); return MakeAction("New Project...", nullptr, false, enabled);
} }
inline ActionBinding MakeOpenProjectAction() { inline ActionBinding MakeOpenProjectAction(bool enabled = true) {
return MakeAction("Open Project..."); return MakeAction("Open Project...", nullptr, false, enabled);
} }
inline ActionBinding MakeSaveProjectAction() { inline ActionBinding MakeSaveProjectAction(bool enabled = true) {
return MakeAction("Save Project"); return MakeAction("Save Project", nullptr, false, enabled);
} }
inline ActionBinding MakeNewSceneAction() { inline ActionBinding MakeNewSceneAction(bool enabled = true) {
return MakeAction("New Scene", "Ctrl+N", false, true, true, Shortcut(ImGuiKey_N, true)); return MakeAction("New Scene", "Ctrl+N", false, enabled, true, Shortcut(ImGuiKey_N, true));
} }
inline ActionBinding MakeOpenSceneAction() { inline ActionBinding MakeOpenSceneAction(bool enabled = true) {
return MakeAction("Open Scene", "Ctrl+O", false, true, true, Shortcut(ImGuiKey_O, true)); return MakeAction("Open Scene", "Ctrl+O", false, enabled, true, Shortcut(ImGuiKey_O, true));
} }
inline ActionBinding MakeSaveSceneAction() { inline ActionBinding MakeSaveSceneAction(bool enabled = true) {
return MakeAction("Save Scene", "Ctrl+S", false, true, true, Shortcut(ImGuiKey_S, true)); return MakeAction("Save Scene", "Ctrl+S", false, enabled, true, Shortcut(ImGuiKey_S, true));
} }
inline ActionBinding MakeSaveSceneAsAction() { inline ActionBinding MakeSaveSceneAsAction(bool enabled = true) {
return MakeAction("Save Scene As...", "Ctrl+Shift+S", false, true, true, Shortcut(ImGuiKey_S, true, true)); return MakeAction(
"Save Scene As...",
"Ctrl+Shift+S",
false,
enabled,
true,
Shortcut(ImGuiKey_S, true, true));
} }
inline ActionBinding MakeUndoAction(IEditorContext& context) { inline ActionBinding MakeUndoAction(IEditorContext& context) {
auto& undoManager = context.GetUndoManager(); auto& undoManager = context.GetUndoManager();
const bool enabled =
IsEditorSceneUndoRedoAllowed(context.GetRuntimeMode()) &&
undoManager.CanUndo();
const std::string label = undoManager.CanUndo() ? "Undo " + undoManager.GetUndoLabel() : "Undo"; 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) { inline ActionBinding MakeRedoAction(IEditorContext& context) {
auto& undoManager = context.GetUndoManager(); auto& undoManager = context.GetUndoManager();
const bool enabled =
IsEditorSceneUndoRedoAllowed(context.GetRuntimeMode()) &&
undoManager.CanRedo();
const std::string label = undoManager.CanRedo() ? "Redo " + undoManager.GetRedoLabel() : "Redo"; const std::string label = undoManager.CanRedo() ? "Redo " + undoManager.GetRedoLabel() : "Redo";
return MakeAction( return MakeAction(
label, label,
"Ctrl+Y", "Ctrl+Y",
false, false,
undoManager.CanRedo(), enabled,
false, false,
Shortcut(ImGuiKey_Y, true), Shortcut(ImGuiKey_Y, true),
Shortcut(ImGuiKey_Z, true, true)); Shortcut(ImGuiKey_Z, true, true));
@@ -125,10 +137,22 @@ inline ActionBinding MakeCreateSphereEntityAction() {
return MakeAction("Sphere"); return MakeAction("Sphere");
} }
inline ActionBinding MakeCreateCapsuleEntityAction() {
return MakeAction("Capsule");
}
inline ActionBinding MakeCreateCylinderEntityAction() {
return MakeAction("Cylinder");
}
inline ActionBinding MakeCreatePlaneEntityAction() { inline ActionBinding MakeCreatePlaneEntityAction() {
return MakeAction("Plane"); return MakeAction("Plane");
} }
inline ActionBinding MakeCreateQuadEntityAction() {
return MakeAction("Quad");
}
inline ActionBinding MakeResetLayoutAction() { inline ActionBinding MakeResetLayoutAction() {
return MakeAction("Reset Layout"); return MakeAction("Reset Layout");
} }
@@ -141,6 +165,17 @@ inline ActionBinding MakeExitAction() {
return MakeAction("Exit", "Alt+F4"); 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) { inline ActionBinding MakeNavigateBackAction(bool enabled) {
return MakeAction("<", "Alt+Left", false, enabled, false, Shortcut(ImGuiKey_LeftArrow, false, false, true)); return MakeAction("<", "Alt+Left", false, enabled, false, Shortcut(ImGuiKey_LeftArrow, false, false, true));
} }

View File

@@ -14,6 +14,10 @@ namespace XCEngine {
namespace Editor { namespace Editor {
namespace Actions { namespace Actions {
inline bool IsDocumentEditingAllowed(const IEditorContext& context) {
return IsEditorDocumentEditingAllowed(context.GetRuntimeMode());
}
inline void ExecuteNewScene(IEditorContext& context) { inline void ExecuteNewScene(IEditorContext& context) {
Commands::NewScene(context); Commands::NewScene(context);
} }
@@ -58,33 +62,53 @@ inline void RequestDockLayoutReset(IEditorContext& context) {
context.GetEventBus().Publish(DockLayoutResetRequestedEvent{}); 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) { inline void RequestAboutPopup(UI::DeferredPopupState& aboutPopup) {
aboutPopup.RequestOpen(); aboutPopup.RequestOpen();
} }
inline void HandleMainMenuShortcuts(IEditorContext& context, const ShortcutContext& shortcutContext) { inline void HandleMainMenuShortcuts(IEditorContext& context, const ShortcutContext& shortcutContext) {
HandleShortcut(MakeNewSceneAction(), shortcutContext, [&]() { ExecuteNewScene(context); }); const bool canEditDocuments = IsDocumentEditingAllowed(context);
HandleShortcut(MakeOpenSceneAction(), shortcutContext, [&]() { ExecuteOpenScene(context); }); HandleShortcut(MakeTogglePlayModeAction(context.GetRuntimeMode()), shortcutContext, [&]() {
HandleShortcut(MakeSaveSceneAction(), shortcutContext, [&]() { ExecuteSaveScene(context); }); RequestTogglePlayMode(context);
HandleShortcut(MakeSaveSceneAsAction(), shortcutContext, [&]() { ExecuteSaveSceneAs(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(MakeUndoAction(context), shortcutContext, [&]() { ExecuteUndo(context); });
HandleShortcut(MakeRedoAction(context), shortcutContext, [&]() { ExecuteRedo(context); }); HandleShortcut(MakeRedoAction(context), shortcutContext, [&]() { ExecuteRedo(context); });
HandleEditShortcuts(context, shortcutContext); HandleEditShortcuts(context, shortcutContext);
} }
inline void DrawFileMenuActions(IEditorContext& context) { inline void DrawFileMenuActions(IEditorContext& context) {
DrawMenuAction(MakeNewProjectAction(), [&]() { ExecuteNewProject(context); }); const bool canEditDocuments = IsDocumentEditingAllowed(context);
DrawMenuAction(MakeOpenProjectAction(), [&]() { ExecuteOpenProject(context); }); DrawMenuAction(MakeNewProjectAction(canEditDocuments), [&]() { ExecuteNewProject(context); });
DrawMenuAction(MakeSaveProjectAction(), [&]() { ExecuteSaveProject(context); }); DrawMenuAction(MakeOpenProjectAction(canEditDocuments), [&]() { ExecuteOpenProject(context); });
DrawMenuAction(MakeSaveProjectAction(canEditDocuments), [&]() { ExecuteSaveProject(context); });
DrawMenuSeparator(); DrawMenuSeparator();
DrawMenuAction(MakeNewSceneAction(), [&]() { ExecuteNewScene(context); }); DrawMenuAction(MakeNewSceneAction(canEditDocuments), [&]() { ExecuteNewScene(context); });
DrawMenuAction(MakeOpenSceneAction(), [&]() { ExecuteOpenScene(context); }); DrawMenuAction(MakeOpenSceneAction(canEditDocuments), [&]() { ExecuteOpenScene(context); });
DrawMenuAction(MakeSaveSceneAction(), [&]() { ExecuteSaveScene(context); }); DrawMenuAction(MakeSaveSceneAction(canEditDocuments), [&]() { ExecuteSaveScene(context); });
DrawMenuAction(MakeSaveSceneAsAction(), [&]() { ExecuteSaveSceneAs(context); }); DrawMenuAction(MakeSaveSceneAsAction(canEditDocuments), [&]() { ExecuteSaveSceneAs(context); });
DrawMenuSeparator(); DrawMenuSeparator();
DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); }); DrawMenuAction(MakeExitAction(), [&]() { RequestEditorExit(context); });
} }
inline void DrawRunMenuActions(IEditorContext& context) {
DrawMenuAction(MakeTogglePlayModeAction(context.GetRuntimeMode()), [&]() {
RequestTogglePlayMode(context);
});
}
inline void DrawViewMenuActions(IEditorContext& context) { inline void DrawViewMenuActions(IEditorContext& context) {
DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); }); DrawMenuAction(MakeResetLayoutAction(), [&]() { RequestDockLayoutReset(context); });
} }
@@ -108,6 +132,9 @@ inline void DrawMainMenuBar(IEditorContext& context, UI::DeferredPopupState& abo
UI::DrawMenuScope("Edit", [&]() { UI::DrawMenuScope("Edit", [&]() {
DrawEditActions(context); DrawEditActions(context);
}); });
UI::DrawMenuScope("Run", [&]() {
DrawRunMenuActions(context);
});
UI::DrawMenuScope("View", [&]() { UI::DrawMenuScope("View", [&]() {
DrawViewMenuActions(context); DrawViewMenuActions(context);
}); });

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "Application.h" #include "Application.h"
#include "Platform/Win32Utf8.h"
#include "Core/AssetItem.h" #include "Core/AssetItem.h"
#include "Core/IEditorContext.h" #include "Core/IEditorContext.h"
#include "Core/IProjectManager.h" #include "Core/IProjectManager.h"
@@ -14,12 +15,17 @@
#include <algorithm> #include <algorithm>
#include <cwctype> #include <cwctype>
#include <filesystem> #include <filesystem>
#include <fstream>
#include <string> #include <string>
namespace XCEngine { namespace XCEngine {
namespace Editor { namespace Editor {
namespace Commands { namespace Commands {
inline bool IsProjectDocumentEditingAllowed(const IEditorContext& context) {
return IsEditorDocumentEditingAllowed(context.GetRuntimeMode());
}
namespace detail { namespace detail {
inline std::wstring MakeProjectPathKey(const std::filesystem::path& path) { 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); 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) { inline bool DeleteAsset(IProjectManager& projectManager, const std::string& fullPath) {
if (fullPath.empty()) { if (fullPath.empty()) {
return false; return false;
@@ -204,6 +297,10 @@ inline bool SaveProjectDescriptor(IEditorContext& context) {
} }
inline bool SaveProject(IEditorContext& context) { inline bool SaveProject(IEditorContext& context) {
if (!IsProjectDocumentEditingAllowed(context)) {
return false;
}
if (!EnsureProjectStructure(context.GetProjectPath())) { if (!EnsureProjectStructure(context.GetProjectPath())) {
return false; return false;
} }
@@ -218,6 +315,10 @@ inline bool SaveProject(IEditorContext& context) {
} }
inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) { inline bool SwitchProject(IEditorContext& context, const std::string& projectPath) {
if (!IsProjectDocumentEditingAllowed(context)) {
return false;
}
if (projectPath.empty()) { if (projectPath.empty()) {
return false; return false;
} }
@@ -249,6 +350,10 @@ inline bool SwitchProject(IEditorContext& context, const std::string& projectPat
} }
inline bool NewProjectWithDialog(IEditorContext& context) { inline bool NewProjectWithDialog(IEditorContext& context) {
if (!IsProjectDocumentEditingAllowed(context)) {
return false;
}
const std::string projectPath = FileDialogUtils::PickFolderDialog( const std::string projectPath = FileDialogUtils::PickFolderDialog(
L"Select New Project Folder", L"Select New Project Folder",
context.GetProjectPath()); context.GetProjectPath());
@@ -260,6 +365,10 @@ inline bool NewProjectWithDialog(IEditorContext& context) {
} }
inline bool OpenProjectWithDialog(IEditorContext& context) { inline bool OpenProjectWithDialog(IEditorContext& context) {
if (!IsProjectDocumentEditingAllowed(context)) {
return false;
}
const std::string projectPath = FileDialogUtils::PickFolderDialog( const std::string projectPath = FileDialogUtils::PickFolderDialog(
L"Open Project Folder", L"Open Project Folder",
context.GetProjectPath()); context.GetProjectPath());

View File

@@ -13,12 +13,20 @@ namespace XCEngine {
namespace Editor { namespace Editor {
namespace Commands { namespace Commands {
inline bool IsSceneDocumentEditingAllowed(const IEditorContext& context) {
return IsEditorDocumentEditingAllowed(context.GetRuntimeMode());
}
inline void ResetSceneEditingState(IEditorContext& context) { inline void ResetSceneEditingState(IEditorContext& context) {
context.GetSelectionManager().ClearSelection(); context.GetSelectionManager().ClearSelection();
context.GetUndoManager().ClearHistory(); context.GetUndoManager().ClearHistory();
} }
inline bool NewScene(IEditorContext& context, const std::string& sceneName = "Untitled Scene") { inline bool NewScene(IEditorContext& context, const std::string& sceneName = "Untitled Scene") {
if (!IsSceneDocumentEditingAllowed(context)) {
return false;
}
if (!SceneEditorUtils::ConfirmSceneSwitch(context)) { if (!SceneEditorUtils::ConfirmSceneSwitch(context)) {
return false; 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) { inline bool LoadScene(IEditorContext& context, const std::string& filePath, bool confirmSwitch = true) {
if (!IsSceneDocumentEditingAllowed(context)) {
return false;
}
if (filePath.empty()) { if (filePath.empty()) {
return false; return false;
} }
@@ -44,6 +56,10 @@ inline bool LoadScene(IEditorContext& context, const std::string& filePath, bool
} }
inline bool OpenSceneWithDialog(IEditorContext& context) { inline bool OpenSceneWithDialog(IEditorContext& context) {
if (!IsSceneDocumentEditingAllowed(context)) {
return false;
}
if (!SceneEditorUtils::ConfirmSceneSwitch(context)) { if (!SceneEditorUtils::ConfirmSceneSwitch(context)) {
return false; return false;
} }
@@ -59,10 +75,18 @@ inline bool OpenSceneWithDialog(IEditorContext& context) {
} }
inline bool SaveCurrentScene(IEditorContext& context) { inline bool SaveCurrentScene(IEditorContext& context) {
if (!IsSceneDocumentEditingAllowed(context)) {
return false;
}
return SceneEditorUtils::SaveCurrentScene(context); return SceneEditorUtils::SaveCurrentScene(context);
} }
inline bool SaveSceneAsWithDialog(IEditorContext& context) { inline bool SaveSceneAsWithDialog(IEditorContext& context) {
if (!IsSceneDocumentEditingAllowed(context)) {
return false;
}
auto& sceneManager = context.GetSceneManager(); auto& sceneManager = context.GetSceneManager();
const std::string filePath = SceneEditorUtils::SaveSceneFileDialog( const std::string filePath = SceneEditorUtils::SaveSceneFileDialog(
context.GetProjectPath(), context.GetProjectPath(),
@@ -80,6 +104,10 @@ inline bool SaveSceneAsWithDialog(IEditorContext& context) {
} }
inline bool LoadStartupScene(IEditorContext& context) { inline bool LoadStartupScene(IEditorContext& context) {
if (!IsSceneDocumentEditingAllowed(context)) {
return false;
}
const bool loaded = context.GetSceneManager().LoadStartupScene(context.GetProjectPath()); const bool loaded = context.GetSceneManager().LoadStartupScene(context.GetProjectPath());
context.GetProjectManager().RefreshCurrentFolder(); context.GetProjectManager().RefreshCurrentFolder();
ResetSceneEditingState(context); ResetSceneEditingState(context);
@@ -87,6 +115,10 @@ inline bool LoadStartupScene(IEditorContext& context) {
} }
inline bool SaveDirtySceneWithFallback(IEditorContext& context, const std::string& fallbackPath) { inline bool SaveDirtySceneWithFallback(IEditorContext& context, const std::string& fallbackPath) {
if (!IsSceneDocumentEditingAllowed(context)) {
return false;
}
auto& sceneManager = context.GetSceneManager(); auto& sceneManager = context.GetSceneManager();
if (!sceneManager.HasActiveScene() || !sceneManager.IsSceneDirty()) { if (!sceneManager.HasActiveScene() || !sceneManager.IsSceneDirty()) {
return true; return true;

View File

@@ -73,6 +73,20 @@ public:
return m_activeActionRoute; 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 { void SetProjectPath(const std::string& path) override {
m_projectPath = path; m_projectPath = path;
} }
@@ -89,6 +103,7 @@ private:
std::unique_ptr<ProjectManager> m_projectManager; std::unique_ptr<ProjectManager> m_projectManager;
IViewportHostService* m_viewportHostService = nullptr; IViewportHostService* m_viewportHostService = nullptr;
EditorActionRoute m_activeActionRoute = EditorActionRoute::None; EditorActionRoute m_activeActionRoute = EditorActionRoute::None;
EditorRuntimeMode m_runtimeMode = EditorRuntimeMode::Edit;
std::string m_projectPath; std::string m_projectPath;
uint64_t m_entityDeletedHandlerId; uint64_t m_entityDeletedHandlerId;
}; };

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "EditorRuntimeMode.h"
#include <cstdint> #include <cstdint>
#include <vector> #include <vector>
@@ -41,15 +43,24 @@ struct SceneChangedEvent {
struct PlayModeStartedEvent { struct PlayModeStartedEvent {
}; };
struct PlayModeStartRequestedEvent {
};
struct PlayModeStoppedEvent { struct PlayModeStoppedEvent {
}; };
struct PlayModeStopRequestedEvent {
};
struct PlayModePausedEvent { struct PlayModePausedEvent {
}; };
struct PlayModePauseRequestedEvent {
};
struct EditorModeChangedEvent { struct EditorModeChangedEvent {
int oldMode; EditorRuntimeMode oldMode = EditorRuntimeMode::Edit;
int newMode; EditorRuntimeMode newMode = EditorRuntimeMode::Edit;
}; };
struct DockLayoutResetRequestedEvent { struct DockLayoutResetRequestedEvent {

View 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

View File

@@ -11,6 +11,22 @@ namespace Editor {
inline std::string BuildEditorWindowTitle(IEditorContext& context) { inline std::string BuildEditorWindowTitle(IEditorContext& context) {
auto& sceneManager = context.GetSceneManager(); 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"; std::string sceneName = sceneManager.HasActiveScene() ? sceneManager.GetCurrentSceneName() : "No Scene";
if (sceneName.empty()) { if (sceneName.empty()) {
sceneName = "Untitled Scene"; sceneName = "Untitled Scene";
@@ -26,7 +42,7 @@ inline std::string BuildEditorWindowTitle(IEditorContext& context) {
sceneName += std::filesystem::path(sceneManager.GetCurrentScenePath()).filename().string(); sceneName += std::filesystem::path(sceneManager.GetCurrentScenePath()).filename().string();
} }
return sceneName + " - XCEngine Editor"; return modePrefix + sceneName + " - XCEngine Editor";
} }
} // namespace Editor } // namespace Editor

View File

@@ -2,6 +2,7 @@
#include "Commands/SceneCommands.h" #include "Commands/SceneCommands.h"
#include "Core/IEditorContext.h" #include "Core/IEditorContext.h"
#include "Core/PlaySessionController.h"
#include "Layout/DockLayoutController.h" #include "Layout/DockLayoutController.h"
#include "panels/ConsolePanel.h" #include "panels/ConsolePanel.h"
#include "panels/GameViewPanel.h" #include "panels/GameViewPanel.h"
@@ -37,11 +38,13 @@ public:
m_projectPanel->Initialize(context.GetProjectPath()); m_projectPanel->Initialize(context.GetProjectPath());
Commands::LoadStartupScene(context); Commands::LoadStartupScene(context);
m_playSessionController.Attach(context);
m_dockLayoutController->Attach(context); m_dockLayoutController->Attach(context);
m_panels.AttachAll(); m_panels.AttachAll();
} }
void Detach(IEditorContext& context) { void Detach(IEditorContext& context) {
m_playSessionController.Detach(context);
Commands::SaveDirtySceneWithFallback(context, BuildFallbackScenePath(context)); Commands::SaveDirtySceneWithFallback(context, BuildFallbackScenePath(context));
if (m_dockLayoutController) { if (m_dockLayoutController) {
@@ -56,6 +59,9 @@ public:
void Update(float dt) { void Update(float dt) {
::XCEngine::Resources::ResourceManager::Get().UpdateAsyncLoads(); ::XCEngine::Resources::ResourceManager::Get().UpdateAsyncLoads();
if (IEditorContext* context = m_panels.GetContext()) {
m_playSessionController.Update(*context, dt);
}
m_panels.UpdateAll(dt); m_panels.UpdateAll(dt);
} }
@@ -79,6 +85,7 @@ private:
PanelCollection m_panels; PanelCollection m_panels;
ProjectPanel* m_projectPanel = nullptr; ProjectPanel* m_projectPanel = nullptr;
std::unique_ptr<DockLayoutController> m_dockLayoutController; std::unique_ptr<DockLayoutController> m_dockLayoutController;
PlaySessionController m_playSessionController;
}; };
} // namespace Editor } // namespace Editor

View File

@@ -1,6 +1,7 @@
#pragma once #pragma once
#include "EditorActionRoute.h" #include "EditorActionRoute.h"
#include "EditorRuntimeMode.h"
#include <memory> #include <memory>
#include <string> #include <string>
@@ -27,6 +28,8 @@ public:
virtual IViewportHostService* GetViewportHostService() = 0; virtual IViewportHostService* GetViewportHostService() = 0;
virtual void SetActiveActionRoute(EditorActionRoute route) = 0; virtual void SetActiveActionRoute(EditorActionRoute route) = 0;
virtual EditorActionRoute GetActiveActionRoute() const = 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 void SetProjectPath(const std::string& path) = 0;
virtual const std::string& GetProjectPath() const = 0; virtual const std::string& GetProjectPath() const = 0;

View File

@@ -1,5 +1,7 @@
#pragma once #pragma once
#include "SceneSnapshot.h"
#include <string> #include <string>
#include <vector> #include <vector>
#include <cstdint> #include <cstdint>
@@ -34,10 +36,14 @@ public:
virtual bool HasActiveScene() const = 0; virtual bool HasActiveScene() const = 0;
virtual bool IsSceneDirty() const = 0; virtual bool IsSceneDirty() const = 0;
virtual void MarkSceneDirty() = 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& GetCurrentScenePath() const = 0;
virtual const std::string& GetCurrentSceneName() const = 0; virtual const std::string& GetCurrentSceneName() const = 0;
virtual ::XCEngine::Components::Scene* GetScene() = 0; virtual ::XCEngine::Components::Scene* GetScene() = 0;
virtual const ::XCEngine::Components::Scene* GetScene() const = 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; virtual void CreateDemoScene() = 0;
}; };

View 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

View 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

View File

@@ -32,6 +32,10 @@ void SceneManager::SetSceneDirty(bool dirty) {
} }
void SceneManager::MarkSceneDirty() { void SceneManager::MarkSceneDirty() {
if (!m_sceneDocumentDirtyTrackingEnabled) {
return;
}
SetSceneDirty(true); SetSceneDirty(true);
} }
@@ -43,7 +47,7 @@ void SceneManager::MarkSceneDirty() {
::XCEngine::Components::GameObject* entity = m_scene->CreateGameObject(name, parent); ::XCEngine::Components::GameObject* entity = m_scene->CreateGameObject(name, parent);
const auto entityId = entity->GetID(); const auto entityId = entity->GetID();
SyncRootEntities(); SyncRootEntities();
SetSceneDirty(true); MarkSceneDirty();
OnEntityCreated.Invoke(entityId); OnEntityCreated.Invoke(entityId);
OnSceneChanged.Invoke(); OnSceneChanged.Invoke();
@@ -70,7 +74,7 @@ void SceneManager::DeleteEntity(::XCEngine::Components::GameObject::ID id) {
const auto entityId = entity->GetID(); const auto entityId = entity->GetID();
m_scene->DestroyGameObject(entity); m_scene->DestroyGameObject(entity);
SyncRootEntities(); SyncRootEntities();
SetSceneDirty(true); MarkSceneDirty();
OnEntityDeleted.Invoke(entityId); OnEntityDeleted.Invoke(entityId);
OnSceneChanged.Invoke(); OnSceneChanged.Invoke();
@@ -150,7 +154,7 @@ void SceneManager::CopyEntity(::XCEngine::Components::GameObject::ID id) {
const auto newEntityId = PasteEntityRecursive(*m_clipboard, parent); const auto newEntityId = PasteEntityRecursive(*m_clipboard, parent);
SyncRootEntities(); SyncRootEntities();
SetSceneDirty(true); MarkSceneDirty();
OnEntityCreated.Invoke(newEntityId); OnEntityCreated.Invoke(newEntityId);
OnSceneChanged.Invoke(); OnSceneChanged.Invoke();
@@ -320,7 +324,7 @@ void SceneManager::RenameEntity(::XCEngine::Components::GameObject::ID id, const
if (!obj) return; if (!obj) return;
obj->SetName(newName); obj->SetName(newName);
SetSceneDirty(true); MarkSceneDirty();
OnEntityChanged.Invoke(id); OnEntityChanged.Invoke(id);
if (m_eventBus) { if (m_eventBus) {
@@ -342,7 +346,7 @@ void SceneManager::MoveEntity(::XCEngine::Components::GameObject::ID id, ::XCEng
obj->SetParent(newParent); obj->SetParent(newParent);
SyncRootEntities(); SyncRootEntities();
SetSceneDirty(true); MarkSceneDirty();
OnEntityChanged.Invoke(id); OnEntityChanged.Invoke(id);
OnSceneChanged.Invoke(); OnSceneChanged.Invoke();

View File

@@ -57,15 +57,21 @@ public:
bool HasActiveScene() const override { return m_scene != nullptr; } bool HasActiveScene() const override { return m_scene != nullptr; }
bool IsSceneDirty() const override { return m_isSceneDirty; } bool IsSceneDirty() const override { return m_isSceneDirty; }
void MarkSceneDirty() override; 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& GetCurrentScenePath() const override { return m_currentScenePath; }
const std::string& GetCurrentSceneName() const override { return m_currentSceneName; } const std::string& GetCurrentSceneName() const override { return m_currentSceneName; }
::XCEngine::Components::Scene* GetScene() override { return m_scene.get(); } ::XCEngine::Components::Scene* GetScene() override { return m_scene.get(); }
const ::XCEngine::Components::Scene* GetScene() const 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; void CreateDemoScene() override;
bool HasClipboardData() const { return m_clipboard.has_value(); } 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> OnEntityCreated;
::XCEngine::Core::Event<::XCEngine::Components::GameObject::ID> OnEntityDeleted; ::XCEngine::Core::Event<::XCEngine::Components::GameObject::ID> OnEntityDeleted;
@@ -97,6 +103,7 @@ private:
std::string m_currentScenePath; std::string m_currentScenePath;
std::string m_currentSceneName = "Untitled Scene"; std::string m_currentSceneName = "Untitled Scene";
bool m_isSceneDirty = false; bool m_isSceneDirty = false;
bool m_sceneDocumentDirtyTrackingEnabled = true;
}; };
} }

View File

@@ -511,27 +511,27 @@ bool DrawCompactCheckedMenuItem(const char* label, bool checked) {
return false; return false;
} }
const bool clicked = ImGui::Selectable(label, false, ImGuiSelectableFlags_SpanAvailWidth); const bool clicked = ImGui::MenuItem(label, nullptr, false, true);
if (checked) {
const ImRect rect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()); const ImRect rect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax());
if (checked) {
ImDrawList* drawList = ImGui::GetWindowDrawList(); ImDrawList* drawList = ImGui::GetWindowDrawList();
const ImU32 color = ImGui::GetColorU32(ImGuiCol_CheckMark); const ImU32 color = ImGui::GetColorU32(ImGuiCol_CheckMark);
const float height = rect.Max.y - rect.Min.y; const float height = rect.Max.y - rect.Min.y;
const float checkWidth = height * 0.28f; const float checkWidth = height * 0.24f;
const float checkHeight = height * 0.18f; const float checkHeight = height * 0.16f;
const float x = rect.Max.x - 12.0f; const float x = rect.Max.x - 10.0f;
const float y = rect.Min.y + height * 0.52f; const float y = rect.Min.y + height * 0.52f;
drawList->AddLine( drawList->AddLine(
ImVec2(x - checkWidth, y - checkHeight * 0.15f), ImVec2(x - checkWidth, y - checkHeight * 0.15f),
ImVec2(x - checkWidth * 0.42f, y + checkHeight), ImVec2(x - checkWidth * 0.42f, y + checkHeight),
color, color,
1.4f); 1.8f);
drawList->AddLine( drawList->AddLine(
ImVec2(x - checkWidth * 0.42f, y + checkHeight), ImVec2(x - checkWidth * 0.42f, y + checkHeight),
ImVec2(x + checkWidth, y - checkHeight), ImVec2(x + checkWidth, y - checkHeight),
color, color,
1.4f); 1.8f);
} }
return clicked; return clicked;
@@ -973,7 +973,7 @@ void ConsolePanel::Render() {
} }
if (shouldPause) { if (shouldPause) {
m_context->GetEventBus().Publish(PlayModePausedEvent{}); m_context->GetEventBus().Publish(PlayModePauseRequestedEvent{});
m_playModePaused = true; m_playModePaused = true;
} }
m_lastErrorPauseScanSerial = latestSerial; m_lastErrorPauseScanSerial = latestSerial;

View File

@@ -14,6 +14,10 @@ class IEditorContext;
class PanelCollection { class PanelCollection {
public: public:
IEditorContext* GetContext() const {
return m_context;
}
void SetContext(IEditorContext* context) { void SetContext(IEditorContext* context) {
m_context = context; m_context = context;

View File

@@ -377,9 +377,11 @@ add_library(XCEngine STATIC
# Scene # Scene
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/Scene.h ${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/SceneRuntime.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/RuntimeLoop.h
${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scene/SceneManager.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/Scene.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/Scene.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneRuntime.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 ${CMAKE_CURRENT_SOURCE_DIR}/src/Scene/SceneManager.cpp
# Platform # Platform

View 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

View 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

View File

@@ -5,6 +5,7 @@ project(XCEngine_SceneTests)
set(SCENE_TEST_SOURCES set(SCENE_TEST_SOURCES
test_scene.cpp test_scene.cpp
test_scene_runtime.cpp test_scene_runtime.cpp
test_runtime_loop.cpp
test_scene_manager.cpp test_scene_manager.cpp
) )

View 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

View File

@@ -4,6 +4,7 @@ project(XCEngine_EditorTests)
set(EDITOR_TEST_SOURCES set(EDITOR_TEST_SOURCES
test_action_routing.cpp test_action_routing.cpp
test_play_session_controller.cpp
test_scene_viewport_camera_controller.cpp test_scene_viewport_camera_controller.cpp
test_scene_viewport_move_gizmo.cpp test_scene_viewport_move_gizmo.cpp
test_scene_viewport_rotate_gizmo.cpp test_scene_viewport_rotate_gizmo.cpp
@@ -16,6 +17,7 @@ set(EDITOR_TEST_SOURCES
test_viewport_render_flow_utils.cpp test_viewport_render_flow_utils.cpp
test_builtin_icon_layout_utils.cpp test_builtin_icon_layout_utils.cpp
${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.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/SceneManager.cpp
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp ${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp ${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp
@@ -30,6 +32,7 @@ if(MSVC)
set_target_properties(editor_tests PROPERTIES set_target_properties(editor_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib" LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
) )
target_compile_options(editor_tests PRIVATE /FS)
endif() endif()
target_link_libraries(editor_tests PRIVATE target_link_libraries(editor_tests PRIVATE

View File

@@ -7,6 +7,7 @@
#include "Commands/EntityCommands.h" #include "Commands/EntityCommands.h"
#include "Commands/SceneCommands.h" #include "Commands/SceneCommands.h"
#include "Core/EditorContext.h" #include "Core/EditorContext.h"
#include "Core/PlaySessionController.h"
#include <XCEngine/Core/Math/Quaternion.h> #include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h> #include <XCEngine/Core/Math/Vector3.h>
@@ -283,6 +284,45 @@ TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsExitResetAndAboutPopup) {
m_context.GetEventBus().Unsubscribe<DockLayoutResetRequestedEvent>(resetSubscription); 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) { TEST_F(EditorActionRoutingTest, HierarchyRouterRenameHelpersPublishAndCommit) {
auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "BeforeRename"); auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "BeforeRename");
ASSERT_NE(entity, nullptr); ASSERT_NE(entity, nullptr);

View 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