Files
netcode-demo/client/cl_main.cpp

1302 lines
48 KiB
C++
Raw Normal View History

2026-01-11 01:37:39 +04:00
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <ws2tcpip.h>
#endif
#define NOMINMAX
#include <iostream>
#include <string>
#include <format>
#include <algorithm>
#include <chrono>
#include <random>
#include <thread>
#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 "app/client_world.hpp"
#include "app/network_simulation.hpp"
#include <SFML/Graphics.hpp>
#include <SFML/Window.hpp>
#ifdef _WIN32
const std::string defaultFontPath = "C:/Windows/Fonts/arial.ttf";
#else
const std::string defaultFontPath = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf";
#endif
using namespace netcode;
// Состояния клиента
enum class ClientState {
TeamSelection,
Connecting,
Connected,
Disconnected
};
class GameClient {
public:
sf::Clock bot_clock_;
GameClient() : window_(sf::VideoMode({1760, 990}), "Netcode Demo - Client") {
window_.setFramerateLimit(120);
auto_test_mode_ = false;
if (!font_.openFromFile(defaultFontPath)) {
std::cerr << "Warning: Could not load font\n";
}
state_ = ClientState::TeamSelection;
network_presets_ = {
{"Perfect (No Sim)", NetworkPresets::Perfect()},
{"LAN (2ms)", NetworkPresets::LAN()},
{"Good Broadband (25ms)", NetworkPresets::GoodBroadband()},
{"Average Broadband (50ms)", NetworkPresets::AverageBroadband()},
{"Poor Broadband (100ms)", NetworkPresets::PoorBroadband()},
{"Mobile 4G (75ms)", NetworkPresets::Mobile4G()},
{"Mobile 3G (150ms)", NetworkPresets::Mobile3G()},
{"Satellite (300ms)", NetworkPresets::Satellite()}
};
network_sim_.update_config(network_presets_[0].second);
}
void enable_auto_test() {
auto_test_mode_ = true;
bot_enabled_ = false;
bot_config_.test_duration_per_preset_sec = 240.0f;
bot_config_.stabilization_time_sec = 5.0f;
bot_config_.movement_change_interval_sec = 0.7f;
bot_config_.random_movements = true;
bot_config_.circle_radius = 15.0f;
bot_config_.circle_speed = 2.0f;
std::cout << "AUTO TEST MODE ENABLED\n";
}
void direct_select_team(TeamId team) {
selected_team_ = team;
state_ = ClientState::Connecting;
if (!connect_to_server()) {
state_ = ClientState::Disconnected;
}
std::cout << "Auto-selected team: " << (team == TeamId::Red ? "Red" : team == TeamId::Blue ? "Blue" : "Auto") << "\n";
}
void direct_set_algorithm(CompensationAlgorithm algo) {
set_algorithm(algo);
std::cout << "Direct switched to algorithm: " << algorithm_name(algo) << "\n";
}
void direct_set_preset(size_t index) {
if (index >= network_presets_.size()) return;
current_preset_index_ = index;
network_sim_.update_config(network_presets_[index].second);
std::cout << "Direct switched to preset: " << network_presets_[index].first << "\n";
}
InputState get_bot_input() {
InputState input;
if (!bot_enabled_) return input;
float dt = bot_clock_.restart().asSeconds();
bot_timer_ += dt;
direction_timer_ += dt;
if (direction_timer_ >= bot_config_.movement_change_interval_sec) {
direction_timer_ = 0.0f;
if (bot_config_.random_movements) {
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
current_bot_direction_.x = dist(gen);
current_bot_direction_.y = dist(gen);
if (current_bot_direction_.length_squared() > 0.01f) {
current_bot_direction_ = current_bot_direction_.normalized();
}
} else {
circle_angle_ += bot_config_.circle_speed * dt;
current_bot_direction_.x = std::cos(circle_angle_);
current_bot_direction_.y = std::sin(circle_angle_);
}
}
input.move_right = current_bot_direction_.x > 0.3f;
input.move_left = current_bot_direction_.x < -0.3f;
input.move_down = current_bot_direction_.y > 0.3f;
input.move_up = current_bot_direction_.y < -0.3f;
return input;
}
void run_auto_test() {
if (!auto_test_mode_) return;
std::cout << "Starting auto test...\n";
direct_select_team(TeamId::None);
}
bool connect(const std::string& host, uint16_t port) {
host_ = host;
port_ = port;
return true;
}
// Жестко багуется ибо microsoft
bool connect_to_server() {
std::cout << "Creating client...\n";
if (!network_.create_client()) {
std::cerr << "Failed to create client\n";
#ifdef _WIN32
std::cerr << "WSA Error: " << WSAGetLastError() << "\n";
#endif
return false;
}
std::cout << "Client created successfully\n";
std::cout << "Connecting to " << host_ << ":" << port_ << "...\n";
server_peer_ = network_.connect(host_, port_);
if (!server_peer_) {
std::cerr << "Failed to initiate connection\n";
#ifdef _WIN32
std::cerr << "WSA Error: " << WSAGetLastError() << "\n";
#endif
return false;
}
std::cout << "Connection initiated, waiting for response...\n";
ENetHost* raw_host = network_.raw();
if (raw_host) {
std::cout << "Host socket: " << raw_host->socket << "\n";
}
ENetEvent event;
int result = network_.service(event, 5000);
std::cout << "Service returned: " << result << "\n";
#ifdef _WIN32
if (result < 0) {
std::cerr << "WSA Error after service: " << WSAGetLastError() << "\n";
}
#endif
if (result > 0 && event.type == ENET_EVENT_TYPE_CONNECT) {
std::cout << "Connected to server\n";
send_connect_request();
return true;
}
return false;
}
void send_delayed_packets() {
if (!network_sim_.is_enabled()) return;
auto packets = network_sim_.receive_packets();
for (const auto& packet_data : packets) {
if (packet_data.empty()) continue;
NetworkChannel channel = NetworkChannel::Unreliable;
if (!packet_data.empty()) {
MessageType type = static_cast<MessageType>(packet_data[0]);
if (type == MessageType::ClientConnect ||
type == MessageType::ClientSetAlgorithm) {
channel = NetworkChannel::Reliable;
}
}
network_.send(server_peer_, channel,
packet_data.data(), packet_data.size(),
channel == NetworkChannel::Reliable);
}
}
void set_custom_network_config(float latency_ms, float jitter_ms,
float packet_loss = 0.0f, float duplication = 0.0f) {
auto cfg = NetworkPresets::Custom(latency_ms, jitter_ms, packet_loss, duplication);
network_sim_.update_config(cfg);
std::cout << std::format("Custom network: {}ms ±{}ms, loss {:.1f}%\n",
latency_ms, jitter_ms, packet_loss * 100.0f);
}
void run() {
sf::Clock clock;
sf::Clock metrics_clock;
sf::Clock compensation_metrics_clock;
size_t current_algo_index = 0;
size_t current_preset_index = 0;
enum class AutoTestPhase { WaitingConnect, StabilizeAlgo, TestPreset, Finished } phase = AutoTestPhase::WaitingConnect;
float phase_timer = 0.0f;
run_auto_test();
while (window_.isOpen()) {
float dt = clock.restart().asSeconds();
while (const auto event = window_.pollEvent()) {
if (event->is<sf::Event::Closed>()) {
if (state_ == ClientState::Connected && server_peer_) {
enet_peer_disconnect(server_peer_, 0);
ENetEvent event;
while (network_.service(event, 100) > 0) {
if (event.type == ENET_EVENT_TYPE_DISCONNECT) {
break;
}
}
}
window_.close();
}
if (const auto* key = event->getIf<sf::Event::KeyPressed>()) {
handle_key_press(key->code);
}
if (const auto* mouse = event->getIf<sf::Event::MouseButtonPressed>()) {
handle_mouse_click(mouse->position);
}
}
switch (state_) {
case ClientState::TeamSelection:
render_team_selection();
break;
case ClientState::Connecting:
process_network();
render_connecting();
break;
case ClientState::Connected:
process_network();
process_input();
{
double current_time_ms = Timestamp::now_ms();
world_.update(current_time_ms);
}
ping_timer_ += dt;
if (ping_timer_ >= 0.5f) {
send_ping();
ping_timer_ = 0.0f;
}
if (metrics_clock.getElapsedTime().asSeconds() >= 1.0f) {
update_metrics();
metrics_clock.restart();
}
if (compensation_metrics_clock.getElapsedTime().asSeconds() >= 1.0f) {
update_compensation_metrics();
compensation_metrics_clock.restart();
}
render();
if (auto_test_mode_) {
if (!bot_enabled_) {
bot_enabled_ = true;
bot_clock_.restart();
std::cout << "Bot enabled after connection\n";
phase = AutoTestPhase::StabilizeAlgo;
phase_timer = 0.0f;
current_algo_index = 0;
current_preset_index = 0;
direct_set_algorithm(algorithms_to_test_[0]);
std::cout << "[AutoTest] Starting with algorithm: " << algorithm_name(algorithms_to_test_[0]) << "\n";
}
phase_timer += dt;
if (phase == AutoTestPhase::StabilizeAlgo) {
if (phase_timer >= bot_config_.stabilization_time_sec) {
std::cout << "[AutoTest] Stabilization complete, starting test phase\n";
phase = AutoTestPhase::TestPreset;
phase_timer = 0.0f;
}
} else if (phase == AutoTestPhase::TestPreset) {
if (phase_timer >= bot_config_.test_duration_per_preset_sec) {
current_preset_index++;
if (current_preset_index >= network_presets_.size()) {
current_preset_index = 0;
current_algo_index++;
if (current_algo_index >= algorithms_to_test_.size()) {
phase = AutoTestPhase::Finished;
std::cout << "AUTO TEST COMPLETED!\n";
std::this_thread::sleep_for(std::chrono::seconds(5));
window_.close();
return;
}
CompensationAlgorithm next_algo = algorithms_to_test_[current_algo_index];
direct_set_algorithm(next_algo);
direct_set_preset(0);
std::cout << "[AutoTest] Switched to algorithm: " << algorithm_name(next_algo) << "\n";
std::cout << "[AutoTest] Reset to preset: " << network_presets_[0].first << "\n";
phase = AutoTestPhase::StabilizeAlgo;
phase_timer = 0.0f;
} else {
direct_set_preset(current_preset_index);
std::cout << "[AutoTest] Switched to preset: " << network_presets_[current_preset_index].first << "\n";
phase = AutoTestPhase::StabilizeAlgo;
phase_timer = 0.0f;
}
}
}
}
break;
case ClientState::Disconnected:
render_disconnected();
if (auto_test_mode_) {
window_.close();
}
break;
}
window_.display();
}
}
void handle_key_press(sf::Keyboard::Key key) {
if (state_ != ClientState::Connected) {
if (key == sf::Keyboard::Key::Escape) {
window_.close();
}
return;
}
switch (key) {
case sf::Keyboard::Key::Num1:
set_algorithm(CompensationAlgorithm::None);
break;
case sf::Keyboard::Key::Num2:
set_algorithm(CompensationAlgorithm::ClientPrediction);
break;
case sf::Keyboard::Key::Num3:
set_algorithm(CompensationAlgorithm::PredictionReconciliation);
break;
case sf::Keyboard::Key::Num4:
set_algorithm(CompensationAlgorithm::EntityInterpolation);
break;
case sf::Keyboard::Key::Num5:
set_algorithm(CompensationAlgorithm::DeadReckoning);
break;
case sf::Keyboard::Key::Num6:
set_algorithm(CompensationAlgorithm::Hybrid);
break;
case sf::Keyboard::Key::Num7:
set_algorithm(CompensationAlgorithm::ServerLagCompensation);
break;
case sf::Keyboard::Key::F1:
show_server_positions_ = !show_server_positions_;
break;
case sf::Keyboard::Key::F2:
show_predicted_positions_ = !show_predicted_positions_;
break;
case sf::Keyboard::Key::F3:
show_interpolated_positions_ = !show_interpolated_positions_;
break;
case sf::Keyboard::Key::F4:
show_velocity_vectors_ = !show_velocity_vectors_;
break;
case sf::Keyboard::Key::F5:
show_debug_info_ = !show_debug_info_;
break;
case sf::Keyboard::Key::Add:
interpolation_delay_ms_ = (std::min)(interpolation_delay_ms_ + 10.0f, 500.0f);
world_.set_interpolation_delay(interpolation_delay_ms_);
break;
case sf::Keyboard::Key::Subtract:
interpolation_delay_ms_ = (std::max)(interpolation_delay_ms_ - 10.0f, 0.0f);
world_.set_interpolation_delay(interpolation_delay_ms_);
break;
case sf::Keyboard::Key::Escape:
window_.close();
break;
case sf::Keyboard::Key::N:
current_preset_index_ = (current_preset_index_ + 1) % network_presets_.size();
network_sim_.update_config(network_presets_[current_preset_index_].second);
std::cout << "Network preset: " << network_presets_[current_preset_index_].first << "\n";
break;
case sf::Keyboard::Key::M:
if (current_preset_index_ == 0) {
current_preset_index_ = network_presets_.size() - 1;
} else {
current_preset_index_--;
}
network_sim_.update_config(network_presets_[current_preset_index_].second);
std::cout << "Network preset: " << network_presets_[current_preset_index_].first << "\n";
break;
case sf::Keyboard::Key::F6:
show_network_stats_ = !show_network_stats_;
break;
case sf::Keyboard::Key::L:
network_sim_.trigger_lag_spike(1000.0f, 500.0f);
std::cout << "Lag spike triggered (1s duration, +500ms delay)\n";
break;
case sf::Keyboard::Key::PageUp: {
auto cfg = network_sim_.config();
cfg.enabled = true;
cfg.base_latency_ms += 10.0f;
network_sim_.update_config(cfg);
std::cout << "Latency: " << cfg.base_latency_ms << "ms\n";
break;
}
case sf::Keyboard::Key::PageDown: {
auto cfg = network_sim_.config();
cfg.base_latency_ms = (std::max)(0.0f, cfg.base_latency_ms - 10.0f);
if (cfg.base_latency_ms == 0.0f) {
cfg.enabled = false;
}
network_sim_.update_config(cfg);
std::cout << "Latency: " << cfg.base_latency_ms << "ms\n";
break;
}
case sf::Keyboard::Key::Home: {
auto cfg = network_sim_.config();
cfg.enabled = true;
cfg.jitter_ms += 5.0f;
network_sim_.update_config(cfg);
std::cout << "Jitter: " << cfg.jitter_ms << "ms\n";
break;
}
case sf::Keyboard::Key::End: {
auto cfg = network_sim_.config();
cfg.jitter_ms = (std::max)(0.0f, cfg.jitter_ms - 5.0f);
network_sim_.update_config(cfg);
std::cout << "Jitter: " << cfg.jitter_ms << "ms\n";
break;
}
case sf::Keyboard::Key::P: {
auto cfg = network_sim_.config();
cfg.enabled = true;
cfg.packet_loss += 0.01f;
if (cfg.packet_loss > 0.2f) cfg.packet_loss = 0.0f;
network_sim_.update_config(cfg);
std::cout << "Packet loss: " << (cfg.packet_loss * 100.0f) << "%\n";
break;
}
default:
break;
}
}
void handle_mouse_click(sf::Vector2i mouse_pos) {
if (state_ != ClientState::TeamSelection) return;
if (red_team_button_.contains(sf::Vector2f(mouse_pos))) {
selected_team_ = TeamId::Red;
state_ = ClientState::Connecting;
if (!connect_to_server()) {
state_ = ClientState::Disconnected;
}
} else if (blue_team_button_.contains(sf::Vector2f(mouse_pos))) {
selected_team_ = TeamId::Blue;
state_ = ClientState::Connecting;
if (!connect_to_server()) {
state_ = ClientState::Disconnected;
}
} else if (auto_team_button_.contains(sf::Vector2f(mouse_pos))) {
selected_team_ = TeamId::None; // Авто-выбор
state_ = ClientState::Connecting;
if (!connect_to_server()) {
state_ = ClientState::Disconnected;
}
}
}
void set_algorithm(CompensationAlgorithm algo) {
world_.set_algorithm(algo);
current_algorithm_ = algo;
ClientSetAlgorithmMessage msg;
msg.algorithm = algo;
WriteBuffer buf;
buf.write_u8(static_cast<uint8_t>(MessageType::ClientSetAlgorithm));
buf.write_u8(static_cast<uint8_t>(algo));
network_.send(server_peer_, NetworkChannel::Reliable, buf.data(), buf.size(), true);
std::cout << "Algorithm changed to: " << algorithm_name(algo) << "\n";
}
void send_connect_request() {
ClientConnectMessage msg;
msg.protocol_version = 1;
msg.team_preference = selected_team_;
std::strncpy(msg.player_name, player_name_.c_str(), sizeof(msg.player_name) - 1);
WriteBuffer buf;
buf.write_u8(static_cast<uint8_t>(MessageType::ClientConnect));
buf.write_u32(msg.protocol_version);
buf.write_string(msg.player_name, sizeof(msg.player_name));
buf.write_u8(static_cast<uint8_t>(msg.team_preference));
if (network_sim_.is_enabled()) {
network_sim_.send_packet(buf.data(), buf.size());
} else {
network_.send(server_peer_, NetworkChannel::Reliable, buf.data(), buf.size(), true);
}
}
void process_network() {
send_delayed_packets();
ENetEvent event;
while (network_.service(event, 0) > 0) {
switch (event.type) {
case ENET_EVENT_TYPE_RECEIVE:
handle_packet(event.packet);
enet_packet_destroy(event.packet);
break;
case ENET_EVENT_TYPE_DISCONNECT:
std::cout << "Disconnected from server\n";
state_ = ClientState::Disconnected;
break;
default:
break;
}
}
}
void handle_packet(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::ServerAccept:
handle_server_accept(buf);
break;
case MessageType::ServerReject:
handle_server_reject(buf);
break;
case MessageType::ServerSnapshot:
handle_snapshot(packet->data, packet->dataLength);
break;
case MessageType::ServerConfig:
handle_server_config(buf);
break;
case MessageType::PingResponse:
handle_ping_response(buf);
break;
default:
break;
}
}
void handle_server_accept(ReadBuffer& buf) {
uint16_t client_id;
uint32_t entity_id;
uint8_t team;
uint32_t tick;
uint64_t server_time;
if (!buf.read_u16(client_id)) return;
if (!buf.read_u32(entity_id)) return;
if (!buf.read_u8(team)) return;
if (!buf.read_u32(tick)) return;
if (!buf.read_u64(server_time)) return;
local_client_id_ = client_id;
local_entity_id_ = entity_id;
local_team_ = static_cast<TeamId>(team);
world_.set_local_player(local_client_id_, local_entity_id_);
state_ = ClientState::Connected;
std::cout << "Connected! Client ID: " << client_id
<< ", Entity ID: " << entity_id
<< ", Team: " << (local_team_ == TeamId::Red ? "Red" : "Blue") << "\n";
}
void handle_server_reject(ReadBuffer& buf) {
uint8_t reason;
buf.read_u8(reason);
std::cerr << "Connection rejected. Reason: " << static_cast<int>(reason) << "\n";
state_ = ClientState::Disconnected;
}
void handle_snapshot(const uint8_t* data, size_t size) {
ReadBuffer buf(data, size);
WorldSnapshot snapshot;
if (deserialize_snapshot(buf, snapshot)) {
world_.receive_snapshot(snapshot);
last_server_tick_ = snapshot.server_tick;
snapshots_received_++;
}
}
void handle_server_config(ReadBuffer& buf) {
SimulationParams new_params = DEFAULT_SIMULATION_PARAMS;
if (!buf.read_u32(new_params.tick_rate)) return;
if (!buf.read_u32(new_params.snapshot_rate)) return;
if (!buf.read_float(new_params.arena_width)) return;
if (!buf.read_float(new_params.arena_height)) return;
if (!buf.read_float(new_params.player_radius)) return;
if (!buf.read_float(new_params.player_max_speed)) return;
if (!buf.read_float(new_params.ball_radius)) return;
new_params.fixed_delta_time = 1.0 / static_cast<double>(new_params.tick_rate);
world_.set_simulation_params(new_params);
std::cout << "Server config: tick_rate=" << new_params.tick_rate
<< ", snapshot_rate=" << new_params.snapshot_rate << "\n";
}
void handle_ping_response(ReadBuffer& buf) {
uint32_t ping_id;
uint64_t client_time, server_time;
uint32_t server_tick;
if (!buf.read_u32(ping_id)) return;
if (!buf.read_u64(client_time)) return;
if (!buf.read_u64(server_time)) return;
if (!buf.read_u32(server_tick)) return;
uint64_t now = Timestamp::now_us();
time_sync_.add_sample(client_time, server_time, now);
current_rtt_ms_ = time_sync_.get_rtt_ms();
current_jitter_ms_ = time_sync_.get_jitter_ms();
}
void process_input() {
InputState input;
if (auto_test_mode_ && bot_enabled_) {
input = get_bot_input();
} else {
input.move_up = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::W);
input.move_down = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::S);
input.move_left = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::A);
input.move_right = sf::Keyboard::isKeyPressed(sf::Keyboard::Key::D);
}
InputCommand cmd;
cmd.sequence = ++input_sequence_;
cmd.client_tick = client_tick_++;
cmd.timestamp_us = Timestamp::now_us();
cmd.input = input;
world_.process_local_input(cmd);
send_input(cmd);
recent_inputs_.push(cmd);
}
void send_input(const InputCommand& cmd) {
WriteBuffer buf;
buf.write_u8(static_cast<uint8_t>(MessageType::ClientInput));
buf.write_u8(1);
buf.write_u32(cmd.sequence);
buf.write_u32(cmd.client_tick);
buf.write_u64(cmd.timestamp_us);
buf.write_u8(cmd.input.to_byte());
if (network_sim_.is_enabled()) {
network_sim_.send_packet(buf.data(), buf.size());
} else {
network_.send(server_peer_, NetworkChannel::Unreliable, buf.data(), buf.size(), false);
}
inputs_sent_++;
}
void send_ping() {
WriteBuffer buf;
buf.write_u8(static_cast<uint8_t>(MessageType::PingRequest));
buf.write_u32(++ping_id_);
buf.write_u64(Timestamp::now_us());
if (network_sim_.is_enabled()) {
network_sim_.send_packet(buf.data(), buf.size());
} else {
network_.send(server_peer_, NetworkChannel::Unreliable, buf.data(), buf.size(), false);
}
}
void update_metrics() {
fps_ = static_cast<float>(frame_count_);
frame_count_ = 0;
inputs_per_second_ = inputs_sent_;
inputs_sent_ = 0;
snapshots_per_second_ = snapshots_received_;
snapshots_received_ = 0;
uint64_t now_ms = Timestamp::now_ms();
if (state_ == ClientState::Connected && now_ms - last_log_time_ms_ >= 1000) {
auto pos_metrics = world_.metrics().get_position_metrics();
auto comp_metrics = world_.metrics().get_compensation_metrics(1.0); // за последнюю секунду
auto cfg = network_sim_.config();
auto sim_stats = network_sim_.get_stats();
uint64_t packets_sent_ps = sim_stats.packets_sent - prev_packets_sent_;
uint64_t packets_delivered_ps = sim_stats.packets_delivered - prev_packets_delivered_;
uint64_t packets_lost_ps = sim_stats.packets_lost - prev_packets_lost_;
uint64_t packets_duplicated_ps = sim_stats.packets_duplicated - prev_packets_duplicated_;
prev_packets_sent_ = sim_stats.packets_sent;
prev_packets_delivered_ = sim_stats.packets_delivered;
prev_packets_lost_ = sim_stats.packets_lost;
prev_packets_duplicated_ = sim_stats.packets_duplicated;
logger_.log(
now_ms,
network_presets_[current_preset_index_].first,
algorithm_name(current_algorithm_),
current_rtt_ms_,
current_jitter_ms_,
cfg.packet_loss * 100.0f,
packets_sent_ps,
packets_delivered_ps,
packets_lost_ps,
packets_duplicated_ps,
fps_,
inputs_per_second_,
snapshots_per_second_,
pos_metrics,
comp_metrics
);
world_.metrics().reset_compensation_metrics();
world_.metrics().reset();
last_log_time_ms_ = now_ms;
}
}
void render_team_selection() {
frame_count_++;
window_.clear(sf::Color(20, 20, 30));
sf::Text title(font_, "SELECT YOUR TEAM", 48);
title.setPosition(sf::Vector2f(window_.getSize().x / 2 - 250.0f, 100.0f));
title.setFillColor(sf::Color::White);
window_.draw(title);
float button_width = 300.0f;
float button_height = 150.0f;
float center_x = window_.getSize().x / 2;
float center_y = window_.getSize().y / 2;
float red_x = center_x - button_width - 50.0f;
float red_y = center_y - button_height / 2.0f;
red_team_button_.position = { red_x, red_y };
red_team_button_.size = { button_width, button_height };
sf::RectangleShape red_button(sf::Vector2f(button_width, button_height));
red_button.setPosition(sf::Vector2f(red_x, red_y));
red_button.setFillColor(sf::Color(150, 30, 30));
red_button.setOutlineColor(sf::Color(255, 100, 100));
red_button.setOutlineThickness(3.0f);
sf::Vector2i mouse_pos_i = sf::Mouse::getPosition(window_);
sf::Vector2f mouse_pos_f(static_cast<float>(mouse_pos_i.x), static_cast<float>(mouse_pos_i.y));
if (red_team_button_.contains(mouse_pos_f)) {
red_button.setFillColor(sf::Color(200, 50, 50));
}
window_.draw(red_button);
sf::Text red_text(font_, "RED TEAM", 32);
red_text.setPosition(sf::Vector2f(red_x + 50.0f, red_y + 60.0f));
red_text.setFillColor(sf::Color::White);
window_.draw(red_text);
float blue_x = center_x + 50.0f;
float blue_y = center_y - button_height / 2.0f;
blue_team_button_.position = { blue_x, blue_y };
blue_team_button_.size = { button_width, button_height };
sf::RectangleShape blue_button(sf::Vector2f(button_width, button_height));
blue_button.setPosition(sf::Vector2f(blue_x, blue_y));
blue_button.setFillColor(sf::Color(30, 30, 150));
blue_button.setOutlineColor(sf::Color(100, 100, 255));
blue_button.setOutlineThickness(3.0f);
if (blue_team_button_.contains(mouse_pos_f)) {
blue_button.setFillColor(sf::Color(50, 50, 200));
}
window_.draw(blue_button);
sf::Text blue_text(font_, "BLUE TEAM", 32);
blue_text.setPosition(sf::Vector2f(blue_x + 40.0f, blue_y + 60.0f));
blue_text.setFillColor(sf::Color::White);
window_.draw(blue_text);
float auto_x = center_x - button_width / 2.0f;
float auto_y = center_y + button_height;
auto_team_button_.position = { auto_x, auto_y };
auto_team_button_.size = { button_width, button_height * 0.6f };
sf::RectangleShape auto_button(sf::Vector2f(button_width, button_height * 0.6f));
auto_button.setPosition(sf::Vector2f(auto_x, auto_y));
auto_button.setFillColor(sf::Color(60, 60, 60));
auto_button.setOutlineColor(sf::Color(150, 150, 150));
auto_button.setOutlineThickness(3.0f);
if (auto_team_button_.contains(mouse_pos_f)) {
auto_button.setFillColor(sf::Color(80, 80, 80));
}
window_.draw(auto_button);
sf::Text auto_text(font_, "AUTO BALANCE", 24);
auto_text.setPosition(sf::Vector2f(auto_x + 60.0f, auto_y + 30.0f));
auto_text.setFillColor(sf::Color::White);
window_.draw(auto_text);
sf::Text instruction(font_, "Click to select your team", 20);
instruction.setPosition(sf::Vector2f(center_x - 120.0f, window_.getSize().y - 100.0f));
instruction.setFillColor(sf::Color(150, 150, 150));
window_.draw(instruction);
}
void render_connecting() {
frame_count_++;
window_.clear(sf::Color(20, 20, 30));
sf::Text text(font_, "Connecting to server...", 36);
text.setPosition(sf::Vector2f(window_.getSize().x / 2 - 200.0f,
window_.getSize().y / 2));
text.setFillColor(sf::Color::White);
window_.draw(text);
}
void render_disconnected() {
frame_count_++;
window_.clear(sf::Color(20, 20, 30));
sf::Text text(font_, "Disconnected from server", 36);
text.setPosition(sf::Vector2f(window_.getSize().x / 2 - 230.0f,
window_.getSize().y / 2 - 50.0f));
text.setFillColor(sf::Color::Red);
window_.draw(text);
sf::Text instruction(font_, "Press ESC to exit", 24);
instruction.setPosition(sf::Vector2f(window_.getSize().x / 2 - 100.0f,
window_.getSize().y / 2 + 50.0f));
instruction.setFillColor(sf::Color(150, 150, 150));
window_.draw(instruction);
}
void render() {
frame_count_++;
window_.clear(sf::Color(30, 30, 40));
const auto& params = world_.params();
float arena_display_width = netcode::DEFAULT_SIMULATION_PARAMS.arena_width;
float arena_display_height = netcode::DEFAULT_SIMULATION_PARAMS.arena_height;
float scale_x = arena_display_width / params.arena_width;
float scale_y = arena_display_height / params.arena_height;
float scale = (std::min)(scale_x, scale_y);
float offset_x = 60.0f;
float offset_y = 20.0f;
auto to_screen = [&](const Vec2& pos) -> sf::Vector2f {
return sf::Vector2f(offset_x + pos.x * scale, offset_y + pos.y * scale);
};
// Арена
sf::RectangleShape arena(sf::Vector2f(params.arena_width * scale, params.arena_height * scale));
arena.setPosition(sf::Vector2f(offset_x, offset_y));
arena.setFillColor(sf::Color(50, 50, 60));
arena.setOutlineColor(sf::Color::White);
arena.setOutlineThickness(2.0f);
window_.draw(arena);
// Зоны команд
sf::RectangleShape red_zone(sf::Vector2f(params.red_zone.width * scale, params.red_zone.height * scale));
red_zone.setPosition(to_screen(Vec2(params.red_zone.x, params.red_zone.y)));
red_zone.setFillColor(sf::Color(255, 0, 0, 50));
red_zone.setOutlineColor(sf::Color(255, 0, 0, 150));
red_zone.setOutlineThickness(2.0f);
window_.draw(red_zone);
sf::RectangleShape blue_zone(sf::Vector2f(params.blue_zone.width * scale, params.blue_zone.height * scale));
blue_zone.setPosition(to_screen(Vec2(params.blue_zone.x, params.blue_zone.y)));
blue_zone.setFillColor(sf::Color(0, 0, 255, 50));
blue_zone.setOutlineColor(sf::Color(0, 0, 255, 150));
blue_zone.setOutlineThickness(2.0f);
window_.draw(blue_zone);
// Сущности
for (const auto& [id, entity] : world_.entities()) {
render_entity(entity, scale, offset_x, offset_y);
}
// UI
render_ui();
}
void render_entity(const ClientEntity& entity, float scale, float offset_x, float offset_y) {
auto to_screen = [&](const Vec2& pos) -> sf::Vector2f {
return sf::Vector2f(offset_x + pos.x * scale, offset_y + pos.y * scale);
};
float radius = entity.radius * scale;
// Серверная позиция (полупрозрачная)
if (show_server_positions_) {
sf::CircleShape server_circle(radius);
server_circle.setOrigin(sf::Vector2f(radius, radius));
server_circle.setPosition(to_screen(entity.server_position));
server_circle.setFillColor(sf::Color(100, 100, 100, 80));
server_circle.setOutlineColor(sf::Color(150, 150, 150, 150));
server_circle.setOutlineThickness(1.0f);
window_.draw(server_circle);
}
// Предсказанная позиция
if (show_predicted_positions_ && entity.is_local_player) {
sf::CircleShape pred_circle(radius);
pred_circle.setOrigin(sf::Vector2f(radius, radius));
pred_circle.setPosition(to_screen(entity.predicted_position));
pred_circle.setFillColor(sf::Color(0, 255, 0, 60));
pred_circle.setOutlineColor(sf::Color(0, 255, 0, 200));
pred_circle.setOutlineThickness(1.0f);
window_.draw(pred_circle);
}
// Интерполированная позиция
if (show_interpolated_positions_ && !entity.is_local_player) {
sf::CircleShape interp_circle(radius);
interp_circle.setOrigin(sf::Vector2f(radius, radius));
interp_circle.setPosition(to_screen(entity.interpolated_position));
interp_circle.setFillColor(sf::Color(255, 255, 0, 60));
interp_circle.setOutlineColor(sf::Color(255, 255, 0, 200));
interp_circle.setOutlineThickness(1.0f);
window_.draw(interp_circle);
}
// рендер позиция
sf::CircleShape main_circle(radius);
main_circle.setOrigin(sf::Vector2f(radius, radius));
main_circle.setPosition(to_screen(entity.render_position));
if (entity.type == EntityType::Player) {
if (entity.is_local_player) {
main_circle.setFillColor(sf::Color(0, 200, 0));
main_circle.setOutlineColor(sf::Color::White);
} else if (entity.team == TeamId::Red) {
main_circle.setFillColor(sf::Color(200, 50, 50));
main_circle.setOutlineColor(sf::Color(255, 100, 100));
} else {
main_circle.setFillColor(sf::Color(50, 50, 200));
main_circle.setOutlineColor(sf::Color(100, 100, 255));
}
} else if (entity.type == EntityType::Ball) {
main_circle.setFillColor(sf::Color(255, 200, 50));
main_circle.setOutlineColor(sf::Color::White);
}
main_circle.setOutlineThickness(2.0f);
window_.draw(main_circle);
// Вектор скорости
if (show_velocity_vectors_) {
Vec2 vel_end = entity.render_position + entity.server_velocity * 0.1f;
sf::Vertex line[] = {
sf::Vertex(to_screen(entity.render_position), sf::Color::Yellow),
sf::Vertex(to_screen(vel_end), sf::Color::Red)
};
window_.draw(line, 2, sf::PrimitiveType::Lines);
}
// Линия коррекции
if (entity.is_local_player && entity.correction_offset.length_squared() > 0.1f) {
Vec2 correction_end = entity.render_position - entity.correction_offset;
sf::Vertex line[] = {
sf::Vertex(to_screen(entity.render_position), sf::Color::Magenta),
sf::Vertex(to_screen(correction_end), sf::Color::Cyan)
};
window_.draw(line, 2, sf::PrimitiveType::Lines);
}
}
void update_compensation_metrics() {
world_.metrics().reset_compensation_metrics();
}
void render_ui() {
float y = 10.0f;
float line_height = 20.0f;
auto draw_text = [&](const std::string& str, sf::Color color = sf::Color::White) {
sf::Text text(font_, str, 14);
text.setPosition(sf::Vector2f(10.0f, y));
text.setFillColor(color);
window_.draw(text);
y += line_height;
};
// Команда
std::string team_str = (local_team_ == TeamId::Red) ? "RED" :
(local_team_ == TeamId::Blue) ? "BLUE" : "NONE";
sf::Color team_color = (local_team_ == TeamId::Red) ? sf::Color(255, 100, 100) :
sf::Color(100, 100, 255);
draw_text(std::format("Team: {}", team_str), team_color);
// Алгоритм
draw_text(std::format("Algorithm: {} [1-7 to change]", algorithm_name(current_algorithm_)),
sf::Color::Yellow);
// Сеть
draw_text(std::format("RTT: {:.1f} ms | Jitter: {:.1f} ms", current_rtt_ms_, current_jitter_ms_));
draw_text(std::format("Server Tick: {} | FPS: {:.0f}", last_server_tick_, fps_));
draw_text(std::format("Inputs/s: {} | Snapshots/s: {}", inputs_per_second_, snapshots_per_second_));
draw_text(std::format("Interp Delay: {:.0f} ms [+/- to adjust] (Auto: {:.0f} ms)",
interpolation_delay_ms_,
current_rtt_ms_ / 2.0 + current_jitter_ms_ * 2.0));
// Счёт
y = 30.0f;
sf::Text score_text(font_, std::format("Red: {:.1f}s | Blue: {:.1f}s",
world_.red_team_time(), world_.blue_team_time()), 18);
score_text.setPosition(sf::Vector2f(window_.getSize().x - 260.0f, y));
score_text.setFillColor(sf::Color::White);
window_.draw(score_text);
if (show_debug_info_) {
y = 150.0f;
auto pos_metrics = world_.metrics().get_position_metrics();
draw_text(std::format("Position MAE: {:.2f} | MSE: {:.2f}", pos_metrics.mae, pos_metrics.mse));
draw_text(std::format("Max Error: {:.2f} | Samples: {}", pos_metrics.max_error, pos_metrics.sample_count));
auto comp_metrics = world_.metrics().get_compensation_metrics(1.0);
draw_text(std::format("Predictions/s: {} | Reconciliations/s: {}",
comp_metrics.predictions_per_second, comp_metrics.reconciliations_per_second));
draw_text(std::format("Avg Correction: {:.2f} | Max: {:.2f}",
comp_metrics.avg_correction_distance, comp_metrics.max_correction_distance));
}
if (show_network_stats_ && network_sim_.is_enabled()) {
y = 310.0f;
auto sim_stats = network_sim_.get_stats();
auto cfg = network_sim_.config();
draw_text("Network Simulation", sf::Color::Cyan);
draw_text(std::format("Preset: {}", network_presets_[current_preset_index_].first),
sf::Color::Yellow);
draw_text(std::format("Base Latency: {:.1f}ms (RTT: {:.1f}ms)",
cfg.base_latency_ms, cfg.base_latency_ms * 2.0f));
draw_text(std::format("Jitter: {:.1f}ms", cfg.jitter_ms));
draw_text(std::format("Packet Loss: {:.1f}%", cfg.packet_loss * 100.0f));
if (cfg.packet_duplication > 0.0f) {
draw_text(std::format("Duplication: {:.1f}%", cfg.packet_duplication * 100.0f));
}
draw_text(std::format("Sent: {} | Delivered: {} | Lost: {}",
sim_stats.packets_sent, sim_stats.packets_delivered, sim_stats.packets_lost));
if (sim_stats.packets_duplicated > 0) {
draw_text(std::format("Duplicated: {}", sim_stats.packets_duplicated));
}
draw_text(std::format("Queue: {} packets", sim_stats.packets_in_queue));
}
else {
y = 300;
draw_text("No simulation", sf::Color::Yellow);
}
y = window_.getSize().y - 120.0f;
draw_text("F1-F5: Visualization | F6: Network Stats", sf::Color(150, 150, 150));
draw_text("N/M: Network Preset | L: Lag Spike | P: Packet Loss", sf::Color(150, 150, 150));
draw_text("PgUp/PgDn: Latency | Home/End: Jitter", sf::Color(150, 150, 150));
draw_text("WASD: Move | 1-7: Algorithm | ESC: Quit", sf::Color(150, 150, 150));
}
private:
// Окно и рендер
sf::RenderWindow window_;
sf::Font font_;
// Состояние
ClientState state_;
std::string host_;
uint16_t port_;
// Выбор команды
TeamId selected_team_ = TeamId::None;
sf::FloatRect red_team_button_;
sf::FloatRect blue_team_button_;
sf::FloatRect auto_team_button_;
// Сеть
ENetInitializer enet_init_;
NetworkHost network_;
ENetPeer* server_peer_ = nullptr;
TimeSynchronizer time_sync_;
NetworkSimulator network_sim_;
bool show_network_stats_ = true;
std::vector<std::pair<std::string, NetworkSimulator::Config>> network_presets_;
size_t current_preset_index_ = 0;
// Мир
ClientWorld world_;
// Состояние подключения
ClientId local_client_id_ = INVALID_CLIENT_ID;
EntityId local_entity_id_ = INVALID_ENTITY_ID;
TeamId local_team_ = TeamId::None;
std::string player_name_ = "Player";
// Ввод
SequenceNumber input_sequence_ = 0;
TickNumber client_tick_ = 0;
RingBuffer<InputCommand, 64> recent_inputs_;
// Алгоритм
CompensationAlgorithm current_algorithm_ = CompensationAlgorithm::Hybrid;
float interpolation_delay_ms_ = 100.0f;
// Визуализация
bool show_server_positions_ = true;
bool show_predicted_positions_ = true;
bool show_interpolated_positions_ = true;
bool show_velocity_vectors_ = false;
bool show_debug_info_ = true;
// Метрики
float ping_timer_ = 0.0f;
uint32_t ping_id_ = 0;
double current_rtt_ms_ = 0.0;
double current_jitter_ms_ = 0.0;
TickNumber last_server_tick_ = 0;
MetricsLogger logger_{"client_metrics.csv"};
uint64_t last_log_time_ms_ = 0;
uint64_t prev_packets_sent_ = 0;
uint64_t prev_packets_delivered_ = 0;
uint64_t prev_packets_lost_ = 0;
uint64_t prev_packets_duplicated_ = 0;
float fps_ = 0.0f;
uint32_t frame_count_ = 0;
uint32_t inputs_sent_ = 0;
uint32_t inputs_per_second_ = 0;
uint32_t snapshots_received_ = 0;
uint32_t snapshots_per_second_ = 0;
// Тесты
bool auto_test_mode_ = false;
bool bot_enabled_ = false;
struct BotConfig {
float test_duration_per_preset_sec = 10.0f;
float stabilization_time_sec = 10.0f;
float movement_change_interval_sec = 4.0f;
float speed_factor = 1.0f;
bool random_movements = true;
float circle_radius = 15.0f;
float circle_speed = 2.0f;
};
BotConfig bot_config_;
float bot_timer_ = 0.0f;
float direction_timer_ = 0.0f;
Vec2 current_bot_direction_ = {0.0f, 0.0f};
float circle_angle_ = 0.0f;
std::vector<CompensationAlgorithm> algorithms_to_test_ = {
CompensationAlgorithm::ClientPrediction,
CompensationAlgorithm::PredictionReconciliation,
CompensationAlgorithm::EntityInterpolation,
CompensationAlgorithm::DeadReckoning,
CompensationAlgorithm::Hybrid,
};
};
int main(int argc, char* argv[]) {
std::string host = "::1";
uint16_t port = 7777;
bool auto_test = false;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--auto-test") {
auto_test = true;
} else if (arg == "-h" || arg == "--host") {
if (i + 1 < argc) host = argv[++i];
} else if (arg == "-p" || arg == "--port") {
if (i + 1 < argc) port = static_cast<uint16_t>(std::stoi(argv[++i]));
} else if (arg == "--help") {
std::cout << "Usage: client [-h host] [-p port]\n";
return 0;
}
}
std::cout << "Will connect to " << host << ":" << port << "...\n";
try {
GameClient client;
if (auto_test) {
client.enable_auto_test();
}
if (client.connect(host, port)) {
client.run();
}
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
return 0;
}