From bbf2788627d879593013fe9702b7fd4a8024f8bf Mon Sep 17 00:00:00 2001 From: tovjemam Date: Sat, 14 Feb 2026 19:57:03 +0100 Subject: [PATCH] Attachable entities, no physics characters, basic vehicle drivers --- src/game/character.cpp | 108 +++++++++++++++++++++++---------- src/game/character.hpp | 41 ++++++++++--- src/game/entity.cpp | 54 +++++++++++++++-- src/game/entity.hpp | 20 ++++-- src/game/openworld.cpp | 26 ++++++-- src/game/openworld.hpp | 4 +- src/game/player.cpp | 4 +- src/game/transform_node.hpp | 5 ++ src/game/vehicle.cpp | 2 + src/game/world.cpp | 2 +- src/gameview/characterview.cpp | 2 + src/gameview/entityview.cpp | 35 ++++++++++- src/gameview/entityview.hpp | 14 ++++- src/gameview/worldview.cpp | 2 +- src/net/defs.hpp | 1 + 15 files changed, 256 insertions(+), 64 deletions(-) diff --git a/src/game/character.cpp b/src/game/character.cpp index e5b129b..1448af1 100644 --- a/src/game/character.cpp +++ b/src/game/character.cpp @@ -1,25 +1,13 @@ #include "character.hpp" -#include "world.hpp" -#include "net/utils.hpp" #include "assets/cache.hpp" +#include "net/utils.hpp" #include "utils/math.hpp" +#include "world.hpp" game::Character::Character(World& world, const CharacterInfo& info) - : Super(world, net::ET_CHARACTER), shape_(info.shape), bt_shape_(shape_.radius, shape_.height), - bt_character_(&bt_ghost_, &bt_shape_, 0.3f, btVector3(0, 0, 1)) + : Super(world, net::ET_CHARACTER), shape_(info.shape), bt_shape_(shape_.radius, shape_.height) { - btTransform start_transform; - start_transform.setIdentity(); - bt_ghost_.setWorldTransform(start_transform); - bt_ghost_.setCollisionShape(&bt_shape_); - bt_ghost_.setCollisionFlags(btCollisionObject::CF_CHARACTER_OBJECT); - - - btDynamicsWorld& bt_world = world_.GetBtWorld(); - bt_world.addCollisionObject(&bt_ghost_, btBroadphaseProxy::CharacterFilter, - btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter); - // bt_world.addCollisionObject(&bt_ghost_); - bt_world.addAction(&bt_character_); + z_offset_ = shape_.height * 0.5f + shape_.radius - 0.05f; sk_ = SkeletonInstance(assets::CacheManager::GetSkeleton("data/human.sk"), &root_); animstate_.idle_anim_idx = GetAnim("idle"); @@ -50,9 +38,8 @@ void game::Character::Update() { Super::Update(); - auto bt_trans = bt_ghost_.getWorldTransform(); - root_.local.SetBtTransform(bt_trans); - root_.local.position.z -= shape_.height * 0.5f + shape_.radius - 0.05f; // foot pos + SyncTransformFromController(); + root_.UpdateMatrix(); UpdateMovement(); @@ -80,6 +67,19 @@ void game::Character::SendInitData(Player& player, net::OutMessage& msg) const msg.WriteAt(fields_pos, fields); } +void game::Character::EnablePhysics(bool enable) +{ + if (enable && !controller_) + { + controller_ = std::make_unique(world_.GetBtWorld(), bt_shape_); + SyncControllerTransform(); + } + else if (!enable && controller_) + { + controller_.reset(); + } +} + void game::Character::SetInput(CharacterInputType type, bool enable) { if (enable) @@ -116,9 +116,8 @@ void game::Character::SetInput(CharacterInputType type, bool enable) void game::Character::SetPosition(const glm::vec3& position) { - auto trans = bt_ghost_.getWorldTransform(); - trans.setOrigin(btVector3(position.x, position.y, position.z)); - bt_ghost_.setWorldTransform(trans); + root_.local.position = position; + SyncControllerTransform(); } void game::Character::AddClothes(std::string name, const glm::vec3& color) @@ -126,11 +125,31 @@ void game::Character::AddClothes(std::string name, const glm::vec3& color) clothes_.emplace_back(std::move(name), color); } -game::Character::~Character() +void game::Character::SetMainAnim(const std::string& anim_name) { - btDynamicsWorld& bt_world = world_.GetBtWorld(); - bt_world.removeAction(&bt_character_); - bt_world.removeCollisionObject(&bt_ghost_); + animstate_.idle_anim_idx = GetAnim(anim_name); +} + +void game::Character::SyncControllerTransform() +{ + if (!controller_) + return; + + auto& position = root_.local.position; + auto& bt_ghost = controller_->GetBtGhost(); + auto trans = bt_ghost.getWorldTransform(); + trans.setOrigin(btVector3(position.x, position.y, position.z + z_offset_)); + bt_ghost.setWorldTransform(trans); +} + +void game::Character::SyncTransformFromController() +{ + if (!controller_) + return; + + auto bt_trans = controller_->GetBtGhost().getWorldTransform(); + root_.local.SetBtTransform(bt_trans); + root_.local.position.z -= z_offset_; // foot pos } void game::Character::UpdateMovement() @@ -163,11 +182,15 @@ void game::Character::UpdateMovement() walkdir = forward_dir * walk_speed_ * dt; } - bt_character_.setWalkDirection(btVector3(walkdir.x, walkdir.y, walkdir.z)); - - if (in_ & (1 << CIN_JUMP) && bt_character_.canJump()) + if (controller_) { - bt_character_.jump(btVector3(0.0f, 0.0f, 10.0f)); + auto& bt_character = controller_->GetBtController(); + bt_character.setWalkDirection(btVector3(walkdir.x, walkdir.y, walkdir.z)); + + if (in_ & (1 << CIN_JUMP) && bt_character.canJump()) + { + bt_character.jump(btVector3(0.0f, 0.0f, 10.0f)); + } } // update anim @@ -193,12 +216,10 @@ void game::Character::UpdateSyncState() 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() -{ +{ auto msg = BeginEntMsg(net::EMSG_UPDATE); auto fields_pos = msg.Reserve(); auto fields = WriteState(msg, sync_[1 - sync_current_]); @@ -300,3 +321,24 @@ assets::AnimIdx game::Character::GetAnim(const std::string& name) const { return sk_.GetSkeleton()->GetAnimationIdx(name); } + +game::CharacterPhysicsController::CharacterPhysicsController(btDynamicsWorld& bt_world, btCapsuleShapeZ& bt_shape) + : bt_world_(bt_world), bt_character_(&bt_ghost_, &bt_shape, 0.3f, btVector3(0, 0, 1)) +{ + btTransform start_transform; + start_transform.setIdentity(); + bt_ghost_.setWorldTransform(start_transform); + bt_ghost_.setCollisionShape(&bt_shape); + bt_ghost_.setCollisionFlags(btCollisionObject::CF_CHARACTER_OBJECT); + + bt_world_.addCollisionObject(&bt_ghost_, btBroadphaseProxy::CharacterFilter, + btBroadphaseProxy::StaticFilter | btBroadphaseProxy::DefaultFilter); + // bt_world.addCollisionObject(&bt_ghost_); + bt_world_.addAction(&bt_character_); +} + +game::CharacterPhysicsController::~CharacterPhysicsController() +{ + bt_world_.removeAction(&bt_character_); + bt_world_.removeCollisionObject(&bt_ghost_); +} diff --git a/src/game/character.hpp b/src/game/character.hpp index 3fd2e08..4544809 100644 --- a/src/game/character.hpp +++ b/src/game/character.hpp @@ -1,10 +1,11 @@ #pragma once -#include "entity.hpp" -#include "BulletCollision/CollisionDispatch/btGhostObject.h" -#include "BulletDynamics/Character/btKinematicCharacterController.h" +#include +#include +#include #include "character_anim_state.hpp" #include "character_sync.hpp" +#include "entity.hpp" namespace game { @@ -39,6 +40,25 @@ struct CharacterClothes glm::vec3 color; }; +class CharacterPhysicsController +{ +public: + CharacterPhysicsController(btDynamicsWorld& bt_world, btCapsuleShapeZ& bt_shape); + DELETE_COPY_MOVE(CharacterPhysicsController) + + btKinematicCharacterController& GetBtController() { return bt_character_; } + const btKinematicCharacterController& GetBtController() const { return bt_character_; } + btGhostObject& GetBtGhost() { return bt_ghost_; } + const btGhostObject& GetBtGhost() const { return bt_ghost_; } + + ~CharacterPhysicsController(); + +private: + btDynamicsWorld& bt_world_; + btPairCachingGhostObject bt_ghost_; + btKinematicCharacterController bt_character_; +}; + class Character : public Entity { public: @@ -49,18 +69,26 @@ public: virtual void Update() override; virtual void SendInitData(Player& player, net::OutMessage& msg) const override; + void EnablePhysics(bool enable); + void SetInput(CharacterInputType type, bool enable); void SetInputs(CharacterInputFlags inputs) { in_ = inputs; } void SetForwardYaw(float yaw) { forward_yaw_ = yaw; } + void SetYaw(float yaw) { yaw_ = yaw; } void SetPosition(const glm::vec3& position); void AddClothes(std::string name, const glm::vec3& color); - ~Character() override; + void SetMainAnim(const std::string& anim_name); + + ~Character() override = default; private: + void SyncControllerTransform(); + void SyncTransformFromController(); + void UpdateMovement(); void UpdateSyncState(); void SendUpdateMsg(); @@ -79,9 +107,8 @@ private: CharacterInputFlags in_ = 0; btCapsuleShapeZ bt_shape_; - btPairCachingGhostObject bt_ghost_; - - btKinematicCharacterController bt_character_; + float z_offset_ = 0.0f; // offset of controller from root + std::unique_ptr controller_; float yaw_ = 0.0f; float forward_yaw_ = 0.0f; diff --git a/src/game/entity.cpp b/src/game/entity.cpp index 7d12c58..618d5be 100644 --- a/src/game/entity.cpp +++ b/src/game/entity.cpp @@ -4,15 +4,40 @@ game::Entity::Entity(World& world, net::EntType viewtype) : Scheduler(world.GetTime()), world_(world), entnum_(world.GetNewEntnum()), viewtype_(viewtype) {} -void game::Entity::Update() -{ - ResetMsg(); - RunTasks(); -} - void game::Entity::SendInitData(Player& player, net::OutMessage& msg) const { WriteNametag(msg); + WriteAttach(msg); +} + +void game::Entity::Update() +{ + ResetMsg(); + + upd_time_ = world_.GetTime(); + + // ensure parent is updated + parent_ = nullptr; + if (parentnum_) + { + parent_ = world_.GetEntity(parentnum_); + parent_->TryUpdate(); + } + + // update transform parent + root_.parent = parent_ ? &parent_->GetRoot() : nullptr; + + RunTasks(); +} + +bool game::Entity::TryUpdate() +{ + int64_t time = world_.GetTime(); + if (time == upd_time_) + return false; + + Update(); + return true; } void game::Entity::SetNametag(const std::string& nametag) @@ -21,6 +46,12 @@ void game::Entity::SetNametag(const std::string& nametag) SendNametagMsg(); // notify viewers } +void game::Entity::Attach(net::EntNum parentnum) +{ + parentnum_ = parentnum; + SendAttachMsg(); +} + void game::Entity::WriteNametag(net::OutMessage& msg) const { msg.Write(net::NameTag{nametag_}); @@ -32,6 +63,17 @@ void game::Entity::SendNametagMsg() WriteNametag(msg); } +void game::Entity::WriteAttach(net::OutMessage& msg) const +{ + msg.Write(parentnum_); +} + +void game::Entity::SendAttachMsg() +{ + auto msg = BeginEntMsg(net::EMSG_ATTACH); + WriteAttach(msg); +} + net::OutMessage game::Entity::BeginEntMsg(net::EntMsgType type) { auto msg = BeginMsg(net::MSG_ENTMSG); diff --git a/src/game/entity.hpp b/src/game/entity.hpp index d0fd3ae..3ea5df6 100644 --- a/src/game/entity.hpp +++ b/src/game/entity.hpp @@ -22,14 +22,21 @@ public: net::EntNum GetEntNum() const { return entnum_; } net::EntType GetViewType() const { return viewtype_; } - virtual void Update(); virtual void SendInitData(Player& player, net::OutMessage& msg) const; + virtual void Update(); + bool TryUpdate(); // if not already updated + int64_t GetUpdateTime() const { return upd_time_; } + void SetNametag(const std::string& nametag); + void Attach(net::EntNum parentnum); + net::EntNum GetParentNum() const { return parentnum_; } + void Remove() { removed_ = true; } bool IsRemoved() const { return removed_; } + const TransformNode& GetRoot() const { return root_; } const Transform& GetRootTransform() const { return root_.local; } float GetMaxDistance() const { return max_distance_; } @@ -37,9 +44,11 @@ public: private: void WriteNametag(net::OutMessage& msg) const; - void SendNametagMsg(); + void WriteAttach(net::OutMessage& msg) const; + void SendAttachMsg(); + protected: net::OutMessage BeginEntMsg(net::EntMsgType type); @@ -49,13 +58,16 @@ protected: const net::EntType viewtype_; TransformNode root_; + Entity* parent_ = nullptr; float max_distance_ = 700.0f; - std::string nametag_; - bool removed_ = false; +private: + int64_t upd_time_ = -1; + std::string nametag_; + net::EntNum parentnum_ = 0; }; } \ No newline at end of file diff --git a/src/game/openworld.cpp b/src/game/openworld.cpp index 93efc05..e143366 100644 --- a/src/game/openworld.cpp +++ b/src/game/openworld.cpp @@ -247,19 +247,28 @@ static glm::vec3 GetRandomColor() return color; } -void game::OpenWorld::SpawnCharacter(Player& player) +game::Character& game::OpenWorld::SpawnRandomCharacter() { - RemoveCharacter(player); - CharacterInfo cinfo; auto& character = Spawn(cinfo); - character.SetNametag("player (" + std::to_string(character.GetEntNum()) + ")"); - character.SetPosition({ 100.0f, 100.0f, 5.0f }); // add clothes character.AddClothes("tshirt", GetRandomColor()); character.AddClothes("shorts", GetRandomColor()); + return character; +} + + +void game::OpenWorld::SpawnCharacter(Player& player) +{ + RemoveCharacter(player); + + auto& character = SpawnRandomCharacter(); + character.SetNametag("player (" + std::to_string(character.GetEntNum()) + ")"); + character.SetPosition({ 100.0f, 100.0f, 5.0f }); + character.EnablePhysics(true); + player.SetCamera(character.GetEntNum()); player_characters_[&player] = &character; @@ -572,6 +581,13 @@ void game::OpenWorld::SpawnBot() vehicle.Schedule(rand() % 500, [think_state]() { //BotNametagThink(think_state); } ); + + // spawn driver + auto& driver = SpawnRandomCharacter(); + driver.Attach(vehicle.GetEntNum()); + driver.SetPosition(glm::vec3(0.0f, 0.0f, 0.0f)); + driver.SetMainAnim("vehicle_drive"); + driver.SetYaw(0.5f * glm::pi()); } void game::OpenWorld::SpawnVehicle(Player& player) diff --git a/src/game/openworld.hpp b/src/game/openworld.hpp index 04f0b33..4d79f7c 100644 --- a/src/game/openworld.hpp +++ b/src/game/openworld.hpp @@ -22,7 +22,9 @@ public: private: void SpawnVehicle(Player& player); void RemoveVehicle(Player& player); - + + Character& SpawnRandomCharacter(); + void SpawnCharacter(Player& player); void RemoveCharacter(Player& player); diff --git a/src/game/player.cpp b/src/game/player.cpp index 943cd05..03c24c2 100644 --- a/src/game/player.cpp +++ b/src/game/player.cpp @@ -43,7 +43,7 @@ void game::Player::Update() auto cam_ent = world_->GetEntity(cam_ent_); if (cam_ent) { - cull_pos_ = cam_ent->GetRootTransform().position; + cull_pos_ = cam_ent->GetRoot().GetGlobalPosition(); } } @@ -143,7 +143,7 @@ bool game::Player::ShouldSeeEntity(const Entity& entity) const { // max distance check float max_dist = entity.GetMaxDistance(); - if (glm::distance2(entity.GetRootTransform().position, cull_pos_) > (max_dist * max_dist)) + if (glm::distance2(entity.GetRoot().GetGlobalPosition(), cull_pos_) > (max_dist * max_dist)) return false; // TODO: custom callback diff --git a/src/game/transform_node.hpp b/src/game/transform_node.hpp index ca04019..35aaea3 100644 --- a/src/game/transform_node.hpp +++ b/src/game/transform_node.hpp @@ -24,6 +24,11 @@ namespace game matrix = parent->matrix * matrix; } } + + glm::vec3 GetGlobalPosition() const + { + return matrix[3]; + } }; } \ No newline at end of file diff --git a/src/game/vehicle.cpp b/src/game/vehicle.cpp index 6e282cb..aafa4db 100644 --- a/src/game/vehicle.cpp +++ b/src/game/vehicle.cpp @@ -97,6 +97,8 @@ void game::Vehicle::Update() { Super::Update(); + root_.UpdateMatrix(); + flags_ = 0; ProcessInput(); UpdateWheels(); diff --git a/src/game/world.cpp b/src/game/world.cpp index dc4560d..1efa86a 100644 --- a/src/game/world.cpp +++ b/src/game/world.cpp @@ -41,7 +41,7 @@ void game::World::Update(int64_t delta_time) // update entities for (auto it = ents_.begin(); it != ents_.end();) { - it->second->Update(); + it->second->TryUpdate(); if (it->second->IsRemoved()) it = ents_.erase(it); diff --git a/src/gameview/characterview.cpp b/src/gameview/characterview.cpp index a73d569..1419554 100644 --- a/src/gameview/characterview.cpp +++ b/src/gameview/characterview.cpp @@ -53,6 +53,8 @@ bool game::view::CharacterView::ProcessMsg(net::EntMsgType type, net::InMessage& void game::view::CharacterView::Update(const UpdateInfo& info) { + Super::Update(info); + // interpolate states float tps = 25.0f; float t = (info.time - update_time_) * tps * 0.8f; // assume some jitter, interpolate for longer diff --git a/src/gameview/entityview.cpp b/src/gameview/entityview.cpp index c44e8a7..1af7fc0 100644 --- a/src/gameview/entityview.cpp +++ b/src/gameview/entityview.cpp @@ -10,7 +10,8 @@ game::view::EntityView::EntityView(WorldView& world, net::InMessage& msg) : audioplayer_(world_.GetAudioMaster()), nametag_text_(assets::CacheManager::GetFont("data/comic32.font"), 0xFFFFFFFF) { - if (!ReadNametag(msg)) + // read nametag and attachment info + if (!ReadNametag(msg) || !ReadAttach(msg)) throw EntityInitError(); } @@ -20,13 +21,38 @@ bool game::view::EntityView::ProcessMsg(net::EntMsgType type, net::InMessage& ms { case net::EMSG_NAMETAG: return ReadNametag(msg); + case net::EMSG_ATTACH: + return ReadAttach(msg); + default: + return false; } +} - return false; +bool game::view::EntityView::TryUpdate(const UpdateInfo& info) +{ + float time = world_.GetTime(); + if (time == upd_time_) + return false; + + Update(info); + return true; } void game::view::EntityView::Update(const UpdateInfo& info) { + upd_time_ = world_.GetTime(); + + // ensure parent is updated + parent_ = nullptr; + if (parentnum_) + { + parent_ = world_.GetEntity(parentnum_); + parent_->TryUpdate(info); + } + + // update transform parent + root_.parent = parent_ ? &parent_->GetRoot() : nullptr; + audioplayer_.Update(); } @@ -49,6 +75,11 @@ bool game::view::EntityView::ReadNametag(net::InMessage& msg) return true; } +bool game::view::EntityView::ReadAttach(net::InMessage& msg) +{ + return msg.Read(parentnum_); +} + void game::view::EntityView::DrawNametag(const DrawArgs& args) { if (nametag_.empty()) diff --git a/src/gameview/entityview.hpp b/src/gameview/entityview.hpp index ad4d304..b735ac8 100644 --- a/src/gameview/entityview.hpp +++ b/src/gameview/entityview.hpp @@ -36,17 +36,20 @@ public: DELETE_COPY_MOVE(EntityView) virtual bool ProcessMsg(net::EntMsgType type, net::InMessage& msg); + + bool TryUpdate(const UpdateInfo& info); // if not updated already virtual void Update(const UpdateInfo& info); + virtual void Draw(const DrawArgs& args); - Sphere GetBoundingSphere() const { return Sphere{root_.local.position, radius_}; } - + Sphere GetBoundingSphere() const { return Sphere{root_.GetGlobalPosition(), radius_}; } const TransformNode& GetRoot() const { return root_; } virtual ~EntityView() = default; private: bool ReadNametag(net::InMessage& msg); + bool ReadAttach(net::InMessage& msg); void DrawNametag(const DrawArgs& args); void DrawAxes(const DrawArgs& args); @@ -55,13 +58,20 @@ protected: WorldView& world_; TransformNode root_; + + EntityView* parent_ = nullptr; + float radius_ = 1.0f; audio::Player audioplayer_; +private: std::string nametag_; gfx::Text nametag_text_; gfx::HudPosition nametag_pos_; + + net::EntNum parentnum_ = 0; + float upd_time_ = 0.0f; }; } // namespace game::view \ No newline at end of file diff --git a/src/gameview/worldview.cpp b/src/gameview/worldview.cpp index 7e997ad..8699eee 100644 --- a/src/gameview/worldview.cpp +++ b/src/gameview/worldview.cpp @@ -38,7 +38,7 @@ void game::view::WorldView::Update(const UpdateInfo& info) for (const auto& [entnum, ent] : ents_) { - ent->Update(info); + ent->TryUpdate(info); } } diff --git a/src/net/defs.hpp b/src/net/defs.hpp index 02176b8..1b114ab 100644 --- a/src/net/defs.hpp +++ b/src/net/defs.hpp @@ -75,6 +75,7 @@ enum EntMsgType : uint8_t EMSG_NONE, EMSG_NAMETAG, + EMSG_ATTACH, EMSG_UPDATE, };