Initial commit: full project code
This commit is contained in:
5
app/include/app/app_entry.hpp
Normal file
5
app/include/app/app_entry.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#ifndef NETCODE_DEMO_APP_ENTRY_HPP
|
||||
#define NETCODE_DEMO_APP_ENTRY_HPP
|
||||
|
||||
|
||||
#endif //NETCODE_DEMO_APP_ENTRY_HPP
|
||||
54
app/include/app/client_entity.hpp
Normal file
54
app/include/app/client_entity.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/types.hpp"
|
||||
#include "common/math_types.hpp"
|
||||
#include "common/snapshot.hpp"
|
||||
|
||||
namespace netcode {
|
||||
|
||||
// представление сущности на клиенте
|
||||
struct ClientEntity {
|
||||
EntityId id = INVALID_ENTITY_ID;
|
||||
EntityType type = EntityType::None;
|
||||
|
||||
// позиции для разных режимов
|
||||
Vec2 server_position; // подтвержденная
|
||||
Vec2 predicted_position; // предиктнутая
|
||||
Vec2 interpolated_position; // Интерполированная
|
||||
Vec2 render_position; // для рендера
|
||||
|
||||
Vec2 server_velocity;
|
||||
Vec2 predicted_velocity;
|
||||
|
||||
// интерполяция
|
||||
Vec2 prev_position;
|
||||
Vec2 next_position;
|
||||
TickNumber prev_tick = 0;
|
||||
TickNumber next_tick = 0;
|
||||
|
||||
Vec2 correction_offset;
|
||||
|
||||
// Данные сущности
|
||||
float radius = 0.0f;
|
||||
TeamId team = TeamId::None;
|
||||
ClientId owner_id = INVALID_CLIENT_ID;
|
||||
|
||||
// локальный клиент
|
||||
bool is_local_player = false;
|
||||
|
||||
void update_from_state(const EntityState& state) {
|
||||
type = state.type;
|
||||
server_position = state.position;
|
||||
server_velocity = state.velocity;
|
||||
|
||||
if (type == EntityType::Player) {
|
||||
radius = state.data.player.radius;
|
||||
team = state.data.player.team;
|
||||
owner_id = state.data.player.owner_id;
|
||||
} else if (type == EntityType::Ball) {
|
||||
radius = state.data.ball.radius;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
260
app/include/app/client_world.hpp
Normal file
260
app/include/app/client_world.hpp
Normal file
@@ -0,0 +1,260 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/types.hpp"
|
||||
#include "common/math_types.hpp"
|
||||
#include "common/snapshot.hpp"
|
||||
#include "common/simulation_params.hpp"
|
||||
#include "common/compensation_algorithm.hpp"
|
||||
#include "common/metrics.hpp"
|
||||
#include "common/timestamp.hpp"
|
||||
#include "client_entity.hpp"
|
||||
#include "prediction.hpp"
|
||||
#include "reconciliation.hpp"
|
||||
#include "interpolation.hpp"
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace netcode {
|
||||
class LocalSimulator;
|
||||
}
|
||||
|
||||
namespace netcode {
|
||||
|
||||
class ClientWorld {
|
||||
public:
|
||||
explicit ClientWorld(const SimulationParams& params = DEFAULT_SIMULATION_PARAMS)
|
||||
: params_(params)
|
||||
, prediction_(params)
|
||||
, interpolator_(100.0f)
|
||||
, extrapolator_(250.0f)
|
||||
, local_simulator_(params)
|
||||
{}
|
||||
|
||||
void set_local_player(ClientId client_id, EntityId entity_id) {
|
||||
local_client_id_ = client_id;
|
||||
local_entity_id_ = entity_id;
|
||||
}
|
||||
|
||||
void process_local_input(const InputCommand& cmd) {
|
||||
prediction_.add_input(cmd);
|
||||
metrics_.add_prediction();
|
||||
}
|
||||
|
||||
void receive_snapshot(const WorldSnapshot& snapshot) {
|
||||
for (const auto& ack : snapshot.client_acks) {
|
||||
if (ack.client_id == local_client_id_) {
|
||||
last_ack_sequence_ = ack.last_processed_sequence;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
last_server_tick_ = snapshot.server_tick;
|
||||
last_snapshot_time_ = Timestamp::now_us();
|
||||
|
||||
interpolator_.add_snapshot(snapshot);
|
||||
|
||||
SequenceNumber ack_seq = 0;
|
||||
for (const auto& ack : snapshot.client_acks) {
|
||||
if (ack.client_id == local_client_id_) {
|
||||
ack_seq = ack.last_processed_sequence;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& state : snapshot.entities) {
|
||||
auto it = entities_.find(state.id);
|
||||
if (it == entities_.end()) {
|
||||
ClientEntity entity;
|
||||
entity.id = state.id;
|
||||
entity.update_from_state(state);
|
||||
entity.is_local_player = (state.id == local_entity_id_);
|
||||
entity.predicted_position = state.position;
|
||||
entity.predicted_velocity = state.velocity;
|
||||
entity.interpolated_position = state.position;
|
||||
entity.render_position = state.position;
|
||||
entities_[state.id] = entity;
|
||||
} else {
|
||||
ClientEntity& entity = it->second;
|
||||
|
||||
Vec2 old_render_position = entity.render_position;
|
||||
|
||||
entity.update_from_state(state);
|
||||
|
||||
if (entity.is_local_player &&
|
||||
(algorithm_ == CompensationAlgorithm::PredictionReconciliation ||
|
||||
algorithm_ == CompensationAlgorithm::Hybrid)) {
|
||||
|
||||
auto result = reconciliator_.reconcile(
|
||||
entity.predicted_position, entity.predicted_velocity,
|
||||
state.position, state.velocity,
|
||||
ack_seq, prediction_, &metrics_
|
||||
);
|
||||
|
||||
entity.predicted_position = result.new_predicted_pos;
|
||||
entity.predicted_velocity = result.new_predicted_vel;
|
||||
entity.correction_offset = result.correction_delta;
|
||||
}
|
||||
else if (entity.is_local_player &&
|
||||
algorithm_ == CompensationAlgorithm::ClientPrediction) {
|
||||
|
||||
Vec2 pos = state.position;
|
||||
Vec2 vel = state.velocity;
|
||||
|
||||
prediction_.acknowledge(ack_seq);
|
||||
prediction_.predict(pos, vel, ack_seq);
|
||||
|
||||
entity.predicted_position = pos;
|
||||
entity.predicted_velocity = vel;
|
||||
}
|
||||
|
||||
if (entity.is_local_player && algorithm_ != CompensationAlgorithm::None) {
|
||||
metrics_.add_position_error(
|
||||
old_render_position.x, old_render_position.y,
|
||||
state.position.x, state.position.y
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<EntityId> to_remove;
|
||||
for (const auto& [id, entity] : entities_) {
|
||||
if (!snapshot.find_entity(id)) {
|
||||
to_remove.push_back(id);
|
||||
}
|
||||
}
|
||||
for (EntityId id : to_remove) {
|
||||
entities_.erase(id);
|
||||
}
|
||||
|
||||
red_team_time_ = snapshot.red_team_time;
|
||||
blue_team_time_ = snapshot.blue_team_time;
|
||||
}
|
||||
|
||||
void update(double current_time_ms) {
|
||||
auto it = entities_.find(local_entity_id_);
|
||||
if (it != entities_.end() && it->second.is_local_player) {
|
||||
ClientEntity& player = it->second;
|
||||
|
||||
if (algorithm_ == CompensationAlgorithm::ClientPrediction ||
|
||||
algorithm_ == CompensationAlgorithm::PredictionReconciliation ||
|
||||
algorithm_ == CompensationAlgorithm::Hybrid) {
|
||||
|
||||
Vec2 pos = player.server_position;
|
||||
Vec2 vel = player.server_velocity;
|
||||
|
||||
prediction_.predict(pos, vel, last_ack_sequence_);
|
||||
|
||||
player.predicted_position = pos;
|
||||
player.predicted_velocity = vel;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [id, entity] : entities_) {
|
||||
switch (algorithm_) {
|
||||
case CompensationAlgorithm::None:
|
||||
entity.render_position = entity.server_position;
|
||||
break;
|
||||
|
||||
case CompensationAlgorithm::ClientPrediction:
|
||||
case CompensationAlgorithm::PredictionReconciliation:
|
||||
if (entity.is_local_player) {
|
||||
entity.render_position = entity.predicted_position;
|
||||
} else {
|
||||
entity.render_position = entity.server_position;
|
||||
}
|
||||
break;
|
||||
|
||||
case CompensationAlgorithm::EntityInterpolation:
|
||||
entity.render_position = interpolator_.interpolate(id, current_time_ms);
|
||||
break;
|
||||
|
||||
case CompensationAlgorithm::DeadReckoning: {
|
||||
double elapsed = current_time_ms -
|
||||
static_cast<double>(last_snapshot_time_) / 1000.0;
|
||||
entity.render_position = extrapolator_.extrapolate(
|
||||
entity.server_position, entity.server_velocity, elapsed
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
case CompensationAlgorithm::Hybrid:
|
||||
if (entity.is_local_player) {
|
||||
entity.render_position = entity.predicted_position;
|
||||
} else {
|
||||
entity.render_position = interpolator_.interpolate(id, current_time_ms);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
entity.render_position = entity.server_position;
|
||||
break;
|
||||
}
|
||||
|
||||
if (entity.correction_offset.length_squared() > 0.01f) {
|
||||
entity.correction_offset *= 0.9f;
|
||||
entity.render_position += entity.correction_offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void set_algorithm(CompensationAlgorithm algo) {
|
||||
algorithm_ = algo;
|
||||
}
|
||||
|
||||
CompensationAlgorithm algorithm() const { return algorithm_; }
|
||||
|
||||
const std::unordered_map<EntityId, ClientEntity>& entities() const {
|
||||
return entities_;
|
||||
}
|
||||
|
||||
const ClientEntity* get_local_player() const {
|
||||
auto it = entities_.find(local_entity_id_);
|
||||
return it != entities_.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
void set_simulation_params(const SimulationParams& new_params) {
|
||||
params_ = new_params;
|
||||
local_simulator_.params_ = new_params;
|
||||
}
|
||||
|
||||
MetricsCollector& metrics() { return metrics_; }
|
||||
const MetricsCollector& metrics() const { return metrics_; }
|
||||
|
||||
float red_team_time() const { return red_team_time_; }
|
||||
float blue_team_time() const { return blue_team_time_; }
|
||||
|
||||
const SimulationParams& params() const { return params_; }
|
||||
|
||||
void set_interpolation_delay(float ms) { interpolator_.set_delay(ms); }
|
||||
void set_reconciliation_config(const Reconciliator::Config& cfg) {
|
||||
reconciliator_.set_config(cfg);
|
||||
}
|
||||
|
||||
private:
|
||||
SequenceNumber last_ack_sequence_ = 0;
|
||||
|
||||
SimulationParams params_;
|
||||
|
||||
std::unordered_map<EntityId, ClientEntity> entities_;
|
||||
|
||||
ClientId local_client_id_ = INVALID_CLIENT_ID;
|
||||
EntityId local_entity_id_ = INVALID_ENTITY_ID;
|
||||
|
||||
CompensationAlgorithm algorithm_ = CompensationAlgorithm::Hybrid;
|
||||
|
||||
PredictionManager prediction_;
|
||||
Reconciliator reconciliator_;
|
||||
Interpolator interpolator_;
|
||||
Extrapolator extrapolator_;
|
||||
LocalSimulator local_simulator_;
|
||||
|
||||
MetricsCollector metrics_;
|
||||
|
||||
TickNumber last_server_tick_ = 0;
|
||||
uint64_t last_snapshot_time_ = 0;
|
||||
|
||||
float red_team_time_ = 0.0f;
|
||||
float blue_team_time_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace netcode
|
||||
150
app/include/app/interpolation.hpp
Normal file
150
app/include/app/interpolation.hpp
Normal file
@@ -0,0 +1,150 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/types.hpp"
|
||||
#include "common/math_types.hpp"
|
||||
#include "common/snapshot.hpp"
|
||||
#include "common/simulation_params.hpp"
|
||||
#include "common/ring_buffer.hpp"
|
||||
#include "client_entity.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace netcode {
|
||||
|
||||
class Interpolator {
|
||||
public:
|
||||
explicit Interpolator(float delay_ms = 100.0f)
|
||||
: interpolation_delay_ms_(delay_ms) {}
|
||||
|
||||
void set_delay(float delay_ms) {
|
||||
interpolation_delay_ms_ = delay_ms;
|
||||
}
|
||||
|
||||
// новый снапшот в кольцевой буфер
|
||||
void add_snapshot(const WorldSnapshot& snapshot) {
|
||||
snapshots_.push(snapshot);
|
||||
}
|
||||
|
||||
// основная интерполяция с fallback на экстраполяцию
|
||||
Vec2 interpolate(EntityId entity_id, double current_time_ms) const {
|
||||
double render_time = current_time_ms - interpolation_delay_ms_;
|
||||
|
||||
const WorldSnapshot* before = nullptr;
|
||||
const WorldSnapshot* after = nullptr;
|
||||
|
||||
// ищем ближайшие снапшоты слева и справа от render_time
|
||||
for (size_t i = 0; i < snapshots_.size(); ++i) {
|
||||
const WorldSnapshot* snap = snapshots_.at(i);
|
||||
if (!snap) continue;
|
||||
|
||||
double snap_time = static_cast<double>(snap->timestamp_us) / 1000.0;
|
||||
|
||||
if (snap_time <= render_time) {
|
||||
if (!before || snap_time > static_cast<double>(before->timestamp_us) / 1000.0) {
|
||||
before = snap;
|
||||
}
|
||||
}
|
||||
if (snap_time >= render_time) {
|
||||
if (!after || snap_time < static_cast<double>(after->timestamp_us) / 1000.0) {
|
||||
after = snap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!before && !after) return Vec2(); // пусто
|
||||
|
||||
// только будущий снапшот - экстраполируем назад (редко)
|
||||
if (!before) {
|
||||
const auto* e = after->find_entity(entity_id);
|
||||
if (!e) return Vec2();
|
||||
double dt = (render_time - static_cast<double>(after->timestamp_us)/1000.0) / 1000.0;
|
||||
return e->position + e->velocity * static_cast<float>(dt);
|
||||
}
|
||||
|
||||
// только старый - экстраполируем вперед
|
||||
if (!after) {
|
||||
const auto* e = before->find_entity(entity_id);
|
||||
if (!e) return Vec2();
|
||||
|
||||
double dt = (render_time - static_cast<double>(before->timestamp_us)/1000.0) / 1000.0;
|
||||
dt = (std::min)(dt, 0.1); // лимит 100мс
|
||||
|
||||
return e->position + e->velocity * static_cast<float>(dt);
|
||||
}
|
||||
|
||||
// интерполяция
|
||||
const auto* eb = before->find_entity(entity_id);
|
||||
const auto* ea = after->find_entity(entity_id);
|
||||
|
||||
if (!eb || !ea) {
|
||||
return eb ? eb->position : (ea ? ea->position : Vec2());
|
||||
}
|
||||
|
||||
double t1 = static_cast<double>(before->timestamp_us) / 1000.0;
|
||||
double t2 = static_cast<double>(after->timestamp_us) / 1000.0;
|
||||
|
||||
double t = 0.0;
|
||||
if (t2 > t1) {
|
||||
t = (render_time - t1) / (t2 - t1);
|
||||
t = std::clamp(t, 0.0, 1.0);
|
||||
}
|
||||
|
||||
// hermite вместо линейной для более плавного движения
|
||||
return hermite_interpolate(
|
||||
eb->position, eb->velocity,
|
||||
ea->position, ea->velocity,
|
||||
static_cast<float>(t)
|
||||
);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
snapshots_.clear();
|
||||
}
|
||||
|
||||
float get_delay() const { return interpolation_delay_ms_; }
|
||||
|
||||
private:
|
||||
// hermite - пытаемся учесть скорость на концах
|
||||
Vec2 hermite_interpolate(const Vec2& p0, const Vec2& v0,
|
||||
const Vec2& p1, const Vec2& v1,
|
||||
float t) const {
|
||||
float t2 = t*t;
|
||||
float t3 = t2*t;
|
||||
|
||||
float h00 = 2*t3 - 3*t2 + 1;
|
||||
float h10 = t3 - 2*t2 + t;
|
||||
float h01 = -2*t3 + 3*t2;
|
||||
float h11 = t3 - t2;
|
||||
|
||||
// грубая оценка, dt между снапшотами ~50мс
|
||||
float dt = 0.05f;
|
||||
Vec2 m0 = v0 * dt;
|
||||
Vec2 m1 = v1 * dt;
|
||||
|
||||
return p0*h00 + m0*h10 + p1*h01 + m1*h11;
|
||||
}
|
||||
|
||||
RingBuffer<WorldSnapshot, 64> snapshots_;
|
||||
float interpolation_delay_ms_;
|
||||
};
|
||||
|
||||
// простая линейная экстраполяция с отсечкой
|
||||
class Extrapolator {
|
||||
public:
|
||||
explicit Extrapolator(float max_time_ms = 250.0f)
|
||||
: max_extrapolation_ms_(max_time_ms) {}
|
||||
|
||||
Vec2 extrapolate(const Vec2& position, const Vec2& velocity,
|
||||
double elapsed_ms) const {
|
||||
double t = (std::min)(elapsed_ms, static_cast<double>(max_extrapolation_ms_));
|
||||
return position + velocity * static_cast<float>(t / 1000.0);
|
||||
}
|
||||
|
||||
void set_max_time(float max_ms) {
|
||||
max_extrapolation_ms_ = max_ms;
|
||||
}
|
||||
|
||||
private:
|
||||
float max_extrapolation_ms_;
|
||||
};
|
||||
|
||||
}
|
||||
302
app/include/app/network_simulation.hpp
Normal file
302
app/include/app/network_simulation.hpp
Normal file
@@ -0,0 +1,302 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/types.hpp"
|
||||
#include "common/timestamp.hpp"
|
||||
#include <queue>
|
||||
#include <random>
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
|
||||
namespace netcode {
|
||||
|
||||
// Пакет с искусственной задержкой
|
||||
struct DelayedPacket {
|
||||
std::vector<uint8_t> data;
|
||||
uint64_t delivery_time_us; // когда доставить
|
||||
|
||||
DelayedPacket(const uint8_t* packet_data, size_t size, uint64_t delivery)
|
||||
: data(packet_data, packet_data + size)
|
||||
, delivery_time_us(delivery)
|
||||
{}
|
||||
};
|
||||
|
||||
// Симулятор сетевых условий
|
||||
class NetworkSimulator {
|
||||
public:
|
||||
struct Config {
|
||||
bool enabled = false;
|
||||
|
||||
// Задержка (ping/2)
|
||||
float base_latency_ms = 0.0f;
|
||||
|
||||
// Джиттер
|
||||
float jitter_ms = 0.0f; // максимальное отклонение
|
||||
|
||||
// Потеря пакетов
|
||||
float packet_loss = 0.0f; // 0.0 - 1.0 (вероятность)
|
||||
|
||||
// Дубликация пакетов
|
||||
float packet_duplication = 0.0f; // 0.0 - 1.0 (вероятность)
|
||||
|
||||
// Искусственный спайк
|
||||
bool spike_enabled = false;
|
||||
float spike_duration_ms = 0.0f;
|
||||
float spike_delay_ms = 0.0f;
|
||||
};
|
||||
|
||||
explicit NetworkSimulator(const Config& config = Config{})
|
||||
: config_(config)
|
||||
, rng_(std::random_device{}())
|
||||
, jitter_dist_(0.0f, 1.0f)
|
||||
, loss_dist_(0.0f, 1.0f)
|
||||
{}
|
||||
|
||||
// Добавить пакет в очередь с задержкой
|
||||
void send_packet(const uint8_t* data, size_t size) {
|
||||
if (!config_.enabled) {
|
||||
// Режим без симуляции
|
||||
immediate_packets_.emplace(data, size, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Потеря пакета
|
||||
if (config_.packet_loss > 0.0f && loss_dist_(rng_) < config_.packet_loss) {
|
||||
packets_lost_++;
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t now = Timestamp::now_us();
|
||||
float delay_ms = calculate_delay();
|
||||
uint64_t delivery_time = now + static_cast<uint64_t>(delay_ms * 1000.0f);
|
||||
|
||||
delayed_packets_.emplace(data, size, delivery_time);
|
||||
packets_sent_++;
|
||||
|
||||
// Дубликация пакета
|
||||
if (config_.packet_duplication > 0.0f &&
|
||||
loss_dist_(rng_) < config_.packet_duplication) {
|
||||
// Дубликат с дополнительной задержкой
|
||||
float dup_delay = delay_ms + (jitter_dist_(rng_) * 10.0f);
|
||||
uint64_t dup_time = now + static_cast<uint64_t>(dup_delay * 1000.0f);
|
||||
delayed_packets_.emplace(data, size, dup_time);
|
||||
packets_duplicated_++;
|
||||
}
|
||||
}
|
||||
|
||||
// Получить готовые пакеты
|
||||
std::vector<std::vector<uint8_t>> receive_packets() {
|
||||
std::vector<std::vector<uint8_t>> ready;
|
||||
uint64_t now = Timestamp::now_us();
|
||||
|
||||
// Без симуляции
|
||||
if (!config_.enabled) {
|
||||
while (!immediate_packets_.empty()) {
|
||||
ready.push_back(std::move(immediate_packets_.front().data));
|
||||
immediate_packets_.pop();
|
||||
}
|
||||
return ready;
|
||||
}
|
||||
|
||||
// С симуляцией
|
||||
while (!delayed_packets_.empty()) {
|
||||
const auto& packet = delayed_packets_.top();
|
||||
|
||||
if (packet.delivery_time_us <= now) {
|
||||
ready.push_back(packet.data);
|
||||
delayed_packets_.pop();
|
||||
packets_delivered_++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ready;
|
||||
}
|
||||
|
||||
void trigger_lag_spike(float duration_ms, float delay_ms) {
|
||||
spike_start_time_ = Timestamp::now_us();
|
||||
spike_duration_us_ = static_cast<uint64_t>(duration_ms * 1000.0f);
|
||||
spike_delay_us_ = static_cast<uint64_t>(delay_ms * 1000.0f);
|
||||
spike_active_ = true;
|
||||
}
|
||||
|
||||
void update_config(const Config& config) {
|
||||
config_ = config;
|
||||
}
|
||||
|
||||
const Config& config() const { return config_; }
|
||||
|
||||
// Статистика
|
||||
struct Stats {
|
||||
uint64_t packets_sent = 0;
|
||||
uint64_t packets_delivered = 0;
|
||||
uint64_t packets_lost = 0;
|
||||
uint64_t packets_duplicated = 0;
|
||||
size_t packets_in_queue = 0;
|
||||
float current_latency_ms = 0.0f;
|
||||
};
|
||||
|
||||
Stats get_stats() const {
|
||||
Stats stats;
|
||||
stats.packets_sent = packets_sent_;
|
||||
stats.packets_delivered = packets_delivered_;
|
||||
stats.packets_lost = packets_lost_;
|
||||
stats.packets_duplicated = packets_duplicated_;
|
||||
stats.packets_in_queue = delayed_packets_.size();
|
||||
stats.current_latency_ms = config_.base_latency_ms;
|
||||
return stats;
|
||||
}
|
||||
|
||||
void reset_stats() {
|
||||
packets_sent_ = 0;
|
||||
packets_delivered_ = 0;
|
||||
packets_lost_ = 0;
|
||||
packets_duplicated_ = 0;
|
||||
}
|
||||
bool is_enabled() const { return config_.enabled; }
|
||||
|
||||
float get_simulated_rtt_ms() const {
|
||||
return config_.base_latency_ms * 2.0f;
|
||||
}
|
||||
|
||||
private:
|
||||
float calculate_delay() {
|
||||
float delay = config_.base_latency_ms;
|
||||
|
||||
// случайное отклонение
|
||||
if (config_.jitter_ms > 0.0f) {
|
||||
// Джиттер в диапазоне -jitter +jitter
|
||||
float jitter = (jitter_dist_(rng_) * 2.0f - 1.0f) * config_.jitter_ms;
|
||||
delay += jitter;
|
||||
}
|
||||
|
||||
// Lag spike
|
||||
if (spike_active_) {
|
||||
uint64_t now = Timestamp::now_us();
|
||||
uint64_t elapsed = now - spike_start_time_;
|
||||
|
||||
if (elapsed < spike_duration_us_) {
|
||||
delay += static_cast<float>(spike_delay_us_) / 1000.0f;
|
||||
} else {
|
||||
spike_active_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Минимум 0
|
||||
return (std::max)(0.0f, delay);
|
||||
}
|
||||
|
||||
struct PacketCompare {
|
||||
bool operator()(const DelayedPacket& a, const DelayedPacket& b) const {
|
||||
return a.delivery_time_us > b.delivery_time_us;
|
||||
}
|
||||
};
|
||||
|
||||
Config config_;
|
||||
|
||||
// очереди пакетов
|
||||
std::priority_queue<DelayedPacket, std::vector<DelayedPacket>, PacketCompare> delayed_packets_;
|
||||
std::queue<DelayedPacket> immediate_packets_;
|
||||
|
||||
std::mt19937 rng_;
|
||||
std::uniform_real_distribution<float> jitter_dist_;
|
||||
std::uniform_real_distribution<float> loss_dist_;
|
||||
|
||||
bool spike_active_ = false;
|
||||
uint64_t spike_start_time_ = 0;
|
||||
uint64_t spike_duration_us_ = 0;
|
||||
uint64_t spike_delay_us_ = 0;
|
||||
|
||||
// Статистика
|
||||
uint64_t packets_sent_ = 0;
|
||||
uint64_t packets_delivered_ = 0;
|
||||
uint64_t packets_lost_ = 0;
|
||||
uint64_t packets_duplicated_ = 0;
|
||||
};
|
||||
|
||||
// пресеты
|
||||
namespace NetworkPresets {
|
||||
inline NetworkSimulator::Config Perfect() {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = false
|
||||
};
|
||||
}
|
||||
|
||||
inline NetworkSimulator::Config LAN() {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = true,
|
||||
.base_latency_ms = 2.0f,
|
||||
.jitter_ms = 1.0f,
|
||||
.packet_loss = 0.001f
|
||||
};
|
||||
}
|
||||
|
||||
inline NetworkSimulator::Config GoodBroadband() {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = true,
|
||||
.base_latency_ms = 25.0f,
|
||||
.jitter_ms = 5.0f,
|
||||
.packet_loss = 0.01f
|
||||
};
|
||||
}
|
||||
|
||||
inline NetworkSimulator::Config AverageBroadband() {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = true,
|
||||
.base_latency_ms = 50.0f,
|
||||
.jitter_ms = 10.0f,
|
||||
.packet_loss = 0.02f
|
||||
};
|
||||
}
|
||||
|
||||
inline NetworkSimulator::Config PoorBroadband() {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = true,
|
||||
.base_latency_ms = 100.0f,
|
||||
.jitter_ms = 25.0f,
|
||||
.packet_loss = 0.05f
|
||||
};
|
||||
}
|
||||
|
||||
inline NetworkSimulator::Config Mobile4G() {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = true,
|
||||
.base_latency_ms = 75.0f,
|
||||
.jitter_ms = 30.0f,
|
||||
.packet_loss = 0.03f,
|
||||
.packet_duplication = 0.005f
|
||||
};
|
||||
}
|
||||
|
||||
inline NetworkSimulator::Config Mobile3G() {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = true,
|
||||
.base_latency_ms = 150.0f,
|
||||
.jitter_ms = 50.0f,
|
||||
.packet_loss = 0.08f,
|
||||
.packet_duplication = 0.01f
|
||||
};
|
||||
}
|
||||
|
||||
inline NetworkSimulator::Config Satellite() {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = true,
|
||||
.base_latency_ms = 300.0f,
|
||||
.jitter_ms = 100.0f,
|
||||
.packet_loss = 0.05f
|
||||
};
|
||||
}
|
||||
|
||||
inline NetworkSimulator::Config Custom(float latency_ms, float jitter_ms,
|
||||
float loss = 0.0f, float duplication = 0.0f) {
|
||||
return NetworkSimulator::Config{
|
||||
.enabled = true,
|
||||
.base_latency_ms = latency_ms,
|
||||
.jitter_ms = jitter_ms,
|
||||
.packet_loss = loss,
|
||||
.packet_duplication = duplication
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace netcode
|
||||
123
app/include/app/prediction.hpp
Normal file
123
app/include/app/prediction.hpp
Normal file
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/types.hpp"
|
||||
#include "common/math_types.hpp"
|
||||
#include "common/input_command.hpp"
|
||||
#include "common/simulation_params.hpp"
|
||||
#include "common/ring_buffer.hpp"
|
||||
#include <vector>
|
||||
|
||||
namespace netcode {
|
||||
|
||||
// локальная физика игрока (только для предсказания)
|
||||
class LocalSimulator {
|
||||
public:
|
||||
explicit LocalSimulator(const SimulationParams& params = DEFAULT_SIMULATION_PARAMS)
|
||||
: params_(params) {}
|
||||
|
||||
// один тик симуляции по вводу
|
||||
void simulate_player(Vec2& position, Vec2& velocity,
|
||||
const InputState& input, float dt) {
|
||||
Vec2 dir = input.get_direction();
|
||||
Vec2 accel = dir * params_.player_acceleration;
|
||||
|
||||
// трение
|
||||
velocity -= velocity * params_.player_friction * dt;
|
||||
|
||||
// разгон
|
||||
velocity += accel * dt;
|
||||
|
||||
// ограничение скорости
|
||||
float len = velocity.length();
|
||||
if (len > params_.player_max_speed) {
|
||||
velocity = velocity.normalized() * params_.player_max_speed;
|
||||
}
|
||||
|
||||
// движение
|
||||
position += velocity * dt;
|
||||
|
||||
// отскок от стен
|
||||
constrain_to_arena(position, velocity, params_.player_radius);
|
||||
}
|
||||
|
||||
// обработка стен (отражение с потерей энергии)
|
||||
void constrain_to_arena(Vec2& position, Vec2& velocity, float radius) {
|
||||
float minx = params_.wall_thickness + radius;
|
||||
float maxx = params_.arena_width - params_.wall_thickness - radius;
|
||||
float miny = params_.wall_thickness + radius;
|
||||
float maxy = params_.arena_height - params_.wall_thickness - radius;
|
||||
|
||||
if (position.x < minx) { position.x = minx; velocity.x = -velocity.x * params_.collision_restitution; }
|
||||
if (position.x > maxx) { position.x = maxx; velocity.x = -velocity.x * params_.collision_restitution; }
|
||||
if (position.y < miny) { position.y = miny; velocity.y = -velocity.y * params_.collision_restitution; }
|
||||
if (position.y > maxy) { position.y = maxy; velocity.y = -velocity.y * params_.collision_restitution; }
|
||||
}
|
||||
|
||||
const SimulationParams& params() const { return params_; }
|
||||
|
||||
SimulationParams params_;
|
||||
};
|
||||
|
||||
// хранит и воспроизводит очередь неподтверждённых вводов
|
||||
class PredictionManager {
|
||||
public:
|
||||
explicit PredictionManager(const SimulationParams& params = DEFAULT_SIMULATION_PARAMS)
|
||||
: simulator_(params) {}
|
||||
|
||||
// новая команда игрока в очередь
|
||||
void add_input(const InputCommand& cmd) {
|
||||
pending_inputs_.push(cmd);
|
||||
}
|
||||
|
||||
// прогнать все неподтверждённые вводы поверх текущего состояния
|
||||
void predict(Vec2& position, Vec2& velocity,
|
||||
SequenceNumber last_ack_sequence) {
|
||||
float dt = static_cast<float>(simulator_.params().fixed_delta_time);
|
||||
|
||||
for (size_t i = 0; i < pending_inputs_.size(); ++i) {
|
||||
const InputCommand* cmd = pending_inputs_.at(i);
|
||||
if (!cmd) continue;
|
||||
|
||||
// уже подтверждённые пропускаем
|
||||
if (cmd->sequence <= last_ack_sequence) continue;
|
||||
|
||||
simulator_.simulate_player(position, velocity, cmd->input, dt);
|
||||
}
|
||||
}
|
||||
|
||||
// убираем всё что сервер уже обработал
|
||||
void acknowledge(SequenceNumber sequence) {
|
||||
while (!pending_inputs_.empty()) {
|
||||
const InputCommand* cmd = pending_inputs_.peek();
|
||||
if (cmd && cmd->sequence <= sequence) {
|
||||
pending_inputs_.pop();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// для отладки/логов/отправки на сервер при необходимости
|
||||
std::vector<InputCommand> get_unacknowledged(SequenceNumber last_ack) const {
|
||||
std::vector<InputCommand> result;
|
||||
for (size_t i = 0; i < pending_inputs_.size(); ++i) {
|
||||
const InputCommand* cmd = pending_inputs_.at(i);
|
||||
if (cmd && cmd->sequence > last_ack) {
|
||||
result.push_back(*cmd);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
pending_inputs_.clear();
|
||||
}
|
||||
|
||||
size_t pending_count() const { return pending_inputs_.size(); }
|
||||
|
||||
private:
|
||||
LocalSimulator simulator_;
|
||||
RingBuffer<InputCommand, 128> pending_inputs_; // очередь последних вводов
|
||||
};
|
||||
|
||||
} // namespace netcode
|
||||
79
app/include/app/reconciliation.hpp
Normal file
79
app/include/app/reconciliation.hpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include "common/types.hpp"
|
||||
#include "common/math_types.hpp"
|
||||
#include "common/input_command.hpp"
|
||||
#include "common/snapshot.hpp"
|
||||
#include "common/metrics.hpp"
|
||||
#include "prediction.hpp"
|
||||
|
||||
namespace netcode {
|
||||
|
||||
class Reconciliator {
|
||||
public:
|
||||
struct Config {
|
||||
float instant_threshold = 5.0f; // TODO: реализовать логику
|
||||
};
|
||||
|
||||
struct ReconcileResult {
|
||||
Vec2 new_predicted_pos;
|
||||
Vec2 new_predicted_vel;
|
||||
Vec2 correction_delta;
|
||||
};
|
||||
|
||||
explicit Reconciliator(const Config& config = Config{})
|
||||
: config_(config) {}
|
||||
|
||||
// основная точка сверки предсказания с сервером
|
||||
ReconcileResult reconcile(
|
||||
const Vec2& old_predicted_pos,
|
||||
const Vec2& old_predicted_vel, // не используется
|
||||
const Vec2& server_position,
|
||||
const Vec2& server_velocity,
|
||||
SequenceNumber server_ack,
|
||||
PredictionManager& prediction,
|
||||
MetricsCollector* metrics = nullptr
|
||||
) {
|
||||
// берём серверное состояние как базу
|
||||
Vec2 pos = server_position;
|
||||
Vec2 vel = server_velocity;
|
||||
|
||||
// говорим предсказателю что до этого момента всё подтверждено
|
||||
prediction.acknowledge(server_ack);
|
||||
|
||||
// и сразу прогоняем все оставшиеся неподтверждённые вводы
|
||||
prediction.predict(pos, vel, server_ack);
|
||||
|
||||
// считаем насколько сильно разошлось старое предсказание
|
||||
Vec2 delta = pos - old_predicted_pos;
|
||||
float error = delta.length();
|
||||
|
||||
// метрики
|
||||
if (metrics) {
|
||||
metrics->add_reconciliation();
|
||||
metrics->add_position_error(
|
||||
old_predicted_pos.x, old_predicted_pos.y,
|
||||
pos.x, pos.y
|
||||
);
|
||||
if (error > 0.01f) {
|
||||
metrics->add_correction(error);
|
||||
}
|
||||
}
|
||||
|
||||
// возвращаем новое предсказанное состояние + вектор коррекции
|
||||
return { pos, vel, delta };
|
||||
}
|
||||
|
||||
void set_config(const Config& cfg) {
|
||||
config_ = cfg;
|
||||
}
|
||||
|
||||
const Config& config() const {
|
||||
return config_;
|
||||
}
|
||||
|
||||
private:
|
||||
Config config_; // (не используется)
|
||||
};
|
||||
|
||||
}
|
||||
15
app/src/app_entry.cpp
Normal file
15
app/src/app_entry.cpp
Normal file
@@ -0,0 +1,15 @@
|
||||
#include "app/app_entry.hpp"
|
||||
#include "app/client_world.hpp"
|
||||
#include "app/client_entity.hpp"
|
||||
#include "app/prediction.hpp"
|
||||
#include "app/reconciliation.hpp"
|
||||
#include "app/interpolation.hpp"
|
||||
|
||||
namespace netcode {
|
||||
|
||||
// Placeholder для линковки DLL
|
||||
void app_init() {
|
||||
// Инициализация библиотеки app при необходимости
|
||||
}
|
||||
|
||||
} // namespace netcode
|
||||
Reference in New Issue
Block a user