Character Animation

This commit is contained in:
2026-04-19 19:24:55 +03:00
parent a392eb0bf9
commit 43e9fb330f
14 changed files with 686 additions and 1 deletions

View File

@@ -26,6 +26,7 @@ set(EDITSCENE_SOURCES
systems/RoomLayoutSystem.cpp
systems/FurnitureLibrary.cpp
systems/CharacterSlotSystem.cpp
systems/AnimationTreeSystem.cpp
ui/TransformEditor.cpp
ui/RenderableEditor.cpp
ui/PhysicsColliderEditor.cpp
@@ -41,6 +42,7 @@ set(EDITSCENE_SOURCES
ui/PrimitiveEditor.cpp
ui/TriangleBufferEditor.cpp
ui/CharacterSlotsEditor.cpp
ui/AnimationTreeEditor.cpp
ui/CellGridEditor.cpp
ui/LotEditor.cpp
ui/DistrictEditor.cpp
@@ -61,6 +63,7 @@ set(EDITSCENE_SOURCES
components/PrimitiveModule.cpp
components/TriangleBufferModule.cpp
components/CharacterSlotsModule.cpp
components/AnimationTreeModule.cpp
components/CellGridModule.cpp
components/CellGridEditorsModule.cpp
components/CellGrid.cpp
@@ -88,6 +91,7 @@ set(EDITSCENE_HEADERS
components/Primitive.hpp
components/TriangleBuffer.hpp
components/CharacterSlots.hpp
components/AnimationTree.hpp
components/CellGrid.hpp
systems/EditorUISystem.hpp
systems/CellGridSystem.hpp
@@ -96,6 +100,7 @@ set(EDITSCENE_HEADERS
systems/ProceduralMaterialSystem.hpp
systems/ProceduralMeshSystem.hpp
systems/CharacterSlotSystem.hpp
systems/AnimationTreeSystem.hpp
systems/ProceduralTextureSystem.hpp
systems/StaticGeometrySystem.hpp
systems/SceneSerializer.hpp
@@ -121,6 +126,7 @@ set(EDITSCENE_HEADERS
ui/PrimitiveEditor.hpp
ui/TriangleBufferEditor.hpp
ui/CharacterSlotsEditor.hpp
ui/AnimationTreeEditor.hpp
ui/CellGridEditor.hpp
ui/LotEditor.hpp
ui/DistrictEditor.hpp

View File

@@ -10,6 +10,7 @@
#include "systems/ProceduralMaterialSystem.hpp"
#include "systems/ProceduralMeshSystem.hpp"
#include "systems/CharacterSlotSystem.hpp"
#include "systems/AnimationTreeSystem.hpp"
#include "systems/CellGridSystem.hpp"
#include "systems/RoomLayoutSystem.hpp"
#include "camera/EditorCamera.hpp"
@@ -31,6 +32,7 @@
#include "components/Primitive.hpp"
#include "components/TriangleBuffer.hpp"
#include "components/CharacterSlots.hpp"
#include "components/AnimationTree.hpp"
#include "components/CellGrid.hpp"
#include "components/CellGridModule.hpp"
#include <OgreRTShaderSystem.h>
@@ -122,6 +124,7 @@ EditorApp::~EditorApp()
// Release all systems
m_characterSlotSystem.reset();
m_animationTreeSystem.reset();
m_proceduralMeshSystem.reset();
m_proceduralMaterialSystem.reset();
m_proceduralTextureSystem.reset();
@@ -231,6 +234,11 @@ void EditorApp::setup()
m_world, m_sceneMgr);
m_characterSlotSystem->initialize();
// Setup AnimationTree system
m_animationTreeSystem = std::make_unique<AnimationTreeSystem>(
m_world, m_sceneMgr);
m_animationTreeSystem->initialize();
// Setup CellGrid system
m_cellGridSystem =
std::make_unique<CellGridSystem>(m_world, m_sceneMgr);
@@ -300,6 +308,9 @@ void EditorApp::setupECS()
// Register CharacterSlots component
m_world.component<CharacterSlotsComponent>();
// Register AnimationTree component
m_world.component<AnimationTreeComponent>();
// Register CellGrid/Town components
CellGridModule::registerComponents(m_world);
}
@@ -465,6 +476,11 @@ bool EditorApp::frameRenderingQueued(const Ogre::FrameEvent &evt)
m_characterSlotSystem->update();
}
// Update AnimationTree system
if (m_animationTreeSystem) {
m_animationTreeSystem->update(evt.timeSinceLastFrame);
}
// Update ProceduralMesh system
if (m_proceduralMeshSystem) {
m_proceduralMeshSystem->update();

View File

@@ -22,6 +22,7 @@ class ProceduralTextureSystem;
class ProceduralMaterialSystem;
class ProceduralMeshSystem;
class CharacterSlotSystem;
class AnimationTreeSystem;
class CellGridSystem;
class RoomLayoutSystem;
@@ -118,6 +119,7 @@ private:
std::unique_ptr<ProceduralMaterialSystem> m_proceduralMaterialSystem;
std::unique_ptr<ProceduralMeshSystem> m_proceduralMeshSystem;
std::unique_ptr<CharacterSlotSystem> m_characterSlotSystem;
std::unique_ptr<AnimationTreeSystem> m_animationTreeSystem;
std::unique_ptr<CellGridSystem> m_cellGridSystem;
std::unique_ptr<RoomLayoutSystem> m_roomLayoutSystem;

View File

@@ -0,0 +1,22 @@
#ifndef EDITSCENE_ANIMATIONTREE_HPP
#define EDITSCENE_ANIMATIONTREE_HPP
#pragma once
#include <Ogre.h>
/**
* 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).
*/
struct AnimationTreeComponent {
Ogre::String currentAnimation;
float speed = 1.0f;
bool loop = true;
bool enabled = true;
bool useRootMotion = false;
bool dirty = true;
AnimationTreeComponent() = default;
};
#endif // EDITSCENE_ANIMATIONTREE_HPP

View File

@@ -0,0 +1,30 @@
#include "AnimationTree.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/AnimationTreeEditor.hpp"
#include "Transform.hpp"
REGISTER_COMPONENT_GROUP("Animation Tree", "Rendering",
AnimationTreeComponent, AnimationTreeEditor)
{
registry.registerComponent<AnimationTreeComponent>(
"Animation Tree", "Rendering",
std::make_unique<AnimationTreeEditor>(sceneMgr),
/* Adder */
[sceneMgr](flecs::entity e) {
if (!e.has<TransformComponent>()) {
TransformComponent transform;
transform.node =
sceneMgr->getRootSceneNode()
->createChildSceneNode();
e.set<TransformComponent>(transform);
}
AnimationTreeComponent at;
at.dirty = true;
e.set<AnimationTreeComponent>(at);
},
/* Remover */
[sceneMgr](flecs::entity e) {
(void)sceneMgr;
e.remove<AnimationTreeComponent>();
});
}

View File

@@ -14,6 +14,9 @@ struct CharacterSlotsComponent {
std::unordered_map<Ogre::String, Ogre::String> slots;
bool dirty = true;
/* Runtime: master entity with shared skeleton (set by CharacterSlotSystem) */
Ogre::Entity *masterEntity = nullptr;
CharacterSlotsComponent() = default;
};

View File

@@ -0,0 +1,358 @@
#include "AnimationTreeSystem.hpp"
#include "../components/Transform.hpp"
#include "../components/Renderable.hpp"
#include "../components/CharacterSlots.hpp"
#include <OgreEntity.h>
#include <OgreLogManager.h>
#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)
, m_sceneMgr(sceneMgr)
{
m_world.observer<AnimationTreeComponent>("AnimationTreeCleanup")
.event(flecs::OnRemove)
.each([this](flecs::entity e, AnimationTreeComponent &) {
teardownEntity(e);
});
}
AnimationTreeSystem::~AnimationTreeSystem()
{
std::vector<flecs::entity_t> toRemove;
for (auto &pair : m_states)
toRemove.push_back(pair.first);
for (auto id : toRemove)
teardownEntity(m_world.entity(id));
}
void AnimationTreeSystem::initialize()
{
m_initialized = true;
}
Ogre::Entity *AnimationTreeSystem::findAnimatedEntity(flecs::entity e)
{
if (!e.is_alive())
return nullptr;
if (e.has<CharacterSlotsComponent>()) {
auto &cs = e.get<CharacterSlotsComponent>();
if (cs.masterEntity && cs.masterEntity->hasSkeleton())
return cs.masterEntity;
}
if (e.has<RenderableComponent>()) {
auto &rc = e.get<RenderableComponent>();
if (rc.entity && rc.entity->hasSkeleton())
return rc.entity;
}
if (e.has<TransformComponent>()) {
auto &t = e.get<TransformComponent>();
if (t.node) {
for (unsigned int i = 0;
i < t.node->numAttachedObjects(); ++i) {
Ogre::MovableObject *obj =
t.node->getAttachedObject(i);
if (obj->getMovableType() == "Entity") {
Ogre::Entity *ent =
static_cast<Ogre::Entity *>(obj);
if (ent->hasSkeleton())
return ent;
}
}
}
}
return nullptr;
}
void AnimationTreeSystem::setupEntity(flecs::entity e,
AnimationTreeComponent &at)
{
Ogre::Entity *ent = findAnimatedEntity(e);
if (!ent || !ent->hasSkeleton()) {
teardownEntity(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;
state.ogreEntity = ent;
state.animState->setEnabled(at.enabled);
state.animState->setLoop(at.loop);
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();
}
}
}
void AnimationTreeSystem::teardownEntity(flecs::entity e)
{
auto it = m_states.find(e.id());
if (it == m_states.end())
return;
if (it->second.animState)
it->second.animState->setEnabled(false);
if (it->second.rootMotion)
it->second.rootMotion->unapply();
m_states.erase(it);
}
void AnimationTreeSystem::update(float deltaTime)
{
if (!m_initialized)
return;
m_world.query<AnimationTreeComponent>().each(
[this, deltaTime](flecs::entity e, AnimationTreeComponent &at) {
if (at.dirty) {
setupEntity(e, at);
at.dirty = false;
}
auto it = m_states.find(e.id());
if (it == m_states.end())
return;
EntityAnimState &state = it->second;
if (!state.animState)
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)
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);
});
}

View File

@@ -0,0 +1,81 @@
#ifndef EDITSCENE_ANIMATIONTREESYSTEM_HPP
#define EDITSCENE_ANIMATIONTREESYSTEM_HPP
#pragma once
#include <flecs.h>
#include <Ogre.h>
#include <memory>
#include <unordered_map>
#include "../components/AnimationTree.hpp"
/**
* RootMotionTracker applies transforms from a NodeAnimationTrack to an
* Entity's parent SceneNode instead of to its Skeleton's Bone.
*
* 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.
*/
class AnimationTreeSystem {
public:
AnimationTreeSystem(flecs::world &world, Ogre::SceneManager *sceneMgr);
~AnimationTreeSystem();
void initialize();
void update(float deltaTime);
/**
* 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);
private:
struct EntityAnimState {
Ogre::AnimationState *animState = nullptr;
Ogre::Entity *ogreEntity = nullptr;
std::unique_ptr<RootMotionTracker> rootMotion;
Ogre::String currentAnimName;
};
void setupEntity(flecs::entity e, AnimationTreeComponent &at);
void teardownEntity(flecs::entity e);
flecs::world &m_world;
Ogre::SceneManager *m_sceneMgr;
bool m_initialized = false;
std::unordered_map<flecs::entity_t, EntityAnimState> m_states;
};
#endif // EDITSCENE_ANIMATIONTREESYSTEM_HPP

View File

@@ -215,6 +215,7 @@ void CharacterSlotSystem::buildCharacter(flecs::entity e,
masterEnt = m_sceneMgr->createEntity(cs.slots.at(masterSlot));
transform.node->attachObject(masterEnt);
m_entities[e.id()].parts[masterSlot] = masterEnt;
cs.masterEntity = masterEnt;
std::cout << " master loaded: " << masterEnt->getName()
<< std::endl;
} catch (const Ogre::Exception &ex) {

View File

@@ -17,6 +17,7 @@
#include "../components/TriangleBuffer.hpp"
#include "../components/LodSettings.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/CellGrid.hpp"
#include "../ui/TransformEditor.hpp"
#include "../ui/RenderableEditor.hpp"
@@ -412,6 +413,8 @@ void EditorUISystem::renderEntityNode(flecs::entity entity, int depth)
indicators += " [Tex]";
if (entity.has<ProceduralMaterialComponent>())
indicators += " [Mat]";
if (entity.has<AnimationTreeComponent>())
indicators += " [Anim]";
snprintf(label, sizeof(label), "%s%s##%llu", name.c_str(),
indicators.c_str(), (unsigned long long)entity.id());
@@ -632,7 +635,14 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
m_componentRegistry.render<CharacterSlotsComponent>(entity, cs);
componentCount++;
}
// Render AnimationTree if present
if (entity.has<AnimationTreeComponent>()) {
auto &at = entity.get_mut<AnimationTreeComponent>();
m_componentRegistry.render<AnimationTreeComponent>(entity, at);
componentCount++;
}
// Render CellGrid if present
if (entity.has<CellGridComponent>()) {
auto &grid = entity.get_mut<CellGridComponent>();

View File

@@ -16,6 +16,7 @@
#include "../components/Primitive.hpp"
#include "../components/TriangleBuffer.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/CellGrid.hpp"
#include "../components/GeneratedPhysicsTag.hpp"
#include "EditorUISystem.hpp"
@@ -182,6 +183,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
if (entity.has<CharacterSlotsComponent>()) {
json["characterSlots"] = serializeCharacterSlots(entity);
}
if (entity.has<AnimationTreeComponent>()) {
json["animationTree"] = serializeAnimationTree(entity);
}
// CellGrid/Town components
if (entity.has<CellGridComponent>()) {
@@ -303,6 +308,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json& json, flecs::entit
deserializeCharacterSlots(entity, json["characterSlots"]);
}
if (json.contains("animationTree")) {
deserializeAnimationTree(entity, json["animationTree"]);
}
if (json.contains("triangleBuffer")) {
deserializeTriangleBuffer(entity, json["triangleBuffer"]);
}
@@ -1299,6 +1308,34 @@ void SceneSerializer::deserializeCharacterSlots(flecs::entity entity, const nloh
entity.set<CharacterSlotsComponent>(cs);
}
nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity)
{
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;
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);
at.dirty = true;
entity.set<AnimationTreeComponent>(at);
}
// ============================================================================
// CellGrid/Town Component Serialization

View File

@@ -55,6 +55,7 @@ private:
nlohmann::json serializePrimitive(flecs::entity entity);
nlohmann::json serializeTriangleBuffer(flecs::entity entity);
nlohmann::json serializeCharacterSlots(flecs::entity entity);
nlohmann::json serializeAnimationTree(flecs::entity entity);
// CellGrid/Town component serialization
nlohmann::json serializeCellGrid(flecs::entity entity);
@@ -83,6 +84,7 @@ private:
void deserializePrimitive(flecs::entity entity, const nlohmann::json& json);
void deserializeTriangleBuffer(flecs::entity entity, const nlohmann::json& json);
void deserializeCharacterSlots(flecs::entity entity, const nlohmann::json& json);
void deserializeAnimationTree(flecs::entity entity, const nlohmann::json& json);
// CellGrid/Town component deserialization
void deserializeCellGrid(flecs::entity entity, const nlohmann::json& json);

View File

@@ -0,0 +1,92 @@
#include "AnimationTreeEditor.hpp"
#include "../systems/AnimationTreeSystem.hpp"
#include <imgui.h>
AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr)
: m_sceneMgr(sceneMgr)
{
}
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);
}
}
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();
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();
}
/* 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");
}
}
ImGui::Unindent();
}
return modified;
}

View File

@@ -0,0 +1,25 @@
#ifndef EDITSCENE_ANIMATIONTREEEDITOR_HPP
#define EDITSCENE_ANIMATIONTREEEDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/AnimationTree.hpp"
#include <OgreSceneManager.h>
/**
* Editor for AnimationTreeComponent
*/
class AnimationTreeEditor : public ComponentEditor<AnimationTreeComponent> {
public:
explicit AnimationTreeEditor(Ogre::SceneManager *sceneMgr);
const char *getName() const override { return "Animation Tree"; }
protected:
bool renderComponent(flecs::entity entity,
AnimationTreeComponent &at) override;
private:
Ogre::SceneManager *m_sceneMgr;
};
#endif // EDITSCENE_ANIMATIONTREEEDITOR_HPP