Initial commit: full project code

This commit is contained in:
Oleg Ermakov
2026-01-11 01:37:39 +04:00
commit a8d725e94b
34 changed files with 4891 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

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

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