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_COMMON_ENTRY_HPP
#define NETCODE_DEMO_COMMON_ENTRY_HPP
#endif //NETCODE_DEMO_COMMON_ENTRY_HPP

View File

@@ -0,0 +1,59 @@
#pragma once
#include <cstdint>
#include <string>
#include <array>
namespace netcode {
enum class CompensationAlgorithm : uint8_t {
None = 0, // Без компенсации
ClientPrediction, // Client-Side Prediction
PredictionReconciliation, // Prediction + Reconciliation
EntityInterpolation, // Entity Interpolation
DeadReckoning, // Dead Reckoning / Экстраполяция
Hybrid, // Prediction для локального, интерполяция для остальных
ServerLagCompensation, // Server-Side Lag Compensation
COUNT
};
inline constexpr std::array<const char*, static_cast<size_t>(CompensationAlgorithm::COUNT)>
ALGORITHM_NAMES = {
"None",
"Client Prediction",
"Prediction + Reconciliation",
"Entity Interpolation",
"Dead Reckoning",
"Hybrid",
"Server Lag Compensation"
};
inline const char* algorithm_name(CompensationAlgorithm algo) {
auto idx = static_cast<size_t>(algo);
if (idx < ALGORITHM_NAMES.size()) {
return ALGORITHM_NAMES[idx];
}
return "Unknown";
}
// Настройки алгоритмов
struct AlgorithmSettings {
// Интерполяция
float interpolation_delay_ms = 100.0f; // Задержка интерполяции
// Prediction
uint32_t max_prediction_ticks = 30; // Максимум тиков предсказания
// Reconciliation
float correction_smoothing = 0.1f; // Сглаживание коррекции (0-1)
float correction_threshold = 0.5f; // Порог для мгновенной коррекции
// Dead Reckoning
float extrapolation_limit_ms = 250.0f; // Лимит экстраполяции
// Server Lag Compensation
float max_rewind_ms = 200.0f; // Максимум отката на сервере
};
} // namespace netcode

View File

@@ -0,0 +1,164 @@
#pragma once
#include <enet.h>
#include <memory>
#include <functional>
#include <iostream>
#include <string>
#include <stdexcept>
#include <vector>
#include "types.hpp"
#include "network_channel.hpp"
#include <ws2tcpip.h>
namespace netcode {
// инициализация
class ENetInitializer {
public:
ENetInitializer() {
if (enet_initialize() != 0) {
throw std::runtime_error("Failed to initialize ENet");
}
}
~ENetInitializer() {
enet_deinitialize();
}
ENetInitializer(const ENetInitializer&) = delete;
ENetInitializer& operator=(const ENetInitializer&) = delete;
};
// Настройки симуляции сети
struct NetworkConditions {
uint32_t min_latency_ms = 0;
uint32_t max_latency_ms = 0;
float packet_loss_percent = 0.0f;
float duplicate_percent = 0.0f;
bool is_enabled() const {
return min_latency_ms > 0 || max_latency_ms > 0 ||
packet_loss_percent > 0 || duplicate_percent > 0;
}
};
// хост враппер
class NetworkHost {
public:
NetworkHost() = default;
~NetworkHost() {
if (host_) {
enet_host_destroy(host_);
}
}
bool create_server(uint16_t port, size_t max_clients) {
ENetAddress address;
std::memset(&address, 0, sizeof(address));
// address.host = ENET_HOST_ANY;
address.port = port;
host_ = enet_host_create(&address, max_clients, CHANNEL_COUNT, 0, 0);
if (!host_) {
#ifdef _WIN32
std::cerr << "enet_host_create failed, WSA error: " << WSAGetLastError() << "\n";
#else
std::cerr << "enet_host_create failed, errno: " << errno << "\n";
#endif
return false;
}
return true;
}
bool create_client() {
host_ = enet_host_create(nullptr, 1, CHANNEL_COUNT, 0, 0);
if (!host_) {
#ifdef _WIN32
std::cerr << "create_client failed, WSA error: " << WSAGetLastError() << "\n";
#endif
return false;
}
return true;
}
ENetPeer* connect(const std::string& hostname, uint16_t port) {
ENetAddress address;
std::memset(&address, 0, sizeof(address));
int result = enet_address_set_host_ip(&address, hostname.c_str());
if (result != 0) {
std::cout << "Trying DNS lookup for: " << hostname << "\n";
result = enet_address_set_host(&address, hostname.c_str());
}
if (result != 0) {
std::cerr << "Failed to resolve address: " << hostname << "\n";
#ifdef _WIN32
std::cerr << "WSA error: " << WSAGetLastError() << "\n";
#endif
return nullptr;
}
address.port = port;
std::cout << "Resolved address - host: ";
char ipv6_str[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &address.host, ipv6_str, INET6_ADDRSTRLEN);
std::cout << ipv6_str;
std::cout << ", port: " << address.port << "\n";
ENetPeer* peer = enet_host_connect(host_, &address, CHANNEL_COUNT, 0);
if (!peer) {
std::cerr << "enet_host_connect failed\n";
#ifdef _WIN32
std::cerr << "WSA error: " << WSAGetLastError() << "\n";
#endif
}
return peer;
}
// Обработка событий
int service(ENetEvent& event, uint32_t timeout_ms = 0) {
return enet_host_service(host_, &event, timeout_ms);
}
// Отправка
void send(ENetPeer* peer, NetworkChannel channel, const void* data, size_t size, bool reliable) {
uint32_t flags = reliable ? ENET_PACKET_FLAG_RELIABLE : 0;
ENetPacket* packet = enet_packet_create(data, size, flags);
enet_peer_send(peer, static_cast<uint8_t>(channel), packet);
}
void broadcast(NetworkChannel channel, const void* data, size_t size, bool reliable) {
uint32_t flags = reliable ? ENET_PACKET_FLAG_RELIABLE : 0;
ENetPacket* packet = enet_packet_create(data, size, flags);
enet_host_broadcast(host_, static_cast<uint8_t>(channel), packet);
}
void flush() {
enet_host_flush(host_);
}
// Симуляция сетевых условий (применяется к peer)
void apply_conditions(ENetPeer* peer, const NetworkConditions& conditions) {
if (conditions.is_enabled()) {
// реализовано через троттлинг
}
}
ENetHost* raw() { return host_; }
const ENetHost* raw() const { return host_; }
private:
ENetHost* host_ = nullptr;
};
} // namespace netcode

View File

@@ -0,0 +1,64 @@
#pragma once
#include "types.hpp"
#include "math_types.hpp"
#include <cstdint>
namespace netcode {
// Состояние ввода
struct InputState {
bool move_up = false;
bool move_down = false;
bool move_left = false;
bool move_right = false;
Vec2 get_direction() const {
Vec2 dir;
if (move_up) dir.y -= 1.0f;
if (move_down) dir.y += 1.0f;
if (move_left) dir.x -= 1.0f;
if (move_right) dir.x += 1.0f;
if (dir.length_squared() > 0.0f) {
dir = dir.normalized();
}
return dir;
}
bool any_input() const {
return move_up || move_down || move_left || move_right;
}
uint8_t to_byte() const {
uint8_t result = 0;
if (move_up) result |= 0x01;
if (move_down) result |= 0x02;
if (move_left) result |= 0x04;
if (move_right) result |= 0x08;
return result;
}
static InputState from_byte(uint8_t byte) {
InputState state;
state.move_up = (byte & 0x01) != 0;
state.move_down = (byte & 0x02) != 0;
state.move_left = (byte & 0x04) != 0;
state.move_right = (byte & 0x08) != 0;
return state;
}
};
// Команда клиента
struct InputCommand {
SequenceNumber sequence = 0; // Порядковый номер команды
TickNumber client_tick = 0; // Тик клиента
uint64_t timestamp_us = 0; // Временная метка (микросекунды)
InputState input; // Состояние ввода
// Для reconciliation
Vec2 predicted_position; // Предсказанная позиция после команды
Vec2 predicted_velocity; // Предсказанная скорость
};
} // namespace netcode

View File

@@ -0,0 +1,76 @@
#pragma once
#include <cmath>
#include <algorithm>
namespace netcode {
struct Vec2 {
float x = 0.0f;
float y = 0.0f;
Vec2() = default;
Vec2(float x_, float y_) : x(x_), y(y_) {}
Vec2 operator+(const Vec2& other) const { return {x + other.x, y + other.y}; }
Vec2 operator-(const Vec2& other) const { return {x - other.x, y - other.y}; }
Vec2 operator*(float scalar) const { return {x * scalar, y * scalar}; }
Vec2 operator/(float scalar) const { return {x / scalar, y / scalar}; }
Vec2& operator+=(const Vec2& other) { x += other.x; y += other.y; return *this; }
Vec2& operator-=(const Vec2& other) { x -= other.x; y -= other.y; return *this; }
Vec2& operator*=(float scalar) { x *= scalar; y *= scalar; return *this; }
float length() const { return std::sqrt(x * x + y * y); }
float length_squared() const { return x * x + y * y; }
Vec2 normalized() const {
float len = length();
if (len > 0.0001f) return *this / len;
return {0, 0};
}
static float dot(const Vec2& a, const Vec2& b) {
return a.x * b.x + a.y * b.y;
}
static float distance(const Vec2& a, const Vec2& b) {
return (a - b).length();
}
static Vec2 lerp(const Vec2& a, const Vec2& b, float t) {
return a + (b - a) * t;
}
};
struct Rect {
float x = 0.0f;
float y = 0.0f;
float width = 0.0f;
float height = 0.0f;
bool contains(const Vec2& point) const {
return point.x >= x && point.x <= x + width &&
point.y >= y && point.y <= y + height;
}
Vec2 center() const {
return {x + width / 2.0f, y + height / 2.0f};
}
};
struct Circle {
Vec2 center;
float radius = 0.0f;
bool contains(const Vec2& point) const {
return Vec2::distance(center, point) <= radius;
}
bool intersects(const Circle& other) const {
float dist = Vec2::distance(center, other.center);
return dist <= (radius + other.radius);
}
};
} // namespace netcode

View File

@@ -0,0 +1,204 @@
#pragma once
#include "types.hpp"
#include "ring_buffer.hpp"
#include <cmath>
#include <algorithm>
#include <fstream>
#include <string>
#include <iomanip>
namespace netcode {
// Метрики позиции
struct PositionMetrics {
double mae = 0.0; // Mean Absolute Error
double mse = 0.0; // Mean Squared Error
double max_error = 0.0; // Максимальная ошибка
uint32_t sample_count = 0;
};
// Метрики сети
struct NetworkMetrics {
double rtt_ms = 0.0;
double jitter_ms = 0.0;
double packet_loss_percent = 0.0;
uint64_t bytes_sent = 0;
uint64_t bytes_received = 0;
uint64_t packets_sent = 0;
uint64_t packets_received = 0;
};
// Метрики алгоритмов компенсации
struct CompensationMetrics {
uint32_t predictions_per_second = 0;
uint32_t reconciliations_per_second = 0;
double avg_correction_distance = 0.0;
double max_correction_distance = 0.0;
uint32_t rollbacks_count = 0;
double input_delay_ms = 0.0;
};
// Накопитель метрик
class MetricsCollector {
public:
void add_position_error(float predicted_x, float predicted_y,
float actual_x, float actual_y) {
float dx = predicted_x - actual_x;
float dy = predicted_y - actual_y;
float error = std::sqrt(dx * dx + dy * dy);
position_errors_.push(error);
total_error_ += error;
total_squared_error_ += error * error;
max_error_ = (std::max)(max_error_, static_cast<double>(error));
error_count_++;
}
void add_correction(float distance) {
corrections_.push(distance);
correction_count_++;
total_correction_ += distance;
max_correction_ = (std::max)(max_correction_, static_cast<double>(distance));
}
void add_reconciliation() {
reconciliation_count_++;
}
void add_prediction() {
prediction_count_++;
}
void add_rollback() {
rollback_count_++;
}
PositionMetrics get_position_metrics() const {
PositionMetrics m;
m.sample_count = error_count_;
if (error_count_ > 0) {
m.mae = total_error_ / error_count_;
m.mse = total_squared_error_ / error_count_;
m.max_error = max_error_;
}
return m;
}
CompensationMetrics get_compensation_metrics(double elapsed_seconds) const {
CompensationMetrics m;
if (elapsed_seconds > 0) {
m.predictions_per_second = static_cast<uint32_t>(prediction_count_ / elapsed_seconds);
m.reconciliations_per_second = static_cast<uint32_t>(reconciliation_count_ / elapsed_seconds);
}
if (correction_count_ > 0) {
m.avg_correction_distance = total_correction_ / correction_count_;
}
m.max_correction_distance = max_correction_;
m.rollbacks_count = rollback_count_;
return m;
}
void reset_compensation_metrics() {
prediction_count_ = 0;
reconciliation_count_ = 0;
total_correction_ = 0.0;
max_correction_ = 0.0;
correction_count_ = 0;
}
void reset() {
position_errors_.clear();
corrections_.clear();
total_error_ = 0;
total_squared_error_ = 0;
max_error_ = 0;
error_count_ = 0;
total_correction_ = 0;
max_correction_ = 0;
correction_count_ = 0;
prediction_count_ = 0;
reconciliation_count_ = 0;
rollback_count_ = 0;
}
private:
RingBuffer<float, 1024> position_errors_;
RingBuffer<float, 256> corrections_;
double total_error_ = 0;
double total_squared_error_ = 0;
double max_error_ = 0;
uint32_t error_count_ = 0;
double total_correction_ = 0;
double max_correction_ = 0;
uint32_t correction_count_ = 0;
uint32_t prediction_count_ = 0;
uint32_t reconciliation_count_ = 0;
uint32_t rollback_count_ = 0;
};
class MetricsLogger {
public:
explicit MetricsLogger(const std::string& filename = "client_metrics.csv") {
file_.open(filename);
if (file_.is_open()) {
file_ << "timestamp_ms,preset,algorithm,rtt_ms,jitter_ms,packet_loss_percent,"
<< "packets_sent_per_sec,packets_delivered_per_sec,packets_lost_per_sec,packets_duplicated_per_sec,"
<< "fps,inputs_per_sec,snapshots_per_sec,"
<< "position_mae,position_mse,position_max_error,samples,"
<< "predictions_per_sec,reconciliations_per_sec,"
<< "avg_correction_distance,max_correction_distance\n";
file_.flush();
}
}
void log(uint64_t timestamp_ms,
const std::string& preset_name,
const std::string& algorithm_name,
double rtt_ms,
double jitter_ms,
double packet_loss_percent,
uint64_t packets_sent_ps,
uint64_t packets_delivered_ps,
uint64_t packets_lost_ps,
uint64_t packets_duplicated_ps,
float fps,
uint32_t inputs_per_sec,
uint32_t snapshots_per_sec,
const PositionMetrics& pos,
const CompensationMetrics& comp) {
if (!file_.is_open()) return;
file_ << timestamp_ms << ","
<< preset_name << ","
<< algorithm_name << ","
<< std::fixed << std::setprecision(3)
<< rtt_ms << ","
<< jitter_ms << ","
<< packet_loss_percent << ","
<< packets_sent_ps << ","
<< packets_delivered_ps << ","
<< packets_lost_ps << ","
<< packets_duplicated_ps << ","
<< fps << ","
<< inputs_per_sec << ","
<< snapshots_per_sec << ","
<< pos.mae << ","
<< pos.mse << ","
<< pos.max_error << ","
<< pos.sample_count << ","
<< comp.predictions_per_second << ","
<< comp.reconciliations_per_second << ","
<< comp.avg_correction_distance << ","
<< comp.max_correction_distance << "\n";
file_.flush();
}
private:
std::ofstream file_;
};
} // namespace netcode

View File

@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
namespace netcode {
// Каналы ENet
enum class NetworkChannel : uint8_t {
Reliable = 0,
Unreliable = 1,
COUNT = 2
};
constexpr uint8_t CHANNEL_COUNT = static_cast<uint8_t>(NetworkChannel::COUNT);
} // namespace netcode

View File

@@ -0,0 +1,150 @@
#pragma once
#include "types.hpp"
#include "math_types.hpp"
#include "input_command.hpp"
#include "snapshot.hpp"
#include "compensation_algorithm.hpp"
#include <vector>
#include <cstdint>
namespace netcode {
// Типы сообщений
enum class MessageType : uint8_t {
// Подключение
ClientConnect = 1,
ServerAccept,
ServerReject,
ClientDisconnect,
// Игровой процесс
ClientInput,
ServerSnapshot,
ServerDeltaSnapshot,
// Конфигурация
ClientRequestConfig,
ServerConfig,
ClientSetAlgorithm,
// Синхронизация времени
PingRequest,
PingResponse,
// Отладка
DebugInfo
};
// Базовый заголовок сообщения
struct MessageHeader {
MessageType type;
uint16_t payload_size;
};
// Сообщения подключения
struct ClientConnectMessage {
static constexpr MessageType TYPE = MessageType::ClientConnect;
uint32_t protocol_version = 1;
char player_name[32] = {};
TeamId team_preference;
};
struct ServerAcceptMessage {
static constexpr MessageType TYPE = MessageType::ServerAccept;
ClientId assigned_client_id;
EntityId assigned_entity_id;
TeamId assigned_team;
TickNumber current_tick;
uint64_t server_time_us;
};
struct ServerRejectMessage {
static constexpr MessageType TYPE = MessageType::ServerReject;
enum class Reason : uint8_t {
ServerFull,
VersionMismatch,
Banned,
Unknown
} reason;
char message[64] = {};
};
// Игровые сообщения
struct ClientInputMessage {
static constexpr MessageType TYPE = MessageType::ClientInput;
struct InputEntry {
SequenceNumber sequence;
TickNumber client_tick;
uint64_t timestamp_us;
uint8_t input_state;
};
uint8_t input_count = 0;
InputEntry inputs[16]; // Максимум 16 команд в пакете
};
struct ServerSnapshotMessage {
static constexpr MessageType TYPE = MessageType::ServerSnapshot;
TickNumber server_tick;
uint64_t timestamp_us;
struct Ack {
ClientId client_id;
SequenceNumber last_sequence;
};
uint8_t ack_count;
Ack acks[16];
float red_team_time;
float blue_team_time;
uint8_t entity_count;
};
// Конфигурация
struct ServerConfigMessage {
static constexpr MessageType TYPE = MessageType::ServerConfig;
uint32_t tick_rate;
uint32_t snapshot_rate;
float arena_width;
float arena_height;
float player_radius;
float player_max_speed;
float ball_radius;
};
struct ClientSetAlgorithmMessage {
static constexpr MessageType TYPE = MessageType::ClientSetAlgorithm;
CompensationAlgorithm algorithm;
};
// Синхронизация времени
struct PingRequestMessage {
static constexpr MessageType TYPE = MessageType::PingRequest;
uint32_t ping_id;
uint64_t client_time_us;
};
struct PingResponseMessage {
static constexpr MessageType TYPE = MessageType::PingResponse;
uint32_t ping_id;
uint64_t client_time_us;
uint64_t server_time_us;
TickNumber server_tick;
};
} // namespace netcode

View File

@@ -0,0 +1,78 @@
#pragma once
#include <array>
#include <optional>
#include <cstddef>
namespace netcode {
template<typename T, size_t Capacity>
class RingBuffer {
public:
void push(const T& item) {
data_[write_index_] = item;
write_index_ = (write_index_ + 1) % Capacity;
if (count_ < Capacity) {
count_++;
} else {
read_index_ = (read_index_ + 1) % Capacity;
}
}
std::optional<T> pop() {
if (count_ == 0) return std::nullopt;
T item = data_[read_index_];
read_index_ = (read_index_ + 1) % Capacity;
count_--;
return item;
}
const T* peek() const {
if (count_ == 0) return nullptr;
return &data_[read_index_];
}
const T* at(size_t index) const {
if (index >= count_) return nullptr;
size_t actual_index = (read_index_ + index) % Capacity;
return &data_[actual_index];
}
T* at(size_t index) {
if (index >= count_) return nullptr;
size_t actual_index = (read_index_ + index) % Capacity;
return &data_[actual_index];
}
// Доступ по sequence number (для команд)
template<typename SeqGetter>
const T* find_by_sequence(uint32_t seq, SeqGetter getter) const {
for (size_t i = 0; i < count_; ++i) {
const T* item = at(i);
if (item && getter(*item) == seq) {
return item;
}
}
return nullptr;
}
void clear() {
read_index_ = 0;
write_index_ = 0;
count_ = 0;
}
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
bool full() const { return count_ == Capacity; }
static constexpr size_t capacity() { return Capacity; }
private:
std::array<T, Capacity> data_;
size_t read_index_ = 0;
size_t write_index_ = 0;
size_t count_ = 0;
};
} // namespace netcode

View File

@@ -0,0 +1,237 @@
#pragma once
#include "types.hpp"
#include "math_types.hpp"
#include "snapshot.hpp"
#include "network_messages.hpp"
#include <vector>
#include <cstring>
#include <cstdint>
namespace netcode {
class WriteBuffer {
public:
WriteBuffer(size_t initial_capacity = 1024) {
data_.reserve(initial_capacity);
}
void write_u8(uint8_t value) {
data_.push_back(value);
}
void write_u16(uint16_t value) {
data_.push_back(static_cast<uint8_t>(value & 0xFF));
data_.push_back(static_cast<uint8_t>((value >> 8) & 0xFF));
}
void write_u32(uint32_t value) {
for (int i = 0; i < 4; ++i) {
data_.push_back(static_cast<uint8_t>((value >> (i * 8)) & 0xFF));
}
}
void write_u64(uint64_t value) {
for (int i = 0; i < 8; ++i) {
data_.push_back(static_cast<uint8_t>((value >> (i * 8)) & 0xFF));
}
}
void write_float(float value) {
uint32_t bits;
std::memcpy(&bits, &value, sizeof(float));
write_u32(bits);
}
void write_vec2(const Vec2& v) {
write_float(v.x);
write_float(v.y);
}
void write_bytes(const void* data, size_t size) {
const uint8_t* bytes = static_cast<const uint8_t*>(data);
data_.insert(data_.end(), bytes, bytes + size);
}
void write_string(const char* str, size_t max_len) {
size_t len = std::strlen(str);
len = (std::min)(len, max_len - 1);
write_u8(static_cast<uint8_t>(len));
write_bytes(str, len);
}
const uint8_t* data() const { return data_.data(); }
size_t size() const { return data_.size(); }
void clear() { data_.clear(); }
private:
std::vector<uint8_t> data_;
};
class ReadBuffer {
public:
ReadBuffer(const uint8_t* data, size_t size)
: data_(data), size_(size), pos_(0) {}
bool read_u8(uint8_t& value) {
if (pos_ + 1 > size_) return false;
value = data_[pos_++];
return true;
}
bool read_u16(uint16_t& value) {
if (pos_ + 2 > size_) return false;
value = static_cast<uint16_t>(data_[pos_]) |
(static_cast<uint16_t>(data_[pos_ + 1]) << 8);
pos_ += 2;
return true;
}
bool read_u32(uint32_t& value) {
if (pos_ + 4 > size_) return false;
value = 0;
for (int i = 0; i < 4; ++i) {
value |= static_cast<uint32_t>(data_[pos_ + i]) << (i * 8);
}
pos_ += 4;
return true;
}
bool read_u64(uint64_t& value) {
if (pos_ + 8 > size_) return false;
value = 0;
for (int i = 0; i < 8; ++i) {
value |= static_cast<uint64_t>(data_[pos_ + i]) << (i * 8);
}
pos_ += 8;
return true;
}
bool read_float(float& value) {
uint32_t bits;
if (!read_u32(bits)) return false;
std::memcpy(&value, &bits, sizeof(float));
return true;
}
bool read_vec2(Vec2& v) {
return read_float(v.x) && read_float(v.y);
}
bool read_bytes(void* dest, size_t size) {
if (pos_ + size > size_) return false;
std::memcpy(dest, data_ + pos_, size);
pos_ += size;
return true;
}
size_t remaining() const { return size_ - pos_; }
bool empty() const { return pos_ >= size_; }
private:
const uint8_t* data_;
size_t size_;
size_t pos_;
};
// сериализация сущности
inline void serialize_entity(WriteBuffer& buf, const EntityState& entity) {
buf.write_u32(entity.id);
buf.write_u8(static_cast<uint8_t>(entity.type));
buf.write_vec2(entity.position);
buf.write_vec2(entity.velocity);
switch (entity.type) {
case EntityType::Player:
buf.write_u16(entity.data.player.owner_id);
buf.write_u8(static_cast<uint8_t>(entity.data.player.team));
buf.write_float(entity.data.player.radius);
break;
case EntityType::Ball:
buf.write_float(entity.data.ball.radius);
break;
default:
break;
}
}
inline bool deserialize_entity(ReadBuffer& buf, EntityState& entity) {
uint8_t type_byte;
if (!buf.read_u32(entity.id)) return false;
if (!buf.read_u8(type_byte)) return false;
entity.type = static_cast<EntityType>(type_byte);
if (!buf.read_vec2(entity.position)) return false;
if (!buf.read_vec2(entity.velocity)) return false;
switch (entity.type) {
case EntityType::Player: {
uint8_t team_byte;
if (!buf.read_u16(entity.data.player.owner_id)) return false;
if (!buf.read_u8(team_byte)) return false;
entity.data.player.team = static_cast<TeamId>(team_byte);
if (!buf.read_float(entity.data.player.radius)) return false;
break;
}
case EntityType::Ball:
if (!buf.read_float(entity.data.ball.radius)) return false;
break;
default:
break;
}
return true;
}
// сериализация снапшота
inline void serialize_snapshot(WriteBuffer& buf, const WorldSnapshot& snapshot) {
buf.write_u8(static_cast<uint8_t>(MessageType::ServerSnapshot));
buf.write_u32(snapshot.server_tick);
buf.write_u64(snapshot.timestamp_us);
buf.write_float(snapshot.red_team_time);
buf.write_float(snapshot.blue_team_time);
// подтверждения
buf.write_u8(static_cast<uint8_t>(snapshot.client_acks.size()));
for (const auto& ack : snapshot.client_acks) {
buf.write_u16(ack.client_id);
buf.write_u32(ack.last_processed_sequence);
}
// сущности
buf.write_u8(static_cast<uint8_t>(snapshot.entities.size()));
for (const auto& entity : snapshot.entities) {
serialize_entity(buf, entity);
}
}
inline bool deserialize_snapshot(ReadBuffer& buf, WorldSnapshot& snapshot) {
uint8_t msg_type;
if (!buf.read_u8(msg_type)) return false;
if (static_cast<MessageType>(msg_type) != MessageType::ServerSnapshot) return false;
if (!buf.read_u32(snapshot.server_tick)) return false;
if (!buf.read_u64(snapshot.timestamp_us)) return false;
if (!buf.read_float(snapshot.red_team_time)) return false;
if (!buf.read_float(snapshot.blue_team_time)) return false;
// подтверждения
uint8_t ack_count;
if (!buf.read_u8(ack_count)) return false;
snapshot.client_acks.resize(ack_count);
for (uint8_t i = 0; i < ack_count; ++i) {
if (!buf.read_u16(snapshot.client_acks[i].client_id)) return false;
if (!buf.read_u32(snapshot.client_acks[i].last_processed_sequence)) return false;
}
// сущности
uint8_t entity_count;
if (!buf.read_u8(entity_count)) return false;
snapshot.entities.resize(entity_count);
for (uint8_t i = 0; i < entity_count; ++i) {
if (!deserialize_entity(buf, snapshot.entities[i])) return false;
}
return true;
}
} // namespace netcode

View File

@@ -0,0 +1,44 @@
#pragma once
#include "types.hpp"
#include "math_types.hpp"
namespace netcode {
struct SimulationParams {
uint32_t tick_rate = 60;
double fixed_delta_time = 1.0 / 60.0;
uint32_t snapshot_rate = 20;
uint32_t snapshot_history_size = 128;
float arena_width = 1620.0f;
float arena_height = 900.0f;
float wall_thickness = 10.0f;
Rect red_zone = {50.0f, 250.0f, 150.0f, 300.0f};
Rect blue_zone = {1400.0f, 250.0f, 150.0f, 300.0f};
float player_radius = 20.0f;
float player_mass = 1.0f;
float player_max_speed = 400.0f;
float player_acceleration = 1650.0f;
float player_friction = 4.0f;
float ball_radius = 30.0f;
float ball_mass = 1.5f;
float ball_friction = 2.0f;
float ball_restitution = 0.8f;
float collision_restitution = 0.5f;
uint32_t input_buffer_size = 64;
uint32_t jitter_buffer_size = 3;
double tick_interval() const { return 1.0 / tick_rate; }
double snapshot_interval() const { return 1.0 / snapshot_rate; }
};
inline SimulationParams DEFAULT_SIMULATION_PARAMS;
} // namespace netcode

View File

@@ -0,0 +1,62 @@
#pragma once
#include "types.hpp"
#include "math_types.hpp"
#include <vector>
#include <cstring>
namespace netcode {
struct EntityState {
EntityId id = INVALID_ENTITY_ID;
EntityType type = EntityType::None;
Vec2 position;
Vec2 velocity;
union {
struct {
ClientId owner_id;
TeamId team;
float radius;
} player;
struct {
float radius;
} ball;
} data = {};
EntityState() { std::memset(&data, 0, sizeof(data)); }
};
struct WorldSnapshot {
TickNumber server_tick = 0;
uint64_t timestamp_us = 0;
std::vector<EntityState> entities;
struct ClientAck {
ClientId client_id;
SequenceNumber last_processed_sequence;
};
std::vector<ClientAck> client_acks;
float red_team_time = 0.0f;
float blue_team_time = 0.0f;
const EntityState* find_entity(EntityId id) const {
for (const auto& e : entities) {
if (e.id == id) return &e;
}
return nullptr;
}
EntityState* find_entity(EntityId id) {
for (auto& e : entities) {
if (e.id == id) return &e;
}
return nullptr;
}
};
} // namespace netcode

View File

@@ -0,0 +1,96 @@
#pragma once
#include "types.hpp"
#include <chrono>
namespace netcode {
class Timestamp {
public:
static uint64_t now_us() {
auto now = Clock::now();
return std::chrono::duration_cast<Microseconds>(
now.time_since_epoch()
).count();
}
static uint64_t now_ms() {
return now_us() / 1000;
}
static double now_seconds() {
return static_cast<double>(now_us()) / 1000000.0;
}
};
// Синхронизация времени между клиентом и сервером
class TimeSynchronizer {
public:
void add_sample(uint64_t client_send_time, uint64_t server_time, uint64_t client_receive_time) {
// RTT
uint64_t rtt = client_receive_time - client_send_time;
rtt_samples_[sample_index_] = rtt;
// предпологается симметричная задержка
uint64_t one_way_delay = rtt / 2;
int64_t offset = static_cast<int64_t>(server_time) -
static_cast<int64_t>(client_send_time + one_way_delay);
offset_samples_[sample_index_] = offset;
sample_index_ = (sample_index_ + 1) % MAX_SAMPLES;
if (sample_count_ < MAX_SAMPLES) sample_count_++;
update_estimates();
}
uint64_t client_to_server_time(uint64_t client_time) const {
return static_cast<uint64_t>(static_cast<int64_t>(client_time) + clock_offset_);
}
uint64_t server_to_client_time(uint64_t server_time) const {
return static_cast<uint64_t>(static_cast<int64_t>(server_time) - clock_offset_);
}
double get_rtt_ms() const { return rtt_estimate_ / 1000.0; }
double get_jitter_ms() const { return jitter_estimate_ / 1000.0; }
bool is_synchronized() const { return sample_count_ >= 3; }
private:
static constexpr size_t MAX_SAMPLES = 16;
uint64_t rtt_samples_[MAX_SAMPLES] = {};
int64_t offset_samples_[MAX_SAMPLES] = {};
size_t sample_index_ = 0;
size_t sample_count_ = 0;
int64_t clock_offset_ = 0;
double rtt_estimate_ = 0.0;
double jitter_estimate_ = 0.0;
void update_estimates() {
if (sample_count_ == 0) return;
// Среднее RTT
uint64_t rtt_sum = 0;
for (size_t i = 0; i < sample_count_; ++i) {
rtt_sum += rtt_samples_[i];
}
rtt_estimate_ = static_cast<double>(rtt_sum) / sample_count_;
// Джиттер
double jitter_sum = 0;
for (size_t i = 0; i < sample_count_; ++i) {
double diff = static_cast<double>(rtt_samples_[i]) - rtt_estimate_;
jitter_sum += std::abs(diff);
}
jitter_estimate_ = jitter_sum / sample_count_;
int64_t sorted_offsets[MAX_SAMPLES];
std::copy_n(offset_samples_, sample_count_, sorted_offsets);
std::sort(sorted_offsets, sorted_offsets + sample_count_);
clock_offset_ = sorted_offsets[sample_count_ / 2];
}
};
} // namespace netcode

View File

@@ -0,0 +1,49 @@
#pragma once
#include <cstdint>
#include <chrono>
#include <string>
namespace netcode {
// Базовые типы идентификаторов
using ClientId = uint16_t;
using EntityId = uint32_t;
using TickNumber = uint32_t;
using SequenceNumber = uint32_t;
// Константы
constexpr ClientId INVALID_CLIENT_ID = 0xFFFF;
constexpr EntityId INVALID_ENTITY_ID = 0xFFFFFFFF;
constexpr TickNumber INVALID_TICK = 0xFFFFFFFF;
// Типы сущностей
enum class EntityType : uint8_t {
None = 0,
Player,
Ball
};
// Типы команд
enum class TeamId : uint8_t {
None = 0,
Red,
Blue
};
// Высокоточное время
using Clock = std::chrono::steady_clock;
using TimePoint = Clock::time_point;
using Duration = std::chrono::duration<double>;
using Milliseconds = std::chrono::milliseconds;
using Microseconds = std::chrono::microseconds;
inline double to_seconds(Duration d) {
return d.count();
}
inline double to_milliseconds(Duration d) {
return std::chrono::duration<double, std::milli>(d).count();
}
} // namespace netcode

21
common/src/common.cpp Normal file
View File

@@ -0,0 +1,21 @@
#define ENET_IMPLEMENTATION
#include "common/common.hpp"
#include "common/types.hpp"
#include "common/math_types.hpp"
#include "common/serialization.hpp"
#include "common/enet_wrapper.hpp"
#include "common/timestamp.hpp"
#include "common/metrics.hpp"
// Основная логика в заголовочных файлах
namespace netcode {
// Версия протокола
const uint32_t PROTOCOL_VERSION = 1;
// Порт по умолчанию
const uint16_t DEFAULT_PORT = 7777;
} // namespace netcode