Shoot'em up multijoueur en temps réel : deux joueurs défendent un château contre des vagues d'ennemis de plus en plus rapides. Architecture client / serveur / shared strictement séparée, serveur autoritaire à 60 FPS, 30 tests automatisés.
Points clés
Toute la logique (physique, collisions, spawn, score) est calculée côté serveur à 60 FPS. Zéro triche possible.
Le client envoie uniquement les inputs et affiche l'état reçu du serveur via Canvas HTML5.
src/client, src/server et src/shared strictement séparés — aucune duplication de logique.
Tests d'intégration Socket.IO avec vrai serveur en mémoire, tests unitaires et tests de persistance SQLite.
Technologies
Architecture
Le serveur stocke le dernier input reçu et l'applique dans le tick — synchronisation cohérente même sous latence.
Gameplay
L'ennemi de base, prévisible mais nombreux.
Peu résistant mais sa vitesse le rend difficile à cibler.
Trajectoire sinusoïdale imprévisible, le plus difficile à toucher.
Marche au sol et tire des lances — menace directe sur le château.
Qualité
| Fichier de test | Ce qui est couvert |
|---|---|
| CollisionTest | Détection AABB joueur / ennemi |
| MovementTest | Déplacement clavier avec inertie |
| FireballTest | Création de fireball en mode automatique |
| ServerLogicTest | Invincibilité, HP multi-tirs, bonus, direction clic, bounds, lance multi-joueur |
| GameUtilsTest | computeDifficulty (bornes, clamp), sanitizeUsername |
| RoomServiceTest | Room solo privée, suppression de room vide |
| SocketIntegrationTest | Waiting screen, déconnexion partenaire, room solo masquée du lobby |
| DbTest | Persistance SQLite (save + top N) |
Retour d'expérience
Séparer nettement client, serveur et contrats partagés dès le départ évite la duplication et les désynchronisations réseau. J'ai compris que les types partagés dans src/shared/ sont le vrai contrat de l'application.
Stocker le dernier input et l'appliquer dans le tick serveur rend la simulation cohérente sous toute latence. Une leçon directement issue d'un bug où les inputs s'accumulaient et le personnage "sautait" à la reconnexion.
Gérer la déconnexion inattendue d'un joueur (réseau coupé, onglet fermé) sans corrompre l'état du serveur a été la partie la plus délicate. Il fallait nettoyer la room, notifier l'autre joueur et revenir au lobby proprement — chaque cas manqué créait une room fantôme.
Calibrer computeDifficulty (spawn rate, vitesse des ennemis) pour que le jeu soit difficile sans être injuste a demandé de nombreuses parties de test. Les tests unitaires sur les bornes m'ont aidé, mais l'équilibre final reste subjectif et itératif.
J'implémenterais un mécanisme de reconnexion (token de session) pour reprendre une partie interrompue. J'utiliserais aussi un format binaire plus compact pour les snapshots d'état envoyés à 60 FPS, pour réduire la bande passante sur les parties longues.
Certains managers serveur (CollisionManager, EnemyManager) ont grandi au fil du projet et mélangent plusieurs responsabilités. Je les découperais plus finement dès le départ pour que chaque manager soit testable indépendamment sans setup complexe.
Compétences transversales
Définir les types dans src/shared/ dès le début a été notre principal outil de coordination : plutôt que de s'expliquer verbalement comment communiquent client et serveur, on a écrit des types TypeScript que tout le monde pouvait lire. J'ai compris que dans une équipe, le code bien structuré est la meilleure documentation.
J'ai implémenté les managers serveur (Movement, Collision, Score) et l'ensemble des tests d'intégration Socket.IO de façon autonome, en m'appuyant uniquement sur les types partagés et la spécification du projet. Quand je bloquais, je cherchais d'abord seul avant de consulter l'équipe — ce réflexe d'autonomie s'est renforcé tout au long du projet.
La déconnexion propre d'un partenaire n'était pas prévue dans la conception initiale. Quand on a réalisé que ça créait des rooms fantômes, j'ai adapté le code existant sans tout réarchitecturer : identifier le minimum à modifier, implémenter, tester. Cette capacité à traiter l'imprévu sans tout recommencer est une compétence clé en équipe.
Équipe