Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Exercice pratique : ASTEROIDS

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par Monsieur Spi, le 17 décembre 2012

Bonjour,

Aujourd'hui nous allons nous entraîner sur un Asteroids, un jeu que j'ai bien envie de placer au niveau 2 de difficulté pour ma petite série d'exercices, en effet, à l'instar du Space Invader que nous avons vus précédemment, on sort ici des sentiers battus des jeux de réflexion simples qui fonctionnent avec des grilles et on s'attaque à un jeu d'arcade proprement dit.

Tout d'abord le résultat :

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Les sources sont disponibles en fin d'exercice.

Etude préliminaire

Tout d'abord le ASTEROIDS c'est quoi ? (merci Wikipedia)

Asteroids est un shoot them up multidirectionnel au concept simple. Le joueur contrôle un vaisseau spatial, représenté en vue de dessus et confronté à des champs d'astéroïdes et des soucoupes volantes. Le but est de survivre le plus longtemps possible en détruisant les astéroïdes et les soucoupes pour engranger les points. L'aire de jeu est intégralement représenté à l'écran; lorsqu'un élément sort du cadre de l'écran, il réapparaît du côté opposé (idem pour le vaisseau du joueur). La borne d'arcade présente cinq boutons : deux servent à faire pivoter le vaisseau à gauche et à droite, un autre sert accélérer, un autre à déclencher les tirs et le dernier à se téléporter aléatoirement sur l'aire de jeu (à ses risques et périls).

Lorsqu'un astéroïde est touché par un tir, il se divise en deux blocs de taille moyenne, lesquels peuvent à leur tour être divisés en deux plus petits blocs, lesquels peuvent enfin être détruits. Le programme original ne peut cependant afficher que 26 astéroïdes simultanément à l'écran (au-delà les astéroïdes ne se divisent donc plus et se contentent de réduire en taille). Une fois tous les astéroïdes détruits, le joueur passe au niveau suivant : le nombre d'astéroïdes présent au début du niveau varie de quatre (pour le 1er) à onze (pour le 5e et tous les suivants). Le nombre de points rapportés par la destruction d'un élément est dépendante de sa taille. Les soucoupes volantes sont de deux tailles : la plus imposante à la capacité de tirer.

Asteroids présente des graphismes vectoriels en noir et blanc, et une physique étudiée (inertie du vaisseau, dislocation des astéroïdes). Il propose aussi un système de highscore innovant qui permet aux dix meilleurs joueurs d'enregistrer leurs initiales. Asteroids a été conçu par Lyle Rains et programmé par Ed Logg (Centipede, Gauntlet, XYbots). Howard Delman s'est occupé de la partie sonore et du Vector generator display system. À l'origine du projet, l'objectif est de proposer une variation de Space Wars (1977) de Cinematronics, les astéroïdes constituant juste une animation d'arrière-plan. Lyle Rains a suggéré à Ed Logg l'idée que les joueurs apprécieraient de tirer sur des blocs de roches pour les exploser en de plus petits morceaux.

Le 5 avril 2010 à 22 heures et 18 minutes (heure du Pacifique), l’Américain John McAllister battit le record du monde pour le plus haut total de points accumulés lors d’une partie de ce jeu sur borne arcade. Son pointage de 41 338 740 surpasse de justesse l’ancienne marque de 41 336 440 réalisée en 1982 par Scott Safran. Il aurait ainsi détrôné le plus vieux record de l’histoire des jeux vidéo encore en vigueur.

Nous avons là tous les éléments pour essayer d'en faire un petit clone, cependant pour l'exercice je ne vais pas reprendre les soucoupes, ni l'aspect vectoriel. Notez qu'on aborde ici un type de jeu qui n'ont pas de fin et que le joueur ne pourra jamais “finir”, le but est simplement de faire le meilleur score et de tenter de battre le record. Ce qui nous intéresse ici c'est la mécanique, j'ai donc volontairement omis tout ce qui traite du calcul du score etc….. que je vous laisse gérer à votre envie.

Les pré-requis

Je vous recommande fortement d'avoir au moins lu les exercices suivants : DEMINEUR, PONG, SNAKE, TAQUIN
Les exercices sont courts mais de nombreuses astuces y sont proposées, je ne les expliquerai pas à chaque nouvel exercice.

Pour ce programme vous devez connaître :

Si vous souhaitez plus de précisions sur ces points, je vous encourage à parcourir le Wiki de Mediabox où vous trouverez de nombreux tutoriaux détaillés.

Le code

Voyons d'abord tout le code d'un coup.

var vies:int =				3;
var tempoTir:int =			0;
var vitesseArme:int =			2;
var nbrAsteroide:int =			0;
var asteroidesDetruits:int =		0;
var direction:Number =			0;
var W:int = 				stage.stageWidth;
var H:int = 				stage.stageHeight;
var avancer:Boolean =			false;
var tourner:Number =			0;
var tirer:Boolean =			false;
 
// conteneurs
var fond:MovieClip = 			new Fond();
var vaisseau:MovieClip = 		new Vaisseau();
var bruitages:SoundChannel;		
var grosseExplosion:Sound = 		new GrosseExplosion();
var moyenneExplosion:Sound = 		new MoyenneExplosion();
var petiteExplosion:Sound = 		new PetiteExplosion();
 
// tableaux
var stockTirs:Array;
var stockEnnemis:Array;
 
addChild(fond);
 
var panneaux:Panneaux = new Panneaux();
panneaux.addEventListener(MouseEvent.MOUSE_DOWN, init);
panneaux.buttonMode = true;
addChild(panneaux);
 
function init(e:MouseEvent):void {
 
	while(numChildren>1) removeChildAt(1);
 
	stockTirs = 			[];
	stockEnnemis = 			[];
 
	addChild(vaisseau);
	avancer =			false;
	tirer =				false;
	tourner =			0;
	vies = 				3;
	vaisseau.y = 			H*.5;
	vaisseau.x = 			W*.5;
	vaisseau.vx =			0;
	vaisseau.vy =			0;
	vaisseau.vitesse = 		0.3;
	vaisseau.friction = 		0.95;
	vaisseau.rotation = 		90;
	vaisseau.invincible = 		50;
	vaisseau.cat = 			100;
	stage.addEventListener(KeyboardEvent.KEY_DOWN, appuie);
	stage.addEventListener(KeyboardEvent.KEY_UP, relache);
	stage.addEventListener(Event.ENTER_FRAME, main);
	stage.focus = stage;
}
 
// pilote du programme
function main(e:Event):void {
	gestionVaisseau();
	gestionAsteroides();
	deplaceTirs();
	deplaceAsteroide();
}
 
// Appuyer sur une touche
function appuie(e:KeyboardEvent):void {
	var k:int = e.keyCode;
	if(k==38) avancer = true;
	if(k==39) tourner = 0.2;
	if(k==37) tourner = -0.2;
	if(k==32) tirer = true;
}
 
// relâcher une touche
function relache(e:KeyboardEvent):void {
	var k:int = e.keyCode;
	if(k==38) avancer = false;
	if(k==39) tourner = 0;
	if(k==37) tourner = 0;
	if(k==32) tirer = false;
}
 
function gameOver():void{
	removeChild(vaisseau);
	nbrAsteroide = 0;
	stage.removeEventListener(KeyboardEvent.KEY_DOWN, appuie);
	stage.removeEventListener(KeyboardEvent.KEY_UP, relache);
	stage.removeEventListener(Event.ENTER_FRAME, main);
	addChild(panneaux);
	panneaux.gotoAndStop(2);
}
 
// Limites du tableau pour tous les objets
function limites(O:Object):void {
	O.x = (O.x<0) ? W : (O.x>W) ? 0 : O.x;
	O.y = (O.y<0) ? H : (O.y>H) ? 0 : O.y;
}
 
function gestionVaisseau():void {
	if (tirer && tempoTir++>vitesseArme){
		tempoTir = 0;
		var t:Tir = new Tir();
		t.x = vaisseau.x;
		t.y = vaisseau.y;
		t.rotation = vaisseau.rotation+180;
		t.v = 10;
		t.e = 40;
		t.a = t.rotation*Math.PI/180; 
		addChild(t);
		stockTirs.push(t);
	} 
 
	with(vaisseau){
		if (avancer){
			if (vy<10) vy +=  Math.sin(rotation*Math.PI/180)*vitesse;
			if (vx<10) vx +=  Math.cos(rotation*Math.PI/180)*vitesse;
		} else {
			vy *=  friction;
			vx *=  friction;
		}
		y +=  vy;
		x +=  vx;
		rotation += tourner/Math.PI*180;
		if(invincible>0) invincible--;
	}
	limites(vaisseau);
}
 
function detruitVaisseau():void {
	creeExplosion(vaisseau,100);
	vaisseau.invincible = 50;
	if (vies--<1) gameOver();
}
 
function deplaceTirs():void {
	for (var i in stockTirs){
		var t:MovieClip = stockTirs[i];
		t.y -= Math.sin(t.a)*t.v;
		t.x -= Math.cos(t.a)*t.v;
		if(!t.e--) {
			stockTirs.splice(i,1);
			removeChild(t);
			return;
		}
		limites(t);
 
		for(var j in stockEnnemis){
			var B:MovieClip = stockEnnemis[j];
			if(B.hitTestPoint(t.x,t.y)){
				removeChild(t);
				stockTirs.splice(i,1);
				creeExplosion(t,0.4);
				if (!B.res--) {
					creeExplosion(B,B.cat);
					removeChild(B);
					stockEnnemis.splice(j,1);
					nouvelAsteroide(B);
				}
				return;
			}
		}
	}
}
 
function gestionAsteroides():void {
	if (nbrAsteroide<5) {
		creeAsteroide(4, 3, Math.random()*270+200, -Math.random()*500,1);
		nbrAsteroide++;
	}
	if (asteroidesDetruits%30==29) {
		creeAsteroide(4, 10, Math.random()*150+200, -Math.random()*500,4);
	}
}
 
function creeAsteroide(v:Number,c:int,px:Number,py:Number,f:int):void {
	var a:Asteroides = new Asteroides();
	a.y = py;
	a.x = px;
	a.ang = Math.random()*Math.PI*2;
	a.vit = Math.random()*v;
	a.rot = Math.random()*10;
	a.cat = c;
	a.res = c;
	a.gotoAndStop(f);
	stockEnnemis.push(a);
	addChild(a);
}
 
function deplaceAsteroide():void {
	for each(var a in stockEnnemis){
		a.rotation += a.rot;
		a.x += Math.cos(a.ang)*a.vit;
		a.y += Math.sin(a.ang)*a.vit;
		if (a.hitTestPoint(vaisseau.x, vaisseau.y)) {
			creeExplosion(a,a.cat);
			nouvelAsteroide(a);
			removeChild(a);
			stockEnnemis.splice(stockEnnemis.indexOf(a),1);
			if(!vaisseau.invincible) detruitVaisseau();
		}
		limites(a);
	}
}
 
function nouvelAsteroide(A:Object) {
	with(A){
		switch (cat) {
			case 10 :
				for (var j:int=0; j<5; j++) {
					creeAsteroide(06, 2, x, y,2);
					creeAsteroide(06, 2, x, y,2);
					creeAsteroide(10, 1, x, y,3);
					creeAsteroide(10, 1, x, y,3);
				}
				break;
			case 3 :
				creeAsteroide(06, 2, x, y,2);
				creeAsteroide(06, 2, x, y,2);
				asteroidesDetruits++;
				nbrAsteroide--;
				break;
			case 2 :
				creeAsteroide(10, 1, x, y,3);
				creeAsteroide(10, 1, x, y,3);
				break;
		}
	}
}
 
function creeExplosion(ob:Object,echelle:int) {
	if (ob.cat==100) bruitages = grosseExplosion.play();
	if (ob.cat==10)  bruitages = grosseExplosion.play();
	if (ob.cat==3)   bruitages = moyenneExplosion.play();
	if (ob.cat==2)   bruitages = petiteExplosion.play();
	var e:MovieClip = new Explosion();
	e.x = ob.x;
	e.y = ob.y;
	e.scaleX = e.scaleY = echelle*2;
	addChild(e);
	e.addEventListener(Event.ENTER_FRAME,gestionExplosion)
}
 
function gestionExplosion(e:Event):void {
	if(e.target.currentFrame>13) {
		removeChild(MovieClip(e.target));
		e.target.removeEventListener(Event.ENTER_FRAME,gestionExplosion)
	}
}

Etude du programme

Comme d'habitude j'utilise la bibliothèque de Flash pour gérer les graphismes, ce qui me permet d'alléger le code et de ne conserver que ce qui est utile pour l'exercice, si vous n'utilisez pas Flash voici les modifications à faire dans le programme :

Panneaux :

  • un clip qui regroupe tous les panneaux d'interface
  • un panneau différent par frame

Asteroides :

  • un clip qui regroupe tous les types d'astéroïdes (petits, moyens, gros, énormes)
  • un type sur chaque frame

Explosions :

  • l'animation des explosions

Fond :

  • le fond du jeu, une simple image

Tir :

  • les tirs du joueur, une simple image

Vaisseau :

  • le vaisseau du joueur, une simple image

Bon, au boulot, le code est assez long alors on va se débarrasser rapidement des truc qu'on à déjà vu dans les exercices précédents.

Allez c'est parti pour l'étude pas à pas :

var vies:int =				3;
var tempoTir:int =			0;
var vitesseArme:int =			2;
var nbrAsteroide:int =			0;
var asteroidesDetruits:int =		0;
var direction:Number =			0;
var W:int = 				stage.stageWidth;
var H:int = 				stage.stageHeight;
var avancer:Boolean =			false;
var tourner:Number =			0;
var tirer:Boolean =			false;
 
// conteneurs
var fond:MovieClip = 			new Fond();
var vaisseau:MovieClip = 		new Vaisseau();
var bruitages:SoundChannel;		
var grosseExplosion:Sound = 		new GrosseExplosion();
var moyenneExplosion:Sound = 		new MoyenneExplosion();
var petiteExplosion:Sound = 		new PetiteExplosion();
 
// tableaux
var stockTirs:Array;
var stockEnnemis:Array;
 
addChild(fond);
 
var panneaux:Panneaux = new Panneaux();
panneaux.addEventListener(MouseEvent.MOUSE_DOWN, init);
panneaux.buttonMode = true;
addChild(panneaux);

Ce n'est pas la première fois que vous voyez ce genre de choses (encore une fois lisez bien les exercices précédents pour suivre celui-ci). Je déclare toutes mes variables et mes conteneurs, contrairement à d'habitude j'ai utilisé ici des noms explicites, le code étant assez long ça n'aurait fait que vous embrouiller si j'avais gardé des initiales pour les noms des variables. De fait, je considère que vous n'avez pas besoin d'explication pour la plupart d'entre elles et je vais me contenter de vous lister celles qui sont importantes :

tempoTir = sert à temporiser le tir
vitesseArme = réglage de la vitesse de répétition de l'arme
nbrAsteroide = le nombre d'astéroïdes présents
asteroidesDetruits = le nombre d'astéroïdes détruits (c'est là que vous pouvez jouer avec le score)

Le reste est assez simple à comprendre par vous même ;-)
(Ah oui, on est dans le cadre d'un “exercice” pas d'un “tutoriel”, vous allez devoir faire un effort pour bien tout suivre…).

function init(e:MouseEvent):void {
 
	while(numChildren>1) removeChildAt(1);
 
	stockTirs = 			[];
	stockEnnemis = 			[];
 
	addChild(vaisseau);
	avancer =			false;
	tirer =				false;
	tourner =			0;
	vies = 				3;
	vaisseau.y = 			H*.5;
	vaisseau.x = 			W*.5;
	vaisseau.vx =			0;
	vaisseau.vy =			0;
	vaisseau.vitesse = 		0.3;
	vaisseau.friction = 		0.95;
	vaisseau.rotation = 		90;
	vaisseau.invincible = 		50;
	vaisseau.cat = 			100;
	stage.addEventListener(KeyboardEvent.KEY_DOWN, appuie);
	stage.addEventListener(KeyboardEvent.KEY_UP, relache);
	stage.addEventListener(Event.ENTER_FRAME, main);
	stage.focus = stage;
}

On initialise le jeu, je commence par supprimer tout ce qui est affiché, pour le cas où le joueur recommencerait une partie, puis je vide les tableaux de stockage, classique on fait ça à chaque exercice.

J'ajoute le vaisseau que je place et auquel je passe plusieurs paramètres, sa vitesse sur chaque axe (vx, vy), sa vitesse globale de déplacement (vitesse), la friction qui lui permet de ralentir (friction), sa rotation qui permet de l'orienter (rotation), son invincibilité qui permet de lui assurer un temps de répit si il vient d'être détruit (invincible), et enfin sa catégorie qui permet au moteur de déclencher des actions génériques (on y reviendra).

Et pour finir, j'ajoute les écouteurs clavier et un général pour le moteur, notez que je passe aussi le focus au stage afin que les événements clavier soient pris en compte dès que j'ai cliqué sur le panneau d'interface pour lancer le jeu.

Allez zou, ça c'est fait ;-)

On va parler un peu des classes et de la POO, typiquement ce genre de jeu serait plus propre en POO, injecter des paramètres à la volée à un objet (le vaisseau) est rarement une bonne idée, on ne peux pas typer les variables et ça rend le code plus rigide, mais comme nous n'avons pas encore commencé la POO dans les exercices on va continuer comme ça. Cependant je pense qu'il est utile de commencer à en parler et de vous montrer à quels moments cela devient réellement plus efficace de travailler en Orienté Objet plutôt qu'en procédural, ce jeu en est un bon exemple comme vous allez le voir par la suite, le vaisseau n'est pas le seul objet qui mériterai une petite classe pour le rendre indépendant.

// pilote du programme
function main(e:Event):void {
	gestionVaisseau();
	gestionAsteroides();
	deplaceTirs();
	deplaceAsteroide();
}

Avec cette fonction (gérée par un ENTER_FRAME) je vais piloter tout le jeu, elle lance simplement dans l'ordre les fonctions utiles.

// Appuyer sur une touche
function appuie(e:KeyboardEvent):void {
	var k:int = e.keyCode;
	if(k==38) avancer = true;
	if(k==39) tourner = 0.2;
	if(k==37) tourner = -0.2;
	if(k==32) tirer = true;
}
 
// relâcher une touche
function relache(e:KeyboardEvent):void {
	var k:int = e.keyCode;
	if(k==38) avancer = false;
	if(k==39) tourner = 0;
	if(k==37) tourner = 0;
	if(k==32) tirer = false;
}

Bon ça vous connaissez, on utilise les écouteurs clavier depuis un moment dans les exercices, on ne va quand même pas revenir dessus à chaque fois sinon ça ne sert à rien de vous imposer des pré-requis ;-)

Tant qu'on en est à se débarrasser des choses simples, hop on se fait le gameOver :

function gameOver():void{
	removeChild(vaisseau);
	nbrAsteroide = 0;
	stage.removeEventListener(KeyboardEvent.KEY_DOWN, appuie);
	stage.removeEventListener(KeyboardEvent.KEY_UP, relache);
	stage.removeEventListener(Event.ENTER_FRAME, main);
	addChild(panneaux);
	panneaux.gotoAndStop(2);
}

On retire le vaisseau (on peut laisser les astéroïdes affichés si on veut à ce stade puisqu'on les retire quand on relance une partie), on retire les écouteurs, on ajoute le panneau de fin de partie, zou !

// Limites du tableau pour tous les objets
function limites(O:Object):void {
	O.x = (O.x<0) ? W : (O.x>W) ? 0 : O.x;
	O.y = (O.y<0) ? H : (O.y>H) ? 0 : O.y;
}

Ha ! Voici une petite fonction bien utile pour ce type de jeux…. Je vous en parle tout de suite car on va l'utiliser pour TOUS les objets mobiles du jeu. Une des particularités de ce jeu est que le terrain de jeu est limité (pas de scrolling) mais pas les déplacements des objets, si un objet sort d'un côté de la zone de jeu il est replacé de l'autre côté et continue à avancer, c'est le point d'orge du jeu et ce qui fait tout son gameplay original.

Faisons simple, on regarde la position de l'objet, si elle est en dehors de la zone de jeu on le replace de l'autre côté sinon il conserve sa position, ça tient en deux lignes avec deux if…else, un pour chaque axe ;-)

Passons à la gestion du vaisseau :

function gestionVaisseau():void {
	if (tirer && tempoTir++>vitesseArme){
		tempoTir = 0;
		var t:Tir = new Tir();
		t.x = vaisseau.x;
		t.y = vaisseau.y;
		t.rotation = vaisseau.rotation+180;
		t.v = 10;
		t.e = 40;
		t.a = t.rotation*Math.PI/180; 
		addChild(t);
		stockTirs.push(t);
	} 
 
	with(vaisseau){
		if (avancer){
			if (vy<10) vy +=  Math.sin(rotation*Math.PI/180)*vitesse;
			if (vx<10) vx +=  Math.cos(rotation*Math.PI/180)*vitesse;
		} else {
			vy *=  friction;
			vx *=  friction;
		}
		y +=  vy;
		x +=  vx;
		rotation += tourner/Math.PI*180;
		if(invincible>0) invincible--;
	}
	limites(vaisseau);
}

On commence par le tir, histoire de changer un peu…. Lorsque le joueur appuie sur la barre espace, il tire. La première chose à faire c'est de voir si en fonction de la cadence de tir qu'on a fixé au départ un tir peut être envoyé, pour cela on incrémente le tempo du tir, et on regarde si il est supérieur à la vitesse de l'arme, si c'est le cas alors un peut envoyer un nouveau tir. Ceci évite d'avoir un tir trop rapide.

Lorsqu'un nouveau tir est envoyé, le tempo du tir est remis à zéro, puis on crée le tir que l'on place sur le vaisseau (ici je l'ai mis au centre mais vous pouvez vous débrouiller pour le coller sur le nez du vaisseau par exemple). Le tir est également dirigé dans le même sens que le vaisseau et enfin je vais passer trois paramètres au tir, sa vitesse propre (v), sa durée d'existence (e) et son angle de rotation (a). La vitesse lui sert à se déplacer, sa durée d'existence sert à le faire disparaître au bout d'un certain temps (sinon il continuerait à l'infini puisque tous les objets sont replacé quand ils sortent de la zone de jeu, les tirs compris), et son angle va servir à le déplacer dans la bonne direction. Enfin on ajoute le tir au stock des tirs et on l'affiche.

Ensuite on s'occupe de déplacer le vaisseau, là c'est assez simple, si le joueur avance, on regarde si la vitesse sur chaque axe ne dépasse pas une vitesse max (ici 10) puis on incrémente la position sur chaque axe en fonction de la rotation du vaisseau et de sa vitesse globale. Lorsque le joueur n'avance plus on fait agir la friction pour ralentir le vaisseau. On déplace ensuite le vaisseau en incrémentant sa position, et on gère sa rotation (lorsque le joueur décide de le faire tourner). Il faut aussi décrémenter le compteur d'invincibilité au cas où le joueur aurait perdu une vie, histoire que le vaisseau ne reste pas invincible trop longtemps.

Et pour finir, on regarde si le vaisseau sort de la zone de jeu et auquel cas on le replace correctement.

function detruitVaisseau():void {
	creeExplosion(vaisseau,100);
	vaisseau.invincible = 50;
	if (vies--<1) gameOver();
}

Petite fonction dont va se débarrasser tout de suite, la destruction du vaisseau. Quand le vaisseau est détruit on crée une nouvelle explosion (on verra comment par la suite), on rendu le vaisseau invincible pour un court moment, et on lui retire une vie. Si le joueur n'a plus de vie c'est la fin de la partie (on l'a vu plus haut).

function deplaceTirs():void {
	for (var i in stockTirs){
		var t:MovieClip = stockTirs[i];
		t.y -= Math.sin(t.a)*t.v;
		t.x -= Math.cos(t.a)*t.v;
		if(!t.e--) {
			stockTirs.splice(i,1);
			removeChild(t);
			return;
		}
		limites(t);
 
		for(var j in stockEnnemis){
			var B:MovieClip = stockEnnemis[j];
			if(B.hitTestPoint(t.x,t.y)){
				removeChild(t);
				stockTirs.splice(i,1);
				creeExplosion(t,0.4);
				if (!B.res--) {
					creeExplosion(B,B.cat);
					removeChild(B);
					stockEnnemis.splice(j,1);
					nouvelAsteroide(B);
				}
				return;
			}
		}
	}
}

Pour gérer les tirs on va regarder ce qui se trouve dans le tableau de stockage des tirs. Pour chaque tir présent on le déplace en fonction de son angle et de sa vitesse, puis on décrémente sa durée d'existence, si cette dernière est nulle on retire le tir du stock et de l'affichage. Ensuite on vérifie si il sort des limites et on le replace le cas échéant.

Et enfin on va tester la collision avec les astéroïdes. Les tirs étant représentés par des points, j'utilise (pour une fois) hitTestPoint avec la position du tir et chaque astéroïde présent dans le stock des astéroïdes. Lorsqu'un tir touche un astéroïde, on supprime le tir et on crée une petite explosion, puis on regarde la résistance de l'astéroïde touché (on le verra plus tard mais chaque astéroïde a sa propre résistance en fonction de sa taille), si l'astéroïde n'a plus de résistance il explose, on crée une explosion, on retire l'astéroïde de son stock et…… on crée un ou plusieurs nouveaux astéroïdes en fonction de la catégorie de l'astéroïde détruit. Pensez à stopper la boucle lorsqu'un tir à été détruit ;-)

C'est pas clair tout de bazar avec les astéroïdes ?

Pas de panique on va justement voir ce qu'il se passe de ce côté là.

function gestionAsteroides():void {
	if (nbrAsteroide<5) {
		creeAsteroide(4, 3, Math.random()*270+200, -Math.random()*500,1);
		nbrAsteroide++;
	}
	if (asteroidesDetruits%30==29) {
		creeAsteroide(4, 10, Math.random()*150+200, -Math.random()*500,4);
	}
}

Tout d'abord de manière globale, lorsque le nombre de gros astéroïdes présents est inférieur à 5 on crée un nouveau gros astéroïde (on va détailler juste après) et on incrémente le compteur. Attention, cela veut dire que dès que le joueur détruit un gros astéroïde, on en recrée un, mais on ne détruit pas pour autant les débris (les astéroïdes plus petits) , donc plus le joueur détruit de gros astéroïdes et plus il y aura d'astéroïdes de tous genre à l'écran, c'est un choix de gameplay mais vous pouvez moduler à votre guise en comptant par exemple tous les astéroïdes présents et pas seulement les gros.

Le petit bonus c'est que tous les 30 astéroïdes détruits on affiche un énorme astéroïde très difficile à casser. Ca pimente un peu la partie ;-)

function creeAsteroide(v:Number,c:int,px:Number,py:Number,f:int):void {
	var a:Asteroides = new Asteroides();
	a.y = py;
	a.x = px;
	a.ang = Math.random()*Math.PI*2;
	a.vit = Math.random()*v;
	a.rot = Math.random()*10;
	a.cat = c;
	a.res = c;
	a.gotoAndStop(f);
	stockEnnemis.push(a);
	addChild(a);
}

La création d'un astéroïde se passe de la manière suivante, on crée l'objet, on le place (la position est passée en paramètre de la fonction, vous verrez que c'est important par la suite), on lui donne un angle aléatoire pour son déplacement, une vitesse aléatoire, une rotation aléatoire, une catégorie (elle aussi passée en paramètre de la fonction) et une résistance qui correspond à sa catégorie. On affiche la frame correspondante de l'objet, on l'ajoute au stock et on l'affiche, c'est tout ;-)

Là aussi la POO serait plus efficace, pour les même raisons qu'évoquées au début de cet exercice.

function deplaceAsteroide():void {
	for each(var a in stockEnnemis){
		a.rotation += a.rot;
		a.x += Math.cos(a.ang)*a.vit;
		a.y += Math.sin(a.ang)*a.vit;
		if (a.hitTestPoint(vaisseau.x, vaisseau.y)) {
			creeExplosion(a,a.cat);
			nouvelAsteroide(a);
			removeChild(a);
			stockEnnemis.splice(stockEnnemis.indexOf(a),1);
			if(!vaisseau.invincible) detruitVaisseau();
		}
		limites(a);
	}
}

Si vous avez compris comment se déplacent les tirs vous ne devriez pas avoir de difficulté ici, c'est pareil, à la différence qu'on regarde cette fois si l'astéroïde touche le vaisseau, auquel cas il explose et génère un ou plusieurs autres astéroïdes, les débris. Et comme à chaque objet, on regarde si il sort de la zone de jeu bien sur.

function nouvelAsteroide(A:Object) {
	with(A){
		switch (cat) {
			case 10 :
				for (var j:int=0; j<5; j++) {
					creeAsteroide(06, 2, x, y,2);
					creeAsteroide(06, 2, x, y,2);
					creeAsteroide(10, 1, x, y,3);
					creeAsteroide(10, 1, x, y,3);
				}
				break;
			case 3 :
				creeAsteroide(06, 2, x, y,2);
				creeAsteroide(06, 2, x, y,2);
				asteroidesDetruits++;
				nbrAsteroide--;
				break;
			case 2 :
				creeAsteroide(10, 1, x, y,3);
				creeAsteroide(10, 1, x, y,3);
				break;
		}
	}
}

C'est la partie la plus importante du gameplay avec le replacement hors zone de jeu.

Quand on crée un nouvel astéroïde on regarde d'abord l'objet qui à généré cette création. En fonction de la catégorie de l'objet on va créer un ou plusieurs astéroïdes de taille et de catégorie inférieure, c'est comme cela qu'on va “briser” un gros caillou pour en faire des débris plus petits ;-) Notez que la catégorie 3 correspond aux gros astéroïdes, on en profite donc pour signaler au jeu qu'on a détruit un nouveau gros astéroïde en mettant à jour le compteur. Notez également que les nouveaux astéroïdes (les débris) sont créés à la même position que l'astéroïde détruit.

function creeExplosion(ob:Object,echelle:int) {
	if (ob.cat==100) bruitages = grosseExplosion.play();
	if (ob.cat==10)  bruitages = grosseExplosion.play();
	if (ob.cat==3)   bruitages = moyenneExplosion.play();
	if (ob.cat==2)   bruitages = petiteExplosion.play();
	var e:MovieClip = new Explosion();
	e.x = ob.x;
	e.y = ob.y;
	e.scaleX = e.scaleY = echelle*2;
	addChild(e);
	e.addEventListener(Event.ENTER_FRAME,gestionExplosion)
}

Haaaa…. les explosions, faut que ça péte un peu dans tous les sens si on veut que le tout soit vivant…. Créer une explosion génère à la fois une animation dont la taille dépend de la catégorie de l'objet détruit, mais aussi un son qui dépend lui aussi de la même catégorie. On place l'explosion à la position de l'objet détruit, on la met à l'échelle, on l'ajoute à l'affichage et on écoute ce qu'elle fait.

function gestionExplosion(e:Event):void {
	if(e.target.currentFrame>13) {
		removeChild(MovieClip(e.target));
		e.target.removeEventListener(Event.ENTER_FRAME,gestionExplosion)
	}
}

Lorsqu'une explosion a fini de jouer l'animation qui la concerne, on le retire tout simplement ;-)

Et voilà, nous avons terminé ce petit prototype de base pour un Asteroids.

Conclusion

Cet exercice n'est pas vraiment compliqué, cependant il est un exemple type d'un jeu qui demanderait à être travaillé en programmation orienté objet, il semble évident ici que chaque objet (astéroïdes, tirs, vaisseau, explosions) pourrait être doté de son propre comportement n'impactant pas forcément les autres (déplacement, durée de vie, replacement, …), seules les collisions réunissent en fait les différents objets, ce sont les événements déclencheurs. Il serait donc plus approprié de travailler avec des classes et un peu d'héritage afin de rendre le code plus souple et plus propre. J'ai tenu à vous le présenter en procédural car il me semble important que vous voyiez d'abord pourquoi on va choisir à un moment donné de développer plutôt en POO ou en procédural avant de commencer les exercices POO. Détrompez-vous, la POO n'est pas une réponse magique à tout, pour des petits jeux simples comme nous avons abordé dans les exercices précédents, le procédural est bien plus rapide en terme d'écriture et les performances sont tout aussi bonnes, en revanche dès que vous allez avoir de nombreux objets différents à gérer ça devient rapidement un enfer car le code s'allonge et les boucles s'imbriquent un peut comme des spaghettis dans une casserole, ça commence à devenir difficile à suivre si vous avez raté une variable à un endroit qui va déterminer le comportement d'une boucle imbriquée ailleurs. Bref, le passage de procédural à POO est une étape charnière importante que nous mettrons en pratique dans un autre exercice, mais il est important que vous compreniez quand faire le choix de l'un ou de l'autre et que vous soyez aussi à l'aise avec les deux méthodes d'écriture.

Les sources

asteroid_mb_v1.fla version CS6
asteroid_mb_cs5.fla version CS5