Compare commits

...

3 Commits

Author SHA1 Message Date
slapin 414744cceb Use Animation Tree Registry 2026-06-25 18:58:59 +03:00
slapin eea4fbdb2f Update Lua examples path 2026-06-25 09:33:22 +03:00
slapin 19faeee8fa Forgot to update AGENTS.md 2026-06-25 09:11:24 +03:00
33 changed files with 1860 additions and 822 deletions
+6 -6
View File
@@ -261,12 +261,12 @@ keep the controller pointed at the spawner rather than the transient instance.
#### Game Mode Input
Game mode input is kept in sync with the physical keyboard each frame by
calling `SDL_PumpEvents()` followed by `SDL_GetKeyboardState()`. This prevents
missed `keyReleased` events from leaving movement keys stuck (the original
cause of the "infinite slow walk" after releasing Shift+W). Event-driven
`keyPressed`/`keyReleased` handlers are still used for one-shot actions such
as `ePressed`, `fPressed` and `iPressed`.
Game mode movement keys (`W`, `A`, `S`, `D`, left/right `Shift`) are handled
inside `keyPressed`/`keyReleased` by `keysym.sym`. Repeated `KEYDOWN` events
(`evt.repeat != 0`) are ignored for movement keys, because releasing `Shift`
while `W`/`A`/`S`/`D` is held can generate a repeated keydown with a different
symbol after the physical release, which would otherwise leave the key stuck.
One-shot actions (`E`, `F`, `I`, `Escape`) are still matched by `keysym.sym`.
#### CharacterSpawnerComponent
+9
View File
@@ -147,6 +147,14 @@ to avoid stuck keys caused by missed `keyReleased` events. One-shot action flags
### AnimationTreeSystem & Root Motion
- Animation trees are stored in the global `AnimationTreeRegistry`, persisted to
`animation_tree.json`. Each `AnimationTreeComponent` references a registry
tree by name only.
- Trees are authored in **Tools -> Animation Tree Registry**. Each registry
entry can select a skeleton source mesh from the `Characters` resource group;
this is used only by the editor to populate animation-name dropdowns.
- `AnimationTreeSystem` resolves the tree from the registry each frame and
evaluates it.
- Selects a root bone (`Root`, `mixamorig:Hips`, then `Spineroot`), freezes it,
and excludes it from animation via a blend mask.
- Computes `CharacterComponent::linearVelocity` from root-bone displacement when
@@ -202,6 +210,7 @@ restored to the spawner name so the character respawns correctly.
- Please ask questions if anything is unclear.
- Please update tests, documentation and examples every time API or modules and systems change.
Make sure documentation, tests and examples are always in sync.
- Lua examples are stored in lua-examples directory.
## Conventions & Caveats
+8 -3
View File
@@ -37,6 +37,7 @@ set(EDITSCENE_SOURCES
systems/StartupMenuSystem.cpp
systems/PauseMenuSystem.cpp
systems/ItemRegistry.cpp
systems/AnimationTreeRegistry.cpp
systems/ContainerStateRegistry.cpp
systems/ItemStateRegistry.cpp
systems/SaveLoadSystem.cpp
@@ -102,7 +103,8 @@ set(EDITSCENE_SOURCES
ui/CharacterSpawnerEditor.cpp
ui/CharacterIdentityEditor.cpp
ui/AnimationTreeEditor.cpp
ui/AnimationTreeTemplateEditor.cpp
ui/AnimationTreeNodeEditor.cpp
ui/AnimationTreeRegistryEditor.cpp
ui/CharacterEditor.cpp
ui/CellGridEditor.cpp
ui/LotEditor.cpp
@@ -144,7 +146,6 @@ set(EDITSCENE_SOURCES
components/CharacterSlotsModule.cpp
components/CharacterSpawnerModule.cpp
components/AnimationTreeModule.cpp
components/AnimationTreeTemplateModule.cpp
components/AnimationTree.cpp
components/CharacterModule.cpp
components/CellGridModule.cpp
@@ -171,6 +172,7 @@ set(EDITSCENE_SOURCES
lua/LuaState.cpp
lua/LuaEntityApi.cpp
lua/LuaComponentApi.cpp
lua/LuaAnimationTreeApi.cpp
lua/LuaEventApi.cpp
lua/LuaActionApi.cpp
lua/LuaBehaviorTreeApi.cpp
@@ -222,6 +224,7 @@ set(EDITSCENE_HEADERS
systems/StartupMenuSystem.hpp
systems/PauseMenuSystem.hpp
systems/ItemRegistry.hpp
systems/AnimationTreeRegistry.hpp
systems/ContainerStateRegistry.hpp
systems/ItemStateRegistry.hpp
systems/PlayerControllerSystem.hpp
@@ -305,7 +308,8 @@ set(EDITSCENE_HEADERS
ui/CharacterSpawnerEditor.hpp
ui/CharacterIdentityEditor.hpp
ui/AnimationTreeEditor.hpp
ui/AnimationTreeTemplateEditor.hpp
ui/AnimationTreeNodeEditor.hpp
ui/AnimationTreeRegistryEditor.hpp
ui/CharacterEditor.hpp
ui/CellGridEditor.hpp
ui/LotEditor.hpp
@@ -344,6 +348,7 @@ set(EDITSCENE_HEADERS
lua/LuaState.hpp
lua/LuaEntityApi.hpp
lua/LuaComponentApi.hpp
lua/LuaAnimationTreeApi.hpp
lua/LuaEventApi.hpp
lua/LuaActionApi.hpp
lua/LuaBehaviorTreeApi.hpp
+4 -4
View File
@@ -35,6 +35,7 @@
#include "systems/PauseMenuSystem.hpp"
#include "systems/DialogueSystem.hpp"
#include "systems/ItemRegistry.hpp"
#include "systems/AnimationTreeRegistry.hpp"
#include "systems/ContainerStateRegistry.hpp"
#include "systems/ItemStateRegistry.hpp"
#include "systems/CharacterClassSystem.hpp"
@@ -75,7 +76,6 @@
#include "components/CharacterIdentity.hpp"
#include "systems/CharacterRegistry.hpp"
#include "components/AnimationTree.hpp"
#include "components/AnimationTreeTemplate.hpp"
#include "components/Character.hpp"
#include "components/StartupMenu.hpp"
#include "components/PlayerController.hpp"
@@ -115,6 +115,7 @@
#include <SDL2/SDL.h>
#include "lua/LuaEntityApi.hpp"
#include "lua/LuaComponentApi.hpp"
#include "lua/LuaAnimationTreeApi.hpp"
#include "lua/LuaEventApi.hpp"
#include "lua/LuaActionApi.hpp"
#include "lua/LuaBehaviorTreeApi.hpp"
@@ -519,6 +520,7 @@ void EditorApp::setup()
PauseMenuSystem::getInstance().init(this);
static ItemRegistry s_itemRegistry;
ItemRegistry::getSingleton().initialize();
AnimationTreeRegistry::getSingleton().initialize();
ItemStateRegistry::getInstance().loadFromFile(
"item_state.json");
ContainerStateRegistry::getInstance().loadFromFile(
@@ -600,6 +602,7 @@ void EditorApp::setup()
// Component and Event APIs add to it.
editScene::registerLuaEntityApi(L);
editScene::registerLuaComponentApi(L);
editScene::registerLuaAnimationTreeApi(L);
editScene::registerLuaEventApi(L);
editScene::registerLuaActionApi(L);
editScene::registerLuaBehaviorTreeApi(L);
@@ -1224,9 +1227,6 @@ void EditorApp::setupECS()
// Register AnimationTree component
m_world.component<AnimationTreeComponent>();
// Register AnimationTreeTemplate component
m_world.component<AnimationTreeTemplate>();
// Register Character component
m_world.component<CharacterComponent>();
@@ -1,9 +1,61 @@
#include "AnimationTree.hpp"
AnimationTreeComponent::AnimationTreeComponent()
: root()
: treeName()
, enabled(true)
, useRootMotion(false)
, dirty(true)
, treeVersion(0)
{
}
nlohmann::json animationTreeNodeToJson(const AnimationTreeNode &node)
{
nlohmann::json json;
json["type"] = node.type;
if (!node.name.empty())
json["name"] = node.name;
if (!node.animationName.empty())
json["animationName"] = node.animationName;
if (node.speed != 1.0f)
json["speed"] = node.speed;
if (node.fadeSpeed != 7.5f)
json["fadeSpeed"] = node.fadeSpeed;
if (!node.endTransitions.empty()) {
nlohmann::json trans = nlohmann::json::object();
for (const auto &pair : node.endTransitions)
trans[pair.first] = pair.second;
json["endTransitions"] = trans;
}
if (!node.children.empty()) {
json["children"] = nlohmann::json::array();
for (const auto &child : node.children)
json["children"].push_back(animationTreeNodeToJson(child));
}
return json;
}
void animationTreeNodeFromJson(AnimationTreeNode &node,
const nlohmann::json &json)
{
node.type = json.value("type", "animation");
node.name = json.value("name", "");
node.animationName = json.value("animationName", "");
node.speed = json.value("speed", 1.0f);
node.fadeSpeed = json.value("fadeSpeed", 7.5f);
node.endTransitions.clear();
if (json.contains("endTransitions") &&
json["endTransitions"].is_object()) {
for (auto it = json["endTransitions"].begin();
it != json["endTransitions"].end(); ++it)
node.endTransitions[it.key()] = it.value();
}
node.children.clear();
if (json.contains("children") && json["children"].is_array()) {
for (const auto &childJson : json["children"]) {
AnimationTreeNode child;
animationTreeNodeFromJson(child, childJson);
node.children.push_back(child);
}
}
}
@@ -2,6 +2,7 @@
#define EDITSCENE_ANIMATIONTREE_HPP
#pragma once
#include <Ogre.h>
#include <nlohmann/json.hpp>
#include <vector>
#include <unordered_map>
@@ -138,6 +139,11 @@ struct AnimationTreeNode {
}
};
/* Serialization helpers shared between registry and scene serializer. */
nlohmann::json animationTreeNodeToJson(const AnimationTreeNode &node);
void animationTreeNodeFromJson(AnimationTreeNode &node,
const nlohmann::json &json);
/**
* Animation tree component for hierarchical state-machine-based animation.
*
@@ -145,16 +151,15 @@ struct AnimationTreeNode {
* which Ogre AnimationStates are active and their blend weights.
*/
struct AnimationTreeComponent {
AnimationTreeNode root;
/* Name of the tree in AnimationTreeRegistry. */
Ogre::String treeName;
bool enabled = true;
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: last resolved registry version (not serialized) */
uint64_t treeVersion = 0;
/* Runtime: current state of each state machine (not serialized) */
std::unordered_map<Ogre::String, Ogre::String> currentStates;
@@ -1,14 +1,14 @@
#include "AnimationTree.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/AnimationTreeEditor.hpp"
#include "../systems/AnimationTreeRegistry.hpp"
#include "Transform.hpp"
static AnimationTreeComponent createDefaultTree()
static Ogre::String createDefaultTree()
{
AnimationTreeComponent at;
at.root.type = "output";
at.root.speed = 1.0f;
AnimationTreeNode root;
root.type = "output";
root.speed = 1.0f;
AnimationTreeNode sm;
sm.type = "stateMachine";
@@ -25,9 +25,17 @@ static AnimationTreeComponent createDefaultTree()
state.children.push_back(anim);
sm.children.push_back(state);
at.root.children.push_back(sm);
root.children.push_back(sm);
return at;
AnimationTreeRegistry &reg = AnimationTreeRegistry::getSingleton();
Ogre::String name = reg.generateUniqueName("default");
AnimationTreeRegistry::AnimationTreeDefinition def;
def.name = name;
def.root = root;
reg.registerTree(def);
return name;
}
REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
@@ -45,7 +53,8 @@ REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
->createChildSceneNode();
e.set<TransformComponent>(transform);
}
AnimationTreeComponent at = createDefaultTree();
AnimationTreeComponent at;
at.treeName = createDefaultTree();
at.dirty = true;
e.set<AnimationTreeComponent>(at);
},
@@ -1,20 +0,0 @@
#ifndef EDITSCENE_ANIMATIONTREETEMPLATE_HPP
#define EDITSCENE_ANIMATIONTREETEMPLATE_HPP
#pragma once
#include <Ogre.h>
/**
* 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
@@ -1,23 +0,0 @@
#include "AnimationTreeTemplate.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/AnimationTreeTemplateEditor.hpp"
REGISTER_COMPONENT_GROUP("Animation Tree Template", "Animation",
AnimationTreeTemplate, AnimationTreeTemplateEditor)
{
registry.registerComponent<AnimationTreeTemplate>(
AnimationTreeTemplate_name, AnimationTreeTemplate_group,
std::make_unique<AnimationTreeTemplateEditor>(),
// Adder
[](flecs::entity e) {
if (!e.has<AnimationTreeTemplate>()) {
e.set<AnimationTreeTemplate>({});
}
},
// Remover
[](flecs::entity e) {
if (e.has<AnimationTreeTemplate>()) {
e.remove<AnimationTreeTemplate>();
}
});
}
@@ -0,0 +1,79 @@
-- =============================================================================
-- Animation Tree Registry Lua API Examples
-- =============================================================================
-- Animation trees are stored in the Animation Tree Registry
-- (animation_tree.json). They are referenced from entities by name and edited
-- in the registry editor (Tools -> Animation Tree Registry).
--
-- The Lua API lets you create, query and remove registry entries.
-- =============================================================================
-- Register a simple humanoid locomotion tree.
-- skeletonSource is optional; when set, the registry editor loads that mesh
-- from the Characters resource group to populate animation-name dropdowns.
ecs.animation_trees.register("humanoid", {
skeletonSource = "male_Face.mesh",
root = {
type = "output",
speed = 1.0,
children = {
{
type = "stateMachine",
name = "main",
fadeSpeed = 7.5,
children = {
{
type = "state",
name = "idle",
children = {
{ type = "animation", animationName = "idle" }
}
},
{
type = "state",
name = "walk",
children = {
{ type = "animation", animationName = "walk" }
}
},
{
type = "state",
name = "run",
children = {
{ type = "animation", animationName = "run" }
}
}
},
endTransitions = {
-- auto-transition when the source state's animation ends
idle = "walk"
}
}
}
}
})
-- List all registered trees
local trees = ecs.animation_trees.list()
for _, name in ipairs(trees) do
print("Registered animation tree: " .. name)
end
-- Query a tree definition
local def = ecs.animation_trees.find("humanoid")
if def then
print("Tree: " .. def.name)
print("Skeleton source: " .. def.skeletonSource)
print("Root type: " .. def.root.type)
end
-- Attach the tree to an entity
local entity = ecs.create_entity()
ecs.set_component(entity, "AnimationTree", {
treeName = "humanoid",
enabled = true,
useRootMotion = false
})
-- Persist the registry to disk
ecs.animation_trees.save()
@@ -551,15 +551,44 @@ local chair = create_furniture("Wooden Chair", "chair.mesh", "seating")
-- Working with Animation Components
-- =============================================================================
-- Animation trees are authored in the Animation Tree Registry
-- (Tools -> Animation Tree Registry). Entities reference a registry tree by
-- name. The registry can also be manipulated from Lua:
ecs.animation_trees.register("humanoid", {
skeletonSource = "male_Face.mesh",
root = {
type = "output",
children = {
{
type = "stateMachine",
name = "main",
fadeSpeed = 7.5,
children = {
{
type = "state",
name = "idle",
children = {
{ type = "animation", animationName = "idle" }
}
},
{
type = "state",
name = "walk",
children = {
{ type = "animation", animationName = "walk" }
}
}
}
}
}
}
})
-- Add animation tree to a character:
ecs.set_component(npc, "AnimationTree", {
treeName = "humanoid",
enabled = true
})
ecs.set_component(npc, "AnimationTreeTemplate", {
templateName = "humanoid_base",
blendTime = 0.2
enabled = true,
useRootMotion = false
})
-- =============================================================================
@@ -0,0 +1,217 @@
#include "LuaAnimationTreeApi.hpp"
#include "../systems/AnimationTreeRegistry.hpp"
#include "../components/AnimationTree.hpp"
#include <lua.hpp>
namespace editScene
{
static void pushAnimationTreeNode(lua_State *L,
const AnimationTreeNode &node);
static bool readAnimationTreeNode(lua_State *L, int idx,
AnimationTreeNode &node);
static void pushAnimationTreeNode(lua_State *L,
const AnimationTreeNode &node)
{
lua_newtable(L);
lua_pushstring(L, node.type.c_str());
lua_setfield(L, -2, "type");
if (!node.name.empty()) {
lua_pushstring(L, node.name.c_str());
lua_setfield(L, -2, "name");
}
if (!node.animationName.empty()) {
lua_pushstring(L, node.animationName.c_str());
lua_setfield(L, -2, "animationName");
}
if (node.speed != 1.0f) {
lua_pushnumber(L, node.speed);
lua_setfield(L, -2, "speed");
}
if (node.fadeSpeed != 7.5f) {
lua_pushnumber(L, node.fadeSpeed);
lua_setfield(L, -2, "fadeSpeed");
}
if (!node.endTransitions.empty()) {
lua_newtable(L);
for (const auto &pair : node.endTransitions) {
lua_pushstring(L, pair.second.c_str());
lua_setfield(L, -2, pair.first.c_str());
}
lua_setfield(L, -2, "endTransitions");
}
if (!node.children.empty()) {
lua_newtable(L);
for (size_t i = 0; i < node.children.size(); ++i) {
pushAnimationTreeNode(L, node.children[i]);
lua_rawseti(L, -2, (int)i + 1);
}
lua_setfield(L, -2, "children");
}
}
static bool readAnimationTreeNode(lua_State *L, int idx,
AnimationTreeNode &node)
{
if (!lua_istable(L, idx))
return false;
node.type = "animation";
if (lua_getfield(L, idx, "type"), lua_isstring(L, -1))
node.type = lua_tostring(L, -1);
lua_pop(L, 1);
node.name = "";
if (lua_getfield(L, idx, "name"), lua_isstring(L, -1))
node.name = lua_tostring(L, -1);
lua_pop(L, 1);
node.animationName = "";
if (lua_getfield(L, idx, "animationName"), lua_isstring(L, -1))
node.animationName = lua_tostring(L, -1);
lua_pop(L, 1);
node.speed = 1.0f;
if (lua_getfield(L, idx, "speed"), lua_isnumber(L, -1))
node.speed = (float)lua_tonumber(L, -1);
lua_pop(L, 1);
node.fadeSpeed = 7.5f;
if (lua_getfield(L, idx, "fadeSpeed"), lua_isnumber(L, -1))
node.fadeSpeed = (float)lua_tonumber(L, -1);
lua_pop(L, 1);
node.endTransitions.clear();
if (lua_getfield(L, idx, "endTransitions"), lua_istable(L, -1)) {
lua_pushnil(L);
while (lua_next(L, -2) != 0) {
if (lua_isstring(L, -2) && lua_isstring(L, -1)) {
node.endTransitions[lua_tostring(L, -2)] =
lua_tostring(L, -1);
}
lua_pop(L, 1);
}
}
lua_pop(L, 1);
node.children.clear();
if (lua_getfield(L, idx, "children"), lua_istable(L, -1)) {
int childIdx = 1;
while (true) {
lua_rawgeti(L, -1, childIdx++);
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
break;
}
AnimationTreeNode child;
if (readAnimationTreeNode(L, lua_gettop(L), child))
node.children.push_back(child);
lua_pop(L, 1);
}
}
lua_pop(L, 1);
return true;
}
static int luaAnimationTreeRegister(lua_State *L)
{
if (lua_gettop(L) < 2 || !lua_isstring(L, 1) || !lua_istable(L, 2))
return 0;
Ogre::String name = lua_tostring(L, 1);
AnimationTreeRegistry::AnimationTreeDefinition def;
def.name = name;
if (lua_getfield(L, 2, "skeletonSource"), lua_isstring(L, -1))
def.skeletonSource = lua_tostring(L, -1);
lua_pop(L, 1);
if (lua_getfield(L, 2, "root"), lua_istable(L, -1))
readAnimationTreeNode(L, lua_gettop(L), def.root);
lua_pop(L, 1);
AnimationTreeRegistry::getSingleton().registerTree(def);
return 0;
}
static int luaAnimationTreeFind(lua_State *L)
{
if (lua_gettop(L) < 1 || !lua_isstring(L, 1))
return 0;
Ogre::String name = lua_tostring(L, 1);
const AnimationTreeRegistry::AnimationTreeDefinition *def =
AnimationTreeRegistry::getSingleton().findTree(name);
if (!def)
return 0;
lua_newtable(L);
lua_pushstring(L, def->name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, def->skeletonSource.c_str());
lua_setfield(L, -2, "skeletonSource");
pushAnimationTreeNode(L, def->root);
lua_setfield(L, -2, "root");
return 1;
}
static int luaAnimationTreeList(lua_State *L)
{
lua_newtable(L);
std::vector<Ogre::String> names =
AnimationTreeRegistry::getSingleton().getTreeNames();
std::sort(names.begin(), names.end());
for (size_t i = 0; i < names.size(); ++i) {
lua_pushstring(L, names[i].c_str());
lua_rawseti(L, -2, (int)i + 1);
}
return 1;
}
static int luaAnimationTreeRemove(lua_State *L)
{
if (lua_gettop(L) < 1 || !lua_isstring(L, 1))
return 0;
AnimationTreeRegistry::getSingleton().removeTree(lua_tostring(L, 1));
return 0;
}
static int luaAnimationTreeSave(lua_State *L)
{
(void)L;
bool ok = AnimationTreeRegistry::getSingleton().saveToFile(
AnimationTreeRegistry::defaultPath());
lua_pushboolean(L, ok ? 1 : 0);
return 1;
}
static int luaAnimationTreeLoad(lua_State *L)
{
(void)L;
bool ok = AnimationTreeRegistry::getSingleton().loadFromFile(
AnimationTreeRegistry::defaultPath());
lua_pushboolean(L, ok ? 1 : 0);
return 1;
}
void registerLuaAnimationTreeApi(lua_State *L)
{
lua_newtable(L);
lua_pushcfunction(L, luaAnimationTreeRegister);
lua_setfield(L, -2, "register");
lua_pushcfunction(L, luaAnimationTreeFind);
lua_setfield(L, -2, "find");
lua_pushcfunction(L, luaAnimationTreeList);
lua_setfield(L, -2, "list");
lua_pushcfunction(L, luaAnimationTreeRemove);
lua_setfield(L, -2, "remove");
lua_pushcfunction(L, luaAnimationTreeSave);
lua_setfield(L, -2, "save");
lua_pushcfunction(L, luaAnimationTreeLoad);
lua_setfield(L, -2, "load");
lua_setfield(L, -2, "animation_trees");
}
} // namespace editScene
@@ -0,0 +1,14 @@
#ifndef EDITSCENE_LUA_ANIMATIONTREE_API_HPP
#define EDITSCENE_LUA_ANIMATIONTREE_API_HPP
#pragma once
#include <lua.hpp>
namespace editScene
{
void registerLuaAnimationTreeApi(lua_State *L);
} // namespace editScene
#endif // EDITSCENE_LUA_ANIMATIONTREE_API_HPP
+7 -17
View File
@@ -37,7 +37,6 @@
#include "components/CharacterIdentity.hpp"
#include "systems/CharacterRegistry.hpp"
#include "components/AnimationTree.hpp"
#include "components/AnimationTreeTemplate.hpp"
#include "components/Character.hpp"
#include "components/StartupMenu.hpp"
#include "components/PlayerController.hpp"
@@ -595,31 +594,22 @@ static void registerAllComponents()
// --- AnimationTree ---
REGISTER_COMPONENT(
AnimationTreeComponent, "AnimationTree",
lua_pushstring(L, c.treeName.c_str());
lua_setfield(L, -2, "treeName");
lua_pushboolean(L, c.enabled ? 1 : 0);
lua_setfield(L, -2, "enabled");
lua_pushboolean(L, c.useRootMotion ? 1 : 0);
lua_setfield(L, -2, "useRootMotion");
lua_pushstring(L, c.templateName.c_str());
lua_setfield(L, -2, "templateName");
, if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
c.enabled = lua_toboolean(L, -1) != 0;
, if (lua_getfield(L, idx, "treeName"), lua_isstring(L, -1))
c.treeName = lua_tostring(L, -1);
lua_pop(L, 1);
if (lua_getfield(L, idx, "enabled"), lua_isboolean(L, -1))
c.enabled = lua_toboolean(L, -1) != 0;
lua_pop(L, 1);
if (lua_getfield(L, idx, "useRootMotion"), lua_isboolean(L, -1))
c.useRootMotion = lua_toboolean(L, -1) != 0;
lua_pop(L, 1);
if (lua_getfield(L, idx, "templateName"), lua_isstring(L, -1))
c.templateName = lua_tostring(L, -1);
lua_pop(L, 1););
// --- AnimationTreeTemplate ---
REGISTER_COMPONENT(AnimationTreeTemplate, "AnimationTreeTemplate",
lua_pushstring(L, c.name.c_str());
lua_setfield(L, -2, "name");
, if (lua_getfield(L, idx, "name"),
lua_isstring(L, -1))
c.name = lua_tostring(L, -1);
lua_pop(L, 1););
// --- BehaviorTree ---
REGISTER_COMPONENT(
BehaviorTreeComponent, "BehaviorTree",
@@ -24,7 +24,7 @@
* Supported component names (case-sensitive):
* "EntityName", "Transform", "Renderable", "Light", "Camera",
* "RigidBody", "PhysicsCollider", "Character", "CharacterSlots",
* "AnimationTree", "AnimationTreeTemplate", "BehaviorTree",
* "AnimationTree", "BehaviorTree",
* "GoapBlackboard", "GoapAction", "GoapGoal", "ActionDatabase",
* "ActionDebug", "SmartObject", "Actuator", "EventHandler",
* "GoapPlanner", "GoapRunner", "PathFollowing", "NavMesh",
@@ -0,0 +1,237 @@
#include "AnimationTreeRegistry.hpp"
#include <OgreLogManager.h>
#include <OgreMeshManager.h>
#include <OgreResourceGroupManager.h>
#include <OgreSkeleton.h>
#include <OgreAnimation.h>
#include <fstream>
#include <filesystem>
AnimationTreeRegistry *AnimationTreeRegistry::ms_singleton = nullptr;
AnimationTreeRegistry &AnimationTreeRegistry::getSingleton()
{
static AnimationTreeRegistry instance;
return instance;
}
AnimationTreeRegistry *AnimationTreeRegistry::getSingletonPtr()
{
return &getSingleton();
}
AnimationTreeRegistry::AnimationTreeRegistry()
: m_autoSavePath(defaultPath())
{
ms_singleton = this;
}
void AnimationTreeRegistry::initialize()
{
if (!std::filesystem::exists(m_autoSavePath))
return;
if (!loadFromFile(m_autoSavePath)) {
Ogre::LogManager::getSingleton().logMessage(
"AnimationTreeRegistry: auto-load failed: " +
m_lastError);
}
}
bool AnimationTreeRegistry::registerTree(const AnimationTreeDefinition &def)
{
bool wasNew = m_trees.find(def.name) == m_trees.end();
m_trees[def.name] = def;
if (wasNew)
Ogre::LogManager::getSingleton().logMessage(
"AnimationTreeRegistry: registered tree '" + def.name +
"'");
autoSave();
return wasNew;
}
bool AnimationTreeRegistry::removeTree(const Ogre::String &name)
{
if (m_trees.erase(name) == 0)
return false;
autoSave();
return true;
}
AnimationTreeRegistry::AnimationTreeDefinition *
AnimationTreeRegistry::findTree(const Ogre::String &name)
{
auto it = m_trees.find(name);
if (it != m_trees.end())
return &it->second;
return nullptr;
}
const AnimationTreeRegistry::AnimationTreeDefinition *
AnimationTreeRegistry::findTree(const Ogre::String &name) const
{
auto it = m_trees.find(name);
if (it != m_trees.end())
return &it->second;
return nullptr;
}
std::vector<Ogre::String> AnimationTreeRegistry::getTreeNames() const
{
std::vector<Ogre::String> names;
names.reserve(m_trees.size());
for (const auto &pair : m_trees)
names.push_back(pair.first);
return names;
}
Ogre::String AnimationTreeRegistry::generateUniqueName(const Ogre::String &base)
const
{
if (m_trees.find(base) == m_trees.end())
return base;
for (int i = 1; i < 10000; ++i) {
Ogre::String candidate = base + "_" + Ogre::StringConverter::toString(i);
if (m_trees.find(candidate) == m_trees.end())
return candidate;
}
return base + "_" + Ogre::StringConverter::toString(
(unsigned long long)this);
}
void AnimationTreeRegistry::markModified(const Ogre::String &name)
{
auto it = m_trees.find(name);
if (it != m_trees.end()) {
++it->second.version;
autoSave();
}
}
std::vector<Ogre::String>
AnimationTreeRegistry::getAvailableSkeletonSources() const
{
std::vector<Ogre::String> result;
Ogre::ResourceGroupManager &rgm =
Ogre::ResourceGroupManager::getSingleton();
try {
Ogre::StringVectorPtr names =
rgm.findResourceNames("Characters", "*.mesh");
if (names)
result.assign(names->begin(), names->end());
} catch (...) {
}
std::sort(result.begin(), result.end());
return result;
}
std::vector<Ogre::String>
AnimationTreeRegistry::getAnimationsForSource(const Ogre::String &source) const
{
std::vector<Ogre::String> result;
if (source.empty())
return result;
try {
Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load(
source, "Characters");
if (!mesh || !mesh->getSkeleton())
return result;
Ogre::SkeletonPtr skel = mesh->getSkeleton();
unsigned short numAnims = skel->getNumAnimations();
for (unsigned short i = 0; i < numAnims; ++i) {
Ogre::Animation *anim = skel->getAnimation(i);
if (anim)
result.push_back(anim->getName());
}
} catch (const std::exception &e) {
Ogre::LogManager::getSingleton().logMessage(
"AnimationTreeRegistry: failed to enumerate animations for '" +
source + "': " + e.what());
}
std::sort(result.begin(), result.end());
return result;
}
nlohmann::json AnimationTreeRegistry::serialize() const
{
nlohmann::json j;
j["version"] = "1.0";
j["trees"] = nlohmann::json::array();
for (const auto &pair : m_trees) {
const AnimationTreeDefinition &def = pair.second;
nlohmann::json treeJson;
treeJson["name"] = def.name;
treeJson["skeletonSource"] = def.skeletonSource;
treeJson["version"] = def.version;
treeJson["root"] = animationTreeNodeToJson(def.root);
j["trees"].push_back(treeJson);
}
return j;
}
void AnimationTreeRegistry::deserialize(const nlohmann::json &j)
{
m_trees.clear();
if (!j.contains("trees") || !j["trees"].is_array())
return;
for (const auto &treeJson : j["trees"]) {
if (!treeJson.contains("name"))
continue;
AnimationTreeDefinition def;
def.name = treeJson.value("name", "");
def.skeletonSource = treeJson.value("skeletonSource", "");
def.version = treeJson.value("version", 1);
if (treeJson.contains("root"))
animationTreeNodeFromJson(def.root, treeJson["root"]);
if (!def.name.empty())
m_trees[def.name] = def;
}
}
bool AnimationTreeRegistry::saveToFile(const std::string &filepath)
{
try {
std::ofstream file(filepath);
if (!file.is_open()) {
m_lastError = "Failed to open file for writing: " + filepath;
return false;
}
file << serialize().dump(4);
file.close();
return true;
} catch (const std::exception &e) {
m_lastError = std::string("Save error: ") + e.what();
return false;
}
}
bool AnimationTreeRegistry::loadFromFile(const std::string &filepath)
{
try {
if (!std::filesystem::exists(filepath)) {
m_lastError = "File not found: " + filepath;
return false;
}
std::ifstream file(filepath);
if (!file.is_open()) {
m_lastError = "Failed to open file for reading: " + filepath;
return false;
}
nlohmann::json j;
file >> j;
file.close();
deserialize(j);
return true;
} catch (const std::exception &e) {
m_lastError = std::string("Load error: ") + e.what();
return false;
}
}
void AnimationTreeRegistry::autoSave()
{
if (!m_autoSavePath.empty())
saveToFile(m_autoSavePath);
}
@@ -0,0 +1,108 @@
#ifndef EDITSCENE_ANIMATIONTREEREGISTRY_HPP
#define EDITSCENE_ANIMATIONTREEREGISTRY_HPP
#pragma once
#include "../components/AnimationTree.hpp"
#include <Ogre.h>
#include <nlohmann/json.hpp>
#include <string>
#include <unordered_map>
#include <vector>
/**
* Global animation tree registry.
*
* Holds named animation tree definitions. Each tree is referenced by name
* from AnimationTreeComponent. Trees are edited in the Animation Tree
* Registry window (Tools menu) and persisted to animation_tree.json.
*/
class AnimationTreeRegistry {
public:
static AnimationTreeRegistry &getSingleton();
static AnimationTreeRegistry *getSingletonPtr();
private:
static AnimationTreeRegistry *ms_singleton;
public:
struct AnimationTreeDefinition {
Ogre::String name;
Ogre::String skeletonSource;
AnimationTreeNode root;
uint64_t version = 1;
};
AnimationTreeRegistry();
~AnimationTreeRegistry() = default;
AnimationTreeRegistry(const AnimationTreeRegistry &) = delete;
AnimationTreeRegistry &operator=(const AnimationTreeRegistry &) = delete;
/* ------------------------------------------------------------------ */
/* Life-cycle */
/* ------------------------------------------------------------------ */
void initialize();
/* ------------------------------------------------------------------ */
/* Definitions */
/* ------------------------------------------------------------------ */
bool registerTree(const AnimationTreeDefinition &def);
bool removeTree(const Ogre::String &name);
AnimationTreeDefinition *findTree(const Ogre::String &name);
const AnimationTreeDefinition *findTree(const Ogre::String &name) const;
const std::unordered_map<Ogre::String, AnimationTreeDefinition> &
getTrees() const
{
return m_trees;
}
std::vector<Ogre::String> getTreeNames() const;
bool hasTree(const Ogre::String &name) const
{
return m_trees.find(name) != m_trees.end();
}
/* Generate a unique name from a base prefix. */
Ogre::String generateUniqueName(const Ogre::String &base) const;
/* Bump version and auto-save. */
void markModified(const Ogre::String &name);
/* ------------------------------------------------------------------ */
/* Skeleton source helpers */
/* ------------------------------------------------------------------ */
std::vector<Ogre::String> getAvailableSkeletonSources() const;
std::vector<Ogre::String> getAnimationsForSource(
const Ogre::String &source) const;
/* ------------------------------------------------------------------ */
/* Persistence */
/* ------------------------------------------------------------------ */
nlohmann::json serialize() const;
void deserialize(const nlohmann::json &j);
bool saveToFile(const std::string &filepath);
bool loadFromFile(const std::string &filepath);
const std::string &getLastError() const
{
return m_lastError;
}
void autoSave();
/* Default file path used by initialize()/autoSave(). */
static const char *defaultPath()
{
return "animation_tree.json";
}
private:
std::unordered_map<Ogre::String, AnimationTreeDefinition> m_trees;
std::string m_autoSavePath;
std::string m_lastError;
};
#endif // EDITSCENE_ANIMATIONTREEREGISTRY_HPP
@@ -1,4 +1,5 @@
#include "AnimationTreeSystem.hpp"
#include "AnimationTreeRegistry.hpp"
#include "CharacterSlotSystem.hpp"
#include "../components/Transform.hpp"
#include "../components/Renderable.hpp"
@@ -99,7 +100,8 @@ Ogre::Entity *AnimationTreeSystem::findAnimatedEntity(flecs::entity e)
}
bool AnimationTreeSystem::setupEntity(flecs::entity e,
AnimationTreeComponent &at)
AnimationTreeComponent &at,
EntityAnimTreeState &state)
{
Ogre::Entity *ent = findAnimatedEntity(e);
std::cout << "AnimationTreeSystem::setupEntity: entity=" << e.id()
@@ -111,7 +113,6 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
return false;
}
EntityAnimTreeState &state = m_states[e.id()];
/* Preserve root-motion tracking state across rebuilds so
* setupEntity() does not produce a one-frame zero-delta
* stutter when the mesh is recreated by CharacterSlotSystem.
@@ -239,7 +240,8 @@ bool AnimationTreeSystem::setupEntity(flecs::entity e,
}
/* Initialize default state machine states */
initializeTreeStates(at.root, at);
if (state.resolvedRoot)
initializeTreeStates(*state.resolvedRoot, at);
std::cout
<< " setupEntity done: animations=" << state.animations.size()
<< " rootBone="
@@ -326,35 +328,29 @@ void AnimationTreeSystem::initializeTreeStates(const AnimationTreeNode &node,
initializeTreeStates(child, at);
}
void AnimationTreeSystem::resolveTemplate(AnimationTreeComponent &at)
const AnimationTreeNode *
AnimationTreeSystem::getResolvedRoot(const AnimationTreeComponent &at) const
{
if (at.templateName.empty())
if (at.treeName.empty())
return nullptr;
const AnimationTreeRegistry::AnimationTreeDefinition *def =
AnimationTreeRegistry::getSingleton().findTree(at.treeName);
if (!def)
return nullptr;
return &def->root;
}
void AnimationTreeSystem::resolveTree(AnimationTreeComponent &at,
EntityAnimTreeState &state)
{
state.resolvedRoot = getResolvedRoot(at);
if (!state.resolvedRoot)
return;
AnimationTreeTemplate *templ = nullptr;
AnimationTreeComponent *templAt = nullptr;
m_world.query<AnimationTreeTemplate, AnimationTreeComponent>().each(
[&](flecs::entity, AnimationTreeTemplate &t,
AnimationTreeComponent &ta) {
if (t.name == at.templateName) {
templ = &t;
templAt = &ta;
}
});
if (!templ) {
m_world.query<AnimationTreeTemplate>().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;
const AnimationTreeRegistry::AnimationTreeDefinition *def =
AnimationTreeRegistry::getSingleton().findTree(at.treeName);
if (def && at.treeVersion != def->version) {
at.treeVersion = def->version;
at.dirty = true;
}
}
@@ -374,31 +370,38 @@ void AnimationTreeSystem::update(float deltaTime)
if (e.has<CharacterComponent>())
e.get_mut<CharacterComponent>().linearVelocity =
Ogre::Vector3::ZERO;
resolveTemplate(at);
auto stateIt = m_states.find(e.id());
if (stateIt == m_states.end()) {
EntityAnimTreeState newState;
stateIt = m_states.emplace(e.id(), newState).first;
}
EntityAnimTreeState &state = stateIt->second;
resolveTree(at, state);
if (!state.resolvedRoot) {
/* No registry entry for this tree name. */
return;
}
if (at.dirty) {
if (setupEntity(e, at))
if (setupEntity(e, at, state))
at.dirty = false;
}
auto it = m_states.find(e.id());
if (it == m_states.end())
if (!state.ogreEntity)
return;
/* Validate cached entity pointer -
* CharacterSlotSystem rebuilds can destroy and
* recreate the Ogre::Entity */
Ogre::Entity *currentEnt = findAnimatedEntity(e);
if (currentEnt != it->second.ogreEntity) {
if (!setupEntity(e, at)) {
return;
}
it = m_states.find(e.id());
if (it == m_states.end())
if (currentEnt != state.ogreEntity) {
if (!setupEntity(e, at, state))
return;
}
EntityAnimTreeState &state = it->second;
if (!state.ogreEntity)
return;
@@ -431,7 +434,7 @@ void AnimationTreeSystem::update(float deltaTime)
/* Evaluate tree */
EvalContext ctx;
ctx.deltaTime = deltaTime;
evaluateNode(at.root, 1.0f, 1.0f, at, state, ctx);
evaluateNode(*state.resolvedRoot, 1.0f, 1.0f, at, state, ctx);
/* Apply animation weights and advance */
Ogre::Vector3 totalRootMotion = Ogre::Vector3::ZERO;
@@ -758,8 +761,11 @@ void AnimationTreeSystem::checkEndTransitions(flecs::entity e,
const Ogre::String &smName = pair.first;
const Ogre::String &stateName = pair.second;
const AnimationTreeNode *root = getResolvedRoot(at);
if (!root)
continue;
const AnimationTreeNode *smNode =
findStateMachineNode(at.root, smName);
findStateMachineNode(*root, smName);
if (!smNode)
continue;
@@ -837,8 +843,11 @@ void AnimationTreeSystem::setStateInternal(flecs::entity e,
if (reset) {
/* Reset the new state's animation */
const AnimationTreeNode *smNode =
findStateMachineNode(at.root, stateMachineName);
const AnimationTreeNode *root = getResolvedRoot(at);
const AnimationTreeNode *smNode = root ?
findStateMachineNode(*root,
stateMachineName) :
nullptr;
if (smNode) {
const AnimationTreeNode *stNode =
findStateNode(*smNode, stateName);
@@ -9,7 +9,7 @@
#include <set>
#include "../components/AnimationTree.hpp"
#include "../components/AnimationTreeTemplate.hpp"
#include "AnimationTreeRegistry.hpp"
/**
@@ -89,6 +89,9 @@ private:
std::unordered_map<Ogre::String, FadeInfo> fadeStates;
/* Track previous currentStates to detect external changes */
std::unordered_map<Ogre::String, Ogre::String> prevStates;
/* Cached root from the registry. Not owned. */
const AnimationTreeNode *resolvedRoot = nullptr;
};
struct AnimEvalData {
@@ -102,7 +105,8 @@ private:
std::vector<std::pair<Ogre::String, Ogre::String> > endChecks;
};
bool setupEntity(flecs::entity e, AnimationTreeComponent &at);
bool setupEntity(flecs::entity e, AnimationTreeComponent &at,
EntityAnimTreeState &state);
void teardownEntity(flecs::entity e);
void disableAllAnimations(EntityAnimTreeState &state);
void initializeTreeStates(const AnimationTreeNode &node,
@@ -119,8 +123,11 @@ private:
const Ogre::String &stateMachineName,
const Ogre::String &stateName, bool reset);
/* Resolve template reference and copy tree if template changed */
void resolveTemplate(AnimationTreeComponent &at);
/* Resolve tree reference from registry and copy tree if version changed */
void resolveTree(AnimationTreeComponent &at, EntityAnimTreeState &state);
const AnimationTreeNode *getResolvedRoot(
const AnimationTreeComponent &at) const;
const AnimationTreeNode *
findStateMachineNode(const AnimationTreeNode &root,
@@ -428,8 +428,12 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
* lookup on the same entity. */
if (e.has<AnimationTreeComponent>()) {
auto &at = e.get<AnimationTreeComponent>();
const AnimationTreeRegistry::AnimationTreeDefinition *def =
AnimationTreeRegistry::getSingleton().findTree(
at.treeName);
const AnimationTreeNode *smNode = nullptr;
for (const auto &child : at.root.children) {
if (def) {
for (const auto &child : def->root.children) {
if (child.type == "stateMachine" &&
child.name == node.name) {
smNode = &child;
@@ -442,6 +446,7 @@ BehaviorTreeSystem::evaluateNode(const BehaviorTreeNode &node, flecs::entity e,
break;
}
}
}
if (smNode) {
for (const auto &st : smNode->children) {
if (st.type == "state" &&
@@ -30,7 +30,6 @@
#include "../components/CharacterSpawner.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"
@@ -382,6 +381,12 @@ void EditorUISystem::update(float deltaTime)
ItemRegistry::getSingleton().drawEditor(&m_showItemRegistry);
}
// Render Animation Tree Registry window
if (m_showAnimationTreeRegistry) {
m_animationTreeRegistryEditor.render(
&m_showAnimationTreeRegistry);
}
// Render Inventory Dialog Config window
if (m_showInventoryConfig) {
renderInventoryConfigWindow();
@@ -490,6 +495,9 @@ void EditorUISystem::renderHierarchyWindow()
if (ImGui::MenuItem("Item Registry")) {
m_showItemRegistry = true;
}
if (ImGui::MenuItem("Animation Tree Registry")) {
m_showAnimationTreeRegistry = true;
}
ImGui::Separator();
if (ImGui::MenuItem(
"Inventory Dialog Config")) {
@@ -1098,14 +1106,6 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
componentCount++;
}
// Render AnimationTreeTemplate if present
if (entity.has<AnimationTreeTemplate>()) {
auto &templ = entity.get_mut<AnimationTreeTemplate>();
m_componentRegistry.render<AnimationTreeTemplate>(entity,
templ);
componentCount++;
}
// Render StartupMenu if present
if (entity.has<StartupMenuComponent>()) {
auto &sm = entity.get_mut<StartupMenuComponent>();
@@ -10,6 +10,7 @@
#include "../ui/ComponentRegistry.hpp"
#include "../ui/ActionDatabaseSingletonEditor.hpp"
#include "../ui/CharacterClassDatabaseEditor.hpp"
#include "../ui/AnimationTreeRegistryEditor.hpp"
#include "../components/EntityName.hpp"
#include "../gizmo/Gizmo.hpp"
#include "../gizmo/Cursor3D.hpp"
@@ -347,6 +348,10 @@ private:
// Item registry
bool m_showItemRegistry = false;
// Animation tree registry
bool m_showAnimationTreeRegistry = false;
AnimationTreeRegistryEditor m_animationTreeRegistryEditor;
// Queries
flecs::query<EntityNameComponent> m_nameQuery;
@@ -24,7 +24,8 @@
#include "../components/CharacterIdentity.hpp"
#include "CharacterRegistry.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/AnimationTreeTemplate.hpp"
#include "AnimationTreeRegistry.hpp"
#include "AnimationTreeRegistry.hpp"
#include "../components/StartupMenu.hpp"
#include "../components/PlayerController.hpp"
#include "../components/CellGrid.hpp"
@@ -263,11 +264,6 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
json["animationTree"] = serializeAnimationTree(entity);
}
if (entity.has<AnimationTreeTemplate>()) {
json["animationTreeTemplate"] =
serializeAnimationTreeTemplate(entity);
}
if (entity.has<StartupMenuComponent>()) {
json["startupMenu"] = serializeStartupMenu(entity);
}
@@ -2294,11 +2290,6 @@ void SceneSerializer::deserializeCharacterSpawner(flecs::entity entity,
entity.set<CharacterSpawnerComponent>(spawner);
}
/* Forward declarations for AnimationTreeNode serialization */
static nlohmann::json serializeAnimationTreeNode(const AnimationTreeNode &node);
static void deserializeAnimationTreeNode(AnimationTreeNode &node,
const nlohmann::json &json);
nlohmann::json SceneSerializer::serializeCharacterSlots(flecs::entity entity)
{
/* CharacterSlotsComponent is deprecated and no longer saved. */
@@ -2379,65 +2370,36 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity,
CharacterRegistry::getSingleton().markCharacterDirty(registryId);
}
static nlohmann::json serializeAnimationTreeNode(const AnimationTreeNode &node)
/* Migrate a legacy inline animation tree into the registry and return
* the registry name to use. */
static Ogre::String migrateInlineAnimationTree(
const nlohmann::json &json, const Ogre::String &preferredName)
{
nlohmann::json json;
json["type"] = node.type;
if (!node.name.empty())
json["name"] = node.name;
if (!node.animationName.empty())
json["animationName"] = node.animationName;
if (node.speed != 1.0f)
json["speed"] = node.speed;
if (node.fadeSpeed != 7.5f)
json["fadeSpeed"] = node.fadeSpeed;
if (!node.endTransitions.empty()) {
json["endTransitions"] = nlohmann::json::object();
for (const auto &pair : node.endTransitions)
json["endTransitions"][pair.first] = pair.second;
}
if (!node.children.empty()) {
json["children"] = nlohmann::json::array();
for (const auto &child : node.children)
json["children"].push_back(
serializeAnimationTreeNode(child));
}
return json;
}
AnimationTreeNode root;
if (json.contains("root"))
animationTreeNodeFromJson(root, json["root"]);
static void deserializeAnimationTreeNode(AnimationTreeNode &node,
const nlohmann::json &json)
{
node.type = json.value("type", "animation");
node.name = json.value("name", "");
node.animationName = json.value("animationName", "");
node.speed = json.value("speed", 1.0f);
node.fadeSpeed = json.value("fadeSpeed", 7.5f);
node.endTransitions.clear();
if (json.contains("endTransitions") &&
json["endTransitions"].is_object()) {
for (auto &[key, val] : json["endTransitions"].items())
node.endTransitions[key] = val.get<std::string>();
}
node.children.clear();
if (json.contains("children") && json["children"].is_array()) {
for (const auto &childJson : json["children"]) {
AnimationTreeNode child;
deserializeAnimationTreeNode(child, childJson);
node.children.push_back(child);
}
}
AnimationTreeRegistry &reg = AnimationTreeRegistry::getSingleton();
Ogre::String name = preferredName;
if (name.empty())
name = reg.generateUniqueName("migrated");
else if (reg.hasTree(name))
name = reg.generateUniqueName(name);
AnimationTreeRegistry::AnimationTreeDefinition def;
def.name = name;
def.root = root;
reg.registerTree(def);
return name;
}
nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity)
{
auto &at = entity.get<AnimationTreeComponent>();
nlohmann::json json;
json["treeName"] = at.treeName;
json["enabled"] = at.enabled;
json["useRootMotion"] = at.useRootMotion;
if (!at.templateName.empty())
json["templateName"] = at.templateName;
json["root"] = serializeAnimationTreeNode(at.root);
return json;
}
@@ -2447,30 +2409,70 @@ 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;
if (json.contains("treeName")) {
/* Current format. */
at.treeName = json.value("treeName", "");
} else if (json.contains("root")) {
/* Legacy inline tree: register in registry. */
Ogre::String preferred = json.value("templateName", "");
at.treeName = migrateInlineAnimationTree(json, preferred);
} else if (json.contains("templateName")) {
/* Legacy template reference: ensure registry entry exists. */
at.treeName = json.value("templateName", "");
AnimationTreeRegistry &reg =
AnimationTreeRegistry::getSingleton();
if (!at.treeName.empty() && !reg.hasTree(at.treeName)) {
AnimationTreeRegistry::AnimationTreeDefinition def;
def.name = at.treeName;
reg.registerTree(def);
}
}
entity.set<AnimationTreeComponent>(at);
}
nlohmann::json
SceneSerializer::serializeAnimationTreeTemplate(flecs::entity entity)
{
auto &templ = entity.get<AnimationTreeTemplate>();
nlohmann::json json;
json["name"] = templ.name;
json["version"] = templ.version;
return json;
/* AnimationTreeTemplate is no longer saved. */
(void)entity;
return nlohmann::json::object();
}
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<AnimationTreeTemplate>(templ);
/* AnimationTreeTemplate has been migrated into AnimationTreeRegistry.
* If the entity already has an AnimationTreeComponent, update its
* treeName to the template name and copy the tree data into the
* registry. Otherwise just ensure a registry placeholder exists. */
Ogre::String templName = json.value("name", "");
if (templName.empty())
return;
AnimationTreeRegistry &reg = AnimationTreeRegistry::getSingleton();
if (entity.has<AnimationTreeComponent>()) {
auto &at = entity.get_mut<AnimationTreeComponent>();
const AnimationTreeRegistry::AnimationTreeDefinition *def =
reg.findTree(at.treeName);
if (def) {
AnimationTreeRegistry::AnimationTreeDefinition renamed =
*def;
renamed.name = templName;
reg.removeTree(at.treeName);
reg.registerTree(renamed);
}
at.treeName = templName;
} else {
if (!reg.hasTree(templName)) {
AnimationTreeRegistry::AnimationTreeDefinition def;
def.name = templName;
reg.registerTree(def);
}
}
}
// ============================================================================
@@ -133,6 +133,7 @@ private:
namespace editScene
{
void registerLuaComponentApi(lua_State *L);
void registerLuaAnimationTreeApi(lua_State *L);
void registerLuaEntityApi(lua_State *L);
void registerLuaDialogueApi(lua_State *L);
}
@@ -1420,12 +1421,14 @@ static int testAnimationTreeComponent(lua_State *L)
"local id = ecs.create_entity();"
"ecs.set_component(id, 'AnimationTree', {"
" treeName = 'humanoid',"
" enabled = true"
" enabled = true,"
" useRootMotion = false"
"});"
"local a = ecs.get_component(id, 'AnimationTree');"
"assert(a ~= nil, 'AnimationTree should exist');"
"assert(a.treeName == 'humanoid', 'wrong treeName');"
"assert(a.enabled == true, 'wrong enabled')");
"assert(a.enabled == true, 'wrong enabled');"
"assert(a.useRootMotion == false, 'wrong useRootMotion')");
if (!ok)
FAIL("AnimationTree component assertion failed");
@@ -1434,26 +1437,47 @@ static int testAnimationTreeComponent(lua_State *L)
}
// ---------------------------------------------------------------------------
// Test 48: AnimationTreeTemplate component
// Test 48: Animation Tree Registry Lua API
// ---------------------------------------------------------------------------
static int testAnimationTreeTemplateComponent(lua_State *L)
static int testAnimationTreeRegistryApi(lua_State *L)
{
TEST("AnimationTreeTemplate component");
TEST("Animation Tree Registry API");
bool ok = runLua(
L,
"local id = ecs.create_entity();"
"ecs.set_component(id, 'AnimationTreeTemplate', {"
" templateName = 'humanoid_base',"
" blendTime = 0.2"
"});"
"local a = ecs.get_component(id, 'AnimationTreeTemplate');"
"assert(a ~= nil, 'AnimationTreeTemplate should exist');"
"assert(a.templateName == 'humanoid_base', 'wrong templateName');"
"assert(a.blendTime == 0.2, 'wrong blendTime')");
bool ok = runLua(L,
"ecs.animation_trees.register('test_humanoid', {"
" skeletonSource = 'male_Face.mesh',"
" root = {"
" type = 'output',"
" children = {"
" { type = 'stateMachine', name = 'main', fadeSpeed = 7.5,"
" children = {"
" { type = 'state', name = 'idle',"
" children = {"
" { type = 'animation', animationName = 'idle' }"
" }"
" }"
" }"
" }"
" }"
" }"
"}); "
"local trees = ecs.animation_trees.list(); "
"assert(#trees >= 1, 'list should contain at least one tree'); "
"local found = false; "
"for _, name in ipairs(trees) do "
" if name == 'test_humanoid' then found = true end; "
"end; "
"assert(found, 'test_humanoid not found in list'); "
"local def = ecs.animation_trees.find('test_humanoid'); "
"assert(def ~= nil, 'find should return definition'); "
"assert(def.name == 'test_humanoid', 'wrong def name'); "
"assert(def.skeletonSource == 'male_Face.mesh', 'wrong skeletonSource'); "
"assert(def.root.type == 'output', 'wrong root type'); "
"ecs.animation_trees.remove('test_humanoid'); "
"assert(ecs.animation_trees.find('test_humanoid') == nil, 'remove failed')");
if (!ok)
FAIL("AnimationTreeTemplate component assertion failed");
FAIL("Animation Tree Registry API assertion failed");
PASS();
return 0;
@@ -1830,6 +1854,7 @@ int main()
// Register the entity and component APIs
editScene::registerLuaEntityApi(L);
editScene::registerLuaComponentApi(L);
editScene::registerLuaAnimationTreeApi(L);
editScene::registerLuaDialogueApi(L);
// Run tests
@@ -1881,7 +1906,7 @@ int main()
failures += testTriangleBufferComponent(L);
failures += testCharacterSpawnerComponent(L);
failures += testAnimationTreeComponent(L);
failures += testAnimationTreeTemplateComponent(L);
failures += testAnimationTreeRegistryApi(L);
failures += testBehaviorTreeComponent(L);
failures += testGoapActionComponent(L);
failures += testGoapGoalComponent(L);
@@ -705,6 +705,106 @@ void registerLuaItemApi(lua_State *L)
lua_setglobal(L, "ecs");
}
// ---------------------------------------------------------------------------
// Stub: LuaAnimationTreeApi
// ---------------------------------------------------------------------------
struct StubAnimTreeDef {
std::string name;
std::string skeletonSource;
std::string rootJson;
};
static std::unordered_map<std::string, StubAnimTreeDef> s_stubAnimTrees;
static void pushStubAnimTreeNode(lua_State *L, const std::string &json)
{
/* Simplified stub: return a minimal root table. */
lua_newtable(L);
lua_pushstring(L, "output");
lua_setfield(L, -2, "type");
}
void registerLuaAnimationTreeApi(lua_State *L)
{
lua_getglobal(L, "ecs");
if (lua_isnil(L, -1)) {
lua_pop(L, 1);
lua_newtable(L);
}
lua_newtable(L);
lua_pushcfunction(L, [](lua_State *L) -> int {
if (lua_gettop(L) < 2 || !lua_isstring(L, 1) ||
!lua_istable(L, 2))
return 0;
std::string name = lua_tostring(L, 1);
StubAnimTreeDef def;
def.name = name;
if (lua_getfield(L, 2, "skeletonSource"), lua_isstring(L, -1))
def.skeletonSource = lua_tostring(L, -1);
lua_pop(L, 1);
s_stubAnimTrees[name] = def;
return 0;
});
lua_setfield(L, -2, "register");
lua_pushcfunction(L, [](lua_State *L) -> int {
if (lua_gettop(L) < 1 || !lua_isstring(L, 1))
return 0;
auto it = s_stubAnimTrees.find(lua_tostring(L, 1));
if (it == s_stubAnimTrees.end())
return 0;
lua_newtable(L);
lua_pushstring(L, it->second.name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, it->second.skeletonSource.c_str());
lua_setfield(L, -2, "skeletonSource");
pushStubAnimTreeNode(L, it->second.rootJson);
lua_setfield(L, -2, "root");
return 1;
});
lua_setfield(L, -2, "find");
lua_pushcfunction(L, [](lua_State *L) -> int {
lua_newtable(L);
int idx = 1;
for (const auto &pair : s_stubAnimTrees) {
lua_pushstring(L, pair.first.c_str());
lua_rawseti(L, -2, idx++);
}
return 1;
});
lua_setfield(L, -2, "list");
lua_pushcfunction(L, [](lua_State *L) -> int {
if (lua_gettop(L) < 1 || !lua_isstring(L, 1))
return 0;
s_stubAnimTrees.erase(lua_tostring(L, 1));
return 0;
});
lua_setfield(L, -2, "remove");
lua_pushcfunction(L, [](lua_State *L) -> int {
(void)L;
lua_pushboolean(L, 1);
return 1;
});
lua_setfield(L, -2, "save");
lua_pushcfunction(L, [](lua_State *L) -> int {
(void)L;
lua_pushboolean(L, 1);
return 1;
});
lua_setfield(L, -2, "load");
lua_setfield(L, -2, "animation_trees");
lua_setglobal(L, "ecs");
}
} // namespace editScene
// ---------------------------------------------------------------------------
+40 -485
View File
@@ -1,6 +1,6 @@
#include "AnimationTreeEditor.hpp"
#include "../systems/AnimationTreeSystem.hpp"
#include "../components/AnimationTreeTemplate.hpp"
#include "../systems/AnimationTreeRegistry.hpp"
#include <imgui.h>
AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr)
@@ -8,433 +8,11 @@ AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr)
{
}
std::vector<Ogre::String> AnimationTreeEditor::getAvailableAnimations(
flecs::entity entity)
static void renderStatePreview(AnimationTreeComponent &at,
const AnimationTreeNode &root)
{
std::vector<Ogre::String> result;
Ogre::Entity *ent =
AnimationTreeSystem::findAnimatedEntity(entity);
if (ent && ent->hasSkeleton()) {
Ogre::AnimationStateSet *states =
ent->getAllAnimationStates();
if (states) {
for (const auto &pair : states->getAnimationStates())
result.push_back(pair.first);
}
}
return result;
}
static ImU32 nodeTypeColorU32(const Ogre::String &type)
{
if (type == "output")
return IM_COL32(0xFF, 0x88, 0x88, 0xFF);
if (type == "stateMachine")
return IM_COL32(0x88, 0xBB, 0xFF, 0xFF);
if (type == "state")
return IM_COL32(0x88, 0xFF, 0x88, 0xFF);
if (type == "speed")
return IM_COL32(0xFF, 0xEE, 0x88, 0xFF);
return IM_COL32(0xFF, 0xFF, 0xFF, 0xFF);
}
static void makeLabel(const AnimationTreeNode &node, char *buf,
size_t bufSize)
{
if (node.type == "animation") {
snprintf(buf, bufSize, "[%s] %s", node.type.c_str(),
node.animationName.empty() ?
"(none)" :
node.animationName.c_str());
} else if (node.type == "speed") {
snprintf(buf, bufSize, "[%s] %.2fx %s", node.type.c_str(),
node.speed,
node.name.empty() ? "" : node.name.c_str());
} else if (node.type == "stateMachine") {
snprintf(buf, bufSize, "[%s] %s (fade %.1f)",
node.type.c_str(), node.name.c_str(),
node.fadeSpeed);
} else {
snprintf(buf, bufSize, "[%s] %s", node.type.c_str(),
node.name.empty() ? "" : node.name.c_str());
}
}
static int countChildrenOfType(const AnimationTreeNode &node,
const Ogre::String &type)
{
int n = 0;
for (const auto &c : node.children)
if (c.type == type)
n++;
return n;
}
static size_t findChildIndex(const AnimationTreeNode &parent,
const AnimationTreeNode *child)
{
for (size_t i = 0; i < parent.children.size(); i++)
if (&parent.children[i] == child)
return i;
return (size_t)-1;
}
void AnimationTreeEditor::queueAddChild(AnimationTreeNode *parent,
const Ogre::String &type)
{
TreeEditOp op;
op.type = TreeEditOp::AddChild;
op.targetNode = parent;
op.childType = type;
m_editOps.push_back(op);
}
void AnimationTreeEditor::queueRemoveNode(AnimationTreeNode *parent,
size_t childIndex)
{
TreeEditOp op;
op.type = TreeEditOp::RemoveNode;
op.targetNode = parent;
op.childIndex = childIndex;
m_editOps.push_back(op);
}
void AnimationTreeEditor::queueMoveNode(AnimationTreeNode *parent,
size_t childIndex,
int delta)
{
TreeEditOp op;
op.type = TreeEditOp::MoveNode;
op.targetNode = parent;
op.childIndex = childIndex;
op.moveDelta = delta;
m_editOps.push_back(op);
}
void AnimationTreeEditor::applyEditOps(AnimationTreeComponent &at)
{
bool changed = false;
for (auto &op : m_editOps) {
if (!op.targetNode)
continue;
auto &vec = op.targetNode->children;
switch (op.type) {
case TreeEditOp::AddChild: {
AnimationTreeNode child;
child.type = op.childType;
if (op.childType == "stateMachine") {
child.name = "sm" +
std::to_string(countChildrenOfType(
*op.targetNode,
"stateMachine"));
child.fadeSpeed = 7.5f;
} else if (op.childType == "state") {
child.name = "state" +
std::to_string(countChildrenOfType(
*op.targetNode, "state"));
} else if (op.childType == "animation") {
child.animationName = "";
} else if (op.childType == "speed") {
child.speed = 1.0f;
}
vec.push_back(child);
changed = true;
break;
}
case TreeEditOp::RemoveNode: {
if (op.childIndex < vec.size()) {
vec.erase(vec.begin() + op.childIndex);
changed = true;
}
break;
}
case TreeEditOp::MoveNode: {
if (op.childIndex < vec.size()) {
int newIdx = (int)op.childIndex + op.moveDelta;
if (newIdx >= 0 &&
newIdx < (int)vec.size()) {
std::swap(vec[op.childIndex],
vec[newIdx]);
changed = true;
}
}
break;
}
}
}
m_editOps.clear();
if (changed)
at.dirty = true;
}
static bool canHaveChildren(const Ogre::String &type)
{
return type == "output" || type == "stateMachine" ||
type == "state" || type == "speed";
}
void AnimationTreeEditor::renderTree(AnimationTreeNode &node,
AnimationTreeNode *parent,
AnimationTreeNode *&selectedNode,
AnimationTreeNode *&selectedParent,
int &id)
{
int myId = id++;
bool isLeaf = node.children.empty();
ImGuiTreeNodeFlags flags =
ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (isLeaf)
flags |= ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen;
if (selectedNode == &node)
flags |= ImGuiTreeNodeFlags_Selected;
char label[256];
makeLabel(node, label, sizeof(label));
/* Color the whole tree node label */
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(
nodeTypeColorU32(node.type)));
bool open = ImGui::TreeNodeEx(
("##animnode" + std::to_string(myId)).c_str(), flags,
"%s", label);
ImGui::PopStyleColor();
if (ImGui::IsItemClicked()) {
selectedNode = &node;
selectedParent = parent;
}
/* Right-click context menu — must be attached to TreeNodeEx
* (the last item), before we add buttons on the same line */
if (ImGui::BeginPopupContextItem()) {
if (canHaveChildren(node.type)) {
if (node.type == "output" ||
node.type == "stateMachine" ||
node.type == "state") {
if (ImGui::MenuItem("Add State Machine"))
queueAddChild(&node, "stateMachine");
}
if (node.type == "stateMachine" ||
node.type == "output" ||
node.type == "state") {
if (ImGui::MenuItem("Add State"))
queueAddChild(&node, "state");
}
if (node.type == "output" ||
node.type == "state" ||
node.type == "speed") {
if (ImGui::MenuItem("Add Animation"))
queueAddChild(&node, "animation");
}
if (node.type == "output" ||
node.type == "state") {
if (ImGui::MenuItem("Add Speed"))
queueAddChild(&node, "speed");
}
}
if (parent) {
size_t idx = findChildIndex(*parent, &node);
if (ImGui::MenuItem("Remove")) {
queueRemoveNode(parent, idx);
if (selectedNode == &node) {
selectedNode = nullptr;
selectedParent = nullptr;
}
}
if (ImGui::MenuItem("Move Up"))
queueMoveNode(parent, idx, -1);
if (ImGui::MenuItem("Move Down"))
queueMoveNode(parent, idx, +1);
}
ImGui::EndPopup();
}
/* Inline buttons on the same line as the tree node label */
ImGui::PushID(myId);
if (canHaveChildren(node.type)) {
ImGui::SameLine();
if (ImGui::SmallButton("+")) {
ImGui::OpenPopup("AddChild");
}
if (ImGui::BeginPopup("AddChild")) {
if (node.type == "output" ||
node.type == "stateMachine" ||
node.type == "state") {
if (ImGui::MenuItem("State Machine"))
queueAddChild(&node, "stateMachine");
}
if (node.type == "stateMachine" ||
node.type == "output" ||
node.type == "state") {
if (ImGui::MenuItem("State"))
queueAddChild(&node, "state");
}
if (node.type == "output" ||
node.type == "state" ||
node.type == "speed") {
if (ImGui::MenuItem("Animation"))
queueAddChild(&node, "animation");
}
if (node.type == "output" ||
node.type == "state") {
if (ImGui::MenuItem("Speed"))
queueAddChild(&node, "speed");
}
ImGui::EndPopup();
}
}
if (parent) {
size_t myIndex = findChildIndex(*parent, &node);
ImGui::SameLine();
if (ImGui::SmallButton("-"))
queueRemoveNode(parent, myIndex);
if (myIndex > 0) {
ImGui::SameLine();
if (ImGui::SmallButton("^"))
queueMoveNode(parent, myIndex, -1);
}
if (myIndex + 1 < parent->children.size()) {
ImGui::SameLine();
if (ImGui::SmallButton("v"))
queueMoveNode(parent, myIndex, +1);
}
}
ImGui::PopID();
/* Render children and TreePop (only when node is open) */
if (!isLeaf && open) {
for (auto &child : node.children)
renderTree(child, &node, selectedNode,
selectedParent, id);
ImGui::TreePop();
}
}
void AnimationTreeEditor::renderProperties(
AnimationTreeNode *node, AnimationTreeComponent &at,
flecs::entity entity)
{
if (!node)
return;
ImGui::Separator();
ImGui::Text("Node Properties");
bool modified = false;
/* Type selector */
const char *types[] = {"output", "stateMachine", "state",
"speed", "animation"};
int typeIdx = 0;
for (int i = 0; i < IM_ARRAYSIZE(types); i++) {
if (node->type == types[i]) {
typeIdx = i;
break;
}
}
if (ImGui::Combo("Type", &typeIdx, types,
IM_ARRAYSIZE(types))) {
node->type = types[typeIdx];
modified = true;
}
/* Name */
if (node->type == "stateMachine" || node->type == "state") {
char buf[256];
strncpy(buf, node->name.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (ImGui::InputText("Name", buf, sizeof(buf))) {
node->name = buf;
modified = true;
}
}
/* Animation name */
if (node->type == "animation") {
std::vector<Ogre::String> anims =
getAvailableAnimations(entity);
Ogre::String current = node->animationName;
Ogre::String preview = current.empty() ? "(none)" :
current;
if (ImGui::BeginCombo("Animation", preview.c_str())) {
bool noneSel = current.empty();
if (ImGui::Selectable("(none)", noneSel)) {
node->animationName = "";
modified = true;
}
for (const auto &name : anims) {
bool sel = (current == name);
if (ImGui::Selectable(name.c_str(), sel)) {
node->animationName = name;
modified = true;
}
}
ImGui::EndCombo();
}
}
/* Speed */
if (node->type == "output" || node->type == "speed") {
if (ImGui::SliderFloat("Speed", &node->speed, -2.0f,
10.0f, "%.2f"))
modified = true;
}
/* Fade speed */
if (node->type == "stateMachine") {
if (ImGui::SliderFloat("Fade Speed", &node->fadeSpeed,
0.1f, 30.0f, "%.1f"))
modified = true;
}
/* End transitions (for state machines) */
if (node->type == "stateMachine" && !node->children.empty()) {
ImGui::Separator();
ImGui::Text("End Transitions");
ImGui::TextDisabled(
"Auto-switch state when animation ends");
for (const auto &child : node->children) {
if (child.type != "state")
continue;
char buf[256];
auto it = node->endTransitions.find(child.name);
if (it != node->endTransitions.end())
strncpy(buf, it->second.c_str(),
sizeof(buf) - 1);
else
buf[0] = '\0';
buf[sizeof(buf) - 1] = '\0';
Ogre::String lbl = "On '" + child.name +
"' end →";
if (ImGui::InputText(lbl.c_str(), buf,
sizeof(buf))) {
if (buf[0] == '\0')
node->endTransitions.erase(
child.name);
else
node->endTransitions[child.name] =
buf;
modified = true;
}
}
}
if (modified)
at.dirty = true;
}
void AnimationTreeEditor::renderStatePreview(
AnimationTreeComponent &at)
{
std::vector<AnimationTreeNode *> stateMachines;
at.root.collectStateMachines(stateMachines);
std::vector<const AnimationTreeNode *> stateMachines;
root.collectStateMachines(stateMachines);
if (stateMachines.empty())
return;
@@ -473,8 +51,8 @@ void AnimationTreeEditor::renderStatePreview(
}
}
bool AnimationTreeEditor::renderComponent(
flecs::entity entity, AnimationTreeComponent &at)
bool AnimationTreeEditor::renderComponent(flecs::entity entity,
AnimationTreeComponent &at)
{
ImGui::PushID("AnimTree");
bool modified = false;
@@ -483,33 +61,35 @@ bool AnimationTreeEditor::renderComponent(
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.");
AnimationTreeRegistry &reg =
AnimationTreeRegistry::getSingleton();
std::vector<Ogre::String> names = reg.getTreeNames();
Ogre::String current = at.treeName;
Ogre::String preview = current.empty() ? "(none)" : current;
if (ImGui::BeginCombo("Tree", preview.c_str())) {
bool noneSel = current.empty();
if (ImGui::Selectable("(none)", noneSel)) {
at.treeName = "";
modified = true;
at.dirty = true;
}
for (const auto &name : names) {
bool sel = (current == name);
if (ImGui::Selectable(name.c_str(), sel)) {
at.treeName = name;
modified = true;
at.dirty = true;
}
}
ImGui::EndCombo();
}
bool isTemplateEntity = entity.has<AnimationTreeTemplate>();
if (isTemplateEntity) {
auto &templ = entity.get_mut<AnimationTreeTemplate>();
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);
if (!at.treeName.empty() &&
ImGui::Button("Edit in Registry")) {
/* TODO: signal EditorUISystem to open the registry
* editor on this tree. For now the user can open
* Tools -> Animation Tree Registry manually. */
}
ImGui::Separator();
@@ -517,45 +97,20 @@ bool AnimationTreeEditor::renderComponent(
/* Global toggles */
if (ImGui::Checkbox("Enabled", &at.enabled))
modified = true;
if (ImGui::Checkbox("Use Root Motion",
&at.useRootMotion)) {
if (ImGui::Checkbox("Use Root Motion", &at.useRootMotion)) {
modified = true;
at.dirty = true;
}
ImGui::Separator();
/* Tree view */
static AnimationTreeNode *selectedNode = nullptr;
static AnimationTreeNode *selectedParent = nullptr;
int id = 0;
renderTree(at.root, nullptr, selectedNode,
selectedParent, id);
/* Apply all deferred edits. Vector reallocations may
* invalidate pointers, so clear selection afterwards. */
bool hadEdits = !m_editOps.empty();
applyEditOps(at);
if (hadEdits) {
selectedNode = nullptr;
selectedParent = nullptr;
}
/* Node properties */
renderProperties(selectedNode, at, entity);
/* State preview */
renderStatePreview(at);
/* State preview from registry tree */
const AnimationTreeRegistry::AnimationTreeDefinition *def =
reg.findTree(at.treeName);
if (def)
renderStatePreview(at, def->root);
ImGui::Unindent();
}
/* Auto-publish template changes */
if (modified && entity.has<AnimationTreeTemplate>()) {
auto &templ = entity.get_mut<AnimationTreeTemplate>();
templ.version++;
}
ImGui::PopID();
return modified;
}
@@ -4,13 +4,14 @@
#include "ComponentEditor.hpp"
#include "../components/AnimationTree.hpp"
#include <OgreSceneManager.h>
#include <vector>
/**
* Editor for AnimationTreeComponent.
*
* Provides a recursive tree view for editing the animation hierarchy
* and state-machine current-state selectors for preview.
* In the new registry-based design this editor is only a picker: it
* selects the tree name from AnimationTreeRegistry and exposes the
* per-instance toggles. Tree authoring happens in
* AnimationTreeRegistryEditor (Tools -> Animation Tree Registry).
*/
class AnimationTreeEditor : public ComponentEditor<AnimationTreeComponent> {
public:
@@ -18,42 +19,12 @@ public:
const char *getName() const override { return "Animation Tree"; }
/* Deferred edit operations — safe to call during UI rendering
* since they are applied only after the tree is fully drawn. */
struct TreeEditOp {
enum OpType { AddChild, RemoveNode, MoveNode } type;
AnimationTreeNode *targetNode = nullptr;
size_t childIndex = 0;
Ogre::String childType;
int moveDelta = 0;
};
void queueAddChild(AnimationTreeNode *parent,
const Ogre::String &type);
void queueRemoveNode(AnimationTreeNode *parent, size_t childIndex);
void queueMoveNode(AnimationTreeNode *parent, size_t childIndex,
int delta);
protected:
bool renderComponent(flecs::entity entity,
AnimationTreeComponent &at) override;
private:
void renderTree(AnimationTreeNode &node,
AnimationTreeNode *parent,
AnimationTreeNode *&selectedNode,
AnimationTreeNode *&selectedParent,
int &id);
void renderProperties(AnimationTreeNode *node,
AnimationTreeComponent &at,
flecs::entity entity);
void renderStatePreview(AnimationTreeComponent &at);
void applyEditOps(AnimationTreeComponent &at);
std::vector<Ogre::String> getAvailableAnimations(
flecs::entity entity);
Ogre::SceneManager *m_sceneMgr;
std::vector<TreeEditOp> m_editOps;
};
#endif // EDITSCENE_ANIMATIONTREEEDITOR_HPP
@@ -0,0 +1,440 @@
#include "AnimationTreeNodeEditor.hpp"
#include <imgui.h>
#include <cstring>
AnimationTreeNodeEditor::AnimationTreeNodeEditor()
{
}
void AnimationTreeNodeEditor::reset()
{
m_editOps.clear();
m_selectedNode = nullptr;
m_selectedParent = nullptr;
m_treeId = 0;
m_modified = false;
}
static ImU32 nodeTypeColorU32(const Ogre::String &type)
{
if (type == "output")
return IM_COL32(0xFF, 0x88, 0x88, 0xFF);
if (type == "stateMachine")
return IM_COL32(0x88, 0xBB, 0xFF, 0xFF);
if (type == "state")
return IM_COL32(0x88, 0xFF, 0x88, 0xFF);
if (type == "speed")
return IM_COL32(0xFF, 0xEE, 0x88, 0xFF);
return IM_COL32(0xFF, 0xFF, 0xFF, 0xFF);
}
static void makeLabel(const AnimationTreeNode &node, char *buf,
size_t bufSize)
{
if (node.type == "animation") {
snprintf(buf, bufSize, "[%s] %s", node.type.c_str(),
node.animationName.empty() ?
"(none)" :
node.animationName.c_str());
} else if (node.type == "speed") {
snprintf(buf, bufSize, "[%s] %.2fx %s", node.type.c_str(),
node.speed,
node.name.empty() ? "" : node.name.c_str());
} else if (node.type == "stateMachine") {
snprintf(buf, bufSize, "[%s] %s (fade %.1f)",
node.type.c_str(), node.name.c_str(),
node.fadeSpeed);
} else {
snprintf(buf, bufSize, "[%s] %s", node.type.c_str(),
node.name.empty() ? "" : node.name.c_str());
}
}
static int countChildrenOfType(const AnimationTreeNode &node,
const Ogre::String &type)
{
int n = 0;
for (const auto &c : node.children)
if (c.type == type)
n++;
return n;
}
static size_t findChildIndex(const AnimationTreeNode &parent,
const AnimationTreeNode *child)
{
for (size_t i = 0; i < parent.children.size(); i++)
if (&parent.children[i] == child)
return i;
return (size_t)-1;
}
static bool canHaveChildren(const Ogre::String &type)
{
return type == "output" || type == "stateMachine" ||
type == "state" || type == "speed";
}
void AnimationTreeNodeEditor::queueAddChild(AnimationTreeNode *parent,
const Ogre::String &type)
{
TreeEditOp op;
op.type = TreeEditOp::AddChild;
op.targetNode = parent;
op.childType = type;
m_editOps.push_back(op);
}
void AnimationTreeNodeEditor::queueRemoveNode(AnimationTreeNode *parent,
size_t childIndex)
{
TreeEditOp op;
op.type = TreeEditOp::RemoveNode;
op.targetNode = parent;
op.childIndex = childIndex;
m_editOps.push_back(op);
}
void AnimationTreeNodeEditor::queueMoveNode(AnimationTreeNode *parent,
size_t childIndex,
int delta)
{
TreeEditOp op;
op.type = TreeEditOp::MoveNode;
op.targetNode = parent;
op.childIndex = childIndex;
op.moveDelta = delta;
m_editOps.push_back(op);
}
void AnimationTreeNodeEditor::applyEditOps(AnimationTreeNode &root)
{
bool changed = false;
for (auto &op : m_editOps) {
if (!op.targetNode)
continue;
auto &vec = op.targetNode->children;
switch (op.type) {
case TreeEditOp::AddChild: {
AnimationTreeNode child;
child.type = op.childType;
if (op.childType == "stateMachine") {
child.name = "sm" +
std::to_string(countChildrenOfType(
*op.targetNode,
"stateMachine"));
child.fadeSpeed = 7.5f;
} else if (op.childType == "state") {
child.name = "state" +
std::to_string(countChildrenOfType(
*op.targetNode, "state"));
} else if (op.childType == "animation") {
child.animationName = "";
} else if (op.childType == "speed") {
child.speed = 1.0f;
}
vec.push_back(child);
changed = true;
break;
}
case TreeEditOp::RemoveNode: {
if (op.childIndex < vec.size()) {
vec.erase(vec.begin() + op.childIndex);
changed = true;
}
break;
}
case TreeEditOp::MoveNode: {
if (op.childIndex < vec.size()) {
int newIdx = (int)op.childIndex + op.moveDelta;
if (newIdx >= 0 &&
newIdx < (int)vec.size()) {
std::swap(vec[op.childIndex],
vec[newIdx]);
changed = true;
}
}
break;
}
}
}
m_editOps.clear();
if (changed)
m_modified = true;
if (changed) {
/* Selection may have been invalidated by vector reallocation. */
m_selectedNode = nullptr;
m_selectedParent = nullptr;
}
}
void AnimationTreeNodeEditor::renderTree(AnimationTreeNode &node,
AnimationTreeNode *parent,
AnimationTreeNode *&selectedNode,
AnimationTreeNode *&selectedParent,
int &id)
{
int myId = id++;
bool isLeaf = node.children.empty();
ImGuiTreeNodeFlags flags =
ImGuiTreeNodeFlags_OpenOnArrow |
ImGuiTreeNodeFlags_OpenOnDoubleClick;
if (isLeaf)
flags |= ImGuiTreeNodeFlags_Leaf |
ImGuiTreeNodeFlags_NoTreePushOnOpen;
if (selectedNode == &node)
flags |= ImGuiTreeNodeFlags_Selected;
char label[256];
makeLabel(node, label, sizeof(label));
/* Color the whole tree node label */
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(
nodeTypeColorU32(node.type)));
bool open = ImGui::TreeNodeEx(
("##animnode" + std::to_string(myId)).c_str(), flags,
"%s", label);
ImGui::PopStyleColor();
if (ImGui::IsItemClicked()) {
selectedNode = &node;
selectedParent = parent;
}
/* Right-click context menu */
if (ImGui::BeginPopupContextItem()) {
if (canHaveChildren(node.type)) {
if (node.type == "output" ||
node.type == "stateMachine" ||
node.type == "state") {
if (ImGui::MenuItem("Add State Machine"))
queueAddChild(&node, "stateMachine");
}
if (node.type == "stateMachine" ||
node.type == "output" ||
node.type == "state") {
if (ImGui::MenuItem("Add State"))
queueAddChild(&node, "state");
}
if (node.type == "output" ||
node.type == "state" ||
node.type == "speed") {
if (ImGui::MenuItem("Add Animation"))
queueAddChild(&node, "animation");
}
if (node.type == "output" ||
node.type == "state") {
if (ImGui::MenuItem("Add Speed"))
queueAddChild(&node, "speed");
}
}
if (parent) {
size_t idx = findChildIndex(*parent, &node);
if (ImGui::MenuItem("Remove")) {
queueRemoveNode(parent, idx);
if (selectedNode == &node) {
selectedNode = nullptr;
selectedParent = nullptr;
}
}
if (ImGui::MenuItem("Move Up"))
queueMoveNode(parent, idx, -1);
if (ImGui::MenuItem("Move Down"))
queueMoveNode(parent, idx, +1);
}
ImGui::EndPopup();
}
/* Inline buttons on the same line as the tree node label */
ImGui::PushID(myId);
if (canHaveChildren(node.type)) {
ImGui::SameLine();
if (ImGui::SmallButton("+")) {
ImGui::OpenPopup("AddChild");
}
if (ImGui::BeginPopup("AddChild")) {
if (node.type == "output" ||
node.type == "stateMachine" ||
node.type == "state") {
if (ImGui::MenuItem("State Machine"))
queueAddChild(&node, "stateMachine");
}
if (node.type == "stateMachine" ||
node.type == "output" ||
node.type == "state") {
if (ImGui::MenuItem("State"))
queueAddChild(&node, "state");
}
if (node.type == "output" ||
node.type == "state" ||
node.type == "speed") {
if (ImGui::MenuItem("Animation"))
queueAddChild(&node, "animation");
}
if (node.type == "output" ||
node.type == "state") {
if (ImGui::MenuItem("Speed"))
queueAddChild(&node, "speed");
}
ImGui::EndPopup();
}
}
if (parent) {
size_t myIndex = findChildIndex(*parent, &node);
ImGui::SameLine();
if (ImGui::SmallButton("-"))
queueRemoveNode(parent, myIndex);
if (myIndex > 0) {
ImGui::SameLine();
if (ImGui::SmallButton("^"))
queueMoveNode(parent, myIndex, -1);
}
if (myIndex + 1 < parent->children.size()) {
ImGui::SameLine();
if (ImGui::SmallButton("v"))
queueMoveNode(parent, myIndex, +1);
}
}
ImGui::PopID();
/* Render children and TreePop (only when node is open) */
if (!isLeaf && open) {
for (auto &child : node.children)
renderTree(child, &node, selectedNode,
selectedParent, id);
ImGui::TreePop();
}
}
void AnimationTreeNodeEditor::renderProperties(
AnimationTreeNode *node,
const std::vector<Ogre::String> &animNames)
{
if (!node)
return;
ImGui::Separator();
ImGui::Text("Node Properties");
/* Type selector */
const char *types[] = { "output", "stateMachine", "state",
"speed", "animation" };
int typeIdx = 0;
for (int i = 0; i < IM_ARRAYSIZE(types); i++) {
if (node->type == types[i]) {
typeIdx = i;
break;
}
}
if (ImGui::Combo("Type", &typeIdx, types,
IM_ARRAYSIZE(types))) {
node->type = types[typeIdx];
m_modified = true;
}
/* Name */
if (node->type == "stateMachine" || node->type == "state") {
char buf[256];
strncpy(buf, node->name.c_str(), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
if (ImGui::InputText("Name", buf, sizeof(buf))) {
node->name = buf;
m_modified = true;
}
}
/* Animation name */
if (node->type == "animation") {
Ogre::String current = node->animationName;
Ogre::String preview = current.empty() ? "(none)" : current;
if (ImGui::BeginCombo("Animation", preview.c_str())) {
bool noneSel = current.empty();
if (ImGui::Selectable("(none)", noneSel)) {
node->animationName = "";
m_modified = true;
}
for (const auto &name : animNames) {
bool sel = (current == name);
if (ImGui::Selectable(name.c_str(), sel)) {
node->animationName = name;
m_modified = true;
}
}
ImGui::EndCombo();
}
}
/* Speed */
if (node->type == "output" || node->type == "speed") {
if (ImGui::SliderFloat("Speed", &node->speed, -2.0f, 10.0f,
"%.2f"))
m_modified = true;
}
/* Fade speed */
if (node->type == "stateMachine") {
if (ImGui::SliderFloat("Fade Speed", &node->fadeSpeed,
0.1f, 30.0f, "%.1f"))
m_modified = true;
}
/* End transitions (for state machines) */
if (node->type == "stateMachine" && !node->children.empty()) {
ImGui::Separator();
ImGui::Text("End Transitions");
ImGui::TextDisabled(
"Auto-switch state when animation ends");
for (const auto &child : node->children) {
if (child.type != "state")
continue;
char buf[256];
auto it = node->endTransitions.find(child.name);
if (it != node->endTransitions.end())
strncpy(buf, it->second.c_str(),
sizeof(buf) - 1);
else
buf[0] = '\0';
buf[sizeof(buf) - 1] = '\0';
Ogre::String lbl = "On '" + child.name +
"' end →";
if (ImGui::InputText(lbl.c_str(), buf,
sizeof(buf))) {
if (buf[0] == '\0')
node->endTransitions.erase(
child.name);
else
node->endTransitions[child.name] =
buf;
m_modified = true;
}
}
}
}
bool AnimationTreeNodeEditor::render(AnimationTreeNode &root,
const std::vector<Ogre::String> &animNames)
{
ImGui::PushID("AnimTreeNodeEditor");
m_treeId = 0;
renderTree(root, nullptr, m_selectedNode, m_selectedParent,
m_treeId);
/* Apply deferred edits before rendering properties so the
* property panel reflects the current selection. */
applyEditOps(root);
/* Node properties */
renderProperties(m_selectedNode, animNames);
ImGui::PopID();
bool wasModified = m_modified;
m_modified = false;
return wasModified;
}
@@ -0,0 +1,65 @@
#ifndef EDITSCENE_ANIMATIONTREENODEEDITOR_HPP
#define EDITSCENE_ANIMATIONTREENODEEDITOR_HPP
#pragma once
#include "../components/AnimationTree.hpp"
#include <Ogre.h>
#include <vector>
/**
* Reusable editor for an AnimationTreeNode hierarchy.
*
* This editor does not know about registries or components; it simply
* edits the provided root node in place. It is used by the Animation Tree
* Registry editor and can be used anywhere a tree needs to be authored.
*/
class AnimationTreeNodeEditor {
public:
AnimationTreeNodeEditor();
/**
* Render the tree editor.
*
* @param root Root node to edit.
* @param animNames Optional list of animation names for dropdowns.
* @return true if the tree was modified.
*/
bool render(AnimationTreeNode &root,
const std::vector<Ogre::String> &animNames);
/** Clear selection and pending edit operations. */
void reset();
private:
struct TreeEditOp {
enum OpType { AddChild, RemoveNode, MoveNode } type;
AnimationTreeNode *targetNode = nullptr;
size_t childIndex = 0;
Ogre::String childType;
int moveDelta = 0;
};
void queueAddChild(AnimationTreeNode *parent,
const Ogre::String &type);
void queueRemoveNode(AnimationTreeNode *parent, size_t childIndex);
void queueMoveNode(AnimationTreeNode *parent, size_t childIndex,
int delta);
void applyEditOps(AnimationTreeNode &root);
void renderTree(AnimationTreeNode &node,
AnimationTreeNode *parent,
AnimationTreeNode *&selectedNode,
AnimationTreeNode *&selectedParent,
int &id);
void renderProperties(AnimationTreeNode *node,
const std::vector<Ogre::String> &animNames);
void renderStatePreview(AnimationTreeNode &root);
std::vector<TreeEditOp> m_editOps;
AnimationTreeNode *m_selectedNode = nullptr;
AnimationTreeNode *m_selectedParent = nullptr;
int m_treeId = 0;
bool m_modified = false;
};
#endif // EDITSCENE_ANIMATIONTREENODEEDITOR_HPP
@@ -0,0 +1,165 @@
#include "AnimationTreeRegistryEditor.hpp"
#include <imgui.h>
#include <OgreLogManager.h>
AnimationTreeRegistryEditor::AnimationTreeRegistryEditor()
{
m_newNameBuf[0] = '\0';
}
void AnimationTreeRegistryEditor::selectTree(const Ogre::String &name)
{
m_pendingSelect = true;
m_pendingSelectName = name;
m_nodeEditor.reset();
}
void AnimationTreeRegistryEditor::renderMenuBar()
{
if (!ImGui::BeginMenuBar())
return;
if (ImGui::BeginMenu("File")) {
if (ImGui::MenuItem("Save", nullptr))
AnimationTreeRegistry::getSingleton().saveToFile(
AnimationTreeRegistry::defaultPath());
if (ImGui::MenuItem("Load", nullptr))
AnimationTreeRegistry::getSingleton().loadFromFile(
AnimationTreeRegistry::defaultPath());
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
void AnimationTreeRegistryEditor::renderListPane()
{
ImGui::BeginChild("TreeList", ImVec2(250, 0), true);
if (ImGui::Button("Add Tree")) {
AnimationTreeRegistry &reg =
AnimationTreeRegistry::getSingleton();
Ogre::String name = reg.generateUniqueName("new_tree");
AnimationTreeRegistry::AnimationTreeDefinition def;
def.name = name;
def.root.type = "output";
def.root.speed = 1.0f;
reg.registerTree(def);
m_selectedTreeName = name;
}
AnimationTreeRegistry &reg = AnimationTreeRegistry::getSingleton();
std::vector<Ogre::String> names = reg.getTreeNames();
std::sort(names.begin(), names.end());
for (const auto &name : names) {
bool selected = (m_selectedTreeName == name);
if (ImGui::Selectable(name.c_str(), selected)) {
if (m_selectedTreeName != name)
m_nodeEditor.reset();
m_selectedTreeName = name;
}
}
ImGui::EndChild();
}
void AnimationTreeRegistryEditor::renderEditPane()
{
ImGui::SameLine();
ImGui::BeginChild("TreeEdit", ImVec2(0, 0), true);
AnimationTreeRegistry &reg = AnimationTreeRegistry::getSingleton();
AnimationTreeRegistry::AnimationTreeDefinition *def =
reg.findTree(m_selectedTreeName);
if (!def) {
ImGui::Text("Select a tree from the list to edit.");
ImGui::EndChild();
return;
}
/* Name */
char nameBuf[256];
snprintf(nameBuf, sizeof(nameBuf), "%s", def->name.c_str());
if (ImGui::InputText("Name", nameBuf, sizeof(nameBuf))) {
Ogre::String newName = nameBuf;
if (!newName.empty() && newName != def->name &&
!reg.hasTree(newName)) {
AnimationTreeRegistry::AnimationTreeDefinition renamed =
*def;
renamed.name = newName;
reg.removeTree(def->name);
reg.registerTree(renamed);
m_selectedTreeName = newName;
def = reg.findTree(newName);
}
}
/* Skeleton source */
std::vector<Ogre::String> sources = reg.getAvailableSkeletonSources();
Ogre::String currentSource = def->skeletonSource;
Ogre::String preview = currentSource.empty() ? "(none)" : currentSource;
if (ImGui::BeginCombo("Skeleton Source", preview.c_str())) {
bool noneSel = currentSource.empty();
if (ImGui::Selectable("(none)", noneSel)) {
def->skeletonSource = "";
reg.markModified(def->name);
}
for (const auto &src : sources) {
bool sel = (currentSource == src);
if (ImGui::Selectable(src.c_str(), sel)) {
def->skeletonSource = src;
reg.markModified(def->name);
}
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Mesh from the Characters resource group used to "
"populate animation-name dropdowns.");
}
ImGui::Separator();
/* Tree node editor */
std::vector<Ogre::String> animNames =
reg.getAnimationsForSource(def->skeletonSource);
AnimationTreeNode *rootPtr = def ? &def->root : nullptr;
if (rootPtr) {
bool edited = m_nodeEditor.render(*rootPtr, animNames);
if (edited)
reg.markModified(def->name);
}
ImGui::Separator();
if (ImGui::Button("Delete Tree")) {
reg.removeTree(def->name);
m_selectedTreeName.clear();
}
ImGui::EndChild();
}
void AnimationTreeRegistryEditor::render(bool *open)
{
if (m_pendingSelect) {
m_selectedTreeName = m_pendingSelectName;
m_pendingSelect = false;
}
ImGui::SetNextWindowSize(ImVec2(900, 600), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Animation Tree Registry", open,
ImGuiWindowFlags_MenuBar)) {
ImGui::End();
return;
}
renderMenuBar();
renderListPane();
renderEditPane();
ImGui::End();
}
@@ -0,0 +1,39 @@
#ifndef EDITSCENE_ANIMATIONTREEREGISTRYEDITOR_HPP
#define EDITSCENE_ANIMATIONTREEREGISTRYEDITOR_HPP
#pragma once
#include "../systems/AnimationTreeRegistry.hpp"
#include "AnimationTreeNodeEditor.hpp"
#include <Ogre.h>
#include <string>
/**
* Editor window for the global Animation Tree Registry.
*
* Accessible from Tools -> Animation Tree Registry. Allows creating,
* renaming, deleting and editing named animation trees. Each tree can
* optionally select a skeleton source from the Characters resource group
* to populate animation-name dropdowns.
*/
class AnimationTreeRegistryEditor {
public:
AnimationTreeRegistryEditor();
void render(bool *open);
/* Ask the editor to select a specific tree on next render. */
void selectTree(const Ogre::String &name);
private:
void renderMenuBar();
void renderListPane();
void renderEditPane();
AnimationTreeNodeEditor m_nodeEditor;
Ogre::String m_selectedTreeName;
char m_newNameBuf[256];
bool m_pendingSelect = false;
Ogre::String m_pendingSelectName;
};
#endif // EDITSCENE_ANIMATIONTREEREGISTRYEDITOR_HPP
@@ -1,37 +0,0 @@
#include "AnimationTreeTemplateEditor.hpp"
#include <imgui.h>
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;
}
@@ -1,24 +0,0 @@
#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<AnimationTreeTemplate> {
public:
bool renderComponent(flecs::entity entity,
AnimationTreeTemplate &templ) override;
const char *getName() const override
{
return "Animation Tree Template";
}
};
#endif // EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP