From 7557c710fb0a3578eadb7ba356d636daa54361eb Mon Sep 17 00:00:00 2001 From: Sergey Lapin Date: Sun, 26 Apr 2026 16:43:37 +0300 Subject: [PATCH] Added prefabs --- src/features/editScene/CMakeLists.txt | 5 + src/features/editScene/EditorApp.cpp | 9 + .../editScene/components/PrefabInstance.hpp | 24 +++ .../editScene/systems/EditorUISystem.cpp | 147 ++++++++++++++++ .../editScene/systems/EditorUISystem.hpp | 14 ++ .../editScene/systems/PrefabSystem.cpp | 97 +++++++++++ .../editScene/systems/PrefabSystem.hpp | 74 ++++++++ .../editScene/systems/SceneSerializer.cpp | 163 +++++++++++++++--- .../editScene/systems/SceneSerializer.hpp | 50 ++++++ .../editScene/ui/PrefabInstanceEditor.cpp | 20 +++ .../editScene/ui/PrefabInstanceEditor.hpp | 20 +++ 11 files changed, 598 insertions(+), 25 deletions(-) create mode 100644 src/features/editScene/components/PrefabInstance.hpp create mode 100644 src/features/editScene/systems/PrefabSystem.cpp create mode 100644 src/features/editScene/systems/PrefabSystem.hpp create mode 100644 src/features/editScene/ui/PrefabInstanceEditor.cpp create mode 100644 src/features/editScene/ui/PrefabInstanceEditor.hpp diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index b246285..29b258c 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -46,6 +46,8 @@ set(EDITSCENE_SOURCES systems/SmartObjectSystem.cpp components/SmartObjectModule.cpp ui/SmartObjectEditor.cpp + systems/PrefabSystem.cpp + ui/PrefabInstanceEditor.cpp ui/TransformEditor.cpp ui/RenderableEditor.cpp @@ -170,6 +172,9 @@ set(EDITSCENE_HEADERS systems/SmartObjectSystem.hpp components/SmartObject.hpp ui/SmartObjectEditor.hpp + systems/PrefabSystem.hpp + components/PrefabInstance.hpp + ui/PrefabInstanceEditor.hpp systems/ProceduralTextureSystem.hpp systems/StaticGeometrySystem.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 96b34b2..b01c989 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -62,6 +62,8 @@ #include "components/ActionDebug.hpp" #include "components/BehaviorTree.hpp" #include "components/GoapBlackboard.hpp" +#include "components/PrefabInstance.hpp" +#include "systems/PrefabSystem.hpp" #include "components/NavMesh.hpp" #include "components/SmartObject.hpp" @@ -371,6 +373,8 @@ void EditorApp::setup() "Game mode: Loading startup_menu.json..."); if (serializer.loadFromFile("startup_menu.json", m_uiSystem.get())) { + PrefabSystem prefabSys(m_world, m_sceneMgr); + prefabSys.resolveInstances(); Ogre::LogManager::getSingleton().logMessage( "Game mode: startup_menu.json loaded"); } else { @@ -492,6 +496,8 @@ void EditorApp::startNewGame(const Ogre::String &scenePath) clearScene(); SceneSerializer serializer(m_world, m_sceneMgr); if (serializer.loadFromFile(scenePath, m_uiSystem.get())) { + PrefabSystem prefabSys(m_world, m_sceneMgr); + prefabSys.resolveInstances(); setGamePlayState(GamePlayState::Playing); Ogre::LogManager::getSingleton().logMessage( "Game started: loaded scene " + scenePath); @@ -575,6 +581,9 @@ void EditorApp::setupECS() // Register CellGrid/Town components CellGridModule::registerComponents(m_world); + + // Register PrefabInstance component + m_world.component(); } void EditorApp::createDefaultEntities() diff --git a/src/features/editScene/components/PrefabInstance.hpp b/src/features/editScene/components/PrefabInstance.hpp new file mode 100644 index 0000000..de3d21f --- /dev/null +++ b/src/features/editScene/components/PrefabInstance.hpp @@ -0,0 +1,24 @@ +#ifndef EDITSCENE_PREFABINSTANCE_HPP +#define EDITSCENE_PREFABINSTANCE_HPP +#pragma once +#include + +/** + * @brief Marks an entity as an instance of a prefab asset. + * + * The entity's TransformComponent acts as the world-space override + * for the prefab root. All other components (and the child subtree) + * are loaded from the prefab file at runtime and are NOT serialized + * with the main scene. + */ +struct PrefabInstanceComponent { + /** Path to the prefab JSON file (relative to working dir) */ + std::string prefabPath; + + /** Set to true once the prefab has been instantiated. + * Prevents double-instantiation on repeated resolve calls. + */ + bool instantiated = false; +}; + +#endif // EDITSCENE_PREFABINSTANCE_HPP diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 303fb69..f2711ce 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -1,5 +1,6 @@ #include "../components/GeneratedPhysicsTag.hpp" #include "EditorUISystem.hpp" +#include "PrefabSystem.hpp" #include "../components/EntityName.hpp" #include "../components/Transform.hpp" #include "../components/Renderable.hpp" @@ -34,12 +35,14 @@ #include "../components/GoapBlackboard.hpp" #include "../components/NavMesh.hpp" #include "../components/SmartObject.hpp" +#include "../components/PrefabInstance.hpp" #include "../ui/TransformEditor.hpp" #include "../ui/RenderableEditor.hpp" #include "../ui/PhysicsColliderEditor.hpp" #include "../ui/RigidBodyEditor.hpp" #include "../ui/ComponentRegistration.hpp" +#include "../ui/PrefabInstanceEditor.hpp" #include "PhysicsSystem.hpp" #include "BuoyancySystem.hpp" #include "NavMeshSystem.hpp" @@ -190,6 +193,23 @@ void EditorUISystem::registerComponentEditors() } }); + // Register PrefabInstance component + auto prefabEditor = std::make_unique(); + m_componentRegistry.registerComponent( + "Prefab Instance", "Scene", std::move(prefabEditor), + // Adder + [this](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [this](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); + // Register modular components (Light, Camera, etc.) registerModularComponents(); } @@ -224,6 +244,13 @@ void EditorUISystem::update(float deltaTime) renderFileDialog(); } + // Render prefab dialogs + if (m_showCreatePrefabDialog) { + showCreatePrefabDialog(m_prefabSourceEntity); + } + + renderPrefabBrowser(); + // Render FPS overlay renderFPSOverlay(deltaTime); } @@ -520,6 +547,11 @@ void EditorUISystem::renderEntityContextMenu(flecs::entity entity) if (ImGui::MenuItem("Duplicate")) { duplicateEntity(entity); } + if (ImGui::MenuItem("Create Prefab")) { + m_prefabSourceEntity = entity; + m_showCreatePrefabDialog = true; + m_prefabNameBuffer[0] = '\0'; + } ImGui::Separator(); if (ImGui::MenuItem("Delete")) { deleteEntity(entity); @@ -1460,3 +1492,118 @@ void EditorUISystem::renderFPSOverlay(float deltaTime) } ImGui::End(); } + +void EditorUISystem::showCreatePrefabDialog(flecs::entity entity) +{ + ImGui::OpenPopup("Create Prefab"); + if (ImGui::BeginPopupModal("Create Prefab", &m_showCreatePrefabDialog, + ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::InputText("Prefab Name", m_prefabNameBuffer, + sizeof(m_prefabNameBuffer)); + + if (ImGui::Button("Save", ImVec2(120, 0))) { + if (entity.is_alive() && + m_prefabNameBuffer[0] != '\0') { + std::string prefabPath = + PrefabSystem::getPrefabsDirectory() + + "/" + m_prefabNameBuffer + + ".json"; + PrefabSystem prefabSys(m_world, m_sceneMgr); + if (prefabSys.savePrefab(entity, prefabPath)) { + // Convert source entity to prefab instance + entity.set( + PrefabInstanceComponent{ + prefabPath, true }); + // Re-instantiate so children come + // from prefab + prefabSys.resolveInstances(); + m_refreshPrefabList = true; + } + } + m_showCreatePrefabDialog = false; + m_prefabSourceEntity = flecs::entity::null(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) { + m_showCreatePrefabDialog = false; + m_prefabSourceEntity = flecs::entity::null(); + } + ImGui::EndPopup(); + } +} + +void EditorUISystem::renderPrefabBrowser() +{ + if (!m_showPrefabBrowser) + return; + + ImGui::SetNextWindowPos( + ImVec2(LEFT_PANEL_WIDTH, 300), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowSize(ImVec2(250, 400), + ImGuiCond_FirstUseEver); + + ImGuiWindowFlags flags = ImGuiWindowFlags_NoCollapse; + if (ImGui::Begin("Prefab Browser", &m_showPrefabBrowser, flags)) { + // Refresh prefab list + if (m_refreshPrefabList) { + m_prefabFiles.clear(); + std::string prefabDir = + PrefabSystem::getPrefabsDirectory(); + if (std::filesystem::exists(prefabDir)) { + for (const auto &entry : + std::filesystem::directory_iterator( + prefabDir)) { + if (entry.is_regular_file() && + entry.path().extension() == + ".json") { + m_prefabFiles.push_back( + entry.path().filename() + .string()); + } + } + } + m_refreshPrefabList = false; + } + + // Refresh button + if (ImGui::Button("Refresh")) { + m_refreshPrefabList = true; + } + ImGui::SameLine(); + if (ImGui::Button("Create Instance")) { + // Will show file picker or use selected prefab + } + + ImGui::Separator(); + + // Prefab list + for (const auto &file : m_prefabFiles) { + std::string path = PrefabSystem::getPrefabsDirectory() + + "/" + file; + if (ImGui::Selectable(file.c_str())) { + Ogre::Vector3 pos(0, 0, 0); + if (m_selectedEntity.is_alive() && + m_selectedEntity.has()) { + pos = m_selectedEntity + .get() + .position + + Ogre::Vector3(2, 0, 0); + } + PrefabSystem prefabSys(m_world, + m_sceneMgr); + flecs::entity parent = + m_selectedEntity.is_alive() + ? m_selectedEntity + : flecs::entity::null(); + auto instance = prefabSys.createInstance( + path, parent, pos, + file.substr(0, file.find_last_of('.')), + this); + if (instance.is_alive()) { + setSelectedEntity(instance); + } + } + } + } + ImGui::End(); +} diff --git a/src/features/editScene/systems/EditorUISystem.hpp b/src/features/editScene/systems/EditorUISystem.hpp index 8e6066e..d10bfc4 100644 --- a/src/features/editScene/systems/EditorUISystem.hpp +++ b/src/features/editScene/systems/EditorUISystem.hpp @@ -166,6 +166,12 @@ public: void renderFileDialog(); void closeFileDialog(); + /** + * Prefab operations + */ + void showCreatePrefabDialog(flecs::entity entity); + void renderPrefabBrowser(); + private: // File menu void renderFileMenu(); @@ -242,6 +248,14 @@ private: bool m_refreshDirectory = true; char m_filenameBuffer[256] = "scene.json"; + // Prefab dialog state + bool m_showCreatePrefabDialog = false; + flecs::entity m_prefabSourceEntity = flecs::entity::null(); + char m_prefabNameBuffer[256] = { 0 }; + bool m_showPrefabBrowser = true; + std::vector m_prefabFiles; + bool m_refreshPrefabList = true; + // Queries flecs::query m_nameQuery; diff --git a/src/features/editScene/systems/PrefabSystem.cpp b/src/features/editScene/systems/PrefabSystem.cpp new file mode 100644 index 0000000..bc93c1a --- /dev/null +++ b/src/features/editScene/systems/PrefabSystem.cpp @@ -0,0 +1,97 @@ +#include "PrefabSystem.hpp" +#include "SceneSerializer.hpp" +#include "EditorUISystem.hpp" +#include "../components/PrefabInstance.hpp" +#include "../components/Transform.hpp" +#include "../components/EntityName.hpp" +#include "../components/EditorMarker.hpp" +#include +#include + +std::string PrefabSystem::getPrefabsDirectory() +{ + return "prefabs"; +} + +PrefabSystem::PrefabSystem(flecs::world &world, + Ogre::SceneManager *sceneMgr) + : m_world(world) + , m_sceneMgr(sceneMgr) +{ +} + +void PrefabSystem::resolveInstances() +{ + m_world.query().each( + [&](flecs::entity entity, PrefabInstanceComponent &prefab) { + if (prefab.instantiated || prefab.prefabPath.empty()) + return; + + SceneSerializer serializer(m_world, m_sceneMgr); + if (serializer.instantiatePrefab(entity, + prefab.prefabPath)) { + prefab.instantiated = true; + } else { + m_lastError = serializer.getLastError(); + Ogre::LogManager::getSingleton().logMessage( + "PrefabSystem: Failed to instantiate '" + + prefab.prefabPath + "': " + m_lastError); + } + }); +} + +flecs::entity PrefabSystem::createInstance(const std::string &prefabPath, + flecs::entity parent, + const Ogre::Vector3 &position, + const std::string &name, + EditorUISystem *uiSystem) +{ + flecs::entity instance = m_world.entity(); + instance.add(); + instance.set(EntityNameComponent(name)); + instance.set( + PrefabInstanceComponent{ prefabPath, false }); + + // Create transform + TransformComponent transform; + Ogre::SceneNode *parentNode = m_sceneMgr->getRootSceneNode(); + if (parent.is_valid() && parent != 0 && + parent.has()) { + parentNode = parent.get().node; + instance.child_of(parent); + } + transform.node = parentNode->createChildSceneNode(); + transform.position = position; + transform.rotation = Ogre::Quaternion::IDENTITY; + transform.scale = Ogre::Vector3::UNIT_SCALE; + transform.applyToNode(); + instance.set(transform); + + if (uiSystem) + uiSystem->addEntity(instance); + + // Instantiate prefab + SceneSerializer serializer(m_world, m_sceneMgr); + if (serializer.instantiatePrefab(instance, prefabPath, uiSystem)) { + auto &prefab = instance.get_mut(); + prefab.instantiated = true; + } else { + m_lastError = serializer.getLastError(); + Ogre::LogManager::getSingleton().logMessage( + "PrefabSystem: Failed to create instance of '" + + prefabPath + "': " + m_lastError); + } + + return instance; +} + +bool PrefabSystem::savePrefab(flecs::entity rootEntity, + const std::string &prefabPath) +{ + SceneSerializer serializer(m_world, m_sceneMgr); + if (!serializer.savePrefab(rootEntity, prefabPath)) { + m_lastError = serializer.getLastError(); + return false; + } + return true; +} diff --git a/src/features/editScene/systems/PrefabSystem.hpp b/src/features/editScene/systems/PrefabSystem.hpp new file mode 100644 index 0000000..b710ce3 --- /dev/null +++ b/src/features/editScene/systems/PrefabSystem.hpp @@ -0,0 +1,74 @@ +#ifndef EDITSCENE_PREFABSYSTEM_HPP +#define EDITSCENE_PREFABSYSTEM_HPP +#pragma once +#include +#include +#include + +// Forward declarations +class EditorUISystem; +class SceneSerializer; + +/** + * @brief System for managing prefab instances. + * + * Prefabs are reusable entity subtrees stored in external JSON files. + * Entities with PrefabInstanceComponent are lightweight proxies that + * reference a prefab file. Their children are created at runtime and + * are NOT serialized with the main scene. + */ +class PrefabSystem { +public: + PrefabSystem(flecs::world &world, Ogre::SceneManager *sceneMgr); + + /** + * @brief Instantiate all unloaded prefab instances. + * + * Should be called once after scene load. + */ + void resolveInstances(); + + /** + * @brief Create a new prefab instance entity and instantiate it. + * @param prefabPath Path to the prefab JSON file. + * @param parent flecs parent entity (null for root). + * @param position World-space position for the instance. + * @param name Display name for the instance entity. + * @param uiSystem Optional UI system for entity registration. + * @return The instance entity. + */ + flecs::entity createInstance(const std::string &prefabPath, + flecs::entity parent, + const Ogre::Vector3 &position, + const std::string &name, + EditorUISystem *uiSystem = nullptr); + + /** + * @brief Save an entity subtree as a prefab file. + * @param rootEntity Root of the subtree to save. + * @param prefabPath Destination file path. + * @return true on success. + */ + bool savePrefab(flecs::entity rootEntity, + const std::string &prefabPath); + + /** + * @brief Get the directory where prefabs are stored. + */ + static std::string getPrefabsDirectory(); + + /** + * @brief Get last error message. + */ + const std::string &getLastError() const + { + return m_lastError; + } + +private: + flecs::world &m_world; + Ogre::SceneManager *m_sceneMgr; + std::string m_lastError; +}; + +#endif // EDITSCENE_PREFABSYSTEM_HPP diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 916e7ef..b213c44 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -34,10 +34,12 @@ #include "../components/BehaviorTree.hpp" #include "../components/GoapBlackboard.hpp" #include "../components/NavMesh.hpp" +#include "../components/PrefabInstance.hpp" #include "EditorUISystem.hpp" #include #include #include +#include SceneSerializer::SceneSerializer(flecs::world &world, Ogre::SceneManager *sceneMgr) @@ -300,6 +302,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["behaviorTree"] = serializeBehaviorTree(entity); } + if (entity.has()) { + json["prefabInstance"] = serializePrefabInstance(entity); + } + if (entity.has()) { json["sun"] = serializeSun(entity); } @@ -536,14 +542,26 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, entity.child_of(parent); } - // Deserialize components that don't depend on material references - if (json.contains("name")) { - deserializeEntityName(entity, json["name"]); - } else { - entity.set(EntityNameComponent("Entity")); + deserializeEntityComponents(entity, json, parent, uiSystem, + true, true, true); +} + +void SceneSerializer::deserializeEntityComponents( + flecs::entity entity, const nlohmann::json &json, + flecs::entity parent, EditorUISystem *uiSystem, + bool processTransform, bool processName, + bool addEditorMarker) +{ + if (processName) { + if (json.contains("name")) { + deserializeEntityName(entity, json["name"]); + } else { + entity.set( + EntityNameComponent("Entity")); + } } - if (json.contains("transform")) { + if (processTransform && json.contains("transform")) { deserializeTransform(entity, json["transform"], parent); } @@ -585,7 +603,8 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, } if (json.contains("proceduralTexture")) { - deserializeProceduralTexture(entity, json["proceduralTexture"]); + deserializeProceduralTexture(entity, + json["proceduralTexture"]); } if (json.contains("proceduralMaterial")) { @@ -619,23 +638,25 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, } if (json.contains("playerController")) { - deserializePlayerController(entity, json["playerController"]); + deserializePlayerController(entity, + json["playerController"]); } if (json.contains("triangleBuffer")) { deserializeTriangleBuffer(entity, json["triangleBuffer"]); } - // CellGrid/Town components - deserialize WITHOUT resolving material references - // Material references will be resolved in the second pass + if (json.contains("prefabInstance")) { + deserializePrefabInstance(entity, json["prefabInstance"]); + } + + // CellGrid/Town components - deserialize WITHOUT resolving + // material references (deferred to second pass) if (json.contains("cellGrid")) { deserializeCellGrid(entity, json["cellGrid"]); } if (json.contains("town")) { - // Store the raw JSON for second-pass resolution m_pendingTownResolutions.push_back({ entity, json["town"] }); - // Still deserialize basic town data (name, colorRects, etc.) - // but skip material entity resolution TownComponent town; town.townName = json["town"].value("townName", "New Town"); town.materialName = json["town"].value("materialName", ""); @@ -643,7 +664,6 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, json["town"].value("proceduralMaterialEntityId", ""); town.textureRectName = json["town"].value("textureRectName", ""); - // Deserialize color rects if (json["town"].contains("colorRects") && json["town"]["colorRects"].is_object()) { for (auto &[name, rectJson] : @@ -668,10 +688,8 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, entity.set(town); } if (json.contains("district")) { - // Store the raw JSON for second-pass resolution m_pendingDistrictResolutions.push_back( { entity, json["district"] }); - // Still deserialize basic district data DistrictComponent district; district.radius = json["district"].value("radius", 50.0f); district.elevation = json["district"].value("elevation", 0.0f); @@ -696,9 +714,7 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, entity.set(district); } if (json.contains("lot")) { - // Store the raw JSON for second-pass resolution m_pendingLotResolutions.push_back({ entity, json["lot"] }); - // Still deserialize basic lot data LotComponent lot; lot.width = json["lot"].value("width", 10); lot.depth = json["lot"].value("depth", 10); @@ -721,7 +737,8 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, deserializeRoof(entity, json["roof"]); } if (json.contains("furnitureTemplate")) { - deserializeFurnitureTemplate(entity, json["furnitureTemplate"]); + deserializeFurnitureTemplate(entity, + json["furnitureTemplate"]); } if (json.contains("clearArea")) { deserializeClearArea(entity, json["clearArea"]); @@ -732,11 +749,10 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, } if (json.contains("navMeshGeometrySource")) { - deserializeNavMeshGeometrySource(entity, - json["navMeshGeometrySource"]); + deserializeNavMeshGeometrySource( + entity, json["navMeshGeometrySource"]); } - // Buoyancy components if (json.contains("buoyancyInfo")) { deserializeBuoyancyInfo(entity, json["buoyancyInfo"]); } @@ -770,19 +786,116 @@ void SceneSerializer::deserializeEntityFirstPass(const nlohmann::json &json, deserializeSkybox(entity, json["skybox"]); } - // Add to UI system if provided if (uiSystem) { uiSystem->addEntity(entity); } - // Deserialize children recursively (first pass) if (json.contains("children") && json["children"].is_array()) { for (const auto &childJson : json["children"]) { - deserializeEntityFirstPass(childJson, entity, uiSystem); + flecs::entity child = m_world.entity(); + if (addEditorMarker) + child.add(); + if (json.contains("id")) { + uint64_t cid = childJson.value("id", 0ULL); + if (cid) + m_entityMap[cid] = child; + } + child.child_of(entity); + deserializeEntityComponents(child, childJson, entity, + uiSystem, true, true, + addEditorMarker); } } } +// ------------------------------------------------------------------ +// Prefab support +// ------------------------------------------------------------------ + +nlohmann::json SceneSerializer::serializePrefabInstance(flecs::entity entity) +{ + nlohmann::json json; + auto &prefab = entity.get(); + json["prefabPath"] = prefab.prefabPath; + return json; +} + +void SceneSerializer::deserializePrefabInstance( + flecs::entity entity, const nlohmann::json &json) +{ + PrefabInstanceComponent prefab; + prefab.prefabPath = json.value("prefabPath", ""); + prefab.instantiated = false; + entity.set(prefab); +} + +bool SceneSerializer::savePrefab(flecs::entity rootEntity, + const std::string &filepath) +{ + try { + std::filesystem::path p(filepath); + std::filesystem::create_directories(p.parent_path()); + + nlohmann::json prefab = serializeEntity(rootEntity); + std::ofstream file(filepath); + if (!file.is_open()) { + m_lastError = "Failed to open prefab for writing: " + + filepath; + return false; + } + file << prefab.dump(4); + file.close(); + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Prefab save error: ") + e.what(); + return false; + } +} + +bool SceneSerializer::instantiatePrefab(flecs::entity instanceEntity, + const std::string &filepath, + EditorUISystem *uiSystem) +{ + try { + std::ifstream file(filepath); + if (!file.is_open()) { + m_lastError = "Failed to open prefab: " + filepath; + return false; + } + + nlohmann::json prefabJson; + file >> prefabJson; + file.close(); + + // Save entity map state — prefabs use their own local IDs + auto savedMap = m_entityMap; + m_entityMap.clear(); + + // Store prefab root ID mapping if present + if (prefabJson.contains("id")) { + m_entityMap[prefabJson["id"]] = instanceEntity; + } + + // Apply prefab root components to instance entity. + // Skip transform — the instance entity's TransformComponent + // is the world-space override. + // Skip name — the instance entity keeps its own name. + flecs::entity parent = instanceEntity.parent(); + deserializeEntityComponents(instanceEntity, prefabJson, + parent, uiSystem, false, + false, false); + + // Restore main scene entity map + m_entityMap = savedMap; + + return true; + } catch (const std::exception &e) { + m_lastError = std::string("Prefab instantiate error: ") + + e.what(); + return false; + } +} + void SceneSerializer::resolveMaterialReferences() { // Resolve Town material references diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 00460d2..80932ea 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -37,6 +37,29 @@ public: return m_lastError; } + /** + * Save an entity subtree as a prefab JSON file. + */ + bool savePrefab(flecs::entity rootEntity, + const std::string &filepath); + + /** + * Instantiate a prefab onto an existing entity. + * + * The prefab root components are copied onto instanceEntity, + * except transform (the instance entity's transform is the + * world-space override). Prefab children are created as + * children of instanceEntity without EditorMarkerComponent. + * + * @param instanceEntity Existing entity with PrefabInstanceComponent. + * @param filepath Path to prefab JSON file. + * @param uiSystem Optional UI system for registering new entities. + * @return true on success. + */ + bool instantiatePrefab(flecs::entity instanceEntity, + const std::string &filepath, + EditorUISystem *uiSystem = nullptr); + private: // Serialization helpers nlohmann::json serializeEntity(flecs::entity entity); @@ -182,6 +205,33 @@ private: void deserializeBehaviorTree(flecs::entity entity, const nlohmann::json &json); + // PrefabInstance serialization + nlohmann::json serializePrefabInstance(flecs::entity entity); + void deserializePrefabInstance(flecs::entity entity, + const nlohmann::json &json); + + /** + * Deserialize all components from JSON onto an existing entity. + * Used by both scene load and prefab instantiation. + * + * @param entity Target entity. + * @param json Entity JSON. + * @param parent flecs parent entity. + * @param uiSystem Optional UI system. + * @param processTransform If false, skip the "transform" key + * (used when the instance entity already has an override + * TransformComponent). + * @param processName If false, skip the "name" key. + * @param addEditorMarker If true, children get EditorMarkerComponent. + */ + void deserializeEntityComponents(flecs::entity entity, + const nlohmann::json &json, + flecs::entity parent, + EditorUISystem *uiSystem, + bool processTransform, + bool processName = true, + bool addEditorMarker = true); + flecs::world &m_world; Ogre::SceneManager *m_sceneMgr; std::string m_lastError; diff --git a/src/features/editScene/ui/PrefabInstanceEditor.cpp b/src/features/editScene/ui/PrefabInstanceEditor.cpp new file mode 100644 index 0000000..4141c9e --- /dev/null +++ b/src/features/editScene/ui/PrefabInstanceEditor.cpp @@ -0,0 +1,20 @@ +#include "PrefabInstanceEditor.hpp" +#include + +bool PrefabInstanceEditor::renderComponent(flecs::entity entity, + PrefabInstanceComponent &component) +{ + bool modified = false; + + char pathBuf[256] = { 0 }; + strncpy(pathBuf, component.prefabPath.c_str(), sizeof(pathBuf) - 1); + if (ImGui::InputText("Prefab Path", pathBuf, sizeof(pathBuf))) { + component.prefabPath = pathBuf; + modified = true; + } + + ImGui::TextDisabled("Instantiated: %s", + component.instantiated ? "yes" : "no"); + + return modified; +} diff --git a/src/features/editScene/ui/PrefabInstanceEditor.hpp b/src/features/editScene/ui/PrefabInstanceEditor.hpp new file mode 100644 index 0000000..a49a351 --- /dev/null +++ b/src/features/editScene/ui/PrefabInstanceEditor.hpp @@ -0,0 +1,20 @@ +#ifndef EDITSCENE_PREFABINSTANCEEDITOR_HPP +#define EDITSCENE_PREFABINSTANCEEDITOR_HPP +#pragma once +#include "ComponentEditor.hpp" +#include "../components/PrefabInstance.hpp" + +/** + * @brief Editor UI for PrefabInstanceComponent + */ +class PrefabInstanceEditor : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, + PrefabInstanceComponent &component) override; + const char *getName() const override + { + return "Prefab Instance"; + } +}; + +#endif // EDITSCENE_PREFABINSTANCEEDITOR_HPP