diff --git a/CMakeLists.txt b/CMakeLists.txt index 897487a..67714c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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" diff --git a/src/assets/model.cpp b/src/assets/model.cpp index f76aa36..93601a2 100644 --- a/src/assets/model.cpp +++ b/src/assets/model.cpp @@ -8,6 +8,7 @@ std::shared_ptr assets::Model::LoadFromFile(const std::string& filename) { auto model = std::make_shared(); + model->name_ = filename; // TODO: name not filename std::vector vert_pos; // rember for collision trimesh CLIENT_ONLY(MeshBuilder mb(gfx::MF_NONE);) @@ -45,7 +46,12 @@ std::shared_ptr 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 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 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(); @@ -157,6 +174,12 @@ std::shared_ptr 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); diff --git a/src/assets/model.hpp b/src/assets/model.hpp index 3295e98..a6d0fd5 100644 --- a/src/assets/model.hpp +++ b/src/assets/model.hpp @@ -41,6 +41,9 @@ public: Model() = default; static std::shared_ptr 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 cmesh_; // std::vector cshapes_; std::vector> subshapes_; diff --git a/src/game/destroyed_object.cpp b/src/game/destroyed_object.cpp new file mode 100644 index 0000000..75e5b38 --- /dev/null +++ b/src/game/destroyed_object.cpp @@ -0,0 +1,22 @@ +#include "destroyed_object.hpp" + +game::DestroyedObject::DestroyedObject(World& world, std::unique_ptr 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(); +} diff --git a/src/game/destroyed_object.hpp b/src/game/destroyed_object.hpp new file mode 100644 index 0000000..ffaf98e --- /dev/null +++ b/src/game/destroyed_object.hpp @@ -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 col); + + virtual void UpdatePreSync() override; + +private: + std::unique_ptr col_; +}; + +} // namespace game \ No newline at end of file diff --git a/src/game/mapinstance.cpp b/src/game/mapinstance.cpp index 3e59473..a7fd434 100644 --- a/src/game/mapinstance.cpp +++ b/src/game/mapinstance.cpp @@ -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(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::btRigidBodyConstructionInfo(mass, nullptr, shape, local_inertia)); + body_->setWorldTransform(trans); body_->setUserIndex(static_cast(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() diff --git a/src/game/mapinstance.hpp b/src/game/mapinstance.hpp index 9c1cce5..c918b62 100644 --- a/src/game/mapinstance.hpp +++ b/src/game/mapinstance.hpp @@ -23,8 +23,12 @@ public: void Break(); + void GetModelTransform(Transform& trans) const; + + const std::shared_ptr& GetModel() const { return model_; } btRigidBody& GetBtBody() { return *body_; } net::ObjNum GetNum() const { return num_; } + ~MapObjectCollision(); diff --git a/src/game/openworld.cpp b/src/game/openworld.cpp index 689c91d..8230589 100644 --- a/src/game/openworld.cpp +++ b/src/game/openworld.cpp @@ -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 col) +{ + auto& destroyed_obj = Spawn(std::move(col)); + + // Schedule(100000, [this, objnum = num]() + // { + // RespawnObj(objnum); + // }); +} + std::optional> game::OpenWorld::GetBestUseTarget(const glm::vec3& pos) const { std::optional> best_target; diff --git a/src/game/openworld.hpp b/src/game/openworld.hpp index 4caff0d..7b385f1 100644 --- a/src/game/openworld.hpp +++ b/src/game/openworld.hpp @@ -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 col) override; + std::optional> GetBestUseTarget(const glm::vec3& pos) const; private: diff --git a/src/game/simple_entity.cpp b/src/game/simple_entity.cpp new file mode 100644 index 0000000..9a880d3 --- /dev/null +++ b/src/game/simple_entity.cpp @@ -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(); + 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(); + auto fields = WriteState(msg, prev); + msg.WriteAt(fields_pos, fields); +} diff --git a/src/game/simple_entity.hpp b/src/game/simple_entity.hpp new file mode 100644 index 0000000..0cbdcab --- /dev/null +++ b/src/game/simple_entity.hpp @@ -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 \ No newline at end of file diff --git a/src/game/simple_entity_sync.hpp b/src/game/simple_entity_sync.hpp new file mode 100644 index 0000000..7499bc0 --- /dev/null +++ b/src/game/simple_entity_sync.hpp @@ -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, +}; + + +} \ No newline at end of file diff --git a/src/game/world.cpp b/src/game/world.cpp index bbeb507..15e9939 100644 --- a/src/game/world.cpp +++ b/src/game/world.cpp @@ -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); diff --git a/src/game/world.hpp b/src/game/world.hpp index 6051fd0..e5121e1 100644 --- a/src/game/world.hpp +++ b/src/game/world.hpp @@ -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>& GetEntities() const { return ents_; } diff --git a/src/gameview/simple_entity_view.cpp b/src/gameview/simple_entity_view.cpp new file mode 100644 index 0000000..b4681e4 --- /dev/null +++ b/src/gameview/simple_entity_view.cpp @@ -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); +} diff --git a/src/gameview/simple_entity_view.hpp b/src/gameview/simple_entity_view.hpp new file mode 100644 index 0000000..2f557ab --- /dev/null +++ b/src/gameview/simple_entity_view.hpp @@ -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 model_; + + SimpleEntitySyncState sync_; + SimpleEntityViewState states_[2]; + float update_time_ = 0.0f; + +}; + + +} \ No newline at end of file diff --git a/src/gameview/worldview.cpp b/src/gameview/worldview.cpp index 04dde08..bb82137 100644 --- a/src/gameview/worldview.cpp +++ b/src/gameview/worldview.cpp @@ -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(*this, msg); + break; + case net::ET_CHARACTER: entslot = std::make_unique(*this, msg); break; @@ -132,6 +137,7 @@ bool game::view::WorldView::ProcessEntSpawnMsg(net::InMessage& msg) break; default: + ents_.erase(entnum); return false; // unknown type } diff --git a/src/net/defs.hpp b/src/net/defs.hpp index 2a059ce..094dd50 100644 --- a/src/net/defs.hpp +++ b/src/net/defs.hpp @@ -72,9 +72,10 @@ enum EntType : uint8_t { ET_NONE, + ET_SIMPLE, ET_CHARACTER, ET_VEHICLE, - + ET_COUNT, };