diff --git a/src/features/editScene/CMakeLists.txt b/src/features/editScene/CMakeLists.txt index 4a64861..2272870 100644 --- a/src/features/editScene/CMakeLists.txt +++ b/src/features/editScene/CMakeLists.txt @@ -47,6 +47,7 @@ set(EDITSCENE_SOURCES ui/TriangleBufferEditor.cpp ui/CharacterSlotsEditor.cpp ui/AnimationTreeEditor.cpp + ui/AnimationTreeTemplateEditor.cpp ui/CharacterEditor.cpp ui/CellGridEditor.cpp ui/LotEditor.cpp @@ -72,6 +73,7 @@ set(EDITSCENE_SOURCES components/TriangleBufferModule.cpp components/CharacterSlotsModule.cpp components/AnimationTreeModule.cpp + components/AnimationTreeTemplateModule.cpp components/AnimationTree.cpp components/CharacterModule.cpp components/CellGridModule.cpp @@ -152,6 +154,7 @@ set(EDITSCENE_HEADERS ui/TriangleBufferEditor.hpp ui/CharacterSlotsEditor.hpp ui/AnimationTreeEditor.hpp + ui/AnimationTreeTemplateEditor.hpp ui/CharacterEditor.hpp ui/CellGridEditor.hpp ui/LotEditor.hpp diff --git a/src/features/editScene/EditorApp.cpp b/src/features/editScene/EditorApp.cpp index 5bcecff..041aa00 100644 --- a/src/features/editScene/EditorApp.cpp +++ b/src/features/editScene/EditorApp.cpp @@ -42,6 +42,7 @@ #include "components/TriangleBuffer.hpp" #include "components/CharacterSlots.hpp" #include "components/AnimationTree.hpp" +#include "components/AnimationTreeTemplate.hpp" #include "components/Character.hpp" #include "components/StartupMenu.hpp" #include "components/PlayerController.hpp" @@ -488,6 +489,9 @@ void EditorApp::setupECS() // Register AnimationTree component m_world.component(); + // Register AnimationTreeTemplate component + m_world.component(); + // Register Character component m_world.component(); diff --git a/src/features/editScene/components/AnimationTree.hpp b/src/features/editScene/components/AnimationTree.hpp index 099ded2..d3b4f2d 100644 --- a/src/features/editScene/components/AnimationTree.hpp +++ b/src/features/editScene/components/AnimationTree.hpp @@ -150,6 +150,12 @@ struct AnimationTreeComponent { bool useRootMotion = false; bool dirty = true; + /* If set, the tree root is copied from the named template */ + Ogre::String templateName; + + /* Runtime: last copied template version (not serialized) */ + uint64_t templateVersion = 0; + /* Runtime: current state of each state machine (not serialized) */ std::unordered_map currentStates; diff --git a/src/features/editScene/components/AnimationTreeTemplate.hpp b/src/features/editScene/components/AnimationTreeTemplate.hpp new file mode 100644 index 0000000..964d3a0 --- /dev/null +++ b/src/features/editScene/components/AnimationTreeTemplate.hpp @@ -0,0 +1,20 @@ +#ifndef EDITSCENE_ANIMATIONTREETEMPLATE_HPP +#define EDITSCENE_ANIMATIONTREETEMPLATE_HPP +#pragma once + +#include + +/** + * Template marker for reusable animation trees. + * + * Entities with this component serve as shared animation tree templates. + * They should also have an AnimationTreeComponent for editing the tree. + * Other entities reference the template by name via + * AnimationTreeComponent::templateName. + */ +struct AnimationTreeTemplate { + Ogre::String name; + uint64_t version = 1; +}; + +#endif // EDITSCENE_ANIMATIONTREETEMPLATE_HPP diff --git a/src/features/editScene/components/AnimationTreeTemplateModule.cpp b/src/features/editScene/components/AnimationTreeTemplateModule.cpp new file mode 100644 index 0000000..e8a8a5a --- /dev/null +++ b/src/features/editScene/components/AnimationTreeTemplateModule.cpp @@ -0,0 +1,23 @@ +#include "AnimationTreeTemplate.hpp" +#include "../ui/ComponentRegistration.hpp" +#include "../ui/AnimationTreeTemplateEditor.hpp" + +REGISTER_COMPONENT_GROUP("Animation Tree Template", "Animation", + AnimationTreeTemplate, AnimationTreeTemplateEditor) +{ + registry.registerComponent( + AnimationTreeTemplate_name, AnimationTreeTemplate_group, + std::make_unique(), + // Adder + [](flecs::entity e) { + if (!e.has()) { + e.set({}); + } + }, + // Remover + [](flecs::entity e) { + if (e.has()) { + e.remove(); + } + }); +} diff --git a/src/features/editScene/systems/AnimationTreeSystem.cpp b/src/features/editScene/systems/AnimationTreeSystem.cpp index 5e988f9..fe510d5 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.cpp +++ b/src/features/editScene/systems/AnimationTreeSystem.cpp @@ -228,6 +228,39 @@ void AnimationTreeSystem::initializeTreeStates( initializeTreeStates(child, at); } +void AnimationTreeSystem::resolveTemplate(AnimationTreeComponent &at) +{ + if (at.templateName.empty()) + return; + + AnimationTreeTemplate *templ = nullptr; + AnimationTreeComponent *templAt = nullptr; + + m_world.query() + .each([&](flecs::entity, AnimationTreeTemplate &t, + AnimationTreeComponent &ta) { + if (t.name == at.templateName) { + templ = &t; + templAt = &ta; + } + }); + + if (!templ) { + m_world.query() + .each([&](flecs::entity, AnimationTreeTemplate &t) { + if (t.name == at.templateName) + templ = &t; + }); + } + + if (templ && at.templateVersion != templ->version) { + if (templAt) + at.root = templAt->root; + at.templateVersion = templ->version; + at.dirty = true; + } +} + void AnimationTreeSystem::update(float deltaTime) { if (!m_initialized) @@ -236,6 +269,8 @@ void AnimationTreeSystem::update(float deltaTime) m_world.query().each( [this, deltaTime](flecs::entity e, AnimationTreeComponent &at) { + resolveTemplate(at); + if (at.dirty) { if (setupEntity(e, at)) at.dirty = false; diff --git a/src/features/editScene/systems/AnimationTreeSystem.hpp b/src/features/editScene/systems/AnimationTreeSystem.hpp index 8464d2d..00024b1 100644 --- a/src/features/editScene/systems/AnimationTreeSystem.hpp +++ b/src/features/editScene/systems/AnimationTreeSystem.hpp @@ -9,6 +9,7 @@ #include #include "../components/AnimationTree.hpp" +#include "../components/AnimationTreeTemplate.hpp" /** * System that evaluates an AnimationTreeComponent each frame. @@ -100,6 +101,9 @@ private: const Ogre::String &stateMachineName, const Ogre::String &stateName, bool reset); + /* Resolve template reference and copy tree if template changed */ + void resolveTemplate(AnimationTreeComponent &at); + const AnimationTreeNode *findStateMachineNode( const AnimationTreeNode &root, const Ogre::String &name) const; diff --git a/src/features/editScene/systems/EditorUISystem.cpp b/src/features/editScene/systems/EditorUISystem.cpp index 52339ff..c1180af 100644 --- a/src/features/editScene/systems/EditorUISystem.cpp +++ b/src/features/editScene/systems/EditorUISystem.cpp @@ -21,6 +21,7 @@ #include "../components/CharacterSlots.hpp" #include "../components/Character.hpp" #include "../components/AnimationTree.hpp" +#include "../components/AnimationTreeTemplate.hpp" #include "../components/StartupMenu.hpp" #include "../components/PlayerController.hpp" #include "../components/CellGrid.hpp" @@ -701,6 +702,13 @@ void EditorUISystem::renderComponentList(flecs::entity entity) componentCount++; } + // Render AnimationTreeTemplate if present + if (entity.has()) { + auto &templ = entity.get_mut(); + m_componentRegistry.render(entity, templ); + componentCount++; + } + // Render StartupMenu if present if (entity.has()) { auto &sm = entity.get_mut(); diff --git a/src/features/editScene/systems/SceneSerializer.cpp b/src/features/editScene/systems/SceneSerializer.cpp index 31506ff..cf11996 100644 --- a/src/features/editScene/systems/SceneSerializer.cpp +++ b/src/features/editScene/systems/SceneSerializer.cpp @@ -18,6 +18,7 @@ #include "../components/Character.hpp" #include "../components/CharacterSlots.hpp" #include "../components/AnimationTree.hpp" +#include "../components/AnimationTreeTemplate.hpp" #include "../components/StartupMenu.hpp" #include "../components/PlayerController.hpp" #include "../components/CellGrid.hpp" @@ -211,6 +212,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity) json["animationTree"] = serializeAnimationTree(entity); } + if (entity.has()) { + json["animationTreeTemplate"] = serializeAnimationTreeTemplate(entity); + } + if (entity.has()) { json["startupMenu"] = serializeStartupMenu(entity); } @@ -364,6 +369,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json, deserializeAnimationTree(entity, json["animationTree"]); } + if (json.contains("animationTreeTemplate")) { + deserializeAnimationTreeTemplate(entity, json["animationTreeTemplate"]); + } + if (json.contains("startupMenu")) { deserializeStartupMenu(entity, json["startupMenu"]); } @@ -1549,6 +1558,8 @@ nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity) nlohmann::json json; json["enabled"] = at.enabled; json["useRootMotion"] = at.useRootMotion; + if (!at.templateName.empty()) + json["templateName"] = at.templateName; json["root"] = serializeAnimationTreeNode(at.root); return json; } @@ -1559,12 +1570,31 @@ void SceneSerializer::deserializeAnimationTree(flecs::entity entity, AnimationTreeComponent at; at.enabled = json.value("enabled", true); at.useRootMotion = json.value("useRootMotion", false); + at.templateName = json.value("templateName", ""); if (json.contains("root")) deserializeAnimationTreeNode(at.root, json["root"]); at.dirty = true; entity.set(at); } +nlohmann::json SceneSerializer::serializeAnimationTreeTemplate(flecs::entity entity) +{ + auto &templ = entity.get(); + nlohmann::json json; + json["name"] = templ.name; + json["version"] = templ.version; + return json; +} + +void SceneSerializer::deserializeAnimationTreeTemplate(flecs::entity entity, + const nlohmann::json &json) +{ + AnimationTreeTemplate templ; + templ.name = json.value("name", ""); + templ.version = json.value("version", 1); + entity.set(templ); +} + // ============================================================================ // CellGrid/Town Component Serialization // ============================================================================ diff --git a/src/features/editScene/systems/SceneSerializer.hpp b/src/features/editScene/systems/SceneSerializer.hpp index 68d2812..0090ba2 100644 --- a/src/features/editScene/systems/SceneSerializer.hpp +++ b/src/features/editScene/systems/SceneSerializer.hpp @@ -62,6 +62,7 @@ private: nlohmann::json serializeCharacter(flecs::entity entity); nlohmann::json serializeCharacterSlots(flecs::entity entity); nlohmann::json serializeAnimationTree(flecs::entity entity); + nlohmann::json serializeAnimationTreeTemplate(flecs::entity entity); nlohmann::json serializeStartupMenu(flecs::entity entity); nlohmann::json serializePlayerController(flecs::entity entity); @@ -111,6 +112,8 @@ private: const nlohmann::json &json); void deserializeAnimationTree(flecs::entity entity, const nlohmann::json &json); + void deserializeAnimationTreeTemplate(flecs::entity entity, + const nlohmann::json &json); void deserializeStartupMenu(flecs::entity entity, const nlohmann::json &json); void deserializePlayerController(flecs::entity entity, diff --git a/src/features/editScene/ui/AnimationTreeEditor.cpp b/src/features/editScene/ui/AnimationTreeEditor.cpp index 926f326..c1da9a2 100644 --- a/src/features/editScene/ui/AnimationTreeEditor.cpp +++ b/src/features/editScene/ui/AnimationTreeEditor.cpp @@ -1,5 +1,6 @@ #include "AnimationTreeEditor.hpp" #include "../systems/AnimationTreeSystem.hpp" +#include "../components/AnimationTreeTemplate.hpp" #include AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr) @@ -211,7 +212,8 @@ void AnimationTreeEditor::renderTree(AnimationTreeNode &node, if (ImGui::BeginPopupContextItem()) { if (canHaveChildren(node.type)) { if (node.type == "output" || - node.type == "stateMachine") { + node.type == "stateMachine" || + node.type == "state") { if (ImGui::MenuItem("Add State Machine")) queueAddChild(&node, "stateMachine"); } @@ -259,7 +261,8 @@ void AnimationTreeEditor::renderTree(AnimationTreeNode &node, } if (ImGui::BeginPopup("AddChild")) { if (node.type == "output" || - node.type == "stateMachine") { + node.type == "stateMachine" || + node.type == "state") { if (ImGui::MenuItem("State Machine")) queueAddChild(&node, "stateMachine"); } @@ -473,12 +476,44 @@ void AnimationTreeEditor::renderStatePreview( bool AnimationTreeEditor::renderComponent( flecs::entity entity, AnimationTreeComponent &at) { + ImGui::PushID("AnimTree"); bool modified = false; if (ImGui::CollapsingHeader("Animation Tree", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::Indent(); + /* Template reference */ + char templBuf[256]; + snprintf(templBuf, sizeof(templBuf), "%s", + at.templateName.c_str()); + if (ImGui::InputText("Template Name", templBuf, + sizeof(templBuf))) { + at.templateName = templBuf; + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Leave empty for local tree. Set to template name to copy from a template entity."); + } + + bool isTemplateEntity = entity.has(); + if (isTemplateEntity) { + auto &templ = entity.get_mut(); + char nameBuf[256]; + snprintf(nameBuf, sizeof(nameBuf), "%s", + templ.name.c_str()); + if (ImGui::InputText("Template Publish Name", nameBuf, + sizeof(nameBuf))) { + templ.name = nameBuf; + modified = true; + } + ImGui::TextDisabled("Template version: %llu", + (unsigned long long)templ.version); + } + + ImGui::Separator(); + /* Global toggles */ if (ImGui::Checkbox("Enabled", &at.enabled)) modified = true; @@ -515,5 +550,12 @@ bool AnimationTreeEditor::renderComponent( ImGui::Unindent(); } + /* Auto-publish template changes */ + if (modified && entity.has()) { + auto &templ = entity.get_mut(); + templ.version++; + } + + ImGui::PopID(); return modified; } diff --git a/src/features/editScene/ui/AnimationTreeTemplateEditor.cpp b/src/features/editScene/ui/AnimationTreeTemplateEditor.cpp new file mode 100644 index 0000000..fbad9fc --- /dev/null +++ b/src/features/editScene/ui/AnimationTreeTemplateEditor.cpp @@ -0,0 +1,37 @@ +#include "AnimationTreeTemplateEditor.hpp" +#include + +bool AnimationTreeTemplateEditor::renderComponent( + flecs::entity entity, AnimationTreeTemplate &templ) +{ + ImGui::PushID("AnimTreeTempl"); + (void)entity; + bool modified = false; + + if (ImGui::CollapsingHeader("Animation Tree Template", + ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Indent(); + + char nameBuf[256]; + snprintf(nameBuf, sizeof(nameBuf), "%s", templ.name.c_str()); + if (ImGui::InputText("Template Name", nameBuf, + sizeof(nameBuf))) { + templ.name = nameBuf; + modified = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip( + "Other entities reference this template by name in their Animation Tree component."); + } + + ImGui::TextDisabled("Version: %llu", + (unsigned long long)templ.version); + ImGui::TextDisabled( + "Edit the Animation Tree component on this entity to modify the template."); + + ImGui::Unindent(); + } + + ImGui::PopID(); + return modified; +} diff --git a/src/features/editScene/ui/AnimationTreeTemplateEditor.hpp b/src/features/editScene/ui/AnimationTreeTemplateEditor.hpp new file mode 100644 index 0000000..eae8d01 --- /dev/null +++ b/src/features/editScene/ui/AnimationTreeTemplateEditor.hpp @@ -0,0 +1,24 @@ +#ifndef EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP +#define EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP +#pragma once + +#include "ComponentEditor.hpp" +#include "../components/AnimationTreeTemplate.hpp" + +/** + * Editor for AnimationTreeTemplate component. + * The actual tree editing is done via the co-located AnimationTreeComponent. + * This editor only exposes the template name and version. + */ +class AnimationTreeTemplateEditor + : public ComponentEditor { +public: + bool renderComponent(flecs::entity entity, + AnimationTreeTemplate &templ) override; + const char *getName() const override + { + return "Animation Tree Template"; + } +}; + +#endif // EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP