feat: add play / editor mode
This commit is contained in:
@@ -7,7 +7,10 @@
|
||||
|
||||
#include <destrum/FS/AssetFS.h>
|
||||
#include <destrum/Graphics/Resources/FontCache.h>
|
||||
#include <destrum/ObjectModel/GameObject.h>
|
||||
#include <destrum/Scene/Scene.h>
|
||||
#include <destrum/Scene/SceneManager.h>
|
||||
#include <destrum/Serialization/SceneSerializer.h>
|
||||
#include <destrum/Util/DeltaTime.h>
|
||||
|
||||
#include "imgui.h"
|
||||
@@ -67,6 +70,8 @@ void App::init(const AppParams& params)
|
||||
|
||||
customInitStarted = true;
|
||||
customInit();
|
||||
playing = !params.startInEditMode;
|
||||
SceneManager::GetInstance().SetPlaying(playing);
|
||||
cleanedUp = false;
|
||||
} catch (...) {
|
||||
try {
|
||||
@@ -179,6 +184,11 @@ if (!isRunning) break;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F5))
|
||||
{
|
||||
playModeToggleRequested = true;
|
||||
}
|
||||
|
||||
// Consume SDL events before updating the base camera so held and
|
||||
// newly pressed inputs are applied in the same frame.
|
||||
camera.Update(dt);
|
||||
@@ -239,6 +249,14 @@ if (!isRunning) break;
|
||||
imguiPass.endFrame();
|
||||
}
|
||||
|
||||
if (playModeToggleRequested)
|
||||
{
|
||||
ZoneScopedN("Play Mode Toggle");
|
||||
playModeToggleRequested = false;
|
||||
applyPlayModeToggle();
|
||||
accumulator = 0.0f;
|
||||
}
|
||||
|
||||
{
|
||||
ZoneScopedN("FixedUpdate");
|
||||
|
||||
@@ -406,9 +424,111 @@ void App::drawWindowsMenu()
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
drawPlayControls();
|
||||
|
||||
ImGui::EndMainMenuBar();
|
||||
}
|
||||
|
||||
void App::drawPlayControls()
|
||||
{
|
||||
const char* state = playing ? "PLAYING" : "EDIT";
|
||||
const char* action = playing ? "Stop (F5)" : "Play (F5)";
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float width = ImGui::CalcTextSize(state).x + ImGui::CalcTextSize(action).x +
|
||||
style.ItemSpacing.x * 2.0f + style.FramePadding.x * 2.0f;
|
||||
|
||||
// Centered like Godot's run buttons, unless the menus already reach past the middle
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), (ImGui::GetWindowWidth() - width) * 0.5f));
|
||||
ImGui::TextColored(playing ? ImVec4{0.4f, 0.9f, 0.4f, 1.0f} : ImVec4{0.7f, 0.7f, 0.7f, 1.0f}, "%s", state);
|
||||
if (ImGui::MenuItem(action))
|
||||
{
|
||||
playModeToggleRequested = true;
|
||||
}
|
||||
}
|
||||
|
||||
void App::applyPlayModeToggle()
|
||||
{
|
||||
if (playing)
|
||||
{
|
||||
exitPlayMode();
|
||||
}
|
||||
else
|
||||
{
|
||||
enterPlayMode();
|
||||
}
|
||||
}
|
||||
|
||||
bool App::enterPlayMode()
|
||||
{
|
||||
auto& sceneManager = SceneManager::GetInstance();
|
||||
if (sceneManager.GetActiveScenes().empty())
|
||||
{
|
||||
spdlog::error("Cannot enter play mode without an active scene");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::shared_ptr<Scene> scene = sceneManager.GetActiveScenes().front();
|
||||
auto snapshot = SceneSerializer::Capture(*scene);
|
||||
if (!snapshot)
|
||||
{
|
||||
spdlog::error("Cannot enter play mode, scene \"{}\" could not be saved", scene->GetName());
|
||||
return false;
|
||||
}
|
||||
if (const GameObject* selected = scene->GetSelectedObject())
|
||||
{
|
||||
(*snapshot)["selected"] = selected->GetId();
|
||||
}
|
||||
|
||||
// Play runs the scene as it would load from disk, so nothing the editor did to the live objects
|
||||
// (started components, unsaved runtime state) leaks into the game
|
||||
if (!SceneSerializer::LoadFromJson(*scene, *snapshot))
|
||||
{
|
||||
spdlog::error("Cannot enter play mode, scene \"{}\" did not load back", scene->GetName());
|
||||
return false;
|
||||
}
|
||||
|
||||
editSnapshot = std::move(snapshot);
|
||||
editScene = scene;
|
||||
playing = true;
|
||||
sceneManager.SetPlaying(true);
|
||||
spdlog::info("Entered play mode");
|
||||
onPlayModeChanged(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
void App::exitPlayMode()
|
||||
{
|
||||
auto& sceneManager = SceneManager::GetInstance();
|
||||
playing = false;
|
||||
sceneManager.SetPlaying(false);
|
||||
InputManager::GetInstance().SetMouseCaptured(false);
|
||||
|
||||
const std::shared_ptr<Scene> scene = editScene.lock();
|
||||
if (scene && editSnapshot)
|
||||
{
|
||||
const auto& scenes = sceneManager.GetScenes();
|
||||
const auto it = std::find(scenes.begin(), scenes.end(), scene);
|
||||
if (it != scenes.end() && (sceneManager.GetActiveScenes().empty() ||
|
||||
sceneManager.GetActiveScenes().front() != scene))
|
||||
{
|
||||
sceneManager.SwitchScene(static_cast<int>(std::distance(scenes.begin(), it)));
|
||||
}
|
||||
if (!SceneSerializer::LoadFromJson(*scene, *editSnapshot))
|
||||
{
|
||||
spdlog::error("Could not restore scene \"{}\" to its state before play mode", scene->GetName());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
spdlog::warn("The scene play mode started from no longer exists, nothing to restore");
|
||||
}
|
||||
|
||||
editSnapshot.reset();
|
||||
editScene.reset();
|
||||
spdlog::info("Exited play mode");
|
||||
onPlayModeChanged(false);
|
||||
}
|
||||
|
||||
void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator)
|
||||
{
|
||||
if (!ImGui::Begin("Physics", &m_DebugWindows.physics))
|
||||
|
||||
@@ -16,17 +16,27 @@ Scene& SceneManager::GetCurrentScene() const {
|
||||
|
||||
void SceneManager::Update(float dt) {
|
||||
for (const auto& scene : m_activeScenes) {
|
||||
scene->Update(dt);
|
||||
if (m_playing) {
|
||||
scene->Update(dt);
|
||||
} else {
|
||||
scene->CommitPendingAdditions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneManager::FixedUpdate(float dt) {
|
||||
if (!m_playing) {
|
||||
return;
|
||||
}
|
||||
for (const auto& scene : m_activeScenes) {
|
||||
scene->FixedUpdate(dt);
|
||||
}
|
||||
}
|
||||
|
||||
void SceneManager::LateUpdate(float dt) {
|
||||
if (!m_playing) {
|
||||
return;
|
||||
}
|
||||
for (const auto& scene : m_activeScenes) {
|
||||
scene->LateUpdate(dt);
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ namespace MaterialSerialization {
|
||||
return materials;
|
||||
}
|
||||
|
||||
void RegisterEmbedded(const json& materials) {
|
||||
void RegisterEmbedded(const json& materials, bool overwriteExisting) {
|
||||
RenderResources* resources = GetResources();
|
||||
if (resources == nullptr || !materials.is_object()) {
|
||||
return;
|
||||
@@ -197,12 +197,14 @@ namespace MaterialSerialization {
|
||||
}
|
||||
};
|
||||
|
||||
// A key that is already cached wins, so duplicating objects keeps sharing their material
|
||||
for (auto it = materials.begin(); it != materials.end(); ++it) {
|
||||
if (resources->materials().findMaterialByKey(it.key())) {
|
||||
continue;
|
||||
const auto existing = resources->materials().findMaterialByKey(it.key());
|
||||
if (!existing) {
|
||||
resources->materials().addMaterial(FromJson(it.value(), readTexture), it.key());
|
||||
} else if (overwriteExisting) {
|
||||
resources->materials().getMaterialMutable(*existing) = FromJson(it.value(), readTexture);
|
||||
GameState::GetInstance().Renderer().updateMaterialGPU(*existing);
|
||||
}
|
||||
resources->materials().addMaterial(FromJson(it.value(), readTexture), it.key());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,10 @@ namespace {
|
||||
return "\"currentCamera\" must be an object id";
|
||||
}
|
||||
|
||||
if (root.contains("selected") && !root.at("selected").is_number_unsigned()) {
|
||||
return "\"selected\" must be an object id";
|
||||
}
|
||||
|
||||
if (root.contains("materials")) {
|
||||
if (auto error = MaterialSerialization::Validate(root.at("materials"))) {
|
||||
return error;
|
||||
@@ -60,10 +64,10 @@ namespace {
|
||||
}
|
||||
}
|
||||
|
||||
bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
std::optional<json> SceneSerializer::Capture(Scene& scene) {
|
||||
if (scene.IsIterating()) {
|
||||
spdlog::error("Cannot save a scene during an update or render phase: {}", scene.GetName());
|
||||
return false;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
RegisterEngineComponents();
|
||||
@@ -71,7 +75,7 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
scene.CommitPendingAdditions();
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Failed to commit scene additions before save: {}", exception.what());
|
||||
return false;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
json root;
|
||||
@@ -106,23 +110,31 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
}
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Cannot save scene \"{}\": {}", scene.GetName(), exception.what());
|
||||
return false;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (auto bad = ObjectSerialization::FindNonFinite(root, "scene")) {
|
||||
spdlog::error("Cannot save scene \"{}\": {} is NaN or Inf", scene.GetName(), *bad);
|
||||
return false;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Refuse to write anything Load would reject
|
||||
if (auto error = ValidateSceneJson(root)) {
|
||||
spdlog::error("Cannot save scene \"{}\", it would not load back: {}", scene.GetName(), *error);
|
||||
return std::nullopt;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
||||
const auto root = Capture(scene);
|
||||
if (!root) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string contents;
|
||||
try {
|
||||
contents = root.dump(4);
|
||||
contents = root->dump(4);
|
||||
} catch (const std::exception& exception) {
|
||||
spdlog::error("Failed to encode scene \"{}\": {}", scene.GetName(), exception.what());
|
||||
return false;
|
||||
@@ -195,7 +207,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
try {
|
||||
if (root.contains("materials")) {
|
||||
context = "materials";
|
||||
MaterialSerialization::RegisterEmbedded(root.at("materials"));
|
||||
MaterialSerialization::RegisterEmbedded(root.at("materials"), true);
|
||||
context = "scene";
|
||||
}
|
||||
idMap = ObjectSerialization::BuildObjects(stagingScene, root.at("objects")).idMap;
|
||||
@@ -247,6 +259,11 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
||||
scene.m_name = root.at("name").get<std::string>();
|
||||
}
|
||||
scene.m_SelectedObjectId = InvalidObjectId;
|
||||
if (const ObjectId selectedFileId = root.value("selected", InvalidObjectId); selectedFileId != InvalidObjectId) {
|
||||
if (const auto it = idMap.find(selectedFileId); it != idMap.end()) {
|
||||
scene.m_SelectedObjectId = it->second->GetId();
|
||||
}
|
||||
}
|
||||
scene.m_CurrentCameraId = InvalidObjectId;
|
||||
if (const ObjectId cameraFileId = root.value("currentCamera", InvalidObjectId); cameraFileId != InvalidObjectId) {
|
||||
if (const auto it = idMap.find(cameraFileId); it != idMap.end()) {
|
||||
@@ -313,6 +330,18 @@ bool SceneSerializer::Load(Scene& scene, const std::filesystem::path& path,
|
||||
return ConstructScene(scene, result, std::move(progressCallback));
|
||||
}
|
||||
|
||||
bool SceneSerializer::LoadFromJson(Scene& scene, const json& root) {
|
||||
RegisterEngineComponents();
|
||||
LoadResult result;
|
||||
if (auto error = ValidateSceneJson(root)) {
|
||||
spdlog::error("Invalid scene data for \"{}\": {}", scene.GetName(), *error);
|
||||
return false;
|
||||
}
|
||||
result.root = root;
|
||||
result.success = true;
|
||||
return ConstructScene(scene, result);
|
||||
}
|
||||
|
||||
std::future<SceneSerializer::LoadResult> SceneSerializer::LoadSceneFileAsync(
|
||||
const std::filesystem::path& path) {
|
||||
return std::async(std::launch::async, [path]() {
|
||||
|
||||
Reference in New Issue
Block a user