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 "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<CharacterPhysicsController>(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<CharacterSyncFieldFlags>();
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_);
}

View File

@ -1,10 +1,11 @@
#pragma once
#include "entity.hpp"
#include "BulletCollision/CollisionDispatch/btGhostObject.h"
#include "BulletDynamics/Character/btKinematicCharacterController.h"
#include <btBulletDynamicsCommon.h>
#include <BulletCollision/CollisionDispatch/btGhostObject.h>
#include <BulletDynamics/Character/btKinematicCharacterController.h>
#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<CharacterPhysicsController> controller_;
float 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) {}
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);

View File

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

View File

@ -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<Character>(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<float>());
}
void game::OpenWorld::SpawnVehicle(Player& player)

View File

@ -22,7 +22,9 @@ public:
private:
void SpawnVehicle(Player& player);
void RemoveVehicle(Player& player);
Character& SpawnRandomCharacter();
void SpawnCharacter(Player& player);
void RemoveCharacter(Player& player);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ void game::view::WorldView::Update(const UpdateInfo& info)
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_NAMETAG,
EMSG_ATTACH,
EMSG_UPDATE,
};