Protocole UDP¶
Utilisé pour la synchronisation du jeu en temps réel.
Port¶
4124/UDP
Header UDP (12 bytes)¶
Tous les messages UDP commencent par ce header :
struct UDPHeader {
uint16_t type; // 2 bytes (network byte order)
uint16_t sequence_num; // 2 bytes (network byte order)
uint64_t timestamp; // 8 bytes (millisecondes depuis epoch)
};
Types de Messages¶
| Type | Value | Direction | Description |
|---|---|---|---|
HeartBeat |
0x0001 |
Both | Keep-alive |
HeartBeatAck |
0x0002 |
S→C | Réponse heartbeat |
JoinGame |
0x0010 |
C→S | Auth UDP (token + roomCode) |
JoinGameAck |
0x0011 |
S→C | player_id assigné |
JoinGameNack |
0x0012 |
S→C | Rejet (token invalide) |
Snapshot |
0x0040 |
S→C | État complet du jeu (20Hz) |
PlayerInput |
0x0061 |
C→S | Inputs du joueur |
PlayerJoin |
0x0070 |
S→C | Nouveau joueur |
PlayerLeave |
0x0071 |
S→C | Joueur déconnecté |
ShootMissile |
0x0080 |
C→S | Demande de tir |
MissileSpawned |
0x0081 |
S→C | Missile créé |
MissileDestroyed |
0x0082 |
S→C | Missile détruit |
EnemyDestroyed |
0x0091 |
S→C | Ennemi détruit |
PlayerDamaged |
0x00A0 |
S→C | Joueur touché |
PlayerDied |
0x00A1 |
S→C | Joueur mort |
Structures¶
PlayerInput (4 bytes)¶
IMPORTANT : Le client envoie les touches pressées, pas la position !
struct PlayerInput {
uint16_t keys; // Bitfield des touches (network order)
uint16_t sequenceNum; // Pour réconciliation client-side
};
// Bitfield des touches
namespace InputKeys {
constexpr uint16_t UP = 0x0001;
constexpr uint16_t DOWN = 0x0002;
constexpr uint16_t LEFT = 0x0004;
constexpr uint16_t RIGHT = 0x0008;
constexpr uint16_t SHOOT = 0x0010;
}
JoinGame (39 bytes)¶
struct JoinGame {
SessionToken token; // 32 bytes - Token du login TCP
uint8_t shipSkin; // 1 byte - Skin vaisseau (1-6)
char roomCode[6]; // 6 bytes - Code de la room
};
PlayerState (10 bytes)¶
struct PlayerState {
uint8_t id; // 1 byte
uint16_t x; // 2 bytes (network order)
uint16_t y; // 2 bytes (network order)
uint8_t health; // 1 byte (0-100)
uint8_t alive; // 1 byte (0 ou 1)
uint16_t lastAckedInputSeq; // 2 bytes - Pour réconciliation
uint8_t shipSkin; // 1 byte (1-6)
};
MissileState (7 bytes)¶
struct MissileState {
uint16_t id; // 2 bytes
uint8_t owner_id; // 1 byte (0xFF = ennemi)
uint16_t x; // 2 bytes
uint16_t y; // 2 bytes
};
EnemyState (8 bytes)¶
struct EnemyState {
uint16_t id; // 2 bytes
uint16_t x; // 2 bytes
uint16_t y; // 2 bytes
uint8_t health; // 1 byte
uint8_t enemy_type; // 1 byte
};
GameSnapshot (variable)¶
struct GameSnapshot {
uint8_t player_count;
PlayerState players[MAX_PLAYERS]; // MAX = 4
uint8_t missile_count;
MissileState missiles[MAX_MISSILES]; // MAX = 32
uint8_t enemy_count;
EnemyState enemies[MAX_ENEMIES]; // MAX = 16
uint8_t enemy_missile_count;
MissileState enemy_missiles[MAX_ENEMY_MISSILES]; // MAX = 32
};
Game Loop Réseau¶
Serveur (Broadcast 20 Hz)¶
static constexpr int BROADCAST_INTERVAL_MS = 50; // 20 Hz
void UDPServer::scheduleBroadcast() {
_broadcastTimer.expires_after(
std::chrono::milliseconds(BROADCAST_INTERVAL_MS));
_broadcastTimer.async_wait([this](auto ec) {
if (!ec) {
broadcastSnapshots(); // Envoie GameSnapshot à tous
scheduleBroadcast();
}
});
}
Client (Frame-based)¶
void GameScene::update(float dt) {
// 1. Collecter les inputs
uint16_t keys = 0;
if (isKeyPressed(Key::Up)) keys |= InputKeys::UP;
if (isKeyPressed(Key::Down)) keys |= InputKeys::DOWN;
if (isKeyPressed(Key::Left)) keys |= InputKeys::LEFT;
if (isKeyPressed(Key::Right)) keys |= InputKeys::RIGHT;
// 2. Envoyer au serveur
_udpClient->sendInput(keys);
// 3. Client-side prediction
applyInputLocally(keys, dt);
// 4. Réconcilier avec le snapshot serveur
reconcileWithServer();
}
Client-Side Prediction¶
sequenceDiagram
participant C as Client
participant S as Server
Note over C: Input #1 (seq=1)
C->>C: Apply locally (prediction)
C->>S: PlayerInput (seq=1)
Note over C: Input #2 (seq=2)
C->>C: Apply locally (prediction)
C->>S: PlayerInput (seq=2)
S->>C: Snapshot (lastAckedInputSeq=1)
Note over C: Reconcile: garde #2, réapplique
Réconciliation¶
void GameScene::reconcileWithServer() {
auto snapshot = _udpClient->getLatestSnapshot();
auto& myState = findMyPlayer(snapshot);
// Supprimer les inputs déjà traités par le serveur
_pendingInputs.erase(
std::remove_if(_pendingInputs.begin(), _pendingInputs.end(),
[&](auto& i) { return i.seq <= myState.lastAckedInputSeq; }),
_pendingInputs.end()
);
// Partir de la position serveur
_localPlayer.x = myState.x;
_localPlayer.y = myState.y;
// Réappliquer les inputs non encore traités
for (auto& input : _pendingInputs) {
applyInputLocally(input.keys, TICK_DURATION);
}
}
Constantes¶
| Constante | Valeur | Description |
|---|---|---|
BROADCAST_INTERVAL_MS |
50 | Intervalle snapshot (20 Hz) |
PLAYER_TIMEOUT_MS |
2000 | Timeout déconnexion |
MAX_PLAYERS |
4 | Joueurs max par partie |
MAX_MISSILES |
32 | Missiles joueurs max |
MAX_ENEMIES |
16 | Ennemis max |
MAX_ENEMY_MISSILES |
32 | Missiles ennemis max |
Gestion de la Perte de Paquets¶
UDP ne garantit pas la livraison. Stratégies utilisées :
| Stratégie | Description |
|---|---|
| Sequence numbers | Détecter les paquets manquants via lastAckedInputSeq |
| Full state | Snapshot contient l'état complet (pas de delta) |
| Redundancy | 20 snapshots/sec compense la perte |
| Timeout | Joueur déconnecté après 2000ms sans heartbeat |