From c1da86ebab41895bf49ed747c75ecf722e8c5586 Mon Sep 17 00:00:00 2001 From: Daniel Karbach Date: Thu, 1 Oct 2015 12:30:54 +0200 Subject: [PATCH] transmit player input from client to server --- doc/protocol | 11 ++- src/client/Client.hpp | 8 +- src/client/InteractiveState.hpp | 16 +--- src/client/NetworkedInput.hpp | 53 ++++++++++++ src/client/client.cpp | 88 ++------------------ src/client/net.cpp | 142 +++++++++++++++++++++++++++++++- src/net/Packet.hpp | 20 +++-- src/net/net.cpp | 65 +++++++++++++-- src/server/ClientConnection.hpp | 15 ++-- src/server/Server.hpp | 6 +- src/server/net.cpp | 69 ++++++++++++---- src/ui/DirectInput.hpp | 20 ----- src/ui/PlayerController.hpp | 47 ++++++++++- src/ui/ui.cpp | 139 +++++++++++++++++-------------- tst/net/PacketTest.cpp | 78 +++++++++++++----- tst/net/PacketTest.hpp | 4 +- 16 files changed, 540 insertions(+), 241 deletions(-) create mode 100644 src/client/NetworkedInput.hpp diff --git a/doc/protocol b/doc/protocol index 4dc245f..a707d25 100644 --- a/doc/protocol +++ b/doc/protocol @@ -90,8 +90,13 @@ Sent by clients to notify the server of their changes to the player. Code: 4 Payload: - 0 entity state of the player as seen by the client -Length: 64 + 0 player's entity state as predicted by the client + 64 movement input, 3x 16bit signed int, each component mapped from [-1,1] to [-32767,32767] + 70 pitch input, 16bit signed int, mapped from [-PI/2,PI/2] to [-32767,32767] + 72 yaw input, 16bit signed int, mapped from [-PI,PI] to [-32767,32767] + 74 active actions, 8bit bit field, first three bits are primary, secondary, and tertiary + 75 selected inventory slot, 8bit unsigned int +Length: 76 Spawn Entity @@ -134,7 +139,7 @@ Payload: 4 entity ID, 32bit unsigned int 8 entity state 72 next entity... -Length: 4 + multiple of 68, max 452 +Length: 4 + multiple of 68, max 480 Player Correction diff --git a/src/client/Client.hpp b/src/client/Client.hpp index fb7eab1..a5f85e5 100644 --- a/src/client/Client.hpp +++ b/src/client/Client.hpp @@ -30,7 +30,13 @@ public: std::uint16_t SendPing(); std::uint16_t SendLogin(const std::string &); std::uint16_t SendPart(); - std::uint16_t SendPlayerUpdate(const Entity &); + std::uint16_t SendPlayerUpdate( + const EntityState &prediction, + const glm::vec3 &movement, + float pitch, + float yaw, + std::uint8_t actions, + std::uint8_t slot); private: void HandlePacket(const UDPpacket &); diff --git a/src/client/InteractiveState.hpp b/src/client/InteractiveState.hpp index 1b9c9a5..1129e99 100644 --- a/src/client/InteractiveState.hpp +++ b/src/client/InteractiveState.hpp @@ -6,11 +6,11 @@ #include "ChunkReceiver.hpp" #include "ChunkRequester.hpp" +#include "NetworkedInput.hpp" #include "../app/IntervalTimer.hpp" #include "../graphics/SkyBox.hpp" #include "../io/WorldSave.hpp" #include "../model/Skeletons.hpp" -#include "../ui/DirectInput.hpp" #include "../ui/HUD.hpp" #include "../ui/InteractiveManipulator.hpp" #include "../ui/Interface.hpp" @@ -20,8 +20,6 @@ #include "../world/Player.hpp" #include "../world/World.hpp" -#include - namespace blank { @@ -49,7 +47,6 @@ public: void Update(int dt) override; void Render(Viewport &) override; - void PushPlayerUpdate(const Entity &, int dt); void MergePlayerCorrection(std::uint16_t, const EntityState &); void SetAudio(bool) override; @@ -66,7 +63,7 @@ private: Player &player; HUD hud; InteractiveManipulator manip; - DirectInput input; + NetworkedInput input; Interface interface; ChunkRequester chunk_requester; ChunkReceiver chunk_receiver; @@ -76,15 +73,6 @@ private: SkyBox sky; - struct PlayerHistory { - EntityState state; - int delta_t; - std::uint16_t packet; - PlayerHistory(EntityState s, int dt, std::uint16_t p) - : state(s), delta_t(dt), packet(p) { } - }; - std::list player_hist; - }; } diff --git a/src/client/NetworkedInput.hpp b/src/client/NetworkedInput.hpp new file mode 100644 index 0000000..462b6a7 --- /dev/null +++ b/src/client/NetworkedInput.hpp @@ -0,0 +1,53 @@ +#ifndef BLANK_CLIENT_NETWORKEDINPUT_HPP_ +#define BLANK_CLIENT_NETWORKEDINPUT_HPP_ + +#include "../ui/PlayerController.hpp" + +#include "../world/EntityState.hpp" + +#include +#include + + +namespace blank { +namespace client { + +class Client; + +class NetworkedInput +: public PlayerController { + +public: + explicit NetworkedInput(World &, Player &, Client &); + + void Update(int dt); + void PushPlayerUpdate(int dt); + void MergePlayerCorrection(std::uint16_t, const EntityState &); + + void StartPrimaryAction() override; + void StopPrimaryAction() override; + void StartSecondaryAction() override; + void StopSecondaryAction() override; + void StartTertiaryAction() override; + void StopTertiaryAction() override; + +private: + Client &client; + + struct PlayerHistory { + EntityState state; + int delta_t; + std::uint16_t packet; + PlayerHistory(EntityState s, int dt, std::uint16_t p) + : state(s), delta_t(dt), packet(p) { } + }; + std::list player_hist; + + std::uint8_t actions; + +}; + +} +} + +#endif diff --git a/src/client/client.cpp b/src/client/client.cpp index cfc89cd..524454b 100644 --- a/src/client/client.cpp +++ b/src/client/client.cpp @@ -111,7 +111,7 @@ InteractiveState::InteractiveState(MasterState &master, uint32_t player_id) , player(*world.AddPlayer(master.GetConfig().player.name)) , hud(master.GetEnv(), master.GetConfig(), player) , manip(master.GetEnv(), player.GetEntity()) -, input(world, player, manip) +, input(world, player, master.GetClient()) , interface(master.GetConfig(), master.GetEnv().keymap, input, *this) // TODO: looks like chunk requester and receiver can and should be merged , chunk_requester(world.Chunks(), save) @@ -119,8 +119,7 @@ InteractiveState::InteractiveState(MasterState &master, uint32_t player_id) , chunk_renderer(player.GetChunks()) , skeletons() , loop_timer(16) -, sky(master.GetEnv().loader.LoadCubeMap("skybox")) -, player_hist() { +, sky(master.GetEnv().loader.LoadCubeMap("skybox")) { if (!save.Exists()) { save.Write(master.GetWorldConf()); } @@ -190,7 +189,7 @@ void InteractiveState::Update(int dt) { chunk_renderer.Update(dt); if (world_dt > 0) { - PushPlayerUpdate(player.GetEntity(), world_dt); + input.PushPlayerUpdate(world_dt); } glm::mat4 trans = player.GetEntity().Transform(player.GetEntity().ChunkCoords()); @@ -201,83 +200,6 @@ void InteractiveState::Update(int dt) { master.GetEnv().audio.Orientation(dir, up); } -void InteractiveState::PushPlayerUpdate(const Entity &player, int dt) { - std::uint16_t packet = master.GetClient().SendPlayerUpdate(player); - if (player_hist.size() < 16) { - player_hist.emplace_back(player.GetState(), dt, packet); - } else { - auto entry = player_hist.begin(); - entry->state = player.GetState(); - entry->delta_t = dt; - entry->packet = packet; - player_hist.splice(player_hist.end(), player_hist, entry); - } -} - -void InteractiveState::MergePlayerCorrection(uint16_t seq, const EntityState &corrected_state) { - if (player_hist.empty()) return; - - auto entry = player_hist.begin(); - auto end = player_hist.end(); - - // we may have received an older packet - int pack_diff = int16_t(seq) - int16_t(entry->packet); - if (pack_diff < 0) { - // indeed we have, just ignore it - return; - } - - // drop anything older than the fix - while (entry != end) { - pack_diff = int16_t(seq) - int16_t(entry->packet); - if (pack_diff > 0) { - entry = player_hist.erase(entry); - } else { - break; - } - } - - EntityState replay_state(corrected_state); - EntityState &player_state = player.GetEntity().GetState(); - - if (entry != end) { - entry->state.chunk_pos = replay_state.chunk_pos; - entry->state.block_pos = replay_state.block_pos; - ++entry; - } - - while (entry != end) { - replay_state.velocity = entry->state.velocity; - replay_state.Update(entry->delta_t); - entry->state.chunk_pos = replay_state.chunk_pos; - entry->state.block_pos = replay_state.block_pos; - ++entry; - } - - glm::vec3 displacement(replay_state.Diff(player_state)); - const float disp_squared = dot(displacement, displacement); - - if (disp_squared < 16.0f * numeric_limits::epsilon()) { - return; - } - - // if offset > 10cm, warp the player - // otherwise, move at most 1cm per frame towards - // the fixed position (160ms, so shouldn't be too noticeable) - constexpr float warp_thresh = 0.01f; // (1/10)^2 - constexpr float max_disp = 0.0001f; // (1/100)^2 - - if (disp_squared > warp_thresh) { - player_state.chunk_pos = replay_state.chunk_pos; - player_state.block_pos = replay_state.block_pos; - } else if (disp_squared < max_disp) { - player_state.block_pos += displacement; - } else { - displacement *= 0.01f / sqrt(disp_squared); - player_state.block_pos += displacement; - } -} - void InteractiveState::Render(Viewport &viewport) { viewport.WorldPosition(player.GetEntity().Transform(player.GetEntity().ChunkCoords())); if (master.GetConfig().video.world) { @@ -288,6 +210,10 @@ void InteractiveState::Render(Viewport &viewport) { hud.Render(viewport); } +void InteractiveState::MergePlayerCorrection(std::uint16_t pack, const EntityState &state) { + input.MergePlayerCorrection(pack, state); +} + void InteractiveState::SetAudio(bool b) { master.GetConfig().audio.enabled = b; if (b) { diff --git a/src/client/net.cpp b/src/client/net.cpp index a5feaf5..6e8d00c 100644 --- a/src/client/net.cpp +++ b/src/client/net.cpp @@ -1,11 +1,13 @@ #include "ChunkReceiver.hpp" #include "ChunkTransmission.hpp" #include "Client.hpp" +#include "NetworkedInput.hpp" #include "../app/init.hpp" #include "../net/Packet.hpp" #include "../world/Chunk.hpp" #include "../world/ChunkStore.hpp" +#include "../world/Player.hpp" #include #include @@ -236,9 +238,21 @@ uint16_t Client::SendLogin(const string &name) { return conn.Send(client_pack, client_sock); } -uint16_t Client::SendPlayerUpdate(const Entity &player) { +uint16_t Client::SendPlayerUpdate( + const EntityState &prediction, + const glm::vec3 &movement, + float pitch, + float yaw, + std::uint8_t actions, + std::uint8_t slot +) { auto pack = Packet::Make(client_pack); - pack.WritePlayer(player); + pack.WritePredictedState(prediction); + pack.WriteMovement(movement); + pack.WritePitch(pitch); + pack.WriteYaw(yaw); + pack.WriteActions(actions); + pack.WriteSlot(slot); return conn.Send(client_pack, client_sock); } @@ -247,5 +261,129 @@ uint16_t Client::SendPart() { return conn.Send(client_pack, client_sock); } + +NetworkedInput::NetworkedInput(World &world, Player &player, Client &client) +: PlayerController(world, player) +, client(client) +, player_hist() +, actions(0) { + +} + +void NetworkedInput::Update(int dt) { + Invalidate(); + UpdatePlayer(); +} + +void NetworkedInput::PushPlayerUpdate(int dt) { + const EntityState &state = GetPlayer().GetEntity().GetState(); + + std::uint16_t packet = client.SendPlayerUpdate( + state, + GetMovement(), + GetPitch(), + GetYaw(), + actions, + InventorySlot() + ); + if (player_hist.size() < 16) { + player_hist.emplace_back(state, dt, packet); + } else { + auto entry = player_hist.begin(); + entry->state = state; + entry->delta_t = dt; + entry->packet = packet; + player_hist.splice(player_hist.end(), player_hist, entry); + } +} + +void NetworkedInput::MergePlayerCorrection(uint16_t seq, const EntityState &corrected_state) { + if (player_hist.empty()) return; + + auto entry = player_hist.begin(); + auto end = player_hist.end(); + + // we may have received an older packet + int pack_diff = int16_t(seq) - int16_t(entry->packet); + if (pack_diff < 0) { + // indeed we have, just ignore it + return; + } + + // drop anything older than the fix + while (entry != end) { + pack_diff = int16_t(seq) - int16_t(entry->packet); + if (pack_diff > 0) { + entry = player_hist.erase(entry); + } else { + break; + } + } + + EntityState replay_state(corrected_state); + EntityState &player_state = GetPlayer().GetEntity().GetState(); + + if (entry != end) { + entry->state.chunk_pos = replay_state.chunk_pos; + entry->state.block_pos = replay_state.block_pos; + ++entry; + } + + while (entry != end) { + replay_state.velocity = entry->state.velocity; + replay_state.Update(entry->delta_t); + entry->state.chunk_pos = replay_state.chunk_pos; + entry->state.block_pos = replay_state.block_pos; + ++entry; + } + + glm::vec3 displacement(replay_state.Diff(player_state)); + const float disp_squared = dot(displacement, displacement); + + if (disp_squared < 16.0f * numeric_limits::epsilon()) { + return; + } + + // if offset > 10cm, warp the player + // otherwise, move at most 1cm per frame towards + // the fixed position (160ms, so shouldn't be too noticeable) + constexpr float warp_thresh = 0.01f; // (1/10)^2 + constexpr float max_disp = 0.0001f; // (1/100)^2 + + if (disp_squared > warp_thresh) { + player_state.chunk_pos = replay_state.chunk_pos; + player_state.block_pos = replay_state.block_pos; + } else if (disp_squared < max_disp) { + player_state.block_pos += displacement; + } else { + displacement *= 0.01f / sqrt(disp_squared); + player_state.block_pos += displacement; + } +} + +void NetworkedInput::StartPrimaryAction() { + actions |= 0x01; +} + +void NetworkedInput::StopPrimaryAction() { + actions &= ~0x01; +} + +void NetworkedInput::StartSecondaryAction() { + actions |= 0x02; +} + +void NetworkedInput::StopSecondaryAction() { + actions &= ~0x02; +} + +void NetworkedInput::StartTertiaryAction() { + actions |= 0x04; +} + +void NetworkedInput::StopTertiaryAction() { + actions &= ~0x04; +} + } } diff --git a/src/net/Packet.hpp b/src/net/Packet.hpp index e32621c..b32b42b 100644 --- a/src/net/Packet.hpp +++ b/src/net/Packet.hpp @@ -107,10 +107,20 @@ struct Packet { struct PlayerUpdate : public Payload { static constexpr std::uint8_t TYPE = 4; - static constexpr std::size_t MAX_LEN = 64; - - void WritePlayer(const Entity &) noexcept; - void ReadPlayerState(EntityState &) const noexcept; + static constexpr std::size_t MAX_LEN = 76; + + void WritePredictedState(const EntityState &) noexcept; + void ReadPredictedState(EntityState &) const noexcept; + void WriteMovement(const glm::vec3 &) noexcept; + void ReadMovement(glm::vec3 &) const noexcept; + void WritePitch(float) noexcept; + void ReadPitch(float &) const noexcept; + void WriteYaw(float) noexcept; + void ReadYaw(float &) const noexcept; + void WriteActions(std::uint8_t) noexcept; + void ReadActions(std::uint8_t &) const noexcept; + void WriteSlot(std::uint8_t) noexcept; + void ReadSlot(std::uint8_t &) const noexcept; }; struct SpawnEntity : public Payload { @@ -133,7 +143,7 @@ struct Packet { struct EntityUpdate : public Payload { static constexpr std::uint8_t TYPE = 7; - static constexpr std::size_t MAX_LEN = 452; + static constexpr std::size_t MAX_LEN = 480; static constexpr std::uint32_t MAX_ENTITIES = 7; static constexpr std::size_t GetSize(std::uint32_t num) noexcept { diff --git a/src/net/net.cpp b/src/net/net.cpp index fa9c381..e766c76 100644 --- a/src/net/net.cpp +++ b/src/net/net.cpp @@ -284,14 +284,67 @@ void Packet::Join::ReadWorldName(string &name) const noexcept { ReadString(name, 68, 32); } -void Packet::PlayerUpdate::WritePlayer(const Entity &player) noexcept { - Write(player.GetState(), 0); +void Packet::PlayerUpdate::WritePredictedState(const EntityState &state) noexcept { + Write(state, 0); } -void Packet::PlayerUpdate::ReadPlayerState(EntityState &state) const noexcept { +void Packet::PlayerUpdate::ReadPredictedState(EntityState &state) const noexcept { Read(state, 0); } +void Packet::PlayerUpdate::WriteMovement(const glm::vec3 &mov) noexcept { + glm::ivec3 conv = clamp(glm::ivec3(mov * 32767.0f), -32767, 32767); + Write(int16_t(conv.x), 64); + Write(int16_t(conv.y), 66); + Write(int16_t(conv.z), 68); +} + +void Packet::PlayerUpdate::ReadMovement(glm::vec3 &mov) const noexcept { + int16_t x, y, z; + Read(x, 64); + Read(y, 66); + Read(z, 68); + mov = glm::vec3(x, y, z) * .00003051850947599719f; +} + +void Packet::PlayerUpdate::WritePitch(float pitch) noexcept { + int16_t conv = pitch * 20860.12008116853786870640f; + Write(conv, 70); +} + +void Packet::PlayerUpdate::ReadPitch(float &pitch) const noexcept { + int16_t conv = 0; + Read(conv, 70); + pitch = conv * .00004793836258415163f; +} + +void Packet::PlayerUpdate::WriteYaw(float yaw) noexcept { + int16_t conv = yaw * 10430.06004058426893435320f; + Write(conv, 72); +} + +void Packet::PlayerUpdate::ReadYaw(float &yaw) const noexcept { + int16_t conv = 0; + Read(conv, 72); + yaw = conv * .00009587672516830326f; +} + +void Packet::PlayerUpdate::WriteActions(uint8_t actions) noexcept { + Write(actions, 74); +} + +void Packet::PlayerUpdate::ReadActions(uint8_t &actions) const noexcept { + Read(actions, 74); +} + +void Packet::PlayerUpdate::WriteSlot(uint8_t slot) noexcept { + Write(slot, 75); +} + +void Packet::PlayerUpdate::ReadSlot(uint8_t &slot) const noexcept { + Read(slot, 75); +} + void Packet::SpawnEntity::WriteEntity(const Entity &e) noexcept { Write(e.ID(), 0); if (e.GetModel()) { @@ -351,19 +404,19 @@ void Packet::EntityUpdate::ReadEntityCount(uint32_t &count) const noexcept { } void Packet::EntityUpdate::WriteEntity(const Entity &entity, uint32_t num) noexcept { - uint32_t off = GetSize(num);; + uint32_t off = GetSize(num); Write(entity.ID(), off); Write(entity.GetState(), off + 4); } void Packet::EntityUpdate::ReadEntityID(uint32_t &id, uint32_t num) const noexcept { - uint32_t off = GetSize(num);; + uint32_t off = GetSize(num); Read(id, off); } void Packet::EntityUpdate::ReadEntityState(EntityState &state, uint32_t num) const noexcept { - uint32_t off = GetSize(num);; + uint32_t off = GetSize(num); Read(state, off + 4); } diff --git a/src/server/ClientConnection.hpp b/src/server/ClientConnection.hpp index 2089dd1..4927d1c 100644 --- a/src/server/ClientConnection.hpp +++ b/src/server/ClientConnection.hpp @@ -4,6 +4,7 @@ #include "ChunkTransmitter.hpp" #include "Server.hpp" #include "../app/IntervalTimer.hpp" +#include "../ui/DirectInput.hpp" #include "../net/Connection.hpp" #include "../net/ConnectionHandler.hpp" #include "../world/EntityState.hpp" @@ -11,6 +12,7 @@ #include #include +#include #include #include @@ -49,11 +51,11 @@ public: void AttachPlayer(Player &); void DetachPlayer(); - bool HasPlayer() const noexcept { return player; } - Entity &PlayerEntity() noexcept { return player->GetEntity(); } - const Entity &PlayerEntity() const noexcept { return player->GetEntity(); } - ChunkIndex &PlayerChunks() noexcept { return player->GetChunks(); } - const ChunkIndex &PlayerChunks() const noexcept { return player->GetChunks(); } + bool HasPlayer() const noexcept { return !!input; } + Entity &PlayerEntity() noexcept { return input->GetPlayer().GetEntity(); } + const Entity &PlayerEntity() const noexcept { return input->GetPlayer().GetEntity(); } + ChunkIndex &PlayerChunks() noexcept { return input->GetPlayer().GetChunks(); } + const ChunkIndex &PlayerChunks() const noexcept { return input->GetPlayer().GetChunks(); } void SetPlayerModel(const CompositeModel &) noexcept; bool HasPlayerModel() const noexcept; @@ -95,7 +97,7 @@ private: private: Server &server; Connection conn; - Player *player; + std::unique_ptr input; const CompositeModel *player_model; std::list spawns; unsigned int confirm_wait; @@ -105,6 +107,7 @@ private: EntityState player_update_state; std::uint16_t player_update_pack; IntervalTimer player_update_timer; + std::uint8_t old_actions; ChunkTransmitter transmitter; std::deque chunk_queue; diff --git a/src/server/Server.hpp b/src/server/Server.hpp index cf4fc04..1a4c0b4 100644 --- a/src/server/Server.hpp +++ b/src/server/Server.hpp @@ -2,6 +2,7 @@ #define BLANK_SERVER_SERVER_HPP #include "../app/Config.hpp" +#include "../world/WorldManipulator.hpp" #include #include @@ -16,7 +17,8 @@ namespace server { class ClientConnection; -class Server { +class Server +: public WorldManipulator { public: Server(const Config::Network &, World &); @@ -35,6 +37,8 @@ public: bool HasPlayerModel() const noexcept; const CompositeModel &GetPlayerModel() const noexcept; + void SetBlock(Chunk &, int, const Block &) override; + private: void HandlePacket(const UDPpacket &); diff --git a/src/server/net.cpp b/src/server/net.cpp index 44a4268..ea5b4d9 100644 --- a/src/server/net.cpp +++ b/src/server/net.cpp @@ -10,6 +10,7 @@ #include #include +#include using namespace std; @@ -172,7 +173,7 @@ void ChunkTransmitter::Release() { ClientConnection::ClientConnection(Server &server, const IPaddress &addr) : server(server) , conn(addr) -, player(nullptr) +, input() , player_model(nullptr) , spawns() , confirm_wait(0) @@ -180,6 +181,7 @@ ClientConnection::ClientConnection(Server &server, const IPaddress &addr) , player_update_state() , player_update_pack(0) , player_update_timer(1500) +, old_actions(0) , transmitter(*this) , chunk_queue() , old_base() { @@ -243,6 +245,7 @@ void ClientConnection::Update(int dt) { } SendUpdates(); + input->Update(dt); CheckPlayerFix(); CheckChunkQueue(); } @@ -387,9 +390,9 @@ void ClientConnection::CheckChunkQueue() { } } -void ClientConnection::AttachPlayer(Player &new_player) { +void ClientConnection::AttachPlayer(Player &player) { DetachPlayer(); - player = &new_player; + input.reset(new DirectInput(server.GetWorld(), player, server)); PlayerEntity().Ref(); old_base = PlayerChunks().Base(); @@ -406,17 +409,18 @@ void ClientConnection::AttachPlayer(Player &new_player) { GetPlayerModel().Instantiate(PlayerEntity().GetModel()); } - cout << "player \"" << player->Name() << "\" joined" << endl; + cout << "player \"" << player.Name() << "\" joined" << endl; } void ClientConnection::DetachPlayer() { if (!HasPlayer()) return; - cout << "player \"" << player->Name() << "\" left" << endl; - player->GetEntity().Kill(); - player->GetEntity().UnRef(); - player = nullptr; + cout << "player \"" << input->GetPlayer().Name() << "\" left" << endl; + PlayerEntity().Kill(); + PlayerEntity().UnRef(); + input.reset(); transmitter.Abort(); chunk_queue.clear(); + old_actions = 0; } void ClientConnection::SetPlayerModel(const CompositeModel &m) noexcept { @@ -511,13 +515,44 @@ void ClientConnection::On(const Packet::PlayerUpdate &pack) { int pack_diff = int16_t(pack.Seq()) - int16_t(player_update_pack); bool overdue = player_update_timer.HitOnce(); player_update_timer.Reset(); - if (pack_diff > 0 || overdue) { - player_update_pack = pack.Seq(); - pack.ReadPlayerState(player_update_state); - // accept velocity and orientation as "user input" - PlayerEntity().Velocity(player_update_state.velocity); - PlayerEntity().Orientation(player_update_state.orient); + if (pack_diff <= 0 && !overdue) { + // drop old packets if we have a fairly recent state + return; + } + glm::vec3 movement(0.0f); + float pitch = 0.0f; + float yaw = 0.0f; + uint8_t new_actions; + uint8_t slot; + + player_update_pack = pack.Seq(); + pack.ReadPredictedState(player_update_state); + pack.ReadMovement(movement); + pack.ReadPitch(pitch); + pack.ReadYaw(yaw); + pack.ReadActions(new_actions); + pack.ReadSlot(slot); + + input->SetMovement(movement); + input->TurnHead(pitch - input->GetPitch(), yaw - input->GetYaw()); + input->SelectInventory(slot); + + if ((new_actions & 0x01) && !(old_actions & 0x01)) { + input->StartPrimaryAction(); + } else if (!(new_actions & 0x01) && (old_actions & 0x01)) { + input->StopPrimaryAction(); + } + if ((new_actions & 0x02) && !(old_actions & 0x02)) { + input->StartSecondaryAction(); + } else if (!(new_actions & 0x02) && (old_actions & 0x02)) { + input->StopSecondaryAction(); + } + if ((new_actions & 0x04) && !(old_actions & 0x04)) { + input->StartTertiaryAction(); + } else if (!(new_actions & 0x04) && (old_actions & 0x04)) { + input->StopTertiaryAction(); } + old_actions = new_actions; } @@ -608,5 +643,11 @@ const CompositeModel &Server::GetPlayerModel() const noexcept { return *player_model; } +void Server::SetBlock(Chunk &chunk, int index, const Block &block) { + chunk.SetBlock(index, block); + // TODO: send to clients + // also TODO: batch chunk changes +} + } } diff --git a/src/ui/DirectInput.hpp b/src/ui/DirectInput.hpp index d4a2e96..142da66 100644 --- a/src/ui/DirectInput.hpp +++ b/src/ui/DirectInput.hpp @@ -4,8 +4,6 @@ #include "PlayerController.hpp" #include "../app/IntervalTimer.hpp" -#include "../world/EntityCollision.hpp" -#include "../world/WorldCollision.hpp" namespace blank { @@ -20,41 +18,23 @@ class DirectInput public: DirectInput(World &, Player &, WorldManipulator &); - const WorldCollision &BlockFocus() const noexcept { return aim_world; } - const EntityCollision &EntityFocus() const noexcept { return aim_entity; } - void Update(int dt); - void SetMovement(const glm::vec3 &) override; - void TurnHead(float pitch, float yaw) override; void StartPrimaryAction() override; void StopPrimaryAction() override; void StartSecondaryAction() override; void StopSecondaryAction() override; void StartTertiaryAction() override; void StopTertiaryAction() override; - void SelectInventory(int) override; private: - void UpdatePlayer(); - void PickBlock(); void PlaceBlock(); void RemoveBlock(); private: - World &world; - Player &player; WorldManipulator &manip; - WorldCollision aim_world; - EntityCollision aim_entity; - - glm::vec3 move_dir; - float pitch; - float yaw; - bool dirty; - IntervalTimer place_timer; IntervalTimer remove_timer; diff --git a/src/ui/PlayerController.hpp b/src/ui/PlayerController.hpp index 8d9e5a3..b29795b 100644 --- a/src/ui/PlayerController.hpp +++ b/src/ui/PlayerController.hpp @@ -3,16 +3,39 @@ #include +#include "../world/EntityCollision.hpp" +#include "../world/WorldCollision.hpp" + namespace blank { -struct PlayerController { +class Player; +class World; + +class PlayerController { + +public: + PlayerController(World &, Player &); + + Player &GetPlayer() noexcept { return player; } + const Player &GetPlayer() const noexcept { return player; } + + WorldCollision &BlockFocus() noexcept { return aim_world; } + const WorldCollision &BlockFocus() const noexcept { return aim_world; } + EntityCollision &EntityFocus() noexcept { return aim_entity; } + const EntityCollision &EntityFocus() const noexcept { return aim_entity; } /// set desired direction of movement /// the magnitude (clamped to [0..1]) can be used to attenuate target velocity - virtual void SetMovement(const glm::vec3 &) = 0; + void SetMovement(const glm::vec3 &) noexcept; + const glm::vec3 &GetMovement() const noexcept { return move_dir; } /// turn the controlled entity's head by given pitch and yaw deltas - virtual void TurnHead(float pitch, float yaw) = 0; + void TurnHead(float pitch, float yaw) noexcept; + + /// get player pitch in radians, normalized to [-PI/2,PI/2] + float GetPitch() const noexcept { return pitch; } + /// get player yaw in radians, normalized to [-PI,PI] + float GetYaw() const noexcept { return yaw; } /// start doing primary action /// what exactly this means depends on the active item @@ -26,7 +49,23 @@ struct PlayerController { virtual void StopTertiaryAction() = 0; /// set the item at given inventory slot as active - virtual void SelectInventory(int) = 0; + void SelectInventory(int) noexcept; + int InventorySlot() const noexcept; + +protected: + void Invalidate() noexcept; + void UpdatePlayer() noexcept; + +private: + World &world; + Player &player; + glm::vec3 move_dir; + float pitch; + float yaw; + bool dirty; + + WorldCollision aim_world; + EntityCollision aim_entity; }; diff --git a/src/ui/ui.cpp b/src/ui/ui.cpp index 75129c1..4dd9711 100644 --- a/src/ui/ui.cpp +++ b/src/ui/ui.cpp @@ -32,46 +32,28 @@ namespace blank { -DirectInput::DirectInput(World &world, Player &player, WorldManipulator &manip) +PlayerController::PlayerController(World &world, Player &player) : world(world) , player(player) -, manip(manip) -, aim_world() -, aim_entity() , move_dir(0.0f) , pitch(0.0f) , yaw(0.0f) , dirty(true) -, place_timer(256) -, remove_timer(256) { - -} - -void DirectInput::Update(int dt) { - dirty = true; // world has changed in the meantime - UpdatePlayer(); - - remove_timer.Update(dt); - if (remove_timer.Hit()) { - RemoveBlock(); - } +, aim_world() +, aim_entity() { - place_timer.Update(dt); - if (place_timer.Hit()) { - PlaceBlock(); - } } -void DirectInput::SetMovement(const glm::vec3 &m) { +void PlayerController::SetMovement(const glm::vec3 &m) noexcept { if (dot(m, m) > 1.0f) { move_dir = normalize(m); } else { move_dir = m; } - dirty = true; + Invalidate(); } -void DirectInput::TurnHead(float dp, float dy) { +void PlayerController::TurnHead(float dp, float dy) noexcept { pitch += dp; if (pitch > PI / 2) { pitch = PI / 2; @@ -84,9 +66,70 @@ void DirectInput::TurnHead(float dp, float dy) { } else if (yaw < -PI) { yaw += PI * 2; } + Invalidate(); +} + +void PlayerController::SelectInventory(int i) noexcept { + player.SetInventorySlot(i); +} + +int PlayerController::InventorySlot() const noexcept { + return player.GetInventorySlot(); +} + +void PlayerController::Invalidate() noexcept { dirty = true; } +void PlayerController::UpdatePlayer() noexcept { + constexpr float max_vel = 0.005f; + if (dirty) { + player.GetEntity().Orientation(glm::quat(glm::vec3(pitch, yaw, 0.0f))); + player.GetEntity().Velocity(glm::rotateY(move_dir * max_vel, yaw)); + + Ray aim = player.Aim(); + if (!world.Intersection(aim, glm::mat4(1.0f), player.GetEntity().ChunkCoords(), aim_world)) { + aim_world = WorldCollision(); + } + if (!world.Intersection(aim, glm::mat4(1.0f), player.GetEntity(), aim_entity)) { + aim_entity = EntityCollision(); + } + if (aim_world && aim_entity) { + // got both, pick the closest one + if (aim_world.depth < aim_entity.depth) { + aim_entity = EntityCollision(); + } else { + aim_world = WorldCollision(); + } + } + dirty = false; + } +} + + +DirectInput::DirectInput(World &world, Player &player, WorldManipulator &manip) +: PlayerController(world, player) +, manip(manip) +, place_timer(256) +, remove_timer(256) { + +} + +void DirectInput::Update(int dt) { + Invalidate(); // world has changed in the meantime + UpdatePlayer(); + + remove_timer.Update(dt); + if (remove_timer.Hit()) { + RemoveBlock(); + } + + place_timer.Update(dt); + if (place_timer.Hit()) { + PlaceBlock(); + } +} + void DirectInput::StartPrimaryAction() { if (!remove_timer.Running()) { RemoveBlock(); @@ -117,59 +160,29 @@ void DirectInput::StopTertiaryAction() { // nothing } -void DirectInput::SelectInventory(int i) { - player.SetInventorySlot(i); -} - -void DirectInput::UpdatePlayer() { - constexpr float max_vel = 0.005f; - if (dirty) { - player.GetEntity().Orientation(glm::quat(glm::vec3(pitch, yaw, 0.0f))); - player.GetEntity().Velocity(glm::rotateY(move_dir * max_vel, yaw)); - - Ray aim = player.Aim(); - if (!world.Intersection(aim, glm::mat4(1.0f), player.GetEntity().ChunkCoords(), aim_world)) { - aim_world = WorldCollision(); - } - if (!world.Intersection(aim, glm::mat4(1.0f), player.GetEntity(), aim_entity)) { - aim_entity = EntityCollision(); - } - if (aim_world && aim_entity) { - // got both, pick the closest one - if (aim_world.depth < aim_entity.depth) { - aim_entity = EntityCollision(); - } else { - aim_world = WorldCollision(); - } - } - // TODO: update outline if applicable - dirty = false; - } -} - void DirectInput::PickBlock() { UpdatePlayer(); - if (!aim_world) return; - player.SetInventorySlot(aim_world.GetBlock().type - 1); + if (!BlockFocus()) return; + SelectInventory(BlockFocus().GetBlock().type - 1); } void DirectInput::PlaceBlock() { UpdatePlayer(); - if (!aim_world) return; + if (!BlockFocus()) return; - BlockLookup next_block(aim_world.chunk, aim_world.BlockPos(), Block::NormalFace(aim_world.normal)); + BlockLookup next_block(BlockFocus().chunk, BlockFocus().BlockPos(), Block::NormalFace(BlockFocus().normal)); if (!next_block) { return; } - manip.SetBlock(next_block.GetChunk(), next_block.GetBlockIndex(), Block(player.GetInventorySlot() + 1)); - dirty = true; + manip.SetBlock(next_block.GetChunk(), next_block.GetBlockIndex(), Block(InventorySlot() + 1)); + Invalidate(); } void DirectInput::RemoveBlock() { UpdatePlayer(); - if (!aim_world) return; - manip.SetBlock(aim_world.GetChunk(), aim_world.block, Block(0)); - dirty = true; + if (!BlockFocus()) return; + manip.SetBlock(BlockFocus().GetChunk(), BlockFocus().block, Block(0)); + Invalidate(); } diff --git a/tst/net/PacketTest.cpp b/tst/net/PacketTest.cpp index 3f47d0f..42d8348 100644 --- a/tst/net/PacketTest.cpp +++ b/tst/net/PacketTest.cpp @@ -3,8 +3,6 @@ #include "model/CompositeModel.hpp" #include "world/Entity.hpp" -#include - CPPUNIT_TEST_SUITE_REGISTRATION(blank::test::PacketTest); using namespace std; @@ -149,22 +147,61 @@ void PacketTest::testPart() { void PacketTest::testPlayerUpdate() { auto pack = Packet::Make(udp_pack); - AssertPacket("PlayerUpdate", 4, 64, pack); + AssertPacket("PlayerUpdate", 4, 76, pack); + + EntityState write_state; + write_state.chunk_pos = { 7, 2, -3 }; + write_state.block_pos = { 1.5f, 0.9f, 12.0f }; + write_state.velocity = { 0.025f, 0.001f, 0.0f }; + write_state.orient = { 1.0f, 0.0f, 0.0f, 0.0f }; + write_state.ang_vel = { 0.01f, 0.00302f, 0.0985f }; + glm::vec3 write_movement(0.5f, -1.0f, 1.0f); + float write_pitch = 1.25f; + float write_yaw = -2.5f; + uint8_t write_actions = 0x05; + uint8_t write_slot = 3; + pack.WritePredictedState(write_state); + pack.WriteMovement(write_movement); + pack.WritePitch(write_pitch); + pack.WriteYaw(write_yaw); + pack.WriteActions(write_actions); + pack.WriteSlot(write_slot); - Entity write_entity; - write_entity.ID(534574); - write_entity.GetState().chunk_pos = { 7, 2, -3 }; - write_entity.GetState().block_pos = { 1.5f, 0.9f, 12.0f }; - write_entity.GetState().velocity = { 0.025f, 0.001f, 0.0f }; - write_entity.GetState().orient = { 1.0f, 0.0f, 0.0f, 0.0f }; - write_entity.GetState().ang_vel = { 0.01f, 0.00302f, 0.0985f }; EntityState read_state; - pack.WritePlayer(write_entity); - - pack.ReadPlayerState(read_state); + glm::vec3 read_movement; + float read_pitch; + float read_yaw; + uint8_t read_actions; + uint8_t read_slot; + pack.ReadPredictedState(read_state); + pack.ReadMovement(read_movement); + pack.ReadPitch(read_pitch); + pack.ReadYaw(read_yaw); + pack.ReadActions(read_actions); + pack.ReadSlot(read_slot); AssertEqual( - "player entity state not correctly transported in PlayerUpdate packet", - write_entity.GetState(), read_state + "player predicted entity state not correctly transported in PlayerUpdate packet", + write_state, read_state + ); + AssertEqual( + "player movement input not correctly transported in PlayerUpdate packet", + write_movement, read_movement, 0.0001f + ); + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( + "player pitch input not correctly transported in PlayerUpdate packet", + write_pitch, read_pitch, 0.0001f + ); + CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( + "player yaw input not correctly transported in PlayerUpdate packet", + write_yaw, read_yaw, 0.0001f + ); + CPPUNIT_ASSERT_EQUAL_MESSAGE( + "player actions not correctly transported in PlayerUpdate packet", + int(write_actions), int(read_actions) + ); + CPPUNIT_ASSERT_EQUAL_MESSAGE( + "player inventory slot not correctly transported in PlayerUpdate packet", + int(write_slot), int(read_slot) ); } @@ -237,7 +274,7 @@ void PacketTest::testDespawnEntity() { void PacketTest::testEntityUpdate() { auto pack = Packet::Make(udp_pack); - AssertPacket("EntityUpdate", 7, 4, 452, pack); + AssertPacket("EntityUpdate", 7, 4, 480, pack); pack.length = Packet::EntityUpdate::GetSize(3); CPPUNIT_ASSERT_EQUAL_MESSAGE( @@ -501,19 +538,20 @@ void PacketTest::AssertEqual( void PacketTest::AssertEqual( const string &message, const glm::vec3 &expected, - const glm::vec3 &actual + const glm::vec3 &actual, + float epsilon ) { CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( message + " (X component)", - expected.x, actual.x, numeric_limits::epsilon() + expected.x, actual.x, epsilon ); CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( message + " (Y component)", - expected.y, actual.y, numeric_limits::epsilon() + expected.y, actual.y, epsilon ); CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE( message + " (Z component)", - expected.z, actual.z, numeric_limits::epsilon() + expected.z, actual.z, epsilon ); } diff --git a/tst/net/PacketTest.hpp b/tst/net/PacketTest.hpp index 119c696..89e15dd 100644 --- a/tst/net/PacketTest.hpp +++ b/tst/net/PacketTest.hpp @@ -6,6 +6,7 @@ #include "world/EntityState.hpp" #include +#include #include #include #include @@ -80,7 +81,8 @@ private: static void AssertEqual( const std::string &message, const glm::vec3 &expected, - const glm::vec3 &actual); + const glm::vec3 &actual, + float epsilon = std::numeric_limits::epsilon()); static void AssertEqual( const std::string &message, const glm::quat &expected, -- 2.39.2