Initial commit: full project code
This commit is contained in:
5
common/include/common/common.hpp
Normal file
5
common/include/common/common.hpp
Normal file
@@ -0,0 +1,5 @@
|
||||
#ifndef NETCODE_DEMO_COMMON_ENTRY_HPP
|
||||
#define NETCODE_DEMO_COMMON_ENTRY_HPP
|
||||
|
||||
|
||||
#endif //NETCODE_DEMO_COMMON_ENTRY_HPP
|
||||
59
common/include/common/compensation_algorithm.hpp
Normal file
59
common/include/common/compensation_algorithm.hpp
Normal 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
|
||||
164
common/include/common/enet_wrapper.hpp
Normal file
164
common/include/common/enet_wrapper.hpp
Normal 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
|
||||
64
common/include/common/input_command.hpp
Normal file
64
common/include/common/input_command.hpp
Normal 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
|
||||
76
common/include/common/math_types.hpp
Normal file
76
common/include/common/math_types.hpp
Normal 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
|
||||
204
common/include/common/metrics.hpp
Normal file
204
common/include/common/metrics.hpp
Normal 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
|
||||
17
common/include/common/network_channel.hpp
Normal file
17
common/include/common/network_channel.hpp
Normal 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
|
||||
150
common/include/common/network_messages.hpp
Normal file
150
common/include/common/network_messages.hpp
Normal 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
|
||||
78
common/include/common/ring_buffer.hpp
Normal file
78
common/include/common/ring_buffer.hpp
Normal 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
|
||||
237
common/include/common/serialization.hpp
Normal file
237
common/include/common/serialization.hpp
Normal 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
|
||||
44
common/include/common/simulation_params.hpp
Normal file
44
common/include/common/simulation_params.hpp
Normal 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
|
||||
62
common/include/common/snapshot.hpp
Normal file
62
common/include/common/snapshot.hpp
Normal 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
|
||||
96
common/include/common/timestamp.hpp
Normal file
96
common/include/common/timestamp.hpp
Normal 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
|
||||
49
common/include/common/types.hpp
Normal file
49
common/include/common/types.hpp
Normal 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
|
||||
Reference in New Issue
Block a user