Initial commit: full project code
This commit is contained in:
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
|
||||
Reference in New Issue
Block a user