Projet - Space Invaders

Le but de ce projet est de reproduire un des grands classiques du jeu vidéo : Space Invaders ! Space Invaders est un shoot’em up où le principe est de détruire des vagues d’aliens au moyen d’un canon en se déplaçant horizontalement sur l’écran.

Travail demandé et évaluation

L’évaluation porte sur le travail personnel de l’étudiant évalué en deux étapes:

  • une évaluation du projet de base (implémentation des éléments fondamentaux du cahier des charges) réalisée impérativement pendant les heures de cours par un intervenant qui pourra vous poser des questions;

  • une évaluation finale du projet complet (avec les éléments complémentaires) réalisée hors ligne à partir du code rendu et d’une vidéo de démonstration.

Le barême ci-dessous est donné à titre indicatif et peut évoluer.

La notation du projet de base diffère suivant le parcours choisi (voir fin de la section Démarrage):

  • Piste verte et piste bleue donnent un maximum de 12 points sur 20.

  • Piste noire: donnent un maximum de 15 points sur 20.

L’évaluation finale du projet complet est réalisée uniquement si le projet de base a été validé par un intervenant pendant le cours. Elle porte sur les éléments complémentaires du cahier des charges (addons) et sur la qualité de code.

La note finale du projet en additionnant la note du projet de base et la note du projet complet, avec un maximum de 20 points.

Un projet non validé pendant les heures de cours ne pourra pas être évalué hors ligne et sera donc noté 0/20.

Modalité de rendu pour la version finale

Votre dêpot Git (voir ci-dessous) devra contenir les éléments suivants:

  • le code source de votre projet,

  • un fichier texte (README.md) contenant la liste des éléments complémentaires que vous avez implémentés,

  • une lien vers une vidéo de démonstration de 3 minutes maximum montrant les différentes éléments complémentaires implémentés.

La date limite de rendu sera précisée par mail.

Il est fortement recommandé de réaliser un clone de votre dépôt avant la date limite et sur une autre machine que celle que vous avez utilisée pendant vos développements afin de vérifier que celui-ci est complet, que votre projet compile et s’exécute correctement.

Tout évènement menant à un retard de rendu (par exemple dépôt Git incomplet à la date limite) entrainera une pénalité de \(2^{n-1}\) points (avec :math:`n` le nombre de jours de retard).

Important

Utilisation d’IA générative : l’utilisation d’IA générative est autorisée mais vous devez être en mesure d’expliquer le fonctionnement de tout le code produit. Si vous ne savez pas expliquer le fonctionnement d’une partie de votre code, vous n’aurez pas les points correspondants.

Plagiat : La récupération de tout ou portion du travail d’une tierce personne, qu’il s’agisse d’un étudiant, d’un site internet, d’une vidéo ou tout autre support, n’est permise qu’avec la permission de l’auteur, ce dernier devant être cité dans le code et dans le rapport. Le travail utilisé avec citation des sources n’est pas considéré comme le résultat du travail de l’étudiant et ne peut donc pas apporté de point lors de la notation. Un travail utilisé sans référence à son auteur constitue un plagiat. Le plagiat est interdit par le règlement intérieur de l’école (Article 3). En cas de plagiat, le conseil de discipline peut prononcer des sanctions allant jusqu’à l’exclusion définitive de l’étudiant.

Démarrage

Prise en main de Git

Les informations vous permettant de créer un dépôt Github pré-initialisé et rattaché à cette unité vous serons envoyées par mail.

Si vous ne connaissez pas Git, suivez le guide: Git .

Comprendre la structure d’un programme animé

Votre dépôt Git contient déjà un squelette fonctionnel d’un programme interactif que vous modifierez et complèterez.

Pour le moment, le projet comporte trois classes importantes :

  • Game.cs : c’est elle qui contient les mécanismes fondamentaux du jeu.

  • GameObject.cs : c’est une entité abstraite pour représenter un objet actif du jeux.

  • BalleQuiTombe.cs : c’est un objet de « démonstration » qui hérite de GameObject, il s’agit d’une balle qui ne fait que tomber. Elle est capable de calculer l’évolution de sa position en fonction du temps qui passe. Elle sait également se représenter (dessiner l’image qui la représente).

../_images/cd1.png

Démarrez le jeux et appuyez sur la touche Espace : trouvez les portions de code qui sont responsables des actions suivantes:

  • Création de l’objet BalleQuiTombe lors de l’appui sur espace.

  • Déplacement de l’objet vers le bas.

  • Destruction de l’objet lorsqu’il arrive en bas de la fenêtre.

Questions de compréhension:

  • Que faut-il modifier pour que la balle tombe plus lentement ou plus rapidement ?

  • Ajoutez la gestion des touches « flèche gauche » et « flèche droite » dans la méthode Update de la classe BalleQuiTombe. Lorsque le joueur appuie sur une des flèches, la (ou les) balle qui existe doit se déplacer dans la direction indiquée tout en continuant à tomber.

Classe utilitaire Vecteur2D

Afin de faciliter la gestion des coordonnées et des déplacements, il est nécessaire de disposer d’un type permettant de représenter des points et des vecteurs du plan et de réaliser des opérations basiques (addition, multiplication par un scalaire…). Comme .net ne propose pas de structure pour cela (il y a la classe Point mais qui est en coordonnées entières) nous allons écrire une classe Vecteur2D:

  • Créez une classe Vecteur2D avec deux champs x et y de type double.

  • Créez un constructeur paramétrique permettant d’initialiser les valeurs de x et de y. Mettez la valeur par défaut 0 sur les deux paramètres.

  • Ajoutez une propriété publique Norme de type double en lecture seule qui retourne la norme du vecteur.

  • Redéfinissez les opérateurs suivants:

    • Vecteur2D + Vecteur2D: Addition vectorielle

    • Vecteur2D - Vecteur2D: Soustraction vectorielle

    • -Vecteur2D : Moins unaire

    • Vecteur2D * double: Multiplication par un scalaire à droite

    • double * Vecteur2D: Multiplication par un scalaire à gauche

    • Vecteur2D / double : Division par un scalaire

Suite

Vous poursuivez l’implémentation selon le cahier des charges donné ci-dessous avec 3 parcours possibles:

  • 👶 Piste verte : Vous débutez en programmation orientée objet; suivez le guide étape par étape Space Invaders - Piste Verte .

  • 😎 Piste bleue : Vous vous sentez à l’aise en programmation orientée objet; structurez vous-même votre logiciel.

  • 🐱‍👤 Piste noire : Vous maitrisez la programmation orientée objet; pour un peu de défi essayez-vous au modèle Entité-Composant-Système .

Cahier des charges fonctionnel

Éléments fondamentaux

L’interface est composée des éléments suivants:

  • le vaisseau du joueur;

  • une rangée de bunker;

  • les vaisseaux ennemis;

  • les missiles;

  • les vaisseaux, les bunkers et les missiles sont représentés à l’écran par des sprites/bitmap.

  • le nombre de vies restantes du joueur.

../_images/ihm.svg

Déplacement du joueur :

  • Le vaisseau du joueur se déplace uniquement horizontalement.

  • Le vaisseau du joueur ne peut pas sortir de l’écran.

  • Le déplacement se fait au moyen des touches flèche gauche et flèche droite.

Bloc d’ennemis :

  • Les ennemis sont organisés en un bloc.

  • Le bloc est constitué de plusieurs lignes d’ennemis.

  • Chaque ligne d’ennemis comprend un ou plusieurs vaisseaux du même type.

  • Le bloc arrive par le haut de la fenêtre.

  • Le bloc se déplace alternativement de gauche à droite et de droite à gauche.

  • Le bloc change de direction dès qu’un des vaisseaux du bloc arrive au bord de l’écran.

  • Lorsque le bloc change de direction, il se décale vers le bas.

  • Lorsque le bloc change de direction, sa vitesse de déplacement augmente.

Missiles :

  • Les missiles se déplacent en ligne droite verticale: de haut en bas si tiré par un ennemis et de bas en haut si c’est un missile du joueur.

  • Les missiles des ennemis peuvent entrer en collision avec les bunkers, le vaisseau du joueur et le missile du joueur (pas de friendly fire).

  • Les missiles du joueur peuvent entrer en collision avec les bunkers, les vaisseaux ennemis et les missiles des ennemis.

  • Les missiles sont détruits si ils sortent de l’écran.

  • Le joueur tire avec la touche espace.

  • Les ennemis tirent aléatoirement.

  • La fréquence de tir des ennemis augmente lorsqu’ils se rapprochent du bas de l’écran.

  • Un vaisseau ne peut tirer qu’un seul missile à la fois: lorsqu’il tire il doit attendre que le missile soit détruit par une collision ou sorte de l’écran avant de pouvoir tirer à nouveau.

Vie et mort :

  • Le joueur, les ennemis et les missiles possèdent chacun un nombre de vies.

  • Un élément dont le nombre de vie est inférieur ou égal à zéro est détruit.

Collision missile-vaisseau :

  • Un missile est en collision avec un vaisseau si un pixel noir de son sprite est sur un pixel noir du sprite du vaisseau.

  • Lorsqu’un missile touche un vaisseau, les nombres de vies du missile et du vaisseau sont décrémentés de la valeur minimum entre le nombre de vies du missile et du vaisseau.

  • Tous les pixels en collision sont retirés du sprite du vaisseau: ils deviennent transparents et ne peuvent plus participer à des collisions.

Collision missile-bunker :

  • Un missile est en collision avec un bunker si un pixel noir de son sprite est sur un pixel noir du sprite du vaisseau: un tel pixel est dit en collision.

  • Tous les pixels en collision sont retirés du sprite du bunker: ils deviennent transparents et ne peuvent plus participer à des collisions.

  • Le nombre de vie du missile est décrémenté du nombre de pixels en collision.

Collision missile-missile :

  • Deux missiles qui entrent en collision se détruisent mutuellement (ne dépent pas du nombre de vie des missiles).

Victoire et défaite, la première des conditions suivantes qui survient met fin à la partie:

  • Le joueur gagne si il détruit tous les vaisseaux ennemis.

  • Le joueur perd si il est détruit.

  • Le joueur perd si le bloc d’ennemis atteint la hauteur du joueur.

  • A la fin de la partie, le joueur peut relancer une nouvelle partie en appuyant sur Espace

Pause :

  • Tant que la partie n’est pas terminée le joueur peut mettre le jeu en pause: pendant la pause tous les déplacements et donc toute la dynamique du jeu est interrompue.

  • En cours de jeu, le joueur passe en pause avec la touche P.

  • Lorsque le jeu est en pause, le joueur peut quitter la pause avec la même touche P.

Addons

Les addons seront pris en compte uniquement si tous les éléments fondamentaux fonctionnent et ont été validés par un intervenant pendant les heures de cours. Vous trouverez ci-dessous une liste d’addons possibles, vous pouvez également proposer vos propres addons (n’hésitez pas à en discuter avec les intervenants pour vérifier que votre idée est pertinente et réalisable dans le temps imparti).

Bonus :

  • La destruction d’un vaisseau ennemis a une probabilité de faire apparaitre un bonus.

  • Le bonus est représenté par un sprite et de déplace du haut vers le bas.

  • Si le vaisseau du joueur entre en collision avec le bonus il le récupère.

  • Les bonus peuvent être de différentes natures: invincibilité temporaire, missile indestructible, missile auto-guidé, tire multiple, vie bonusldots

Affichage avancé :

  • Les sprites des différents éléments sont animés.

  • Les actions de collisions génèrent des effets visuels (par exemple, explosion).

Son :

  • Le jeu est accompagné d’une musique en fond sonore.

  • Les différentes actions produisent l’émission de sons spécifiques.

Système de niveaux :

  • Le jeu comporte plusieurs niveaux.

  • Les niveaux sont codés dans des fichiers textes éditables.

  • Le jeu propose un système de high score.

Multi-joueurs :

  • Plusieurs joueurs peuvent jouer simultanément sur la même machine.

  • Plusieurs joueurs peuvent jouer simultanément en réseau.

Qualité de code

Environnement technique :

  • Le logiciel doit être compilable et exécutable sous la forme d’une solution Visual Studio sur les machines de TP de l’école.

  • Le logiciel doit être intégralement écrit en C#.

Structuration :

  • Le logiciel doit être structuré en utilisant les techniques de programmation orientée objets.

  • Les éléments de langages vus en cours doivent être privilégié dès que cela est pertinent.

  • Chaque classe doit correspondre à un groupe de composants bien identifié.

  • Le rôle de chaque composant doit être clairement identifié.

  • Tout ce qui correspond au rôle d’un composant doit être regroupé dans la classe le représentant.

Norme de codage :

  • Le code doit être correctement formaté selon les conventions de codage recommandées pour le C#

  • Une fonction ne doit pas contenir plus de 10 lignes de codes.

  • On respectera le principe DRY: la duplication de code est proscrite.

Explications et commentaires :

  • Tous les identificateurs, à l’exception des variables de boucles, doivent avoir un nom explicite qui permet de comprendre leur utilité.

  • Toutes les méthodes doivent être documentées: rôle de la méthode, description des paramètres, description de la valeur de retour (voir les conventions officielles ).

Gestion de sources :

  • Le dépôt sur Github ne devra pas contenir de résultats de compilation (fichiers exécutables, fichiers objets, informations de débogageldots).

Aides et ressources

Gestions des sprites

Des images de vaisseaux/bunker/missile sont déjà incluses dans le projet SpaceInvaders (Menu Projet -> Propriétés -> Ressources -> Images). Ces images sont incluses dans l’exécutable généré à partir de la solution et sont accessibles directement depuis le code:

Bitmap image = SpaceInvaders.Properties.Resources.ship3;

Pour dessiner une image image à la position positionX, positionY, utilisez la méthode DrawImage de l’objet graphics.

g.DrawImage(image, positionX, positionY, image.Width, image.Height);

Note

En précisant manuellement la largeur et la hauteur de l’image, on s’assure que celle-ci est dessinée à sa taille originale. Dans le cas contraire, la méthode DrawImage peut redimensionner l’image pour l’adapter à la résolution de l’écran, ce qui peut poser des problèmes pour les tests de collision pixel à pixel.

Diagramme d’état du jeu

Le jeu peut être dans 4 états différents: en cours, en pause, gagné, perdu. Le diagramme d’état indiquant les transitions possibles est le suivant:

../_images/etats.svg

Test de collision

Le test de collision entre 2 objets (missile/bunker ou missile/vaisseau) fait intervenir le calcul de l’intersection entre deux sprites situés à des positions différentes. Pour des raisons de performances, le test est effectué en 2 temps. On commence par tester si le rectangle englobant du bunker/vaisseau intersecte le rectangle englobant du missile. Si ce n’est pas le cas, on sait qu’il n’y a pas de collision possible, sinon il faut tester plus précisément.

Test des rectangles englobants

  • les rectangles englobants sont disjoints; aucune collision possible:

  • les rectangles englobant s’intersectent; on ne peut pas conclure:

Pour tester si deux rectangles s’intersectent, on considère qu’un rectangle est paramétré de la façon suivante:

../_images/rectangle.png

Si on dispose de deux rectangles \((x_1,y_1,lx_1,ly_1)\) et \((x_2,y_2,lx_2,ly_2)\), les deux rectangles sont disjoints si

  • \(x_2 > x_1 + lx_1\) : le deuxième rectangle est à droite du premier OU

  • \(y_2 > y_1 + ly_1\) : le deuxième rectangle est haut dessus du premier OU

  • \(x_1 > x_2 + lx_2\) : le premier rectangle est à droite du deuxième OU

  • \(y_1 > y_2 + ly_2\) : le premier rectangle est haut dessus du deuxième.

Test pixel à pixel

Si les rectangles englobant s’intersectent, il faut tester pixel par pixel si il y a une intersection. On parcourt l’ensemble des pixels du missile et on calcule la position correspondante sur le bunker/vaisseau (on connait la position du missile et du bunker/vaisseau par rapport au coin supérieur gauche de la fenêtre, et on connait les coordonnées du pixel par rapport à la position du missile, on peut donc en déduire les coordonnées du pixel par rapport à la position du bunker/vaisseau). Si le pixel de l’image du bunker/vaisseau qui correspond est noir, il y a collision au niveau de ce pixel.

../_images/reperes.png

Le changement de repère, lorsque les axes sont alignés, est une opération simple. Imaginons que l’on dispose de 2 repères. On connait la position du deuxième repère par rapport au premier \((O_x,O_y)\). On connait la position d’un point \(P\) dans le second repère \((P_{x'},P_{y'})\) et on souhaite obtenir les coordonnées de \(P\) dans le premier repère \((P_x,P_y)\). On a directement \(P_x=P_{x'}+O_x\) et \(P_y=P_{y'}+O_y\).

../_images/repere.png

Dans le cas qui nous intéresse il sera nécessaire d’effectuer deux changements de repères consécutifs: 1) du repère missile vers le repère écran, et 2) du repère écran vers le repère vaisseau/bunker.

Gestion des couleurs

Les images fournies avec le projet possèdent une couche alpha pour gérer la transparence. Les pixels noirs de l’image correspondent à la couleur (255,0,0,0): du noir (0 pour les 3 composantes rouge, verte et bleue) opaque (255 sur la composante alpha) alors que les pixels en dehors du bunker/vaisseau/missile sont représentés par la couleur (0,255,255,255): du blanc (255 pour les 3 composantes rouge, verte et bleue) transparent (0 sur la composante alpha).