Agar.io Simulator

Travail pratique de programmation concurrente

Steven Liatti

Orestis Malaspinas

Vincent Pilloux

Figure 1: Screenshot du jeu
Figure 1: Screenshot du jeu

But du travail

Le but de ce travail pratique est de réaliser une simulation du jeu agar.io. Dans ce jeu 2D dans le navigateur, en réseau, chaque joueur contrôle, à la souris, le déplacement d’une cellule, représentée par un disque de couleur. En début de partie, la cellule apparaît sur le domaine de jeu à une position aléatoire. Ce domaine a des dimensions finies, de sorte qu’une cellule ne peut en “sortir”. La “loi de la nature” s’applique aux cellules : une cellule plus grosse peut “gober” une plus petite. En contrepartie, une cellule plus petite se déplace plus rapidement qu’une grosse. Pour gagner en masse, une cellule doit soit manger des cellules plus petites qu’elle-même, soit manger de la nourriture disséminée sur le domaine de jeu de manière aléatoire. Le but étant bien entendu de manger les autres, rester en vie et d’être la plus grosse cellule au classement. L’objectif du TP est de réaliser une simulation simplifiée du jeu original, comme vu en fig. 1. Les cellules seront contrôlées par ordinateur et il faudra gérer l’affichage, la gestion du clavier et la logique du jeu de manière concurrente.

Règles du simulateur

Relations entre masse, rayon et vitesse des cellules

Pour ne pas faire “grossir” vos cellules trop rapidement, nous vous recommandons de déduire le rayon d’une cellule en fonction de sa masse par la fonction suivante :

\[r = 4 + 6 \sqrt{m}\]\(r\) est le rayon et \(m\) la masse.

De manière analogue, pour obtenir une vitesse inversement proportionnelle, mais lissée, en fonction de la masse, votre professeur de maths favori vous recommande d’utiliser la fonction sigmoïde suivante :

\[s = \alpha + \frac{\beta}{1 + e^{\gamma (m - \delta)}}\]\(s\) est la vitesse et \(m\) la masse.

La fig. 2 illustre cette fonction et ses paramètres.

Figure 2: Graphe de la vitesse en fonction de la masse
Figure 2: Graphe de la vitesse en fonction de la masse

Conditions de collisions

Les trois types de collisions sont les suivants :

Figure 3: Cas de collisions avec les bords
Figure 3: Cas de collisions avec les bords
Figure 4: Cas de non collision et de collision entre deux cellules
Figure 4: Cas de non collision et de collision entre deux cellules

Architecture multi-threadée

Le programme à développer est basé sur une architecture multi-threadée organisée de la manière suivante :

Concernant les collisions, deux variantes sont possibles :

  1. Chaque thread travailleur réalise uniquement la détection de collisions entre cellules et entre cellules et particules de nourriture, puis délègue totalement le traitement de ces collisions à l’unique thread collisionneur.
  2. Chaque thread travailleur détecte toutes les collisions, mais traite les collisions entre cellules et particules de nourriture. L’unique thread collisionneur ne traite en conséquences uniquement que les collisions entre cellules.

Cahier des charges

agario <width> <height> <seed> <food> <dir> <res> <nf> <freq> <workers> <cells>
Exemple : agario 960 600 0 0.001 0.01 0.05 0.1 30 4 10

Informations utiles

Options de compilation

Nous vous conseillons de compiler votre code avec gcc et les options de compilation suivantes :

-g -std=c11 -Wall -Wextra -fsanitize=address -fsanitize=leak -fsanitize=undefined

De plus, si vous utilisez des barrières dans votre code, vous devez inclure cette directive :

#define _GNU_SOURCE

Affichage graphique

Pour réaliser le rendu graphique de votre programme, vous devez utiliser la librairie gfx incluse dans le repository du TP. L’entête de la fonction gfx_drawcircle vous est fournie dans gfx, à vous de l’implémenter.

Gestion des événements aléatoires

Pour obtenir un déroulement identique du simulateur entre différentes exécutions, vous devrez vous servir du seed donné en argument. Dans un contexte multi threads, le combo srand() et rand() n’est pas suffisant, car rand() n’est pas réentrante. Vous devrez donc utiliser à la place rand_r(unsigned int* seed) en lui passant un dérivé par thread de ce seed initial (man rand_r pour plus d’infos). Veillez à ce que chaque thread dispose d’un seed unique et différent.

Mesure du temps d’exécution

Les mesures du temps d’exécution peuvent être effectuées avec la fonction clock_gettime de la librairie <time.h> comme montré ci-dessous :

struct timespec start, finish;
clock_gettime(CLOCK_MONOTONIC, &start);
...  // code à mesurer
clock_gettime(CLOCK_MONOTONIC, &finish);
double elapsed = (finish.tv_sec - start.tv_sec) * 1e6;
elapsed += (finish.tv_nsec - start.tv_nsec) / 1e3;

À noter que selon les versions de gcc, le code ci-dessus requiert la librairie rt. Pour ce faire, passez -lrt à gcc au moment de l’édition des liens.

Schéma du fil d’exécution et de synchronisation

Pour appuyer vos explications à l’oral, vous devrez produire un schéma du fil d’exécution du code et de la synchronisation entre threads, à rendre avec votre code. La fig. 5 illustre un exemple d’un tel schéma. La syntaxe à utiliser est libre, mais les mécanismes de synchronisation doivent clairement apparaître.

Figure 5: Exemple de schéma du fil d’exécution et de synchronisation d’un programme multi threads
Figure 5: Exemple de schéma du fil d’exécution et de synchronisation d’un programme multi threads

Travail à rendre

N’oubliez pas de lire attentivement le document “Consignes Travaux Pratiques.pdf” disponible sur la page CyberLearn du cours. Les consignes supplémentaires et spécifiques pour ce travail sont les suivantes :