# Rapport 📝
###### tags: `projet-p2`, `INF2dlm-a`, `2020-2021`, `Qt`

# 1. Table des matières
[TOC]
# 2. Introduction
Ce document présente le projet réalisé durant le cours de P2 dont l'objectif est de concevoir, réaliser et développer un programme à l'aide du framework Qt. Lors de l'introduction de ce cours, l'aspect graphique englobant des images et des animations a été relevé, ce qui nous a tournés vers la réalisation d'un jeu vidéo. Le choix du projet était totalement libre.
## 2.1. Déroulement du projet
Ce projet a démarré le mardi 6 octobre 2020 et s'est terminé le lundi 25 janvier 2021. Il s'est déroulé sur un total de 180 heures, réparties entre les 3 membres du projet avec un minimum d'une heure de travail hors-cours attendues par semaine par personne.
Les quatre leçons de cours par semaine le mardi nous servaient à nous réunir et à mettre au point l'avancement du projet. Comme c'est la première fois que nous utilisons le framework Qt dans un projet, le déroulement a démarré avec une phase de recherche en créant notamment plusieurs petits projets afin de comprendre au mieux les outils à notre disposition avant d'entamer le projet principal.
## 2.2. Inspiration
L'idée du jeu est inspirée d'un autre jeu vidéo, réalisé par Google pour leur Doodle de la journée d'Halloween du 31 octobre 2018.
Dans ce jeu, les joueurs incarnent des fantômes, séparés en deux équipes et devant ramasser des flammes sur un terrain de jeu. Les flammes ramassées forment une sorte de serpent derrière le personnage et doivent être ramenées à sa base.
Il est possible de voler les flammes de son adversaire en coupant le serpent de flammes formées derrière celui-ci. Lorsque les flammes sont ramenées à la base, elles sont validées et ne peuvent plus être volées.
Ci-dessous, vous trouverez une capture d'écran de l'écran de jeu afin d'en avoir une meilleure vision :

Référence : [google.com/doodles](https://www.google.com/doodles/halloween-2018)
## 2.3. Nouveautés apportées
Notre implémentation se base sur une autre fête : celle de Saint-Nicolas, qui a lieu le 6 décembre.
Dans notre jeu, les joueurs incarnent des écoliers, également séparés en 2 équipes. Une équipe en rouge est avec le Saint-Nicolas, l'autre en noir est avec le Père Fouettard.
Le terrain de jeu comporte le bâtiment de l'école ainsi que sa cour de récréation enneigée où sont dispersées des cacahuètes et des mandarines que les joueurs doivent aller ramasser.
Lorsqu'une friandise a été récoltée, elle forme un serpent derrière le joueur avec les autres friandises qu'il a ramassé. Ainsi, chaque joueur doit ramener le plus de friandises à sa base en un temps imparti.
Afin de remporter la partie, il est possible de voler les friandises de ses adversaires avant qu'elles soient ramenées dans leur base. Pour ce faire, il faut couper la queue de cacahuètes et mandarines des joueurs de l'équipe adverse.
Le but du jeu est donc d'être l'équipe ayant ramené le plus de friandises dans son camp à la fin du temps imparti.

Le jeu est jouable de deux manières :
- À 2 joueurs sur le même ordinateur : le clavier est partagé et l'écran est scindé en deux pour afficher la vue de chaque joueur
- En réseau contre d'autres personnes jouant à partir d'autres ordinateurs
Pour l'implémentation, le framework Qt possède plusieurs librairies graphiques mais c'est le Graphics View Framework qui est utilisé dans ce projet. Ce framework donne une surface pour gérer et interagir avec un grand nombre d'éléments graphiques 2D personnalisés et un widget de visualisation des éléments, le tout supportant des transformations graphiques comme le zoom, la rotation, etc...
# 3. Analyse
Comme cité ci-dessus, le projet a été coder en utilisant le langage C++ avec le Framework `Qt`. Nous avons utilisé le Framework `Graphics View` qui offre plusieurs fonctionnalités (la vue `QGraphicsView`, la scène `QGraphicsScene`). La musique a été ajouté grâce au module "multimedia" de Qt. C'est un module qui fournit plusieurs classe C++ pour gérer le contenu audio. La création du terrain de jeu s'est faite avec `Tiled`.
## 3.1. Fonctionnalités
Le projet contient plusieurs buts de réalisation, du plus basique au plus complexe. Chaque but représente les fonctionnalités minimales à implémenter afin d'avoir un projet cohérent et complet lors de son rendu.
Pour commencer, nous avons rédigé un [cahier des charges](https://hackmd.io/T08GcHDBQFOnEoYFFdqtbQ?view#Fonctionnalit%C3%A9s-d%C3%A9taill%C3%A9es) détaillé ainsi qu'une liste de [points chauds](https://hackmd.io/PjrcFIZhSKSGncB2QK7EsQ), afin d'avoir une vue d'ensemble du projet que nous allons réaliser et identifier les points complexes qui prendront plus de temps à développer.
### 3.1.1. Objectifs primaires
- Fonctionnalités
- [x] Déplacements personnages
- [x] Tracé de bonbons derrière le personnage
- [x] Voler le serpent de l'adversaire
- [x] Multijoueur sur la même machine
- [x] Musique de fond
- Type de parties
- [x] Joueur vs Joueur (1vs1)
### 3.1.2. Objectifs secondaires
- Fonctionnalités
- [ ]
: facile et difficile
- [x] Bonus répartis sur le terrain
- [ ] Malus répartis sur le terrain
- [x] Multijoueur en réseau LAN
- Type de parties
- [x] Joueur vs Joueur (1vs1 en réseau)
### 3.1.3. Objectifs tertiaires (bonus)
- Fonctionnalités
- [x] Multijoueur en ligne (via Internet)
- Types de parties
- [x] Joueur vs Joueur (jusqu'à 4vs4)
- [ ] Joueur vs IA (1vs1)
- [ ] Rééquilibrage du jeu
### 3.1.4. Points chauds
Ce sous-chapitre présente les points que nous estimons les plus difficiles à réaliser.
- Gérer le tracé se formant derrière le personnage lors du ramassage des friandises
- Gérer le vol des friandises en coupant le tracé d'un adversaire
- Afficher le jeu (gestion avec QGraphicsView et QGraphicsScene)
- Gérer le multijoueur local
- Faire un split-screen
En effet, n'ayant que peu d'expérience avec le framework Qt, nous devrons effectuer une phase de recherches en créant plusieurs petits projets afin de comprendre au mieux les outils à notre disposition avant d'entamer le projet principal.
## 3.2. Planning
```mermaid
gantt
title Gantt
dateFormat MM-DD-YY
section Analyse
Cahier des charges (2h) -- KB, VP, YM : a1, 10-06-20, 7d %%Kevin
Maquettes (1h) -- KB, VP, YM : a1, 10-06-20, 7d %%Kevin
Rédaction des tâches (2h) -- KB :a1, 10-13-20, 7d %%Kevin
Création du Gantt (2h) -- KB, YM :a1, 10-13-20, 7d %%Kevin
Fonctionnalités détaillés (1h) -- KB, VP, YM :a1, 10-13-20, 7d %%Kevin
section Conception
Prise en main des déplacements simultanément (5h) -- YM :a1, 10-20-20, 14d
Prise en main du snake derrière le personnage (7h) -- VP :a1, 10-20-20, 14d
Prise en main du vol du snake (5h) -- VP :a1, 11-03-20, 14d
Ajouter une musique de fond et bruitages (2h) -- KB :a1, 11-03-20, 14d
++ Multijoueur en ligne (10h) -- KB, VP, YM :a1, 01-12-21, 7d
title Gantt projet P2
section Implémentation
Déplacements (0.5h) -- YM :a1, 11-17-20, 7d
Snake (2h) -- VP :a1, 11-17-20, 14d
Voler snake (3h) -- VP :a1, 11-24-20, 14d
Multijoueur sur une machine (2h) -- YM :a1, 11-24-20, 14d
Musique de fond et bruitages (1h) -- KB :a1, 12-01-20, 14d
++Bonus (2h) -- KB, VP, YM :a1, 12-29-20, 7d
++Malus (2h) -- KB, VP, YM :a1, 12-29-20, 7d
++Multijoueur en ligne (10h) -- KB, VP, YM :a1, 01-05-21, 14d
++Niveau de difficulté (5h) -- KB, VP, YM :a1, 01-05-21, 7d
++Joueur vs IA (20h) -- KB, VP, YM :a1, 12-29-20, 21d
section Documentation
Rapport -- KB, VP, YM :a1, 10-06-20, 112d
Journal de travail -- KB, VP, YM :a1, 10-06-20, 112d
Planning -- KB, VP, YM :a1, 10-06-20, 112d
Documentation d'utilisation -- KB :a1, 01-19-21, 7d
section Tests
Déplacements (2h) -- KB, VP, YM :a1, 11-17-20, 7d
Snake (2h) -- KB, VP, YM :a1, 11-17-20, 14d
Vol bonbons en coupant snake (3h) -- KB, VP, YM :a1, 11-24-20, 14d
Multijoueur (2h) -- KB, VP, YM :a1, 11-24-20, 14d
++Bonus (1h) -- KB, VP, YM :a1, 12-29-20, 7d
++Malus (1h) -- KB, VP, YM :a1, 12-29-20, 7d
++Multijoueur en ligne (4h) -- KB, VP, YM :a1, 01-05-21, 14d
++Niveau de difficulté (3h) -- KB, VP, YM :a1, 01-05-21, 7d
++Joueur vs IA (10h) -- KB, VP, YM :a1, 12-29-20, 21d
```
## 3.3. Scénarios d’utilisations
### 3.3.1. Même ordinateur
1. Les deux joueurs souhaitent démarrer une nouvelle partie sur le même ordinateur. Pour ce faire, ils vont lancer l'exécutable et appuyer sur le bouton "Démarrer le jeu en local à 2 joueurs".

2. La partie va ensuite débuter avec l'écran divisé en deux pour que chaque joueur ait son côté et puisse se déplacer sur le terrain.

3. Les deux joueurs commencent en même temps et ramassent les bonbons sur le terrain. Dans ce scénario, le joueur 2 va ramener plus de friandises à sa base que le joueur 1. Nous pouvons voir le score dans la barre en haut de l'écran.

4. Le timer se termine, ce qui annonce la fin de la partie. Une fenêtre de fin s'affiche et annonce le joueur 2 comme vainqueur et demande s'ils veulent revenir à la page d'accueil.

### 3.3.2. Plusieurs ordinateurs
1. L'un des joueurs doit lancer l'exécutable afin de démarrer le serveur.
 
2. Les joueurs vont lancer l'exécutable du jeu, entrer l'adresse IP ainsi que le port du serveur, puis appuyer sur le bouton "Se connecter à un serveur".

3. Chaque joueur peut entrer son pseudo puis appuyer sur le bouton "Prêt".

4. Lorsque tous les joueurs sont prêts, la partie va démarrer et ils devront ramener le maximum de friandises à leur base respective. Nous pouvons voir le pseudo des autres joueurs au-dessus de leur personnage durant la partie.
5. 
5. Le timer se termine, ce qui annonce la fin de la partie. Une fenêtre de fin s'affiche afin d'annoncer l'équipe gagnante et demander si les joueurs souhaitent revenir à la page d'accueil.

## 3.4. Maquette et final
### 3.4.1. Accueil
Dans un premier temps, nous avons réalisé une maquette de notre menu principal :

Voici notre menu principal final :

Le bouton "Démarrer le jeu en local à 2 joueurs" permet, comme son nom l'indique, de lancer une partie en local avec les deux joueurs jouant sur le même clavier.
Le bouton "Se connecter à un serveur" permet de jouer en multijoueur, en spécifiant l'adresse du serveur ainsi que le port.
Le bouton "Quitter le jeu" quittera le jeu.
### 3.4.2. Terrain
Nous avons réalisé plusieurs ébauches de ce à quoi pourrait ressembler le jeu au début du projet.
La première ébauche a été réaliser avec paint.net

La seconde ébauche a également été réaliser avec paint.net

La troisième ébauche a été réaliser sur Tiled.

Au final, nous avons décidé de créer plusieurs terrains de jeu à l'aide du logiciel Tiled. Voici l'aperçu de l'un d'eux.

### 3.4.3. Fin de partie
Ecran de victoire de l'équipe rouge :

Ecran de victoire de l'équipe noire :

Lorsque le temps est écoulé, un des menus ci-dessus apparait afin d'indiquer qui a gagné la partie et permet aux joueurs de revenir au menu principal.
### 3.4.4. Page d'attente du multijoueur
Lorsque le serveur sera démarré et que les joueurs s'y seront connectés, chacun pourra rentrer son pseudo.
Après validation du pseudo, chaque joueur va arriver sur la page d'attente puis appuyera sur le bouton "Prêt", lorsqu'il sera prêt à démarrer la partie.
Lorsque tous les joueurs seront prêts, la partie va démarrer.

## 3.5. Diagrammes de classe
Sur les diagrammes de classe présents dans ce chapitre, un code couleur est présent afin d'indiquer la nature des fonctions présentent dans chaque classe :
- <span style="color:blue">Bleu : la fonction est un slot</span>
- <span style="color:green">Vert : la fonction est un signal</span>
Les signaux et slots sont des outils directement fournis par Qt qui permettent la communication entre différents objets. Comme il n'est pas optimal de montrer le diagramme de classe avec les connexions de tous les slots et signaux, ils ont été simplement mis en couleur pour être facilement reconnaissables.
### 3.5.1. Client
Voici le diagramme de classe du projet :
| |
|-|
Ce diagramme de classe est conséquent, ce qui le rend difficile à lire et à analyser en détail directement dans ce document.
Pour en simplifier la lecture et les explications, un autre diagramme non-UML gardant les connexions les plus simples possible et omettant les structures et les énumérations du projet a été conçu :

<!-- Lien exciladraw : https://excalidraw.com/#json=5663787323490304,Ng_DBQIlAEIqUY3Okb-T9A
(faire attention à regénérer le lien à chaque modification) -->
Dans le schéma ci-dessus, chaque case représente une classe du projet, chaque couleur indique de quelle classe de Qt cette classe du projet hérite, et les symboles aux bouts des liens indiquent les connexions entre les classes.
<div style="display: flex; flex-wrap: wrap;">
<div style="margin-right: 2rem">
| Couleur | Classe dérivée |
| ------- | -------------- |
|  | `QStackedWidget` |
|  | `QWidget` |
|  | `QObject` |
|  | `QGraphicsView` |
|  | `QGraphicsScene` |
|  | `QGraphicsItem` |
|  | La classe ne dérive pas<br> d'une classe de Qt |
</div>
<div>
| Connexion | Explication |
| ------- | -------------- |
|  | L'objet de type `A` <br>créé un objet de type `B` |
|  | L'objet de type `A` <br>possède un membre <br>pointant sur l'objet <br>de type `B` |
|  | La classe `A`dérive <br> de la classe `B` |
</div>
</div>
#### 3.5.1.1. Description des classes
- **`MainWidget`** est le widget principal qui se trouve dans l'objet `QMainWindow` du programme. Cette classe hérite de `QStackedWidget`, ce qui lui permet d'afficher un objet `QWidget` à la fois, donnant une façon simple d'implémenter des menus d'interface où l'on passe d'une page à l'autre. Au lancement du programme, `MainWidget` met comme widget visible celui provenant de la classe `StartMenu`.
- **`TcpClient`** est la classe qui s'occupe de la communication au serveur lors du mode de jeu multijoueur. Contrairement à ce que l'on peut attendre, `TcpClient` n'hérite pas de `QTcpSocket` mais possède un membre du type `QTcpSocket`. Également, sur le diagramme de classe, on peut apercevoir que cette classe possède un nombre de signaux et slots plus grand que les autres classes, car chaque type de donnée reçue ou envoyée au serveur se fait via des slots et des signaux. Si un joueur vole un candy lors d'une partie multijoueur, un signal est émis à la classe `TcpClient` qui envoie un message au serveur.
- **`StartMenu`** est la classe créant le premier widget visible au lancement du programme, composé d'une interface simple pour choisir le mode de jeu.
- **`WaitingRoom`** est la classe créant le widget affichant l'écran de préparation au mode de jeu multijoueur. Ce widget sert à accueillir tous les joueurs souhaitant jouer ensemble avant de lancer la partie.
- **`GameWidget`** est le widget contenant les objets de la classe `QGraphicsView` et de la classe `QGraphicsScene`. Ce widget est visible à l'écran lorsque le jeu est lancé.
- **`View`**, dérivant de `QGraphicsView` est la classe qui crée la ou les vues qui permettront de visualiser la scène.
- **`Game`** est la classe héritant de `QGraphicsScene` qui s'occupe de la création, du déroulement et de la suppression de chaque partie. Elle crée tous les `QGraphicsItem` nécessaires, les ajoute à la scène et les connecte entre eux pour leur fonctionnement. Cette classe s'occupe également d'envoyer et recevoir des signaux à la classe `TcpClient` lorsqu'il faut communiquer avec le serveur.
- **`KeyInputs`** est une classe ne créant qu'un seul objet. Son utilité est de pouvoir capturer les événements du clavier au même endroit avant de les envoyer aux objets `Player` correspondants.
- **`DataLoader`** est une classe qui, comme `KeyInputs`, ne créaient qu'un seul objet. Son utilité est de réunir au même endroit toutes les ressources communes à plusieurs objets/classes dans le projet, comme les données d'animations des joueurs, des bonbons, les couches de `Tiles`, les ressources de chaque type de bonbon, etc... Par exemple, l'objet `DataLoader` ne charge qu'une seule fois chaque image présente dans les décors puis chaque tile à un pointeur sur son image correspondante.
- **`Player`** est la classe s'occupant de gérer les joueurs présents sur la partie.
- **`Boss`** est la classe qui s'occupe de créer et d'animer le Saint-Nicolas et le Père Fouettard qui se trouve sur le terrain. La classe dérive de `Player` afin de bénéficier de son support pour l'animation de l'item.
- **`Tile`** est une classe dont chaque objet est un carreau sur le terrain de jeu. Avec le support des couches de tiles, plusieurs tiles peuvent se trouver au même endroit.
- **`TileCandyPlacement`** est une classe dérivant de`Tile` qui a pour but de faire apparaître des bonbons à intervalles réguliers sur le terrain.
- **`Candy`** est la classe s'occupant de gérer les bonbons sur la partie. Chaque bonbon est soit présent sur le jeu n'appartenant à personne, soit dans la file derrière un joueur. Dès qu'un bonbon atteint la base d'une des deux équipes, il se fait valider puis supprimer de la scène.
- **`FinishMenu`** est le widget de fin de partie, affichant le gagnant de la partie. Un bouton est présent pour permettre aux joueurs de retourner au menu principal.
### 3.5.2. Serveur
|  |
|-|
#### 3.5.2.1. Description des classes
- **`MainWindow`** est une classe simple créant l'interface du serveur. Elle possède un objet `QPlainTextEdit` qui montre les arrivées et envois de tous les messages que le serveur traite.
- **`TcpServer`** est une classe s'occupant de tous les clients connectés au serveur. Lorsqu'un utilisateur se connecte, elle créé un objet `ServerWorker`, lui assigne un thread et en garde une référence dans un de ses membres. La classe `TcpServer` permet de centraliser le traitement des données reçues par les différents clients et offre des fonctions utiles pour envoyer des messages comme un broadcast, envoyer un paquet à tous les clients l'envoyeur inclus, etc.
- **`ServerWorker`** est une classe représentant chaque client connecté au serveur. C'est dans cette classe que l'objet `QTcpSocket` se trouve et c'est cette classe qui envoie les paquets au client qui lui sont assignés.
# 4. Implémentation
## 4.1. Gestion des ressources
Au moment du projet où les joueurs étaient approximativement implémentés, ils avaient des animations et se déplacaient sur un terrain. La question de la gestion des ressources ne se posait pas encore. Chaque entité crééait des objets `QPixmap` et chargait tous les fichiers des différentes animations en mémoire. Il n'y avait encore que peu d'images présentes sur le projet à cet instant, donc cette façon de fonctionner était acceptable.
Ensuite, au point où il a fallu commencer l'implémentation de la classe `Tile`, nous avons remarqué que chaque tile ne pouvait pas charger individuellement son fichier image. En effet, il est beaucoup plus efficace que chaque tile du même type, c'est-à-dire montrant la même texture, aie un pointeur vers un objet `QPixmap`, qui a été le seul à charger cette image en mémoire.
En premier, nous avons essayé d'implémenter cette gestion des ressources en statique dans la classe `Tile`, mais rapidement nous nous sommes aperçus qu'il serait plus simple de centraliser la gestion des ressources pour tous les objets de la scène.
L'idée était donc de créer une classe donnant son type à un seul objet qui se chargerait de créer, sauvegarder et donner l'accès à toutes les ressources utilisées par plusieurs objets dans le projet. Cette classe est la classe `DataLoader`.
Lors de la modification de la classe `Player`, afin de centraliser les animations dans l'objet `dataLoader`, nous avons vite vu que tout ne pouvait pas être partagé entre les même ressources. Voici un tableau donnant toutes les ressources nécessaires à l'animation des joueurs et si cette ressource peut être partagée entre chaque joueur présent sur la scène ou non :
| Nom de la ressource | Type | Description | Ressource commune |
| - | - | - | - |
| pixmap | `QPixmap` | Objet QPixmap contenant l'image des animations du joueur | :heavy_check_mark: |
| nbFrames | `int` | Integer donnant le nombre total d'images dans l'animation | :heavy_check_mark: |
| framerate | `int` | Temps en milisecondes entre 2 frames de l'animation | :heavy_check_mark: |
| frameIndex | `int` | Integer donnant l'index de l'image actuellement montrée dans l'animation | :heavy_multiplication_x: |
| timer | `QTimer` | Timer qui incrémente à chaque exécution le `frameIndex` pour faire défiler les images des animations | :heavy_multiplication_x: |
À ce moment, le fonctionnement de la classe `DataLoader` s'est clarifié, elle contenient que les données partagées par plusieurs objets dans la scène. Ainsi, pour l'animation des joueurs, les membres `pixmap` et `nbFrames` se trouvent dans l'objet `dataLoader` et les membres `frameIndex` et `timer` directement dans chaque objet `player`.
Mais comme chaque animation de chaque objet `player` doit avoir accès aux données partagées de cette animation, un pointeur nommé `sharedDatas` est mis dans la structure présente dans la classe `Player`.
Voici un schéma montrant comment sont gérées les données pour les animations des joueurs entre les objets de la classe `Player` et les objets de la classe `DataLoader` :

L'exemple utilisé pour les animations des joueurs est également valable pour l'animation des bonbons, les ressources des tiles, etc...
Finalement, la classe `DataLoader` ne contient que des structures, membres et fonctions qui correspondent à un de ces points suivants :
- Déclaration des structures des ressources
- Conteneurs avec tous les objets du type d'une structure
- Fonctions pour charger les ressources et les mettre dans les conteneurs correspondants
- Fonctions pour reprendre des ressources depuis n'importe où dans le projet
Ci-dessous, vous trouverez un tableau donnant, pour chaque ligne, le nom du membre conteneur qui sauvegarde des membres ressources du projet, sa classe d'origine, son type ainsi que le contenu de la structure dont il fait référence.
|Nom|Classe|Type|Structure|
|-|-|-|-|
|playerAnimations|DataLoader|QHash<<br>int,<br> DataLoader::<br>PlayerAnimationsStruct\*>|QPixmap \*image<br>int nbFrame<br>int framerate|
|animationsLocal|Player|QHash<<br>Player::Animations,<br>Player::<br>PlayerAnimationsLocalStruct*>|QTimer \*timer<br>int frameIndex|
|candyRessources|DataLoader|QHash<<br>int,<br>DataLoader::<br>CandyRessourcesStruct*>|int nbPoints<br>int candyType<br>int candySize<br>int respawnDelayMs|
|candyAnimations|DataLoader|QHash<<br>int,<br>DataLoader::<br>CandyAnimationsStruct\*>|QPixmap \*image<br>QPixmap \*imageBlack<br>QPixmap \*imageRed<br>int nbFrame<br>int framerate|
|animationsLocal|Candy|QHash<<br>Candy::Animations,<br>Candy::<br>CandyAnimationsLocalStruct*>|QTimer \*timer<br>int frameIndex|
|candyPlacements|DataLoader|QList<<br>DataLoader::<br>CandyPlacements\*>|int x<br>int y<br>bool taken<br>QTimer \*timer|
|tileLayers|DataLoader|QMap<<br>QString,<br>DataLoader::<br>TileLayerStruct\*>|QList<QList\<int>> tiles<br>int width<br>int height<br>int topLeftX<br>int topLeftY<br>QVariant zIndex|
|tileRessources|DataLoader|QHash<<br>int<br>DataLoader::<br>TileRessourcesStruct\*>|QPixmap *image<br>QString name|
## 4.2. Terrain de jeu
Le terrain de jeu du projet comporte les fonctionnalités suivantes :
- Création d'une grille contenant des tiles
- "Layers", ou couches de tiles pour pouvoir en superposer sur le terrain
- Pas de limite de taille de terrain
### 4.2.1. Création et sauvegarde
La création du terrain de jeu s'est faite avec un programme externe, nommé "Tiled". Ce logiciel est gratuit et libre.
Tiled est un éditeur de carte pour jeu vidéo. La création d'un terrain se fait exactement comme pour la construction d'un toit : tuile après tuile. Ces tuiles sont appelées "tile".
Nous pouvons donc manipuler ces tiles, qui sont de petits carreaux faits d'images, en les plaçant sur une tilemap, qui est une grille prête à accepter ces images.
Voici une capture d'écran d'un de nos terrains sous Tiled :

Lorsque les images sont importées dans Tiled, il n'y a qu'à les placer sur le bon calque.
Finalement, le fichier comportant le terrain est un fichier au format `.tmx`, encodé en XML.
Dans ce fichier, chaque tile de chaque calque est représenté par un ID numérique. Chaque calque est séparé en chunks 16x16 tiles, un chunk étant une liste comportant les ID de ses 256 tiles. Chaque chunk possède également sa position en X et en Y par rapport au point d'origine du calque.
Le fichier `.tmx` possède aussi le chemin de toutes les images utilisées dans le projet, triées par l'ID utilisé dans les chunks. Il est donc facile de faire correspondre une image à l'identifiant de sa tile.
### 4.2.2. Importation des calques
L'importation du fichier `.tmx` dans la scène du jeu se fait en 2 étapes :
1. Créer une liste de tous les ID des tiles pour chaque calque
2. Créer les objets `Tile`
3. Placer les objets `Tile` sur le terrain
La première étape se fait dans la classe `DataLoader`.
Les deux dernières étapes se font dans la classe `Game`.
Ces étapes sont détaillées dans les sous-chapitres suivants.
#### 4.2.2.1. Créer la liste des identifiants
La création de la liste des identifiants des tiles pour chaque calques se fait dans la classe `DataLoader`.
Avant de directement réunir tous les chunks dans une même liste, il faut calculer la taille totale du calque.
Cette opération se fait dans la fonction `getLayerPlacement()` en prenant le chunk le plus à gauche, celui qui est le plus à droite, celui qui est le plus haut et celui qui est le plus bas. En soustrayant les valeurs extrêmes, on obtient la taille du calque que l'on veut créer.
Ensuite, il faut initialiser un tableau en 2 dimensions de la taille que nous venons de calculer avec des 0 partout :
```cpp
// Initialiser la liste
for(int i = 0; i < layerHeight; i++) {
QList<int> subList;
for(int j = 0; j < layerWidth; j++) {
subList.append(0);
}
dimLevel.append(subList);
}
```
Ce tableau représente le calque entier. Une fois qu'il est créé, il suffit d'itérer dans chaque chunk afin de placer l'identifiant de chaque tile dans le tableau du calque :
```cpp
// Pour chaque chunk
for(int i = 0; i < chunks.length(); i++) {
QDomElement chunk = chunks.at(i).toElement();
QList<QString> stringList = chunk.text()
.replace("\n", "").replace("\r", "").split(",");
QList<int> intList;
// On met le chunk sous forme de list de int
for(int i = 0; i < stringList.length(); i++) {
intList.append(stringList.at(i).toInt());
}
// On itère dans les 2 dimentions du chunk
for(int y = 0; y < chunkSize; y++) {
for(int x = 0; x < chunkSize; x++) {
// On ajoute la tile dans le tableau du calque
int insertYList = chunk.attribute("y").toInt() + y - *topLeftY;
int insertXList = chunk.attribute("x").toInt() + x - *topLeftX;
QList<int> subList = dimLevel.value(insertYList);
subList.replace(insertXList, intList.at(y*chunkSize + x));
dimLevel.replace(insertYList, subList);
}
}
}
return dimLevel;
```
Finalement, le tableau à 2 dimensions contenant le calque est retourné à la fin de la fonction, puis placé dans un conteneur de la classe `DataLoader`
#### 4.2.2.2. Créer les tiles
Dans la classe `Game`, au lancement de la partie, la fonction `placeTiles()` est appelée.
Cette fonction va parcourir tous les calques présents dans le conteneur correspondant de la classe `DataLoader`, parcourir tous les identifiants du calque puis créer un objet `Tile` en lui donnant l'identifiant de sa tile.
Plus tard, dans la classe `Tile`, l'objet ira demander au `DataLoader` à quelle image correspond son identifiant de tile.
#### 4.2.2.3. Placement des tiles
Chaque tile présente sur le terrain a sa position déterminée par 3 coordonnées qu'il faut additionner lors de la création des tiles :
1. La coordonnée de la tile dans son chunk
2. La coordonnée du chunk dans le calque
3. La coordonnée du calque dans le terrain du jeu
Voici un exemple avec un calque ne possédant que deux tiles réparties à deux endroits sur le terrain. Chaque point est une origine et <span style="color:orange">chaque flèche jaune est une coordonnée entre deux points</span>.
<span style="color:green">Le calque est entouré en vert</span> et <span style="color:blue">ses chunks, comportant des tiles, sont entourés en bleu</span>.

### 4.2.3. le Z index
Le Z index est une valeur entière qui garantit l'ordre d'affichage. Si le Z index d'un objet est plus grand que celui d'un autre objet, il sera affiché par-dessus celui-ci. Le Z Index peut également être un chiffre négatif.

Dans le jeu, l'utilisation du Z Index est très présente pour renforcer l'impression que les personnages s'intègrent dans leur environnement. Ainsi, un personnage se trouvant derrière un autre personnage sera caché par lui, la même chose est valide entre les personnages, les friandises et certains éléments du décor.
Les personnages peuvent également se placer à la fois devant et derrière des objets plus gros, comme les arbres présents dans la cour de l'école.
  
Dans l'implémentation, le devant et l'arrière correspondent à la coordonnée Y des objets sur le terrain. En déplaçant le personnage de haut en bas, la valeur de son Z index changera.
Bien que le jeu ne soit qu'en 2 dimensions, on peut essayer de visualiser la gestion des couches en mettant comme direction en Z le Z Index :

Le rectangle rouge est le plan incliné sur lequel se déplacent les joueurs. Sa valeur en Z est égale à la coordonnée en Y du joueur.
Mais rapidement un autre problème est soulevé : à quelle valeur doit-on mettre le Z-Index des éléments se trouvant en arrière-plan ? Et ceux qui sont à l'avant-plan ?
Si l'on met ceux à l'arrière à zéro, tous les objets au Z Index dynamique de la scène disparaitront derrière le sol dès qu'ils atteindront une coordonnée négative. Et réciproquement, si l'on met une coordonnée trop faible aux éléments se trouvant sur la décoration d'avant-plan, les joueurs apparaitront devant eux à tout moment.
La solution est de calculer le point le plus haut du terrain de jeu ainsi que son point le plus bas.
```cpp
QHash<QString, int> DataLoader::highestLowestPointsOfMap() {
QMapIterator<QString, TileLayerStruct*> i(tileLayers);
QHash<QString, int> returnValue;
returnValue["highest"] = tileLayers.first()->topLeftY;
returnValue["lowest"] = tileLayers.first()->tiles.length() +
tileLayers.first()->topLeftY;
while(i.hasNext()) {
i.next();
if(i.value()->topLeftY < returnValue["highest"])
returnValue["highest"] = i.value()->topLeftY;
if(i.value()->tiles.length() + i.value()->topLeftY > returnValue["lowest"])
returnValue["lowest"] = i.value()->tiles.length() + i.value()->topLeftY;
}
returnValue["highest"] *= 130;
returnValue["lowest"] *= 130;
return returnValue;
}
```
La valeur du Z index des éléments derrière le joueur est donc égale à la coordonnée Y du point le plus bas du terrain de jeu.
La valeur du Z index des éléments devant le joueur sont, quant à eux, égaux à la coordonnée Y du point le plus haut.
## 4.3. Joueurs
### 4.3.1. Déplacements
Les déplacements des joueurs sont faits grâce aux entrées du clavier.
Dans notre projet, cette implémentation est particulière, car elle doit être compatible en mode de jeu local et multijoueur.
Son fonctionnement peut être séparé en deux parties et est représenté dans le schéma ci-dessous. Chaque partie correspond à une couleur et est présenté avec des fonctions et des flèches :
1. <span style="color:purple">La détection de déplacement dans une direction</span>
2. <span style="color:blue">Le déplacement dans cette direction</span>

#### 4.3.1.1. Détection
Au démarrage de la partie, la classe `KeyInputs` va charger dans un membre conteneur les différentes actions à réaliser selon la touche du clavier pressée :
```cpp
void KeyInputs::setPlayerKeys(int focusedPlayerId) {
if(focusedPlayerId == -1)
focusedPlayerId = 0;
// La clé à presser, {l'id du joueur, la direction}
playersKeys.insert(Qt::Key_W, {focusedPlayerId, up});
playersKeys.insert(Qt::Key_A, {focusedPlayerId, left});
playersKeys.insert(Qt::Key_S, {focusedPlayerId, down});
playersKeys.insert(Qt::Key_D, {focusedPlayerId, right});
if(focusedPlayerId == 0) {
// Si l'id du 1er joueur n'est pas un socketDescriptor
playersKeys.insert(Qt::Key_Up, {1, up});
playersKeys.insert(Qt::Key_Left, {1, left});
playersKeys.insert(Qt::Key_Down, {1, down});
playersKeys.insert(Qt::Key_Right, {1, right});
}
}
```
On peut voir dans ce bout de code que, si la partie se déroule en local, les identifiants des joueurs seront égaux à 0 et 1.
Mais, si la partie est en multijoueur, il n'y aura que le premier joueur qui aura des touches assignées dans le membre `playersKeys`.
Une fois que les configurations des touches sont enregistrées dans la variable `playersKeys`, la classe `KeyInputs` peut recevoir les événements du clavier via les fonctions de pression et de relâchement des touches.
Ces deux fonctions réalisent fondamentalement la même chose, c'est-à-dire d'émettre le signal disant qu'une touche de déplacement a été appuyée ou relâchée.
```c++
void KeyInputs::keyPress(QKeyEvent * event) {
int key = event->key();
if(playersKeys.contains(key)) {
int playerId = playersKeys.value(key).at(0);
int playerMove = playersKeys.value(key).at(1);
emit playerKeyToggle(
playerId,
playerMove,
// dans l'autre fonction, cette valeur
// est à false
true
);
}
}
```
Ce signal précédemment connecté depuis la classe `Game` est envoyé à tous les objets de la classe Player via leur slot `keyMove` :
```c++
connect(keyboardInputs, &KeyInputs::playerKeyToggle, i.value(), &Player::keyMove);
```
Une fois le signal arrivé dans la classe `Player`, on regarde si l'identifiant du joueur qui se déplace correspond au nôtre. Si c'est le cas, une valeur du tableau `moves` est modifiée.
```c++
void Player::keyMove(int playerId, int direction, bool value) {
if(playerId == id) {
moves[direction] = value;
}
// [...]
}
```
Le tableau **`move`** dans la classe `player` est un élément central du déplacement des joueurs.
Ce tableau est de type booléen et ne comporte que 4 valeurs, qui représentent respectivement le haut, la droite, le bas et la gauche.
```c++
bool moves[4] = {false, false, false, false};
```
Finalement, lorqu'une touche est pressée, le booléen correspondant à sa direction dans le tableau `moves` est modifié. Ainsi, il ne reste plus qu'à déplacer le joueur à intervalle de temps régulier dans la direction composée de ces 4 booléens.
#### 4.3.1.2. Déplacement
Si l'on reprend le schéma au chapitre [5.3.1](#531-Déplacement), l'exécution du déplacement des personnages se passe dans la classe `Game` et dans la classe `Player`.
Dans la classe `Game`, le membre `playerRefresh` de type `QTimer*` s'exécute à intervalle de temps régulier exécutant la fonction `refreshEntities()`.
La fonction `RefreshEntities()` exécute la fonction `refresh(double delta)` de tous les joueurs.
Cette fonction `refresh(double delta)` détermine le vecteur de déplacement selon le tableau de booléen `moves` via la fonction `calculateMovingVector` :
```c++
QVector2D Player::calculateMovingVector(double delta) {
QVector2D v;
v.setX(int(moves[moveRight]) - int(moves[moveLeft]));
v.setY(int(moves[moveDown]) - int(moves[moveUp]));
v.normalize();
v *= delta * playerSpeed;
return v;
}
```
Finalement, maintenant que le vecteur de déplacement est déterminé, il ne reste plus qu'à appeler la fonction `move(QVector2D v)` pour appliquer le déplacement au joueur.
#### 4.3.1.3. Intervalle Delta
Dans la classe `Game`, le timer `playerRefresh` a un temps d'intervalle très court, plus précisément de : $t = \frac{1}{60}*1000$ millisecondes. Ce temps très court est le temps nécessaire pour qu'il y ait 60 refresh de joueur par seconde.
Cependant, les ordinateurs sont en constant changement dans l'utilisation de leur ressources, et ce délai de $16.\overline{6}$ millisecondes ne sera pas toujours correct à chaque exécution, ce qui fait que la vitesse du joueur changera selon l'utilisation des ressources de l'ordinateur.
La solution est de déterminer le temps passé entre chaque exécution du timer, appelé aussi Delta. Ce facteur est réduit de $10^6$ pour le passer de nanoseconde à milliseconde. Il est ensuite envoyé à la fonction de refresh de tous les joueurs.
```c++
void Game::refreshEntities() {
int delta = playerRefreshDelta->nsecsElapsed();
double deltaMs = delta/10e6;
playerRefreshDelta->restart();
QHashIterator<int, Player*> i(players);
while(i.hasNext()) {
i.next();
i.value()->refresh(deltaMs, [...]);
// [...]
}
}
```
Pour appliquer ce facteur Delta au vecteur de déplacement, l'exemple se trouve dans la première section de code du chapitre [5.3.1.2. Déplacement du joueur](#5312-Déplacement-du-joueur). La formule à utiliser pour calculer le vecteur de déplacement est la suivante :
$V_{deplacement} = V_{normalisé}*v*\Delta_{ms}$
où $V_{normalisé}$ est le vecteur de déplacement selon le tableau de booleens `moves`, $v$ est la vitesse du joueur et $\Delta_{ms}$ est Delta en millisecondes.
Cet intervalle Delta est également utilisé à chaque calcul de déplacement dans le projet, c'est à dire :
- Le déplacement des friandises
- Le déplacement de la caméra
### 4.3.2. Animations
L'animation des joueurs est réalisée directement depuis la classe `Player`. La structure de données utilisée afin d'avoir à disposition toutes les ressources nécessaires à l'animation du joueur se trouvent dans le chapitre [5.1. Gestion des ressources](#51-Gestion-des-ressources).
Pour rappel, voici les ressources dont le joueur a besoin pour être animé :
- Ressources partagées :
- pixmap, l'objet qui contient la spritesheet (feuille d'animation)
- nbFrames, l'entier qui indique le nombre d'images sur la spritesheet
- framerate, l'entier donne en millisecondes le temps entre 2 frames de l'animation
- Ressources privées :
- frameIndex, l'entier définit sur quelle frame de l'animation le joueur se trouve actuellement
- timer, l'objet `QTimer` qui met la frame suivante à chaque exécution
Vu du côté du joueur, il n'est pas possible d'accéder aux ressources partagées de ses animations sans avoir accès à l'objet `dataLoader`. C'est pour cela que dans la structure des ressources privées de chaque animation se trouve un pointeur sur un objet de la structure se trouvant dans la classe `DataLoader` :
```c++
typedef struct Animations_s {
QTimer *timer;
int frameIndex;
// Pointeur sur les ressources partagées de l'animation
DataLoader::PlayerAnimationsStruct *sharedDatas;
} AnimationsLocalStruct;
```
Finalement, l'affichage de l'image se fait dans la fonction virtuelle `paint()` de la classe `Player` :
```c++
// Dessiner le joueur
QRectF sourceRect = QRectF(
imageToDraw->width() / animToDraw->sharedDatas->nbFrame * animToDraw->frameIndex,
0,
imageToDraw->width() / animToDraw->sharedDatas->nbFrame,
imageToDraw->height());
QRectF targetRect = QRectF(
0,
0,
dataLoader->getPlayerSize().x(),
dataLoader->getPlayerSize().y());
painter->drawPixmap(targetRect, *imageToDraw, sourceRect);
```
On peut y voir qu'un rectangle source ainsi qu'un rectangle de destination sont définis et que l'objet `painter` dessine l'image selon ces rectangles.
Ces deux rectangles correspondent respectivement à la zone de l'image de base que l'on veut dessiner et à la zone dans laquelle on veut dessiner ce bout d'image.
En faisant incrémenter l'index de l'animation avec le timer et en le remettant à 0 lorsqu'il arrive au bout de toutes les frames, on obtient une animation fluide des personnages.

Tout le procédé expliqué pour les animations des joueurs est également applicable pour les animations des candy.
### 4.3.3. Collisions
Dans le jeu, les joueurs détectent les collisions avec plusieurs objets : les murs, les points d'apparition, les candy ainsi que certains décors. Le plus intéressant est la collision avec les murs, car il y a en plus un vecteur résultat qui doit être appliqué pour corriger le vecteur de direction du joueur afin de ne pas pouvoir passer à travers les murs.
L'algorithme simplifié qui permet de le réaliser est le suivant :
1. Déterminer le vecteur mouvement
2. S'il y a une collision
2.1. Vecteur de mouvement = déterminer le vecteur de réponse
3. Déplacer le joueur en fonction du vecteur de mouvement
Pour commencer, la détermination du vecteur de mouvement est expliquée au chapitre [4.3.1.2. Déplacement](#4312-Déplacement).
Ensuite, le test de collision se fait via la fonction `collideWithWalls()`, qui fonctionne de la manière suivante :
1. On déplace le joueur de 3 fois le vecteur de déplacement
2. On teste si le joueur entre en collision avec une tile de collision
3. On remet le joueur à sa place
Si le joueur touche une tile de collision, le vecteur de réponse est calculé de la manière suivante :
|**Le vecteur normal est l'inverse de la somme des vecteurs décomposés du vecteur mouvement qui entrent en collision avec un autre objet.**<br>**Le vecteur réponse est la somme du vecteur mouvement et du vecteur normal**|
|-|
Ceci signifie que les mouvements du joueur sont décomposés en mouvement X ainsi qu'en mouvement Y qui sont chacun testés pour la collision.
Ensuite, le vecteur normal, celui qui répond lorsque nous poussons contre un mur, est construit de l'addition de l'inverse des vecteurs X ou Y qui entrent en collision avec un mur.
Dans le schéma suivant, on peut constater que c'est le vecteur X qui, lorsqu'il se déplace en négatif, entre en collision avec un mur.
||
|-|
Le **<span style="color:green">vecteur normal</span>** est donc égal a l'inverse du vecteur X.
Finalement, le **<span style="color:red">vecteur résultat</span>** est la somme du **<span style="color:blue">vecteur de direction</span>** et du **<span style="color:green">vecteur normal</span>**.
De cette façon, lorsqu'un joueur se déplace en diagonal contre un mur, il se déplacera le long du mur à une vitesse réduite, contrairement à un déplacement en ligne droite le long du mur.
Ce procédé donne un côté plus réaliste au système de collisions.
Finalement, la fonction qui détermine si une collision subvient entre le joueur et un mur ne vérifie pas toutes les tiles du terrain de jeu. Effectivement, Pour améliorer les performances de l'algorithme, uniquement les tiles à proximité du joueur seront testées pour détecter une éventuelle collision.

## 4.4. Candy
Sur la scène du jeu, les candy sont des objets animés que le joueur peut ramasser et ramener dans sa base.
Les candy ont donc comme cycle de vie :
- Apparaître
- Être ramassé par un joueur
- Être volé par un autre joueur
- Se faire valider pour une équipe
Sur notre terrain, nous avons réparti quatre types de friandises, rapportant chacun un nombre de points différents :
|Friandise | Nombre de points|
| --------------- | --------------- |
|Grande mandarine | 10 |
|Petite mandarine | 3 |
|Grande cacahuète | 5 |
|Petite cacahuète | 1 |
### 4.4.1. Apparition
Lors des premiers tests de la gestion des friandises, leur position était définie de manière complètement aléatoire sur le terrain, ce qui pouvait donner des résultats comme des bonbons trop proches d'autres, ou par la suite devoir gérer l'apparition des friandises dans les murs.
Finalement, c'est dans un calque du fichier `.tmx`, fait avec Tiled, que les emplacements d'apparition des friandises sont définis. Cette manière de faire permet d'avoir une répartition un minimum harmonique des bonbons sur le terrain et de mieux les intégrer dans l'environnement en les faisant apparaître, par exemple, le long des chemins.
Lorsque les classes `DataLoader` et `Game` ont terminé de faire apparaître le terrain de jeu, c'est la fonction `placeTilesCandyPlacement()` qui créé des objets de type `TileCandyPlacement`, une classe héritant de `Tile` avec les informations du calque où sont initialement placés les friandises.
Chaque objet `tileCandyPlacement` possède un timer et un type de candy associé. À chaque exécution de ce timer, l'objet envoie un signal à la classe `Game` pour créer un bonbon et l'ajouter sur la scène.
Lorsqu'un bonbon est ajouté sur la scène, sa référence est également sauvegardée dans un membre conteneur de la classe `Game` afin d'avoir une liste de tous les bonbons présents sur la scène, triés par identifiant. (cf. [le traitement des données en multijoueur]()).
Cependant, il est nécessaire de créer un identifiant unique, sachant que les Candy sont créés par plusieurs objets `tileCandyPlacement`. La solution est de définir un membre static dans la classe `TilecandyPlacement` qui est incrémenté par chaque instance de cette classe à chaque fois qu'un bonbon apparaît.
Finalement, le candy possède également une référence à l'objet `tileCandyPlacement` qui l'a créé, afin de pouvoir le prévenir qu'il a été ramassé lorsqu'un joueur passe dessus et démarrer le timer qui fera apparaître un autre candy par la suite.
Dans l'image ci-dessous, le joueur est entouré des objets `tileCandyPlacement`, faisant apparaître les candy petit à petit sur le terrain :

### 4.4.2. Récolte
La récolte d'un candy par un joueur est initiée par le joueur qui entre en contact avec le candy en question.
Comme pour la [collision avec les murs](#433-Collisions), le joueur appelle la fonction `pickUp(int id, int team)` de l'objet Candy qu'il vient de ramasser.
```cpp
void Candy::pickUp(int playerId, int idTeam) {
taken = true;
// Envoyer un msg au slot du TileCandyPlacement qu'il
// peut en placer un nouveau
emit pickedUp();
currentPlayerId = playerId;
this->idTeam = idTeam;
}
```
Dans la classe `Player`, lorsqu'une collision est détectée avec un candy, la fonction `pickUp(id, team)` est non seulement appelée, mais l'identifiant du candy est généré [lors de sa création](#441-Apparition). Il est ensuite ajouté à une liste contenant les identifiants de tous les candy que le joueur a ramassé.
```cpp
candyNearby->pickUp(id, team);
IdsCandiesTaken.prepend(candyNearby->getId());
```
### 4.4.3. File de candy
L'implémentation de la file de candy a été une tâche complexe et un réel défi à implémenter pour nous.
Dès le départ, plusieurs solutions ont été envisagées et proposées avant de faire le choix final et pouvoir l'implémenter.
Sur ce schéma se trouve les différentes propositions de comment implémenter cette fonctionnalité dans le projet :

Finalement, c'est la solution utilisant de l'interpolation linéaire sans collisions qui a été retenue. En effet, sa simplicité d'implémentation face aux autres et son efficacité nous semblait plus garantie que pour certaines autres propositions.
L'interpolation linéaire est une méthode très simple pour définir des valeurs se trouvant entre deux autres valeurs. En y ajoutant un facteur de division et en l'appliquant sur des coordonnées X et Y, on obtient cette formule qui permet de trouver un point résultat selon le point de coordonnée actuel et le point cible :
$P_{resultat} = P_{actuel} + \frac{P_{cible} - P_{actuel}}{lerpFactor}$
Graphiquement, cette formule se traduit par ce schéma :

La différence entre le schéma de gauche et celui de droite est le facteur $lerpFactor$.
Dans celui de droite, $lerpFactor$ est à 2, tandis que dans celui de gauche $lerpFactor$ est à une valeur plus grande.
Plus $lerpFactor$ est grand, plus le point résultat sera proche du point actuel.
Dans l'implémentation de la file de candy, l'interpolation linéaire est faite entre chaque candy et son précédent. Ainsi, chaque point actuel représente les coordonnées du candy, le point cible représente le candy se trouvant avant lui et le point résultat est l'endroit où le candy sera placé.
En implémentant cette solution, les candy se mettent chacun à suivre leur candy prédécesseur dans la file des candy du joueur.

Mais un autre problème est vite soulevé : plus un candy est loin dans la liste des candy ramassés d'un joueur, plus son point cible sera une approximation de l'endroit où s'est trouvé le joueur auparavant.
Le résultat est que les candy proches du joueur sont très collés à lui et ceux qui sont loin derrière seront de plus en plus distants.
Pour corriger ce défaut, le facteur d'interpolation $lerpFactor$ peut être modifié pour prendre en compte la position du candy dans la file de candy de la façon suivante :

Ainsi, plus un candy est loin dans la liste des candy que le joueur a ramassé, plus il voudra se téléporter plus proche de son prédécesseur.
Dans le code, ces formules sont implémentées de la manière suivante :
```c++
// pos est la position du candy précédant dans la liste
void Candy::refresh(QPointF pos, int posInQueue, double delta) {
// Si le candy n'est pas pris par un joueur ou est validé (en train
// d'aller vers une base), on ne fait rien
if(!taken || valid) return;
int yOffset = 0;
int lerpFactor = (LERP_AMOUNT * LERP_ACCELERATION) / (LERP_AMOUNT + posInQueue);
// placement du 1er candy vers les pieds du joueur
if(posInQueue == 0) yOffset = dataLoader->getPlayerSize().y() / 8;
setX(this->x() + (pos.x() - this->x() ) / lerpFactor * delta);
setY(this->y() + (pos.y() - this->y() + yOffset) / lerpFactor * delta);
setZIndex();
}
```
Aussi, le facteur Delta multiplie le tout, comme expliqué au chapitre [4.3.1.3. Intervalle Delta](#4313-Intervalle-Delta).
Finalement, en jouant un peu avec les facteurs `LERP_AMOUNT` et `LERP_ACCELERATION` des formules, nous avons trouvé des valeurs optimales pour que l'espacement des candy soit à peu près égal, peu importe la taille de la file de candy.

### 4.4.4. Vol des candy
Le vol des candy est implémenté d'une manière assez similaire à la [récolte des candy](#442-Récolte), qui sont dispersés sur le terrain.
Dès qu'un joueur entre en collision avec un candy, il regarde si ce candy est déjà ramassé par un autre joueur. Si c'est le cas, le joueur émet le signal `stealCandies(int candyIdStartingFrom, int playerWinningId)`, qui est connecté à la classe `Game`.
Ensuite, la classe `Game` va indiquer à quel joueur appartient chaque candy volé :
```cpp
void Game::playerStealsCandies(int candyIdStartingFrom, int playerWinningId) {
Player *victim = players[candies[candyIdStartingFrom]->getCurrentPlayerId()];
Player *stealer = players[playerWinningId];
// diverses vérifications pour être sûr que le joueur peut voler le candy
// (pas de la même équipe, le candy existe bel et bien, etc...)
// On informe au joueur qu'il a perdu des bonbons
// et il nous donne en retour la liste de tous les bonbons volés
QList<int>candiesGained = victim->looseCandies(candyIdStartingFrom);
// définir le nouveau joueur pour chacun de ces candy
for(int i = 0; i < candiesGained.length(); i++) {
candies[candiesGained.at(i)]->setCurrentPlayerId(playerWinningId);
candies[candiesGained.at(i)]->setTeamId(stealer->getTeam());
}
// Ajouter tous les candies volés dans la queue du joueur
stealer->prependCandiesTaken(candiesGained);
// Protéger la queue du joueur
stealer->protectQueue();
}
```
À la fin de ce code, le joueur ayant volé les candy se fait protéger sa file pendant un court instant, car nous avons remarqué que si un joueur reste immobile avec ses candy empilés derrière lui, il était difficile de lui voler des bonbons sans protéger la file du voleur.
## 4.5. Multijoueur
Le multijoueur est une fonctionnalité importante de ce projet. À la base prévue comme objectif non-primaire, durant son implémentation il a modifié la structure du projet de manière significative.
Premièrement, au lieu d'implémenter le serveur dans un le client, le serveur est un projet Qt à part. En effet, la séparation en deux programmes distincts a permis de simplifier l'implémentation de ce dernier.
### 4.5.1. Gestion des ressources
Jouer en multijoueur signifie pour un projet que chaque entité partagée entre tous les clients doit être identifiable. Si un joueur se déplace, un message doit être envoyé à tous les clients pour afficher partout que ce joueur s'est déplacé.

Dans ce schéma, si le <span style="color:blue">client 1</span> appuie sur la touche `[D]`, pour se déplacer vers la droite, il changera sa position et devra l'envoyer au serveur.
Le serveur, quant à lui, broadcastera son message à tous les autres clients avec les données que le client 1 s'est déplacé vers la droite.
Une fois le message arrivé auprès du <span style="color:red">client 2</span> ainsi qu'au <span style="color:green">client 3</span>, il faut appliquer l'action de se déplacer au bon joueur. C'est pourquoi il est primordial de pouvoir identifier les entités de la même façon sur tous les clients.
Avant l'implémentation du multijoueur, les différents joueurs étaient placés dans une `QList<Player *>` dans la classe `Game`. Les bonbons volés possédaient un pointeur sur le joueur qui les avait récolté, tout se faisait via référence.
Mais comme en multijoueur, il n'est pas possible de faire passer des références à des objets via un réseau, les clients sont passés d'une `QList<Player *>` à un `QHash<int, Player *>`, afin d'avoir un identifiant étant un entier.
Cette approche a modifié la gestion des ressources dans le projet. En effet, toutes les identités sont désormais identifiables par un entier numérique, que ce soit en partie locale ou multijoueur.
Cet identifiant unique est directement offert par la classe `QAbstractSocket`. Il s'agit du `socket descriptor` qui fait office d'identifiant pour les classes `QTcpServer` et `QTcpSocket`. Lorsqu'un client se connecte au serveur pour la première fois, son socket descriptor est définit puis envoyé à tous les clients pour leur annoncer sa connexion.
### 4.5.2. Communication
Ce chapitre présente la manière dont le client communique avec le serveur.
#### 4.5.2.1. Côté serveur

Pour le serveur, chaque client connecté est représenté par un objet de la classe `ServerWorker`. Lors de la création de cet objet, le socket descriptor est mis et le client est déplacé vers un nouveau thread.
Voici la fonction `incomingConnection`, qui est déclenchée quand un utilisateur se connecte au serveur. La fonction définit le socket descriptor, fait des connexions pour divers signaux/slots et déplace l'objet `ServerWorker` vers un nouveau thread.
```cpp
void TcpServer::incomingConnection(qintptr socketDescriptor) {
ServerWorker *worker = new ServerWorker;
// Mettre le socket descriptor au nouveau client
if(!worker->setSocketDescriptor(socketDescriptor)) {
worker->deleteLater();
return;
}
// Ajouter le client à un thread
int threadIdx = availableThreads.size();
if(threadIdx < idealThreadCount) {
availableThreads.append(new QThread);
threadsLoaded.append(1);
availableThreads.last()->start();
}else{
// Trouver le thread avec le moins de clients et l'utiliser
threadIdx = std::distance(
threadsLoaded.cbegin(),
std::min_element(threadsLoaded.cbegin(),
threadsLoaded.cend()));
threadsLoaded[threadIdx]++;
}
worker->moveToThread(availableThreads.at(threadIdx));
// [...]
clients.append(worker);
emit logMessage("Nouveau client connecté");
}
```
#### 4.5.2.2. Côté client
Lors de la création de l'objet `Game` au début de la partie, un booléen lui est passé en paramètre pour indiquer si la partie sera en multijoueur ou non.
Avec l'exemple du déplacement des joueurs, si la partie est en local, une <span style="color:purple">connexion sera faite entre l'objet `KeyInputs` et les objets `Player`</span>. Ainsi, les événements du clavier seront envoyés aux deux clients qui testeront si l'identifiant du joueur à déplacer est le leur ou non.
Si la partie est en multijoueur, l'objet `Game` va créer <span style="color:orange">deux connexions supplémentaires</span> :
- L'envoi des événements du clavier au `TcpClient`, qui l'enverra à son tour au serveur
- La réception des messages du `TcpClient` destinés aux déplacements des joueurs qui sont envoyés directement aux `Player`.
Il y a également une autre différence : en local le signal sortant de `KeyInputs` est connecté à tous les objets `Player`, tandis qu'en multijoueur le signal ne sera connecté qu'au `Player`, qui est le nôtre dans la scène.
Cette façon de communiquer du côté client est valable également pour les autres messages que le client pourrait envoyer au serveur. Comme par exemple, le fait de ramasser un candy, en voler un ou encore les valider à sa base.

### 4.5.3. Déplacement des entités
Afin de réaliser le mouvement des entités, nous avons utilisé le déplacement relatif et le rollback. Chaque client est responsable de sa position et de la position de ses candy.
Lorsqu'il s'agit de déplacer des joueurs en multijoueur sur un jeu vidéo, la question du nombre élevé de messages envoyés entre les instances vient vite se poser. Il existe principalement deux façons de le faire :
1. Envoyer la position absolue du joueur aux autres clients à chaque fois que le jeu se rafraîchit.
2. N'envoyer que les événements du clavier
Dans le premier cas, le nombre de messages est très élevé. Plus il y a de joueurs sur le terrain, plus le nombre de messages augmentera jusqu'à saturation.
Cette technique peut être optimisée si on n'envoie des messages qu'aux moments où le joueur se déplace. Mais cela fait tout de même beaucoup de messages à envoyer, car les joueurs passent plus de temps à se déplacer sur le jeu qu'à rester immobile.
Dans le deuxième cas, les messages envoyés sont du type :
- Joueur 1294, direction gauche, se déplace
- Joueur 802, direction haut, ne se déplace plus
- Joueur 2114, direction droite, se déplace
- Joueur 1294, direction gauche, ne se déplace plus
De cette manière, le nombre d'envoi de messages est beaucoup moins élevé que le premier cas.
Cependant, il faut prendre en compte la latence du réseau potentielle, si un message indiquant qu'un joueur arrête de se déplacer arrive en retard, il y aura une différence de position entre le client qui joue et les autres clients.
La meilleure façon de faire est finalement de combiner les 2.
Les événements sont tous envoyés comme dans le deuxième cas, à intervalle régulier, avec un message contenant la position du joueur, destiné aux autres clients pour que la position absolue du joueur soit remise au même endroit partout.
Dans le code, ce temps d'intervalle est d'une seconde. En conclusion, cette façon de procéder est la plus efficace au niveau du nombres de messages envoyés et de sa fiabilité.
### 4.5.4. Synchronisation des apparitions des friandises
Comme les friandises sont des objets synchronisés entre les différents clients, leur apparition l'est également.
Cependant, dans la version locale du jeu, les friandises apparaissent sur les `TileCandyPlacement` à intervalle irrégulier.
Lors de l'implémentation du multijoueur, il a fallu adapter ce procédé pour synchroniser tous les clients.
Une façon assez directe de le faire est de maintenir une liste des objets `TileCandyPlacement` sur le serveur et à intervalle irrégulier, le serveur envoie à tous les clients qu'un objet `TileCandyPlacement` fasse apparaître une friandise.
Cependant, cette façon aurait demandé d'intégrer sur le serveur un système de timer et de suivi pour savoir quels `tileCandyPlacement` ont une friandise et lesquels n'en ont pas. Cela aurait fait une répétition de code, car ces fonctionnalités sont déjà intégrées dans chaque client.
Le procédé choisi est de définir un client au hasard qui sera nommé le **`CandyMaster`** de la partie. C'est donc ce client qui enverra au serveur des messages, qui seront broadcastés à tous les autres clients, leur ordonnant de faire apparaître une friandise à un endroit précis. Ainsi, l'apparition des friandises est synchronisée entre tous les clients.
## 4.6. Musique
La musique a été ajouté grâce au module "multimedia" de Qt. C'est un module qui fournit plusieurs classe C++ afin de gérer le contenu audio.
1. Pour créer un lien avec les bibliothèques C++, il faut ajouter cette ligne de code dans le fichier ".pro" du projet :
```cpp
greaterThan(QT_MAJOR_VERSION, 4): QT += multimedia
```
2. Il faut ensuite inclure "QMediaPlaylist" dans la classe souhaité, afin de lire un contenu audio à partir d'une source :
```cpp
#include <QMediaPlaylist>
```
3. Puis, créer une liste de lecture avec nos fichiers audios :
```cpp
QMediaPlaylist *menuMusicPlaylist = new QMediaPlaylist(this);
menuMusicPlaylist->addMedia(QUrl("qrc:/Resources/sounds/menuTitle.wav"));
```
4. Ensuite, nous allons la faire jouer en boucle :
```cpp
menuMusicPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
```
5. Finalement, nous lançons notre liste de lecture :
```cpp
menuMusicPlayer = new QMediaPlayer(this);
menuMusicPlayer->setPlaylist(menuMusicPlaylist);
startMenuMusic();
```
```cpp
void MainWidget::startMenuMusic() {
menuMusicPlayer->play();
}
```
# 5. Tests
|Description|Résultat attendu|Résultat réel|Remarques|
|-|-|-|-|
|Lancer le .exe serveur|OK|OK|Cliquer sur "Démarrer" avant de démarrer les clients, si on souhaite jouer en multijoueur|
|Lancer le .exe jeu|OK|OK|Fonctionne correctement en local et avec le serveur|
|Menu principal - Affichage|OK|OK|La fenêtre s'ajuste bien selon la taille de la fenêtre|
|Menu principal - Musique|OK|OK|La musique tourne bien en boucle|
|Menu principal - Bouton quitter|OK|OK|-|
|Menu principal - Bouton jouer en local|OK|OK|-|
|Menu principal - Champs pour l'adresse serveur et son port|OK|OK|-|
|Menu principal - Bouton jouer en multijoueur|OK|OK|Redirige bien sur la page de la salle d'attente|
|Jeu - Lancement de la partie|OK|OK|-|
|Jeu - Musique|OK|OK|-|
|Jeu - Déplacements joueur|OK|OK|-|
|Jeu - Ramasser bonbons|OK|OK|-|
|Jeu - Création du snake derrière le personnage|OK|OK|La couleur de l'équipe se met bien sur les friandises|
|Jeu - Voler snake|OK|OK|-|
|Jeu - Ramener bonbons à la base|OK|OK|Les bonbons disparaissent bien du tracé derrière le joueur|
|Jeu - Comptabilisation des points|OK|OK|Barre des points en haut de l'écran|
|Jeu - Timer de la partie|OK|OK|La partie se termine à la fin de la musique|
|Fin - Afficher le menu avec le gagnant|OK|OK|Affiche bien la bonne fenêtre suivant l'équipe gagnante|
|Fin - Retour au menu principal ou quitter|OK|OK|-|
# 6. Conclusion
De manière générale, nous sommes satisfaits du résultat que nous avons obtenu. Nous avons atteint tous nos objectifs primaires et nous avons même pu réaliser quelques objectifs secondaires et tertiaires.
Concernant la planification, nous avons mal estimé la durée de nos tâches sur notre Gantt et ne les avons donc pas respectés. Les prochaines fois, il faudrait que l'on prévoie des délais plus longs et même prendre une marge supplémentaire.
Malgré cela, nous avons beaucoup appris quant à la gestion d'un projet de groupe. De plus, ces quatre mois nous ont permis d'acquérir plus d'aisance dans l'utilisation de GitLab, de Qt, du langage C++ et les différents outils que nous avons utilisés.
Grâce aux applications "Discord" et "WhatsApp", nous avons gardé une bonne communication au sein du groupe. Nous avons également effectué des réunions régulièrement afin de présenter aux autres membres du groupe ce que chacun avait réalisé, se répartir les futures tâches et également s'entraider.
Nous avons encore beaucoup d'idées pour ce projet, que nous espérons pouvoir concrétiser par la suite. Plusieurs objectifs secondaires et tertiaires n'ont pas été réalisé, ce sont les améliorations qui nous manquent pour notre projet, mais d'autres idées pourraient également s'y ajouter.
En conclusion, nous retenons l'importance que portent la planification de projet, la répartition des tâches ainsi que la communication au sein de l'équipe pour sa réussite.
# 7. Bibliographie
**Site web :**
Qt
https://doc.qt.io/
https://doc.qt.io/qt-5/qgraphicsview.html
https://doc.qt.io/qt-5/qgraphicsscene.html
https://doc.qt.io/archives/qt-4.8/qgraphicsitem.html
https://doc.qt.io/archives/qt-4.8/qgraphicsitem.html#collidesWithItem
https://doc.qt.io/archives/qt-4.8/containers.html
https://stackoverflow.com/questions/244646/get-elapsed-time-in-qt
https://github.com/MeLikeyCode/QtGameTutorial/blob/master/tutorial8/Bullet.cpp
https://lucidar.me/fr/dev-c-cpp/reading-xml-files-with-qt/
https://github.com/equati0n/Super_Mario
Tiled
https://www.youtube.com/watch?v=ZwaomOYGuYo&start=996s&ab_channel=Gamefromscratch
https://opengameart.org/
Diagramme
https://www.gantt.io/
https://hackmd.io/s/features
https://mermaid-js.github.io/mermaid/#/
**Cours :**
Qt2020v1.pdf
05-qml-animations.pdf
10-graphicsview.pdf
**Application :**
Qt creator
GitLab
HackMD
Paint(.net)
Tiled
Aseprite
Microsoft PowerPoint
Microsoft Whiteboard