Compétence 3 : Solution Technique Créative¶
Présenter une solution technique créative, en collaboration avec l'équipe projet et ses différentes expertises, en prenant en considération les différentes contraintes apportées par le client (économique, RSE, ...) ou imposées par l'environnement technique dans le but de résoudre la problématique exposée.
Observable 3.1 : Travaux de Prototypage¶
Contexte et Contraintes du Projet¶
Le projet R-Type devait répondre à plusieurs contraintes techniques et fonctionnelles :
| Contrainte | Description | Impact |
|---|---|---|
| Multijoueur temps réel | 4 joueurs simultanés, 20 Hz refresh | Latence < 50ms |
| Cross-platform | Linux, Windows | Abstraction graphique |
| Performances réseau | Bande passante limitée (mobile) | Compression nécessaire |
| Accessibilité | Support daltonisme | Shaders post-process |
| Maintenabilité | Projet long terme | Architecture propre |
| Temps de développement | Délais étudiants | Réutilisation maximale |
Prototype 1 : Architecture Multi-Backend Graphique¶
Problématique¶
Comment supporter plusieurs bibliothèques graphiques (SFML, SDL2, futures) sans dupliquer le code métier ?
Solution Prototypée¶
graph TB
subgraph "Prototype Initial"
V1["Version 1<br/>Couplage direct SFML"]
V1 -->|"Problème"| P1["Code dupliqué<br/>si SDL2 ajouté"]
end
subgraph "Prototype Final"
IF["IWindow<br/>(Interface)"]
IF --> SFML["SFMLWindow"]
IF --> SDL2["SDL2Window"]
IF --> Future["FutureBackend..."]
end
V1 -->|"Évolution"| IF
Implémentation¶
Interface abstraite (src/client/include/graphics/IWindow.hpp:20-62) :
class IWindow {
public:
virtual ~IWindow() = default;
// Gestion fenêtre
virtual Vec2u getSize() const = 0;
virtual bool isOpen() = 0;
virtual void close() = 0;
// Événements
virtual events::Event pollEvent() = 0;
// Rendu
virtual void drawRect(float x, float y, float w, float h, rgba color) = 0;
virtual void drawSprite(const std::string& key, float x, float y, float w, float h) = 0;
virtual void drawText(const std::string& fontKey, const std::string& text,
float x, float y, unsigned int size, rgba color) = 0;
// Ressources
virtual bool loadTexture(const std::string& key, const std::string& filepath) = 0;
virtual bool loadFont(const std::string& key, const std::string& filepath) = 0;
// Shaders (optionnel - SFML only)
virtual bool supportsShaders() const = 0;
virtual bool loadShader(const std::string& key, const std::string& vertPath,
const std::string& fragPath) = 0;
virtual void setPostProcessShader(const std::string& key) = 0;
// Fullscreen
virtual void setFullscreen(bool enabled) = 0;
virtual void toggleFullscreen() = 0;
};
Plugin dynamique (src/client/include/graphics/IGraphicPlugin.hpp) :
class IGraphicPlugin {
public:
virtual const char* getName() const = 0;
virtual std::shared_ptr<IWindow> createWindow(Vec2u winSize,
const std::string& name) = 0;
};
// Chargement runtime via dlopen()
typedef IGraphicPlugin* (*create_t)();
typedef void (*destroy_t)(IGraphicPlugin*);
Résultat¶
- 100% du code client utilise
IWindow, jamais SFML/SDL2 directement - Changement de backend = 1 ligne de configuration
- Ajout futur (Raylib, Vulkan) = implémenter
IWindow
Prototype 2 : Compression Réseau LZ4¶
Problématique¶
Les GameSnapshot (800-2000 bytes) envoyés à 20 Hz consomment 16-40 KB/s par joueur. Comment réduire la bande passante ?
Solutions Évaluées¶
| Solution | Ratio | Vitesse | Complexité |
|---|---|---|---|
| Pas de compression | 1:1 | Max | Aucune |
| zlib/gzip | ~70% | Lent | Moyenne |
| LZ4 | ~60% | Très rapide | Faible |
| Delta encoding | Variable | Rapide | Haute |
Solution Prototypée : LZ4¶
Fichier : src/common/compression/Compression.hpp
namespace compression {
inline std::vector<uint8_t> compress(const uint8_t* src, size_t srcSize) {
if (srcSize == 0 || src == nullptr) return {};
int maxDstSize = LZ4_compressBound(static_cast<int>(srcSize));
std::vector<uint8_t> compressed(maxDstSize);
int compressedSize = LZ4_compress_default(
reinterpret_cast<const char*>(src),
reinterpret_cast<char*>(compressed.data()),
static_cast<int>(srcSize),
maxDstSize
);
// Compression uniquement si gain réel
if (static_cast<size_t>(compressedSize) >= srcSize) {
return {}; // Pas rentable
}
compressed.resize(compressedSize);
return compressed;
}
} // namespace compression
Protocole avec flag :
// Si bit 15 set -> message compressé
static constexpr uint16_t COMPRESSION_FLAG = 0x8000;
// Format: [UDPHeader][CompressionHeader][LZ4 data]
struct CompressionHeader {
uint16_t originalSize; // Taille décompressée
};
Résultats Mesurés¶
| Métrique | Sans LZ4 | Avec LZ4 | Gain |
|---|---|---|---|
| Snapshot moyen | 1000 B | 450 B | -55% |
| Bande passante/joueur | 20 KB/s | 9 KB/s | -55% |
| Latence ajoutée | 0 | ~0.1ms | Négligeable |
Prototype 3 : Protocole Binaire Custom¶
Problématique¶
JSON/Protobuf ajoutent overhead et dépendances. Comment avoir un protocole léger, performant et maintenable ?
Solution Prototypée¶
Sérialisation manuelle big-endian avec pattern to_bytes()/from_bytes() :
struct PlayerState {
uint8_t id;
uint16_t x, y;
uint8_t health, alive;
uint16_t lastAckedInputSeq;
uint8_t shipSkin;
uint32_t score;
// ... 23 bytes total
static constexpr size_t WIRE_SIZE = 23;
void to_bytes(uint8_t* buf) const {
buf[0] = id;
uint16_t net_x = swap16(x); // Host -> Network byte order
std::memcpy(buf + 1, &net_x, 2);
// ...
}
static std::optional<PlayerState> from_bytes(const void* buf, size_t len) {
if (len < WIRE_SIZE) return std::nullopt; // Validation
// Parse avec swap16/32 pour endianness
}
};
Avantages du Prototype¶
| Aspect | JSON | Protobuf | Custom Binary |
|---|---|---|---|
| Taille message | ~500 B | ~100 B | 23 B |
| Parsing | Lent | Moyen | O(1) memcpy |
| Dépendances | nlohmann/json | protoc + runtime | Aucune |
| Endianness | N/A | Géré | Explicite (swap) |
Prototype 4 : Système de Combo avec Grace Period¶
Problématique¶
Comment encourager le skill sans punir les débutants ? Un combo qui reset immédiatement est frustrant.
Solutions Évaluées¶
| Approche | Comportement | Feeling |
|---|---|---|
| Reset immédiat | 1 kill = +combo, timeout = reset 1.0x | Punitif |
| Decay linéaire | -X/seconde constant | Prévisible mais fade |
| Grace period + decay | 3s stable puis decay progressif | Équilibré |
Solution Prototypée¶
Fichier : src/server/infrastructure/game/GameWorld.cpp:1754-1772
static constexpr float COMBO_GRACE_TIME = 3.0f; // 3 secondes sans decay
static constexpr float COMBO_DECAY_RATE = 0.5f; // -0.5x par seconde après
static constexpr float COMBO_MAX = 3.0f; // Maximum 3.0x
void GameWorld::updateComboTimers(float deltaTime) {
for (auto& [playerId, score] : _playerScores) {
score.comboTimer += deltaTime;
// Decay seulement après grace period
if (score.comboTimer > COMBO_GRACE_TIME &&
score.comboMultiplier > 1.0f) {
float decay = COMBO_DECAY_RATE * deltaTime;
score.comboMultiplier = std::max(1.0f, score.comboMultiplier - decay);
}
}
}
void GameWorld::onPlayerDamaged(uint8_t playerId) {
auto it = _playerScores.find(playerId);
if (it != _playerScores.end()) {
it->second.comboMultiplier = 1.0f; // Reset immédiat sur dégâts
}
}
Flux du Système¶
sequenceDiagram
participant Player
participant Combo
Player->>Combo: Kill enemy
Combo->>Combo: combo += 0.1x
Combo->>Combo: timer = 0
Note over Combo: Grace period (3s)
Player->>Combo: Kill enemy
Combo->>Combo: combo += 0.1x
Combo->>Combo: timer = 0
Note over Combo: 4 seconds pass...
Combo->>Combo: decay starts (-0.5x/s)
Player->>Combo: Takes damage
Combo->>Combo: combo = 1.0x (reset)
Prototype 5 : Voice Chat avec Opus¶
Problématique¶
Comment implémenter du voice chat de qualité avec une latence acceptable pour le gaming ?
Technologies Évaluées¶
| Codec | Bitrate | Latence | Qualité |
|---|---|---|---|
| Opus | 6-128 kbps | 2.5-60ms | Excellent |
| Speex | 2-44 kbps | 30ms+ | Bon |
| G.711 | 64 kbps | <1ms | Téléphone |
Solution Prototypée¶
Architecture :
Client Server (VoiceUDPServer)
│ │
│─────► OpusCodec.encode() ─────────►│
│ (PCM -> Opus frame) │───► Relay to room
│ │
│◄───── OpusCodec.decode() ◄────────│
│ (Opus frame -> PCM) │
Fichiers clés :
- src/client/src/audio/VoiceChatManager.cpp : Capture/playback PortAudio
- src/client/src/audio/OpusCodec.cpp : Encodage/décodage
- src/server/infrastructure/adapters/in/network/VoiceUDPServer.cpp : Relay
Configuration Opus :
// Optimisé pour voix en temps réel
opus_encoder_ctl(encoder, OPUS_SET_APPLICATION(OPUS_APPLICATION_VOIP));
opus_encoder_ctl(encoder, OPUS_SET_BITRATE(24000)); // 24 kbps
opus_encoder_ctl(encoder, OPUS_SET_COMPLEXITY(5)); // Équilibre CPU/qualité
Prototype 6 : Shaders d'Accessibilité¶
Problématique¶
8% des hommes sont daltoniens. Comment leur permettre de jouer confortablement ?
Solution Prototypée¶
Shaders GLSL pour simulation daltonisme :
// assets/shaders/colorblind.frag
uniform int u_mode; // 0=None, 1=Protanopia, 2=Deuteranopia, 3=Tritanopia
vec3 applyProtanopia(vec3 color) {
// Matrice de transformation pour absence de cônes rouges
return vec3(
0.567 * color.r + 0.433 * color.g,
0.558 * color.r + 0.442 * color.g,
0.242 * color.r + 0.758 * color.b
);
}
Gestion (src/client/include/accessibility/ColorblindShaderManager.hpp) :
class ColorblindShaderManager {
bool initialize(std::shared_ptr<graphics::IWindow> window);
void updateFromConfig(); // Lit AccessibilityConfig::getColorBlindMode()
bool isAvailable() const; // false si backend SDL2 (pas de shaders)
};
Observable 3.2 : Comparatif des Prototypes¶
Les prototypes présentés résolvent chacun une problématique spécifique. Cette section argumente leurs avantages et inconvénients.
Comparatif Synthétique¶
graph LR
subgraph "Évaluation Prototypes"
P1["Multi-Backend"]
P2["LZ4 Compression"]
P3["Binary Protocol"]
P4["Combo System"]
P5["Voice Chat"]
P6["Colorblind Shaders"]
end
P1 --> |"Flexibilité ++"| OK1["Adopté"]
P2 --> |"Perf ++"| OK2["Adopté"]
P3 --> |"Légèreté ++"| OK3["Adopté"]
P4 --> |"UX ++"| OK4["Adopté"]
P5 --> |"Social ++"| OK5["Adopté"]
P6 --> |"Accessibilité ++"| OK6["Adopté"]
Prototype 1 : Multi-Backend¶
| Avantages | Inconvénients |
|---|---|
| Changement de backend transparent | Complexité d'abstraction initiale |
| Testabilité (mock IWindow) | Certaines features SFML-only (shaders) |
| Évolutivité (futurs backends) | Overhead minimal mais présent |
| Découplage code métier/rendu | Maintenance de 2 implémentations |
Verdict : Les avantages de maintenabilité et portabilité surpassent le coût de développement initial. Le pattern Plugin permet même le chargement dynamique.
Prototype 2 : Compression LZ4¶
| Avantages | Inconvénients |
|---|---|
| -55% bande passante | CPU côté serveur (+~2%) |
| Latence négligeable (~0.1ms) | Complexité protocole (flag compression) |
| Dépendance unique légère (liblz4) | Pas de gain sur petits messages (<128B) |
| Fallback automatique si non rentable | - |
Verdict : Le gain de bande passante justifie largement l'overhead CPU minimal. Essentiel pour le multijoueur mobile.
Prototype 3 : Protocole Binaire¶
| Avantages | Inconvénients |
|---|---|
| Aucune dépendance (0 libs parsing) | Évolution manuelle (ajouter champs) |
| Taille minimale (23B vs 500B JSON) | Pas de schéma auto-documenté |
| Parsing O(1) | Débogage plus difficile (pas human-readable) |
| Contrôle total endianness | - |
Verdict : Pour un protocole temps réel à 20 Hz, la performance prime. La documentation Protocol.hpp compense l'absence de schéma.
Prototype 4 : Système de Combo¶
| Avantages | Inconvénients |
|---|---|
| Feedback positif (grace period) | Plus complexe qu'un reset simple |
| Skill ceiling élevé (3.0x max) | Équilibrage nécessaire |
| Pénalité sur dégâts (stratégie) | - |
Verdict : Meilleure expérience joueur que les alternatives testées. Le grace period de 3s est le sweet spot identifié.
Prototype 5 : Voice Chat Opus¶
| Avantages | Inconvénients |
|---|---|
| Qualité audio excellente | Dépendances (Opus, PortAudio) |
| Latence faible (20-40ms) | CPU encoding/decoding |
| Standard industrie | Configuration audio complexe (Linux) |
Verdict : Opus est le codec optimal pour le gaming. Les scripts run-client.sh gèrent les subtilités PipeWire/PulseAudio.
Prototype 6 : Shaders Accessibilité¶
| Avantages | Inconvénients |
|---|---|
| Support 3 types daltonisme | SFML-only (pas SDL2) |
| Post-processing transparent | Overhead GPU (~1%) |
| Différenciation compétitive | - |
Verdict : Fonctionnalité inclusive importante. Le fallback SDL2 est acceptable (pas de filtre mais jouable).
Matrice de Décision Finale¶
| Prototype | Valeur Ajoutée | Coût Implémentation | ROI | Décision |
|---|---|---|---|---|
| Multi-Backend | Haute | Moyen | Positif | Adopté |
| LZ4 | Haute | Faible | Très positif | Adopté |
| Binary Protocol | Haute | Moyen | Positif | Adopté |
| Combo System | Moyenne | Faible | Positif | Adopté |
| Voice Chat | Moyenne | Élevé | Neutre | Adopté |
| Colorblind | Moyenne | Faible | Positif | Adopté |
Conclusion¶
Tous les prototypes ont été adoptés car ils répondent chacun à une contrainte identifiée :
- Multi-Backend : Portabilité et maintenabilité
- LZ4 : Performance réseau
- Binary Protocol : Efficacité temps réel
- Combo : Expérience utilisateur
- Voice Chat : Fonctionnalité sociale
- Colorblind : Accessibilité et conformité
L'approche itérative par prototypage a permis de valider chaque solution avant intégration, réduisant les risques techniques et assurant la cohérence de l'architecture globale.