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
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
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 &);
#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"
#include "../world/Player.hpp"
#include "../world/World.hpp"
-#include <list>
-
namespace blank {
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;
Player &player;
HUD hud;
InteractiveManipulator manip;
- DirectInput input;
+ NetworkedInput input;
Interface interface;
ChunkRequester chunk_requester;
ChunkReceiver chunk_receiver;
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<PlayerHistory> player_hist;
-
};
}
--- /dev/null
+#ifndef BLANK_CLIENT_NETWORKEDINPUT_HPP_
+#define BLANK_CLIENT_NETWORKEDINPUT_HPP_
+
+#include "../ui/PlayerController.hpp"
+
+#include "../world/EntityState.hpp"
+
+#include <cstdint>
+#include <list>
+
+
+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<PlayerHistory> player_hist;
+
+ std::uint8_t actions;
+
+};
+
+}
+}
+
+#endif
, 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)
, 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());
}
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());
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<float>::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) {
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) {
#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 <iostream>
#include <zlib.h>
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<Packet::PlayerUpdate>(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);
}
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<float>::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;
+}
+
}
}
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 {
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 {
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()) {
}
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);
}
#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"
#include <deque>
#include <list>
+#include <memory>
#include <SDL_net.h>
#include <vector>
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;
private:
Server &server;
Connection conn;
- Player *player;
+ std::unique_ptr<DirectInput> input;
const CompositeModel *player_model;
std::list<SpawnStatus> spawns;
unsigned int confirm_wait;
EntityState player_update_state;
std::uint16_t player_update_pack;
IntervalTimer player_update_timer;
+ std::uint8_t old_actions;
ChunkTransmitter transmitter;
std::deque<glm::ivec3> chunk_queue;
#define BLANK_SERVER_SERVER_HPP
#include "../app/Config.hpp"
+#include "../world/WorldManipulator.hpp"
#include <list>
#include <SDL_net.h>
class ClientConnection;
-class Server {
+class Server
+: public WorldManipulator {
public:
Server(const Config::Network &, World &);
bool HasPlayerModel() const noexcept;
const CompositeModel &GetPlayerModel() const noexcept;
+ void SetBlock(Chunk &, int, const Block &) override;
+
private:
void HandlePacket(const UDPpacket &);
#include <iostream>
#include <zlib.h>
+#include <glm/gtx/io.hpp>
using namespace std;
ClientConnection::ClientConnection(Server &server, const IPaddress &addr)
: server(server)
, conn(addr)
-, player(nullptr)
+, input()
, player_model(nullptr)
, spawns()
, confirm_wait(0)
, player_update_state()
, player_update_pack(0)
, player_update_timer(1500)
+, old_actions(0)
, transmitter(*this)
, chunk_queue()
, old_base() {
}
SendUpdates();
+ input->Update(dt);
CheckPlayerFix();
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();
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 {
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;
}
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
+}
+
}
}
#include "PlayerController.hpp"
#include "../app/IntervalTimer.hpp"
-#include "../world/EntityCollision.hpp"
-#include "../world/WorldCollision.hpp"
namespace blank {
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;
#include <glm/glm.hpp>
+#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
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;
};
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;
} 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();
// 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();
}
#include "model/CompositeModel.hpp"
#include "world/Entity.hpp"
-#include <limits>
-
CPPUNIT_TEST_SUITE_REGISTRATION(blank::test::PacketTest);
using namespace std;
void PacketTest::testPlayerUpdate() {
auto pack = Packet::Make<Packet::PlayerUpdate>(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)
);
}
void PacketTest::testEntityUpdate() {
auto pack = Packet::Make<Packet::EntityUpdate>(udp_pack);
- AssertPacket("EntityUpdate", 7, 4, 452, pack);
+ AssertPacket("EntityUpdate", 7, 4, 480, pack);
pack.length = Packet::EntityUpdate::GetSize(3);
CPPUNIT_ASSERT_EQUAL_MESSAGE(
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<float>::epsilon()
+ expected.x, actual.x, epsilon
);
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
message + " (Y component)",
- expected.y, actual.y, numeric_limits<float>::epsilon()
+ expected.y, actual.y, epsilon
);
CPPUNIT_ASSERT_DOUBLES_EQUAL_MESSAGE(
message + " (Z component)",
- expected.z, actual.z, numeric_limits<float>::epsilon()
+ expected.z, actual.z, epsilon
);
}
#include "world/EntityState.hpp"
#include <cstdint>
+#include <limits>
#include <string>
#include <SDL_net.h>
#include <glm/glm.hpp>
static void AssertEqual(
const std::string &message,
const glm::vec3 &expected,
- const glm::vec3 &actual);
+ const glm::vec3 &actual,
+ float epsilon = std::numeric_limits<float>::epsilon());
static void AssertEqual(
const std::string &message,
const glm::quat &expected,