diff --git a/CMakeLists.txt b/CMakeLists.txt index 43990e0..3529aa6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,8 @@ set(COMMON_SOURCES "src/collision/motionstate.hpp" "src/collision/trianglemesh.hpp" "src/collision/trianglemesh.cpp" + "src/game/character_anim_state.hpp" + "src/game/character_anim_state.cpp" "src/game/player_input.hpp" "src/game/skeletoninstance.hpp" "src/game/skeletoninstance.cpp" diff --git a/src/assets/skeleton.cpp b/src/assets/skeleton.cpp index f2c697b..ea8f316 100644 --- a/src/assets/skeleton.cpp +++ b/src/assets/skeleton.cpp @@ -54,14 +54,27 @@ int assets::Skeleton::GetBoneIndex(const std::string& name) const return -1; } +assets::AnimIdx assets::Skeleton::GetAnimationIdx(const std::string& name) const +{ + auto it = anim_idxs_.find(name); + if (it != anim_idxs_.end()) + { + return it->second; + } + return NO_ANIM; +} + +const assets::Animation* assets::Skeleton::GetAnimation(AnimIdx idx) const +{ + if (idx >= anims_.size()) + return nullptr; + + return anims_[idx].get(); +} + const assets::Animation* assets::Skeleton::GetAnimation(const std::string& name) const { - auto it = anims_.find(name); - if (it != anims_.end()) - { - return it->second.get(); - } - return nullptr; + return GetAnimation(GetAnimationIdx(name)); } void assets::Skeleton::AddBone(const std::string& name, const std::string& parent_name, const Transform& transform) @@ -75,4 +88,10 @@ void assets::Skeleton::AddBone(const std::string& name, const std::string& paren bone.inv_bind_matrix = glm::inverse(transform.ToMatrix()); bone_map_[bone.name] = index; -} \ No newline at end of file +} + +void assets::Skeleton::AddAnimation(const std::string& name, const std::shared_ptr& anim) +{ + anim_idxs_[name] = anims_.size(); + anims_.push_back(anim); +} diff --git a/src/assets/skeleton.hpp b/src/assets/skeleton.hpp index 6b9dc66..e4e3d4e 100644 --- a/src/assets/skeleton.hpp +++ b/src/assets/skeleton.hpp @@ -19,6 +19,9 @@ struct Bone glm::mat4 inv_bind_matrix; }; +using AnimIdx = uint8_t; +constexpr AnimIdx NO_ANIM = 255; + class Skeleton { public: @@ -30,17 +33,20 @@ public: size_t GetNumBones() const { return bones_.size(); } const Bone& GetBone(size_t idx) const { return bones_[idx]; } + AnimIdx GetAnimationIdx(const std::string& name) const; + const Animation* GetAnimation(AnimIdx idx) const; const Animation* GetAnimation(const std::string& name) const; private: void AddBone(const std::string& name, const std::string& parent_name, const Transform& transform); - void AddAnimation(const std::string& name, const std::shared_ptr& anim) { anims_[name] = anim; } + void AddAnimation(const std::string& name, const std::shared_ptr& anim); private: std::vector bones_; std::map bone_map_; - std::map> anims_; + std::vector> anims_; + std::map anim_idxs_; }; } // namespace assets \ No newline at end of file diff --git a/src/collision/dynamicsworld.cpp b/src/collision/dynamicsworld.cpp index bcdc0df..94e1684 100644 --- a/src/collision/dynamicsworld.cpp +++ b/src/collision/dynamicsworld.cpp @@ -8,18 +8,21 @@ collision::DynamicsWorld::DynamicsWorld(std::shared_ptr map) { bt_world_.setGravity(btVector3(0, 0, -9.81f)); + bt_broadphase_.getOverlappingPairCache()->setInternalGhostPairCallback(&bt_ghost_pair_cb_); + AddMapCollision(); - btTransform t; - t.setIdentity(); - t.setOrigin(btVector3(0,0,-12)); + // btTransform t; + // t.setIdentity(); + // t.setOrigin(btVector3(0,0,-12)); + // TODO: remove - static btDefaultMotionState motion(t); - static btBoxShape box(btVector3(100, 100, 2)); - btRigidBody::btRigidBodyConstructionInfo rbInfo(0.0f, &motion, &box, btVector3(0,0,0)); - static btRigidBody body(rbInfo); - bt_world_.addRigidBody(&body); + // static btDefaultMotionState motion(t); + // static btBoxShape box(btVector3(100, 100, 2)); + // btRigidBody::btRigidBodyConstructionInfo rbInfo(0.0f, &motion, &box, btVector3(0,0,0)); + // static btRigidBody body(rbInfo); + // bt_world_.addRigidBody(&body); } void collision::DynamicsWorld::AddMapCollision() @@ -36,11 +39,13 @@ void collision::DynamicsWorld::AddMapCollision() } // add static objects - - // for (const auto& sobjs = map_->GetStaticObjects(); const auto& sobj : sobjs) - // { - // AddModelInstance(*sobj.model, sobj.node.local); - // } + for (const auto& chunks = map_->GetChunks(); const auto& chunk : chunks) + { + for (const auto& obj : chunk.objs) + { + AddModelInstance(*obj.model, obj.node.local); + } + } } void collision::DynamicsWorld::AddModelInstance(const assets::Model& model, const Transform& trans) diff --git a/src/collision/dynamicsworld.hpp b/src/collision/dynamicsworld.hpp index a7396a9..9b5c8be 100644 --- a/src/collision/dynamicsworld.hpp +++ b/src/collision/dynamicsworld.hpp @@ -3,6 +3,7 @@ #include #include +#include #include "assets/map.hpp" @@ -44,6 +45,7 @@ private: btDefaultCollisionConfiguration bt_cfg_; btCollisionDispatcher bt_dispatcher_; + btGhostPairCallback bt_ghost_pair_cb_; btDbvtBroadphase bt_broadphase_; btSequentialImpulseConstraintSolver bt_solver_; btDiscreteDynamicsWorld bt_world_; diff --git a/src/game/character.cpp b/src/game/character.cpp index be380c8..50c92e5 100644 --- a/src/game/character.cpp +++ b/src/game/character.cpp @@ -1,6 +1,8 @@ #include "character.hpp" #include "world.hpp" #include "net/utils.hpp" +#include "assets/cache.hpp" +#include "utils/math.hpp" game::Character::Character(World& world, const CharacterInfo& info) : Super(world, net::ET_CHARACTER), shape_(info.shape), bt_shape_(shape_.radius, shape_.height), @@ -14,12 +16,14 @@ game::Character::Character(World& world, const CharacterInfo& info) btDynamicsWorld& bt_world = world_.GetBtWorld(); - static btGhostPairCallback ghostpaircb; - bt_world.getBroadphase()->getOverlappingPairCache()->setInternalGhostPairCallback(&ghostpaircb); - // bt_world.addCollisionObject(&bt_ghost_, btBroadphaseProxy::CharacterFilter, - // btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter); - bt_world.addCollisionObject(&bt_ghost_); + bt_world.addCollisionObject(&bt_ghost_, btBroadphaseProxy::CharacterFilter, + btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter); + // bt_world.addCollisionObject(&bt_ghost_); bt_world.addAction(&bt_character_); + + sk_ = SkeletonInstance(assets::CacheManager::GetSkeleton("data/human.sk"), &root_); + animstate_.idle_anim_idx = GetAnim("idle"); + animstate_.walk_anim_idx = GetAnim("walk"); } static bool Turn(float& angle, float target, float step) @@ -51,12 +55,21 @@ void game::Character::Update() root_.local.position.z -= shape_.height * 0.5f + shape_.radius - 0.05f; // foot pos UpdateMovement(); + + sync_current_ = 1 - sync_current_; + UpdateSyncState(); SendUpdateMsg(); } void game::Character::SendInitData(Player& player, net::OutMessage& msg) const { Super::SendInitData(player, msg); + + // write state against default + static const CharacterSyncState default_state; + size_t fields_pos = msg.Reserve(); + auto fields = WriteState(msg, default_state); + msg.WriteAt(fields_pos, fields); } void game::Character::SetInput(CharacterInputType type, bool enable) @@ -110,7 +123,7 @@ game::Character::~Character() void game::Character::UpdateMovement() { constexpr float dt = 1.0f / 25.0f; - + bool walking = false; glm::vec2 movedir(0.0f); if (in_ & (1 << CIN_FORWARD)) @@ -129,6 +142,7 @@ void game::Character::UpdateMovement() if (movedir.x != 0.0f || movedir.y != 0.0f) { + walking = true; float target_yaw = forward_yaw_ + std::atan2(movedir.y, movedir.x); Turn(yaw_, target_yaw, 4.0f * dt); @@ -142,16 +156,96 @@ void game::Character::UpdateMovement() { bt_character_.jump(btVector3(0.0f, 0.0f, 10.0f)); } + + // update anim + float run_blend_target = walking ? 0.5f : 0.0f; + MoveToward(animstate_.loco_blend, run_blend_target, dt * 2.0f); + float anim_speed = glm::mix(0.5f, 1.5f, UnMix(0.0f, 0.5f, animstate_.loco_blend)); + animstate_.loco_phase = glm::mod(animstate_.loco_phase + anim_speed * dt, 1.0f); +} + +void game::Character::UpdateSyncState() +{ + auto& state = sync_[sync_current_]; + + // transform + net::EncodePosition(root_.local.position, state.pos); + state.yaw.Encode(yaw_); + + // idle + state.idle_anim = animstate_.idle_anim_idx; + + // loco + state.walk_anim = animstate_.walk_anim_idx; + state.run_anim = animstate_.run_anim_idx; + state.loco_phase.Encode(animstate_.loco_phase); + state.loco_blend.Encode(animstate_.loco_blend); + + } void game::Character::SendUpdateMsg() { - net::PositionQ posq; - net::EncodePosition(root_.local.position, posq); - auto msg = BeginEntMsg(net::EMSG_UPDATE); - net::WritePositionQ(msg, posq); - msg.Write(yaw_); + auto fields_pos = msg.Reserve(); + auto fields = WriteState(msg, sync_[1 - sync_current_]); + + // TODO: allow this + // if (fields == 0) + // { + // DiscardMsg(); + // return; + // } + + msg.WriteAt(fields_pos, fields); +} + +game::CharacterSyncFieldFlags game::Character::WriteState(net::OutMessage& msg, const CharacterSyncState& base) const +{ + const auto& curr = sync_[sync_current_]; + + game::CharacterSyncFieldFlags fields = 0; + + // transform + if (curr.pos.x.value != base.pos.x.value || curr.pos.y.value != base.pos.y.value || + curr.pos.z.value != base.pos.z.value || curr.yaw.value != base.yaw.value) + { + fields |= CSF_TRANSFORM; + + net::WriteDelta(msg, curr.pos.x, base.pos.x); + net::WriteDelta(msg, curr.pos.y, base.pos.y); + net::WriteDelta(msg, curr.pos.z, base.pos.z); + + net::WriteDelta(msg, curr.yaw, base.yaw); + } + + // idle + if (curr.idle_anim != base.idle_anim) + { + fields |= CSF_IDLE_ANIM; + + msg.Write(curr.idle_anim); + } + + // loco anims + if (curr.walk_anim != base.walk_anim || curr.run_anim != base.run_anim) + { + fields |= CSF_LOCO_ANIMS; + + msg.Write(curr.walk_anim); + msg.Write(curr.run_anim); + } + + // loco vals + if (curr.loco_blend.value != base.loco_blend.value || curr.loco_phase.value != base.loco_phase.value) + { + fields |= CSF_LOCO_VALS; + + net::WriteDelta(msg, curr.loco_blend, base.loco_blend); + net::WriteDelta(msg, curr.loco_phase, base.loco_phase); + } + + return fields; } void game::Character::Move(glm::vec3& velocity, float t) @@ -188,3 +282,8 @@ void game::Character::Move(glm::vec3& velocity, float t) // velocity -= glm::dot(velocity, hit_normal) * hit_normal; // Adjust the velocity // } } + +assets::AnimIdx game::Character::GetAnim(const std::string& name) const +{ + return sk_.GetSkeleton()->GetAnimationIdx(name); +} diff --git a/src/game/character.hpp b/src/game/character.hpp index cbb264c..36c6134 100644 --- a/src/game/character.hpp +++ b/src/game/character.hpp @@ -3,6 +3,8 @@ #include "entity.hpp" #include "BulletCollision/CollisionDispatch/btGhostObject.h" #include "BulletDynamics/Character/btKinematicCharacterController.h" +#include "character_anim_state.hpp" +#include "character_sync.hpp" namespace game { @@ -52,10 +54,14 @@ public: private: void UpdateMovement(); + void UpdateSyncState(); void SendUpdateMsg(); + CharacterSyncFieldFlags WriteState(net::OutMessage& msg, const CharacterSyncState& base) const; void Move(glm::vec3& velocity, float t); + assets::AnimIdx GetAnim(const std::string& name) const; + private: CapsuleShape shape_; @@ -74,6 +80,11 @@ private: float walk_speed_ = 2.0f; + SkeletonInstance sk_; + CharacterAnimState animstate_; + + CharacterSyncState sync_[2]; + size_t sync_current_ = 0; }; } // namespace game \ No newline at end of file diff --git a/src/game/character_anim_state.cpp b/src/game/character_anim_state.cpp new file mode 100644 index 0000000..5227b26 --- /dev/null +++ b/src/game/character_anim_state.cpp @@ -0,0 +1,44 @@ +#include "character_anim_state.hpp" +#include "utils/math.hpp" + +void game::CharacterAnimState::ApplyToSkeleton(SkeletonInstance& sk) const +{ + const auto& skeleton = sk.GetSkeleton(); + const assets::Animation* idle_anim = skeleton->GetAnimation(idle_anim_idx); + const assets::Animation* walk_anim = skeleton->GetAnimation(walk_anim_idx); + const assets::Animation* run_anim = skeleton->GetAnimation(run_anim_idx); + + if (!idle_anim) + return; + + if (!walk_anim) + walk_anim = idle_anim; + + if (!run_anim) + run_anim = walk_anim; + + if (loco_blend == 0.0f) // idle + { + sk.ApplySkelAnim(*idle_anim, loco_phase, 1.0f); + } + else if (loco_blend < 0.5f) // idle-walk + { + sk.ApplySkelAnim(*idle_anim, loco_phase, 1.0f); + sk.ApplySkelAnim(*walk_anim, loco_phase, UnMix(0.0f, 0.5f, loco_blend)); + } + + else if (loco_blend == 0.5f) // walk + { + sk.ApplySkelAnim(*walk_anim, loco_phase, 1.0f); + } + else if (loco_blend < 1.0f) // walk-run + { + sk.ApplySkelAnim(*walk_anim, loco_phase, 1.0f); + sk.ApplySkelAnim(*run_anim, loco_phase, UnMix(0.5f, 1.0f, loco_blend)); + } + else // run + { + sk.ApplySkelAnim(*run_anim, loco_phase, 1.0f); + } + +} diff --git a/src/game/character_anim_state.hpp b/src/game/character_anim_state.hpp new file mode 100644 index 0000000..3e16e0b --- /dev/null +++ b/src/game/character_anim_state.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "skeletoninstance.hpp" + +namespace game +{ + +struct CharacterAnimState +{ + assets::AnimIdx idle_anim_idx = assets::NO_ANIM; + assets::AnimIdx walk_anim_idx = assets::NO_ANIM; + assets::AnimIdx run_anim_idx = assets::NO_ANIM; + + float loco_blend = 0.0f; + float loco_phase = 0.0f; + + void ApplyToSkeleton(SkeletonInstance& sk) const; + + +}; +} // namespace game diff --git a/src/game/character_sync.hpp b/src/game/character_sync.hpp new file mode 100644 index 0000000..d49f779 --- /dev/null +++ b/src/game/character_sync.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "net/defs.hpp" +#include "assets/skeleton.hpp" + +namespace game +{ + +struct CharacterSyncState +{ + // transform + net::PositionQ pos; + net::PositiveAngleQ yaw; + + // idle + assets::AnimIdx idle_anim = assets::NO_ANIM; + + // loco anim + assets::AnimIdx walk_anim = assets::NO_ANIM; + assets::AnimIdx run_anim = assets::NO_ANIM; + net::AnimBlendQ loco_blend; + net::AnimTimeQ loco_phase; + //assets::AnimIdx strafe_left_anim = assets::NO_ANIM; + //assets::AnimIdx strafe_right_anim = assets::NO_ANIM; + + // TODO: action +}; + +using CharacterSyncFieldFlags = uint8_t; + +enum CharacterSyncFieldFlag +{ + CSF_TRANSFORM = 0x01, + CSF_IDLE_ANIM = 0x02, + CSF_LOCO_ANIMS = 0x04, + CSF_LOCO_VALS = 0x08, +}; + +} // namespace game \ No newline at end of file diff --git a/src/gameview/characterview.cpp b/src/gameview/characterview.cpp index 3fc2f62..bac629b 100644 --- a/src/gameview/characterview.cpp +++ b/src/gameview/characterview.cpp @@ -2,9 +2,16 @@ #include "assets/cache.hpp" #include "assets/model.hpp" #include "net/utils.hpp" +#include "worldview.hpp" + game::view::CharacterView::CharacterView(WorldView& world, net::InMessage& msg) : EntityView(world, msg), ubo_(sk_) { + if (!ReadState(msg)) + throw EntityInitError(); + + states_[0] = states_[1]; // lerp from the read state to avoid jump + basemodel_ = assets::CacheManager::GetModel("data/human.mdl"); sk_ = SkeletonInstance(basemodel_->GetSkeleton(), &root_); ubo_.Update(); @@ -25,8 +32,17 @@ bool game::view::CharacterView::ProcessMsg(net::EntMsgType type, net::InMessage& void game::view::CharacterView::Update(const UpdateInfo& info) { - auto anim = sk_.GetSkeleton()->GetAnimation("walk"); - sk_.ApplySkelAnim(*anim, info.time, 1.0f); + // interpolate states + float tps = 25.0f; + float t = (info.time - update_time_) * tps * 0.8f; // assume some jitter, interpolate for longer + t = glm::clamp(t, 0.0f, 2.0f); + + root_.local = Transform::Lerp(states_[0].trans, states_[1].trans, t); + animstate_.loco_blend = glm::mix(states_[0].loco_blend, states_[1].loco_blend, t); + animstate_.loco_phase = glm::mod(glm::mix(states_[0].loco_phase, states_[1].loco_phase, t), 1.0f); + + animstate_.ApplyToSkeleton(sk_); + root_.UpdateMatrix(); sk_.UpdateBoneMatrices(); ubo_valid_ = false; @@ -45,11 +61,11 @@ void game::view::CharacterView::Draw(const DrawArgs& args) args.dlist.AddBeam(start, end, 0xFF007700, 0.05f); //// draw bones debug - //const auto& bone_nodes = sk_.GetBoneNodes(); - //for (const auto& bone_node : bone_nodes) + // const auto& bone_nodes = sk_.GetBoneNodes(); + // for (const auto& bone_node : bone_nodes) //{ - // if (!bone_node.parent) - // continue; + // if (!bone_node.parent) + // continue; // glm::vec3 p0 = bone_node.parent->matrix[3]; // glm::vec3 p1 = bone_node.matrix[3]; @@ -58,7 +74,7 @@ void game::view::CharacterView::Draw(const DrawArgs& args) //} // draw human - + if (!ubo_valid_) { ubo_.Update(); @@ -76,14 +92,67 @@ void game::view::CharacterView::Draw(const DrawArgs& args) } } -bool game::view::CharacterView::ProcessUpdateMsg(net::InMessage& msg) +bool game::view::CharacterView::ReadState(net::InMessage& msg) { - net::PositionQ posq; - if (!net::ReadPositionQ(msg, posq) || !msg.Read(yaw_)) + update_time_ = world_.GetTime(); + + // init lerp start state + states_[0].trans = root_.local; + states_[0].loco_blend = animstate_.loco_blend; + states_[0].loco_phase = animstate_.loco_phase; + + auto& new_state = states_[1]; + + // parse state delta + CharacterSyncFieldFlags fields; + if (!msg.Read(fields)) return false; - net::DecodePosition(posq, root_.local.position); - root_.local.rotation = glm::rotate(glm::quat(1.0f, 0.0f, 0.0f, 0.0f), yaw_ + glm::pi() * 0.5f, glm::vec3(0, 0, 1)); + // transform + if (fields & CSF_TRANSFORM) + { + if (!net::ReadDelta(msg, sync_.pos.x) || !net::ReadDelta(msg, sync_.pos.y) || + !net::ReadDelta(msg, sync_.pos.z) || !net::ReadDelta(msg, sync_.yaw)) + return false; + + net::DecodePosition(sync_.pos, new_state.trans.position); + new_state.trans.rotation = glm::rotate(glm::quat(1.0f, 0.0f, 0.0f, 0.0f), + sync_.yaw.Decode() + glm::pi() * 0.5f, glm::vec3(0, 0, 1)); + } + + if (fields & CSF_IDLE_ANIM) + { + if (!msg.Read(sync_.idle_anim)) + return false; + + animstate_.idle_anim_idx = sync_.idle_anim; + } + + if (fields & CSF_LOCO_ANIMS) + { + if (!msg.Read(sync_.walk_anim) || !msg.Read(sync_.run_anim)) + return false; + + animstate_.walk_anim_idx = sync_.walk_anim; + animstate_.run_anim_idx = sync_.run_anim; + } + + if (fields & CSF_LOCO_VALS) + { + if (!net::ReadDelta(msg, sync_.loco_blend) || !net::ReadDelta(msg, sync_.loco_phase)) + return false; + + new_state.loco_blend = sync_.loco_blend.Decode(); + new_state.loco_phase = sync_.loco_phase.Decode(); + + if (new_state.loco_phase < states_[0].loco_phase) + states_[0].loco_phase -= 1.0f; + } return true; } + +bool game::view::CharacterView::ProcessUpdateMsg(net::InMessage& msg) +{ + return ReadState(msg); +} diff --git a/src/gameview/characterview.hpp b/src/gameview/characterview.hpp index fd423e0..314ae61 100644 --- a/src/gameview/characterview.hpp +++ b/src/gameview/characterview.hpp @@ -4,10 +4,19 @@ #include "assets/model.hpp" #include "game/skeletoninstance.hpp" #include "skinning_ubo.hpp" +#include "game/character_anim_state.hpp" +#include "game/character_sync.hpp" namespace game::view { +struct CharacterViewState +{ + Transform trans; + float loco_blend = 0.0f; + float loco_phase = 0.0f; +}; + class CharacterView : public EntityView { public: @@ -21,6 +30,7 @@ public: virtual void Draw(const DrawArgs& args) override; private: + bool ReadState(net::InMessage& msg); bool ProcessUpdateMsg(net::InMessage& msg); private: @@ -31,6 +41,14 @@ private: SkinningUBO ubo_; bool ubo_valid_ = false; + CharacterAnimState animstate_; + + // sync + CharacterSyncState sync_; + CharacterViewState states_[2]; + float update_time_ = 0.0f; + + }; } diff --git a/src/net/defs.hpp b/src/net/defs.hpp index a3008d0..8324718 100644 --- a/src/net/defs.hpp +++ b/src/net/defs.hpp @@ -100,4 +100,7 @@ using ColorQ = Quantized; using NameTag = FixedStr<64>; +using AnimBlendQ = Quantized; +using AnimTimeQ = Quantized; + } // namespace net \ No newline at end of file diff --git a/src/utils/math.hpp b/src/utils/math.hpp new file mode 100644 index 0000000..3d627f4 --- /dev/null +++ b/src/utils/math.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include + +inline void MoveToward(float& val, float target, float max_delta) +{ + if (val == target) + return; + + if (val < target) + { + val += max_delta; + if (val > target) + val = target; + } + else + { + val -= max_delta; + if (val < target) + val = target; + } +} + +inline float UnMix(float a, float b, float x) +{ + return (x - a) / (b - a); +}