Compétence 5 : Composants Logiciels¶
Traduire les spécifications techniques et fonctionnelles en un système cohérent de composants logiciels en mobilisant son expertise et en s'appuyant sur l'état de l'art en termes d'architecture logicielle afin de produire une solution technique adaptée au besoin du client.
Observable 5.1 : Implémentation de l'Architecture¶
Vue d'Ensemble des Composants¶
Le projet R-Type se compose de composants logiciels distincts, chacun avec une responsabilité claire.
graph TB
subgraph "Client"
SCENES["Scenes<br/>(Game, Login, Menu)"]
UI["UI Components<br/>(Button, TextInput)"]
NET_C["Network<br/>(UDPClient, TCPClient)"]
GFX["Graphics<br/>(IWindow, Plugins)"]
AUDIO["Audio<br/>(VoiceChat, Opus)"]
ACCESS["Accessibility<br/>(Colorblind, Keys)"]
end
subgraph "Serveur"
ADAPTERS_IN["Adapters IN<br/>(TCP, UDP, Voice)"]
USECASES["Use Cases<br/>(Login, Register)"]
DOMAIN["Domain<br/>(Entities, Services)"]
ADAPTERS_OUT["Adapters OUT<br/>(MongoDB, Logger)"]
GAME["Game Engine<br/>(GameWorld)"]
end
subgraph "Common"
PROTO["Protocol<br/>(Serialization)"]
COMPRESS["Compression<br/>(LZ4)"]
COLLISION["Collision<br/>(AABB)"]
end
SCENES --> UI
SCENES --> NET_C
SCENES --> GFX
SCENES --> AUDIO
SCENES --> ACCESS
ADAPTERS_IN --> USECASES
USECASES --> DOMAIN
USECASES --> ADAPTERS_OUT
ADAPTERS_IN --> GAME
NET_C --> PROTO
ADAPTERS_IN --> PROTO
NET_C --> COMPRESS
ADAPTERS_IN --> COMPRESS
GAME --> COLLISION
Composant 1 : Système de Scènes (Client)¶
Architecture¶
classDiagram
class IScene {
<<interface>>
+handleEvent(Event)
+update(float deltaTime)
+render()
#SceneManager* _sceneManager
#GameContext _context
}
class SceneManager {
-stack~IScene~ _sceneStack
+changeScene(IScene)
+pushScene(IScene)
+popScene()
+handleEvent(Event)
+update(float)
+render()
}
class GameScene {
-renderPlayers()
-renderMissiles()
-renderEnemies()
-renderHUD()
}
class LoginScene {
-TextInput _usernameInput
-TextInput _passwordInput
-attemptLogin()
}
class LobbyScene {
-vector~PlayerSlot~ _slots
-toggleReady()
-startGame()
}
IScene <|-- GameScene
IScene <|-- LoginScene
IScene <|-- LobbyScene
SceneManager o-- IScene
Implémentation¶
Interface IScene (src/client/include/scenes/IScene.hpp:20-42) :
struct GameContext {
std::shared_ptr<graphics::IWindow> window;
std::shared_ptr<network::UDPClient> udpClient;
std::shared_ptr<network::TCPClient> tcpClient;
std::string sessionToken;
};
class IScene {
public:
virtual void handleEvent(const events::Event& event) = 0;
virtual void update(float deltatime) = 0;
virtual void render() = 0;
void setSceneManager(SceneManager* manager) { _sceneManager = manager; }
void setContext(const GameContext& ctx) { _context = ctx; }
protected:
SceneManager* _sceneManager = nullptr;
GameContext _context;
};
SceneManager avec Stack (src/client/include/scenes/SceneManager.hpp:17-72) :
class SceneManager {
public:
void changeScene(std::unique_ptr<IScene> newScene); // Remplace toute la pile
void pushScene(std::unique_ptr<IScene> scene); // Overlay (pause)
void popScene(); // Ferme overlay
void handleEvent(const events::Event& event) {
if (!_sceneStack.empty()) {
_sceneStack.top()->handleEvent(event);
}
}
private:
std::stack<std::unique_ptr<IScene>> _sceneStack;
enum class PendingAction { None, Change, Push, Pop };
PendingAction _pendingAction = PendingAction::None;
};
Composant 2 : Protocole Réseau (Common)¶
Structure des Messages¶
graph LR
subgraph "Message Structure"
H["UDPHeader<br/>12 bytes"]
P["Payload<br/>Variable"]
end
subgraph "UDPHeader"
T["type: u16"]
S["sequence: u16"]
TS["timestamp: u64"]
end
H --> T
H --> S
H --> TS
UDPHeader (src/common/protocol/Protocol.hpp:1536-1583) :
struct UDPHeader {
uint16_t type;
uint16_t sequence_num;
uint64_t timestamp;
static constexpr size_t WIRE_SIZE = 12;
void to_bytes(void* buf) const {
auto* ptr = static_cast<uint8_t*>(buf);
uint16_t net_type = swap16(type);
uint16_t net_seq = swap16(sequence_num);
uint64_t net_ts = swap64(timestamp);
std::memcpy(ptr, &net_type, 2);
std::memcpy(ptr + 2, &net_seq, 2);
std::memcpy(ptr + 4, &net_ts, 8);
}
static std::optional<UDPHeader> from_bytes(const void* buf, size_t len) {
if (buf == nullptr || len < WIRE_SIZE) return std::nullopt;
// ... parsing avec swap pour endianness
}
};
Types de Messages (40+ types) :
enum class MessageType : uint16_t {
// Core
HeartBeat = 0x0001,
// Game (0x004x-0x00Cx)
Snapshot = 0x0040,
PlayerInput = 0x0061,
ShootMissile = 0x0080,
MissileSpawned = 0x0081,
EnemyDestroyed = 0x0091,
BossSpawn = 0x00C0,
// Social (0x050x-0x069x)
GetLeaderboard = 0x0500,
SendFriendRequest = 0x0600,
SendPrivateMessage = 0x0690,
// ...
};
Composant 3 : GameWorld (Serveur)¶
Responsabilités¶
Le composant GameWorld gère l'état complet d'une partie.
classDiagram
class GameWorld {
-unordered_map players
-unordered_map missiles
-unordered_map enemies
-optional~Boss~ boss
-uint16_t waveNumber
+addPlayer(uint8_t id)
+removePlayer(uint8_t id)
+spawnMissile(uint8_t playerId)
+update(float deltaTime)
+checkCollisions()
+getSnapshot() GameSnapshot
}
class PlayerScore {
+uint32_t score
+uint16_t kills
+float comboMultiplier
+float comboTimer
}
class Missile {
+uint16_t id
+uint8_t owner_id
+float x, y
+WeaponType weaponType
+uint8_t weaponLevel
}
class Enemy {
+uint16_t id
+EnemyType type
+float x, y
+uint8_t health
}
GameWorld *-- PlayerScore
GameWorld *-- Missile
GameWorld *-- Enemy
Implémentation (src/server/include/infrastructure/game/GameWorld.hpp) :
class GameWorld {
private:
std::unordered_map<uint8_t, ConnectedPlayer> _players;
std::unordered_map<uint16_t, Missile> _missiles;
std::unordered_map<uint16_t, Enemy> _enemies;
std::unordered_map<uint8_t, PlayerScore> _playerScores;
std::optional<Boss> _boss;
uint16_t _waveNumber = 0;
std::vector<uint16_t> _destroyedMissiles; // Events par frame
std::vector<uint16_t> _destroyedEnemies;
public:
void addPlayer(uint8_t id);
void removePlayer(uint8_t id);
void updatePlayerPosition(uint8_t id, uint16_t x, uint16_t y);
void spawnMissile(uint8_t playerId, WeaponType type, uint8_t level);
void update(float deltaTime);
void checkCollisions();
protocol::GameSnapshot getSnapshot() const; // Pour broadcast 20Hz
};
Composant 4 : Système UI (Client)¶
Hiérarchie des Composants UI¶
classDiagram
class IUIElement {
<<interface>>
+getPos() Vec2f
+setPos(Vec2f)
+getSize() Vec2f
+contains(float x, float y) bool
+handleEvent(Event)
+update(float dt)
+render(IWindow)
+isFocused() bool
#bool _visible
#bool _enabled
}
class Button {
-string _text
-State _state
-ClickCallback _onClick
+setOnClick(callback)
+setText(string)
}
class TextInput {
-string _text
-string _placeholder
-size_t _cursorPos
-bool _isPassword
+getText() string
+setOnSubmit(callback)
}
IUIElement <|-- Button
IUIElement <|-- TextInput
IUIElement (src/client/include/ui/IUIElement.hpp:20-52) :
class IUIElement {
public:
virtual Vec2f getPos() const = 0;
virtual void setPos(const Vec2f& pos) = 0;
virtual Vec2f getSize() const = 0;
virtual bool contains(float x, float y) const {
Vec2f pos = getPos();
Vec2f size = getSize();
return x >= pos.x && x <= pos.x + size.x &&
y >= pos.y && y <= pos.y + size.y;
}
virtual void handleEvent(const events::Event& event) = 0;
virtual void update(float deltaTime) = 0;
virtual void render(graphics::IWindow& window) = 0;
protected:
bool _visible = true;
bool _enabled = true;
};
Button avec États (src/client/include/ui/Button.hpp:20-78) :
class Button : public IUIElement {
public:
enum class State { Normal, Hovered, Pressed, Disabled };
using ClickCallback = std::function<void()>;
void setOnClick(ClickCallback callback) { _onClick = std::move(callback); }
void handleEvent(const events::Event& event) override {
if (auto* pressed = std::get_if<events::MouseButtonPressed>(&event)) {
if (contains(pressed->x, pressed->y)) {
_state = State::Pressed;
if (_onClick) _onClick();
}
}
}
private:
State _state = State::Normal;
ClickCallback _onClick;
rgba _normalColor{60, 60, 80, 255};
rgba _hoveredColor{80, 80, 110, 255};
};
Observable 5.2 : Bonnes Pratiques de Développement¶
Design Patterns Utilisés¶
Le projet mobilise 10 design patterns reconnus de l'état de l'art.
1. Hexagonal Architecture (Ports & Adapters)¶
Localisation : Structure globale du serveur
// Port (abstraction)
class IUserRepository {
virtual void save(const User& user) = 0;
};
// Adapter (implémentation)
class MongoDBUserRepository : public IUserRepository {
void save(const User& user) override { /* MongoDB */ }
};
2. Dependency Injection¶
Localisation : Register.hpp:33-36
explicit Register(
std::shared_ptr<IUserRepository> userRepository, // Injecté
std::shared_ptr<IIdGenerator> idGenerator, // Injecté
std::shared_ptr<ILogger> logger // Injecté
);
3. Repository Pattern¶
Localisation : application/ports/out/persistence/
// Interface
class IUserRepository {
virtual std::optional<User> findById(const std::string& id) = 0;
virtual std::optional<User> findByEmail(const std::string& email) = 0;
};
4. Singleton Pattern¶
Localisation : Logger.hpp:26-40
class Logger {
public:
static void init();
static std::shared_ptr<spdlog::logger> getMainLogger();
static std::shared_ptr<spdlog::logger> getNetworkLogger();
private:
static std::shared_ptr<spdlog::logger> s_mainLogger;
};
5. Strategy Pattern (Armes)¶
Localisation : GameWorld.hpp:104-234
struct Missile {
WeaponType weaponType; // Strategy selector
static uint8_t getBaseDamage(WeaponType type) {
switch (type) {
case WeaponType::Spread: return 8;
case WeaponType::Laser: return 12;
case WeaponType::Missile: return 50;
default: return 20;
}
}
};
| Arme | Dégâts | Vitesse | Comportement |
|---|---|---|---|
| Standard | 20 | 600 | Ligne droite |
| Spread | 8×3 | 550 | 3 projectiles en éventail |
| Laser | 12 | 900 | Rapide, fin |
| Missile | 50 | 350 | Guidé (homing) |
6. Observer Pattern (Signal)¶
Localisation : src/client/include/events/Signal.hpp
template<typename ...Args>
class Signal {
public:
using Slot = std::function<void(Args...)>;
void connect(const Slot& slot) {
slots_.push_back(slot);
}
void emit(Args... args) {
for (const auto& slot : slots_) {
slot(args...);
}
}
private:
std::vector<Slot> slots_;
};
7. State Pattern (Room)¶
Localisation : Room.hpp:44-49
enum class State {
Waiting, // Attente de joueurs
Starting, // Compte à rebours
InGame, // Partie en cours
Closed // Fermée
};
8. Template Method (Scenes)¶
Localisation : IScene.hpp:27-42
class IScene {
public:
virtual void handleEvent(const events::Event& event) = 0; // Hook
virtual void update(float deltatime) = 0; // Hook
virtual void render() = 0; // Hook
};
9. Factory Pattern (Graphics Plugin)¶
Localisation : IGraphicPlugin.hpp
class IGraphicPlugin {
virtual std::shared_ptr<IWindow> createWindow(Vec2u size, const std::string& name) = 0;
};
// Usage
auto plugin = loadPlugin("sfml.so");
auto window = plugin->createWindow({1920, 1080}, "R-Type");
10. Chain of Responsibility (Scene Manager)¶
Localisation : SceneManager.hpp:17-72
void SceneManager::handleEvent(const events::Event& event) {
if (!_sceneStack.empty()) {
_sceneStack.top()->handleEvent(event); // Passe à la scène courante
}
}
Clean Code : Conventions Respectées¶
Nommage¶
| Type | Convention | Exemple |
|---|---|---|
| Classes | PascalCase | GameScene, UDPClient |
| Interfaces | I + PascalCase | IWindow, IScene |
| Méthodes | camelCase | handleEvent(), getSnapshot() |
| Membres privés | _prefix | _position, _health |
| Constantes | SCREAMING_SNAKE | MAX_PLAYERS, MOVE_SPEED |
| Namespaces | lowercase | domain::entities, events |
Exemple de Code Propre¶
Fichier : src/client/src/ui/Button.cpp:13-22
Button::Button(const Vec2f& pos, const Vec2f& size,
const std::string& text, const std::string& fontKey)
: _pos(pos) // Initialiseur aligné
, _size(size) // Un par ligne
, _text(text) // Virgule en début
, _fontKey(fontKey)
, _state(State::Normal)
, _focused(false)
{
}
Constantes Nommées¶
Fichier : GameScene.hpp:121-174
static constexpr float MOVE_SPEED = 200.0f;
static constexpr float SHIP_WIDTH = 64.0f;
static constexpr float SHIP_HEIGHT = 30.0f;
static constexpr float HUD_HEALTH_BAR_WIDTH = 200.0f;
static constexpr uint8_t MAX_HEALTH = 100;
static constexpr float SHOOT_COOLDOWN_TIME = 0.3f;
static constexpr float SCREEN_WIDTH = 1920.0f;
static constexpr float SCREEN_HEIGHT = 1080.0f;
Tableau Récapitulatif des Patterns¶
| Pattern | Localisation | Bénéfice |
|---|---|---|
| Hexagonal | Serveur global | Découplage domaine/infra |
| Dependency Injection | UseCases | Testabilité |
| Repository | Persistence | Abstraction DB |
| Singleton | Logger | Instance unique |
| Strategy | Weapons | Comportements interchangeables |
| Observer | Events | Découplage émetteur/récepteur |
| State | Room | Transitions explicites |
| Template Method | Scenes | Structure commune |
| Factory | Graphics | Création abstraite |
| Chain of Resp. | SceneManager | Propagation événements |
Conclusion¶
L'implémentation de R-Type respecte l'architecture hexagonale présentée et mobilise les bonnes pratiques :
- 10 design patterns de l'état de l'art
- Conventions de code cohérentes (nommage, formatage)
- Constantes nommées pour la maintenabilité
- Séparation des responsabilités claire entre composants
Cette approche produit une solution technique adaptée aux besoins (jeu multijoueur temps réel) tout en garantissant maintenabilité et évolutivité.