From 51a0b19601cb4b044c7eee1782aa85fc3a399d33 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Tue, 8 Sep 2015 11:55:00 +0200 Subject: [PATCH] sync entities with clients --- doc/protocol | 43 ++++++ src/app/ServerState.cpp | 11 +- src/app/ServerState.hpp | 2 + src/client/MasterState.hpp | 3 + src/client/client.cpp | 84 ++++++++++- src/net/Client.hpp | 1 + src/net/ClientConnection.hpp | 28 ++++ src/net/ConnectionHandler.hpp | 3 + src/net/Packet.hpp | 34 +++++ src/net/net.cpp | 271 +++++++++++++++++++++++++++++++++- src/world/World.hpp | 1 + 11 files changed, 470 insertions(+), 11 deletions(-) diff --git a/doc/protocol b/doc/protocol index 43c14da..a02ff2a 100644 --- a/doc/protocol +++ b/doc/protocol @@ -85,3 +85,46 @@ Payload: 0 chunk coords of the player, 3x 32bit signed int 12 pos/vel/rot/ang of the player, 13x 32bit float Length: 64 + + +Spawn Entity +------------ + +Sent by the server to notify the client of an entity entering spawn range. + +Code: 5 +Payload: + 0 entity ID, 32bit unsigned int + 4 chunk coords of the entity, 3x 32bit signed int + 16 pos/vel/rot/ang of the entity, 13x 32bit float + 68 bounding box of the entity, 6x 32bit float + 92 flags, 32bit bitfield with boolean values + 1: world collision + 96 entity name, max 32 byte UTF-8 string +Length: 128 + + +Despawn Entity +-------------- + +Sent by the server to notify the client of an entity leaving spawn range. + +Code: 6 +Payload: + 0 entity ID, 32bit unsigned int +Length: 4 + + +Entity Update +------------- + +Sent by the server to notify the client of updated entity properties. +Contained entities must be ordered by ascending entity ID. + +Code: 7 +Payload: + 0 number of entities, 32bit int, 1-7 + 4 chunk coords of the entity, 3x 32bit signed int + 16 pos/vel/rot/ang of the entity, 13x 32bit float + 68 next entity... +Length: 4 + multiple of 64, max 452 diff --git a/src/app/ServerState.cpp b/src/app/ServerState.cpp index d26f552..88e817d 100644 --- a/src/app/ServerState.cpp +++ b/src/app/ServerState.cpp @@ -18,10 +18,13 @@ ServerState::ServerState( : env(env) , block_types() , world(block_types, wc, ws) -, server(sc, world) { +, server(sc, world) +, push_timer(16) { TextureIndex tex_index; env.loader.LoadBlockTypes("default", block_types, tex_index); + push_timer.Start(); + std::cout << "listening on UDP port " << sc.port << std::endl; } @@ -34,8 +37,12 @@ void ServerState::Handle(const SDL_Event &event) { void ServerState::Update(int dt) { + push_timer.Update(dt); + server.Handle(); - server.Update(dt); + if (push_timer.Hit()) { + server.Update(dt); + } } diff --git a/src/app/ServerState.hpp b/src/app/ServerState.hpp index fc9ac19..5a3c0dc 100644 --- a/src/app/ServerState.hpp +++ b/src/app/ServerState.hpp @@ -1,6 +1,7 @@ #ifndef BLANK_APP_SERVERSTATE_HPP_ #define BLANK_APP_SERVERSTATE_HPP_ +#include "IntervalTimer.hpp" #include "State.hpp" #include "../net/Server.hpp" #include "../world/BlockTypeRegistry.hpp" @@ -31,6 +32,7 @@ private: BlockTypeRegistry block_types; World world; Server server; + IntervalTimer push_timer; }; diff --git a/src/client/MasterState.hpp b/src/client/MasterState.hpp index 6bdb671..8eeadf0 100644 --- a/src/client/MasterState.hpp +++ b/src/client/MasterState.hpp @@ -51,6 +51,9 @@ public: void On(const Packet::Join &) override; void On(const Packet::Part &) override; + void On(const Packet::SpawnEntity &) override; + void On(const Packet::DespawnEntity &) override; + void On(const Packet::EntityUpdate &) override; private: Environment &env; diff --git a/src/client/client.cpp b/src/client/client.cpp index 9c87549..829cc6b 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -7,6 +7,9 @@ #include "../app/TextureIndex.hpp" #include +#include + +using namespace std; namespace blank { @@ -134,6 +137,9 @@ MasterState::MasterState( } void MasterState::Quit() { + if (!client.GetConnection().Closed()) { + client.SendPart(); + } env.state.PopUntil(this); } @@ -160,7 +166,7 @@ void MasterState::Render(Viewport &) { } -void MasterState::OnPacketLost(std::uint16_t id) { +void MasterState::OnPacketLost(uint16_t id) { if (id == login_packet) { login_packet = client.SendLogin(intf_conf.player_name); } @@ -168,8 +174,9 @@ void MasterState::OnPacketLost(std::uint16_t id) { void MasterState::OnTimeout() { if (client.GetConnection().Closed()) { - Quit(); // TODO: push disconnected message + cout << "connection timed out" << endl; + Quit(); } } @@ -178,10 +185,10 @@ void MasterState::On(const Packet::Join &pack) { if (state) { // changing worlds - std::cout << "server changing worlds" << std::endl; + cout << "server changing worlds to \"" << world_conf.name << '"' << endl; } else { // joining game - std::cout << "joined game" << std::endl; + cout << "joined game \"" << world_conf.name << '"' << endl; } uint32_t player_id; @@ -197,13 +204,78 @@ void MasterState::On(const Packet::Join &pack) { void MasterState::On(const Packet::Part &pack) { if (state) { // kicked - std::cout << "kicked by server" << std::endl; + cout << "kicked by server" << endl; } else { // join refused - std::cout << "login refused by server" << std::endl; + cout << "login refused by server" << endl; } Quit(); } +void MasterState::On(const Packet::SpawnEntity &pack) { + if (!state) { + cout << "got entity spawn before world was created" << endl; + Quit(); + return; + } + uint32_t entity_id; + pack.ReadEntityID(entity_id); + Entity *entity = state->GetWorld().AddEntity(entity_id); + if (!entity) { + cout << "entity ID inconsistency" << endl; + Quit(); + return; + } + pack.ReadEntity(*entity); + cout << "spawned entity " << entity->Name() << " at " << entity->AbsolutePosition() << endl; +} + +void MasterState::On(const Packet::DespawnEntity &pack) { + if (!state) { + cout << "got entity despawn before world was created" << endl; + Quit(); + return; + } + uint32_t entity_id; + pack.ReadEntityID(entity_id); + for (Entity &entity : state->GetWorld().Entities()) { + if (entity.ID() == entity_id) { + entity.Kill(); + cout << "despawned entity " << entity.Name() << " at " << entity.AbsolutePosition() << endl; + return; + } + } +} + +void MasterState::On(const Packet::EntityUpdate &pack) { + if (!state) { + cout << "got entity update before world was created" << endl; + Quit(); + return; + } + + auto world_iter = state->GetWorld().Entities().begin(); + auto world_end = state->GetWorld().Entities().end(); + + uint32_t count = 0; + pack.ReadEntityCount(count); + + for (uint32_t i = 0; i < count; ++i) { + uint32_t entity_id = 0; + pack.ReadEntityID(entity_id, i); + + while (world_iter != world_end && world_iter->ID() < entity_id) { + ++world_iter; + } + if (world_iter == world_end) { + // nothing can be done from here + return; + } + if (world_iter->ID() == entity_id) { + pack.ReadEntity(*world_iter, i); + } + } +} + } } diff --git a/src/net/Client.hpp b/src/net/Client.hpp index 5c0a9c2..2848aed 100644 --- a/src/net/Client.hpp +++ b/src/net/Client.hpp @@ -32,6 +32,7 @@ public: std::uint16_t SendPing(); std::uint16_t SendLogin(const std::string &); + std::uint16_t SendPart(); std::uint16_t SendPlayerUpdate(const Entity &); private: diff --git a/src/net/ClientConnection.hpp b/src/net/ClientConnection.hpp index 8c5306c..fb6420a 100644 --- a/src/net/ClientConnection.hpp +++ b/src/net/ClientConnection.hpp @@ -4,6 +4,7 @@ #include "Connection.hpp" #include "ConnectionHandler.hpp" +#include #include @@ -30,15 +31,42 @@ public: void DetachPlayer(); bool HasPlayer() const noexcept { return player; } Entity &Player() noexcept { return *player; } + const Entity &Player() const noexcept { return *player; } + +private: + struct SpawnStatus { + // the entity in question + Entity *const entity = nullptr; + // sequence number of the spawn packet or -1 after it's been ack'd + std::int32_t spawn_pack = -1; + // sequence number of the despawn packet or -1 if no despawn has been sent + std::int32_t despawn_pack = -1; + + explicit SpawnStatus(Entity &); + ~SpawnStatus(); + }; + +private: + void OnPacketReceived(std::uint16_t) override; + void OnPacketLost(std::uint16_t) override; void On(const Packet::Login &) override; void On(const Packet::Part &) override; void On(const Packet::PlayerUpdate &) override; + bool CanSpawn(const Entity &) const noexcept; + bool CanDespawn(const Entity &) const noexcept; + + void SendSpawn(SpawnStatus &); + void SendDespawn(SpawnStatus &); + void SendUpdate(SpawnStatus &); + private: Server &server; Connection conn; Entity *player; + std::list spawns; + unsigned int confirm_wait; }; diff --git a/src/net/ConnectionHandler.hpp b/src/net/ConnectionHandler.hpp index ab9761a..56538c6 100644 --- a/src/net/ConnectionHandler.hpp +++ b/src/net/ConnectionHandler.hpp @@ -26,6 +26,9 @@ private: virtual void On(const Packet::Join &) { } virtual void On(const Packet::Part &) { } virtual void On(const Packet::PlayerUpdate &) { } + virtual void On(const Packet::SpawnEntity &) { } + virtual void On(const Packet::DespawnEntity &) { } + virtual void On(const Packet::EntityUpdate &) { } }; diff --git a/src/net/Packet.hpp b/src/net/Packet.hpp index 4ff45df..bb6140d 100644 --- a/src/net/Packet.hpp +++ b/src/net/Packet.hpp @@ -99,6 +99,40 @@ struct Packet { void ReadPlayer(Entity &) const noexcept; }; + struct SpawnEntity : public Payload { + static constexpr std::uint8_t TYPE = 5; + static constexpr std::size_t MAX_LEN = 128; + + void WriteEntity(const Entity &) noexcept; + void ReadEntityID(std::uint32_t &) const noexcept; + void ReadEntity(Entity &) const noexcept; + }; + + struct DespawnEntity : public Payload { + static constexpr std::uint8_t TYPE = 6; + static constexpr std::size_t MAX_LEN = 4; + + void WriteEntityID(std::uint32_t) noexcept; + void ReadEntityID(std::uint32_t &) const noexcept; + }; + + struct EntityUpdate : public Payload { + static constexpr std::uint8_t TYPE = 7; + static constexpr std::size_t MAX_LEN = 452; + + static constexpr std::uint32_t MAX_ENTITIES = 7; + static constexpr std::size_t GetSize(std::uint32_t num) noexcept { + return 4 + (num * 64); + } + + void WriteEntityCount(std::uint32_t) noexcept; + void ReadEntityCount(std::uint32_t &) const noexcept; + + void WriteEntity(const Entity &, std::uint32_t) noexcept; + void ReadEntityID(std::uint32_t &, std::uint32_t) const noexcept; + void ReadEntity(Entity &, std::uint32_t) const noexcept; + }; + template PayloadType As() { diff --git a/src/net/net.cpp b/src/net/net.cpp index 1876157..d56e409 100644 --- a/src/net/net.cpp +++ b/src/net/net.cpp @@ -23,6 +23,9 @@ constexpr size_t Packet::Login::MAX_LEN; constexpr size_t Packet::Join::MAX_LEN; constexpr size_t Packet::Part::MAX_LEN; constexpr size_t Packet::PlayerUpdate::MAX_LEN; +constexpr size_t Packet::SpawnEntity::MAX_LEN; +constexpr size_t Packet::DespawnEntity::MAX_LEN; +constexpr size_t Packet::EntityUpdate::MAX_LEN; namespace { @@ -109,11 +112,18 @@ uint16_t Client::SendPlayerUpdate(const Entity &player) { return conn.Send(client_pack, client_sock); } +uint16_t Client::SendPart() { + Packet::Make(client_pack); + return conn.Send(client_pack, client_sock); +} + ClientConnection::ClientConnection(Server &server, const IPaddress &addr) : server(server) , conn(addr) -, player(nullptr) { +, player(nullptr) +, spawns() +, confirm_wait(0) { conn.SetHandler(this); } @@ -124,25 +134,165 @@ ClientConnection::~ClientConnection() { void ClientConnection::Update(int dt) { conn.Update(dt); if (Disconnected()) { - cout << "disconnect from " << conn.Address() << endl; - } else if (conn.ShouldPing()) { + return; + } + if (HasPlayer()) { + // sync entities + auto global_iter = server.GetWorld().Entities().begin(); + auto global_end = server.GetWorld().Entities().end(); + auto local_iter = spawns.begin(); + auto local_end = spawns.end(); + + while (global_iter != global_end && local_iter != local_end) { + if (global_iter->ID() == local_iter->entity->ID()) { + // they're the same + if (CanDespawn(*global_iter)) { + SendDespawn(*local_iter); + } else { + // update + SendUpdate(*local_iter); + } + ++global_iter; + ++local_iter; + } else if (global_iter->ID() < local_iter->entity->ID()) { + // global entity was inserted + if (CanSpawn(*global_iter)) { + auto spawned = spawns.emplace(local_iter, *global_iter); + SendSpawn(*spawned); + } + ++global_iter; + } else { + // global entity was removed + SendDespawn(*local_iter); + ++local_iter; + } + } + + // leftover spawns + while (global_iter != global_end) { + if (CanSpawn(*global_iter)) { + spawns.emplace_back(*global_iter); + SendSpawn(spawns.back()); + } + ++global_iter; + } + + // leftover despawns + while (local_iter != local_end) { + SendDespawn(*local_iter); + ++local_iter; + } + } + if (conn.ShouldPing()) { conn.SendPing(server.GetPacket(), server.GetSocket()); } } +ClientConnection::SpawnStatus::SpawnStatus(Entity &e) +: entity(&e) +, spawn_pack(-1) +, despawn_pack(-1) { + entity->Ref(); +} + +ClientConnection::SpawnStatus::~SpawnStatus() { + entity->UnRef(); +} + +bool ClientConnection::CanSpawn(const Entity &e) const noexcept { + return + &e != player && + !e.Dead() && + manhattan_radius(e.ChunkCoords() - Player().ChunkCoords()) < 7; +} + +bool ClientConnection::CanDespawn(const Entity &e) const noexcept { + return + e.Dead() || + manhattan_radius(e.ChunkCoords() - Player().ChunkCoords()) > 7; +} + +void ClientConnection::SendSpawn(SpawnStatus &status) { + // don't double spawn + if (status.spawn_pack != -1) return; + + auto pack = Packet::Make(server.GetPacket()); + pack.WriteEntity(*status.entity); + status.spawn_pack = conn.Send(server.GetPacket(), server.GetSocket()); + ++confirm_wait; +} + +void ClientConnection::SendDespawn(SpawnStatus &status) { + // don't double despawn + if (status.despawn_pack != -1) return; + + auto pack = Packet::Make(server.GetPacket()); + pack.WriteEntityID(status.entity->ID()); + status.despawn_pack = conn.Send(server.GetPacket(), server.GetSocket()); + ++confirm_wait; +} + +void ClientConnection::SendUpdate(SpawnStatus &status) { + // don't send updates while spawn not ack'd or despawn sent + if (status.spawn_pack != -1 || status.despawn_pack != -1) return; + + // TODO: pack entity updates + auto pack = Packet::Make(server.GetPacket()); + pack.WriteEntityCount(1); + pack.WriteEntity(*status.entity, 0); + server.GetPacket().len = Packet::EntityUpdate::GetSize(1); + conn.Send(server.GetPacket(), server.GetSocket()); +} + void ClientConnection::AttachPlayer(Entity &new_player) { DetachPlayer(); player = &new_player; player->Ref(); + cout << "player \"" << player->Name() << "\" joined" << endl; } void ClientConnection::DetachPlayer() { if (!player) return; player->Kill(); player->UnRef(); + cout << "player \"" << player->Name() << "\" left" << endl; player = nullptr; } +void ClientConnection::OnPacketReceived(uint16_t seq) { + if (!confirm_wait) return; + for (auto iter = spawns.begin(), end = spawns.end(); iter != end; ++iter) { + if (seq == iter->spawn_pack) { + iter->spawn_pack = -1; + --confirm_wait; + return; + } + if (seq == iter->despawn_pack) { + spawns.erase(iter); + --confirm_wait; + return; + } + } +} + +void ClientConnection::OnPacketLost(uint16_t seq) { + if (!confirm_wait) return; + for (SpawnStatus &status : spawns) { + if (seq == status.spawn_pack) { + status.spawn_pack = -1; + --confirm_wait; + SendSpawn(status); + return; + } + if (seq == status.despawn_pack) { + status.despawn_pack = -1; + --confirm_wait; + SendDespawn(status); + return; + } + } +} + void ClientConnection::On(const Packet::Login &pack) { string name; pack.ReadPlayerName(name); @@ -319,6 +469,12 @@ const char *Packet::Type2String(uint8_t t) noexcept { return "Part"; case PlayerUpdate::TYPE: return "PlayerUpdate"; + case SpawnEntity::TYPE: + return "SpawnEntity"; + case DespawnEntity::TYPE: + return "DespawnEntity"; + case EntityUpdate::TYPE: + return "EntityUpdate"; default: return "Unknown"; } @@ -438,6 +594,106 @@ void Packet::PlayerUpdate::ReadPlayer(Entity &player) const noexcept { player.AngularVelocity(ang); } +void Packet::SpawnEntity::WriteEntity(const Entity &e) noexcept { + Write(e.ID(), 0); + Write(e.ChunkCoords(), 4); + Write(e.Position(), 16); + Write(e.Velocity(), 28); + Write(e.Orientation(), 40); + Write(e.AngularVelocity(), 56); + Write(e.Bounds(), 68); + uint32_t flags = 0; + if (e.WorldCollidable()) { + flags |= 1; + } + Write(flags, 92); + WriteString(e.Name(), 96, 32); +} + +void Packet::SpawnEntity::ReadEntityID(uint32_t &id) const noexcept { + Read(id, 0); +} + +void Packet::SpawnEntity::ReadEntity(Entity &e) const noexcept { + glm::ivec3 chunk_coords(0); + glm::vec3 pos; + glm::vec3 vel; + glm::quat rot; + glm::vec3 ang; + AABB bounds; + uint32_t flags = 0; + string name; + + Read(chunk_coords, 4); + Read(pos, 16); + Read(vel, 28); + Read(rot, 40); + Read(ang, 56); + Read(bounds, 68); + Read(flags, 92); + ReadString(name, 96, 32); + + e.Position(chunk_coords, pos); + e.Velocity(vel); + e.Orientation(rot); + e.AngularVelocity(ang); + e.Bounds(bounds); + e.WorldCollidable(flags & 1); + e.Name(name); +} + +void Packet::DespawnEntity::WriteEntityID(uint32_t id) noexcept { + Write(id, 0); +} + +void Packet::DespawnEntity::ReadEntityID(uint32_t &id) const noexcept { + Read(id, 0); +} + +void Packet::EntityUpdate::WriteEntityCount(uint32_t count) noexcept { + Write(count, 0); +} + +void Packet::EntityUpdate::ReadEntityCount(uint32_t &count) const noexcept { + Read(count, 0); +} + +void Packet::EntityUpdate::WriteEntity(const Entity &entity, uint32_t num) noexcept { + uint32_t off = 4 + (num * 64); + + Write(entity.ID(), off); + Write(entity.ChunkCoords(), off + 4); + Write(entity.Position(), off + 16); + Write(entity.Velocity(), off + 28); + Write(entity.Orientation(), off + 40); + Write(entity.AngularVelocity(), off + 56); +} + +void Packet::EntityUpdate::ReadEntityID(uint32_t &id, uint32_t num) const noexcept { + Read(id, 4 + (num * 64)); +} + +void Packet::EntityUpdate::ReadEntity(Entity &entity, uint32_t num) const noexcept { + uint32_t off = 4 + (num * 64); + + glm::ivec3 chunk_coords(0); + glm::vec3 pos; + glm::vec3 vel; + glm::quat rot; + glm::vec3 ang; + + Read(chunk_coords, off + 4); + Read(pos, off + 16); + Read(vel, off + 28); + Read(rot, off + 40); + Read(ang, off + 56); + + entity.Position(chunk_coords, pos); + entity.Velocity(vel); + entity.Orientation(rot); + entity.AngularVelocity(ang); +} + void ConnectionHandler::Handle(const UDPpacket &udp_pack) { const Packet &pack = *reinterpret_cast(udp_pack.data); @@ -457,6 +713,15 @@ void ConnectionHandler::Handle(const UDPpacket &udp_pack) { case Packet::PlayerUpdate::TYPE: On(Packet::As(udp_pack)); break; + case Packet::SpawnEntity::TYPE: + On(Packet::As(udp_pack)); + break; + case Packet::DespawnEntity::TYPE: + On(Packet::As(udp_pack)); + break; + case Packet::EntityUpdate::TYPE: + On(Packet::As(udp_pack)); + break; default: // drop unknown or unhandled packets break; diff --git a/src/world/World.hpp b/src/world/World.hpp index 9739e21..c605d77 100644 --- a/src/world/World.hpp +++ b/src/world/World.hpp @@ -81,6 +81,7 @@ public: Entity *AddEntity(std::uint32_t id); const std::vector &Players() const noexcept { return players; } + std::list &Entities() noexcept { return entities; } const std::list &Entities() const noexcept { return entities; } void Update(int dt); -- 2.39.2