diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index ef6b78874f..6392d6e4db 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -239,9 +239,10 @@ add_library(imgui STATIC "lib/imgui/imgui_tables.cpp" "lib/imgui/imgui_widgets.cpp" "lib/imgui/imgui_demo.cpp" + "lib/imgui/misc/cpp/imgui_stdlib.cpp" ) -target_include_directories(imgui PUBLIC "lib/imgui") +target_include_directories(imgui PUBLIC "lib/imgui" "lib/imgui/misc/cpp/imgui_stdlib.hpp") add_subdirectory(lib/stduuid) target_link_libraries(cubos-core PUBLIC stduuid) diff --git a/core/include/cubos/core/data/serialization_map.hpp b/core/include/cubos/core/data/serialization_map.hpp index f771387f0b..42172e8df1 100644 --- a/core/include/cubos/core/data/serialization_map.hpp +++ b/core/include/cubos/core/data/serialization_map.hpp @@ -119,6 +119,12 @@ namespace cubos::core::data return mRefToId.size(); } + /// @brief Returns the internal map that maps references to IDs + /// @return Map of references and Ids. + inline std::unordered_map getMap() const { + return mRefToId; + } + private: bool mUsingFunctions; ///< True if the map is using functions instead of keeping a map. std::function mSerialize; ///< Function used to serialize references. diff --git a/core/include/cubos/core/ecs/blueprint.hpp b/core/include/cubos/core/ecs/blueprint.hpp index b07a16f8ad..72ef779448 100644 --- a/core/include/cubos/core/ecs/blueprint.hpp +++ b/core/include/cubos/core/ecs/blueprint.hpp @@ -62,6 +62,13 @@ namespace cubos::core::ecs /// @brief Clears the blueprint, removing any added entities and components. void clear(); + /// @brief Returns the internal map that maps entities to their names + /// @return Map of entities and names. + inline std::unordered_map getMap() const { + return mMap.getMap(); + } + + private: friend class CommandBuffer; diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 1df15f9b63..245faee11d 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -25,6 +25,7 @@ set(CUBOS_ENGINE_SOURCE "src/cubos/engine/tools/entity_selector/plugin.cpp" "src/cubos/engine/tools/world_inspector/plugin.cpp" "src/cubos/engine/tools/entity_inspector/plugin.cpp" + "src/cubos/engine/tools/scene_editor/plugin.cpp" "src/cubos/engine/assets/plugin.cpp" "src/cubos/engine/assets/assets.cpp" @@ -72,6 +73,7 @@ set(CUBOS_ENGINE_INCLUDE "include/cubos/engine/tools/entity_selector/plugin.hpp" "include/cubos/engine/tools/world_inspector/plugin.hpp" "include/cubos/engine/tools/entity_inspector/plugin.hpp" + "include/cubos/engine/tools/scene_editor/plugin.hpp" "include/cubos/engine/transform/position.hpp" "include/cubos/engine/transform/rotation.hpp" diff --git a/engine/include/cubos/engine/assets/plugin.hpp b/engine/include/cubos/engine/assets/plugin.hpp index 01eb7304bf..99ebd26b69 100644 --- a/engine/include/cubos/engine/assets/plugin.hpp +++ b/engine/include/cubos/engine/assets/plugin.hpp @@ -30,7 +30,7 @@ namespace cubos::engine /// ## Startup tags /// - `cubos.assets.init` - initializes the assets manager and loads the meta files (after `cubos.settings`). /// - `cubos.assets.bridge` - systes which add bridges to the asset manager should be tagged with this. - /// - `cubos.assets` - systems which load assets should be tagged with this. + /// - `cubos.assets` - startup systems which load assets should be tagged with this. /// /// ## Tags /// - `cubos.assets.cleanup` - frees any assets no longer in use. diff --git a/engine/include/cubos/engine/tools/scene_editor/plugin.hpp b/engine/include/cubos/engine/tools/scene_editor/plugin.hpp new file mode 100644 index 0000000000..89c82cbd4a --- /dev/null +++ b/engine/include/cubos/engine/tools/scene_editor/plugin.hpp @@ -0,0 +1,30 @@ +/// @dir +/// @brief @ref scene-editor-tool-plugin plugin directory. + +/// @file +/// @brief Plugin entry point. +/// @ingroup scene-editor-tool-plugin + +#pragma once + +#include + +namespace cubos::engine::tools +{ + /// @defgroup scene-editor-tool-plugin Scene editor + /// @ingroup tool-plugins + /// @brief Adds a window to edit scenes and select entities in them. + /// + /// @note Selected entities are registered in the @ref EntitySelector resource. + /// @note The opened scene is identified by the @ref AssetSelectedEvent event. + /// + /// ## Dependencies + /// - @ref scene-plugin + /// - @ref asset-explorer-tool-plugin + /// - @ref entity-selector-tool-plugin + /// + /// @brief Plugin entry function. + /// @param cubos @b CUBOS. main class + /// @ingroup scene-editor-tool-plugin + void sceneEditorPlugin(Cubos& cubos); +}; // namespace cubos::engine::tools \ No newline at end of file diff --git a/engine/src/cubos/engine/tools/scene_editor/plugin.cpp b/engine/src/cubos/engine/tools/scene_editor/plugin.cpp new file mode 100644 index 0000000000..708f7c350c --- /dev/null +++ b/engine/src/cubos/engine/tools/scene_editor/plugin.cpp @@ -0,0 +1,263 @@ +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +using cubos::core::ecs::Commands; +using cubos::core::ecs::Entity; +using cubos::core::ecs::Read; +using cubos::core::ecs::Write; + +using namespace cubos::engine; + +using tools::AssetSelectedEvent; + +/// @brief Resource used to store Scene(s) information +struct SceneInfo +{ + bool isExpanded{false}; + bool isSelected{false}; + bool shouldBeRemoved{false}; + std::string name; + std::vector> entities; + std::vector> scenes; +}; + +static void closeScene(Commands& commands, SceneInfo& scene) +{ + for (const auto& [name, entity] : scene.entities) + { + commands.destroy(entity); + } + scene.entities.clear(); + + for (auto& [name, subscene] : scene.scenes) + { + closeScene(commands, subscene); + } + scene.scenes.clear(); +} + +static void placeEntity(std::string name, Entity handle, SceneInfo& scene) +{ + auto split = name.find("."); + if (split == std::string::npos) + { + scene.entities.emplace_back(name, handle); + } + else + { + auto subsceneName = name.substr(0, split - 1); + for (auto& [sname, subscene] : scene.scenes) + { + if (sname.compare(subsceneName) == 0) + { + placeEntity(name.substr(split + 1), handle, subscene); + return; + } + } + SceneInfo subScene; + subScene.name = subsceneName; + auto& [_, subSceneHandle] = scene.scenes.emplace_back(subsceneName, subScene); + placeEntity(name.substr(split + 1), handle, subSceneHandle); + } +} + +static void openScene(const Asset& sceneToSpawn, Commands& commands, const Assets& assets, SceneInfo& scene) +{ + closeScene(commands, scene); + auto sceneRead = assets.read(sceneToSpawn); + auto builder = commands.spawn(sceneRead->blueprint); + for (const auto& [entity, name] : sceneRead->blueprint.getMap()) + { + placeEntity(name, builder.entity(name), scene); + } +} + +static void checkAssetEventSystem(cubos::core::ecs::EventReader reader, Commands commands, + Read assets, Write scene) +{ + for (const auto& event : reader) + { + assets->load(event.asset); + if (assets->type(event.asset) == typeid(Scene)) + { + CUBOS_TRACE("CAN SPAWN"); + openScene(event.asset, commands, *assets, *scene); + } + else + { + CUBOS_TRACE("CANNOT SPAWN"); + } + } +} + +/// @brief Used to check if input entity names are valid. +static int entityNameFilter(ImGuiInputTextCallbackData* data) +{ + if (data->EventChar == '.') + return 1; + return 0; +} + +/// draws the entities within a scene and allows the user to add or remove entities +static void drawSceneEntities(std::vector>& entities, SceneInfo& scene, + cubos::engine::tools::EntitySelector& selector, Commands& cmds, int hierarchyDepth) +{ + // Add entity to current scene (needs to be root, not a sub scene) + if (hierarchyDepth == 0 && ImGui::Button("Add Entity")) + { + std::string entityName = scene.name + "_entity_" + std::to_string(scene.entities.size() + 1); + entities.emplace_back(entityName, cmds.create().entity()); + } + + // List entities (that can be removed if it's not a sub scene and selected) + std::vector entitiesToRemove; + for (std::size_t i = 0; i < entities.size(); i++) + { + auto name = entities[i].first; + auto handle = entities[i].second; + ImGui::PushID(&entities[i]); + + if (hierarchyDepth == 0) + { + std::string buff = name; + + ImGui::InputText("", &name, ImGuiInputTextFlags_CallbackCharFilter, entityNameFilter); + + if (name.compare(buff)) + { + entities[i].first = buff; + } + } + else + { + ImGui::BulletText("%s", name.c_str()); + } + + ImGui::Spacing(); + if (ImGui::Button("Select")) + { + CUBOS_INFO("Selected entity {}", name); + selector.selection = handle; + } + if (hierarchyDepth == 0) + { + ImGui::SameLine(); + if (ImGui::Button("Remove")) + { + entitiesToRemove.push_back(i); + CUBOS_INFO("Removing entity '{}' from scene '{}'", name, scene.name); + cmds.destroy(handle); + } + } + + ImGui::PopID(); + } + + for (const auto& i : entitiesToRemove) + { + entities.erase(entities.begin() + static_cast(i)); + } +} + +/// @brief recursively draws the scene hierarchy and allows the user to remove scenes +static void showSceneHierarchy(SceneInfo& scene, Commands& cmds, cubos::engine::tools::EntitySelector& selector, + int hierarchyDepth) +{ + ImGui::PushID(&scene); + + /// Root node scene + bool nodeOpen = ImGui::TreeNode(&scene, "%s", scene.name.c_str()); + scene.isExpanded = nodeOpen; + + // Remove scene + if (hierarchyDepth == 1) + { + ImGui::SameLine(); + ImGui::PushID(&scene.shouldBeRemoved); + if (ImGui::Button("Remove Scene")) + { + scene.shouldBeRemoved = true; + } + ImGui::PopID(); + } + + if (nodeOpen) + { + if (hierarchyDepth == 1) + { + std::string buff = scene.name; + ImGui::PushID("nameChange"); + ImGui::InputText("", &buff, ImGuiInputTextFlags_CallbackCharFilter, entityNameFilter); + ImGui::PopID(); + + if (scene.name.compare(buff)) + { + scene.name = buff; + } + } + + // Add entity to current scene (needs to be root, not a sub scene) + if (hierarchyDepth == 0) + { + if (ImGui::Button("Add Scene")) + { + std::string sceneName = scene.name + "_scene_" + std::to_string(scene.scenes.size() + 1); + SceneInfo newSubscene; + newSubscene.name = sceneName; + scene.scenes.emplace_back(sceneName, newSubscene); + } + ImGui::SameLine(); + } + drawSceneEntities(scene.entities, scene, selector, cmds, hierarchyDepth); + + std::vector sceneToRemove; + for (std::size_t i = 0; i < scene.scenes.size(); i++) + { + if (scene.scenes[i].second.shouldBeRemoved) + { + sceneToRemove.push_back(i); + } + else + { + showSceneHierarchy(scene.scenes[i].second, cmds, selector, hierarchyDepth + 1); + } + } + for (const auto& i : sceneToRemove) + { + scene.scenes.erase(scene.scenes.begin() + static_cast(i)); + } + ImGui::TreePop(); + } + + ImGui::PopID(); +} + +static void sceneEditorSystem(Commands cmds, Write scene, + Write selector) +{ + ImGui::Begin("Scene Editor"); + showSceneHierarchy(*scene, cmds, *selector, 0); + ImGui::End(); +} + +void cubos::engine::tools::sceneEditorPlugin(Cubos& cubos) +{ + cubos.addPlugin(scenePlugin); + + cubos.addPlugin(cubos::engine::tools::entitySelectorPlugin); + cubos.addPlugin(cubos::engine::tools::assetExplorerPlugin); + + cubos.addResource(); + + cubos.system(checkAssetEventSystem); + cubos.system(sceneEditorSystem).tagged("cubos.imgui"); +} diff --git a/tools/tesseratos/assets/example_asset.txt b/tools/tesseratos/assets/example_asset.txt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tools/tesseratos/assets/example_asset.txt.meta b/tools/tesseratos/assets/example_asset.txt.meta deleted file mode 100644 index c7afcf5e7c..0000000000 --- a/tools/tesseratos/assets/example_asset.txt.meta +++ /dev/null @@ -1,3 +0,0 @@ -{ - "id": "059c16e7-a439-44c7-9bdc-6e069dba0c75" -} \ No newline at end of file diff --git a/tools/tesseratos/assets/main.cubos b/tools/tesseratos/assets/main.cubos new file mode 100644 index 0000000000..9e93c3ff8d --- /dev/null +++ b/tools/tesseratos/assets/main.cubos @@ -0,0 +1,18 @@ +{ + "imports": { + "sub1": "00d86ba8-5f34-440f-a180-d9d12c8e8b91" + }, + "entities": { + "main": { + "cubos/position": { + "x": 1, + "y": 1, + "z": 1 + } + }, + "sub1.sub_main": { + + } + } +} + diff --git a/tools/tesseratos/assets/main.cubos.meta b/tools/tesseratos/assets/main.cubos.meta new file mode 100644 index 0000000000..b49d0c18b2 --- /dev/null +++ b/tools/tesseratos/assets/main.cubos.meta @@ -0,0 +1,3 @@ +{ + "id": "10d86ba8-5f34-440f-a180-d9d12c8e8b91" +} diff --git a/tools/tesseratos/assets/sub.cubos b/tools/tesseratos/assets/sub.cubos new file mode 100644 index 0000000000..f88b61e489 --- /dev/null +++ b/tools/tesseratos/assets/sub.cubos @@ -0,0 +1,14 @@ +{ + "imports": { + }, + "entities": { + "sub_main": { + "cubos/position": { + "x": 2, + "y": 2, + "z": 2 + } + } + } + } + \ No newline at end of file diff --git a/tools/tesseratos/assets/sub.cubos.meta b/tools/tesseratos/assets/sub.cubos.meta new file mode 100644 index 0000000000..71d95f2f99 --- /dev/null +++ b/tools/tesseratos/assets/sub.cubos.meta @@ -0,0 +1,3 @@ +{ + "id": "00d86ba8-5f34-440f-a180-d9d12c8e8b91" +} diff --git a/tools/tesseratos/src/main.cpp b/tools/tesseratos/src/main.cpp index eab156ded1..bd528dcfcf 100644 --- a/tools/tesseratos/src/main.cpp +++ b/tools/tesseratos/src/main.cpp @@ -1,11 +1,11 @@ -#include - #include -#include #include #include +#include +#include #include +#include #include using namespace cubos::engine; @@ -30,13 +30,14 @@ static void mockSettings(Write settings) int main(int argc, char** argv) { Cubos cubos(argc, argv); - cubos.addPlugin(imguiPlugin); cubos.addPlugin(rendererPlugin); + cubos.addPlugin(tools::sceneEditorPlugin); + cubos.addPlugin(tools::entityInspectorPlugin); + cubos.addPlugin(tools::worldInspectorPlugin); cubos.addPlugin(tools::assetExplorerPlugin); - cubos.addPlugin(tools::settingsInspectorPlugin); cubos.startupSystem(mockCamera).tagged("setup"); cubos.startupSystem(mockSettings).tagged("setup"); cubos.run(); -} +} \ No newline at end of file