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

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
/.idea

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "extern/enet"]
path = extern/enet
url = https://github.com/zpl-c/enet

153
CMakeLists.txt Normal file
View File

@@ -0,0 +1,153 @@
cmake_minimum_required(VERSION 4.2)
project(netcode_demo LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# ENet enable shared
set(ENET_SHARED ON CACHE BOOL "Build enet as a shared library" FORCE)
set(ENET_STATIC OFF CACHE BOOL "Build enet as a static library" FORCE)
# Build flags
option(BUILD_ALL "Build everything" ON)
option(BUILD_COMMON "Build common" ON)
option(BUILD_CLIENT "Build client" ON)
option(BUILD_SERVER "Build server" ON)
option(BUILD_APP "Build app" ON)
option(BUILD_SAPP "Build sapp" ON)
find_package(SFML 3.0 COMPONENTS System Window Graphics Network REQUIRED)
#add_compile_definitions(ENET_IPV6=1)
add_subdirectory(extern/enet)
#target_compile_definitions(enet_shared PUBLIC ENET_IPV6=1)
# ---------------------------
# Common library
# ---------------------------
if(BUILD_COMMON OR BUILD_ALL)
add_library(common SHARED
common/src/common.cpp
)
set_target_properties(common PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
target_include_directories(common PUBLIC common/include)
target_link_libraries(common PUBLIC enet::enet_shared)
if(WIN32)
target_link_libraries(common PUBLIC ws2_32 winmm)
endif()
endif()
# ---------------------------
# App library
# ---------------------------
if(BUILD_APP OR BUILD_ALL)
add_library(app SHARED
app/src/app_entry.cpp
app/include/app/network_simulation.hpp
)
set_target_properties(app PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
target_include_directories(app PUBLIC app/include)
if(BUILD_COMMON OR BUILD_ALL)
target_link_libraries(app PUBLIC common)
endif()
endif()
# ---------------------------
# Sapp library
# ---------------------------
if(BUILD_SAPP OR BUILD_ALL)
add_library(sapp SHARED
sapp/src/sapp_entry.cpp
)
set_target_properties(sapp PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
target_include_directories(sapp PUBLIC sapp/include)
if(BUILD_COMMON OR BUILD_ALL)
target_link_libraries(sapp PUBLIC common)
endif()
endif()
# ---------------------------
# Client executable
# ---------------------------
if(BUILD_CLIENT OR BUILD_ALL)
add_executable(client client/cl_main.cpp
)
target_include_directories(client PRIVATE client)
target_link_libraries(client PUBLIC
$<$<BOOL:${BUILD_COMMON}>:common>
$<$<BOOL:${BUILD_APP}>:app>
SFML::System SFML::Window SFML::Graphics SFML::Network enet::enet_shared
)
if(WIN32)
target_link_libraries(client PRIVATE ws2_32 winmm)
endif()
set_target_properties(client PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/client/$<CONFIG>
)
if(WIN32)
add_custom_command(TARGET client POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:common> $<TARGET_FILE_DIR:client>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:app> $<TARGET_FILE_DIR:client>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:enet_shared> $<TARGET_FILE_DIR:client>
COMMAND_EXPAND_LISTS
)
endif()
endif()
# ---------------------------
# Server executable
# ---------------------------
if(BUILD_SERVER OR BUILD_ALL)
add_executable(server server/sv_main.cpp)
target_include_directories(server PRIVATE server)
target_link_libraries(server PUBLIC
$<$<BOOL:${BUILD_COMMON}>:common>
$<$<BOOL:${BUILD_SAPP}>:sapp>
enet::enet_shared
)
if(WIN32)
target_link_libraries(server PRIVATE ws2_32 winmm)
endif()
set_target_properties(server PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin/server/$<CONFIG>
)
if(WIN32)
add_custom_command(TARGET server POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:common> $<TARGET_FILE_DIR:server>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:sapp> $<TARGET_FILE_DIR:server>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:enet_shared> $<TARGET_FILE_DIR:server>
COMMAND_EXPAND_LISTS
)
endif()
endif()
# ---------------------------
# Main target
# ---------------------------
add_custom_target(netcode_demo ALL
COMMENT "Build all enabled subprojects according to BUILD_* flags"
)
if(BUILD_COMMON OR BUILD_ALL)
add_dependencies(netcode_demo common)
endif()
if(BUILD_APP OR BUILD_ALL)
add_dependencies(netcode_demo app)
endif()
if(BUILD_SAPP OR BUILD_ALL)
add_dependencies(netcode_demo sapp)
endif()
if(BUILD_CLIENT OR BUILD_ALL)
add_dependencies(netcode_demo client)
endif()
if(BUILD_SERVER OR BUILD_ALL)
add_dependencies(netcode_demo server)
endif()

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

1302
client/cl_main.cpp Normal file

File diff suppressed because it is too large Load Diff

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

1
extern/enet vendored Submodule

Submodule extern/enet added at 8647b6eaea

View File

@@ -0,0 +1,66 @@
// на будущее расширение
// #pragma once
//
// #include "common/types.hpp"
// #include "common/math_types.hpp"
// #include "server_world.hpp"
//
// namespace netcode {
//
// class LagCompensator {
// public:
// explicit LagCompensator(float max_rewind_ms = 200.0f)
// : max_rewind_ms_(max_rewind_ms) {}
//
// struct HitCheckResult {
// bool hit = false;
// EntityId hit_entity = INVALID_ENTITY_ID;
// Vec2 hit_position;
// };
//
// HitCheckResult check_hit(const ServerWorld& world,
// ClientId shooter_client,
// const Vec2& origin,
// const Vec2& direction,
// float max_distance,
// uint64_t client_timestamp_us) {
// HitCheckResult result;
//
// auto history = world.get_history_at(client_timestamp_us);
// if (!history) return result;
//
// for (const auto& entity : history->entities) {
// if (entity.type != EntityType::Player) continue;
//
// if (entity.data.player.owner_id == shooter_client) continue;
//
// float radius = entity.data.player.radius;
// Vec2 to_center = entity.position - origin;
// float proj = Vec2::dot(to_center, direction);
//
// if (proj < 0 || proj > max_distance) continue;
//
// Vec2 closest = origin + direction * proj;
// float dist = Vec2::distance(closest, entity.position);
//
// if (dist <= radius) {
// result.hit = true;
// result.hit_entity = entity.id;
// result.hit_position = closest;
// return result;
// }
// }
//
// return result;
// }
//
// void set_max_rewind(float ms) { max_rewind_ms_ = ms; }
// float max_rewind() const { return max_rewind_ms_; }
//
// private:
// float max_rewind_ms_;
// };
//
// } // namespace netcode

View File

@@ -0,0 +1,193 @@
#pragma once
#include "common/types.hpp"
#include "common/math_types.hpp"
#include "common/simulation_params.hpp"
#include <vector>
namespace netcode {
struct PhysicsBody {
EntityId entity_id = INVALID_ENTITY_ID;
EntityType type = EntityType::None;
Vec2 position;
Vec2 velocity;
Vec2 acceleration;
float radius = 0.0f;
float mass = 1.0f;
float friction = 0.0f;
float restitution = 0.5f;
bool is_static = false;
};
class PhysicsWorld {
public:
explicit PhysicsWorld(const SimulationParams& params = DEFAULT_SIMULATION_PARAMS)
: params_(params) {}
// Добавление тела
void add_body(const PhysicsBody& body) {
bodies_.push_back(body);
}
// Удаление тела
void remove_body(EntityId id) {
bodies_.erase(
std::remove_if(bodies_.begin(), bodies_.end(),
[id](const PhysicsBody& b) { return b.entity_id == id; }),
bodies_.end()
);
}
// Получение тела
PhysicsBody* get_body(EntityId id) {
for (auto& body : bodies_) {
if (body.entity_id == id) return &body;
}
return nullptr;
}
const PhysicsBody* get_body(EntityId id) const {
for (const auto& body : bodies_) {
if (body.entity_id == id) return &body;
}
return nullptr;
}
// Симуляция одного шага
void step(float dt) {
for (auto& body : bodies_) {
if (body.is_static) continue;
// трение
body.velocity -= body.velocity * body.friction * dt;
// ускорение
body.velocity += body.acceleration * dt;
// скорость
if (body.type == EntityType::Player) {
float speed = body.velocity.length();
if (speed > params_.player_max_speed) {
body.velocity = body.velocity.normalized() * params_.player_max_speed;
}
}
// Обновление позиции
body.position += body.velocity * dt;
// Сбрасываем ускорение
body.acceleration = Vec2();
}
// обработка коллизиц
resolve_collisions();
// Коллизии со стенами
for (auto& body : bodies_) {
constrain_to_arena(body);
}
}
// Применить силу к телу
void apply_force(EntityId id, const Vec2& force) {
PhysicsBody* body = get_body(id);
if (body) {
body->acceleration += force / body->mass;
}
}
// Установить ускорение
void set_acceleration(EntityId id, const Vec2& acc) {
PhysicsBody* body = get_body(id);
if (body) {
body->acceleration = acc;
}
}
const std::vector<PhysicsBody>& bodies() const { return bodies_; }
private:
void constrain_to_arena(PhysicsBody& body) {
float min_x = params_.wall_thickness + body.radius;
float max_x = params_.arena_width - params_.wall_thickness - body.radius;
float min_y = params_.wall_thickness + body.radius;
float max_y = params_.arena_height - params_.wall_thickness - body.radius;
if (body.position.x < min_x) {
body.position.x = min_x;
body.velocity.x = -body.velocity.x * body.restitution;
}
if (body.position.x > max_x) {
body.position.x = max_x;
body.velocity.x = -body.velocity.x * body.restitution;
}
if (body.position.y < min_y) {
body.position.y = min_y;
body.velocity.y = -body.velocity.y * body.restitution;
}
if (body.position.y > max_y) {
body.position.y = max_y;
body.velocity.y = -body.velocity.y * body.restitution;
}
}
void resolve_collisions() {
for (size_t i = 0; i < bodies_.size(); ++i) {
for (size_t j = i + 1; j < bodies_.size(); ++j) {
resolve_collision(bodies_[i], bodies_[j]);
}
}
}
void resolve_collision(PhysicsBody& a, PhysicsBody& b) {
Vec2 diff = b.position - a.position;
float dist = diff.length();
float min_dist = a.radius + b.radius;
if (dist < min_dist && dist > 0.001f) {
// Нормаль коллизии
Vec2 normal = diff / dist;
// Глубина проникновения
float penetration = min_dist - dist;
// Разделение тел
float total_mass = a.mass + b.mass;
Vec2 separation = normal * penetration;
if (!a.is_static && !b.is_static) {
a.position -= separation * (b.mass / total_mass);
b.position += separation * (a.mass / total_mass);
} else if (!a.is_static) {
a.position -= separation;
} else if (!b.is_static) {
b.position += separation;
}
// Относительная скорость
Vec2 rel_vel = b.velocity - a.velocity;
float rel_vel_normal = Vec2::dot(rel_vel, normal);
// Только если сближаются
if (rel_vel_normal < 0) {
float restitution = (std::min)(a.restitution, b.restitution);
float impulse_mag = -(1.0f + restitution) * rel_vel_normal;
impulse_mag /= (1.0f / a.mass) + (1.0f / b.mass);
Vec2 impulse = normal * impulse_mag;
if (!a.is_static) a.velocity -= impulse / a.mass;
if (!b.is_static) b.velocity += impulse / b.mass;
}
}
}
SimulationParams params_;
std::vector<PhysicsBody> bodies_;
};
} // namespace netcode

View File

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

View File

@@ -0,0 +1,351 @@
#pragma once
#include <iostream>
#include "common/types.hpp"
#include "common/math_types.hpp"
#include "common/snapshot.hpp"
#include "common/input_command.hpp"
#include "common/simulation_params.hpp"
#include "common/ring_buffer.hpp"
#include "physics.hpp"
#include <unordered_map>
#include <vector>
#include <queue>
#include <common/timestamp.hpp>
namespace netcode {
// Информация об игроке
struct ServerPlayer {
ClientId client_id = INVALID_CLIENT_ID;
EntityId entity_id = INVALID_ENTITY_ID;
TeamId team = TeamId::None;
std::string name;
SequenceNumber last_processed_input = 0;
RingBuffer<InputCommand, 64> input_buffer;
uint64_t estimated_rtt_us = 0;
};
// история состояний
struct WorldHistoryEntry {
TickNumber tick;
uint64_t timestamp_us;
std::vector<EntityState> entities;
};
class ServerWorld {
public:
explicit ServerWorld(const SimulationParams& params = DEFAULT_SIMULATION_PARAMS)
: params_(params), physics_(params) {
create_ball();
}
EntityId create_player(ClientId client_id, const std::string& name) {
EntityId entity_id = next_entity_id_++;
// Определяем команду
TeamId team = (red_team_count_ <= blue_team_count_) ? TeamId::Red : TeamId::Blue;
if (team == TeamId::Red) red_team_count_++;
else blue_team_count_++;
// Позиция спавна
Vec2 spawn_pos;
if (team == TeamId::Red) {
spawn_pos = params_.red_zone.center();
} else {
spawn_pos = params_.blue_zone.center();
}
// Физическое тело
PhysicsBody body;
body.entity_id = entity_id;
body.type = EntityType::Player;
body.position = spawn_pos;
body.radius = params_.player_radius;
body.mass = params_.player_mass;
body.friction = params_.player_friction;
body.restitution = params_.collision_restitution;
physics_.add_body(body);
// Данные игрока
ServerPlayer player;
player.client_id = client_id;
player.entity_id = entity_id;
player.team = team;
player.name = name;
players_[client_id] = player;
return entity_id;
}
void remove_player(ClientId client_id) {
auto it = players_.find(client_id);
if (it != players_.end()) {
physics_.remove_body(it->second.entity_id);
if (it->second.team == TeamId::Red) red_team_count_--;
else if (it->second.team == TeamId::Blue) blue_team_count_--;
players_.erase(it);
}
}
TeamId get_player_team(ClientId client_id) const {
auto it = players_.find(client_id);
if (it != players_.end()) {
return it->second.team;
}
return TeamId::None;
}
EntityId get_player_entity(ClientId client_id) const {
auto it = players_.find(client_id);
if (it != players_.end()) {
return it->second.entity_id;
}
return INVALID_ENTITY_ID;
}
void add_player_input(ClientId client_id, const InputCommand& cmd) {
auto it = players_.find(client_id);
if (it != players_.end()) {
it->second.input_buffer.push(cmd);
}
}
void update_player_rtt(ClientId client_id, uint64_t rtt_us) {
auto it = players_.find(client_id);
if (it != players_.end()) {
it->second.estimated_rtt_us = rtt_us;
}
}
void tick() {
current_tick_++;
// Обрабатываем ввод всех игроков
for (auto& [client_id, player] : players_) {
process_player_input(player);
}
// Физика
physics_.step(static_cast<float>(params_.fixed_delta_time));
// Обновляем счёт
update_score();
// Сохраняем историю
save_history();
}
// Получить снапшот
WorldSnapshot create_snapshot() const {
WorldSnapshot snapshot;
snapshot.server_tick = current_tick_;
snapshot.timestamp_us = Timestamp::now_us();
snapshot.red_team_time = red_team_time_;
snapshot.blue_team_time = blue_team_time_;
// Сущности
for (const auto& body : physics_.bodies()) {
EntityState state;
state.id = body.entity_id;
state.type = body.type;
state.position = body.position;
state.velocity = body.velocity;
if (body.type == EntityType::Player) {
// Находим данные игрока
for (const auto& [cid, player] : players_) {
if (player.entity_id == body.entity_id) {
state.data.player.owner_id = player.client_id;
state.data.player.team = player.team;
state.data.player.radius = body.radius;
break;
}
}
} else if (body.type == EntityType::Ball) {
state.data.ball.radius = body.radius;
}
snapshot.entities.push_back(state);
}
// Подтверждения
for (const auto& [client_id, player] : players_) {
WorldSnapshot::ClientAck ack;
ack.client_id = client_id;
ack.last_processed_sequence = player.last_processed_input;
snapshot.client_acks.push_back(ack);
}
return snapshot;
}
std::optional<WorldHistoryEntry> get_history_at(uint64_t timestamp_us) const {
for (size_t i = 0; i < history_.size(); ++i) {
const auto* entry = history_.at(i);
if (entry && entry->timestamp_us <= timestamp_us) {
const auto* next = history_.at(i + 1);
if (!next || next->timestamp_us > timestamp_us) {
return *entry;
}
}
}
return std::nullopt;
}
EntityId create_player(ClientId client_id, const std::string& name,
TeamId team_preference = TeamId::None) {
EntityId entity_id = next_entity_id_++;
TeamId team;
if (team_preference == TeamId::Red || team_preference == TeamId::Blue) {
if (team_preference == TeamId::Red) {
if (red_team_count_ <= blue_team_count_ + 2) {
team = TeamId::Red;
} else {
team = TeamId::Blue;
std::cout << "Balancing: assigning to Blue instead of Red\n";
}
} else {
if (blue_team_count_ <= red_team_count_ + 2) {
team = TeamId::Blue;
} else {
team = TeamId::Red;
std::cout << "Balancing: assigning to Red instead of Blue\n";
}
}
} else {
team = (red_team_count_ <= blue_team_count_) ? TeamId::Red : TeamId::Blue;
}
if (team == TeamId::Red) red_team_count_++;
else blue_team_count_++;
// Позиция спавна
Vec2 spawn_pos;
if (team == TeamId::Red) {
spawn_pos = params_.red_zone.center();
} else {
spawn_pos = params_.blue_zone.center();
}
// Физическое тело
PhysicsBody body;
body.entity_id = entity_id;
body.type = EntityType::Player;
body.position = spawn_pos;
body.radius = params_.player_radius;
body.mass = params_.player_mass;
body.friction = params_.player_friction;
body.restitution = params_.collision_restitution;
physics_.add_body(body);
// Данные игрока
ServerPlayer player;
player.client_id = client_id;
player.entity_id = entity_id;
player.team = team;
player.name = name;
players_[client_id] = player;
return entity_id;
}
TickNumber current_tick() const { return current_tick_; }
const SimulationParams& params() const { return params_; }
float red_team_time() const { return red_team_time_; }
float blue_team_time() const { return blue_team_time_; }
private:
void create_ball() {
EntityId ball_id = next_entity_id_++;
ball_entity_id_ = ball_id;
PhysicsBody body;
body.entity_id = ball_id;
body.type = EntityType::Ball;
body.position = Vec2(params_.arena_width / 2, params_.arena_height / 2);
body.radius = params_.ball_radius;
body.mass = params_.ball_mass;
body.friction = params_.ball_friction;
body.restitution = params_.ball_restitution;
physics_.add_body(body);
}
void process_player_input(ServerPlayer& player) {
// Обрабатываем накопленный ввод
while (!player.input_buffer.empty()) {
auto cmd_opt = player.input_buffer.pop();
if (!cmd_opt) break;
const InputCommand& cmd = *cmd_opt;
// Пропускаем старые команды
if (cmd.sequence <= player.last_processed_input) continue;
// Применяем ввод
Vec2 input_dir = cmd.input.get_direction();
physics_.set_acceleration(player.entity_id,
input_dir * params_.player_acceleration);
player.last_processed_input = cmd.sequence;
}
}
void update_score() {
const PhysicsBody* ball = physics_.get_body(ball_entity_id_);
if (!ball) return;
float dt = static_cast<float>(params_.fixed_delta_time);
if (params_.red_zone.contains(ball->position)) {
red_team_time_ += dt;
}
if (params_.blue_zone.contains(ball->position)) {
blue_team_time_ += dt;
}
}
void save_history() {
WorldHistoryEntry entry;
entry.tick = current_tick_;
entry.timestamp_us = Timestamp::now_us();
for (const auto& body : physics_.bodies()) {
EntityState state;
state.id = body.entity_id;
state.type = body.type;
state.position = body.position;
state.velocity = body.velocity;
entry.entities.push_back(state);
}
history_.push(entry);
}
SimulationParams params_;
PhysicsWorld physics_;
std::unordered_map<ClientId, ServerPlayer> players_;
EntityId ball_entity_id_ = INVALID_ENTITY_ID;
TickNumber current_tick_ = 0;
EntityId next_entity_id_ = 1;
uint32_t red_team_count_ = 0;
uint32_t blue_team_count_ = 0;
float red_team_time_ = 0.0f;
float blue_team_time_ = 0.0f;
RingBuffer<WorldHistoryEntry, 128> history_;
};
} // namespace netcode

14
sapp/src/sapp_entry.cpp Normal file
View File

@@ -0,0 +1,14 @@
#include "sapp/sapp_entry.hpp"
#include "sapp/server_world.hpp"
#include "sapp/physics.hpp"
// Точка входа для DLL
namespace netcode {
void sapp_init() {
// Инициализация серверной библиотеки
}
} // namespace netcode

480
server/sv_main.cpp Normal file
View File

@@ -0,0 +1,480 @@
// server/sv_main.cpp
#include "common/types.hpp"
#include "common/enet_wrapper.hpp"
#include "common/serialization.hpp"
#include "common/network_messages.hpp"
#include "common/timestamp.hpp"
#include "common/simulation_params.hpp"
#include "sapp/server_world.hpp"
#include <iostream>
#include <unordered_map>
#include <string>
#include <format>
#include <thread>
#include <chrono>
#include <vector>
using namespace netcode;
struct ClientSession {
ENetPeer* peer = nullptr;
ClientId client_id = INVALID_CLIENT_ID;
EntityId entity_id = INVALID_ENTITY_ID;
std::string name;
bool authenticated = false;
uint64_t last_ping_time = 0;
uint64_t estimated_rtt_us = 0;
uint64_t last_activity_time = 0;
CompensationAlgorithm client_algorithm = CompensationAlgorithm::Hybrid;
};
class GameServer {
public:
explicit GameServer(uint16_t port = 7777, const SimulationParams& params = DEFAULT_SIMULATION_PARAMS)
: port_(port)
, params_(params)
, world_(params)
{}
bool start() {
if (!network_.create_server(port_, 16)) {
std::cerr << "Failed to create server on port " << port_ << "\n";
return false;
}
std::cout << "Server started on port " << port_ << "\n";
std::cout << "Tick rate: " << params_.tick_rate << " Hz\n";
std::cout << "Snapshot rate: " << params_.snapshot_rate << " Hz\n";
running_ = true;
return true;
}
void run() {
using namespace std::chrono;
auto last_tick_time = steady_clock::now();
auto last_timeout_check = steady_clock::now();
double tick_accumulator = 0.0;
double snapshot_accumulator = 0.0;
double log_timer = 0.0;
while (running_) {
auto now = steady_clock::now();
// Прошедшее время с последнего тика
double dt = duration<double>(now - last_tick_time).count();
// Обработка сети
process_network();
// тики
uint32_t ticks_performed = 0;
tick_accumulator += dt;
while (tick_accumulator >= params_.tick_interval() && ticks_performed < 8) {
world_.tick();
tick_accumulator -= params_.tick_interval();
tick_count_++;
ticks_performed++;
}
// снапшоты
snapshot_accumulator += dt;
if (snapshot_accumulator >= params_.snapshot_interval()) {
send_snapshots();
snapshot_count_++;
snapshot_accumulator -= params_.snapshot_interval();
}
// проверка таймаутов
if (duration<double>(now - last_timeout_check).count() >= 5.0) {
check_timeouts();
last_timeout_check = now;
}
// логирование
log_timer += dt;
if (log_timer >= 1.0) {
print_stats();
log_timer -= 1.0;
}
// вычисление следующего тика
double time_to_next_tick = params_.tick_interval() - tick_accumulator;
double time_to_next_snapshot = params_.snapshot_interval() - snapshot_accumulator;
double time_to_next_event = (std::min)(time_to_next_tick, time_to_next_snapshot);
time_to_next_event = (std::max)(time_to_next_event, 0.0);
// Обновляем время последнего тика
last_tick_time = now;
// Спим до следующего события
if (time_to_next_event > 0.0005) { // > 500 мкс
auto sleep_duration = duration_cast<microseconds>(duration<double>(time_to_next_event));
std::this_thread::sleep_for(sleep_duration);
} else {
std::this_thread::yield(); // если система решит забрать это время то ждем, если нет -- сразу начинаем выполнение
}
}
}
void stop() {
running_ = false;
}
private:
void process_network() {
ENetEvent event;
while (network_.service(event, 0) > 0) {
switch (event.type) {
case ENET_EVENT_TYPE_CONNECT:
handle_connect(event.peer);
break;
case ENET_EVENT_TYPE_RECEIVE:
handle_packet(event.peer, event.packet);
enet_packet_destroy(event.packet);
break;
case ENET_EVENT_TYPE_DISCONNECT:
handle_disconnect(event.peer);
break;
default:
break;
}
}
}
void handle_connect(ENetPeer* peer) {
char hostStr[46];
if (enet_address_get_host_ip(&peer->address, hostStr, sizeof(hostStr)) == 0) {
std::cout << "New connection from "
<< hostStr << ":"
<< peer->address.port << "\n";
} else {
std::cout << "New connection from [unknown address]:"
<< peer->address.port << "\n";
}
}
void handle_disconnect(ENetPeer* peer) {
auto it = sessions_by_peer_.find(peer);
if (it != sessions_by_peer_.end()) {
ClientId client_id = it->second;
auto session_it = sessions_.find(client_id);
if (session_it != sessions_.end()) {
std::cout << "Player disconnected: " << session_it->second.name
<< " (ID: " << client_id << ")\n";
world_.remove_player(client_id);
sessions_.erase(session_it);
}
sessions_by_peer_.erase(it);
}
}
void handle_packet(ENetPeer* peer, ENetPacket* packet) {
if (packet->dataLength < 1) return;
ReadBuffer buf(packet->data, packet->dataLength);
uint8_t type_byte;
if (!buf.read_u8(type_byte)) return;
MessageType type = static_cast<MessageType>(type_byte);
switch (type) {
case MessageType::ClientConnect:
handle_client_connect(peer, buf);
break;
case MessageType::ClientInput:
handle_client_input(peer, buf);
break;
case MessageType::ClientSetAlgorithm:
handle_client_set_algorithm(peer, buf);
break;
case MessageType::PingRequest:
handle_ping_request(peer, buf);
break;
default:
break;
}
}
void handle_client_connect(ENetPeer* peer, ReadBuffer& buf) {
uint32_t protocol_version;
if (!buf.read_u32(protocol_version)) return;
uint8_t name_len;
if (!buf.read_u8(name_len)) return;
char name[33] = {};
if (name_len > 0 && name_len <= 32) {
buf.read_bytes(name, name_len);
}
uint8_t team_pref_byte;
TeamId team_preference = TeamId::None;
if (buf.read_u8(team_pref_byte)) {
team_preference = static_cast<TeamId>(team_pref_byte);
}
if (protocol_version != 1) {
send_reject(peer, ServerRejectMessage::Reason::VersionMismatch);
return;
}
if (sessions_.size() >= 16) {
send_reject(peer, ServerRejectMessage::Reason::ServerFull);
return;
}
ClientId client_id = next_client_id_++;
EntityId entity_id = world_.create_player(client_id, name, team_preference);
TeamId team = world_.get_player_team(client_id);
ClientSession session;
session.peer = peer;
session.client_id = client_id;
session.entity_id = entity_id;
session.name = name;
session.authenticated = true;
session.last_activity_time = Timestamp::now_us(); // НОВОЕ
sessions_[client_id] = session;
sessions_by_peer_[peer] = client_id;
send_accept(peer, client_id, entity_id, team);
send_config(peer);
std::cout << "Player connected: " << name
<< " (ID: " << client_id
<< ", Entity: " << entity_id
<< ", Team: " << (team == TeamId::Red ? "Red" : "Blue")
<< ", Preferred: " << (team_preference == TeamId::Red ? "Red" :
team_preference == TeamId::Blue ? "Blue" : "Auto") << ")\n";
}
void handle_client_input(ENetPeer* peer, ReadBuffer& buf) {
auto it = sessions_by_peer_.find(peer);
if (it == sessions_by_peer_.end()) return;
ClientId client_id = it->second;
auto session_it = sessions_.find(client_id);
if (session_it != sessions_.end()) {
session_it->second.last_activity_time = Timestamp::now_us();
}
uint8_t input_count;
if (!buf.read_u8(input_count)) return;
for (uint8_t i = 0; i < input_count && i < 16; ++i) {
InputCommand cmd;
if (!buf.read_u32(cmd.sequence)) break;
if (!buf.read_u32(cmd.client_tick)) break;
if (!buf.read_u64(cmd.timestamp_us)) break;
uint8_t input_byte;
if (!buf.read_u8(input_byte)) break;
cmd.input = InputState::from_byte(input_byte);
world_.add_player_input(client_id, cmd);
inputs_received_++;
}
}
void handle_client_set_algorithm(ENetPeer* peer, ReadBuffer& buf) {
auto it = sessions_by_peer_.find(peer);
if (it == sessions_by_peer_.end()) return;
uint8_t algo_byte;
if (!buf.read_u8(algo_byte)) return;
ClientId client_id = it->second;
auto session_it = sessions_.find(client_id);
if (session_it != sessions_.end()) {
session_it->second.client_algorithm = static_cast<CompensationAlgorithm>(algo_byte);
std::cout << "Client " << client_id << " switched to algorithm: "
<< algorithm_name(session_it->second.client_algorithm) << "\n";
}
}
void handle_ping_request(ENetPeer* peer, ReadBuffer& buf) {
uint32_t ping_id;
uint64_t client_time;
if (!buf.read_u32(ping_id)) return;
if (!buf.read_u64(client_time)) return;
WriteBuffer response;
response.write_u8(static_cast<uint8_t>(MessageType::PingResponse));
response.write_u32(ping_id);
response.write_u64(client_time);
response.write_u64(Timestamp::now_us());
response.write_u32(world_.current_tick());
network_.send(peer, NetworkChannel::Unreliable, response.data(), response.size(), false);
auto it = sessions_by_peer_.find(peer);
if (it != sessions_by_peer_.end()) {
auto session_it = sessions_.find(it->second);
if (session_it != sessions_.end()) {
uint64_t now = Timestamp::now_us();
if (session_it->second.last_ping_time > 0) {
session_it->second.estimated_rtt_us = now - session_it->second.last_ping_time;
world_.update_player_rtt(it->second, session_it->second.estimated_rtt_us);
}
session_it->second.last_ping_time = now;
}
}
}
void check_timeouts() {
uint64_t now = Timestamp::now_us();
const uint64_t timeout_us = 30'000'000; // 30 секунд
std::vector<ClientId> to_disconnect;
for (const auto& [client_id, session] : sessions_) {
if (session.authenticated &&
session.last_activity_time > 0 &&
now - session.last_activity_time > timeout_us) {
to_disconnect.push_back(client_id);
}
}
for (ClientId client_id : to_disconnect) {
auto it = sessions_.find(client_id);
if (it != sessions_.end()) {
std::cout << "Client timeout: " << it->second.name << "\n";
if (it->second.peer) {
enet_peer_disconnect(it->second.peer, 0);
}
world_.remove_player(client_id);
sessions_by_peer_.erase(it->second.peer);
sessions_.erase(it);
}
}
}
void send_reject(ENetPeer* peer, ServerRejectMessage::Reason reason) {
WriteBuffer buf;
buf.write_u8(static_cast<uint8_t>(MessageType::ServerReject));
buf.write_u8(static_cast<uint8_t>(reason));
network_.send(peer, NetworkChannel::Reliable, buf.data(), buf.size(), true);
}
void send_accept(ENetPeer* peer, ClientId client_id, EntityId entity_id, TeamId team) {
WriteBuffer buf;
buf.write_u8(static_cast<uint8_t>(MessageType::ServerAccept));
buf.write_u16(client_id);
buf.write_u32(entity_id);
buf.write_u8(static_cast<uint8_t>(team));
buf.write_u32(world_.current_tick());
buf.write_u64(Timestamp::now_us());
network_.send(peer, NetworkChannel::Reliable, buf.data(), buf.size(), true);
}
void send_config(ENetPeer* peer) {
WriteBuffer buf;
buf.write_u8(static_cast<uint8_t>(MessageType::ServerConfig));
buf.write_u32(params_.tick_rate);
buf.write_u32(params_.snapshot_rate);
buf.write_float(params_.arena_width);
buf.write_float(params_.arena_height);
buf.write_float(params_.player_radius);
buf.write_float(params_.player_max_speed);
buf.write_float(params_.ball_radius);
network_.send(peer, NetworkChannel::Reliable, buf.data(), buf.size(), true);
}
void send_snapshots() {
if (sessions_.empty()) return;
WorldSnapshot snapshot = world_.create_snapshot();
WriteBuffer buf;
serialize_snapshot(buf, snapshot);
network_.broadcast(NetworkChannel::Unreliable, buf.data(), buf.size(), false);
snapshots_sent_++;
}
void print_stats() {
std::cout << std::format("[Tick {}] Players: {} | Inputs/s: {} | Snapshots/s: {} | Red: {:.1f}s | Blue: {:.1f}s\n",
world_.current_tick(),
sessions_.size(),
inputs_received_,
snapshots_sent_,
world_.red_team_time(),
world_.blue_team_time());
inputs_received_ = 0;
snapshots_sent_ = 0;
}
private:
uint16_t port_;
SimulationParams params_;
ENetInitializer enet_init_;
NetworkHost network_;
ServerWorld world_;
std::unordered_map<ClientId, ClientSession> sessions_;
std::unordered_map<ENetPeer*, ClientId> sessions_by_peer_;
ClientId next_client_id_ = 1;
bool running_ = false;
uint32_t tick_count_ = 0;
uint32_t snapshot_count_ = 0;
uint32_t inputs_received_ = 0;
uint32_t snapshots_sent_ = 0;
double log_timer_ = 0.0;
};
int main(int argc, char* argv[]) {
uint16_t port = 7777;
SimulationParams params = DEFAULT_SIMULATION_PARAMS;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "-p" || arg == "--port") {
if (i + 1 < argc) port = static_cast<uint16_t>(std::stoi(argv[++i]));
} else if (arg == "--tick-rate") {
if (i + 1 < argc) {
params.tick_rate = static_cast<uint32_t>(std::stoi(argv[++i]));
params.fixed_delta_time = 1.0 / params.tick_rate;
}
} else if (arg == "--snapshot-rate") {
if (i + 1 < argc) {
params.snapshot_rate = static_cast<uint32_t>(std::stoi(argv[++i]));
}
} else if (arg == "--help") {
std::cout << "Usage: server [-p port] [--tick-rate N] [--snapshot-rate N]\n";
return 0;
}
}
try {
GameServer server(port, params);
if (server.start()) {
server.run();
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
return 0;
}