aboutsummaryrefslogtreecommitdiff
path: root/src/client/entity
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/entity')
-rw-r--r--src/client/entity/animate.cc158
-rw-r--r--src/client/entity/animate.hh71
-rw-r--r--src/client/entity/entity.cc6
-rw-r--r--src/client/entity/entity.hh27
-rw-r--r--src/client/entity/moveable.cc86
-rw-r--r--src/client/entity/moveable.hh29
-rw-r--r--src/client/entity/player.cc54
-rw-r--r--src/client/entity/player.hh73
8 files changed, 504 insertions, 0 deletions
diff --git a/src/client/entity/animate.cc b/src/client/entity/animate.cc
new file mode 100644
index 0000000..9142b55
--- /dev/null
+++ b/src/client/entity/animate.cc
@@ -0,0 +1,158 @@
+#include "player.hh"
+
+namespace client {
+
+// We might start receiving data slower or faster. We compensate by adjusting
+// how fast time progresses on our client.;
+void animate::update_time_factor(const shared::tick_t& sequence,
+ const shared::tick_t& tick) noexcept {
+
+ // Check if we're older than the latest, don't update if so.
+ if (std::ranges::any_of(this->updates, [&](const auto& update) {
+ return update.from_server && update.tick_sequence >= sequence;
+ })) {
+ return;
+ }
+
+ // How many ticks in the future we prefer to be at. If the server is
+ // jittery, we would want this value to be greater, but this should be OK
+ // for most cases. A larger value decreases the max ping we can handle.
+ constexpr float TARGET_TICKS_AHEAD = 1.341519f;
+
+ if (const auto tick_dist = static_cast<std::int64_t>(tick) -
+ static_cast<std::int64_t>(state::tick);
+ tick_dist > 0) {
+
+ // We're behind, jump ahead. We only touch delta_ticks so extrapolate
+ // is called the correct number of times in client.cc.
+#ifndef NDEBUG
+ shared::print::debug << shared::print::time
+ << "client: time_factor jumped ahead, got tick "
+ << tick << ", was " << state::tick << " + "
+ << state::delta_ticks
+ << ", time_factor: " << state::time_factor << '\n';
+#endif
+ state::delta_ticks = static_cast<float>(tick_dist) + TARGET_TICKS_AHEAD;
+ state::time_factor = 1.0f;
+
+ return;
+ }
+
+ // Otherwise we try to move towards the value by adjusting how fast time
+ // progresses. This shouldn't be possible to notice, our constants should
+ // be adjusted if it is.
+ const float time_diff =
+ (static_cast<float>(tick) + TARGET_TICKS_AHEAD) -
+ (static_cast<float>(state::tick) + state::delta_ticks);
+
+ constexpr float MAX_TIME_FACTOR_DIFF = 0.1f;
+ constexpr float TIME_FACTOR_AGGRESSIVENESS = 0.25f;
+ state::time_factor =
+ std::clamp(1.0f + (time_diff * TIME_FACTOR_AGGRESSIVENESS),
+ 1.0f - MAX_TIME_FACTOR_DIFF, 1.0f + MAX_TIME_FACTOR_DIFF);
+}
+
+void animate::notify(const shared::animate& animate,
+ const shared::tick_t& tick_sequence,
+ const bool from_server) noexcept {
+ // If it's from the server we want to update previously emplaced predicted
+ // (and potentially predicted) values.
+ if (const auto it = std::ranges::find_if(this->updates,
+ [&](const auto& update) {
+ return update.tick_sequence ==
+ tick_sequence;
+ });
+ it != std::end(this->updates)) {
+
+ if (from_server) {
+ *it = animate_update{.tick_sequence = tick_sequence,
+ .animate = animate,
+ .from_server = from_server};
+ }
+
+ return;
+ }
+
+ this->updates.push_back({.tick_sequence = tick_sequence,
+ .animate = animate,
+ .from_server = from_server});
+
+ std::ranges::sort(this->updates, [](const auto& a, const auto& b) {
+ return a.tick_sequence < b.tick_sequence;
+ });
+}
+
+float animate::get_target_ticks_back() noexcept {
+ // The localplayer is interpolated via the latest tick.
+ const unsigned base_ticks_back = [&]() -> unsigned {
+ if (this->index == *state::localplayer_index) {
+ return 0u;
+ }
+ const float base = settings::get<float>({"engine", "interp"}, 0.25f);
+ const float ret = base * static_cast<float>(state::tickrate);
+ return std::clamp(static_cast<unsigned>(ret), 0u, state::tickrate);
+ }();
+
+ return state::delta_ticks + static_cast<float>(base_ticks_back);
+}
+
+void animate::interpolate() noexcept {
+ const float target_ticks_back = this->get_target_ticks_back();
+
+ const bool is_localplayer = this->index == *state::localplayer_index;
+ const auto b_it = [&, this]() {
+ if (is_localplayer) {
+ return std::rbegin(this->updates);
+ }
+ return std::ranges::find_if(
+ std::rbegin(this->updates), std::rend(this->updates),
+ [&](const auto& update) {
+ const unsigned target =
+ state::tick - static_cast<unsigned>(target_ticks_back);
+ return update.tick_sequence <= target;
+ });
+ }();
+ if (b_it == std::rend(this->updates)) {
+ return;
+ }
+
+ const auto a_it = std::next(b_it);
+ if (a_it == std::rend(this->updates)) {
+ return;
+ }
+
+ const glm::vec3 a_pos = a_it->animate.get_local_pos();
+ const glm::vec3 b_pos = shared::movement::make_relative(
+ a_it->animate.get_chunk_pos(), b_it->animate.get_local_pos(),
+ b_it->animate.get_chunk_pos());
+
+ const float b_interp = [&]() {
+ const float diff =
+ is_localplayer
+ ? 1.0f
+ : static_cast<float>(b_it->tick_sequence - a_it->tick_sequence);
+ const float base = std::fmod(target_ticks_back, 1.0f);
+ const float a_dist = diff - (1.0f - base);
+ return a_dist / diff;
+ }();
+ const float a_interp = (1.0f - b_interp);
+
+ const auto& a = a_it->animate;
+ const auto& b = b_it->animate;
+ this->local_pos = a_pos * a_interp + b_pos * b_interp;
+ this->chunk_pos = a.get_chunk_pos();
+ this->velocity = a.get_velocity() * a_interp + b.get_velocity() * b_interp;
+ // Update viewangles if we're not the localplayer, yaw requires special care
+ // because it should snap to the closest difference angle.
+ if (!is_localplayer) {
+ this->viewangles.pitch =
+ a.get_angles().pitch * a_interp + b.get_angles().pitch * b_interp;
+ const float yaw_delta = shared::math::angles::get_yaw_delta(
+ a.get_angles().yaw, b.get_angles().yaw);
+ this->viewangles.yaw = a.get_angles().yaw + yaw_delta * b_interp;
+ this->viewangles.normalise();
+ }
+ shared::movement::normalise_position(this->local_pos, this->chunk_pos);
+}
+
+} // namespace client
diff --git a/src/client/entity/animate.hh b/src/client/entity/animate.hh
new file mode 100644
index 0000000..79016ea
--- /dev/null
+++ b/src/client/entity/animate.hh
@@ -0,0 +1,71 @@
+#ifndef CLIENT_ENTITY_ANIMATE_HH_
+#define CLIENT_ENTITY_ANIMATE_HH_
+
+#include <algorithm>
+#include <chrono>
+#include <cmath>
+#include <ranges>
+#include <utility>
+#include <vector>
+
+#include <boost/circular_buffer.hpp>
+
+#include "client/entity/entity.hh"
+#include "client/movement/movement.hh"
+#include "client/settings.hh"
+#include "client/state/state.hh"
+#include "shared/entity/animate.hh"
+#include "shared/entity/entity.hh"
+#include "shared/movement/movement.hh"
+#include "shared/net/proto.hh"
+#include "shared/shared.hh"
+
+namespace client {
+
+class animate : virtual public shared::animate, virtual public client::entity {
+protected:
+ struct animate_update {
+ shared::tick_t tick_sequence; // tick or sequence, if LP then sequence.
+ shared::animate animate;
+ bool from_server;
+ };
+
+ // We have 1 second of potential player interpolation to use here when we
+ // initialise this with the server's tickrate.
+ boost::circular_buffer<animate_update> updates{state::tickrate};
+ shared::tick_t latest_sequence = 0;
+
+private:
+ float get_target_ticks_back() noexcept;
+
+public:
+ animate(shared::player&& p) noexcept
+ : shared::entity(p), shared::animate(p), client::entity(p) {}
+
+ animate(const proto::animate& proto) noexcept
+ : shared::entity(proto.entity()), shared::animate(proto),
+ client::entity(proto.entity()) {}
+
+public:
+ // Call when localplayer gets tick to update state timers.
+ void update_time_factor(const shared::tick_t& sequence,
+ const shared::tick_t& tick) noexcept;
+
+ virtual void notify(const shared::animate& animate,
+ const shared::tick_t& tick_sequence,
+ const bool from_server) noexcept;
+
+ shared::tick_t& get_mutable_latest_sequence() noexcept {
+ return this->latest_sequence;
+ }
+ shared::tick_t get_latest_sequence() const noexcept {
+ return this->latest_sequence;
+ }
+ // An animate may be interpolated, however it's the moveable class that is
+ // required for extrapolation and other prediction.
+ void interpolate() noexcept;
+};
+
+} // namespace client
+
+#endif
diff --git a/src/client/entity/entity.cc b/src/client/entity/entity.cc
new file mode 100644
index 0000000..c305a66
--- /dev/null
+++ b/src/client/entity/entity.cc
@@ -0,0 +1,6 @@
+#include "client/entity/entity.hh"
+
+namespace client {
+
+
+} // namespace client
diff --git a/src/client/entity/entity.hh b/src/client/entity/entity.hh
new file mode 100644
index 0000000..e8d6cc6
--- /dev/null
+++ b/src/client/entity/entity.hh
@@ -0,0 +1,27 @@
+#ifndef CLIENT_ENTITY_ENTITY_HH_
+#define CLIENT_ENTITY_ENTITY_HH_
+
+#include "shared/entity/entity.hh"
+#include "shared/entity/player.hh"
+
+namespace client {
+
+class player; // forward declaration
+
+// A client::entity is a renderable shared::entity.
+class entity : virtual public shared::entity {
+public:
+ entity(shared::entity& e) noexcept
+ : shared::entity(std::forward<shared::entity>(e)) {}
+ entity(const proto::entity& e) noexcept : shared::entity(e) {}
+
+ virtual ~entity() noexcept {}
+
+public:
+ virtual void draw(const client::player& localplayer) noexcept = 0;
+ virtual void draw_wts(const client::player& localplayer) noexcept = 0;
+};
+
+} // namespace client
+
+#endif
diff --git a/src/client/entity/moveable.cc b/src/client/entity/moveable.cc
new file mode 100644
index 0000000..480e6db
--- /dev/null
+++ b/src/client/entity/moveable.cc
@@ -0,0 +1,86 @@
+#include "client/entity/moveable.hh"
+
+namespace client {
+
+static auto find_sequence_it(auto& updates,
+ const shared::tick_t& sequence) noexcept {
+ return std::ranges::find_if(updates, [&sequence](const auto& update) {
+ return update.tick_sequence == sequence;
+ });
+}
+
+void moveable::repredict_from(const shared::tick_t& sequence) noexcept {
+ // find the animate update we want to repredict starting from
+ const auto find_it = find_sequence_it(this->updates, sequence);
+ if (find_it == std::end(this->updates)) {
+ return;
+ }
+
+ // backup current animate
+ const shared::animate before_move = *this;
+
+ // predict the next ticks starting from find_it until we run
+ // out of prediction history
+ for (auto it = find_it; it != std::end(this->updates); ++it) {
+ const auto next_it = std::next(it);
+ if (next_it == std::end(this->updates)) {
+ break;
+ }
+
+ const auto viewangles = next_it->animate.get_angles();
+ const auto commands = next_it->animate.get_commands();
+
+ this->shared::animate::operator=(it->animate);
+ this->commands = commands;
+ this->viewangles = viewangles;
+ const auto predicted = movement::move(*this, state::chunks);
+ if (!predicted.has_value()) {
+ break;
+ }
+ next_it->animate = *predicted;
+ next_it->animate.get_mutable_angles() = viewangles;
+ next_it->animate.get_mutable_commands() = commands;
+ }
+ // restore current animate
+ this->shared::animate::operator=(before_move);
+}
+
+void moveable::notify(const shared::animate& animate,
+ const shared::tick_t& sequence,
+ const bool from_server) noexcept {
+ this->animate::notify(animate, sequence, from_server);
+
+ if (!from_server) { // Only repredict if we're from the server.
+ return;
+ }
+ this->repredict_from(sequence);
+}
+
+void moveable::extrapolate() noexcept {
+ // can't extrapolate if no data
+ if (this->updates.empty()) {
+ return;
+ }
+
+ const auto& latest_it = std::rbegin(this->updates);
+ if (latest_it == std::rend(this->updates)) {
+ return;
+ }
+
+ // backup current animate, load latest animate
+ const shared::animate before_extrapolate = *this;
+ this->shared::animate::operator=(latest_it->animate);
+
+ // FIXED: we were writing our commands with the old commands, so we were
+ // always predicting the first commands, which were always for not moving!
+ this->commands = before_extrapolate.get_commands();
+ this->viewangles = before_extrapolate.get_angles();
+
+ const auto predicted = movement::move(*this, state::chunks);
+ if (predicted.has_value()) {
+ this->notify(*predicted, this->get_latest_sequence() + 1, false);
+ }
+ this->shared::animate::operator=(before_extrapolate);
+}
+
+} // namespace client
diff --git a/src/client/entity/moveable.hh b/src/client/entity/moveable.hh
new file mode 100644
index 0000000..fa08d22
--- /dev/null
+++ b/src/client/entity/moveable.hh
@@ -0,0 +1,29 @@
+#ifndef CLIENT_ENTITY_MOVEABLE_HH_
+#define CLIENT_ENTITY_MOVEABLE_HH_
+
+#include "client/entity/animate.hh"
+#include "client/state/chunks.hh"
+#include "client/state/state.hh"
+#include "client/world/chunk.hh"
+#include "shared/entity/moveable.hh"
+
+namespace client {
+
+class moveable : virtual public client::animate,
+ virtual public shared::moveable,
+ virtual public shared::animate {
+private:
+ void repredict_from(const shared::tick_t& sequence) noexcept;
+
+public:
+ // Notify repredicts if necessary.
+ virtual void notify(const shared::animate& animate,
+ const shared::tick_t& sequence,
+ const bool from_server) noexcept override;
+
+ void extrapolate() noexcept;
+};
+
+} // namespace client
+
+#endif
diff --git a/src/client/entity/player.cc b/src/client/entity/player.cc
new file mode 100644
index 0000000..550968f
--- /dev/null
+++ b/src/client/entity/player.cc
@@ -0,0 +1,54 @@
+#include "player.hh"
+namespace client {
+
+glm::vec3 player::get_world_pos(const client::player& lp) noexcept {
+ const auto& lp_cp = lp.get_chunk_pos();
+
+ // clang-format off
+ const float world_x = static_cast<float>(this->get_chunk_pos().x - lp_cp.x) * shared::world::chunk::WIDTH + this->local_pos.x;
+ const float world_y = static_cast<float>(this->get_local_pos().y) + shared::player::HEIGHT / 2.0f;
+ const float world_z = static_cast<float>(this->get_chunk_pos().z - lp_cp.z) * shared::world::chunk::WIDTH + this->local_pos.z;
+ // clang-format on
+ return {world_x, world_y, world_z};
+}
+
+void player::draw(const client::player& localplayer) noexcept {
+
+ static render::model playermodel{"res/models/player/player.obj"};
+ static render::program program{"res/shaders/model.vs",
+ "res/shaders/model.fs"};
+
+ const glm::vec3 world_pos = this->get_world_pos(localplayer);
+ const glm::mat4 rotate = glm::rotate(glm::mat4(1.0f), -this->viewangles.yaw,
+ glm::vec3(0.0f, 1.0f, 0.0f));
+ const glm::mat4 translate = glm::translate(glm::mat4(1.0f), world_pos);
+ const glm::mat4& view = render::camera::get_view();
+ const glm::mat4& proj = render::camera::get_proj();
+
+ playermodel.draw(program, proj * view * translate * rotate);
+}
+
+void player::draw_wts(const client::player& localplayer) noexcept {
+ const auto& msg = this->get_message();
+ if (!msg.has_value()) {
+ return;
+ }
+
+ const glm::vec3 world_pos = this->get_world_pos(localplayer);
+ const auto pos = math::world_to_screen(world_pos);
+
+ if (!pos.has_value()) {
+ return;
+ }
+
+ const glm::vec2& window = render::get_window_size();
+ const glm::mat4 proj = glm::ortho(0.0f, window.x, 0.0f, window.y);
+ const glm::mat4 tran =
+ glm::translate(glm::mat4(1.0f),
+ glm::vec3{std::floor(pos->x), std::floor(pos->y), 0.0f});
+
+ constexpr glm::vec4 white{1.0f, 1.0f, 1.0f, 1.0f};
+ render::render_text(msg->text, 30.0f, true, true, white, proj * tran);
+}
+
+} // namespace client
diff --git a/src/client/entity/player.hh b/src/client/entity/player.hh
new file mode 100644
index 0000000..d9bdc18
--- /dev/null
+++ b/src/client/entity/player.hh
@@ -0,0 +1,73 @@
+#ifndef CLIENT_ENTITY_PLAYER_HH_
+#define CLIENT_ENTITY_PLAYER_HH_
+
+#include <chrono>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include "client/entity/moveable.hh"
+#include "client/entity/entity.hh"
+#include "client/item/block.hh"
+#include "client/item/items.hh"
+#include "client/math.hh"
+#include "client/render/camera.hh"
+#include "client/render/model.hh"
+#include "client/render/program.hh"
+
+namespace client {
+
+// Renderable player, similar to client::chunk. We also store the message the
+// player wants to say.
+class player : virtual public shared::player, public client::moveable {
+public:
+ static constexpr auto MSG_SHOW_TIME = std::chrono::seconds{15};
+ struct message {
+ std::string text;
+ std::chrono::time_point<std::chrono::steady_clock> receive_time;
+ message(const std::string& message) noexcept {
+ this->text = message;
+ this->receive_time = std::chrono::steady_clock::now();
+ }
+ };
+ std::optional<message> message;
+
+private:
+ glm::vec3 get_world_pos(const client::player& lp) noexcept;
+
+public:
+ player(shared::player&& p) noexcept
+ : shared::entity(p), shared::animate(p), shared::player(p),
+ client::entity(p), client::animate(std::move(p)) {
+
+ // upgrade our inventory contents to client::item::item
+ for (auto& item : this->inventory.contents) {
+ if (item == nullptr) {
+ continue;
+ }
+
+ item = client::item::make_item(item->get_type(), item->quantity);
+ }
+ }
+ player(const proto::animate& proto) noexcept
+ : shared::entity(proto.entity()), shared::animate(proto),
+ shared::player(proto), client::entity(proto.entity()),
+ client::animate(proto) {}
+
+ virtual void draw(const client::player& localplayer) noexcept override;
+ virtual void draw_wts(const client::player& localplayer) noexcept override;
+
+ const std::optional<struct message>& get_message() noexcept {
+ if (this->message.has_value()) {
+ const auto now = std::chrono::steady_clock::now();
+ if (now > this->message->receive_time + player::MSG_SHOW_TIME) {
+ this->message.reset();
+ }
+ }
+ return this->message;
+ }
+};
+
+} // namespace client
+
+#endif