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