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.
This commit is contained in:
2026-08-06 10:57:49 +08:00
parent b2187bf0ad
commit d6051486a8
18 changed files with 249 additions and 42 deletions

View File

@@ -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<BaseClientCreature>();
for (auto e : view) {
auto& c = view.get<BaseClientCreature>(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<RenderTransform>(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));
}

View File

@@ -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();

View File

@@ -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));

View File

@@ -27,4 +27,5 @@ message S2CEntityUpdate {
uint64 id = 1;
Vec3 pos = 2;
Vec3 direction = 3;
int32 gait = 4;
}

View File

@@ -3,6 +3,12 @@
#include "Cubed/tools/cubed_assert.hpp"
#include "Cubed/tools/log.hpp"
#include "Cubed/tools/name_space.hpp"
#include <filesystem>
#include <rapidjson/document.h>
#include <rapidjson/istreamwrapper.h>
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

View File

@@ -3,37 +3,64 @@
#include "Cubed/camera.hpp"
#include "Cubed/render/model_manager.hpp"
#include "Cubed/render/renderer.hpp"
#include <cmath>
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

View File

@@ -307,9 +307,9 @@ void WorldRenderer::shadow_entity(ClientWorld& world,
auto& t = view.get<RenderTransform>(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<RenderTransform>(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);