Attachable entities, no physics characters, basic vehicle drivers

This commit is contained in:
tovjemam 2026-02-14 19:57:03 +01:00
parent fd3f981ec0
commit bbf2788627
15 changed files with 256 additions and 64 deletions

View File

@ -1,25 +1,13 @@
#include "character.hpp" #include "character.hpp"
#include "world.hpp"
#include "net/utils.hpp"
#include "assets/cache.hpp" #include "assets/cache.hpp"
#include "net/utils.hpp"
#include "utils/math.hpp" #include "utils/math.hpp"
#include "world.hpp"
game::Character::Character(World& world, const CharacterInfo& info) game::Character::Character(World& world, const CharacterInfo& info)
: Super(world, net::ET_CHARACTER), shape_(info.shape), bt_shape_(shape_.radius, shape_.height), : 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))
{ {
btTransform start_transform; z_offset_ = shape_.height * 0.5f + shape_.radius - 0.05f;
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_);
sk_ = SkeletonInstance(assets::CacheManager::GetSkeleton("data/human.sk"), &root_); sk_ = SkeletonInstance(assets::CacheManager::GetSkeleton("data/human.sk"), &root_);
animstate_.idle_anim_idx = GetAnim("idle"); animstate_.idle_anim_idx = GetAnim("idle");
@ -50,9 +38,8 @@ void game::Character::Update()
{ {
Super::Update(); Super::Update();
auto bt_trans = bt_ghost_.getWorldTransform(); SyncTransformFromController();
root_.local.SetBtTransform(bt_trans); root_.UpdateMatrix();
root_.local.position.z -= shape_.height * 0.5f + shape_.radius - 0.05f; // foot pos
UpdateMovement(); UpdateMovement();
@ -80,6 +67,19 @@ void game::Character::SendInitData(Player& player, net::OutMessage& msg) const
msg.WriteAt(fields_pos, fields); msg.WriteAt(fields_pos, fields);
} }
void game::Character::EnablePhysics(bool enable)
{
if (enable && !controller_)
{
controller_ = std::make_unique<CharacterPhysicsController>(world_.GetBtWorld(), bt_shape_);
SyncControllerTransform();
}
else if (!enable && controller_)
{
controller_.reset();
}
}
void game::Character::SetInput(CharacterInputType type, bool enable) void game::Character::SetInput(CharacterInputType type, bool enable)
{ {
if (enable) if (enable)
@ -116,9 +116,8 @@ void game::Character::SetInput(CharacterInputType type, bool enable)
void game::Character::SetPosition(const glm::vec3& position) void game::Character::SetPosition(const glm::vec3& position)
{ {
auto trans = bt_ghost_.getWorldTransform(); root_.local.position = position;
trans.setOrigin(btVector3(position.x, position.y, position.z)); SyncControllerTransform();
bt_ghost_.setWorldTransform(trans);
} }
void game::Character::AddClothes(std::string name, const glm::vec3& color) 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); clothes_.emplace_back(std::move(name), color);
} }
game::Character::~Character() void game::Character::SetMainAnim(const std::string& anim_name)
{ {
btDynamicsWorld& bt_world = world_.GetBtWorld(); animstate_.idle_anim_idx = GetAnim(anim_name);
bt_world.removeAction(&bt_character_); }
bt_world.removeCollisionObject(&bt_ghost_);
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() void game::Character::UpdateMovement()
@ -163,11 +182,15 @@ void game::Character::UpdateMovement()
walkdir = forward_dir * walk_speed_ * dt; walkdir = forward_dir * walk_speed_ * dt;
} }
bt_character_.setWalkDirection(btVector3(walkdir.x, walkdir.y, walkdir.z)); if (controller_)
if (in_ & (1 << CIN_JUMP) && bt_character_.canJump())
{ {
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 // update anim
@ -193,8 +216,6 @@ void game::Character::UpdateSyncState()
state.run_anim = animstate_.run_anim_idx; state.run_anim = animstate_.run_anim_idx;
state.loco_phase.Encode(animstate_.loco_phase); state.loco_phase.Encode(animstate_.loco_phase);
state.loco_blend.Encode(animstate_.loco_blend); state.loco_blend.Encode(animstate_.loco_blend);
} }
void game::Character::SendUpdateMsg() void game::Character::SendUpdateMsg()
@ -300,3 +321,24 @@ assets::AnimIdx game::Character::GetAnim(const std::string& name) const
{ {
return sk_.GetSkeleton()->GetAnimationIdx(name); 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_);
}

View File

@ -1,10 +1,11 @@
#pragma once #pragma once
#include "entity.hpp" #include <btBulletDynamicsCommon.h>
#include "BulletCollision/CollisionDispatch/btGhostObject.h" #include <BulletCollision/CollisionDispatch/btGhostObject.h>
#include "BulletDynamics/Character/btKinematicCharacterController.h" #include <BulletDynamics/Character/btKinematicCharacterController.h>
#include "character_anim_state.hpp" #include "character_anim_state.hpp"
#include "character_sync.hpp" #include "character_sync.hpp"
#include "entity.hpp"
namespace game namespace game
{ {
@ -39,6 +40,25 @@ struct CharacterClothes
glm::vec3 color; 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 class Character : public Entity
{ {
public: public:
@ -49,18 +69,26 @@ public:
virtual void Update() override; virtual void Update() override;
virtual void SendInitData(Player& player, net::OutMessage& msg) const override; virtual void SendInitData(Player& player, net::OutMessage& msg) const override;
void EnablePhysics(bool enable);
void SetInput(CharacterInputType type, bool enable); void SetInput(CharacterInputType type, bool enable);
void SetInputs(CharacterInputFlags inputs) { in_ = inputs; } void SetInputs(CharacterInputFlags inputs) { in_ = inputs; }
void SetForwardYaw(float yaw) { forward_yaw_ = yaw; } void SetForwardYaw(float yaw) { forward_yaw_ = yaw; }
void SetYaw(float yaw) { yaw_ = yaw; }
void SetPosition(const glm::vec3& position); void SetPosition(const glm::vec3& position);
void AddClothes(std::string name, const glm::vec3& color); void AddClothes(std::string name, const glm::vec3& color);
~Character() override; void SetMainAnim(const std::string& anim_name);
~Character() override = default;
private: private:
void SyncControllerTransform();
void SyncTransformFromController();
void UpdateMovement(); void UpdateMovement();
void UpdateSyncState(); void UpdateSyncState();
void SendUpdateMsg(); void SendUpdateMsg();
@ -79,9 +107,8 @@ private:
CharacterInputFlags in_ = 0; CharacterInputFlags in_ = 0;
btCapsuleShapeZ bt_shape_; btCapsuleShapeZ bt_shape_;
btPairCachingGhostObject bt_ghost_; float z_offset_ = 0.0f; // offset of controller from root
std::unique_ptr<CharacterPhysicsController> controller_;
btKinematicCharacterController bt_character_;
float yaw_ = 0.0f; float yaw_ = 0.0f;
float forward_yaw_ = 0.0f; float forward_yaw_ = 0.0f;

View File

@ -4,15 +4,40 @@
game::Entity::Entity(World& world, net::EntType viewtype) : Scheduler(world.GetTime()), world_(world), entnum_(world.GetNewEntnum()), viewtype_(viewtype) {} 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 void game::Entity::SendInitData(Player& player, net::OutMessage& msg) const
{ {
WriteNametag(msg); 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) void game::Entity::SetNametag(const std::string& nametag)
@ -21,6 +46,12 @@ void game::Entity::SetNametag(const std::string& nametag)
SendNametagMsg(); // notify viewers SendNametagMsg(); // notify viewers
} }
void game::Entity::Attach(net::EntNum parentnum)
{
parentnum_ = parentnum;
SendAttachMsg();
}
void game::Entity::WriteNametag(net::OutMessage& msg) const void game::Entity::WriteNametag(net::OutMessage& msg) const
{ {
msg.Write(net::NameTag{nametag_}); msg.Write(net::NameTag{nametag_});
@ -32,6 +63,17 @@ void game::Entity::SendNametagMsg()
WriteNametag(msg); 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) net::OutMessage game::Entity::BeginEntMsg(net::EntMsgType type)
{ {
auto msg = BeginMsg(net::MSG_ENTMSG); auto msg = BeginMsg(net::MSG_ENTMSG);

View File

@ -22,14 +22,21 @@ public:
net::EntNum GetEntNum() const { return entnum_; } net::EntNum GetEntNum() const { return entnum_; }
net::EntType GetViewType() const { return viewtype_; } net::EntType GetViewType() const { return viewtype_; }
virtual void Update();
virtual void SendInitData(Player& player, net::OutMessage& msg) const; 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 SetNametag(const std::string& nametag);
void Attach(net::EntNum parentnum);
net::EntNum GetParentNum() const { return parentnum_; }
void Remove() { removed_ = true; } void Remove() { removed_ = true; }
bool IsRemoved() const { return removed_; } bool IsRemoved() const { return removed_; }
const TransformNode& GetRoot() const { return root_; }
const Transform& GetRootTransform() const { return root_.local; } const Transform& GetRootTransform() const { return root_.local; }
float GetMaxDistance() const { return max_distance_; } float GetMaxDistance() const { return max_distance_; }
@ -37,9 +44,11 @@ public:
private: private:
void WriteNametag(net::OutMessage& msg) const; void WriteNametag(net::OutMessage& msg) const;
void SendNametagMsg(); void SendNametagMsg();
void WriteAttach(net::OutMessage& msg) const;
void SendAttachMsg();
protected: protected:
net::OutMessage BeginEntMsg(net::EntMsgType type); net::OutMessage BeginEntMsg(net::EntMsgType type);
@ -49,13 +58,16 @@ protected:
const net::EntType viewtype_; const net::EntType viewtype_;
TransformNode root_; TransformNode root_;
Entity* parent_ = nullptr;
float max_distance_ = 700.0f; float max_distance_ = 700.0f;
std::string nametag_;
bool removed_ = false; bool removed_ = false;
private:
int64_t upd_time_ = -1;
std::string nametag_;
net::EntNum parentnum_ = 0;
}; };
} }

View File

@ -247,19 +247,28 @@ static glm::vec3 GetRandomColor()
return color; return color;
} }
void game::OpenWorld::SpawnCharacter(Player& player) game::Character& game::OpenWorld::SpawnRandomCharacter()
{ {
RemoveCharacter(player);
CharacterInfo cinfo; CharacterInfo cinfo;
auto& character = Spawn<Character>(cinfo); auto& character = Spawn<Character>(cinfo);
character.SetNametag("player (" + std::to_string(character.GetEntNum()) + ")");
character.SetPosition({ 100.0f, 100.0f, 5.0f });
// add clothes // add clothes
character.AddClothes("tshirt", GetRandomColor()); character.AddClothes("tshirt", GetRandomColor());
character.AddClothes("shorts", 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.SetCamera(character.GetEntNum());
player_characters_[&player] = &character; player_characters_[&player] = &character;
@ -572,6 +581,13 @@ void game::OpenWorld::SpawnBot()
vehicle.Schedule(rand() % 500, [think_state]() { vehicle.Schedule(rand() % 500, [think_state]() {
//BotNametagThink(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<float>());
} }
void game::OpenWorld::SpawnVehicle(Player& player) void game::OpenWorld::SpawnVehicle(Player& player)

View File

@ -23,6 +23,8 @@ private:
void SpawnVehicle(Player& player); void SpawnVehicle(Player& player);
void RemoveVehicle(Player& player); void RemoveVehicle(Player& player);
Character& SpawnRandomCharacter();
void SpawnCharacter(Player& player); void SpawnCharacter(Player& player);
void RemoveCharacter(Player& player); void RemoveCharacter(Player& player);

View File

@ -43,7 +43,7 @@ void game::Player::Update()
auto cam_ent = world_->GetEntity(cam_ent_); auto cam_ent = world_->GetEntity(cam_ent_);
if (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 // max distance check
float max_dist = entity.GetMaxDistance(); 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; return false;
// TODO: custom callback // TODO: custom callback

View File

@ -24,6 +24,11 @@ namespace game
matrix = parent->matrix * matrix; matrix = parent->matrix * matrix;
} }
} }
glm::vec3 GetGlobalPosition() const
{
return matrix[3];
}
}; };
} }

View File

@ -97,6 +97,8 @@ void game::Vehicle::Update()
{ {
Super::Update(); Super::Update();
root_.UpdateMatrix();
flags_ = 0; flags_ = 0;
ProcessInput(); ProcessInput();
UpdateWheels(); UpdateWheels();

View File

@ -41,7 +41,7 @@ void game::World::Update(int64_t delta_time)
// update entities // update entities
for (auto it = ents_.begin(); it != ents_.end();) for (auto it = ents_.begin(); it != ents_.end();)
{ {
it->second->Update(); it->second->TryUpdate();
if (it->second->IsRemoved()) if (it->second->IsRemoved())
it = ents_.erase(it); it = ents_.erase(it);

View File

@ -53,6 +53,8 @@ bool game::view::CharacterView::ProcessMsg(net::EntMsgType type, net::InMessage&
void game::view::CharacterView::Update(const UpdateInfo& info) void game::view::CharacterView::Update(const UpdateInfo& info)
{ {
Super::Update(info);
// interpolate states // interpolate states
float tps = 25.0f; float tps = 25.0f;
float t = (info.time - update_time_) * tps * 0.8f; // assume some jitter, interpolate for longer float t = (info.time - update_time_) * tps * 0.8f; // assume some jitter, interpolate for longer

View File

@ -10,7 +10,8 @@ game::view::EntityView::EntityView(WorldView& world, net::InMessage& msg) :
audioplayer_(world_.GetAudioMaster()), audioplayer_(world_.GetAudioMaster()),
nametag_text_(assets::CacheManager::GetFont("data/comic32.font"), 0xFFFFFFFF) nametag_text_(assets::CacheManager::GetFont("data/comic32.font"), 0xFFFFFFFF)
{ {
if (!ReadNametag(msg)) // read nametag and attachment info
if (!ReadNametag(msg) || !ReadAttach(msg))
throw EntityInitError(); throw EntityInitError();
} }
@ -20,13 +21,38 @@ bool game::view::EntityView::ProcessMsg(net::EntMsgType type, net::InMessage& ms
{ {
case net::EMSG_NAMETAG: case net::EMSG_NAMETAG:
return ReadNametag(msg); 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) 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(); audioplayer_.Update();
} }
@ -49,6 +75,11 @@ bool game::view::EntityView::ReadNametag(net::InMessage& msg)
return true; return true;
} }
bool game::view::EntityView::ReadAttach(net::InMessage& msg)
{
return msg.Read(parentnum_);
}
void game::view::EntityView::DrawNametag(const DrawArgs& args) void game::view::EntityView::DrawNametag(const DrawArgs& args)
{ {
if (nametag_.empty()) if (nametag_.empty())

View File

@ -36,17 +36,20 @@ public:
DELETE_COPY_MOVE(EntityView) DELETE_COPY_MOVE(EntityView)
virtual bool ProcessMsg(net::EntMsgType type, net::InMessage& msg); 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 Update(const UpdateInfo& info);
virtual void Draw(const DrawArgs& args); 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_; } const TransformNode& GetRoot() const { return root_; }
virtual ~EntityView() = default; virtual ~EntityView() = default;
private: private:
bool ReadNametag(net::InMessage& msg); bool ReadNametag(net::InMessage& msg);
bool ReadAttach(net::InMessage& msg);
void DrawNametag(const DrawArgs& args); void DrawNametag(const DrawArgs& args);
void DrawAxes(const DrawArgs& args); void DrawAxes(const DrawArgs& args);
@ -55,13 +58,20 @@ protected:
WorldView& world_; WorldView& world_;
TransformNode root_; TransformNode root_;
EntityView* parent_ = nullptr;
float radius_ = 1.0f; float radius_ = 1.0f;
audio::Player audioplayer_; audio::Player audioplayer_;
private:
std::string nametag_; std::string nametag_;
gfx::Text nametag_text_; gfx::Text nametag_text_;
gfx::HudPosition nametag_pos_; gfx::HudPosition nametag_pos_;
net::EntNum parentnum_ = 0;
float upd_time_ = 0.0f;
}; };
} // namespace game::view } // namespace game::view

View File

@ -38,7 +38,7 @@ void game::view::WorldView::Update(const UpdateInfo& info)
for (const auto& [entnum, ent] : ents_) for (const auto& [entnum, ent] : ents_)
{ {
ent->Update(info); ent->TryUpdate(info);
} }
} }

View File

@ -75,6 +75,7 @@ enum EntMsgType : uint8_t
EMSG_NONE, EMSG_NONE,
EMSG_NAMETAG, EMSG_NAMETAG,
EMSG_ATTACH,
EMSG_UPDATE, EMSG_UPDATE,
}; };