feat: add play / editor mode
This commit is contained in:
@@ -2,8 +2,12 @@
|
|||||||
#define APP_H
|
#define APP_H
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <memory>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
#include "glm/vec2.hpp"
|
#include "glm/vec2.hpp"
|
||||||
|
|
||||||
#include <SDL2/SDL.h>
|
#include <SDL2/SDL.h>
|
||||||
@@ -17,6 +21,8 @@
|
|||||||
#include <destrum/Input/InputManager.h>
|
#include <destrum/Input/InputManager.h>
|
||||||
|
|
||||||
|
|
||||||
|
class Scene;
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
public:
|
public:
|
||||||
struct AppParams {
|
struct AppParams {
|
||||||
@@ -28,6 +34,7 @@ public:
|
|||||||
bool showDebugUi{true};
|
bool showDebugUi{true};
|
||||||
WindowMode windowMode{WindowMode::Windowed};
|
WindowMode windowMode{WindowMode::Windowed};
|
||||||
bool startMaximized{false};
|
bool startMaximized{false};
|
||||||
|
bool startInEditMode{false};
|
||||||
};
|
};
|
||||||
|
|
||||||
App();
|
App();
|
||||||
@@ -45,7 +52,11 @@ public:
|
|||||||
|
|
||||||
virtual void onWindowResize(int, int) {};
|
virtual void onWindowResize(int, int) {};
|
||||||
|
|
||||||
// Called inside the "Windows" menu of the main menu bar, add toggles for the game's own windows here
|
virtual void onPlayModeChanged(bool) {}
|
||||||
|
|
||||||
|
[[nodiscard]] bool isPlaying() const { return playing; }
|
||||||
|
void requestPlayModeToggle() { playModeToggleRequested = true; }
|
||||||
|
|
||||||
virtual void customWindowsMenu() {}
|
virtual void customWindowsMenu() {}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -92,6 +103,17 @@ protected:
|
|||||||
void drawWindowsMenu();
|
void drawWindowsMenu();
|
||||||
void drawPhysicsPanel(float dt, float fixedDt, float accumulator);
|
void drawPhysicsPanel(float dt, float fixedDt, float accumulator);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void drawPlayControls();
|
||||||
|
void applyPlayModeToggle();
|
||||||
|
bool enterPlayMode();
|
||||||
|
void exitPlayMode();
|
||||||
|
|
||||||
|
bool playing{true};
|
||||||
|
bool playModeToggleRequested{false};
|
||||||
|
std::optional<nlohmann::json> editSnapshot;
|
||||||
|
std::weak_ptr<Scene> editScene;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public:
|
|||||||
Scene& GetCurrentScene() const;
|
Scene& GetCurrentScene() const;
|
||||||
const std::vector<std::shared_ptr<Scene>>& GetActiveScenes() const { return m_activeScenes; }
|
const std::vector<std::shared_ptr<Scene>>& GetActiveScenes() const { return m_activeScenes; }
|
||||||
|
|
||||||
|
void SetPlaying(bool playing) { m_playing = playing; }
|
||||||
|
[[nodiscard]] bool IsPlaying() const { return m_playing; }
|
||||||
|
|
||||||
void Update(float dt);
|
void Update(float dt);
|
||||||
void FixedUpdate(float dt);
|
void FixedUpdate(float dt);
|
||||||
void LateUpdate(float dt);
|
void LateUpdate(float dt);
|
||||||
@@ -52,6 +55,7 @@ private:
|
|||||||
|
|
||||||
std::vector<std::shared_ptr<Scene>> m_activeScenes;
|
std::vector<std::shared_ptr<Scene>> m_activeScenes;
|
||||||
std::vector<std::shared_ptr<Scene>> m_scenes;
|
std::vector<std::shared_ptr<Scene>> m_scenes;
|
||||||
|
bool m_playing{true};
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif //SCENEMANAGER_H
|
#endif //SCENEMANAGER_H
|
||||||
|
|||||||
@@ -18,10 +18,8 @@ namespace MaterialSerialization {
|
|||||||
|
|
||||||
[[nodiscard]] std::optional<std::string> Validate(const nlohmann::json& materials);
|
[[nodiscard]] std::optional<std::string> Validate(const nlohmann::json& materials);
|
||||||
|
|
||||||
// Materials that only exist at runtime (no source file) are stored in the scene or prefab
|
|
||||||
// file under their cache key, so the materialKeys in its objects resolve after a restart
|
|
||||||
[[nodiscard]] nlohmann::json CollectEmbedded(const nlohmann::json& objects);
|
[[nodiscard]] nlohmann::json CollectEmbedded(const nlohmann::json& objects);
|
||||||
void RegisterEmbedded(const nlohmann::json& materials);
|
void RegisterEmbedded(const nlohmann::json& materials, bool overwriteExisting = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif //DESTRUM_MATERIALSERIALIZATION_H
|
#endif //DESTRUM_MATERIALSERIALIZATION_H
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <future>
|
#include <future>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
@@ -17,8 +18,11 @@ public:
|
|||||||
nlohmann::json root;
|
nlohmann::json root;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] static std::optional<nlohmann::json> Capture(Scene& scene);
|
||||||
static bool Save(Scene& scene, const std::filesystem::path& path);
|
static bool Save(Scene& scene, const std::filesystem::path& path);
|
||||||
|
|
||||||
|
static bool LoadFromJson(Scene& scene, const nlohmann::json& root);
|
||||||
|
|
||||||
static bool Load(Scene& scene, const std::filesystem::path& path,
|
static bool Load(Scene& scene, const std::filesystem::path& path,
|
||||||
std::function<void(float)> progressCallback = nullptr);
|
std::function<void(float)> progressCallback = nullptr);
|
||||||
|
|
||||||
@@ -27,8 +31,6 @@ public:
|
|||||||
static bool ConstructScene(Scene& scene, LoadResult& result,
|
static bool ConstructScene(Scene& scene, LoadResult& result,
|
||||||
std::function<void(float)> progressCallback = nullptr);
|
std::function<void(float)> progressCallback = nullptr);
|
||||||
|
|
||||||
// Parse the scene file on a background thread. Call LoadFromJson on the
|
|
||||||
// main thread once the future is ready.
|
|
||||||
static std::future<LoadResult> LoadSceneFileAsync(const std::filesystem::path& path);
|
static std::future<LoadResult> LoadSceneFileAsync(const std::filesystem::path& path);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,10 @@
|
|||||||
|
|
||||||
#include <destrum/FS/AssetFS.h>
|
#include <destrum/FS/AssetFS.h>
|
||||||
#include <destrum/Graphics/Resources/FontCache.h>
|
#include <destrum/Graphics/Resources/FontCache.h>
|
||||||
|
#include <destrum/ObjectModel/GameObject.h>
|
||||||
|
#include <destrum/Scene/Scene.h>
|
||||||
#include <destrum/Scene/SceneManager.h>
|
#include <destrum/Scene/SceneManager.h>
|
||||||
|
#include <destrum/Serialization/SceneSerializer.h>
|
||||||
#include <destrum/Util/DeltaTime.h>
|
#include <destrum/Util/DeltaTime.h>
|
||||||
|
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
@@ -67,6 +70,8 @@ void App::init(const AppParams& params)
|
|||||||
|
|
||||||
customInitStarted = true;
|
customInitStarted = true;
|
||||||
customInit();
|
customInit();
|
||||||
|
playing = !params.startInEditMode;
|
||||||
|
SceneManager::GetInstance().SetPlaying(playing);
|
||||||
cleanedUp = false;
|
cleanedUp = false;
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
try {
|
try {
|
||||||
@@ -179,6 +184,11 @@ if (!isRunning) break;
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F5))
|
||||||
|
{
|
||||||
|
playModeToggleRequested = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Consume SDL events before updating the base camera so held and
|
// Consume SDL events before updating the base camera so held and
|
||||||
// newly pressed inputs are applied in the same frame.
|
// newly pressed inputs are applied in the same frame.
|
||||||
camera.Update(dt);
|
camera.Update(dt);
|
||||||
@@ -239,6 +249,14 @@ if (!isRunning) break;
|
|||||||
imguiPass.endFrame();
|
imguiPass.endFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (playModeToggleRequested)
|
||||||
|
{
|
||||||
|
ZoneScopedN("Play Mode Toggle");
|
||||||
|
playModeToggleRequested = false;
|
||||||
|
applyPlayModeToggle();
|
||||||
|
accumulator = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
ZoneScopedN("FixedUpdate");
|
ZoneScopedN("FixedUpdate");
|
||||||
|
|
||||||
@@ -406,9 +424,111 @@ void App::drawWindowsMenu()
|
|||||||
ImGui::EndMenu();
|
ImGui::EndMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drawPlayControls();
|
||||||
|
|
||||||
ImGui::EndMainMenuBar();
|
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)
|
void App::drawPhysicsPanel(float dt, float fixedDt, float accumulator)
|
||||||
{
|
{
|
||||||
if (!ImGui::Begin("Physics", &m_DebugWindows.physics))
|
if (!ImGui::Begin("Physics", &m_DebugWindows.physics))
|
||||||
|
|||||||
@@ -16,17 +16,27 @@ Scene& SceneManager::GetCurrentScene() const {
|
|||||||
|
|
||||||
void SceneManager::Update(float dt) {
|
void SceneManager::Update(float dt) {
|
||||||
for (const auto& scene : m_activeScenes) {
|
for (const auto& scene : m_activeScenes) {
|
||||||
|
if (m_playing) {
|
||||||
scene->Update(dt);
|
scene->Update(dt);
|
||||||
|
} else {
|
||||||
|
scene->CommitPendingAdditions();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SceneManager::FixedUpdate(float dt) {
|
void SceneManager::FixedUpdate(float dt) {
|
||||||
|
if (!m_playing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const auto& scene : m_activeScenes) {
|
for (const auto& scene : m_activeScenes) {
|
||||||
scene->FixedUpdate(dt);
|
scene->FixedUpdate(dt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void SceneManager::LateUpdate(float dt) {
|
void SceneManager::LateUpdate(float dt) {
|
||||||
|
if (!m_playing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const auto& scene : m_activeScenes) {
|
for (const auto& scene : m_activeScenes) {
|
||||||
scene->LateUpdate(dt);
|
scene->LateUpdate(dt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ namespace MaterialSerialization {
|
|||||||
return materials;
|
return materials;
|
||||||
}
|
}
|
||||||
|
|
||||||
void RegisterEmbedded(const json& materials) {
|
void RegisterEmbedded(const json& materials, bool overwriteExisting) {
|
||||||
RenderResources* resources = GetResources();
|
RenderResources* resources = GetResources();
|
||||||
if (resources == nullptr || !materials.is_object()) {
|
if (resources == nullptr || !materials.is_object()) {
|
||||||
return;
|
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) {
|
for (auto it = materials.begin(); it != materials.end(); ++it) {
|
||||||
if (resources->materials().findMaterialByKey(it.key())) {
|
const auto existing = resources->materials().findMaterialByKey(it.key());
|
||||||
continue;
|
if (!existing) {
|
||||||
}
|
|
||||||
resources->materials().addMaterial(FromJson(it.value(), readTexture), it.key());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ namespace {
|
|||||||
return "\"currentCamera\" must be an object id";
|
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 (root.contains("materials")) {
|
||||||
if (auto error = MaterialSerialization::Validate(root.at("materials"))) {
|
if (auto error = MaterialSerialization::Validate(root.at("materials"))) {
|
||||||
return error;
|
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()) {
|
if (scene.IsIterating()) {
|
||||||
spdlog::error("Cannot save a scene during an update or render phase: {}", scene.GetName());
|
spdlog::error("Cannot save a scene during an update or render phase: {}", scene.GetName());
|
||||||
return false;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterEngineComponents();
|
RegisterEngineComponents();
|
||||||
@@ -71,7 +75,7 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
scene.CommitPendingAdditions();
|
scene.CommitPendingAdditions();
|
||||||
} catch (const std::exception& exception) {
|
} catch (const std::exception& exception) {
|
||||||
spdlog::error("Failed to commit scene additions before save: {}", exception.what());
|
spdlog::error("Failed to commit scene additions before save: {}", exception.what());
|
||||||
return false;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
json root;
|
json root;
|
||||||
@@ -106,23 +110,31 @@ bool SceneSerializer::Save(Scene& scene, const std::filesystem::path& path) {
|
|||||||
}
|
}
|
||||||
} catch (const std::exception& exception) {
|
} catch (const std::exception& exception) {
|
||||||
spdlog::error("Cannot save scene \"{}\": {}", scene.GetName(), exception.what());
|
spdlog::error("Cannot save scene \"{}\": {}", scene.GetName(), exception.what());
|
||||||
return false;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (auto bad = ObjectSerialization::FindNonFinite(root, "scene")) {
|
if (auto bad = ObjectSerialization::FindNonFinite(root, "scene")) {
|
||||||
spdlog::error("Cannot save scene \"{}\": {} is NaN or Inf", scene.GetName(), *bad);
|
spdlog::error("Cannot save scene \"{}\": {} is NaN or Inf", scene.GetName(), *bad);
|
||||||
return false;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refuse to write anything Load would reject
|
// Refuse to write anything Load would reject
|
||||||
if (auto error = ValidateSceneJson(root)) {
|
if (auto error = ValidateSceneJson(root)) {
|
||||||
spdlog::error("Cannot save scene \"{}\", it would not load back: {}", scene.GetName(), *error);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string contents;
|
std::string contents;
|
||||||
try {
|
try {
|
||||||
contents = root.dump(4);
|
contents = root->dump(4);
|
||||||
} catch (const std::exception& exception) {
|
} catch (const std::exception& exception) {
|
||||||
spdlog::error("Failed to encode scene \"{}\": {}", scene.GetName(), exception.what());
|
spdlog::error("Failed to encode scene \"{}\": {}", scene.GetName(), exception.what());
|
||||||
return false;
|
return false;
|
||||||
@@ -195,7 +207,7 @@ bool SceneSerializer::ConstructScene(Scene& scene, LoadResult& result,
|
|||||||
try {
|
try {
|
||||||
if (root.contains("materials")) {
|
if (root.contains("materials")) {
|
||||||
context = "materials";
|
context = "materials";
|
||||||
MaterialSerialization::RegisterEmbedded(root.at("materials"));
|
MaterialSerialization::RegisterEmbedded(root.at("materials"), true);
|
||||||
context = "scene";
|
context = "scene";
|
||||||
}
|
}
|
||||||
idMap = ObjectSerialization::BuildObjects(stagingScene, root.at("objects")).idMap;
|
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_name = root.at("name").get<std::string>();
|
||||||
}
|
}
|
||||||
scene.m_SelectedObjectId = InvalidObjectId;
|
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;
|
scene.m_CurrentCameraId = InvalidObjectId;
|
||||||
if (const ObjectId cameraFileId = root.value("currentCamera", InvalidObjectId); cameraFileId != InvalidObjectId) {
|
if (const ObjectId cameraFileId = root.value("currentCamera", InvalidObjectId); cameraFileId != InvalidObjectId) {
|
||||||
if (const auto it = idMap.find(cameraFileId); it != idMap.end()) {
|
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));
|
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(
|
std::future<SceneSerializer::LoadResult> SceneSerializer::LoadSceneFileAsync(
|
||||||
const std::filesystem::path& path) {
|
const std::filesystem::path& path) {
|
||||||
return std::async(std::launch::async, [path]() {
|
return std::async(std::launch::async, [path]() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef LIGHTKEEPER_H
|
#ifndef LIGHTKEEPER_H
|
||||||
#define LIGHTKEEPER_H
|
#define LIGHTKEEPER_H
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <destrum/App.h>
|
#include <destrum/App.h>
|
||||||
@@ -28,6 +29,7 @@ public:
|
|||||||
|
|
||||||
void onWindowResize(int newWidth, int newHeight) override;
|
void onWindowResize(int newWidth, int newHeight) override;
|
||||||
void customWindowsMenu() override;
|
void customWindowsMenu() override;
|
||||||
|
void onPlayModeChanged(bool nowPlaying) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setupSkybox();
|
void setupSkybox();
|
||||||
@@ -53,6 +55,7 @@ private:
|
|||||||
void submitColliderDebugLines();
|
void submitColliderDebugLines();
|
||||||
|
|
||||||
Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)};
|
Camera camera{glm::vec3(0.f, 0.f, -5.f), glm::vec3(0, 1, 0)};
|
||||||
|
std::optional<Camera> editorCamera;
|
||||||
|
|
||||||
std::unique_ptr<CubeMap> skyboxCubemap;
|
std::unique_ptr<CubeMap> skyboxCubemap;
|
||||||
MainSceneObjects mainScene{};
|
MainSceneObjects mainScene{};
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ void LightKeeper::updateViewMode()
|
|||||||
auto& input = InputManager::GetInstance();
|
auto& input = InputManager::GetInstance();
|
||||||
FirstPersonController* player = mainScene.playerController;
|
FirstPersonController* player = mainScene.playerController;
|
||||||
|
|
||||||
if (player != nullptr && input.WasKeyPressed(SDL_SCANCODE_F5))
|
if (isPlaying() && player != nullptr && input.WasKeyPressed(SDL_SCANCODE_F6))
|
||||||
{
|
{
|
||||||
player->SetPossessed(!player->IsPossessed());
|
player->SetPossessed(!player->IsPossessed());
|
||||||
}
|
}
|
||||||
@@ -100,13 +100,13 @@ void LightKeeper::updateViewMode()
|
|||||||
|
|
||||||
bool LightKeeper::isPlayerViewActive() const
|
bool LightKeeper::isPlayerViewActive() const
|
||||||
{
|
{
|
||||||
return mainScene.playerController != nullptr && mainScene.playerController->IsPossessed();
|
return isPlaying() && mainScene.playerController != nullptr && mainScene.playerController->IsPossessed();
|
||||||
}
|
}
|
||||||
|
|
||||||
CameraComponent* LightKeeper::getSceneCamera() const
|
CameraComponent* LightKeeper::getSceneCamera() const
|
||||||
{
|
{
|
||||||
const auto& activeScenes = SceneManager::GetInstance().GetActiveScenes();
|
const auto& activeScenes = SceneManager::GetInstance().GetActiveScenes();
|
||||||
if (useFreeCamera || activeScenes.empty()) {
|
if (!isPlaying() || useFreeCamera || activeScenes.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return activeScenes.front()->GetCurrentCamera();
|
return activeScenes.front()->GetCurrentCamera();
|
||||||
@@ -187,6 +187,23 @@ void LightKeeper::customFixedUpdate(float dt)
|
|||||||
SceneManager::GetInstance().FixedUpdate(dt);
|
SceneManager::GetInstance().FixedUpdate(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void LightKeeper::onPlayModeChanged(bool nowPlaying)
|
||||||
|
{
|
||||||
|
findPlayer();
|
||||||
|
if (nowPlaying) {
|
||||||
|
// The player view moves the free camera, put it back where the editor left it on Stop
|
||||||
|
editorCamera = camera;
|
||||||
|
if (mainScene.playerController != nullptr) {
|
||||||
|
mainScene.playerController->SetPossessed(true);
|
||||||
|
}
|
||||||
|
} else if (editorCamera) {
|
||||||
|
const float aspectRatio = camera.GetAspectRatio();
|
||||||
|
camera = *editorCamera;
|
||||||
|
camera.setAspectRatio(aspectRatio);
|
||||||
|
editorCamera.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void LightKeeper::onWindowResize(int newWidth, int newHeight)
|
void LightKeeper::onWindowResize(int newWidth, int newHeight)
|
||||||
{
|
{
|
||||||
renderer.resize(gfxDevice, glm::ivec2{newWidth, newHeight});
|
renderer.resize(gfxDevice, glm::ivec2{newWidth, newHeight});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ int main(int argc, char* argv[]) {
|
|||||||
.appName = "Destrum Engine",
|
.appName = "Destrum Engine",
|
||||||
.windowTitle = "Lightkeeper",
|
.windowTitle = "Lightkeeper",
|
||||||
.exeDir = exeDir,
|
.exeDir = exeDir,
|
||||||
|
.startInEditMode = true,
|
||||||
});
|
});
|
||||||
app.run();
|
app.run();
|
||||||
app.cleanup();
|
app.cleanup();
|
||||||
|
|||||||
@@ -37,6 +37,20 @@ namespace {
|
|||||||
std::string GetTypeName() const override { return "TestComponent"; }
|
std::string GetTypeName() const override { return "TestComponent"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class CountingComponent final : public Component {
|
||||||
|
public:
|
||||||
|
explicit CountingComponent(GameObject& owner) : Component(owner, "CountingComponent") {}
|
||||||
|
|
||||||
|
void Update(float) override { ++updates; }
|
||||||
|
void LateUpdate(float) override { ++lateUpdates; }
|
||||||
|
void FixedUpdate(float) override { ++fixedUpdates; }
|
||||||
|
std::string GetTypeName() const override { return "CountingComponent"; }
|
||||||
|
|
||||||
|
int updates{0};
|
||||||
|
int lateUpdates{0};
|
||||||
|
int fixedUpdates{0};
|
||||||
|
};
|
||||||
|
|
||||||
class RemovingListener final : public EventListener {
|
class RemovingListener final : public EventListener {
|
||||||
public:
|
public:
|
||||||
explicit RemovingListener(std::unique_ptr<RemovingListener>* victim)
|
explicit RemovingListener(std::unique_ptr<RemovingListener>* victim)
|
||||||
@@ -268,6 +282,30 @@ namespace {
|
|||||||
|
|
||||||
SceneManager::GetInstance().Destroy();
|
SceneManager::GetInstance().Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testEditModeSkipsUpdates()
|
||||||
|
{
|
||||||
|
SceneManager& sceneManager = SceneManager::GetInstance();
|
||||||
|
Scene& scene = sceneManager.CreateScene("edit mode");
|
||||||
|
auto* counter = scene.CreateGameObject("counter")->AddComponent<CountingComponent>();
|
||||||
|
|
||||||
|
sceneManager.SetPlaying(false);
|
||||||
|
sceneManager.Update(0.1f);
|
||||||
|
sceneManager.FixedUpdate(0.1f);
|
||||||
|
sceneManager.LateUpdate(0.1f);
|
||||||
|
check(counter->updates == 0 && counter->fixedUpdates == 0 && counter->lateUpdates == 0,
|
||||||
|
"edit mode must not tick components");
|
||||||
|
check(scene.GetObjects().size() == 1, "edit mode must still add objects created in the editor");
|
||||||
|
|
||||||
|
sceneManager.SetPlaying(true);
|
||||||
|
sceneManager.Update(0.1f);
|
||||||
|
sceneManager.FixedUpdate(0.1f);
|
||||||
|
sceneManager.LateUpdate(0.1f);
|
||||||
|
check(counter->updates == 1 && counter->fixedUpdates == 1 && counter->lateUpdates == 1,
|
||||||
|
"play mode must tick components");
|
||||||
|
|
||||||
|
sceneManager.Destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RunCoreTests()
|
void RunCoreTests()
|
||||||
@@ -280,4 +318,5 @@ void RunCoreTests()
|
|||||||
testEngineComponentRegistration();
|
testEngineComponentRegistration();
|
||||||
testSceneCurrentCamera();
|
testSceneCurrentCamera();
|
||||||
testHierarchyMoves();
|
testHierarchyMoves();
|
||||||
|
testEditModeSkipsUpdates();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -334,6 +334,40 @@ namespace {
|
|||||||
|
|
||||||
SceneManager::GetInstance().Destroy();
|
SceneManager::GetInstance().Destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void testPlayModeSnapshotRestore()
|
||||||
|
{
|
||||||
|
Scene& scene = SceneManager::GetInstance().CreateScene("snapshot");
|
||||||
|
GameObject* box = scene.CreateGameObject("box");
|
||||||
|
box->GetTransform().SetLocalPosition({1.f, 2.f, 3.f});
|
||||||
|
scene.CreateGameObject("other");
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
scene.SetSelectedObject(box->GetId());
|
||||||
|
|
||||||
|
auto snapshot = SceneSerializer::Capture(scene);
|
||||||
|
check(snapshot.has_value(), "a scene must be capturable without a file");
|
||||||
|
(*snapshot)["selected"] = box->GetId();
|
||||||
|
|
||||||
|
// What play mode does to the scene before Stop restores it
|
||||||
|
box->GetTransform().SetLocalPosition({9.f, 9.f, 9.f});
|
||||||
|
scene.CreateGameObject("spawned during play");
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
|
||||||
|
check(SceneSerializer::LoadFromJson(scene, *snapshot), "restoring a snapshot must succeed");
|
||||||
|
scene.CommitPendingAdditions();
|
||||||
|
check(scene.GetObjects().size() == 2 && findByName(scene, "spawned during play") == nullptr,
|
||||||
|
"restoring must drop objects created after the snapshot");
|
||||||
|
const GameObject* restored = findByName(scene, "box");
|
||||||
|
check(restored != nullptr && close(restored->GetTransform().GetLocalPosition().x, 1.f),
|
||||||
|
"restoring must bring back the captured transforms");
|
||||||
|
check(scene.GetSelectedObject() == restored, "the selection must follow the restored object");
|
||||||
|
|
||||||
|
check(!SceneSerializer::LoadFromJson(scene, nlohmann::json{{"objects", 5}}),
|
||||||
|
"an invalid snapshot must be rejected");
|
||||||
|
check(scene.GetObjects().size() == 2, "a rejected snapshot must leave the scene alone");
|
||||||
|
|
||||||
|
SceneManager::GetInstance().Destroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void RunSerializationTests()
|
void RunSerializationTests()
|
||||||
@@ -350,4 +384,5 @@ void RunSerializationTests()
|
|||||||
testSceneFileIdsAreLocal();
|
testSceneFileIdsAreLocal();
|
||||||
testMaterialJsonRoundTrip();
|
testMaterialJsonRoundTrip();
|
||||||
testSceneValidatesEmbeddedMaterials();
|
testSceneValidatesEmbeddedMaterials();
|
||||||
|
testPlayModeSnapshotRestore();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user