Initial commit: full project code

This commit is contained in:
Oleg Ermakov
2026-01-11 01:37:39 +04:00
commit a8d725e94b
34 changed files with 4891 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
#ifndef NETCODE_DEMO_APP_ENTRY_HPP
#define NETCODE_DEMO_APP_ENTRY_HPP
#endif //NETCODE_DEMO_APP_ENTRY_HPP

View 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;
}
}
};
}

View 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

View 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_;
};
}

View 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

View 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

View 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
View 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