Use Animation Tree Registry
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ® = 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
|
||||
@@ -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 ® = 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 ® =
|
||||
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 ® = 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
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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 ® =
|
||||
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 ® =
|
||||
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 ® = 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 ® = 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
|
||||
Reference in New Issue
Block a user