diff options
Diffstat (limited to 'src/client/entity')
| -rw-r--r-- | src/client/entity/animate.cc | 158 | ||||
| -rw-r--r-- | src/client/entity/animate.hh | 71 | ||||
| -rw-r--r-- | src/client/entity/entity.cc | 6 | ||||
| -rw-r--r-- | src/client/entity/entity.hh | 27 | ||||
| -rw-r--r-- | src/client/entity/moveable.cc | 86 | ||||
| -rw-r--r-- | src/client/entity/moveable.hh | 29 | ||||
| -rw-r--r-- | src/client/entity/player.cc | 54 | ||||
| -rw-r--r-- | src/client/entity/player.hh | 73 |
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 |
