Animation Tree was implemented

This commit is contained in:
2026-04-19 22:06:46 +03:00
parent 43e9fb330f
commit 529476d8cd
9 changed files with 1333 additions and 367 deletions

View File

@@ -64,6 +64,7 @@ set(EDITSCENE_SOURCES
components/TriangleBufferModule.cpp
components/CharacterSlotsModule.cpp
components/AnimationTreeModule.cpp
components/AnimationTree.cpp
components/CellGridModule.cpp
components/CellGridEditorsModule.cpp
components/CellGrid.cpp

View File

@@ -0,0 +1,9 @@
#include "AnimationTree.hpp"
AnimationTreeComponent::AnimationTreeComponent()
: root()
, enabled(true)
, useRootMotion(false)
, dirty(true)
{
}

View File

@@ -2,21 +2,158 @@
#define EDITSCENE_ANIMATIONTREE_HPP
#pragma once
#include <Ogre.h>
#include <vector>
#include <unordered_map>
/**
* Animation tree component for playing skeletal animations on entities.
* Works with any entity that has an Ogre::Entity with a skeleton
* (either via RenderableComponent or CharacterSlotsComponent).
* A node in the animation tree.
*
* Node types:
* "output" - Root output node, optional speed multiplier (1 child)
* "stateMachine" - Cross-fades between child states, named for lookup
* "state" - A named state within a state machine (1 child)
* "speed" - Playback speed multiplier (1 child)
* "animation" - Leaf referencing an Ogre animation by name
*/
struct AnimationTreeNode {
Ogre::String type = "animation";
Ogre::String name;
Ogre::String animationName;
float speed = 1.0f;
float fadeSpeed = 7.5f;
std::vector<AnimationTreeNode> children;
/* For stateMachine nodes: auto-transition when animation ends */
std::unordered_map<Ogre::String, Ogre::String> endTransitions;
AnimationTreeNode() = default;
AnimationTreeNode *findChild(const Ogre::String &childName)
{
for (auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
const AnimationTreeNode *findChild(
const Ogre::String &childName) const
{
for (const auto &child : children) {
if (child.name == childName)
return &child;
}
return nullptr;
}
AnimationTreeNode *findStateMachine(const Ogre::String &smName)
{
if (type == "stateMachine" && name == smName)
return this;
for (auto &child : children) {
auto *found = child.findStateMachine(smName);
if (found)
return found;
}
return nullptr;
}
const AnimationTreeNode *findStateMachine(
const Ogre::String &smName) const
{
if (type == "stateMachine" && name == smName)
return this;
for (const auto &child : children) {
auto *found = child.findStateMachine(smName);
if (found)
return found;
}
return nullptr;
}
AnimationTreeNode *findState(const Ogre::String &stateName)
{
if (type == "state" && name == stateName)
return this;
for (auto &child : children) {
auto *found = child.findState(stateName);
if (found)
return found;
}
return nullptr;
}
const AnimationTreeNode *findState(
const Ogre::String &stateName) const
{
if (type == "state" && name == stateName)
return this;
for (const auto &child : children) {
auto *found = child.findState(stateName);
if (found)
return found;
}
return nullptr;
}
AnimationTreeNode *findAnimationLeaf()
{
if (type == "animation")
return this;
for (auto &child : children) {
auto *found = child.findAnimationLeaf();
if (found)
return found;
}
return nullptr;
}
const AnimationTreeNode *findAnimationLeaf() const
{
if (type == "animation")
return this;
for (const auto &child : children) {
auto *found = child.findAnimationLeaf();
if (found)
return found;
}
return nullptr;
}
void collectStateMachines(std::vector<AnimationTreeNode *> &out)
{
if (type == "stateMachine")
out.push_back(this);
for (auto &child : children)
child.collectStateMachines(out);
}
void collectStateMachines(
std::vector<const AnimationTreeNode *> &out) const
{
if (type == "stateMachine")
out.push_back(this);
for (const auto &child : children)
child.collectStateMachines(out);
}
};
/**
* Animation tree component for hierarchical state-machine-based animation.
*
* The tree is evaluated each frame by AnimationTreeSystem to determine
* which Ogre AnimationStates are active and their blend weights.
*/
struct AnimationTreeComponent {
Ogre::String currentAnimation;
float speed = 1.0f;
bool loop = true;
AnimationTreeNode root;
bool enabled = true;
bool useRootMotion = false;
bool dirty = true;
AnimationTreeComponent() = default;
/* Runtime: current state of each state machine (not serialized) */
std::unordered_map<Ogre::String, Ogre::String> currentStates;
AnimationTreeComponent();
};
#endif // EDITSCENE_ANIMATIONTREE_HPP

View File

@@ -3,6 +3,33 @@
#include "../ui/AnimationTreeEditor.hpp"
#include "Transform.hpp"
static AnimationTreeComponent createDefaultTree()
{
AnimationTreeComponent at;
at.root.type = "output";
at.root.speed = 1.0f;
AnimationTreeNode sm;
sm.type = "stateMachine";
sm.name = "main";
sm.fadeSpeed = 7.5f;
AnimationTreeNode state;
state.type = "state";
state.name = "idle";
AnimationTreeNode anim;
anim.type = "animation";
anim.animationName = "idle";
state.children.push_back(anim);
sm.children.push_back(state);
at.root.children.push_back(sm);
return at;
}
REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
AnimationTreeComponent, AnimationTreeEditor)
{
@@ -18,7 +45,7 @@ REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
->createChildSceneNode();
e.set<TransformComponent>(transform);
}
AnimationTreeComponent at;
AnimationTreeComponent at = createDefaultTree();
at.dirty = true;
e.set<AnimationTreeComponent>(at);
},

View File

@@ -7,171 +7,6 @@
#include <OgreSceneNode.h>
#include <cmath>
//=============================================================================
// RootMotionTracker
//=============================================================================
RootMotionTracker::RootMotionTracker(Ogre::AnimationState *animationState,
Ogre::Entity *entity)
: mEntity(entity)
, mTrack(nullptr)
, mAppliedTranslation(Ogre::Vector3::ZERO)
, mAppliedRotation(Ogre::Quaternion::IDENTITY)
{
assert(animationState);
assert(entity);
Ogre::SkeletonInstance *skel = entity->getSkeleton();
Ogre::Animation *anim = nullptr;
try {
anim = skel->getAnimation(animationState->getAnimationName());
} catch (...) {
return;
}
/* Try known root bone names */
const char *rootNames[] = {"Root", "mixamorig:Hips", "Spineroot"};
for (const char *name : rootNames) {
try {
Ogre::Bone *bone = skel->getBone(name);
mTrack = anim->getNodeTrack(bone->getHandle());
break;
} catch (...) {
}
}
/* Fallback: first node track */
if (!mTrack) {
try {
const auto &trackList = anim->_getNodeTrackList();
if (!trackList.empty())
mTrack = trackList.begin()->second;
} catch (...) {
}
}
if (!mTrack)
return;
/* Get root bone binding pose */
try {
Ogre::Bone *rootBone = skel->getBone(mTrack->getHandle());
mRootBindingPosition = rootBone->getInitialPosition();
mRootBindingOrientation = rootBone->getInitialOrientation();
mRootBindingOrientationInverse = mRootBindingOrientation.Inverse();
} catch (...) {
mTrack = nullptr;
return;
}
/* Compute loop deltas */
if (mTrack->getNumKeyFrames() >= 2) {
Ogre::TransformKeyFrame *tkfBeg = mTrack->getNodeKeyFrame(0);
Ogre::TransformKeyFrame *tkfEnd =
mTrack->getNodeKeyFrame(mTrack->getNumKeyFrames() - 1);
Ogre::Quaternion begRotation =
mRootBindingOrientation * tkfBeg->getRotation() *
mRootBindingOrientationInverse;
Ogre::Quaternion endRotation =
mRootBindingOrientation * tkfEnd->getRotation() *
mRootBindingOrientationInverse;
mLoopRotation = endRotation * begRotation.Inverse();
/* Limit rotation to Y-axis */
Ogre::Matrix3 mat;
mLoopRotation.ToRotationMatrix(mat);
Ogre::Radian yAngle, zAngle, xAngle;
mat.ToEulerAnglesYZX(yAngle, zAngle, xAngle);
mat.FromEulerAnglesYZX(yAngle, Ogre::Radian(0.0f),
Ogre::Radian(0.0f));
mLoopRotation.FromRotationMatrix(mat);
mLoopRotationInverse = mLoopRotation.Inverse();
Ogre::Vector3 begTranslation =
mRootBindingPosition + tkfBeg->getTranslate() -
begRotation * mRootBindingPosition;
Ogre::Vector3 endTranslation =
mRootBindingPosition + tkfEnd->getTranslate() -
endRotation * mRootBindingPosition;
mLoopTranslation =
endTranslation - mLoopRotation * begTranslation;
mLoopTranslation.y = 0.0f;
} else {
mLoopRotation = Ogre::Quaternion::IDENTITY;
mLoopRotationInverse = Ogre::Quaternion::IDENTITY;
mLoopTranslation = Ogre::Vector3::ZERO;
}
/* Suppress root bone movement in skeleton */
if (!animationState->hasBlendMask()) {
animationState->createBlendMask(
entity->getSkeleton()->getNumBones(), 1.0f);
}
animationState->setBlendMaskEntry(mTrack->getHandle(), 0.0f);
}
void RootMotionTracker::apply(int loops, float thisTime)
{
if (!mTrack)
return;
Ogre::SceneNode *sceneNode = mEntity->getParentSceneNode();
if (!sceneNode)
return;
Ogre::TransformKeyFrame tkf(nullptr, 0.0f);
/* Unapply transform from last frame */
sceneNode->rotate(mAppliedRotation.Inverse());
sceneNode->translate(-mAppliedTranslation, Ogre::Node::TS_LOCAL);
/* Apply periodic loop transforms */
while (loops < 0) {
sceneNode->rotate(mLoopRotationInverse);
sceneNode->translate(-mLoopTranslation, Ogre::Node::TS_LOCAL);
loops++;
}
while (loops > 0) {
sceneNode->translate(mLoopTranslation, Ogre::Node::TS_LOCAL);
sceneNode->rotate(mLoopRotation);
loops--;
}
/* Apply transform from this frame */
mTrack->getInterpolatedKeyFrame(thisTime, &tkf);
mAppliedRotation = mRootBindingOrientation * tkf.getRotation() *
mRootBindingOrientationInverse;
mAppliedTranslation = mRootBindingPosition + tkf.getTranslate() -
mAppliedRotation * mRootBindingPosition;
sceneNode->translate(mAppliedTranslation, Ogre::Node::TS_LOCAL);
sceneNode->rotate(mAppliedRotation);
}
void RootMotionTracker::unapply()
{
if (!mTrack)
return;
Ogre::SceneNode *sceneNode = mEntity->getParentSceneNode();
if (!sceneNode)
return;
sceneNode->rotate(mAppliedRotation.Inverse());
sceneNode->translate(-mAppliedTranslation, Ogre::Node::TS_LOCAL);
mAppliedRotation = Ogre::Quaternion::IDENTITY;
mAppliedTranslation = Ogre::Vector3::ZERO;
}
//=============================================================================
// AnimationTreeSystem
//=============================================================================
AnimationTreeSystem::AnimationTreeSystem(flecs::world &world,
Ogre::SceneManager *sceneMgr)
: m_world(world)
@@ -244,50 +79,88 @@ void AnimationTreeSystem::setupEntity(flecs::entity e,
return;
}
auto it = m_states.find(e.id());
if (it != m_states.end() && it->second.animState) {
if (it->second.currentAnimName != at.currentAnimation) {
it->second.animState->setEnabled(false);
if (it->second.rootMotion)
it->second.rootMotion->unapply();
it->second.animState = nullptr;
it->second.rootMotion.reset();
}
}
if (at.currentAnimation.empty()) {
teardownEntity(e);
return;
}
EntityAnimState &state = m_states[e.id()];
try {
state.animState = ent->getAnimationState(at.currentAnimation);
} catch (const Ogre::Exception &) {
state.animState = nullptr;
state.rootMotion.reset();
state.currentAnimName.clear();
return;
}
state.currentAnimName = at.currentAnimation;
EntityAnimTreeState &state = m_states[e.id()];
state.ogreEntity = ent;
state.animState->setEnabled(at.enabled);
state.animState->setLoop(at.loop);
Ogre::SkeletonInstance *skel = ent->getSkeleton();
if (at.useRootMotion) {
if (!state.rootMotion || !state.rootMotion->hasTrack()) {
state.rootMotion = std::make_unique<RootMotionTracker>(
state.animState, ent);
}
} else {
if (state.rootMotion) {
state.rootMotion->unapply();
state.rootMotion.reset();
/* Find and freeze root bone */
const char *rootNames[] = {"Root", "mixamorig:Hips", "Spineroot"};
for (const char *name : rootNames) {
try {
state.rootBone = skel->getBone(name);
break;
} catch (...) {
}
}
if (state.rootBone) {
state.rootBindingPosition = state.rootBone->getInitialPosition();
state.rootBindingOrientation =
state.rootBone->getInitialOrientation();
state.rootBindingScale = state.rootBone->getInitialScale();
state.rootBone->setManuallyControlled(true);
state.rootBone->setPosition(state.rootBindingPosition);
state.rootBone->setOrientation(state.rootBindingOrientation);
state.rootBone->setScale(state.rootBindingScale);
}
/* Discover all animations and precompute root tracks */
Ogre::AnimationStateSet *states = ent->getAllAnimationStates();
if (states) {
for (const auto &pair : states->getAnimationStates()) {
const Ogre::String &animName = pair.first;
Ogre::AnimationState *as = pair.second;
/* Use blend mask to exclude root bone from animation
* (setManuallyControlled alone does not prevent
* Animation::apply() in OGRE 14) */
if (state.rootBone) {
as->destroyBlendMask();
as->createBlendMask(skel->getNumBones(),
1.0f);
as->setBlendMaskEntry(
state.rootBone->getHandle(),
0.0f);
}
AnimationRuntimeInfo info;
info.ogreAnimState = as;
try {
Ogre::Animation *anim =
skel->getAnimation(animName);
if (state.rootBone) {
unsigned short handle =
state.rootBone->getHandle();
if (anim->hasNodeTrack(handle)) {
info.rootTrack =
anim->getNodeTrack(handle);
if (info.rootTrack->getNumKeyFrames() >=
2) {
Ogre::TransformKeyFrame *tkfBeg =
info.rootTrack
->getNodeKeyFrame(0);
Ogre::TransformKeyFrame *tkfEnd =
info.rootTrack
->getNodeKeyFrame(
info.rootTrack
->getNumKeyFrames() -
1);
info.loopTranslation =
tkfEnd->getTranslate() -
tkfBeg->getTranslate();
}
}
}
} catch (...) {
}
state.animations[animName] = info;
}
}
/* Initialize default state machine states */
initializeTreeStates(at.root, at);
}
void AnimationTreeSystem::teardownEntity(flecs::entity e)
@@ -296,21 +169,61 @@ void AnimationTreeSystem::teardownEntity(flecs::entity e)
if (it == m_states.end())
return;
if (it->second.animState)
it->second.animState->setEnabled(false);
if (it->second.rootMotion)
it->second.rootMotion->unapply();
EntityAnimTreeState &state = it->second;
for (auto &pair : state.animations) {
if (pair.second.ogreAnimState)
pair.second.ogreAnimState->destroyBlendMask();
}
disableAllAnimations(state);
if (state.rootBone) {
state.rootBone->setManuallyControlled(false);
state.rootBone = nullptr;
}
state.animations.clear();
state.fadeStates.clear();
state.prevStates.clear();
m_states.erase(it);
}
void AnimationTreeSystem::disableAllAnimations(EntityAnimTreeState &state)
{
for (auto &pair : state.animations) {
if (pair.second.ogreAnimState)
pair.second.ogreAnimState->setEnabled(false);
}
}
void AnimationTreeSystem::initializeTreeStates(
const AnimationTreeNode &node, AnimationTreeComponent &at)
{
if (node.type == "stateMachine") {
if (at.currentStates.find(node.name) ==
at.currentStates.end()) {
for (const auto &child : node.children) {
if (child.type == "state") {
at.currentStates[node.name] = child.name;
break;
}
}
}
}
for (const auto &child : node.children)
initializeTreeStates(child, at);
}
void AnimationTreeSystem::update(float deltaTime)
{
if (!m_initialized)
return;
m_world.query<AnimationTreeComponent>().each(
[this, deltaTime](flecs::entity e, AnimationTreeComponent &at) {
[this, deltaTime](flecs::entity e,
AnimationTreeComponent &at) {
if (at.dirty) {
setupEntity(e, at);
at.dirty = false;
@@ -320,39 +233,374 @@ void AnimationTreeSystem::update(float deltaTime)
if (it == m_states.end())
return;
EntityAnimState &state = it->second;
if (!state.animState)
EntityAnimTreeState &state = it->second;
if (!state.ogreEntity)
return;
/* Sync runtime toggles */
if (state.animState->getEnabled() != at.enabled) {
state.animState->setEnabled(at.enabled);
if (!at.enabled && state.rootMotion)
state.rootMotion->unapply();
}
if (state.animState->getLoop() != at.loop)
state.animState->setLoop(at.loop);
if (!at.enabled)
if (!at.enabled) {
disableAllAnimations(state);
return;
float dt = deltaTime * at.speed;
if (dt == 0.0f)
return;
float lastTime = state.animState->getTimePosition();
state.animState->addTime(dt);
float thisTime = state.animState->getTimePosition();
float length = state.animState->getLength();
bool loop = state.animState->getLoop();
int loops = 0;
if (loop && length > 0.0f) {
loops = (int)std::round(
(lastTime + dt - thisTime) / length);
}
if (at.useRootMotion && state.rootMotion)
state.rootMotion->apply(loops, thisTime);
/* Sync external state changes (editor) */
syncStateChanges(at, state);
/* Evaluate tree */
EvalContext ctx;
ctx.deltaTime = deltaTime;
evaluateNode(at.root, 1.0f, 1.0f, at, state, ctx);
/* Apply animation weights and advance */
Ogre::Vector3 totalRootMotion =
Ogre::Vector3::ZERO;
Ogre::SceneNode *sceneNode =
state.ogreEntity->getParentSceneNode();
for (auto &pair : ctx.animData) {
const Ogre::String &animName = pair.first;
AnimEvalData &data = pair.second;
auto itAnim =
state.animations.find(animName);
if (itAnim == state.animations.end())
continue;
AnimationRuntimeInfo &info =
itAnim->second;
if (!info.ogreAnimState)
continue;
bool active = data.weight > 0.001f;
info.ogreAnimState->setEnabled(active);
info.ogreAnimState->setWeight(
Ogre::Math::Clamp(data.weight, 0.0f,
1.0f));
if (active && data.timeDelta != 0.0f) {
float lastTime = info.ogreAnimState
->getTimePosition();
info.ogreAnimState->addTime(
data.timeDelta);
float thisTime =
info.ogreAnimState
->getTimePosition();
float length =
info.ogreAnimState
->getLength();
bool loop =
info.ogreAnimState
->getLoop();
int loops = 0;
if (loop && length > 0.0f) {
loops = (int)std::round(
(lastTime + data.timeDelta -
thisTime) /
length);
}
if (at.useRootMotion &&
info.rootTrack) {
Ogre::TransformKeyFrame tkf(
nullptr, 0.0f);
info.rootTrack
->getInterpolatedKeyFrame(
lastTime, &tkf);
Ogre::Vector3 lastPos =
tkf.getTranslate();
info.rootTrack
->getInterpolatedKeyFrame(
thisTime, &tkf);
Ogre::Vector3 thisPos =
tkf.getTranslate();
Ogre::Vector3 delta =
thisPos - lastPos +
loops *
info.loopTranslation;
totalRootMotion +=
delta * data.weight;
}
}
}
if (at.useRootMotion && sceneNode)
sceneNode->translate(totalRootMotion,
Ogre::Node::TS_LOCAL);
/* Reset root bone to binding pose */
if (state.rootBone) {
state.rootBone->setPosition(
state.rootBindingPosition);
state.rootBone->setOrientation(
state.rootBindingOrientation);
state.rootBone->setScale(
state.rootBindingScale);
}
/* Handle end-of-animation transitions */
checkEndTransitions(e, at, state, ctx);
});
}
void AnimationTreeSystem::evaluateNode(
const AnimationTreeNode &node, float parentWeight, float timeMul,
const AnimationTreeComponent &at, EntityAnimTreeState &state,
EvalContext &ctx)
{
if (parentWeight < 0.001f)
return;
if (node.type == "output") {
if (!node.children.empty())
evaluateNode(node.children[0], parentWeight,
timeMul * node.speed, at, state, ctx);
} else if (node.type == "speed") {
if (!node.children.empty())
evaluateNode(node.children[0], parentWeight,
timeMul * node.speed, at, state, ctx);
} else if (node.type == "stateMachine") {
auto itCurrent = at.currentStates.find(node.name);
if (itCurrent == at.currentStates.end())
return;
auto &fade = state.fadeStates[node.name];
const Ogre::String &currentName = itCurrent->second;
/* Ensure weight entries exist */
for (const auto &child : node.children) {
if (child.type == "state" &&
fade.weights.find(child.name) ==
fade.weights.end()) {
fade.weights[child.name] =
(child.name == currentName) ? 1.0f :
0.0f;
}
}
/* Update fades */
std::vector<Ogre::String> doneIn;
for (const auto &inName : fade.fadeIn) {
fade.weights[inName] +=
ctx.deltaTime * node.fadeSpeed;
if (fade.weights[inName] >= 1.0f) {
fade.weights[inName] = 1.0f;
doneIn.push_back(inName);
}
}
for (const auto &n : doneIn)
fade.fadeIn.erase(n);
std::vector<Ogre::String> doneOut;
for (const auto &outName : fade.fadeOut) {
fade.weights[outName] -=
ctx.deltaTime * node.fadeSpeed;
if (fade.weights[outName] <= 0.0f) {
fade.weights[outName] = 0.0f;
doneOut.push_back(outName);
}
}
for (const auto &n : doneOut)
fade.fadeOut.erase(n);
/* Evaluate active states */
for (const auto &child : node.children) {
if (child.type != "state")
continue;
float w = fade.weights[child.name];
if (w > 0.001f)
evaluateNode(child, parentWeight * w,
timeMul, at, state, ctx);
}
/* Queue end-transition check */
if (node.endTransitions.find(currentName) !=
node.endTransitions.end())
ctx.endChecks.push_back(
{node.name, currentName});
} else if (node.type == "state") {
if (!node.children.empty())
evaluateNode(node.children[0], parentWeight,
timeMul, at, state, ctx);
} else if (node.type == "animation") {
auto &data = ctx.animData[node.animationName];
data.weight += parentWeight;
data.timeDelta = ctx.deltaTime * timeMul;
}
}
void AnimationTreeSystem::syncStateChanges(
AnimationTreeComponent &at, EntityAnimTreeState &state)
{
for (auto &pair : at.currentStates) {
auto itPrev = state.prevStates.find(pair.first);
if (itPrev == state.prevStates.end() ||
itPrev->second != pair.second) {
/* State changed externally (editor); set up fade */
auto &fade = state.fadeStates[pair.first];
if (itPrev != state.prevStates.end())
fade.fadeOut.insert(itPrev->second);
fade.fadeIn.insert(pair.second);
fade.weights[pair.second] = 0.0f;
state.prevStates[pair.first] = pair.second;
}
}
/* Also track newly added state machines */
for (auto &pair : at.currentStates) {
if (state.prevStates.find(pair.first) ==
state.prevStates.end())
state.prevStates[pair.first] = pair.second;
}
}
void AnimationTreeSystem::checkEndTransitions(
flecs::entity e, AnimationTreeComponent &at,
EntityAnimTreeState &state, EvalContext &ctx)
{
for (auto &pair : ctx.endChecks) {
const Ogre::String &smName = pair.first;
const Ogre::String &stateName = pair.second;
const AnimationTreeNode *smNode =
findStateMachineNode(at.root, smName);
if (!smNode)
continue;
auto itTrans = smNode->endTransitions.find(stateName);
if (itTrans == smNode->endTransitions.end())
continue;
const AnimationTreeNode *stNode =
findStateNode(*smNode, stateName);
if (!stNode)
continue;
const AnimationTreeNode *animNode =
findAnimationNode(*stNode);
if (!animNode)
continue;
auto itAnim = state.animations.find(
animNode->animationName);
if (itAnim == state.animations.end())
continue;
Ogre::AnimationState *as =
itAnim->second.ogreAnimState;
if (!as)
continue;
float t = as->getTimePosition();
float len = as->getLength();
if (len > 0.0f && t >= len * 0.99f)
setStateInternal(e, at, state, smName,
itTrans->second, true);
}
}
void AnimationTreeSystem::setState(flecs::entity e,
const Ogre::String &stateMachineName,
const Ogre::String &stateName,
bool reset)
{
auto it = m_states.find(e.id());
if (it == m_states.end())
return;
if (!e.has<AnimationTreeComponent>())
return;
auto &at = e.get_mut<AnimationTreeComponent>();
setStateInternal(e, at, it->second, stateMachineName,
stateName, reset);
}
void AnimationTreeSystem::setStateInternal(
flecs::entity e, AnimationTreeComponent &at,
EntityAnimTreeState &state,
const Ogre::String &stateMachineName,
const Ogre::String &stateName, bool reset)
{
(void)e;
auto itCurrent = at.currentStates.find(stateMachineName);
if (itCurrent == at.currentStates.end())
return;
if (itCurrent->second == stateName)
return;
auto &fade = state.fadeStates[stateMachineName];
fade.fadeOut.insert(itCurrent->second);
fade.fadeIn.insert(stateName);
fade.weights[stateName] = 0.0f;
itCurrent->second = stateName;
state.prevStates[stateMachineName] = stateName;
if (reset) {
/* Reset the new state's animation */
const AnimationTreeNode *smNode =
findStateMachineNode(at.root, stateMachineName);
if (smNode) {
const AnimationTreeNode *stNode =
findStateNode(*smNode, stateName);
if (stNode) {
const AnimationTreeNode *animNode =
findAnimationNode(*stNode);
if (animNode) {
auto itAnim = state.animations.find(
animNode->animationName);
if (itAnim != state.animations.end() &&
itAnim->second.ogreAnimState)
itAnim->second.ogreAnimState
->setTimePosition(0.0f);
}
}
}
}
}
Ogre::String AnimationTreeSystem::getCurrentState(
flecs::entity e, const Ogre::String &stateMachineName)
{
if (!e.has<AnimationTreeComponent>())
return "";
auto &at = e.get<AnimationTreeComponent>();
auto it = at.currentStates.find(stateMachineName);
if (it != at.currentStates.end())
return it->second;
return "";
}
const AnimationTreeNode *AnimationTreeSystem::findStateMachineNode(
const AnimationTreeNode &root, const Ogre::String &name) const
{
if (root.type == "stateMachine" && root.name == name)
return &root;
for (const auto &child : root.children) {
const AnimationTreeNode *found =
findStateMachineNode(child, name);
if (found)
return found;
}
return nullptr;
}
const AnimationTreeNode *AnimationTreeSystem::findStateNode(
const AnimationTreeNode &smNode,
const Ogre::String &stateName) const
{
for (const auto &child : smNode.children) {
if (child.type == "state" && child.name == stateName)
return &child;
}
return nullptr;
}
const AnimationTreeNode *AnimationTreeSystem::findAnimationNode(
const AnimationTreeNode &stateNode) const
{
if (stateNode.type == "animation")
return &stateNode;
for (const auto &child : stateNode.children) {
const AnimationTreeNode *found =
findAnimationNode(child);
if (found)
return found;
}
return nullptr;
}

View File

@@ -6,44 +6,18 @@
#include <Ogre.h>
#include <memory>
#include <unordered_map>
#include <set>
#include "../components/AnimationTree.hpp"
/**
* RootMotionTracker applies transforms from a NodeAnimationTrack to an
* Entity's parent SceneNode instead of to its Skeleton's Bone.
* System that evaluates an AnimationTreeComponent each frame.
*
* Based on OGRE Sample_SkeletalAnimation's RootMotionApplier.
*/
class RootMotionTracker {
public:
RootMotionTracker(Ogre::AnimationState *animationState,
Ogre::Entity *entity);
void apply(int loops, float thisTime);
void unapply();
bool hasTrack() const { return mTrack != nullptr; }
private:
Ogre::Entity *mEntity;
Ogre::NodeAnimationTrack *mTrack;
Ogre::Vector3 mRootBindingPosition;
Ogre::Quaternion mRootBindingOrientation;
Ogre::Quaternion mRootBindingOrientationInverse;
Ogre::Vector3 mLoopTranslation;
Ogre::Quaternion mLoopRotation;
Ogre::Quaternion mLoopRotationInverse;
Ogre::Vector3 mAppliedTranslation;
Ogre::Quaternion mAppliedRotation;
};
/**
* System that manages skeletal animation playback and root motion
* for entities with AnimationTreeComponent.
* Walks the tree recursively to:
* - Determine active animation states and their blend weights
* - Advance Ogre::AnimationState::addTime() for active animations
* - Compute and apply root motion (optional)
* - Handle end-of-animation auto-transitions
*/
class AnimationTreeSystem {
public:
@@ -53,29 +27,93 @@ public:
void initialize();
void update(float deltaTime);
/**
* Manually switch a state machine to a new state.
* Sets up cross-fade automatically.
*/
void setState(flecs::entity e, const Ogre::String &stateMachineName,
const Ogre::String &stateName, bool reset = false);
/**
* Find the Ogre entity with a skeleton for a given flecs entity.
* Checks CharacterSlotsComponent master, RenderableComponent, then
* attached objects on the transform node.
*/
static Ogre::Entity *findAnimatedEntity(flecs::entity e);
/**
* Get current state name of a state machine (for editor display).
*/
Ogre::String getCurrentState(flecs::entity e,
const Ogre::String &stateMachineName);
private:
struct EntityAnimState {
Ogre::AnimationState *animState = nullptr;
struct AnimationRuntimeInfo {
Ogre::AnimationState *ogreAnimState = nullptr;
Ogre::NodeAnimationTrack *rootTrack = nullptr;
Ogre::Vector3 loopTranslation = Ogre::Vector3::ZERO;
};
struct FadeInfo {
std::set<Ogre::String> fadeIn;
std::set<Ogre::String> fadeOut;
std::unordered_map<Ogre::String, float> weights;
};
struct EntityAnimTreeState {
Ogre::Entity *ogreEntity = nullptr;
std::unique_ptr<RootMotionTracker> rootMotion;
Ogre::String currentAnimName;
Ogre::Bone *rootBone = nullptr;
Ogre::Vector3 rootBindingPosition;
Ogre::Quaternion rootBindingOrientation;
Ogre::Vector3 rootBindingScale;
std::unordered_map<Ogre::String, AnimationRuntimeInfo> animations;
std::unordered_map<Ogre::String, FadeInfo> fadeStates;
/* Track previous currentStates to detect external changes */
std::unordered_map<Ogre::String, Ogre::String> prevStates;
};
struct AnimEvalData {
float weight = 0.0f;
float timeDelta = 0.0f;
};
struct EvalContext {
float deltaTime = 0.0f;
std::unordered_map<Ogre::String, AnimEvalData> animData;
std::vector<std::pair<Ogre::String, Ogre::String>> endChecks;
};
void setupEntity(flecs::entity e, AnimationTreeComponent &at);
void teardownEntity(flecs::entity e);
void disableAllAnimations(EntityAnimTreeState &state);
void initializeTreeStates(const AnimationTreeNode &node,
AnimationTreeComponent &at);
void evaluateNode(const AnimationTreeNode &node, float parentWeight,
float timeMul, const AnimationTreeComponent &at,
EntityAnimTreeState &state, EvalContext &ctx);
void checkEndTransitions(flecs::entity e, AnimationTreeComponent &at,
EntityAnimTreeState &state,
EvalContext &ctx);
void syncStateChanges(AnimationTreeComponent &at,
EntityAnimTreeState &state);
void setStateInternal(flecs::entity e, AnimationTreeComponent &at,
EntityAnimTreeState &state,
const Ogre::String &stateMachineName,
const Ogre::String &stateName, bool reset);
const AnimationTreeNode *findStateMachineNode(
const AnimationTreeNode &root,
const Ogre::String &name) const;
const AnimationTreeNode *findStateNode(
const AnimationTreeNode &smNode,
const Ogre::String &stateName) const;
const AnimationTreeNode *findAnimationNode(
const AnimationTreeNode &stateNode) const;
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
bool m_initialized = false;
std::unordered_map<flecs::entity_t, EntityAnimState> m_states;
std::unordered_map<flecs::entity_t, EntityAnimTreeState> m_states;
};
#endif // EDITSCENE_ANIMATIONTREESYSTEM_HPP

View File

@@ -1308,31 +1308,76 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, const nloh
entity.set<CharacterSlotsComponent>(cs);
}
nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity)
static nlohmann::json serializeAnimationTreeNode(
const AnimationTreeNode &node)
{
auto& at = entity.get<AnimationTreeComponent>();
nlohmann::json json;
json["currentAnimation"] = at.currentAnimation;
json["speed"] = at.speed;
json["loop"] = at.loop;
json["enabled"] = at.enabled;
json["useRootMotion"] = at.useRootMotion;
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;
}
void SceneSerializer::deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json)
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);
}
}
}
nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity)
{
auto &at = entity.get<AnimationTreeComponent>();
nlohmann::json json;
json["enabled"] = at.enabled;
json["useRootMotion"] = at.useRootMotion;
json["root"] = serializeAnimationTreeNode(at.root);
return json;
}
void SceneSerializer::deserializeAnimationTree(flecs::entity entity,
const nlohmann::json &json)
{
AnimationTreeComponent at;
at.currentAnimation = json.value("currentAnimation", "");
at.speed = json.value("speed", 1.0f);
at.loop = json.value("loop", true);
at.enabled = json.value("enabled", true);
at.useRootMotion = json.value("useRootMotion", false);
if (json.contains("root"))
deserializeAnimationTreeNode(at.root, json["root"]);
at.dirty = true;
entity.set<AnimationTreeComponent>(at);
}

View File

@@ -7,84 +7,511 @@ AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr)
{
}
bool AnimationTreeEditor::renderComponent(flecs::entity entity,
AnimationTreeComponent &at)
std::vector<Ogre::String> AnimationTreeEditor::getAvailableAnimations(
flecs::entity entity)
{
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") {
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") {
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);
if (stateMachines.empty())
return;
ImGui::Separator();
ImGui::Text("State Preview");
for (auto *sm : stateMachines) {
if (!sm)
continue;
auto itCurrent = at.currentStates.find(sm->name);
Ogre::String currentState =
(itCurrent != at.currentStates.end()) ?
itCurrent->second :
"";
Ogre::String label = sm->name + "##" + sm->name;
Ogre::String preview = currentState.empty() ?
"(none)" :
currentState;
if (ImGui::BeginCombo(label.c_str(), preview.c_str())) {
for (const auto &child : sm->children) {
if (child.type != "state")
continue;
bool sel = (currentState == child.name);
if (ImGui::Selectable(child.name.c_str(),
sel)) {
at.currentStates[sm->name] =
child.name;
at.dirty = true;
}
}
ImGui::EndCombo();
}
}
}
bool AnimationTreeEditor::renderComponent(
flecs::entity entity, AnimationTreeComponent &at)
{
bool modified = false;
(void)entity;
if (ImGui::CollapsingHeader("Animation Tree",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent();
Ogre::Entity *ent =
AnimationTreeSystem::findAnimatedEntity(entity);
std::vector<Ogre::String> animNames;
if (ent && ent->hasSkeleton()) {
Ogre::AnimationStateSet *states =
ent->getAllAnimationStates();
if (states) {
for (const auto &pair : states->getAnimationStates())
animNames.push_back(pair.first);
}
/* Global toggles */
if (ImGui::Checkbox("Enabled", &at.enabled))
modified = true;
if (ImGui::Checkbox("Use Root Motion",
&at.useRootMotion)) {
modified = true;
at.dirty = true;
}
if (animNames.empty()) {
ImGui::TextDisabled("No skeleton found on entity");
} else {
/* Animation selector */
Ogre::String currentAnim = at.currentAnimation;
Ogre::String preview =
currentAnim.empty() ? "(none)" : currentAnim;
if (ImGui::BeginCombo("Animation", preview.c_str())) {
bool noneSelected = currentAnim.empty();
if (ImGui::Selectable("(none)", noneSelected)) {
at.currentAnimation = "";
modified = true;
at.dirty = true;
}
if (noneSelected)
ImGui::SetItemDefaultFocus();
ImGui::Separator();
for (const auto &name : animNames) {
bool isSelected = (currentAnim == name);
if (ImGui::Selectable(name.c_str(), isSelected)) {
at.currentAnimation = name;
modified = true;
at.dirty = true;
}
if (isSelected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
/* Tree view */
static AnimationTreeNode *selectedNode = nullptr;
static AnimationTreeNode *selectedParent = nullptr;
int id = 0;
renderTree(at.root, nullptr, selectedNode,
selectedParent, id);
/* Speed */
if (ImGui::SliderFloat("Speed", &at.speed, -2.0f, 5.0f,
"%.2f"))
modified = true;
/* Loop */
if (ImGui::Checkbox("Loop", &at.loop))
modified = true;
/* Enabled (play/pause) */
if (ImGui::Checkbox("Enabled", &at.enabled))
modified = true;
/* Root motion */
if (ImGui::Checkbox("Use Root Motion", &at.useRootMotion)) {
modified = true;
at.dirty = true;
}
if (!at.currentAnimation.empty()) {
ImGui::Text("State: %s",
at.enabled ? "Playing" : "Paused");
}
/* 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);
ImGui::Unindent();
}

View File

@@ -4,9 +4,13 @@
#include "ComponentEditor.hpp"
#include "../components/AnimationTree.hpp"
#include <OgreSceneManager.h>
#include <vector>
/**
* Editor for AnimationTreeComponent
* Editor for AnimationTreeComponent.
*
* Provides a recursive tree view for editing the animation hierarchy
* and state-machine current-state selectors for preview.
*/
class AnimationTreeEditor : public ComponentEditor<AnimationTreeComponent> {
public:
@@ -14,12 +18,42 @@ 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