#1 - quicr module
This commit is contained in:
@@ -10,6 +10,7 @@ file(GLOB FILES
|
||||
src/draw/*.cpp
|
||||
src/draw/RenderPasses/*.cpp
|
||||
src/debug/*.cpp
|
||||
src/debug/metrics/*.cpp
|
||||
src/debug/tools/*.cpp
|
||||
)
|
||||
|
||||
@@ -20,11 +21,14 @@ target_link_libraries(${PROJECT_NAME}
|
||||
PUBLIC
|
||||
towards
|
||||
tw::network
|
||||
tw::metrics
|
||||
tw::quicr
|
||||
loft::common
|
||||
loft::base
|
||||
loft_window
|
||||
loft::render_graph
|
||||
tw::protocol
|
||||
tw::message_protocol
|
||||
tw::serialization
|
||||
tw::gui
|
||||
imgui::imgui
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include "metrics/MetricSeries.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
|
||||
namespace tw::dbg {
|
||||
|
||||
/**
|
||||
* Per-second history of what the client sends, receives and waits for.
|
||||
*
|
||||
* Traffic arrives as running totals, one reading per tick: sample() keeps the
|
||||
* change since the previous reading, so a bucket sums to the traffic of that
|
||||
* second and its extremes are the quietest and busiest tick within it.
|
||||
* Durations are recorded as they are measured.
|
||||
*/
|
||||
class NetworkMetrics {
|
||||
public:
|
||||
using Interval = std::chrono::seconds;
|
||||
using Series = metrics::MetricSeries<Interval>;
|
||||
|
||||
/** How many seconds of history are kept. */
|
||||
static constexpr size_t DEFAULT_HISTORY = 300;
|
||||
|
||||
/** Running totals as of one tick. */
|
||||
struct Totals {
|
||||
uint64_t bytes_sent = 0;
|
||||
uint64_t bytes_received = 0;
|
||||
uint64_t messages_sent = 0;
|
||||
uint64_t messages_received = 0;
|
||||
};
|
||||
|
||||
private:
|
||||
Series m_bytes_out;
|
||||
Series m_bytes_in;
|
||||
Series m_messages_out;
|
||||
Series m_messages_in;
|
||||
Series m_response_ms;
|
||||
Series m_update_ms;
|
||||
|
||||
Series m_rollbacks;
|
||||
Series m_correction_distance;
|
||||
Series m_ack_lag_frames;
|
||||
Series m_replayed_frames;
|
||||
|
||||
Totals m_previous;
|
||||
bool m_has_previous = false;
|
||||
|
||||
static uint64_t delta(uint64_t current, uint64_t previous) {
|
||||
return current > previous ? current - previous : 0;
|
||||
}
|
||||
|
||||
static double to_millis(std::chrono::nanoseconds elapsed) {
|
||||
return std::chrono::duration<double, std::milli>(elapsed).count();
|
||||
}
|
||||
|
||||
public:
|
||||
explicit NetworkMetrics(size_t history_in_seconds = DEFAULT_HISTORY) :
|
||||
m_bytes_out(history_in_seconds),
|
||||
m_bytes_in(history_in_seconds),
|
||||
m_messages_out(history_in_seconds),
|
||||
m_messages_in(history_in_seconds),
|
||||
m_response_ms(history_in_seconds),
|
||||
m_update_ms(history_in_seconds),
|
||||
m_rollbacks(history_in_seconds),
|
||||
m_correction_distance(history_in_seconds),
|
||||
m_ack_lag_frames(history_in_seconds),
|
||||
m_replayed_frames(history_in_seconds)
|
||||
{ }
|
||||
|
||||
/**
|
||||
* Records how much `totals` grew since the previous call. The first call
|
||||
* only remembers where the counters started.
|
||||
*/
|
||||
void sample(const Totals& totals) {
|
||||
if(m_has_previous) {
|
||||
m_bytes_out.push((double)delta(totals.bytes_sent, m_previous.bytes_sent));
|
||||
m_bytes_in.push((double)delta(totals.bytes_received, m_previous.bytes_received));
|
||||
m_messages_out.push((double)delta(totals.messages_sent, m_previous.messages_sent));
|
||||
m_messages_in.push((double)delta(totals.messages_received, m_previous.messages_received));
|
||||
}
|
||||
|
||||
m_previous = totals;
|
||||
m_has_previous = true;
|
||||
}
|
||||
|
||||
/** Time between sending an input and seeing the answer to it. */
|
||||
void record_response_time(std::chrono::nanoseconds elapsed) {
|
||||
m_response_ms.push(to_millis(elapsed));
|
||||
}
|
||||
|
||||
/** Time one tick spent moving messages in and out, handlers included. */
|
||||
void record_update_time(std::chrono::nanoseconds elapsed) {
|
||||
m_update_ms.push(to_millis(elapsed));
|
||||
}
|
||||
|
||||
const Series& bytes_out() const {
|
||||
return m_bytes_out;
|
||||
}
|
||||
|
||||
const Series& bytes_in() const {
|
||||
return m_bytes_in;
|
||||
}
|
||||
|
||||
const Series& messages_out() const {
|
||||
return m_messages_out;
|
||||
}
|
||||
|
||||
const Series& messages_in() const {
|
||||
return m_messages_in;
|
||||
}
|
||||
|
||||
const Series& response_ms() const {
|
||||
return m_response_ms;
|
||||
}
|
||||
|
||||
const Series& update_ms() const {
|
||||
return m_update_ms;
|
||||
}
|
||||
|
||||
/** Records a rollback event (one sample per rollback). */
|
||||
void record_rollback() {
|
||||
m_rollbacks.push(1.0);
|
||||
}
|
||||
|
||||
/** Records the distance in meters of a position correction. */
|
||||
void record_correction_distance(double meters) {
|
||||
m_correction_distance.push(meters);
|
||||
}
|
||||
|
||||
/** Records how many frames behind the ack is trailing the current frame. */
|
||||
void record_ack_lag(uint32_t frames) {
|
||||
m_ack_lag_frames.push((double)frames);
|
||||
}
|
||||
|
||||
/** Records how many frames were replayed during a rollback. */
|
||||
void record_replayed_frames(uint32_t frames) {
|
||||
m_replayed_frames.push((double)frames);
|
||||
}
|
||||
|
||||
const Series& rollbacks() const {
|
||||
return m_rollbacks;
|
||||
}
|
||||
|
||||
const Series& correction_distance() const {
|
||||
return m_correction_distance;
|
||||
}
|
||||
|
||||
const Series& ack_lag_frames() const {
|
||||
return m_ack_lag_frames;
|
||||
}
|
||||
|
||||
const Series& replayed_frames() const {
|
||||
return m_replayed_frames;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -2,95 +2,109 @@
|
||||
|
||||
#include <imgui.h>
|
||||
#include <implot.h>
|
||||
#include <implot_internal.h>
|
||||
|
||||
#include "metrics/BucketMetric.hpp"
|
||||
#include "metrics/MetricSeries.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
template<typename T, typename Interval, typename Op = net::SumOp<T>>
|
||||
/**
|
||||
* Plots one series against the seconds behind now, ending at the last second
|
||||
* that has fully elapsed.
|
||||
*
|
||||
* The line follows whichever statistic the series is read with. Reading an
|
||||
* average also shades the quietest and busiest value of each second behind it;
|
||||
* a total has no such range to show, since its buckets already hold every value
|
||||
* of that second added together.
|
||||
*/
|
||||
class MetricWidget {
|
||||
std::string m_name;
|
||||
|
||||
net::BucketMetric<T, Interval, Op>& m_metric;
|
||||
|
||||
using Self = MetricWidget<T, Interval, Op>;
|
||||
|
||||
struct {
|
||||
T constraint_from;
|
||||
T constraint_to;
|
||||
|
||||
T from;
|
||||
T to;
|
||||
} y_axis;
|
||||
|
||||
bool m_is_scrolling = true;
|
||||
|
||||
public:
|
||||
MetricWidget(
|
||||
const std::string& name,
|
||||
net::BucketMetric<T, Interval, Op>& metric
|
||||
) :
|
||||
m_name(name),
|
||||
m_metric(metric)
|
||||
{
|
||||
y_axis = {
|
||||
.constraint_from = 0,
|
||||
.constraint_to = 500,
|
||||
.from = 0,
|
||||
.to = 250
|
||||
};
|
||||
using Series = metrics::MetricSeries<std::chrono::seconds>;
|
||||
|
||||
private:
|
||||
std::string m_name;
|
||||
std::string m_unit;
|
||||
const Series* m_series;
|
||||
metrics::MetricField m_field;
|
||||
|
||||
/**
|
||||
* The second in progress is left out: it only holds the part of itself
|
||||
* that has elapsed, so drawing it makes the newest point drop and climb
|
||||
* back once a second.
|
||||
*/
|
||||
static constexpr size_t SKIP_IN_PROGRESS = 1;
|
||||
|
||||
std::vector<double> m_ages;
|
||||
std::vector<double> m_values;
|
||||
std::vector<double> m_lows;
|
||||
std::vector<double> m_highs;
|
||||
|
||||
bool has_range() const {
|
||||
return m_field == metrics::MetricField::Avg;
|
||||
}
|
||||
|
||||
Self& set_x_axis_limits_contraints(T from, T to) {
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from, to);
|
||||
return *this;
|
||||
}
|
||||
|
||||
Self& set_y_axis_limits(double from, double to) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
Self& enable_scrolling() {
|
||||
m_is_scrolling = true;
|
||||
}
|
||||
|
||||
Self& disable_scrolling() {
|
||||
m_is_scrolling = true;
|
||||
}
|
||||
|
||||
|
||||
void draw() {
|
||||
ImGui::PushID(m_name.c_str());
|
||||
|
||||
auto head = m_metric.get_head();
|
||||
auto head_timeline = m_metric.get_head_timeline();
|
||||
|
||||
auto tail = m_metric.get_tail();
|
||||
auto tail_timeline = m_metric.get_tail_timeline();
|
||||
|
||||
static float m_metric_history = 10.0f;
|
||||
ImGui::Checkbox("Is Scrolling", &m_is_scrolling);
|
||||
if(m_is_scrolling) {
|
||||
ImGui::SliderFloat("History", &m_metric_history,1,30,"%.1f s");
|
||||
/** Describes what is drawn, so the numbers always match the line. */
|
||||
void draw_summary() const {
|
||||
if(m_values.empty()) {
|
||||
ImGui::TextUnformatted("no samples yet");
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::Text("Min: %i", m_metric.min());
|
||||
ImGui::Text("Max: %i", m_metric.max());
|
||||
auto [low, high] = std::minmax_element(m_values.begin(), m_values.end());
|
||||
|
||||
if(ImPlot::BeginPlot(m_name.c_str())) {
|
||||
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
|
||||
double total = 0.0;
|
||||
for(double value : m_values) {
|
||||
total += value;
|
||||
}
|
||||
|
||||
ImPlot::SetupAxes("Time", m_name.c_str(), ImPlotAxisFlags_None, ImPlotAxisFlags_None);
|
||||
if(m_is_scrolling) {
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from - m_metric_history, from, ImGuiCond_Always);
|
||||
ImGui::Text("min %.1f %s avg %.1f %s max %.1f %s",
|
||||
*low, m_unit.c_str(),
|
||||
total / (double)m_values.size(), m_unit.c_str(),
|
||||
*high, m_unit.c_str());
|
||||
}
|
||||
|
||||
public:
|
||||
MetricWidget(std::string name, std::string unit, const Series& series, metrics::MetricField field) :
|
||||
m_name(std::move(name)),
|
||||
m_unit(std::move(unit)),
|
||||
m_series(&series),
|
||||
m_field(field)
|
||||
{ }
|
||||
|
||||
/** Draws the last `history_in_seconds` seconds of the series. */
|
||||
void draw(size_t history_in_seconds) {
|
||||
ImGui::PushID(m_name.c_str());
|
||||
|
||||
m_series->linearize(m_ages, m_values, m_field, history_in_seconds, SKIP_IN_PROGRESS);
|
||||
|
||||
if(has_range()) {
|
||||
m_series->linearize(m_ages, m_lows, metrics::MetricField::Min,
|
||||
history_in_seconds, SKIP_IN_PROGRESS);
|
||||
m_series->linearize(m_ages, m_highs, metrics::MetricField::Max,
|
||||
history_in_seconds, SKIP_IN_PROGRESS);
|
||||
}
|
||||
|
||||
draw_summary();
|
||||
|
||||
if(ImPlot::BeginPlot(m_name.c_str(), ImVec2(-1.0f, 150.0f))) {
|
||||
ImPlot::SetupAxes("seconds ago", m_unit.c_str(),
|
||||
ImPlotAxisFlags_None, ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, -(double)history_in_seconds, 0.0, ImGuiCond_Always);
|
||||
|
||||
const int count = (int)m_values.size();
|
||||
|
||||
if(has_range() && count > 0) {
|
||||
ImPlot::PlotShaded("range", m_ages.data(), m_lows.data(), m_highs.data(), count);
|
||||
}
|
||||
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, m_metric.max() * 2, ImGuiCond_Always);
|
||||
// ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
|
||||
if(count > 0) {
|
||||
ImPlot::PlotLine(m_name.c_str(), m_ages.data(), m_values.data(), count);
|
||||
}
|
||||
|
||||
ImPlot::PlotLine(m_name.c_str(), head_timeline.data(), head.data(), head.size());
|
||||
ImPlot::PlotLine(m_name.c_str(), tail_timeline.data(), tail.data(), tail.size());
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,59 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "metrics/BucketMetric.hpp"
|
||||
#include "metrics/NetworkStatsLogger.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "debug/tools/MetricWidget.hpp"
|
||||
|
||||
#include <implot.h>
|
||||
#include <implot_internal.h>
|
||||
#include <imgui.h>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
/**
|
||||
* Panel over everything the client measured about its traffic.
|
||||
*
|
||||
* Traffic is shown as the total of each second, since that is the rate the
|
||||
* connection actually carried. Durations are shown as the average of each
|
||||
* second, with the range behind them.
|
||||
*/
|
||||
class NetworkStatsGui {
|
||||
private:
|
||||
// MetricWidget<uint32_t, std::chrono::seconds, net::AverageOp<uint32_t>> m_ping_widget;
|
||||
// MetricWidget<uint32_t, std::chrono::seconds> m_outgoing_widget;
|
||||
// MetricWidget<uint32_t, std::chrono::seconds> m_incoming_widget;
|
||||
MetricWidget m_response;
|
||||
MetricWidget m_update;
|
||||
MetricWidget m_bytes_in;
|
||||
MetricWidget m_bytes_out;
|
||||
MetricWidget m_messages_in;
|
||||
MetricWidget m_messages_out;
|
||||
|
||||
MetricWidget m_rollbacks;
|
||||
MetricWidget m_correction_distance;
|
||||
MetricWidget m_ack_lag_frames;
|
||||
MetricWidget m_replayed_frames;
|
||||
|
||||
int m_history_in_seconds = 30;
|
||||
|
||||
public:
|
||||
// NetworkStatsGui() :
|
||||
// m_ping_widget("Ping", net::NetworkStatsLogger::instance()->ping()),
|
||||
// m_outgoing_widget("Outgoing", net::NetworkStatsLogger::instance()->outgoing()),
|
||||
// m_incoming_widget("Incoming", net::NetworkStatsLogger::instance()->incoming())
|
||||
// {
|
||||
// }
|
||||
explicit NetworkStatsGui(const NetworkMetrics& metrics) :
|
||||
m_response("Response", "ms", metrics.response_ms(), metrics::MetricField::Avg),
|
||||
m_update("Network update", "ms", metrics.update_ms(), metrics::MetricField::Avg),
|
||||
m_bytes_in("Bytes in", "B/s", metrics.bytes_in(), metrics::MetricField::Sum),
|
||||
m_bytes_out("Bytes out", "B/s", metrics.bytes_out(), metrics::MetricField::Sum),
|
||||
m_messages_in("Messages in", "1/s", metrics.messages_in(), metrics::MetricField::Sum),
|
||||
m_messages_out("Messages out", "1/s", metrics.messages_out(), metrics::MetricField::Sum),
|
||||
m_rollbacks("Rollbacks", "1/s", metrics.rollbacks(), metrics::MetricField::Sum),
|
||||
m_correction_distance("Correction distance", "m", metrics.correction_distance(), metrics::MetricField::Avg),
|
||||
m_ack_lag_frames("Ack lag", "frames", metrics.ack_lag_frames(), metrics::MetricField::Avg),
|
||||
m_replayed_frames("Replayed frames", "frames", metrics.replayed_frames(), metrics::MetricField::Avg)
|
||||
{ }
|
||||
|
||||
void draw() {
|
||||
// auto* instance = net::NetworkStatsLogger::instance();
|
||||
// auto& ping = instance->ping();
|
||||
|
||||
ImGui::Begin("Network Stats");
|
||||
|
||||
/* auto head = ping.get_head();
|
||||
auto head_timeline = ping.get_head_timeline();
|
||||
ImGui::SliderInt("History", &m_history_in_seconds, 5, 300, "%d s");
|
||||
|
||||
auto tail = ping.get_tail();
|
||||
auto tail_timeline = ping.get_tail_timeline();
|
||||
const size_t history = (size_t)m_history_in_seconds;
|
||||
|
||||
static float ping_history = 10.0f;
|
||||
ImGui::SliderFloat("Ping History", &ping_history,1,30,"%.1f s");
|
||||
m_response.draw(history);
|
||||
m_update.draw(history);
|
||||
m_bytes_in.draw(history);
|
||||
m_bytes_out.draw(history);
|
||||
m_messages_in.draw(history);
|
||||
m_messages_out.draw(history);
|
||||
|
||||
if(ImPlot::BeginPlot("Ping")) {
|
||||
auto from = tail_timeline.empty() ? *(head_timeline.end() - 1) : *(tail_timeline.end() - 1);
|
||||
|
||||
ImPlot::SetupAxes("FrameIdx","FPS", ImPlotAxisFlags_None, ImPlotAxisFlags_None);
|
||||
ImPlot::SetupAxisLimits(ImAxis_X1, from - ping_history, from, ImGuiCond_Always);
|
||||
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 120);
|
||||
ImPlot::SetupAxisLimitsConstraints(ImAxis_Y1, 0, 10000);
|
||||
|
||||
ImPlot::PlotLine("Ping", head_timeline.data(), head.data(), head.size());
|
||||
ImPlot::PlotLine("Ping", tail_timeline.data(), tail.data(), tail.size());
|
||||
ImPlot::EndPlot();
|
||||
} */
|
||||
|
||||
// m_ping_widget.draw();
|
||||
// m_outgoing_widget.draw();
|
||||
// m_incoming_widget.draw();
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Prediction");
|
||||
m_rollbacks.draw(history);
|
||||
m_correction_distance.draw(history);
|
||||
m_ack_lag_frames.draw(history);
|
||||
m_replayed_frames.draw(history);
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <format>
|
||||
#include <vector>
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
#include "metrics/NetworkStatsLogger.hpp"
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
class PacketBacklogGui {
|
||||
private:
|
||||
std::vector<uint32_t> m_buckets;
|
||||
uint32_t m_last_backlog_idx;
|
||||
|
||||
public:
|
||||
void draw() {
|
||||
// auto* instance = net::NetworkStatsLogger::instance();
|
||||
// size_t size = instance->get_size();
|
||||
|
||||
// if(ImGui::BeginTable("Network Packets", 5)) {
|
||||
// ImGui::TableSetupColumn("Message Type");
|
||||
// ImGui::TableSetupColumn("Time");
|
||||
// ImGui::TableSetupColumn("Is From Us");
|
||||
// ImGui::TableSetupColumn("Target");
|
||||
// ImGui::TableSetupColumn("Size");
|
||||
|
||||
// for(int32_t i = size-1; i >= 0; i--) {
|
||||
// auto& item = instance->get_item(i);
|
||||
// ImGui::PushID(item.timepoint.time_since_epoch().count());
|
||||
|
||||
// ImGui::TableNextRow();
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%i", item.message_type);
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text(std::format("{}", item.timepoint.time_since_epoch()).c_str());
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Checkbox("is_sent_from_us", &item.is_sent_by_us);
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text(item.target.to_string().c_str());
|
||||
|
||||
// ImGui::TableNextColumn();
|
||||
// ImGui::Text("%ld", item.buffer.size());
|
||||
|
||||
// ImGui::PopID();
|
||||
// }
|
||||
// ImGui::EndTable();
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "ByteBuffer.hpp"
|
||||
#include "packets/Packet.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace tw::dbg::tools {
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
#include "PlayerReconciler.hpp"
|
||||
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
#include "world/Transform.hpp"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtx/norm.hpp>
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
PlayerReconciler::PlayerReconciler(JoltPhysicsWorld* physics)
|
||||
: m_physics(physics), m_last_reconciled_ack(0), m_rollback_count(0),
|
||||
m_last_correction_distance(0.0f), m_last_replayed_frames(0), m_last_ack_frame(0)
|
||||
{
|
||||
for (auto& record : m_records) {
|
||||
record.frame = 0;
|
||||
record.valid = false;
|
||||
record.input = glm::vec3(0.0f);
|
||||
record.predicted_position = glm::vec3(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerReconciler::record_input(uint32_t frame, glm::vec3 input) {
|
||||
size_t idx = frame % RING_SIZE;
|
||||
m_records[idx].frame = frame;
|
||||
m_records[idx].valid = true;
|
||||
m_records[idx].input = input;
|
||||
}
|
||||
|
||||
void PlayerReconciler::record_prediction(uint32_t frame, glm::vec3 position) {
|
||||
size_t idx = frame % RING_SIZE;
|
||||
if (m_records[idx].frame == frame && m_records[idx].valid) {
|
||||
m_records[idx].predicted_position = position;
|
||||
}
|
||||
}
|
||||
|
||||
bool PlayerReconciler::reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||
entt::entity player, entt::registry* registry,
|
||||
uint32_t current_frame)
|
||||
{
|
||||
if (ack_frame == 0 || ack_frame <= m_last_reconciled_ack || ack_frame >= current_frame) {
|
||||
return false;
|
||||
}
|
||||
|
||||
m_last_reconciled_ack = ack_frame;
|
||||
m_last_ack_frame = ack_frame;
|
||||
|
||||
Record& record = m_records[ack_frame % RING_SIZE];
|
||||
const bool has_prediction = record.valid && record.frame == ack_frame;
|
||||
|
||||
// With a prediction to compare against, an answer that already matches costs
|
||||
// nothing further. This is the case almost every frame.
|
||||
if (has_prediction) {
|
||||
float distance = glm::distance(record.predicted_position, authoritative_position);
|
||||
m_last_correction_distance = distance;
|
||||
|
||||
if (distance < kPositionEpsilon) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Restoring the frame the answer describes keeps everything the simulation
|
||||
// derived from it, so only the character has to be moved. Without a stored
|
||||
// frame there is nothing to restore and the answer is taken as it stands.
|
||||
const bool restored = has_prediction && m_physics->rollback(ack_frame);
|
||||
|
||||
place_character(player, registry, authoritative_position, !restored);
|
||||
replay_from(ack_frame, player, registry, current_frame);
|
||||
|
||||
m_last_replayed_frames = current_frame - 1 - ack_frame;
|
||||
m_rollback_count++;
|
||||
|
||||
spdlog::debug("Corrected at frame {}: distance {}, replayed {}, restored {}",
|
||||
ack_frame, m_last_correction_distance, m_last_replayed_frames, restored);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlayerReconciler::place_character(entt::entity player, entt::registry* registry,
|
||||
glm::vec3 position, bool clear_velocity) {
|
||||
CharacterBody* body = registry->try_get<CharacterBody>(player);
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
|
||||
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
||||
|
||||
if (clear_velocity) {
|
||||
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
|
||||
body->m_desired_velocity = JPH::Vec3::sZero();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerReconciler::replay_from(uint32_t from_frame, entt::entity player,
|
||||
entt::registry* registry, uint32_t current_frame) {
|
||||
for (uint32_t f = from_frame + 1; f < current_frame; ++f) {
|
||||
m_physics->step(f, tw::JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
|
||||
Transform* transform = registry->try_get<Transform>(player);
|
||||
if (!transform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Frames the ring never saw still need an entry, or the answer to them
|
||||
// arrives with nothing to compare against and forces another correction.
|
||||
Record& replayed = m_records[f % RING_SIZE];
|
||||
replayed.frame = f;
|
||||
replayed.valid = true;
|
||||
replayed.predicted_position = transform->position();
|
||||
}
|
||||
}
|
||||
|
||||
void PlayerReconciler::reset_at(uint32_t frame) {
|
||||
m_last_reconciled_ack = frame;
|
||||
|
||||
for (auto& record : m_records) {
|
||||
record.valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <array>
|
||||
#include <glm/glm.hpp>
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
namespace tw {
|
||||
class JoltPhysicsWorld;
|
||||
}
|
||||
|
||||
namespace tw::net {
|
||||
|
||||
class PlayerReconciler {
|
||||
private:
|
||||
struct Record {
|
||||
uint32_t frame;
|
||||
bool valid;
|
||||
glm::vec3 input;
|
||||
glm::vec3 predicted_position;
|
||||
};
|
||||
|
||||
static constexpr size_t RING_SIZE = 64;
|
||||
static constexpr float kPositionEpsilon = 0.05f;
|
||||
|
||||
tw::JoltPhysicsWorld* m_physics;
|
||||
std::array<Record, RING_SIZE> m_records;
|
||||
|
||||
uint32_t m_last_reconciled_ack = 0;
|
||||
uint64_t m_rollback_count = 0;
|
||||
float m_last_correction_distance = 0.0f;
|
||||
uint32_t m_last_replayed_frames = 0;
|
||||
uint32_t m_last_ack_frame = 0;
|
||||
|
||||
public:
|
||||
PlayerReconciler(tw::JoltPhysicsWorld* physics);
|
||||
|
||||
void record_input(uint32_t frame, glm::vec3 input);
|
||||
void record_prediction(uint32_t frame, glm::vec3 position);
|
||||
|
||||
bool reconcile(uint32_t ack_frame, glm::vec3 authoritative_position,
|
||||
entt::entity player, entt::registry* registry, uint32_t current_frame);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Re-simulates `from_frame + 1` up to the newest frame, refreshing the stored
|
||||
* prediction for each. The inputs come from the character itself, so this
|
||||
* works even for frames this ring never recorded.
|
||||
*/
|
||||
void replay_from(uint32_t from_frame, entt::entity player,
|
||||
entt::registry* registry, uint32_t current_frame);
|
||||
|
||||
/** Places the character at `position` without disturbing the stored frames. */
|
||||
void place_character(entt::entity player, entt::registry* registry,
|
||||
glm::vec3 position, bool clear_velocity);
|
||||
|
||||
public:
|
||||
|
||||
/**
|
||||
* Drops every stored frame and treats `frame` as already answered. Used when
|
||||
* the player is placed outright, where nothing recorded before the placement
|
||||
* describes where it now is.
|
||||
*/
|
||||
void reset_at(uint32_t frame);
|
||||
|
||||
uint64_t rollback_count() const { return m_rollback_count; }
|
||||
float last_correction_distance() const { return m_last_correction_distance; }
|
||||
uint32_t last_replayed_frames() const { return m_last_replayed_frames; }
|
||||
uint32_t last_ack_frame() const { return m_last_ack_frame; }
|
||||
};
|
||||
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "Address.hpp"
|
||||
#include "SDLWindow.h"
|
||||
#include "debug/tools/NetworkStatsGui.hpp"
|
||||
#include "debug/tools/PacketBacklogGui.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "debug/tools/EntityManagerGui.hpp"
|
||||
@@ -52,7 +51,8 @@ Runtime::Runtime(int argc, char** argv) :
|
||||
m_physics_world(&m_world),
|
||||
m_world_renderer("towards", m_window.get(), &m_world, &m_files),
|
||||
m_input_manager(m_window.get()),
|
||||
m_world_controller(&m_input_manager, &m_world, &m_physics_world, &m_world_renderer, { "127.0.0.1", get_port_from_args(argc, argv) }),
|
||||
m_network_metrics(),
|
||||
m_world_controller(&m_input_manager, &m_world, &m_physics_world, &m_world_renderer, { "127.0.0.1", get_port_from_args(argc, argv) }, &m_network_metrics),
|
||||
m_lockstep(60)
|
||||
{
|
||||
}
|
||||
@@ -95,8 +95,7 @@ Runtime::Runtime(int argc, char** argv) :
|
||||
void Runtime::run() {
|
||||
dbg::tools::EntityManagerGui entity_manager(&m_world);
|
||||
dbg::tools::PerformanceStatsGui perf_stats(m_lockstep);
|
||||
dbg::tools::PacketBacklogGui packet_backlog;
|
||||
dbg::tools::NetworkStatsGui network_stats;
|
||||
dbg::tools::NetworkStatsGui network_stats(m_network_metrics);
|
||||
|
||||
ImPlot::CreateContext();
|
||||
m_is_running = true;
|
||||
@@ -147,7 +146,6 @@ void Runtime::run() {
|
||||
m_world_controller.update(m_lockstep.delta_time());
|
||||
|
||||
perf_stats.draw();
|
||||
packet_backlog.draw();
|
||||
network_stats.draw();
|
||||
tw::dbg::ComponentGui<tw::io::InputManager>().draw(&m_input_manager);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "runtime/LockStep.hpp"
|
||||
@@ -28,6 +29,8 @@ private:
|
||||
|
||||
tw::io::InputManager m_input_manager;
|
||||
|
||||
tw::dbg::NetworkMetrics m_network_metrics;
|
||||
|
||||
tw::ClientWorldController m_world_controller;
|
||||
|
||||
tw::LockStep m_lockstep;
|
||||
|
||||
@@ -14,8 +14,7 @@ struct CameraData {
|
||||
CameraData(glm::mat4 projection, Transform view) :
|
||||
projection(projection),
|
||||
view(view)
|
||||
{
|
||||
}
|
||||
{ }
|
||||
};
|
||||
|
||||
class Camera {
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
#include "PlayerMove.pb.h"
|
||||
|
||||
#include "entt/entity/entity.hpp"
|
||||
#include "messenger/MessageHandler.hpp"
|
||||
#include "messenger/Messenger.hpp"
|
||||
#include "TcpStream.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "messages/PlayerMoveMessage.hpp"
|
||||
#include "metrics/HistoryBuffer.hpp"
|
||||
#include "world/CharacterBody.hpp"
|
||||
@@ -22,6 +20,7 @@
|
||||
#include "world/JoltPhysicsWorld.hpp"
|
||||
#include "world/WorldEntity.hpp"
|
||||
#include "tw/serial/WorldStateWriter.hpp"
|
||||
#include "network/EntityInterpolation.hpp"
|
||||
|
||||
namespace tw {
|
||||
|
||||
@@ -83,16 +82,26 @@ ClientWorldController::create_entity(const std::string& name, glm::vec3 position
|
||||
return entity;
|
||||
}
|
||||
|
||||
net::TcpStream create_stream(tw::net::Address address) {
|
||||
auto stream = net::TcpStream::connect(address);
|
||||
if(!stream.has_value()) {
|
||||
spdlog::error("Failed to connect to server");
|
||||
static std::unique_ptr<msg::MessageEndpoint> create_endpoint() {
|
||||
auto endpoint_r = msg::MessageEndpoint::create();
|
||||
if(!endpoint_r) {
|
||||
spdlog::error("Failed to create the endpoint: {}", endpoint_r.error().message());
|
||||
throw std::runtime_error("Failed to create the endpoint");
|
||||
}
|
||||
|
||||
return std::move(endpoint_r.value());
|
||||
}
|
||||
|
||||
static msg::MessageConnection* connect_to_server(msg::MessageEndpoint* endpoint, net::Address address) {
|
||||
auto server_r = endpoint->connect(address.ip_string(), address.port());
|
||||
if(!server_r) {
|
||||
spdlog::error("Failed to connect to server: {}", server_r.error().message());
|
||||
throw std::runtime_error("Failed to connect to server");
|
||||
}
|
||||
|
||||
stream.value().set_non_blocking();
|
||||
spdlog::info("Connected to server at {}", address.to_string());
|
||||
|
||||
return std::move(stream.value());
|
||||
return server_r.value();
|
||||
}
|
||||
|
||||
std::optional<entt::entity> ClientWorldController::map_from_server_entity(int id) {
|
||||
@@ -134,33 +143,49 @@ ClientWorldController::ClientWorldController(
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
tw::net::Address address
|
||||
tw::net::Address address,
|
||||
dbg::NetworkMetrics* network_metrics
|
||||
) :
|
||||
m_input_manager(inputs),
|
||||
m_world(world),
|
||||
m_physics_world(physics_world),
|
||||
m_world_renderer(world_renderer),
|
||||
m_player_entity(create_player_entity(world, physics_world, world_renderer)),
|
||||
// m_player_entity(/* create_player_entity(world, physics_world, world_renderer) */),
|
||||
m_player_controller(&world_renderer->camera(), glm::vec3()),
|
||||
m_messenger{address},
|
||||
m_endpoint(create_endpoint()),
|
||||
m_server(connect_to_server(m_endpoint.get(), address)),
|
||||
m_messages(m_endpoint.get()),
|
||||
m_network_metrics(network_metrics),
|
||||
m_tick_step(20),
|
||||
m_is_connected(false),
|
||||
m_input_send_times(INPUT_SEND_TIME_COUNT),
|
||||
m_position_history_exporter("/home/martin/output.csv"),
|
||||
m_entity_interpolator(&m_world->registry(), m_player_entity, 300)
|
||||
m_entity_interpolator(&m_world->registry(), (entt::entity)0, 300),
|
||||
m_reconciler(physics_world)
|
||||
{
|
||||
m_messenger->set_handler<mmo::LoginResponse>(
|
||||
[&](mmo::LoginResponse* mesg) {
|
||||
m_messages.set_handler<mmo::LoginResponse>(
|
||||
[this](msg::PeerId, const mmo::LoginResponse& mesg) {
|
||||
if(!m_is_connected) {
|
||||
spdlog::info("Joined the game!");
|
||||
spdlog::info("Logged in!");
|
||||
}
|
||||
});
|
||||
|
||||
m_messenger->set_raw_handler(Message<mmo::WorldStateMessage>::value,
|
||||
[&](std::span<std::byte> data) -> tl::expected<void, net::NetworkError> {
|
||||
m_messages.set_handler<mmo::SetControlledEntity>(
|
||||
[this](msg::PeerId, const mmo::SetControlledEntity& mesg) {
|
||||
spdlog::info("Setting controlled entity from server id {}", mesg.entity_id());
|
||||
m_controlled_server_id = mesg.entity_id();
|
||||
try_bind_player_entity();
|
||||
});
|
||||
|
||||
m_endpoint->set_handler(Message<mmo::WorldStateMessage>::value,
|
||||
[this](msg::PeerId, std::span<const std::byte> data) {
|
||||
|
||||
serial::WorldStateReader reader(data);
|
||||
|
||||
auto header = reader.read_header();
|
||||
|
||||
measure_response_time(header.frame_idx);
|
||||
|
||||
while(reader.has_spawn()) {
|
||||
auto spawn = reader.read_spawn();
|
||||
auto entity = create_entity("test", glm::vec3());
|
||||
@@ -168,7 +193,11 @@ ClientWorldController::ClientWorldController(
|
||||
|
||||
map_server_entity(spawn, entity);
|
||||
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == spawn) {
|
||||
try_bind_player_entity();
|
||||
} else {
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
while(reader.has_entity()) {
|
||||
@@ -182,7 +211,57 @@ ClientWorldController::ClientWorldController(
|
||||
}
|
||||
|
||||
glm::vec3 p = {entity_r.position.x, entity_r.position.y, entity_r.position.z};
|
||||
m_entity_interpolator.add_position_for_entity(entity.value(), p);
|
||||
|
||||
if(m_player_entity.has_value() && entity.value() == m_player_entity.value()) {
|
||||
// The entity was created before its position was known, so the
|
||||
// body sits at the origin until the server places it. There is
|
||||
// no predicted history to reconcile against yet.
|
||||
if(!m_player_position_initialized) {
|
||||
m_player_position_initialized = true;
|
||||
snap_player_to(entity.value(), p);
|
||||
continue;
|
||||
}
|
||||
|
||||
glm::vec3 position_before = glm::vec3(0.0f);
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(entity.value());
|
||||
if(player_transform) {
|
||||
position_before = player_transform->position();
|
||||
}
|
||||
|
||||
bool reconcile_happened = m_reconciler.reconcile(header.frame_idx, p, entity.value(), &m_world->registry(), m_frame_idx);
|
||||
|
||||
if(reconcile_happened) {
|
||||
// Where the replay actually ended up, which is ahead of the
|
||||
// acked position by the frames that were re-simulated.
|
||||
glm::vec3 position_after = player_transform
|
||||
? player_transform->position()
|
||||
: p;
|
||||
glm::vec3 correction_delta = position_before - position_after;
|
||||
float correction_magnitude = glm::length(correction_delta);
|
||||
if(correction_magnitude > 5.0f) {
|
||||
correction_delta = glm::normalize(correction_delta) * 5.0f;
|
||||
}
|
||||
m_visual_error += correction_delta;
|
||||
|
||||
m_render_curr_position = position_after;
|
||||
m_render_prev_position = position_after;
|
||||
m_tick_accumulator = 0.0;
|
||||
|
||||
m_network_metrics->record_rollback();
|
||||
m_network_metrics->record_correction_distance(m_reconciler.last_correction_distance());
|
||||
m_network_metrics->record_replayed_frames(m_reconciler.last_replayed_frames());
|
||||
}
|
||||
|
||||
if(header.frame_idx != 0) {
|
||||
uint32_t ack_lag = 0;
|
||||
if(m_frame_idx >= header.frame_idx) {
|
||||
ack_lag = m_frame_idx - header.frame_idx;
|
||||
}
|
||||
m_network_metrics->record_ack_lag(ack_lag);
|
||||
}
|
||||
} else {
|
||||
m_entity_interpolator.add_position_for_entity(entity.value(), p);
|
||||
}
|
||||
|
||||
EntityPositionHistory* history = m_world->registry().try_get<EntityPositionHistory>(entity.value());
|
||||
|
||||
@@ -193,33 +272,111 @@ ClientWorldController::ClientWorldController(
|
||||
}
|
||||
|
||||
// apply_entity_positions();
|
||||
|
||||
|
||||
return {};
|
||||
});
|
||||
|
||||
m_messenger->set_handler<mmo::EntitySpawnMessage>(
|
||||
[&](mmo::EntitySpawnMessage* mesg) {
|
||||
auto entity = create_entity(mesg->name(), glm::vec3());
|
||||
m_messages.set_handler<mmo::EntitySpawnMessage>(
|
||||
[this](msg::PeerId, const mmo::EntitySpawnMessage& mesg) {
|
||||
auto entity = create_entity(mesg.name(), glm::vec3());
|
||||
|
||||
map_server_entity(mesg->entity_id(), entity);
|
||||
map_server_entity(mesg.entity_id(), entity);
|
||||
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
if(m_controlled_server_id.has_value() && m_controlled_server_id.value() == mesg.entity_id()) {
|
||||
try_bind_player_entity();
|
||||
} else {
|
||||
m_entity_interpolator.register_entity(entity);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClientWorldController::~ClientWorldController() {
|
||||
}
|
||||
|
||||
void ClientWorldController::try_bind_player_entity() {
|
||||
if(!m_controlled_server_id.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto local_entity = map_from_server_entity(m_controlled_server_id.value());
|
||||
if(!local_entity.has_value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(m_player_entity.has_value() && m_player_entity.value() == local_entity.value()) {
|
||||
return;
|
||||
}
|
||||
|
||||
entt::entity entity = local_entity.value();
|
||||
spdlog::info("Binding player entity");
|
||||
|
||||
m_player_entity = entity;
|
||||
|
||||
Transform* transform = m_world->registry().try_get<Transform>(entity);
|
||||
glm::vec3 position = transform ? transform->position() : glm::vec3(0.0f);
|
||||
|
||||
m_world->registry().emplace<CharacterController>(entity, 20.0f);
|
||||
m_world->registry().emplace<CharacterBody>(entity, m_physics_world->create_character(
|
||||
new JPH::BoxShape(JPH::Vec3Arg(0.5f, 0.5f, 0.5f)),
|
||||
position
|
||||
));
|
||||
|
||||
if(m_world->registry().all_of<net::EntityPositionInterpolation>(entity)) {
|
||||
m_world->registry().remove<net::EntityPositionInterpolation>(entity);
|
||||
}
|
||||
}
|
||||
|
||||
void ClientWorldController::snap_player_to(entt::entity entity, glm::vec3 position) {
|
||||
CharacterBody* body = m_world->registry().try_get<CharacterBody>(entity);
|
||||
if(body) {
|
||||
body->m_character->SetPosition(JPH::RVec3(position.x, position.y, position.z));
|
||||
body->m_character->SetLinearVelocity(JPH::Vec3::sZero());
|
||||
body->m_desired_velocity = JPH::Vec3::sZero();
|
||||
}
|
||||
|
||||
Transform* transform = m_world->registry().try_get<Transform>(entity);
|
||||
if(transform) {
|
||||
transform->set_position(position);
|
||||
}
|
||||
|
||||
m_render_prev_position = position;
|
||||
m_render_curr_position = position;
|
||||
m_tick_accumulator = 0.0;
|
||||
m_visual_error = glm::vec3(0.0f);
|
||||
|
||||
// Frames simulated before the player was placed describe a position it never
|
||||
// actually had, so answers to them must not be reconciled against.
|
||||
m_reconciler.reset_at(m_frame_idx);
|
||||
}
|
||||
|
||||
void ClientWorldController::export_entity_history() {
|
||||
}
|
||||
|
||||
void ClientWorldController::measure_response_time(uint32_t frame_idx) {
|
||||
// Snapshots carry frame zero until the server has an input to answer, and
|
||||
// repeat the same frame whenever no newer one arrived in between.
|
||||
if(frame_idx == 0 || frame_idx <= m_last_measured_frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Anything the send times no longer cover, including a frame we never sent,
|
||||
// which underflows into a large distance.
|
||||
if(m_frame_idx - frame_idx >= INPUT_SEND_TIME_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_last_measured_frame = frame_idx;
|
||||
|
||||
auto sent_at = m_input_send_times[frame_idx % INPUT_SEND_TIME_COUNT];
|
||||
m_network_metrics->record_response_time(Clock::now() - sent_at);
|
||||
}
|
||||
|
||||
void ClientWorldController::update(double delta_time) {
|
||||
m_player_controller.update(m_input_manager, delta_time);
|
||||
|
||||
ImGui::Begin("Player Controller");
|
||||
if(m_player_entity.has_value()) {
|
||||
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity.value());
|
||||
}
|
||||
|
||||
ImGui::Text("Player entity ID: %d", (uint32_t)m_player_entity);
|
||||
ImGui::Text("Player count: %ld", m_entity_mapping.size());
|
||||
for(auto mapping : m_entity_mapping) {
|
||||
ImGui::Text("%d -> %d", (uint32_t)mapping.first, mapping.second);
|
||||
@@ -228,34 +385,57 @@ void ClientWorldController::update(double delta_time) {
|
||||
ImGui::End();
|
||||
|
||||
if(m_tick_step.update()) {
|
||||
m_messenger->update();
|
||||
auto network_start = Clock::now();
|
||||
m_endpoint->update();
|
||||
m_network_metrics->record_update_time(Clock::now() - network_start);
|
||||
|
||||
m_network_metrics->sample({
|
||||
.bytes_sent = m_endpoint->bytes_sent(),
|
||||
.bytes_received = m_endpoint->bytes_received(),
|
||||
.messages_sent = m_endpoint->messages_sent(),
|
||||
.messages_received = m_endpoint->messages_received()
|
||||
});
|
||||
|
||||
if(!m_is_connected && false) {
|
||||
return;
|
||||
} else {
|
||||
glm::vec3 input = m_player_controller.input();
|
||||
|
||||
// CharacterController& character = m_world->registry().get<CharacterController>(m_player_entity);
|
||||
// character.set_input(m_frame_idx, m_player_controller.input());
|
||||
if(m_player_entity.has_value()) {
|
||||
CharacterController* controller = m_world->registry().try_get<CharacterController>(m_player_entity.value());
|
||||
if(controller) {
|
||||
controller->set_input(m_frame_idx, input);
|
||||
m_reconciler.record_input(m_frame_idx, input);
|
||||
}
|
||||
}
|
||||
|
||||
m_physics_world->step(m_frame_idx, JoltPhysicsWorld::FIXED_DELTA_TIME, true);
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(player_transform) {
|
||||
glm::vec3 true_position = player_transform->position();
|
||||
m_reconciler.record_prediction(m_frame_idx, true_position);
|
||||
|
||||
m_render_prev_position = m_render_curr_position;
|
||||
m_render_curr_position = true_position;
|
||||
m_tick_accumulator = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
mmo::PlayerMoveMessage player_move_message = {};
|
||||
|
||||
player_move_message.set_frame_idx(m_frame_idx);
|
||||
mmo::PlayerInput* player_input = new mmo::PlayerInput();
|
||||
player_input->set_x(m_player_controller.input().x);
|
||||
player_input->set_y(m_player_controller.input().y);
|
||||
player_input->set_z(m_player_controller.input().z);
|
||||
player_input->set_x(input.x);
|
||||
player_input->set_y(input.y);
|
||||
player_input->set_z(input.z);
|
||||
|
||||
player_move_message.set_allocated_input(player_input);
|
||||
auto r = m_messenger->send(player_move_message);
|
||||
auto r = m_messages.send(m_server, player_move_message, false);
|
||||
|
||||
// CharacterBody& ts = m_world->registry().get<CharacterBody>(m_player_entity);
|
||||
// auto position = ts.m_character->GetPosition();
|
||||
// character.position_history().set(m_frame_idx, glm::vec3(position[0], position[1], position[2]));
|
||||
// EntityInterpolation& interpolation = m_world->registry().get<EntityInterpolation>(m_player_entity);
|
||||
// interpolation.push(std::chrono::steady_clock::now(), glm::vec3(position[0], position[1], position[2]));
|
||||
m_input_send_times[m_frame_idx % INPUT_SEND_TIME_COUNT] = Clock::now();
|
||||
|
||||
// m_player_controller.set_target(glm::vec3(position[0], position[1], position[2]));
|
||||
//
|
||||
export_entity_history();
|
||||
|
||||
m_frame_idx++;
|
||||
@@ -268,9 +448,27 @@ void ClientWorldController::update(double delta_time) {
|
||||
}
|
||||
}
|
||||
|
||||
m_physics_world->step(m_frame_idx, delta_time);
|
||||
|
||||
m_entity_interpolator.update();
|
||||
|
||||
if(m_player_entity.has_value()) {
|
||||
Transform* player_transform = m_world->registry().try_get<Transform>(m_player_entity.value());
|
||||
if(player_transform) {
|
||||
m_visual_error *= std::exp(-delta_time * kVisualErrorDecayRate);
|
||||
if(glm::length(m_visual_error) < 0.001f) {
|
||||
m_visual_error = glm::vec3(0.0f);
|
||||
}
|
||||
|
||||
m_tick_accumulator += delta_time;
|
||||
float alpha = glm::clamp(
|
||||
static_cast<float>(m_tick_accumulator / JoltPhysicsWorld::FIXED_DELTA_TIME),
|
||||
0.0f, 1.0f
|
||||
);
|
||||
glm::vec3 smoothed_position = glm::mix(m_render_prev_position, m_render_curr_position, alpha) + m_visual_error;
|
||||
player_transform->set_position(smoothed_position);
|
||||
|
||||
m_player_controller.set_target(smoothed_position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
#include <glm/gtx/io.hpp>
|
||||
|
||||
#include "Address.hpp"
|
||||
#include "TcpStream.hpp"
|
||||
#include "messenger/MessageHandler.hpp"
|
||||
#include "ProtobufMessages.hpp"
|
||||
#include "debug/metrics/NetworkMetrics.hpp"
|
||||
#include "message_protocol/MessageEndpoint.hpp"
|
||||
#include "entt/entity/fwd.hpp"
|
||||
#include "io/InputState.hpp"
|
||||
#include "metrics/HistoryBufferExporter.hpp"
|
||||
@@ -15,6 +16,7 @@
|
||||
#include "draw/WorldRenderer.hpp"
|
||||
#include "world/ThirdPersonPlayerController.hpp"
|
||||
#include "network/EntityPositionInterpolator.hpp"
|
||||
#include "network/PlayerReconciler.hpp"
|
||||
|
||||
|
||||
namespace tw {
|
||||
@@ -35,10 +37,15 @@ class ClientWorldController {
|
||||
drw::WorldRenderer* m_world_renderer;
|
||||
JoltPhysicsWorld* m_physics_world;
|
||||
|
||||
entt::entity m_player_entity;
|
||||
std::optional<entt::entity> m_player_entity;
|
||||
std::optional<uint32_t> m_controlled_server_id;
|
||||
ThirdPersonPlayerController m_player_controller;
|
||||
|
||||
std::optional<tw::net::MessageHandler> m_messenger;
|
||||
std::unique_ptr<msg::MessageEndpoint> m_endpoint;
|
||||
msg::MessageConnection* m_server;
|
||||
ProtobufMessages m_messages;
|
||||
|
||||
dbg::NetworkMetrics* m_network_metrics;
|
||||
|
||||
LockStep m_tick_step;
|
||||
|
||||
@@ -52,10 +59,59 @@ class ClientWorldController {
|
||||
std::optional<drw::Mesh> m_mesh;
|
||||
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
void try_bind_player_entity();
|
||||
|
||||
/**
|
||||
* Places the player at an authoritative position outright, clearing the
|
||||
* predicted state that led there. Used for the first position the server
|
||||
* sends, which the local simulation has no history to reconcile against.
|
||||
*/
|
||||
void snap_player_to(entt::entity entity, glm::vec3 position);
|
||||
|
||||
/**
|
||||
* Whether the server has placed the player at least once. Entities are
|
||||
* created before their position arrives, so the body starts at the origin
|
||||
* and has to be moved once the first position shows up.
|
||||
*/
|
||||
bool m_player_position_initialized = false;
|
||||
|
||||
/**
|
||||
* When each input was sent, indexed by its frame. Holds the most recent
|
||||
* INPUT_SEND_TIME_COUNT frames; an answer that takes longer than that goes
|
||||
* unmeasured.
|
||||
*/
|
||||
static constexpr size_t INPUT_SEND_TIME_COUNT = 256;
|
||||
|
||||
std::vector<Clock::time_point> m_input_send_times;
|
||||
uint32_t m_last_measured_frame = 0;
|
||||
|
||||
/**
|
||||
* Records how long the answer to `frame_idx` took to arrive, ignoring
|
||||
* frames that were already measured or are too old to still have a send
|
||||
* time.
|
||||
*/
|
||||
void measure_response_time(uint32_t frame_idx);
|
||||
|
||||
HistoryBufferExporter<long, glm::vec3> m_position_history_exporter;
|
||||
|
||||
net::EntityPositionInterpolator m_entity_interpolator;
|
||||
|
||||
net::PlayerReconciler m_reconciler;
|
||||
|
||||
/**
|
||||
* Visual smoothing for render-rate interpolation between 20 Hz ticks.
|
||||
*/
|
||||
glm::vec3 m_render_prev_position{0.0f};
|
||||
glm::vec3 m_render_curr_position{0.0f};
|
||||
double m_tick_accumulator = 0.0;
|
||||
|
||||
/**
|
||||
* Visual error from reconciliation corrections, decays over time.
|
||||
*/
|
||||
glm::vec3 m_visual_error{0.0f};
|
||||
static constexpr double kVisualErrorDecayRate = 12.0;
|
||||
|
||||
/**
|
||||
* Mapping from the server entity_id to local entity_id
|
||||
* Server might have the same entity under different name
|
||||
@@ -65,8 +121,6 @@ class ClientWorldController {
|
||||
|
||||
entt::entity create_entity(const std::string& name, glm::vec3 position);
|
||||
|
||||
net::MessageHandler create_messenger();
|
||||
|
||||
std::optional<entt::entity> map_from_server_entity(int id);
|
||||
|
||||
void map_server_entity(int server_id, entt::entity local_id);
|
||||
@@ -86,7 +140,8 @@ public:
|
||||
World* world,
|
||||
JoltPhysicsWorld* physics_world,
|
||||
drw::WorldRenderer* world_renderer,
|
||||
tw::net::Address address
|
||||
tw::net::Address address,
|
||||
dbg::NetworkMetrics* network_metrics
|
||||
);
|
||||
|
||||
~ClientWorldController();
|
||||
|
||||
Reference in New Issue
Block a user