Projet C# – Space Invaders
Transcription
Projet C# – Space Invaders
ESIEE Paris C# - Space Invaders Projet 2015 Projet C# – Space Invaders Etape 1: Comprendre la structure d'un programme animé - TP1 Télécharger le mini-projet Visual C# à cette adresse : http://www.esiee.fr/~perretb/E3FI/SpaceInvaders.zip . Ce projet contient un squelette fonctionnel d'un petit programme interactif que vous modifierez et complèterez tout au long du projet. Avant même d’ouvrir ce projet dans Visual Studio, ajoutez tous les fichiers qu’il contient sur votre dépôt SVN et réalisez un commit. Pour le moment, le projet comporte trois classes : GameForm.cs : c'est la classe d'affichage et d'interaction. Elle représente la fenêtre de jeu. Game.cs : c'est elle qui contient toute la mécanique du jeu. Elle centralise les différents éléments du jeu, elle gère leur évolution et leurs interactions. BalleQuiTombe : c'est un objet de "démonstration", 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). Game.cs GameForm.cs - Gestion des évènements - Gestion de l'affichage - Mécanique du jeu : Gestion du temps Gestion des objets du jeu (mouvement, interaction) - Dessin Dessin des objets du jeu Affichage d'information BalleQuiTombe.cs - Mouvement et interaction (position, vitesse, collision) - Dessin Questions de compréhension: Que faut-il modifier pour que la balle tombe plus lentement ou plus rapidement ? Travail de programmation: Ajouter la gestion des touches "flèche gauche" et "flèche droite". Lorsque le joueur appuie sur une des flèches, la balle (si elle existe) doit se déplacer dans la direction indiquée tout en continuant à tomber. Etape 2: Classe utilitaire Vecteur2D - TP1 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, calcul de la norme, ...). Comme .net ne propose pas de structure pour cela (il y a la classe Point mais qui est en coordonnées entières) on va écrire une classe Vecteur2D. Travail de programmation: 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: Benjamin Perret Page 1 sur 9 ESIEE Paris C# - Space Invaders Opérateur Vecteur + Vecteur Vecteur – Vecteur - Vecteur Vecteur * double double * Vecteur Vecteur / double Projet 2015 Description Addition vectorielle Soustraction vectorielle Moins unaire Multiplication par un scalaire à droite Multiplication par un scalaire à gauche Division par un scalaire Etape 3 : La classe Spaceship - TP1 Tous les vaisseaux du jeu, alliés ou ennemis, sont représentés par la même classe : "SpaceShip". La classe SpaceShip est très simple, car par défaut les vaisseaux ne font rien et ils seront donc "piloter" par une autre entité (le joueur ou "l'IA"). Un vaisseau dispose d'un certain nombre de vies et n'est détruit que lorsque toutes ses vies lui ont été otées. Travail de programmation: Ecrivez la classe SpaceShip. Cette classe possède les membres suivants: Membre Type Nom Description Propriété publique Vecteur2D Poisition Position du vaisseau Propriété publique int Lives Nombre de vie du vaisseau Propriété publique en bool Alive Le vaisseau est-il en vie (nombre de vie >0) lecture seule Champ publique Bitmap image Image représentant le vaisseau Constructeur SpaceShip Permet d'initialiser la position, le nombre de vies et l'image du vaisseau. Méthode publique void ( Graphics ) Draw Dessine l'image du vaisseau dans le graphique donné Remarque : des images de vaisseaux 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 (ex: Bitmap image = SpaceInvaders.Properties.Resources.ship3). Pour dessiner une image « image » à la position « x, y », utilisez la méthode DrawImage de l’objet graphics : g.DrawImage(image, x, y). Remarque 2 : La position du vaisseau représente son coin supérieur gauche. Etape 4 : Déplacement du joueur – TP1 Le joueur dispose d'un vaisseau particulier qu'il peut diriger avec les flèches gauche et droite du clavier. Toute la gestion du joueur sera faite dans la classe Game. Travail de programmation: Ajouter un champ public "playerShip" de type "SpaceShip" à la classe Game. Ajouter un champ privé "playerSpeed" de type double qui représente la vitesse de déplacement du joueur en pixel/seconde. Modifier le constructeur de la classe Game de manière à créer un vaisseau avec 5 vies et centré en bas de l'écran au démarrage du jeu (choisissez une des images de vaisseau pour le représenter). Modifier la méthode Draw pour que le vaisseau soit correctement affiché. Benjamin Perret Page 2 sur 9 ESIEE Paris C# - Space Invaders Projet 2015 Modifier la méthode Update pour qu'un appui sur la touche gauche (resp. droite) déplace le vaisseau sur la gauche (resp. droite) en respectant la vitesse définie dans le champ "playerSpeed". Assurez-vous que le vaisseau ne puisse pas sortir de la zone de jeu ! Etape 5 : La classe Missile – TP2 La classe Missile représente un missile ami ou ennemi. Un missile dispose d'un nombre de vies et d'une vitesse fixée à sa création. Travail de programmation: Ecrivez la classe Missile. Cette classe possède les membres suivants: Membre Type Nom Description Propriété publique Vecteur2D Position Position du missile Propriété publique Vecteur2D Vitesse Vitesse du missile Propriété publique int Lives Nombre de vies du missile Propriété publique en bool Alive Le missile est-il en vie (nombre de vies >0) lecture seule Champ publique Bitmap image Image représentant le missile Constructeur Missile Permet d'initialiser la position, la vitesse et le nombre de vies. L'image est toujours l'image "shoot" disponible dans les ressources du projet. Méthode publique void ( Graphics ) Draw Dessine l'image du missile dans le graphique donné Méthode publique void (double ) Move Fait évoluer la position du missile en fontion du temps écoulé (en paramètre) et de sa vitesse Etape 6 : Tir du joueur – TP2 Lorsque le joueur appuie sur la touche espace, son vaisseau doit tirer un missile sauf si un missile a déjà été tiré et est toujours en vie. Travail de programmation: Ajouter un champ "playerMissile" de type Missile à la classe Game. Modifier la méthode Draw de la classe Game de manière à afficher correctement le missile. Modifier la méthode Update de la classe Game: o Il faut créer un nouveau missile juste au-dessus du vaisseau du joueur si le joueur appuie sur espace et qu'il n'y actuellement pas de missile. o Faire avancer le missile. o Détruire le missile s'il sort de la zone de jeu ou si il n'est plus en vie. Etape 7 : Gestion de l'option Pause – TP2 La touche p doit permettre de mettre le jeu en pause puis de revenir dans l'état initial. Travail de programmation: Mettre en place le système de machine à état. Un jeu vidéo est un exemple classique de machine à état (ou automatique fini) qui peut être dans différents états de jeu qui conduiront à des comportements différents (en jeu, en pause, menu de départ, ...). On va représenter les différents états du jeu par une énumération (lire la ressource http://www.esiee.fr/~perretb/E3FI/Enumeration.pdf). Déclarer une énumération GameState contenant les valeurs "Play" et "Pause" dans la classe Game (d'autres états seront ajoutés par la Benjamin Perret Page 3 sur 9 ESIEE Paris C# - Space Invaders Projet 2015 suite) et ajouter un champ de type GameState qui représentera l'état courant. Modifiez la fonction Draw de manière à afficher le texte "Pause" si le jeu est en pause et la balle si le jeu est dans l'état Play. Modifiez la fonction update de manière à gérer ce nouvel état : o Si le jeu est dans l'état Play et que le joueur appuie sur la touche p, il doit passer en état pause et le chronomètre doit être arrêté. o Si le jeu est dans l'état Pause et que le joueur appuie sur la touche p, le jeu doit redémarrer. p Play Pause p Affiche et anime les éléments du jeu. Affiche le message "pause". Le déroulement du temps est stoppé Etape 8 : La classe Bunker – TP3 Les bunkers permettent au joueur de se mettre à couvert mais peuvent être détruit par les missiles. Les bunkers sont fixes. Travail de programmation: Ecrivez la classe Bunker. Cette classe possède les membres suivants: Membre Type Nom Description Propriété publique Vecteur2D Poisition Position du bunker Champ publique Bitmap image Image représentant le bunker Constructeur Bunker Permet d'initialiser la position du bunker. L'image est toujours l'image "bunker" disponible dans les ressources du projet. Méthode publique void ( Graphics ) Draw Dessine l'image du bunker dans le graphique donné Méthode publique bool (Missile ) Collision Test la collision (cf. ci-dessous) avec un missile et détruit une partie du bunker en cas de collision. Test de collision avec un missile: Pour des raisons de performances, le test est effectué en 2 temps. On commence par tester si le rectangle englobant du bunker 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 englobant : o les rectangles englobants sont disjoints : aucune collision possible: o les rectangles englobants s'intersectent : on ne peut pas conclure: Benjamin Perret Page 4 sur 9 ESIEE Paris C# - Space Invaders Projet 2015 Pour tester si deux rectangles s'intersectent, on peut vérifier la condition suivante. On considère qu'un rectangle est paramétré de la façon suivante: on dispose de deux rectangles (x1,y1,lx1,ly1) et (x2,y2,lx2,ly2), les deux rectangles sont disjoints si o x2 > x1 + lx1 : le deuxième rectangle est à droite du premier OU o y2 > y1 + ly1 : le deuxième rectangle est haut dessus du premier OU o x1 > x2 + lx2 : le premier rectangle est à droite du deuxième OU o y1 > y2 + ly2 : le premier rectangle est haut dessus du deuxième 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 (on connait la position du missile et du bunker 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). Si le pixel de l'image du bunker qui correspond est noir, il y a collision au niveau de ce pixel. On supprime alors l'ensemble des pixels en collisions du bunker et on diminue le nombre de vie du missile de 1. repère missile repère écran repère bunker Il faut pouvoir passer de coordonnées dans le repère "missile" aux coordonnées dans le repère "bunker" sachant que l'on connait la position des repères "missile" et "bunker" par rapport au repère "écran". Le changement de repère, lorsque les axes sont alignés, est une opération mathématique très simple. Imaginons que l'on dispose de 2 repères. On connait la position du deuxième repère par rapport au premier (Ox,Oy). On connait la position d'un point P dans le second repère (Px',Py') et on souhaite obtenir les coordonnées de P dans le premier repère (Px,Py). On a directement Px=Px'+Ox et Py=Py'+Oy . Benjamin Perret Page 5 sur 9 ESIEE Paris C# - Space Invaders Projet 2015 Remarque : 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 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). Etape 9 : Intégration des bunkers – TP3 Il faut maintenant ajouter la gestion des bunkers dans la classe Game. Travail de programmation: Ajoutez un champ "bunkers" de type List<Bunker> à la classe Game. Modifier le constructeur de Game pour qu'il crée automatiquement 3 ou 4 bunkers également repartis sur une ligne au-dessus de la position du joueur et les ajoute à la liste des bunkers. Modifier la méthode Draw de la classe Game de manière à afficher correctement les bunkers présents dans liste. Modifier la méthode Update de la classe Game : il faut tester si le missile du joueur est collision avec le bunker. Etape 10 : Bloc d'ennemis, construction et déplacement – TP4 Les ennemis de Space Invaders ont un comportement assez particulier : ils arrivent par ligne de vaisseaux identiques et ils se déplacent de manière synchronisée. Ainsi, dès qu'un vaisseau atteint le bord de l'écran, c'est l'ensemble des ennemis qui descendent et inversent leur direction de déplacement horizontale. En fait on peut observer que c'est le rectangle englobant de l'ensemble des vaisseaux qui importe. Travail de programmation: Ecriver la classe EnnemyBlock. Cette classe possède les membres suivants: Membre Type Nom Description Propriété publique List<SpaceShip> Ships Liste des vaisseaux ennemis Champ publique Vector position Coordonnées du coin supérieur gauche Champ publique Size size Taille du rectangle englobant les vaisseaux Benjamin Perret Page 6 sur 9 ESIEE Paris C# - Space Invaders Propriété publique en lecture seule Champ privé Champ privé Constructeur publique bool Méthode publique void( int width, int nbShips, int lives, Bitmap im) Méthode publique Méthode publique void (Graphics) void(double deltaT) Projet 2015 Alive Vector Vector Vraie si il y a au moins un vaisseau vivant dans le bloc. speedX Vitesse de déplacement latérale speedY Vitesse de déplacement horizontale EnnemyBlock Initialise un nouveau block à une position donnée. A ce moment le bloc est vide, sa taille est donc nulle. AddLine Ajoute une ligne de vaisseaux en bas du bloc. La ligne est composée de nbShips vaisseaux ayant lives vies, l'image im et également répartis sur la largeur width. Draw Dessine tous les vaisseaux du bloc. Move Déplace tous les vaisseaux de speedX. Si le rectangle englobant des vaisseaux arrive au bord de la zone de jeu alors il faut : 1) inverser speedX, 2) déplacer tous les vaisseaux de speedy, 3) augmenter le module de speedX. Etape 11 : Intégration du bloc d'ennemis – TP4 Il faut maintenant ajouter la gestion des ennemis dans la classe Game. Travail de programmation: Ajouter un champ public de type EnnemyBlock à la classe Game. Modifier le constructeur de Game, pour initialiser le bloc d'ennemis (ajouter au moins 4 lignes d'ennemis). Modifier la méthode Draw de Game pour afficher le bloc d'ennemis. Modifier la méthode Update de Game pour gérer le déplacement du bloc d'ennemis. Etape 12 : Bloc d'ennemis : jeu gagné et jeu perdu ! – TP4 Maintenant que l'on dispose de la notion d'ennemis (on ne peut pas encore les détruire mais cela va venir !), on peut ajouter les états de jeu "Perdu" et "Gagné". Travail de programmation: Ajouter les valeurs "Lost" et "Win" à l'énumération GameState Modifier la méthode Draw de la classe Game pour afficher un message indiquant le résultat lorsque l'état du jeu est Win ou Lost. Modifier la méthode Update de la classe Game pour gérer les changements d'états. Le jeu doit passer dans l'état "Lost" si le bloc d'ennemis a atteint le niveau du joueur ou si le nombre de vie du joueur est inférieur ou égal à zéro. Le jeu doit passer dans l'état Win si le bloc d'ennemis est vide. p Play Nombre d'ennemis = 0 Win Benjamin Perret Pause p Nombre de vies = 0 OU Les ennemis ont atteint le niveau du joueur Lost Page 7 sur 9 ESIEE Paris C# - Space Invaders Projet 2015 Etape 13 : Bloc d'ennemis : collisions - TP5 Nous allons maintenant ajouter les méthodes nécessaires pour pouvoir détruire les ennemis. Il faut pour cela : implémenter les collisions entre un vaisseau et un missile et ajouter les codes de gestions. Travail de programmation: Ajouter une méthode public bool Collision(Missile s) à la classe SpaceShip. Cette méthode est assez proche de la méthode Collision de la classe Bunker mais cette fois, l'image du vaisseau n'est pas modifiée en cas de collision. En cas de collision, les nombres de vies du vaisseau et du missile sont diminués du minimum entre le nombre de vie du vaisseau et du missile. Donc, si le vaisseau possède plus de vie que le missile il n'est pas détruit et l'inverse est également vrai ! Ajouter une méthode private void UpdateBBox() à la classe EnnemyBlock qui permet de recalculer la position et la dimension du bloc d'ennemis en fonction des vaisseaux ennemis présents dans le bloc. Cette fonction sera utilisée pour mettre à jour ces valeurs après la destruction d'un vaisseau. Ajouter une méthode public bool Collision(Missile s) à la classe EnnemyBlock. Cette méthode effectue le test de collision entre le missile et les ennemis du bloc. Si un ennemi est détruit il est supprimé du bloc et les dimensions du bloc sont mises à jour. Modifier la méthode Update de la classe Game de manière à effectuer les tests de collision entre le missile du joueur et le bloc d'ennemis. Si le missile est détruit lors de la collision il faut le supprimer. Etape 14 : Bloc d'ennemis : attaques – TP5 Dans Space Invaders, les ennemis tirent aléatoirement. Un tir ennemi peut endommager un bunker ou le vaisseau du joueur mais pas un autre vaisseau ennemi. Plus le bloc de vaisseau se rapproche du joueur, plus la fréquence de tir est élevée. Travail de programmation: Ajouter un champ de type List<Missile> dans la classe Game et initialiser ce champs avec une liste vide. Modifier la méthode Draw de la classe Game de manière à dessiner les missiles dans la liste. Modifier la méthode Update de la classe Game et ajouter: o le déplacement des missiles de la liste; o la collision des missiles avec les bunkers et le vaisseau du joueur; o la suppression d'un missile de la liste si son nombre de vies vaut 0 ou si il sort de l'écran. Ajouter un champ ShootProbability de type double à la classe EnnemyBloc. Ce champ représente la probabilité qu'un vaisseau tire pendant un intervalle de temps. Par exemple, une valeur de 0,1 signifie que chaque vaisseau tire en moyenne 0,1 fois par seconde, c'est-à-dire 1 fois toutes les 10 secondes. Cette probabilité doit augmenter à chaque fois que le bloc d'ennemis descend. Ajouter une méthode void RandomShoot(Ship ship, double deltaT) à la classe EnnemyBloc qui a pour rôle de générer aléatoirement un tire pour le vaisseau passé en paramètre en fonction du temps écoulé deltaT et la probabilité de tir ShootProbability. On procède de la manière suivante : o On tire au hasard un nombre entre 0 et 1 avec la classe Random o Si ce nombre est inférieur à deltaT multiplié par ShootProbability alors on crée un objet Missile sous le vaisseau concerné et on l'ajoute à la liste des missiles de l'objet Game. Modifier la méthode Update de la classe EnnemyBloc pour appeler la méthode RandomShoot sur chacun des vaisseaux du bloc. Benjamin Perret Page 8 sur 9 ESIEE Paris C# - Space Invaders Etape 15 : Addons Ajoutez des addons (cf. sujet de projet) Benjamin Perret Page 9 sur 9 Projet 2015