mirror of
https://github.com/zhenyan121/Cubed.git
synced 2026-08-08 09:47:01 +08:00
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:
33
assets/model/creature/pig/animation.json
Normal file
33
assets/model/creature/pig/animation.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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<EntityID, entt::entity>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "Cubed/gameplay/player.hpp"
|
||||
#include "Cubed/gameplay/gait.hpp"
|
||||
namespace Cubed {
|
||||
|
||||
struct WalkPose {
|
||||
|
||||
21
include/Cubed/gameplay/gait.hpp
Normal file
21
include/Cubed/gameplay/gait.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
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
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
#pragma once
|
||||
#include "glm/ext/vector_float3.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
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
|
||||
|
||||
@@ -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 <absl/container/flat_hash_set.h>
|
||||
#include <atomic>
|
||||
|
||||
@@ -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
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <glm/glm.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
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<NodeAnimRule> 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<Mesh> meshes;
|
||||
std::vector<ModelNode> children;
|
||||
ModelAnimConfig anim;
|
||||
};
|
||||
|
||||
} // namespace Cubed
|
||||
@@ -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
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -27,4 +27,5 @@ message S2CEntityUpdate {
|
||||
uint64 id = 1;
|
||||
Vec3 pos = 2;
|
||||
Vec3 direction = 3;
|
||||
int32 gait = 4;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user