From d6051486a87f8cc59d3e901c9e4a602294c2b629 Mon Sep 17 00:00:00 2001 From: zhenyan121 <3367366583@qq.com> Date: Thu, 6 Aug 2026 10:57:49 +0800 Subject: [PATCH] feat(creatures): animate creature walk cycles Add a Gait component synchronized over the network and procedural animation for model nodes. Load animation parameters from assets/model/creature/pig/animation.json and apply leg swing, body bob, and head motion based on gait. Refactor Gait into its own header for reuse. --- assets/model/creature/pig/animation.json | 33 +++++++++ .../Cubed/gameplay/client_entity_manager.hpp | 4 +- include/Cubed/gameplay/client_player.hpp | 1 - include/Cubed/gameplay/ecs/animation.hpp | 2 +- include/Cubed/gameplay/gait.hpp | 21 ++++++ include/Cubed/gameplay/local_player.hpp | 1 - include/Cubed/gameplay/player.hpp | 16 ----- include/Cubed/gameplay/server_player.hpp | 2 +- include/Cubed/render/model_manager.hpp | 1 + include/Cubed/render/model_node.hpp | 37 ++++++++++ include/Cubed/render/model_renderer.hpp | 10 ++- src/gameplay/client_entity_manager.cpp | 17 ++++- src/gameplay/client_world.cpp | 2 +- src/gameplay/server_entity_manager.cpp | 8 +++ src/proto/world/entity.proto | 1 + src/render/model_manager.cpp | 55 +++++++++++++++ src/render/model_renderer.cpp | 68 ++++++++++++++++--- src/render/world_renderer.cpp | 12 ++-- 18 files changed, 249 insertions(+), 42 deletions(-) create mode 100644 assets/model/creature/pig/animation.json create mode 100644 include/Cubed/gameplay/gait.hpp diff --git a/assets/model/creature/pig/animation.json b/assets/model/creature/pig/animation.json new file mode 100644 index 0000000..53859c2 --- /dev/null +++ b/assets/model/creature/pig/animation.json @@ -0,0 +1,33 @@ +{ + "walk": { + "speed": 6.0, + "amplitude": 25.0 + }, + "run": { + "speed": 12.0, + "amplitude": 40.0 + }, + "body_bob": 0.03, + "head": { + "node": "Head", + "amplitude": 4.0 + }, + "legs": [ + { + "node": "Foot_FL", + "phase": 0.0 + }, + { + "node": "Foot_FR", + "phase": 3.14159 + }, + { + "node": "Foot_HL", + "phase": 3.14159 + }, + { + "node": "Foot_HR", + "phase": 0.0 + } + ] +} \ No newline at end of file diff --git a/include/Cubed/gameplay/client_entity_manager.hpp b/include/Cubed/gameplay/client_entity_manager.hpp index fc81b9a..0489291 100644 --- a/include/Cubed/gameplay/client_entity_manager.hpp +++ b/include/Cubed/gameplay/client_entity_manager.hpp @@ -1,5 +1,6 @@ #pragma once #include "Cubed/gameplay/ecs/entity.hpp" +#include "Cubed/gameplay/gait.hpp" #include "glm/ext/vector_float3.hpp" #include "world/entity.pb.h" @@ -13,7 +14,7 @@ public: enum class Command { CREATE, DESTORY, UPDATE }; ClientEntityManager(ClientWorld& world); - void update(); + void update(float dt); void init(); void receive_entity_create(S2CEntityCreate& msg); @@ -36,6 +37,7 @@ private: EntityID id; glm::vec3 pos; glm::vec3 direction; + Gait gait; }; using EntityMap = tbb::concurrent_hash_map; diff --git a/include/Cubed/gameplay/client_player.hpp b/include/Cubed/gameplay/client_player.hpp index 7388688..ee255a8 100644 --- a/include/Cubed/gameplay/client_player.hpp +++ b/include/Cubed/gameplay/client_player.hpp @@ -3,7 +3,6 @@ #include "Cubed/gameplay/ecs/animation.hpp" #include "Cubed/gameplay/ecs/identity.hpp" #include "Cubed/gameplay/ecs/transform.hpp" -#include "Cubed/gameplay/player.hpp" namespace Cubed { struct ClientPlayer { diff --git a/include/Cubed/gameplay/ecs/animation.hpp b/include/Cubed/gameplay/ecs/animation.hpp index 4d27d5a..7a261f3 100644 --- a/include/Cubed/gameplay/ecs/animation.hpp +++ b/include/Cubed/gameplay/ecs/animation.hpp @@ -1,6 +1,6 @@ #pragma once -#include "Cubed/gameplay/player.hpp" +#include "Cubed/gameplay/gait.hpp" namespace Cubed { struct WalkPose { diff --git a/include/Cubed/gameplay/gait.hpp b/include/Cubed/gameplay/gait.hpp new file mode 100644 index 0000000..da10ea3 --- /dev/null +++ b/include/Cubed/gameplay/gait.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include + +namespace Cubed { +enum class Gait { STOP = 0, WALK = 1, RUN = 2 }; +constexpr int get_gait_id(Gait gait) { return std::to_underlying(gait); } + +inline Gait get_gait_from_id(int id) { + switch (id) { + case std::to_underlying(Gait::STOP): + return Gait::STOP; + case std::to_underlying(Gait::WALK): + return Gait::WALK; + case std::to_underlying(Gait::RUN): + return Gait::RUN; + default: + throw std::runtime_error("Unknown Gait"); + } +} +} // namespace Cubed \ No newline at end of file diff --git a/include/Cubed/gameplay/local_player.hpp b/include/Cubed/gameplay/local_player.hpp index b4a0cb6..520c05c 100644 --- a/include/Cubed/gameplay/local_player.hpp +++ b/include/Cubed/gameplay/local_player.hpp @@ -11,7 +11,6 @@ #include "Cubed/gameplay/game_time.hpp" #include "Cubed/gameplay/hitbox.hpp" #include "Cubed/gameplay/item_stack.hpp" -#include "Cubed/gameplay/player.hpp" #include "Cubed/input/event.hpp" #include "Cubed/input/input.hpp" diff --git a/include/Cubed/gameplay/player.hpp b/include/Cubed/gameplay/player.hpp index 750786d..20a5ce1 100644 --- a/include/Cubed/gameplay/player.hpp +++ b/include/Cubed/gameplay/player.hpp @@ -1,23 +1,7 @@ #pragma once #include "glm/ext/vector_float3.hpp" -#include -#include namespace Cubed { -enum class Gait { STOP = 0, WALK = 1, RUN = 2 }; -constexpr int get_gait_id(Gait gait) { return std::to_underlying(gait); } -inline Gait get_gait_from_id(int id) { - switch (id) { - case std::to_underlying(Gait::STOP): - return Gait::STOP; - case std::to_underlying(Gait::WALK): - return Gait::WALK; - case std::to_underlying(Gait::RUN): - return Gait::RUN; - default: - throw std::runtime_error("Unknown Gait"); - } -} static constexpr glm::vec3 PLAYER_SIZE{0.6f, 1.8f, 0.6f}; } // namespace Cubed diff --git a/include/Cubed/gameplay/server_player.hpp b/include/Cubed/gameplay/server_player.hpp index c98ee92..9c28b79 100644 --- a/include/Cubed/gameplay/server_player.hpp +++ b/include/Cubed/gameplay/server_player.hpp @@ -1,7 +1,7 @@ #pragma once #include "Cubed/gameplay/chunk_pos.hpp" +#include "Cubed/gameplay/gait.hpp" #include "Cubed/gameplay/game_time.hpp" -#include "Cubed/gameplay/player.hpp" #include #include diff --git a/include/Cubed/render/model_manager.hpp b/include/Cubed/render/model_manager.hpp index 3a01382..c1109f1 100644 --- a/include/Cubed/render/model_manager.hpp +++ b/include/Cubed/render/model_manager.hpp @@ -41,5 +41,6 @@ private: IDMap m_id_map; NameMap m_name_map; Handle load_model(std::string_view model_name); + void load_anim_config(ModelNode& node, const std::string& path); }; } // namespace Cubed \ No newline at end of file diff --git a/include/Cubed/render/model_node.hpp b/include/Cubed/render/model_node.hpp index b3e7414..9f3fac1 100644 --- a/include/Cubed/render/model_node.hpp +++ b/include/Cubed/render/model_node.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace Cubed { struct Mesh { @@ -32,11 +33,47 @@ struct Mesh { (void*)offsetof(Vertex3D, nx)); }; }; +struct NodeAnimRule { + enum class Role { NONE, LEG, HEAD }; + std::string node; + Role role = Role::NONE; + float phase = 0.0f; +}; + +struct ModelAnimConfig { + float walk_speed = 6.0f; + float walk_amp = 25.0f; + float run_speed = 12.0f; + float run_amp = 40.0f; + float body_bob = 0.05f; + float head_amp = 4.0f; + std::vector nodes; + + const NodeAnimRule* rule_for(std::string_view name) const { + for (const auto& r : nodes) { + if (r.node == name) { + return &r; + } + } + return nullptr; + } + + bool has_role(NodeAnimRule::Role role) const { + for (const auto& r : nodes) { + if (r.role == role) { + return true; + } + } + return false; + } +}; struct ModelNode { std::string name; glm::mat4 transform{1.0f}; + std::vector meshes; std::vector children; + ModelAnimConfig anim; }; } // namespace Cubed \ No newline at end of file diff --git a/include/Cubed/render/model_renderer.hpp b/include/Cubed/render/model_renderer.hpp index 477249d..629a61d 100644 --- a/include/Cubed/render/model_renderer.hpp +++ b/include/Cubed/render/model_renderer.hpp @@ -1,5 +1,6 @@ #pragma once +#include "Cubed/gameplay/ecs/animation.hpp" #include "Cubed/gameplay/model.hpp" #include "Cubed/render/model_node.hpp" #include "Cubed/shader.hpp" @@ -12,15 +13,18 @@ class ModelRender { public: ModelRender(Renderer& renderer); void render_model(ModelID id, const glm::vec3& pos, float yaw, - Camera& camera); + Camera& camera, const WalkPose& pose); void shadow_pass(ModelID id, const glm::vec3& pos, float yaw, - Camera& camera); + Camera& camera, const WalkPose& pose); private: Renderer& m_renderer; void render_node(const ModelNode& node, const glm::mat4& parent, - const glm::mat4& view, const Shader& shader, bool shadow); + const glm::mat4& view, const Shader& shader, bool shadow, + const WalkPose& pose, const ModelAnimConfig& cfg); + glm::mat4 pose_node(const ModelNode& node, const ModelAnimConfig& cfg, + const WalkPose& pose); void render_mesh(const Mesh& mesh, bool shadow); }; } // namespace Cubed \ No newline at end of file diff --git a/src/gameplay/client_entity_manager.cpp b/src/gameplay/client_entity_manager.cpp index e10df75..5543ff4 100644 --- a/src/gameplay/client_entity_manager.cpp +++ b/src/gameplay/client_entity_manager.cpp @@ -13,7 +13,19 @@ using namespace google::protobuf; namespace Cubed { ClientEntityManager::ClientEntityManager(ClientWorld& world) : m_world(world) {} -void ClientEntityManager::update() { handle_task(); } +void ClientEntityManager::update(float dt) { + handle_task(); + + auto view = m_registry.view(); + for (auto e : view) { + auto& c = view.get(e); + if (c.pose.gait == Gait::STOP) { + c.pose.walk_time = 0.0f; + } else { + c.pose.walk_time += dt; + } + } +} void ClientEntityManager::init() { m_factories.emplace("cubed:pig", [this](EntityID id) { @@ -53,7 +65,7 @@ void ClientEntityManager::handle_entity_update(UpdateInfo& info) { ASSERT(creature); creature->transform.position.value = info.pos; creature->transform.direction.value = info.direction; - + creature->pose.gait = info.gait; auto r = m_registry.try_get(e); ASSERT(r); r->direction.value = @@ -79,6 +91,7 @@ void ClientEntityManager::receive_entity_update(S2CEntityUpdate& msg) { e.id = msg.id(); e.pos = Tools::get_net_vec3(msg.pos()); e.direction = Tools::get_net_vec3(msg.direction()); + e.gait = get_gait_from_id(msg.gait()); m_tasks.emplace(Command::UPDATE, std::move(e)); } diff --git a/src/gameplay/client_world.cpp b/src/gameplay/client_world.cpp index 7f1b645..90a1c4b 100644 --- a/src/gameplay/client_world.cpp +++ b/src/gameplay/client_world.cpp @@ -743,7 +743,7 @@ void ClientWorld::send_chat_message(ChatMessage& message) { void ClientWorld::update(float delta_time) { m_player_manager.update(delta_time); - m_entity_manager.update(); + m_entity_manager.update(delta_time); { std::lock_guard lk(m_delete_vbo_mutex); m_pending_delete_vbo.clear(); diff --git a/src/gameplay/server_entity_manager.cpp b/src/gameplay/server_entity_manager.cpp index 604625b..1a8483e 100644 --- a/src/gameplay/server_entity_manager.cpp +++ b/src/gameplay/server_entity_manager.cpp @@ -3,6 +3,7 @@ #include "Cubed/gameplay/creatures/pig.hpp" #include "Cubed/gameplay/ecs/identity.hpp" #include "Cubed/gameplay/ecs/server_entity.hpp" +#include "Cubed/gameplay/gait.hpp" #include "Cubed/gameplay/hitbox_manager.hpp" #include "Cubed/gameplay/server_world.hpp" #include "Cubed/gameplay/session.hpp" @@ -11,6 +12,7 @@ #include "Cubed/gameplay/systems/wander_ai_system.hpp" #include "Cubed/tools/cubed_assert.hpp" #include "Cubed/tools/net_utils.hpp" + using namespace google::protobuf; namespace Cubed { @@ -88,6 +90,12 @@ void ServerEntityManager::update_send( Tools::set_net_vec3(p->mutable_direction(), creature.transform.direction.value); + const auto& v = creature.velocity.value; + if (v.x * v.x + v.z * v.z > 1e-4f) { + p->set_gait(get_gait_id(Gait::WALK)); + } else { + p->set_gait(get_gait_id(Gait::STOP)); + } for (auto& s : sessions) { s->send(make_packet(p)); diff --git a/src/proto/world/entity.proto b/src/proto/world/entity.proto index c7a1e7e..43a82e4 100644 --- a/src/proto/world/entity.proto +++ b/src/proto/world/entity.proto @@ -27,4 +27,5 @@ message S2CEntityUpdate { uint64 id = 1; Vec3 pos = 2; Vec3 direction = 3; + int32 gait = 4; } \ No newline at end of file diff --git a/src/render/model_manager.cpp b/src/render/model_manager.cpp index 27da052..f11931a 100644 --- a/src/render/model_manager.cpp +++ b/src/render/model_manager.cpp @@ -3,6 +3,12 @@ #include "Cubed/tools/cubed_assert.hpp" #include "Cubed/tools/log.hpp" #include "Cubed/tools/name_space.hpp" + +#include +#include +#include + +namespace fs = std::filesystem; namespace Cubed { ModelManager& ModelManager::instance() { @@ -80,6 +86,9 @@ ModelManager::Handle ModelManager::load_model(std::string_view model_name) { space[1]); } auto model = m_loader.load(path); + fs::path anim_path = path; + anim_path = anim_path.parent_path() / "animation.json"; + load_anim_config(model, anim_path); ModelMap::accessor acc; if (m_models.insert(acc, m_next++)) { acc->second = std::move(model); @@ -95,4 +104,50 @@ ModelManager::Handle ModelManager::load_model(std::string_view model_name) { return {acc->second, acc->first}; } +void ModelManager::load_anim_config(ModelNode& node, const std::string& path) { + if (!fs::is_regular_file(path)) { + return; + } + std::ifstream s(path); + if (!s.is_open()) { + return; + } + rapidjson::IStreamWrapper isw(s); + rapidjson::Document doc; + doc.ParseStream(isw); + if (doc.HasParseError()) { + Logger::warn("Can't parse anim config {}", path); + return; + } + ModelAnimConfig cfg; + if (doc.HasMember("walk")) { + cfg.walk_speed = doc["walk"]["speed"].GetFloat(); + cfg.walk_amp = doc["walk"]["amplitude"].GetFloat(); + } + if (doc.HasMember("run")) { + cfg.run_speed = doc["run"]["speed"].GetFloat(); + cfg.run_amp = doc["run"]["amplitude"].GetFloat(); + } + if (doc.HasMember("body_bob")) { + cfg.body_bob = doc["body_bob"].GetFloat(); + } + if (doc.HasMember("head")) { + NodeAnimRule r; + r.node = doc["head"]["node"].GetString(); + r.role = NodeAnimRule::Role::HEAD; + cfg.head_amp = doc["head"]["amplitude"].GetFloat(); + cfg.nodes.push_back(std::move(r)); + } + if (doc.HasMember("legs")) { + for (auto& leg : doc["legs"].GetArray()) { + NodeAnimRule r; + r.node = leg["node"].GetString(); + r.role = NodeAnimRule::Role::LEG; + r.phase = leg["phase"].GetFloat(); + cfg.nodes.push_back(std::move(r)); + } + } + node.anim = std::move(cfg); +} + } // namespace Cubed \ No newline at end of file diff --git a/src/render/model_renderer.cpp b/src/render/model_renderer.cpp index 2c56729..739d8d5 100644 --- a/src/render/model_renderer.cpp +++ b/src/render/model_renderer.cpp @@ -3,37 +3,64 @@ #include "Cubed/camera.hpp" #include "Cubed/render/model_manager.hpp" #include "Cubed/render/renderer.hpp" + +#include namespace Cubed { + +namespace { +float swing_angle(const WalkPose& pose, float speed, float amp_deg) { + if (pose.gait == Gait::STOP) { + return 0.0f; + } + return glm::sin(pose.walk_time * speed) * glm::radians(amp_deg); +} +} // namespace + ModelRender::ModelRender(Renderer& renderer) : m_renderer(renderer) {} void ModelRender::render_model(ModelID id, const glm::vec3& pos, float yaw, - Camera& camera) { + Camera& camera, const WalkPose& pose) { auto& root = ModelManager::model(id).node; - glm::mat4 transform = glm::translate(glm::mat4(1.0f), pos) * + glm::vec3 bob_pos = pos; + if (pose.gait != Gait::STOP && + root.anim.has_role(NodeAnimRule::Role::LEG)) { + bob_pos.y += std::abs(glm::sin(pose.walk_time * root.anim.walk_speed)) * + root.anim.body_bob; + } + + glm::mat4 transform = glm::translate(glm::mat4(1.0f), bob_pos) * glm::rotate(glm::mat4(1.0f), yaw, {0, 1, 0}); auto& shader = m_renderer.get_shader("model_shader"); glm::mat4 view = camera.get_camera_lookat(); shader.set_loc("proj_matrix", m_renderer.p_mat()); - render_node(root, transform, view, shader, false); + render_node(root, transform, view, shader, false, pose, root.anim); } void ModelRender::shadow_pass(ModelID id, const glm::vec3& pos, float yaw, - Camera& camera) { + Camera& camera, const WalkPose& pose) { auto& root = ModelManager::model(id).node; - glm::mat4 transform = glm::translate(glm::mat4(1.0f), pos) * + glm::vec3 bob_pos = pos; + if (pose.gait != Gait::STOP && + root.anim.has_role(NodeAnimRule::Role::LEG)) { + bob_pos.y += std::abs(glm::sin(pose.walk_time * root.anim.walk_speed)) * + root.anim.body_bob; + } + + glm::mat4 transform = glm::translate(glm::mat4(1.0f), bob_pos) * glm::rotate(glm::mat4(1.0f), yaw, {0, 1, 0}); auto& shader = m_renderer.get_shader("depth_model"); glm::mat4 view = camera.get_camera_lookat(); - render_node(root, transform, view, shader, true); + render_node(root, transform, view, shader, true, pose, root.anim); } void ModelRender::render_node(const ModelNode& node, const glm::mat4& parent, const glm::mat4& view, const Shader& shader, - bool shadow) { + bool shadow, const WalkPose& pose, + const ModelAnimConfig& cfg) { - glm::mat4 transform = parent * node.transform; + glm::mat4 transform = parent * pose_node(node, cfg, pose); if (shadow) { shader.set_loc("modelMatrix", transform); } else { @@ -48,7 +75,7 @@ void ModelRender::render_node(const ModelNode& node, const glm::mat4& parent, } for (auto& child : node.children) { - render_node(child, transform, view, shader, shadow); + render_node(child, transform, view, shader, shadow, pose, cfg); } } @@ -64,4 +91,27 @@ void ModelRender::render_mesh(const Mesh& mesh, bool) { GL_UNSIGNED_INT, 0); } +glm::mat4 ModelRender::pose_node(const ModelNode& node, + const ModelAnimConfig& cfg, + const WalkPose& pose) { + const auto* rule = cfg.rule_for(node.name); + if (!rule) { + return node.transform; + } + float angle = 0.0f; + if (rule->role == NodeAnimRule::Role::LEG) { + float speed = pose.gait == Gait::RUN ? cfg.run_speed : cfg.walk_speed; + float amp = pose.gait == Gait::RUN ? cfg.run_amp : cfg.walk_amp; + + angle = swing_angle(pose, speed, amp); + angle *= std::cos(rule->phase); + } else if (rule->role == NodeAnimRule::Role::HEAD) { + angle = glm::sin(pose.walk_time * cfg.walk_speed * 0.5f) * + glm::radians(cfg.head_amp); + } else { + return node.transform; + } + return glm::rotate(node.transform, angle, glm::vec3(1.0f, 0.0f, 0.0f)); +} + } // namespace Cubed \ No newline at end of file diff --git a/src/render/world_renderer.cpp b/src/render/world_renderer.cpp index 629c186..4dfe654 100644 --- a/src/render/world_renderer.cpp +++ b/src/render/world_renderer.cpp @@ -307,9 +307,9 @@ void WorldRenderer::shadow_entity(ClientWorld& world, auto& t = view.get(entity); float yaw = std::atan2(t.direction.value.x, t.direction.value.z); - m_renderer.model_renderer().shadow_pass(creature.model, - t.position.value, yaw, - world.world_scene().camera()); + m_renderer.model_renderer().shadow_pass( + creature.model, t.position.value, yaw, world.world_scene().camera(), + creature.pose); } m_player_renderer.render(shader, world, true); } @@ -734,9 +734,9 @@ void WorldRenderer::render_entity(ClientWorld& world) { } auto& t = view.get(entity); float yaw = std::atan2(t.direction.value.x, t.direction.value.z); - m_renderer.model_renderer().render_model(creature.model, - t.position.value, yaw, - world.world_scene().camera()); + m_renderer.model_renderer().render_model( + creature.model, t.position.value, yaw, world.world_scene().camera(), + creature.pose); } m_player_renderer.render(shader, world, false);