Compare commits

..

2 Commits

Author SHA1 Message Date
d55bf970e0 Swim animations 2026-04-22 21:38:33 +03:00
30814ea35a Added animations for swimming 2026-04-22 19:50:14 +03:00
19 changed files with 331 additions and 7 deletions

View File

@@ -47,6 +47,7 @@ set(EDITSCENE_SOURCES
ui/TriangleBufferEditor.cpp
ui/CharacterSlotsEditor.cpp
ui/AnimationTreeEditor.cpp
ui/AnimationTreeTemplateEditor.cpp
ui/CharacterEditor.cpp
ui/CellGridEditor.cpp
ui/LotEditor.cpp
@@ -72,6 +73,7 @@ set(EDITSCENE_SOURCES
components/TriangleBufferModule.cpp
components/CharacterSlotsModule.cpp
components/AnimationTreeModule.cpp
components/AnimationTreeTemplateModule.cpp
components/AnimationTree.cpp
components/CharacterModule.cpp
components/CellGridModule.cpp
@@ -152,6 +154,7 @@ set(EDITSCENE_HEADERS
ui/TriangleBufferEditor.hpp
ui/CharacterSlotsEditor.hpp
ui/AnimationTreeEditor.hpp
ui/AnimationTreeTemplateEditor.hpp
ui/CharacterEditor.hpp
ui/CellGridEditor.hpp
ui/LotEditor.hpp

View File

@@ -27,6 +27,7 @@
#include "components/RigidBody.hpp"
#include "components/GeneratedPhysicsTag.hpp"
#include "components/BuoyancyInfo.hpp"
#include "components/InWater.hpp"
#include "components/WaterPhysics.hpp"
#include "components/WaterPlane.hpp"
#include "components/Light.hpp"
@@ -41,6 +42,7 @@
#include "components/TriangleBuffer.hpp"
#include "components/CharacterSlots.hpp"
#include "components/AnimationTree.hpp"
#include "components/AnimationTreeTemplate.hpp"
#include "components/Character.hpp"
#include "components/StartupMenu.hpp"
#include "components/PlayerController.hpp"
@@ -487,12 +489,16 @@ void EditorApp::setupECS()
// Register AnimationTree component
m_world.component<AnimationTreeComponent>();
// Register AnimationTreeTemplate component
m_world.component<AnimationTreeTemplate>();
// Register Character component
m_world.component<CharacterComponent>();
// Register game components
m_world.component<StartupMenuComponent>();
m_world.component<PlayerControllerComponent>();
m_world.component<InWater>();
// Register CellGrid/Town components
CellGridModule::registerComponents(m_world);

View File

@@ -150,6 +150,12 @@ struct AnimationTreeComponent {
bool useRootMotion = false;
bool dirty = true;
/* If set, the tree root is copied from the named template */
Ogre::String templateName;
/* Runtime: last copied template version (not serialized) */
uint64_t templateVersion = 0;
/* Runtime: current state of each state machine (not serialized) */
std::unordered_map<Ogre::String, Ogre::String> currentStates;

View File

@@ -0,0 +1,20 @@
#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

View File

@@ -0,0 +1,23 @@
#include "AnimationTreeTemplate.hpp"
#include "../ui/ComponentRegistration.hpp"
#include "../ui/AnimationTreeTemplateEditor.hpp"
REGISTER_COMPONENT_GROUP("Animation Tree Template", "Animation",
AnimationTreeTemplate, AnimationTreeTemplateEditor)
{
registry.registerComponent<AnimationTreeTemplate>(
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>();
}
});
}

View File

@@ -0,0 +1,12 @@
#ifndef EDITSCENE_INWATER_HPP
#define EDITSCENE_INWATER_HPP
#pragma once
/**
* Tag component indicating the entity is currently in water.
* Automatically added/removed by BuoyancySystem.
*/
struct InWater {
};
#endif // EDITSCENE_INWATER_HPP

View File

@@ -21,6 +21,11 @@ struct PlayerControllerComponent {
Ogre::String idleState = "idle";
Ogre::String walkState = "walking";
Ogre::String runState = "running";
/* Swim animation states (used when character is in water) */
Ogre::String swimIdleState = "swim-idle";
Ogre::String swimState = "swim";
Ogre::String swimFastState = "swim-fast";
};
#endif // EDITSCENE_PLAYERCONTROLLER_HPP

View File

@@ -228,6 +228,39 @@ void AnimationTreeSystem::initializeTreeStates(
initializeTreeStates(child, at);
}
void AnimationTreeSystem::resolveTemplate(AnimationTreeComponent &at)
{
if (at.templateName.empty())
return;
AnimationTreeTemplate *templ = nullptr;
AnimationTreeComponent *templAt = nullptr;
m_world.query<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;
at.dirty = true;
}
}
void AnimationTreeSystem::update(float deltaTime)
{
if (!m_initialized)
@@ -236,6 +269,8 @@ void AnimationTreeSystem::update(float deltaTime)
m_world.query<AnimationTreeComponent>().each(
[this, deltaTime](flecs::entity e,
AnimationTreeComponent &at) {
resolveTemplate(at);
if (at.dirty) {
if (setupEntity(e, at))
at.dirty = false;

View File

@@ -9,6 +9,7 @@
#include <set>
#include "../components/AnimationTree.hpp"
#include "../components/AnimationTreeTemplate.hpp"
/**
* System that evaluates an AnimationTreeComponent each frame.
@@ -100,6 +101,9 @@ private:
const Ogre::String &stateMachineName,
const Ogre::String &stateName, bool reset);
/* Resolve template reference and copy tree if template changed */
void resolveTemplate(AnimationTreeComponent &at);
const AnimationTreeNode *findStateMachineNode(
const AnimationTreeNode &root,
const Ogre::String &name) const;

View File

@@ -52,6 +52,15 @@ void BuoyancySystem::update(float deltaTime)
// Clear previous frame's water bodies
m_bodiesInWater.clear();
// Remove InWater tag from entities that were in water last frame
for (flecs::entity_t id : m_entitiesInWater) {
flecs::entity e = m_world.entity(id);
if (e.is_alive() && e.has<InWater>()) {
e.remove<InWater>();
}
}
m_entitiesInWater.clear();
// Perform broadphase query to find bodies in water
// Use water surface position from WaterPhysics component, camera XZ position
Ogre::Vector3 waterSurfacePos(m_cameraPosition.x,
@@ -101,6 +110,12 @@ void BuoyancySystem::update(float deltaTime)
continue;
}
// Mark entity as in water
if (!entity.has<InWater>()) {
entity.add<InWater>();
}
m_entitiesInWater.insert(entity.id());
// Debug logging for buoyancy application
if (m_debugEnabled) {
Ogre::Vector3 bodyPos = m_physics->getPosition(bodyID);

View File

@@ -8,6 +8,7 @@
#include <unordered_map>
#include "../physics/physics.h"
#include "../components/BuoyancyInfo.hpp"
#include "../components/InWater.hpp"
#include "../components/WaterPhysics.hpp"
#include "../components/Transform.hpp"
@@ -91,6 +92,9 @@ private:
// Cache for scene node -> entity mapping
std::unordered_map<Ogre::SceneNode *, flecs::entity_t> m_nodeToEntity;
// Entities that were in water last frame (for InWater tag management)
std::set<flecs::entity_t> m_entitiesInWater;
// Query for entities with BuoyancyInfo component
flecs::query<BuoyancyInfo> m_buoyancyInfoQuery;
};

View File

@@ -21,6 +21,7 @@
#include "../components/CharacterSlots.hpp"
#include "../components/Character.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/AnimationTreeTemplate.hpp"
#include "../components/StartupMenu.hpp"
#include "../components/PlayerController.hpp"
#include "../components/CellGrid.hpp"
@@ -701,6 +702,13 @@ void EditorUISystem::renderComponentList(flecs::entity entity)
componentCount++;
}
// Render AnimationTreeTemplate if present
if (entity.has<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>();

View File

@@ -1,6 +1,7 @@
#include "PlayerControllerSystem.hpp"
#include "../EditorApp.hpp"
#include "../components/PlayerController.hpp"
#include "../components/InWater.hpp"
#include "../components/Character.hpp"
#include "../components/Transform.hpp"
#include "../components/EntityName.hpp"
@@ -362,13 +363,24 @@ void PlayerControllerSystem::updateLocomotion(PlayerControllerComponent &pc,
if (!ats)
return;
bool inWater = state.targetEntity.has<InWater>();
Ogre::String animState;
if (!isMoving) {
animState = pc.idleState;
} else if (input.shift) {
animState = pc.runState;
if (inWater) {
if (!isMoving) {
animState = pc.swimIdleState;
} else if (input.shift) {
animState = pc.swimFastState;
} else {
animState = pc.swimState;
}
} else {
animState = pc.walkState;
if (!isMoving) {
animState = pc.idleState;
} else if (input.shift) {
animState = pc.runState;
} else {
animState = pc.walkState;
}
}
if (!animState.empty()) {

View File

@@ -18,6 +18,7 @@
#include "../components/Character.hpp"
#include "../components/CharacterSlots.hpp"
#include "../components/AnimationTree.hpp"
#include "../components/AnimationTreeTemplate.hpp"
#include "../components/StartupMenu.hpp"
#include "../components/PlayerController.hpp"
#include "../components/CellGrid.hpp"
@@ -211,6 +212,10 @@ nlohmann::json SceneSerializer::serializeEntity(flecs::entity entity)
json["animationTree"] = serializeAnimationTree(entity);
}
if (entity.has<AnimationTreeTemplate>()) {
json["animationTreeTemplate"] = serializeAnimationTreeTemplate(entity);
}
if (entity.has<StartupMenuComponent>()) {
json["startupMenu"] = serializeStartupMenu(entity);
}
@@ -364,6 +369,10 @@ void SceneSerializer::deserializeEntity(const nlohmann::json &json,
deserializeAnimationTree(entity, json["animationTree"]);
}
if (json.contains("animationTreeTemplate")) {
deserializeAnimationTreeTemplate(entity, json["animationTreeTemplate"]);
}
if (json.contains("startupMenu")) {
deserializeStartupMenu(entity, json["startupMenu"]);
}
@@ -1549,6 +1558,8 @@ nlohmann::json SceneSerializer::serializeAnimationTree(flecs::entity entity)
nlohmann::json json;
json["enabled"] = at.enabled;
json["useRootMotion"] = at.useRootMotion;
if (!at.templateName.empty())
json["templateName"] = at.templateName;
json["root"] = serializeAnimationTreeNode(at.root);
return json;
}
@@ -1559,12 +1570,31 @@ void SceneSerializer::deserializeAnimationTree(flecs::entity entity,
AnimationTreeComponent at;
at.enabled = json.value("enabled", true);
at.useRootMotion = json.value("useRootMotion", false);
at.templateName = json.value("templateName", "");
if (json.contains("root"))
deserializeAnimationTreeNode(at.root, json["root"]);
at.dirty = true;
entity.set<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;
}
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);
}
// ============================================================================
// CellGrid/Town Component Serialization
// ============================================================================
@@ -2189,6 +2219,9 @@ nlohmann::json SceneSerializer::serializePlayerController(flecs::entity entity)
json["idleState"] = pc.idleState;
json["walkState"] = pc.walkState;
json["runState"] = pc.runState;
json["swimIdleState"] = pc.swimIdleState;
json["swimState"] = pc.swimState;
json["swimFastState"] = pc.swimFastState;
return json;
}
@@ -2222,6 +2255,9 @@ void SceneSerializer::deserializePlayerController(flecs::entity entity,
pc.idleState = json.value("idleState", pc.idleState);
pc.walkState = json.value("walkState", pc.walkState);
pc.runState = json.value("runState", pc.runState);
pc.swimIdleState = json.value("swimIdleState", pc.swimIdleState);
pc.swimState = json.value("swimState", pc.swimState);
pc.swimFastState = json.value("swimFastState", pc.swimFastState);
entity.set<PlayerControllerComponent>(pc);
}

View File

@@ -62,6 +62,7 @@ private:
nlohmann::json serializeCharacter(flecs::entity entity);
nlohmann::json serializeCharacterSlots(flecs::entity entity);
nlohmann::json serializeAnimationTree(flecs::entity entity);
nlohmann::json serializeAnimationTreeTemplate(flecs::entity entity);
nlohmann::json serializeStartupMenu(flecs::entity entity);
nlohmann::json serializePlayerController(flecs::entity entity);
@@ -111,6 +112,8 @@ private:
const nlohmann::json &json);
void deserializeAnimationTree(flecs::entity entity,
const nlohmann::json &json);
void deserializeAnimationTreeTemplate(flecs::entity entity,
const nlohmann::json &json);
void deserializeStartupMenu(flecs::entity entity,
const nlohmann::json &json);
void deserializePlayerController(flecs::entity entity,

View File

@@ -1,5 +1,6 @@
#include "AnimationTreeEditor.hpp"
#include "../systems/AnimationTreeSystem.hpp"
#include "../components/AnimationTreeTemplate.hpp"
#include <imgui.h>
AnimationTreeEditor::AnimationTreeEditor(Ogre::SceneManager *sceneMgr)
@@ -211,7 +212,8 @@ void AnimationTreeEditor::renderTree(AnimationTreeNode &node,
if (ImGui::BeginPopupContextItem()) {
if (canHaveChildren(node.type)) {
if (node.type == "output" ||
node.type == "stateMachine") {
node.type == "stateMachine" ||
node.type == "state") {
if (ImGui::MenuItem("Add State Machine"))
queueAddChild(&node, "stateMachine");
}
@@ -259,7 +261,8 @@ void AnimationTreeEditor::renderTree(AnimationTreeNode &node,
}
if (ImGui::BeginPopup("AddChild")) {
if (node.type == "output" ||
node.type == "stateMachine") {
node.type == "stateMachine" ||
node.type == "state") {
if (ImGui::MenuItem("State Machine"))
queueAddChild(&node, "stateMachine");
}
@@ -473,12 +476,44 @@ void AnimationTreeEditor::renderStatePreview(
bool AnimationTreeEditor::renderComponent(
flecs::entity entity, AnimationTreeComponent &at)
{
ImGui::PushID("AnimTree");
bool modified = false;
if (ImGui::CollapsingHeader("Animation Tree",
ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Indent();
/* Template reference */
char templBuf[256];
snprintf(templBuf, sizeof(templBuf), "%s",
at.templateName.c_str());
if (ImGui::InputText("Template Name", templBuf,
sizeof(templBuf))) {
at.templateName = templBuf;
modified = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip(
"Leave empty for local tree. Set to template name to copy from a template entity.");
}
bool isTemplateEntity = entity.has<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);
}
ImGui::Separator();
/* Global toggles */
if (ImGui::Checkbox("Enabled", &at.enabled))
modified = true;
@@ -515,5 +550,12 @@ bool AnimationTreeEditor::renderComponent(
ImGui::Unindent();
}
/* Auto-publish template changes */
if (modified && entity.has<AnimationTreeTemplate>()) {
auto &templ = entity.get_mut<AnimationTreeTemplate>();
templ.version++;
}
ImGui::PopID();
return modified;
}

View File

@@ -0,0 +1,37 @@
#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;
}

View File

@@ -0,0 +1,24 @@
#ifndef EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP
#define EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP
#pragma once
#include "ComponentEditor.hpp"
#include "../components/AnimationTreeTemplate.hpp"
/**
* Editor for AnimationTreeTemplate component.
* The actual tree editing is done via the co-located AnimationTreeComponent.
* This editor only exposes the template name and version.
*/
class AnimationTreeTemplateEditor
: public ComponentEditor<AnimationTreeTemplate> {
public:
bool renderComponent(flecs::entity entity,
AnimationTreeTemplate &templ) override;
const char *getName() const override
{
return "Animation Tree Template";
}
};
#endif // EDITSCENE_ANIMATIONTREETEMPLATEEDITOR_HPP

View File

@@ -78,6 +78,35 @@ bool PlayerControllerEditor::renderComponent(flecs::entity entity,
modified = true;
}
ImGui::Separator();
ImGui::Text("Swim States (used when in water)");
char swimIdleBuf[256];
snprintf(swimIdleBuf, sizeof(swimIdleBuf), "%s",
pc.swimIdleState.c_str());
if (ImGui::InputText("Swim Idle State", swimIdleBuf,
sizeof(swimIdleBuf))) {
pc.swimIdleState = swimIdleBuf;
modified = true;
}
char swimBuf[256];
snprintf(swimBuf, sizeof(swimBuf), "%s",
pc.swimState.c_str());
if (ImGui::InputText("Swim State", swimBuf, sizeof(swimBuf))) {
pc.swimState = swimBuf;
modified = true;
}
char swimFastBuf[256];
snprintf(swimFastBuf, sizeof(swimFastBuf), "%s",
pc.swimFastState.c_str());
if (ImGui::InputText("Swim Fast State", swimFastBuf,
sizeof(swimFastBuf))) {
pc.swimFastState = swimFastBuf;
modified = true;
}
ImGui::Unindent();
}