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