Destructible objects pt. 2

This commit is contained in:
tovjemam 2026-02-27 22:36:23 +01:00
parent d6947a79d6
commit 986cbc12a6
18 changed files with 432 additions and 14 deletions

View File

@ -28,6 +28,7 @@ set(COMMON_SOURCES
"src/game/character_anim_state.hpp"
"src/game/character_anim_state.cpp"
"src/game/player_input.hpp"
"src/game/simple_entity_sync.hpp"
"src/game/skeletoninstance.hpp"
"src/game/skeletoninstance.cpp"
"src/game/transform_node.hpp"
@ -79,6 +80,8 @@ set(CLIENT_ONLY_SOURCES
"src/gameview/entityview.cpp"
"src/gameview/mapinstanceview.hpp"
"src/gameview/mapinstanceview.cpp"
"src/gameview/simple_entity_view.hpp"
"src/gameview/simple_entity_view.cpp"
"src/gameview/skinning_ubo.hpp"
"src/gameview/skinning_ubo.cpp"
"src/gameview/vehicleview.hpp"
@ -116,6 +119,8 @@ set(SERVER_ONLY_SOURCES
"src/game/character.cpp"
"src/game/controllable_character.hpp"
"src/game/controllable_character.cpp"
"src/game/destroyed_object.hpp"
"src/game/destroyed_object.cpp"
"src/game/drivable_vehicle.hpp"
"src/game/drivable_vehicle.cpp"
"src/game/entity.hpp"
@ -132,6 +137,8 @@ set(SERVER_ONLY_SOURCES
"src/game/player_character.cpp"
"src/game/player.hpp"
"src/game/player.cpp"
"src/game/simple_entity.hpp"
"src/game/simple_entity.cpp"
"src/game/usable.hpp"
"src/game/vehicle.hpp"
"src/game/vehicle.cpp"

View File

@ -8,6 +8,7 @@
std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::string& filename)
{
auto model = std::make_shared<Model>();
model->name_ = filename; // TODO: name not filename
std::vector<glm::vec3> vert_pos; // rember for collision trimesh
CLIENT_ONLY(MeshBuilder mb(gfx::MF_NONE);)
@ -45,7 +46,12 @@ std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::stri
vert_pos.emplace_back(pos);
if (temp_hull)
temp_hull->addPoint(btVector3(pos.x, pos.y, pos.z), false);
{
auto offset_pos = pos - model->col_offset_;
temp_hull->addPoint(btVector3(offset_pos.x, offset_pos.y, offset_pos.z), false);
}
model->aabb_.AddPoint(pos);
}
@ -65,8 +71,17 @@ std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::stri
if (model->cmesh_)
{
// FIXME: possible index segfault
model->cmesh_->AddTriangle(vert_pos[indices[0]], vert_pos[indices[1]], vert_pos[indices[2]]);
glm::vec3 p[3];
for (size_t i = 0; i < 3; ++i)
{
size_t index = indices[i];
if (index >= vert_pos.size())
throw std::runtime_error("Vertex index out of bounds in model");
p[i] = vert_pos[index] - model->col_offset_;
}
model->cmesh_->AddTriangle(p[0], p[1], p[2]);
}
}
else if (command == "surface")
@ -141,6 +156,8 @@ std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::stri
glm::vec3 scale(trans.scale, sy, sz);
trans.scale = 1.0f;
trans.position -= model->col_offset_; // apply offset
if (!compound)
{
compound = std::make_unique<btCompoundShape>();
@ -157,6 +174,12 @@ std::shared_ptr<const assets::Model> assets::Model::LoadFromFile(const std::stri
throw std::runtime_error("Unknown collision shape type: " + shape_type);
}
}
else if (command == "centerofmass")
{
glm::vec3 com;
iss >> com.x >> com.y >> com.z;
model->col_offset_ = com;
}
else
{
throw std::runtime_error("Unknown command in model file: " + command);

View File

@ -41,6 +41,9 @@ public:
Model() = default;
static std::shared_ptr<const Model> LoadFromFile(const std::string& filename);
const std::string& GetName() const { return name_; }
const glm::vec3& GetColOffset() const { return col_offset_; }
const collision::TriangleMesh* GetColMesh() const { return cmesh_.get(); }
btCollisionShape* GetColShape() const { return cshape_.get(); }
@ -49,6 +52,8 @@ public:
const AABB3& GetAABB() const { return aabb_; }
private:
std::string name_;
glm::vec3 col_offset_ = glm::vec3(0.0f);
std::unique_ptr<collision::TriangleMesh> cmesh_;
// std::vector<ModelCollisionShape> cshapes_;
std::vector<std::unique_ptr<btCollisionShape>> subshapes_;

View File

@ -0,0 +1,22 @@
#include "destroyed_object.hpp"
game::DestroyedObject::DestroyedObject(World& world, std::unique_ptr<MapObjectCollision> col)
: Super(world, col->GetModel()->GetName()), col_(std::move(col))
{
// remove after 30s
Schedule(30000, [this]()
{
Remove();
});
}
void game::DestroyedObject::UpdatePreSync()
{
if (!col_)
return;
// sync transform with the physics body
col_->GetModelTransform(root_.local);
root_.UpdateMatrix();
}

View File

@ -0,0 +1,22 @@
#pragma once
#include "simple_entity.hpp"
#include "mapinstance.hpp"
namespace game
{
class DestroyedObject : public SimpleEntity
{
public:
using Super = SimpleEntity;
DestroyedObject(World& world, std::unique_ptr<MapObjectCollision> col);
virtual void UpdatePreSync() override;
private:
std::unique_ptr<MapObjectCollision> col_;
};
} // namespace game

View File

@ -89,7 +89,10 @@ game::MapObjectCollision::MapObjectCollision(collision::DynamicsWorld& world,
btRigidBody::btRigidBodyConstructionInfo(mass, nullptr, cmesh->GetShape(), local_inertia));
}
body_->setWorldTransform(trans.ToBtTransform());
auto offset_trans = trans;
offset_trans.position += trans.rotation * model_->GetColOffset();
body_->setWorldTransform(offset_trans.ToBtTransform());
body_->setUserIndex(static_cast<int>(obj_type));
body_->setUserPointer(this);
@ -102,17 +105,28 @@ void game::MapObjectCollision::Break()
if (!body_)
return;
// body_->setCollisionFlags(body_->getCollisionFlags() & ~btCollisionObject::CF_KINEMATIC_OBJECT);
btCollisionShape* shape = body_->getCollisionShape();
float mass = 10.0f;
btVector3 local_inertia(0, 0, 0);
body_->getCollisionShape()->calculateLocalInertia(mass, local_inertia);
shape->calculateLocalInertia(mass, local_inertia);
body_->setMassProps(mass, local_inertia);
body_->forceActivationState(ACTIVE_TAG);
btTransform trans = body_->getWorldTransform();
// set to undefined to avoid breaking again
// remove old
world_.GetBtWorld().removeRigidBody(body_.get());
body_.reset();
// make new
body_ = std::make_unique<btRigidBody>(btRigidBody::btRigidBodyConstructionInfo(mass, nullptr, shape, local_inertia));
body_->setWorldTransform(trans);
body_->setUserIndex(static_cast<int>(collision::OT_UNDEFINED));
world_.GetBtWorld().addRigidBody(body_.get());
}
void game::MapObjectCollision::GetModelTransform(Transform& trans) const
{
trans.SetBtTransform(body_->getWorldTransform());
trans.position -= trans.rotation * model_->GetColOffset(); // unapply offset
}
game::MapObjectCollision::~MapObjectCollision()

View File

@ -23,9 +23,13 @@ public:
void Break();
void GetModelTransform(Transform& trans) const;
const std::shared_ptr<const assets::Model>& GetModel() const { return model_; }
btRigidBody& GetBtBody() { return *body_; }
net::ObjNum GetNum() const { return num_; }
~MapObjectCollision();
private:

View File

@ -8,6 +8,7 @@
#include "player_character.hpp"
#include "npc_character.hpp"
#include "drivable_vehicle.hpp"
#include "destroyed_object.hpp"
namespace game
{
@ -77,6 +78,16 @@ void game::OpenWorld::PlayerLeft(Player& player)
RemovePlayerCharacter(player);
}
void game::OpenWorld::DestructibleDestroyed(net::ObjNum num, std::unique_ptr<MapObjectCollision> col)
{
auto& destroyed_obj = Spawn<DestroyedObject>(std::move(col));
// Schedule(100000, [this, objnum = num]()
// {
// RespawnObj(objnum);
// });
}
std::optional<std::pair<game::Usable&, const game::UseTarget&>> game::OpenWorld::GetBestUseTarget(const glm::vec3& pos) const
{
std::optional<std::pair<Usable*, const UseTarget*>> best_target;

View File

@ -25,6 +25,8 @@ public:
virtual void PlayerViewAnglesChanged(Player& player, float yaw, float pitch) override;
virtual void PlayerLeft(Player& player) override;
virtual void DestructibleDestroyed(net::ObjNum num, std::unique_ptr<MapObjectCollision> col) override;
std::optional<std::pair<Usable&, const UseTarget&>> GetBestUseTarget(const glm::vec3& pos) const;
private:

View File

@ -0,0 +1,81 @@
#include "simple_entity.hpp"
#include "net/utils.hpp"
game::SimpleEntity::SimpleEntity(World& world, const std::string& modelname) : Super(world, net::ET_SIMPLE), modelname_(modelname)
{
UpdateSyncState();
}
void game::SimpleEntity::SendInitData(Player& player, net::OutMessage& msg) const
{
Super::SendInitData(player, msg);
msg.Write(net::ModelName(modelname_));
// write state against default
static const SimpleEntitySyncState default_state;
size_t fields_pos = msg.Reserve<SimpleEntitySyncFieldFlags>();
auto fields = WriteState(msg, default_state);
msg.WriteAt(fields_pos, fields);
}
void game::SimpleEntity::Update()
{
Super::Update();
UpdatePreSync(); // chance to update state before sent
sync_current_ = 1 - sync_current_;
UpdateSyncState();
SendUpdateMsg();
}
void game::SimpleEntity::UpdateSyncState()
{
SimpleEntitySyncState& state = sync_[sync_current_];
net::EncodePosition(root_.local.position, state.pos);
net::EncodeRotation(root_.local.rotation, state.rot);
}
game::SimpleEntitySyncFieldFlags game::SimpleEntity::WriteState(net::OutMessage& msg, const SimpleEntitySyncState& base) const
{
SimpleEntitySyncFieldFlags fields = 0;
const SimpleEntitySyncState& curr = sync_[sync_current_];
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)
{
fields |= SESF_POSITION;
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);
}
if (curr.rot.x.value != base.rot.x.value ||
curr.rot.y.value != base.rot.y.value ||
curr.rot.z.value != base.rot.z.value)
{
fields |= SESF_ROTATION;
net::WriteDelta(msg, curr.rot.x, base.rot.x);
net::WriteDelta(msg, curr.rot.y, base.rot.y);
net::WriteDelta(msg, curr.rot.z, base.rot.z);
}
return fields;
}
void game::SimpleEntity::SendUpdateMsg()
{
auto msg = BeginEntMsg(net::EMSG_UPDATE);
// write state against previous
const SimpleEntitySyncState& prev = sync_[1 - sync_current_];
size_t fields_pos = msg.Reserve<SimpleEntitySyncFieldFlags>();
auto fields = WriteState(msg, prev);
msg.WriteAt(fields_pos, fields);
}

View File

@ -0,0 +1,32 @@
#include "entity.hpp"
#include "simple_entity_sync.hpp"
namespace game
{
class SimpleEntity : public Entity
{
public:
using Super = Entity;
SimpleEntity(World& world, const std::string& modelname);
virtual void SendInitData(Player& player, net::OutMessage& msg) const override;
virtual void Update() override;
virtual void UpdatePreSync() {}
private:
void UpdateSyncState();
SimpleEntitySyncFieldFlags WriteState(net::OutMessage& msg, const SimpleEntitySyncState& base) const;
void SendUpdateMsg();
private:
std::string modelname_;
SimpleEntitySyncState sync_[2];
size_t sync_current_ = 0;
};
} // namespace game

View File

@ -0,0 +1,23 @@
#pragma once
#include "net/defs.hpp"
namespace game
{
struct SimpleEntitySyncState
{
net::PositionQ pos;
net::QuatQ rot;
};
using SimpleEntitySyncFieldFlags = uint8_t;
enum SimpleEntitySyncFieldFlag
{
SESF_POSITION = 0x01,
SESF_ROTATION = 0x02,
};
}

View File

@ -7,7 +7,7 @@
#include "utils/allocnum.hpp"
#include "collision/object_type.hpp"
game::World::World(std::string mapname) : map_(*this, std::move(mapname))
game::World::World(std::string mapname) : Scheduler(time_ms_), map_(*this, std::move(mapname))
{
}
@ -51,6 +51,8 @@ void game::World::Update(int64_t delta_time)
DetectDestructibleCollisions();
RunTasks();
// update entities
for (auto it = ents_.begin(); it != ents_.end();)
{
@ -61,10 +63,13 @@ void game::World::Update(int64_t delta_time)
else
++it;
}
}
void game::World::FinishFrame()
{
ResetMsg();
// reset ent msgs
for (auto& [entnum, ent] : ents_)
{
@ -82,6 +87,15 @@ game::Entity* game::World::GetEntity(net::EntNum entnum)
return it->second.get();
}
void game::World::RespawnObj(net::ObjNum objnum)
{
if (destroyed_objs_.erase(objnum) > 0)
{
map_.SpawnObj(objnum);
SendObjRespawnedMsg(objnum);
}
}
void game::World::DetectDestructibleCollisions()
{
auto& bt_world = GetBtWorld();
@ -110,7 +124,7 @@ void game::World::DetectDestructibleCollisions()
for (int j = 0; j < contactManifold->getNumContacts(); j++)
{
const float break_threshold = 3000.0f; // TODO: per-object threshold
const float break_threshold = 100.0f; // TODO: per-object threshold
btManifoldPoint& pt = contactManifold->getContactPoint(j);

View File

@ -12,7 +12,7 @@
namespace game
{
class World : public collision::DynamicsWorld, public net::MsgProducer
class World : public collision::DynamicsWorld, public net::MsgProducer, public Scheduler
{
public:
World(std::string mapname);
@ -46,6 +46,8 @@ public:
Entity* GetEntity(net::EntNum entnum);
void RespawnObj(net::ObjNum objnum);
const assets::Map& GetMap() const { return map_.GetMap(); }
const std::string& GetMapName() const { return map_.GetName(); }
const std::map<net::EntNum, std::unique_ptr<Entity>>& GetEntities() const { return ents_; }

View File

@ -0,0 +1,108 @@
#include "simple_entity_view.hpp"
#include "net/defs.hpp"
#include "assets/cache.hpp"
#include "worldview.hpp"
#include "net/utils.hpp"
game::view::SimpleEntityView::SimpleEntityView(WorldView& world, net::InMessage& msg) : Super(world, msg)
{
net::ModelName modelname;
if (!msg.Read(modelname))
throw EntityInitError();
if (modelname.len > 0)
{
model_ = assets::CacheManager::GetModel(std::string(modelname));
if (!model_)
throw EntityInitError();
}
if (!ReadState(msg))
throw EntityInitError();
states_[0] = states_[1]; // lerp from the read state to avoid jump
radius_ = 20.0f;
}
bool game::view::SimpleEntityView::ProcessMsg(net::EntMsgType type, net::InMessage& msg)
{
switch (type)
{
case net::EMSG_UPDATE:
return ProcessUpdateMsg(msg);
default:
return Super::ProcessMsg(type, msg);
}
}
void game::view::SimpleEntityView::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
t = glm::clamp(t, 0.0f, 2.0f);
root_.local = Transform::Lerp(states_[0].trans, states_[1].trans, t);
root_.UpdateMatrix();
}
void game::view::SimpleEntityView::Draw(const DrawArgs& args)
{
Super::Draw(args);
if (!model_)
return;
const auto& mesh = *model_->GetMesh();
for (const auto& surface : mesh.surfaces)
{
gfx::DrawSurfaceCmd cmd;
cmd.surface = &surface;
cmd.matrices = &root_.matrix;
args.dlist.AddSurface(cmd);
}
}
bool game::view::SimpleEntityView::ReadState(net::InMessage& msg)
{
update_time_ = world_.GetTime();
// init lerp start state
states_[0].trans = root_.local;
auto& new_state = states_[1];
// parse state delta
SimpleEntitySyncFieldFlags fields;
if (!msg.Read(fields))
return false;
// pos
if (fields & SESF_POSITION)
{
if (!net::ReadDelta(msg, sync_.pos.x) || !net::ReadDelta(msg, sync_.pos.y) || !net::ReadDelta(msg, sync_.pos.z))
return false;
net::DecodePosition(sync_.pos, new_state.trans.position);
}
// rot
if (fields & SESF_ROTATION)
{
if (!net::ReadDelta(msg, sync_.rot.x) || !net::ReadDelta(msg, sync_.rot.y) || !net::ReadDelta(msg, sync_.rot.z))
return false;
net::DecodeRotation(sync_.rot, new_state.trans.rotation);
}
return true;
}
bool game::view::SimpleEntityView::ProcessUpdateMsg(net::InMessage& msg)
{
return ReadState(msg);
}

View File

@ -0,0 +1,41 @@
#pragma once
#include "assets/model.hpp"
#include "entityview.hpp"
#include "game/simple_entity_sync.hpp"
namespace game::view
{
struct SimpleEntityViewState
{
Transform trans;
};
class SimpleEntityView : public EntityView
{
public:
using Super = EntityView;
SimpleEntityView(WorldView& world, net::InMessage& msg);
virtual bool ProcessMsg(net::EntMsgType type, net::InMessage& msg) override;
virtual void Update(const UpdateInfo& info) override;
virtual void Draw(const DrawArgs& args) override;
private:
bool ReadState(net::InMessage& msg);
bool ProcessUpdateMsg(net::InMessage& msg);
private:
std::shared_ptr<const assets::Model> model_;
SimpleEntitySyncState sync_;
SimpleEntityViewState states_[2];
float update_time_ = 0.0f;
};
}

View File

@ -2,6 +2,7 @@
#include "assets/cache.hpp"
#include "simple_entity_view.hpp"
#include "characterview.hpp"
#include "vehicleview.hpp"
#include "client_session.hpp"
@ -123,6 +124,10 @@ bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg)
{
switch (type)
{
case net::ET_SIMPLE:
entslot = std::make_unique<SimpleEntityView>(*this, msg);
break;
case net::ET_CHARACTER:
entslot = std::make_unique<CharacterView>(*this, msg);
break;
@ -132,6 +137,7 @@ bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg)
break;
default:
ents_.erase(entnum);
return false; // unknown type
}

View File

@ -72,6 +72,7 @@ enum EntType : uint8_t
{
ET_NONE,
ET_SIMPLE,
ET_CHARACTER,
ET_VEHICLE,