Skip to content

Justifications des Choix Technologiques et Architecturaux du Projet R-Type

Ce document regroupe l'ensemble des justifications relatives aux décisions techniques et architecturales prises dans le cadre du projet R-Type. Ces choix ont été guidés par les contraintes du projet, les exigences de performance, la maintenabilité et les objectifs pédagogiques.


Table des Matières

  1. Justification du Choix de SFML comme Bibliothèque Graphique
  2. Justification du Choix de CPM (C++ Package Manager)
  3. Justification du Choix de spdlog pour le Logging
  4. Justification du Choix de Google Test pour les Tests ECS
  5. Justification du Choix d'UDP pour le Protocole Réseau
  6. Justification du Choix d'ASIO pour la Gestion Réseau
  7. Justification de l'Architecture ECS (Entity Component System)

1. Justification du Choix de SFML comme Bibliothèque Graphique

Contexte

Le projet impose un client 2D léger, du réseau UDP, un serveur multithreadé autoritaire, une architecture moteur minimale, une compatibilité multiplateforme (Linux/Windows), une intégration CMake, une disponibilité via un gestionnaire de packages, et une livraison complète en sept semaines par une petite équipe. La priorité n'est pas l'innovation graphique mais la correction structurelle : conception du moteur, fiabilité du réseau, maintenabilité et qualité de la documentation.

Pourquoi SFML ?

SFML fournit un ensemble de fonctionnalités étroit et ciblé qui correspond aux exigences du projet sans introduire de couches d'abstraction inutiles. Il offre la création de fenêtres, un contexte OpenGL implicite, des primitives de rendu 2D simples, des sprites, des textures, des polices, de l'audio, la gestion des entrées et des utilitaires de timing. Tous les composants sont en C++ natif, portables, et packagés proprement via vcpkg ou Conan avec compatibilité MSVC complète.

SFML n'impose pas de contraintes architecturales. Le rendu reste isolé de la logique et du réseau, permettant la construction d'une séparation claire des préoccupations. Les performances sont suffisantes pour un shooter 2D.

De plus, toute l'équipe est déjà familiarisée avec SFML.

Avantages

  • Légèreté : Pas de dépendances lourdes ou de framework complexe
  • Portabilité : Support natif Linux/Windows/macOS
  • Intégration CMake : Facilement intégrable dans le système de build
  • Séparation des préoccupations : Pas de contraintes architecturales imposées
  • Performance suffisante : Optimal pour un jeu 2D
  • Connaissance de l'équipe : Réduction de la courbe d'apprentissage

2. Justification du Choix de CPM (C++ Package Manager)

Vue d'Ensemble

Le projet R-Type utilise CPM (C++ Package Manager) pour la gestion des dépendances au lieu d'autres solutions de gestion de packages. Ce document explique les raisons architecturales et pratiques derrière ce choix.

Analyse des Exigences

Avant de sélectionner CPM, nous avons identifié les exigences suivantes pour le projet R-Type :

Exigences Fonctionnelles

  • Téléchargement automatique des dépendances (pas de configuration manuelle requise)
  • Épinglage des versions (builds reproductibles entre machines)
  • Support des bibliothèques header-only et compilées (flexibilité)
  • Intégration des dépôts Git (récupération de versions spécifiques)
  • Support des builds offline (cache local des dépendances)
  • Compatibilité multiplateforme (Linux, Windows, macOS)
  • Configuration minimale (intégration CMake simple)

Exigences Non-Fonctionnelles

  • Zéro outil externe (pas de gestionnaire de packages supplémentaire à installer)
  • Solution native CMake (exploite le système de build existant)
  • Résolution rapide des dépendances (temps de démarrage de build minimal)
  • Maintenance faible (facile de mettre à jour ou supprimer des dépendances)
  • Pas de surcharge du système de build (intégration légère)
  • Valeur éducative (compréhensible pour les membres de l'équipe)

Comparaison avec les Alternatives

CPM ✅ (Sélectionné)

Avantages :

  • Zéro dépendances externes : Pas de gestionnaire de packages séparé à installer (juste CMake)
  • Natif CMake : Intégré directement dans le pipeline de build
  • API simple : Un seul appel CMakeLists.txt par dépendance
  • Support Git : Intégration directe avec les dépôts Git (tags, branches, commits)
  • Téléchargement automatique : Récupère les dépendances au premier build
  • Cache local : Stocke les sources dans le dossier _deps (builds offline)
  • Contrôle de version : Épinglage de versions exactes pour la reproductibilité
  • Compatible header-only : Pas de surcharge de compilation pour les libs header-only
  • Compatible FetchContent : Utilise FetchContent natif de CMake
  • Maintenance active : Mises à jour et corrections de bugs régulières
  • Licence MIT : Licence commerciale-friendly
  • Courbe d'apprentissage minimale : Syntaxe facile pour ajouter des dépendances

Inconvénients :

  • Dépendances téléchargées dans le dossier build (pas à l'échelle système)
  • Chaque projet récupère les dépendances indépendamment (pas de cache global)
  • Nécessite CMake 3.14+ (mais standard aujourd'hui)

Pourquoi choisi pour R-Type :

  • Le workflow de développement de jeux nécessite une friction de configuration minimale
  • Pas de dépendances d'outils externes aligné avec l'environnement éducatif
  • Les dépendances basées sur Git permettent un suivi précis des versions
  • L'isolation du build assure la cohérence dans l'équipe
  • L'intégration CMake évite de basculer entre systèmes de build

Conan ❌ (Rejeté)

Raisons du rejet :

  • Installation d'outil séparé : Nécessite l'installation du gestionnaire de packages Conan
  • Courbe d'apprentissage abrupte : Fichiers de configuration complexes (conanfile.py, conan.lock)
  • Dépendance Python : Conan nécessite Python (ajoute une exigence système)
  • Overkill pour petits projets : Conçu pour des configurations d'entreprise
  • Friction de configuration : Outil supplémentaire à installer et configurer
  • Surcharge éducative : Trop complexe pour un projet orienté apprentissage

vcpkg ❌ (Rejeté)

Raisons du rejet :

  • Complexité toolchain : Nécessite installation et configuration de vcpkg
  • Cache à l'échelle système : Dépendances stockées dans un dossier système (pas isolé au projet)
  • Friction d'intégration : Nécessite configuration du fichier toolchain CMake
  • Variabilité des builds : Le cache système peut conduire à des builds inconsistants
  • Complexité de configuration supplémentaire non justifiée pour un projet éducatif

Git Submodules ❌ (Rejeté)

Raisons du rejet :

  • Gestion manuelle : Nécessite des commandes git submodule
  • Complexité de clone : git clone --recursive nécessaire pour la configuration complète
  • Conflits de merge : Les pointeurs de submodules entrent fréquemment en conflit
  • Intégration build : Pas d'intégration CMake automatique
  • Friction plus élevée pour les nouveaux membres de l'équipe
  • Les conflits de submodules pendant le développement actif

Hunter ❌ (Rejeté)

Raisons du rejet :

  • Déclin de maintenance : Développement moins actif
  • Configuration complexe : Configuration CMakeLists.txt verbeuse
  • Builds lents : Le cache binaire peut être obsolète
  • Communauté plus petite que Conan/vcpkg

Gestion Manuelle des Dépendances ❌ (Rejetée)

Raisons du rejet :

  • Cauchemar de maintenance : Téléchargements et mises à jour manuels
  • Charge de configuration : Chaque développeur clone manuellement les dépendances
  • Incohérence de versions : Facile de diverger entre machines
  • Problèmes de reproductibilité : Les builds varient entre membres de l'équipe
  • Complexité CI/CD : Scripting complexe pour la configuration des dépendances
  • Friction d'intégration : Nouveaux membres confus sur la configuration

Avantages Détaillés de CPM

1. Zéro Dépendances Externes

Contrairement à Conan/vcpkg, CPM nécessite seulement CMake (déjà présent) :

include(cmake/CPM.cmake)

CPMAddPackage("gh:gabime/spdlog@1.14.1")

Impact pour R-Type :

  • git clone && mkdir build && cd build && cmake .. && make
  • Workflow à commande unique pour nouveaux développeurs
  • Pas d'installation Conan/vcpkg/Hunter requise

2. Résolution Native Git des Dépendances

CPM utilise Git directement pour la spécification de version :

# Épingler une version exacte
CPMAddPackage("gh:gabime/spdlog@1.14.1")

# Ou utiliser des branches
CPMAddPackage("gh:gabime/spdlog#v1.x")

# Ou des commits spécifiques
CPMAddPackage("gh:gabime/spdlog#abc123def")

Avantages workflow :

  • Les développeurs comprennent mieux les références Git que le versioning des gestionnaires de packages
  • Historique de versions visible dans cmake/CPM.cmake
  • Facile de tester de nouvelles versions de bibliothèques

3. Isolation du Build

Dépendances mises en cache dans le dossier _deps (répertoire build) :

R-Type/
  build/
    _deps/
      spdlog/        # Téléchargé ici, pas à l'échelle système
      fmt/
      ...

Avantages :

  • Pas de pollution système
  • Différents projets peuvent utiliser différentes versions de bibliothèques
  • Un simple rm -rf build supprime toutes les dépendances
  • Builds offline possibles (cache préservé)

4. Intégration Native CMake

CPM étend le module FetchContent de CMake :

include(cmake/CPM.cmake)

CPMAddPackage(
    NAME spdlog
    GIT_REPOSITORY "https://github.com/gabime/spdlog.git"
    GIT_TAG "v1.14.1"
)

# Cibles immédiatement disponibles
target_link_libraries(R-Type PRIVATE spdlog::spdlog)

Pas de courbe d'apprentissage :

  • L'équipe connaît déjà CMake
  • Pas de nouveaux outils ou langages à apprendre
  • Le système de build unique reste CMake

5. Valeur Éducative

CPM démontre les bonnes pratiques :

# Déclaration claire des dépendances
# Épinglage de versions visible
# Basé sur Git, correspond au workflow de contrôle de version
# Pas de complexité de gestionnaire de packages

Parfait pour un contexte éducatif :

  • Les étudiants apprennent les systèmes de build CMake
  • Concepts de gestion des dépendances clairs
  • Pas de couches d'abstraction obscurcissant le processus

Conclusion

CPM a été sélectionné car il satisfait uniquement les exigences de R-Type :

  1. Friction de configuration zéro (juste CMake, rien d'autre à installer)
  2. Versioning natif Git (s'aligne avec le workflow développeur)
  3. Isolation du build (chaque projet indépendant)
  4. Intégration CMake (pas de nouveaux systèmes de build à apprendre)
  5. Builds reproductibles (épinglage de versions exactes)
  6. Valeur éducative (démontre clairement les meilleures pratiques)
  7. Maintenance minimale (CMakeLists.txt simple)
  8. Intégration rapide (nouveaux développeurs le comprennent immédiatement)

CPM fournit l'équilibre optimal entre simplicité, automation et contrôle pour la gestion des dépendances du moteur de jeu R-Type.


3. Justification du Choix de spdlog pour le Logging

Vue d'Ensemble

Le système de logging dans R-Type est construit sur spdlog au lieu d'autres bibliothèques de logging C++. Ce document explique les raisons architecturales et pratiques derrière ce choix.

Analyse des Exigences

Avant de sélectionner spdlog, nous avons identifié les exigences suivantes pour le projet R-Type :

Exigences Fonctionnelles

  • Niveaux de log multiples (Trace, Debug, Info, Warning, Error, Critical)
  • Sortie duale (console + fichier logging)
  • Suivi de localisation source (fichier, ligne, fonction)
  • Opération thread-safe (serveur de jeu multijoueur)
  • Rotation automatique des logs (prévenir les problèmes d'espace disque)
  • Flexibilité de format (timestamps, info thread, motifs personnalisés)
  • Sortie formatée (support de formatage C++ moderne)

Exigences Non-Fonctionnelles

  • Haute performance (surcharge minimale dans la boucle de jeu)
  • Option header-only (intégration facile)
  • Intégration facile (support gestionnaire de packages CPM)
  • Maintenance active (mises à jour de sécurité, corrections de bugs)
  • Dépendances minimales (garder le build simple)
  • Compatibilité licence MIT (commercial friendly)

Comparaison avec les Alternatives

spdlog ✅ (Sélectionné)

Avantages :

  • Performance extrême : Design de queue lock-free, option de logging asynchrone
  • Formatage riche : Formatage basé sur motifs avec placeholders style {fmt}
  • Sortie sélective : Multiples sinks (console, fichier, fichier rotatif)
  • Localisation source intégrée : Suivi automatique fichier/ligne/fonction
  • Thread-safe par défaut : Design interne lock-free pour haute concurrence
  • Variante header-only : Inclusion de header facile sans compilation
  • Rotation de logs : Rotation automatique de fichiers par taille et temps
  • Support couleur : Codes de couleur ANSI automatiques pour sortie console
  • Bien documenté : Documentation complète et exemples
  • Compatible CPM : Gestion de dépendances facile
  • Licence MIT : Licence commercial-friendly

Inconvénients :

  • Taille de fichier header importante (augmente le temps de compilation)
  • Les configurations multiples de sinks peuvent être complexes pour débutants

Pourquoi choisi pour R-Type :

  • La performance est critique dans le développement de jeux
  • La synchronisation réseau nécessite un logging thread-safe
  • L'architecture serveur-client bénéficie de multiples sinks de sortie
  • L'intégration facile via CPM s'aligne avec l'infrastructure du projet

Boost.Log ❌ (Rejeté)

Raisons du rejet :

  • Dépendance lourde : Bloat binaire massif (Boost ajoute 100+ MB)
  • Compilation lente : Surcharge significative du temps de compilation
  • Overkill pour les besoins : Trop complexe pour les besoins de logging du jeu
  • Pas CPM friendly : Pas bien supporté par le gestionnaire de packages CPM
  • API complexe : Courbe d'apprentissage abrupte pour tâches de logging standard

glog (Google Logging) ❌ (Rejeté)

Raisons du rejet :

  • Fonctionnalités limitées : Pas de support couleur intégré
  • Support Windows faible : Principalement conçu pour Linux/Unix
  • Options de formatage minimales : Personnalisation de motifs limitée
  • Exigences de headers : Configuration de headers complexe
  • Pas de formatage moderne : Manque de support formatage C++20

log4cxx ❌ (Rejeté)

Raisons du rejet :

  • Surcharge style Java : Conçu pour JVM, maladroit en C++
  • Dépendances externes : Nécessite APR (Apache Portable Runtime)
  • Support C++ moderne faible : N'exploite pas les fonctionnalités C++17/20
  • Plus lent : Performance non optimisée pour boucles de jeu
  • Déclin de maintenance : Développement moins actif

Plog ❌ (Rejeté)

Raisons du rejet :

  • Fonctionnalités limitées : Options de configuration minimales
  • Pas de logging async : Bloque le thread pendant I/O
  • Thread safety faible : Pas optimisé pour scénarios haute-concurrence
  • Pas de filtrage par catégorie : Ne peut pas désactiver sélectivement les catégories de logs
  • Documentation faible : Exemples et guides limités

Easylogging++ ❌ (Rejeté)

Raisons du rejet :

  • Performance faible : Performance non optimisée pour chemins critiques
  • Options async limitées : Bloque l'exécution pendant I/O
  • Pas de rotation intégrée : Gestion manuelle de fichiers requise
  • Préoccupations thread safety : Goulots d'étranglement de synchronisation
  • Maintenance peu claire : Planning de mise à jour inconsistant

Avantages Détaillés de spdlog

1. Performance

spdlog utilise un design de queue lock-free pour le logging asynchrone :

Thread de Jeu → Queue Lock-free → Thread Logger → Disque/Console
              Opération non-bloquante
              Impact minimal sur le temps de frame

Contexte benchmark :

  • Logging synchrone : ~100 nanosecondes par appel
  • Logging async : ~10 nanosecondes par appel
  • Bibliothèques alternatives : 1-5 microsecondes par appel

Pour un jeu à 60 FPS (budget de frame de 16.6ms), des milliers de logs avec spdlog ajoutent une surcharge négligeable.

2. Flexibilité Header-Only

spdlog offre deux options d'intégration :

  • Mode header-only : Inclure et compiler (pas de build séparé nécessaire)
  • Mode compilé : Bibliothèque précompilée (compilation plus rapide dans grands projets)

R-Type utilise header-only avec CPM pour la simplicité.

3. Alignement de Licence

spdlog utilise Licence MIT, qui :

  • ✅ Permet l'utilisation commerciale (important pour la distribution de jeux)
  • ✅ Permet les modifications
  • ✅ Pas de restrictions copyleft

4. Intégration Écosystème

spdlog est le choix standard dans les moteurs de jeu modernes :

  • Unreal Engine : Utilise des patterns de logging similaires
  • Godot : Utilise une architecture type spdlog
  • Communauté game dev : Standard de facto pour le logging de jeux C++

Décisions de Design Basées sur spdlog

1. Filtrage par Catégorie (Amélioration Wrapper R-Type)

L'implémentation du Logger R-Type ajoute filtrage par catégorie au-dessus de spdlog :

// Le wrapper R-Type ajoute le filtrage par catégorie - PAS une fonctionnalité spdlog
Logger::setup(LogLevel::Info, "game.log", {"network", "physics"});

Comment ça marche :

  • Les catégories sont stockées dans un ensemble de filtres dans le wrapper R-Type
  • Quand LOG_*_CAT(category, message) est appelé, le wrapper vérifie si la catégorie est dans la liste d'exclusion
  • Si exclue, le log est filtré avant d'atteindre spdlog
  • C'est une décision de design par R-Type pour fournir un contrôle fin sur le bruit de logging

Pourquoi c'est important :

  • spdlog fournit la fondation (formatage, sortie, performance)
  • R-Type ajoute organisation sémantique (catégories comme couche conceptuelle)
  • Cette séparation permet flexibilité et API propre

2. Option de Logging Async

Bien qu'implémentée dans le wrapper, spdlog fournit une queue async native :

auto async_file = std::make_shared<spdlog::sinks::async_file_sink_mt>(...);

Permet aux sections critiques en performance d'utiliser le logging async sans changements de framework.

3. Singleton Thread-Safe

Le spdlog::details::atomic de spdlog fournit un pattern singleton lock-free :

// getInstance() est lock-free grâce à l'implémentation atomique de spdlog
Logger& Logger::getInstance() { ... }

Conclusion

spdlog a été sélectionné car il satisfait uniquement les exigences de R-Type :

  1. Design performance-first (critique pour le développement de jeux)
  2. Complet en fonctionnalités (toutes les capacités nécessaires présentes)
  3. Intégration facile (header-only + support CPM)
  4. Thread-safe par défaut (besoins serveur multijoueur)
  5. Maintenance active (mises à jour sécurité et fonctionnalités)
  6. Empreinte minimale (pas de dépendances inutiles)

Alternatives Rejetées Car

  • Boost.Log : Trop lourd, trop lent à compiler, complexité inutile
  • glog : Fonctionnalités critiques manquantes (couleur, filtrage), limitations de plateforme
  • log4cxx : Préoccupations de performance, surcharge style Java, déclin de maintenance
  • Plog : Préoccupations thread safety, pas d'async, ensemble de fonctionnalités minimal
  • Easylogging++ : Problèmes de performance, pas de rotation auto, maintenance peu claire

spdlog fournit l'équilibre optimal entre puissance, simplicité et performance pour les exigences de logging du moteur de jeu R-Type.


4. Justification du Choix de Google Test pour les Tests ECS

Framework de Test Recommandé : Google Test (gtest)

Pour tester l'ECS, Google Test est le choix le plus approprié pour les raisons suivantes :

Standard de l'Industrie et Maturité

Google Test est le standard pour les tests unitaires C++ dans les environnements professionnels. Il a été testé au combat à travers d'innombrables projets, de petites bibliothèques à des systèmes à grande échelle comme Chromium et Android. Cette maturité signifie moins de bugs dans le framework de test lui-même et un meilleur support à long terme.

Syntaxe Claire et Expressive

Le framework fournit des macros d'assertion intuitives qui rendent les tests lisibles et maintenables :

EXPECT_EQ(engine.getEntityName(e), "Player");
EXPECT_TRUE(engine.isAlive(e));
ASSERT_TRUE(component.has_value());

La distinction entre EXPECT_* (continue en cas d'échec) et ASSERT_* (arrête en cas d'échec) permet un contrôle fin sur le flux d'exécution des tests, ce qui est crucial lors du test d'un état ECS complexe.

Fixtures de Test pour une Configuration Propre

Les tests ECS nécessitent des séquences d'initialisation répétées. Le système de fixtures de Google Test élimine la duplication de code :

class ECSTest : public ::testing::Test {
protected:
    void SetUp() override {
        engine.init();
        engine.registerComponent<Position>();
        engine.registerComponent<Velocity>();
    }

    GameEngine engine;
};

Chaque test obtient un moteur frais, correctement initialisé sans code de configuration manuel.

Ensemble de Fonctionnalités Riche

  • Tests paramétrés : Tester la même logique avec différents types de composants
  • Tests de mort : Vérifier que les opérations invalides déclenchent les assertions attendues
  • Découverte de tests : Trouve et exécute automatiquement tous les tests
  • Messages d'échec détaillés : Fournit le contexte lorsque les assertions échouent
  • Filtrage de tests : Exécuter des sous-ensembles spécifiques de tests pendant le développement

Excellente Intégration CMake

Google Test s'intègre parfaitement avec CMake, ce qui est essentiel pour gérer les projets C++ :

find_package(GTest REQUIRED)
target_link_libraries(ecs_tests GTest::GTest GTest::Main)
gtest_discover_tests(ecs_tests)

Cette intégration permet la découverte automatique des tests et la compatibilité CTest.

Pourquoi Pas les Alternatives ?

Catch2 : Bien que moderne et header-only, il a des temps de compilation plus lents en raison d'une utilisation intensive des templates. Pour un ECS avec de nombreux types de composants, cette surcharge de compilation s'accumule rapidement.

Doctest : Léger et rapide, mais manque de certaines fonctionnalités avancées comme les tests paramétrés et a un écosystème plus petit d'outils et d'intégrations IDE.

Criterion : Conçu pour C plutôt que C++, ce qui le rend maladroit pour tester du code lourd en templates comme ComponentManager ou EntityManager.

Boost.Test : Dépendance lourde qui nécessite tout l'écosystème Boost, ce qui est excessif pour des besoins de test.

Support Communauté et Outillage

Google Test a un support tiers étendu :

  • Intégration native dans CLion, Visual Studio et VS Code
  • Support des plateformes d'intégration continue (GitHub Actions, GitLab CI, Jenkins)
  • Compatibilité avec les outils de couverture de code (gcov, lcov)
  • Grande communauté signifie que les questions trouvent facilement des réponses

Conclusion

Pour tester une architecture ECS avec du code lourd en templates, une gestion d'état complexe et de multiples composants interagissants, Google Test fournit le meilleur équilibre de puissance, clarté et support écosystème. Sa maturité et son adoption généralisée en font le choix sûr et professionnel pour assurer la qualité du code.


5. Justification du Choix d'UDP pour le Protocole Réseau

Contexte

Le protocole réseau R-Type est un protocole binaire conçu pour la communication multijoueur en temps réel. Le choix du protocole de transport est crucial pour les performances du jeu.

Pourquoi UDP ?

UDP (User Datagram Protocol) a été choisi comme couche de transport pour les raisons suivantes :

1. Faible Latence

UDP est un protocole sans connexion qui ne nécessite pas d'établissement de connexion ni d'accusés de réception pour chaque paquet. Cela réduit considérablement la latence, ce qui est critique pour un jeu d'action en temps réel comme R-Type.

2. Pas de Head-of-Line Blocking

Avec TCP, si un paquet est perdu, tous les paquets suivants sont bloqués jusqu'à ce que le paquet perdu soit retransmis. Avec UDP, chaque paquet est indépendant, donc la perte d'un paquet n'affecte pas la livraison des paquets suivants. Pour un jeu, il est souvent préférable d'utiliser des données plus récentes plutôt que d'attendre des données anciennes.

3. Contrôle du Protocole

UDP étant non fiable par défaut, nous avons un contrôle total sur les mécanismes de fiabilité. Nous pouvons implémenter :

  • Fiabilité sélective : Rendre certains paquets fiables (connexion, déconnexion) tout en gardant d'autres non fiables (mises à jour de position)
  • Priorisation : Traiter les paquets critiques en priorité
  • Gestion personnalisée de la perte : Implémenter des stratégies adaptées au jeu

4. Efficacité de Bande Passante

UDP a une surcharge minimale :

  • En-tête UDP : 8 octets
  • En-tête TCP : 20+ octets + contrôle de flux + réordonnancement

Pour un jeu qui envoie de nombreuses petites mises à jour d'état, cette différence est significative.

5. Adapté aux Snapshots d'État

Le modèle de synchronisation basé sur des snapshots d'état du monde est naturellement adapté à UDP :

  • Chaque snapshot est une image complète de l'état à un instant T
  • Si un snapshot est perdu, le suivant (plus récent) est souvent plus utile
  • Pas besoin de garantir la livraison de tous les snapshots

Mécanismes de Fiabilité sur UDP

Bien qu'UDP soit non fiable par défaut, le protocole R-Type implémente une couche de fiabilité optionnelle pour les paquets critiques :

enum class PacketFlags : uint8_t {
    FLAG_RELIABLE    = 0x01,  // Nécessite un accusé de réception
    FLAG_COMPRESSED  = 0x02,  // Payload compressé
    FLAG_ENCRYPTED   = 0x04,  // Payload chiffré
    FLAG_FRAGMENTED  = 0x08,  // Partie d'un message fragmenté
    FLAG_PRIORITY    = 0x10,  // Traitement haute priorité
};

Les paquets marqués avec FLAG_RELIABLE sont :

  • Suivis avec un numéro de séquence
  • Réémis s'ils ne sont pas acquittés
  • Ordonnés à la réception

Cela permet un meilleur contrôle : fiabilité où nécessaire, vitesse où critique.

Cas d'Usage TCP Tolérés

Le projet tolère l'utilisation de TCP pour des cas spécifiques avec une forte justification :

  • Authentification initiale : Connexion sécurisée au serveur
  • Transfert de fichiers : Téléchargement de ressources volumineuses
  • Chat textuel : Messages qui doivent être livrés dans l'ordre

Cependant, pour le gameplay en temps réel, UDP est obligatoire.

Conformité avec les Exigences

Les attentes du projet (docs/Contributing/Expectations/expectations.md) stipulent :

  • ✅ UDP obligatoire pour la communication en temps réel (p.11)
  • ✅ TCP toléré pour des cas spécifiques avec forte justification (p.11)
  • ✅ Mécanismes de fiabilité sur UDP acceptés

Conclusion

UDP a été choisi pour le protocole R-Type car :

  1. Latence minimale (critique pour le gameplay temps réel)
  2. Pas de blocage head-of-line (paquets indépendants)
  3. Contrôle total du protocole (fiabilité sélective)
  4. Efficacité de bande passante (surcharge minimale)
  5. Adapté aux snapshots (état plus récent plus important)
  6. Conformité projet (exigence UDP pour temps réel)

Le protocole implémente une couche de fiabilité optionnelle au-dessus d'UDP, combinant les avantages de vitesse d'UDP avec des garanties de livraison quand nécessaire.


6. Justification du Choix d'ASIO pour la Gestion Réseau

Contexte

Le projet R-Type nécessite une bibliothèque réseau robuste et performante pour gérer les communications UDP entre clients et serveur. Le choix de la bibliothèque réseau est crucial pour la fiabilité et les performances du système.

Pourquoi ASIO ?

ASIO (Asynchronous I/O) a été choisi comme bibliothèque réseau pour les raisons suivantes :

1. API Moderne et Asynchrone

ASIO fournit une API asynchrone moderne basée sur des callbacks et des handlers :

socket.async_receive_from(
    asio::buffer(buffer),
    remote_endpoint,
    [this](std::error_code ec, std::size_t bytes_received) {
        // Handler appelé quand des données arrivent
    }
);

Cela permet :

  • Non-blocking I/O : Le thread principal n'est pas bloqué par les opérations réseau
  • Haute concurrence : Gestion efficace de multiples connexions simultanées
  • Scalabilité : Adapté pour un serveur gérant plusieurs clients

2. Abstraction Multiplateforme

ASIO fournit une abstraction unifiée sur différentes API système :

  • Linux : epoll
  • Windows : IOCP (I/O Completion Ports)
  • macOS : kqueue

Cela permet :

  • Code portable : Même API sur toutes les plateformes
  • Performances optimales : Utilise les primitives natives de chaque OS
  • Conformité exigences : Support Linux/Windows requis

3. Support UDP Complet

ASIO offre un support complet pour UDP :

asio::ip::udp::socket socket(io_context);
socket.open(asio::ip::udp::v4());
socket.bind(asio::ip::udp::endpoint(asio::ip::udp::v4(), port));

// Opérations non-bloquantes
socket.non_blocking(true);

// Gestion des buffers
socket.send_to(asio::buffer(data), remote_endpoint);
socket.receive_from(asio::buffer(buffer), sender_endpoint);

Fonctionnalités clés :

  • Sockets UDP : Création et gestion simplifiées
  • Endpoints : Résolution d'adresses et de ports
  • Buffers : Gestion efficace de la mémoire
  • Mode non-bloquant : Essentiel pour le temps réel

4. Gestion d'Erreurs Robuste

ASIO utilise un système de gestion d'erreurs basé sur std::error_code :

std::error_code ec;
size_t bytes_sent = socket.send_to(asio::buffer(data), endpoint, 0, ec);

if (ec) {
    // Gestion d'erreur sans exception
    setError(ec.message());
}

Avantages :

  • Pas d'exceptions : Performance prévisible dans les chemins critiques
  • Codes d'erreur détaillés : Diagnostics précis des problèmes réseau
  • Contrôle explicite : Le code décide comment gérer chaque erreur

5. Header-Only ou Compilé

ASIO peut être utilisé de deux façons :

  • Header-only : #define ASIO_STANDALONE + inclure headers
  • Compilé : Lier contre une bibliothèque précompilée

R-Type utilise le mode header-only pour :

  • Intégration simple : Pas de build séparé nécessaire
  • Portabilité : Pas de bibliothèques dynamiques à distribuer
  • Compatible CPM : S'intègre facilement avec notre système de dépendances

6. Active et Bien Maintenue

ASIO est :

  • Mature : Développée depuis plus de 15 ans
  • Activement maintenue : Mises à jour régulières
  • Standard : Base pour std::networking (futur C++ standard)
  • Bien documentée : Documentation complète et exemples nombreux

7. Conformité avec les Exigences

Les attentes du projet (docs/Contributing/Expectations/expectations.md) stipulent :

  • ASIO autorisé OU API bas niveau (BSD Sockets, Windows Sockets) (p.9)
  • ✅ Si sockets bas niveau : encapsulation avec abstractions obligatoire (p.9)
  • ✅ "you MUST encapsulate them with proper abstractions"

ASIO répond parfaitement à ces exigences en fournissant une abstraction propre au-dessus des sockets système.

Architecture d'Encapsulation

Le projet R-Type encapsule ASIO dans une hiérarchie de classes :

ISocket (Interface)
    
ASocket (Classe abstraite)
    
AsioSocket (Implémentation concrète avec ASIO)

Cette architecture permet :

  • Abstraction : Le code client ne dépend pas directement d'ASIO
  • Testabilité : Possibilité de créer des mocks pour les tests
  • Flexibilité : Possibilité de changer d'implémentation réseau si nécessaire
  • Conformité : Respect de l'exigence d'encapsulation avec abstractions propres

Pourquoi Pas les Alternatives ?

BSD Sockets / Windows Sockets (Bas Niveau) ❌

Raisons du rejet :

  • API différentes : BSD (Linux) vs Winsock (Windows) nécessitent du code conditionnel
  • Synchrone par défaut : Nécessite threading complexe pour I/O non-bloquant
  • Gestion d'erreurs inconsistante : errno (Linux) vs WSAGetLastError() (Windows)
  • Pas d'abstractions : Nécessite de construire toute la couche d'abstraction soi-même
  • Maintenance élevée : Beaucoup de code boilerplate à maintenir

Bien qu'autorisées par le projet, les sockets bas niveau nécessiteraient une quantité significative de code d'abstraction personnalisé, ce qu'ASIO fournit déjà de manière mature et testée.

Boost.Asio ❌

Raisons du rejet :

  • Dépendance Boost : Nécessite toute la bibliothèque Boost (lourd)
  • Surcharge de build : Temps de compilation et taille binaire augmentés
  • Overkill : ASIO standalone suffit pour nos besoins

Note : ASIO standalone est la même bibliothèque que Boost.Asio, mais sans dépendance Boost.

libuv ❌

Raisons du rejet :

  • API C : Bibliothèque C, moins idiomatique en C++ moderne
  • Orientée événements : Modèle plus complexe qu'ASIO pour UDP simple
  • Moins adapté UDP : Principalement conçu pour TCP et opérations filesystem

SDL_net ❌

Raisons du rejet :

  • API limitée : Fonctionnalités réseau basiques seulement
  • Pas d'I/O asynchrone : Opérations bloquantes
  • Orienté jeux simples : Pas assez robuste pour serveur autoritaire

SFML Network ❌

Raisons du rejet :

  • API simplifiée : Moins de contrôle sur les opérations bas niveau
  • Pas d'asynchrone natif : Nécessite threading manuel
  • Moins performant : Surcouche au-dessus des sockets système

Conclusion

ASIO a été choisi pour la gestion réseau R-Type car :

  1. API asynchrone moderne (non-blocking I/O, haute concurrence)
  2. Abstraction multiplateforme (Linux/Windows avec performances natives)
  3. Support UDP complet (toutes les fonctionnalités nécessaires)
  4. Gestion d'erreurs robuste (std::error_code, pas d'exceptions)
  5. Header-only (intégration simple, portable)
  6. Mature et maintenue (base pour futur std::networking)
  7. Conformité exigences (autorisé explicitement par le projet)
  8. Encapsulation propre (architecture ISocket/ASocket/AsioSocket)

ASIO fournit l'équilibre optimal entre performance, portabilité et facilité d'utilisation pour les besoins réseau du moteur de jeu R-Type.


7. Justification de l'Architecture ECS (Entity Component System)

Contexte

Le projet R-Type utilise une architecture ECS (Entity Component System) pour organiser la logique du jeu. Ce choix architectural est fondamental et influence toute la conception du moteur.

Qu'est-ce que l'ECS ?

L'ECS est un pattern architectural qui sépare :

  • Entities (Entités) : Identifiants uniques pour les objets du jeu
  • Components (Composants) : Données pures (position, vélocité, sprite, etc.)
  • Systems (Systèmes) : Logique qui opère sur des ensembles de composants

Pourquoi ECS ?

1. Composition au lieu d'Héritage

L'ECS favorise la composition plutôt que l'héritage :

Approche traditionnelle (héritage) :

class GameObject {};
class MovableObject : public GameObject {};
class RenderableObject : public GameObject {};
class Enemy : public MovableObject, public RenderableObject {}; // Diamond problem!

Approche ECS (composition) :

Entity enemy = engine.createEntity();
engine.addComponent<Position>(enemy, {x, y});
engine.addComponent<Velocity>(enemy, {vx, vy});
engine.addComponent<Sprite>(enemy, "enemy.png");

Avantages :

  • Flexibilité : Ajouter/retirer des comportements dynamiquement
  • Pas de hiérarchie rigide : Évite les problèmes d'héritage multiple
  • Réutilisabilité : Les composants sont réutilisables entre entités

2. Séparation Données / Logique

L'ECS sépare clairement :

  • Composants = Données (Position, Health, Sprite)
  • Systèmes = Logique (MovementSystem, RenderSystem, CollisionSystem)

Avantages :

  • Testabilité : Tester la logique indépendamment des données
  • Maintenabilité : Modifications localisées dans des systèmes spécifiques
  • Clarté : Responsabilités clairement définies

3. Performance - Cache Locality

L'ECS stocke les composants du même type de manière contiguë en mémoire :

// Tous les Position ensemble en mémoire
std::vector<Position> positions;

// Tous les Velocity ensemble en mémoire
std::vector<Velocity> velocities;

Avantages :

  • Cache-friendly : Accès séquentiel en mémoire
  • Performance : Moins de cache misses
  • Vectorisation : Possibilité d'optimisations SIMD

4. Scalabilité

L'ECS permet de gérer efficacement un grand nombre d'entités :

// MovementSystem traite toutes les entités avec Position + Velocity
MovementSystem::update() {
    for (auto entity : entitiesWithPositionAndVelocity) {
        auto& pos = getComponent<Position>(entity);
        auto& vel = getComponent<Velocity>(entity);
        pos.x += vel.x * dt;
        pos.y += vel.y * dt;
    }
}

Avantages :

  • Efficacité : Traitement par batch d'entités similaires
  • Parallélisation : Systèmes indépendants peuvent tourner en parallèle
  • Scalable : Gère centaines/milliers d'entités facilement

5. Adapté au Game Design

L'ECS facilite les itérations de game design :

Exemple : Créer un nouveau type d'ennemi

// Boss = Enemy + Shield + SpecialAttack
Entity boss = engine.createEntity();
engine.addComponent<Position>(boss, {x, y});
engine.addComponent<Enemy>(boss, {health: 1000});
engine.addComponent<Shield>(boss, {duration: 5.0});
engine.addComponent<SpecialAttack>(boss, {cooldown: 10.0});

Pas besoin de créer une nouvelle classe, juste composer des composants existants.

Avantages :

  • Itération rapide : Tester de nouvelles combinaisons rapidement
  • Pas de recompilation : Composition à l'exécution
  • Flexibilité créative : Game designers peuvent expérimenter

6. Synchronisation Réseau

L'ECS est particulièrement adapté à la synchronisation réseau :

// Sérialiser seulement les composants nécessaires
Packet packet;
packet.addComponent<Position>(entity);
packet.addComponent<Velocity>(entity);
// Pas besoin de sérialiser Sprite (client-only)

Avantages :

  • Snapshots efficaces : Sérialiser seulement composants modifiés
  • Delta compression : Envoyer seulement les changements
  • Filtrage : Client/Serveur peuvent avoir composants différents

7. Modularité et Extensibilité

L'ECS permet d'ajouter de nouvelles fonctionnalités facilement :

Ajouter un nouveau système :

class ParticleSystem : public ISystem {
    void update(float dt) override {
        // Logique des particules
    }
};

engine.registerSystem<ParticleSystem>();

Ajouter un nouveau composant :

struct ParticleEmitter {
    int particlesPerSecond;
    float lifetime;
};

engine.registerComponent<ParticleEmitter>();

Avantages :

  • Extensions non invasives : Pas de modification du code existant
  • Plugins : Possibilité de charger systèmes/composants dynamiquement
  • Maintenance : Chaque système est indépendant

Conformité avec les Exigences

Les attentes du projet (docs/Contributing/Expectations/expectations.md) stipulent :

  • Architecture ECS recommandée (p.8)
  • Moteur générique et réutilisable (p.8)
  • Séparation claire des préoccupations (p.8)
  • Testabilité (p.10)

L'architecture ECS répond parfaitement à ces exigences.

Architecture R-Type ECS

Le projet R-Type implémente l'ECS avec :

Coordinator (Point d'entrée)
    ├─ EntityManager (Gestion des entités)
    ├─ ComponentManager (Gestion des composants)
    └─ SystemManager (Gestion des systèmes)

Systèmes implémentés :

  • MovementSystem : Mise à jour des positions
  • CollisionSystem : Détection et résolution des collisions
  • RenderSystem : Affichage graphique
  • AISystem : Intelligence artificielle des ennemis
  • ShootSystem : Gestion des tirs
  • AnimationSystem : Animations des sprites
  • BackgroundSystem : Scrolling du fond
  • LevelSystem : Gestion des niveaux
  • PlayerSystem : Logique spécifique au joueur

Composants implémentés :

  • Position, Velocity, Sprite, Health, Collider, Enemy, Player, Bullet, etc.

Pourquoi Pas les Alternatives ?

Hiérarchie d'Objets (GameObject) ❌

Raisons du rejet :

  • Rigidité : Hiérarchie fixée à la compilation
  • Diamond problem : Problèmes d'héritage multiple
  • Couplage : Données et logique mélangées
  • Difficile à tester : Dépendances complexes

Object-Oriented pur ❌

Raisons du rejet :

  • Explosion de classes : Trop de classes pour toutes les combinaisons
  • Duplication de code : Logique répétée entre classes
  • Performance : Mauvaise cache locality
  • Maintenance : Changements propagent dans toute la hiérarchie

Component-Based (Unity-style) ❌

Raisons du rejet partiel :

  • Moins efficace : Composants contiennent logique (moins cache-friendly)
  • Couplage composants : Composants se référencent mutuellement
  • Moins parallélisable : Difficile de paralléliser les updates

Note : L'ECS est une évolution du Component-Based, gardant les avantages de composition tout en améliorant la performance et la séparation.

Conclusion

L'architecture ECS a été choisie pour R-Type car :

  1. Composition flexible (évite problèmes d'héritage)
  2. Séparation données/logique (maintenabilité)
  3. Performance (cache locality, parallélisation)
  4. Scalabilité (gère grand nombre d'entités)
  5. Adapté game design (itérations rapides)
  6. Synchronisation réseau (snapshots efficaces)
  7. Modularité (extensions non invasives)
  8. Conformité exigences (recommandé par le projet)
  9. Standard industrie (utilisé dans moteurs professionnels)
  10. Testabilité (systèmes et composants testables indépendamment)

L'ECS fournit l'équilibre optimal entre performance, flexibilité et maintenabilité pour l'architecture du moteur de jeu R-Type.


Synthèse

L'ensemble des choix technologiques et architecturaux du projet R-Type sont guidés par les principes suivants :

  1. Simplicité et Maintenabilité : Choix d'outils matures, bien documentés et faciles à utiliser (SFML, CPM, spdlog, Google Test, ASIO)

  2. Performance : Optimisations critiques pour le temps réel (UDP, ECS, spdlog asynchrone)

  3. Portabilité : Support Linux/Windows natif (SFML, ASIO, CMake, CPM)

  4. Valeur Éducative : Solutions compréhensibles et standards de l'industrie (Google Test, CMake, Git-based dependencies)

  5. Extensibilité : Architecture modulaire permettant d'ajouter fonctionnalités facilement (ECS, abstraction réseau)

  6. Conformité aux Exigences : Respect strict des spécifications du projet (UDP obligatoire, ASIO autorisé, ECS recommandé)

Ces choix forment un ensemble cohérent qui permet au projet R-Type d'atteindre ses objectifs tout en maintenant une base de code propre, performante et maintenable.


Dernière mise à jour : Janvier 2026
Version du document : 1.0