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/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
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
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) {
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
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() {
|
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();
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
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
|
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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
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