Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Exercice pratique : le SPACE INVADER

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

Voici le premier exercice de niveau 2, jusqu'à présent nous nous sommes attaqués à de petits jeux très simples pour se débarrasser des bases, mais avec Space Invader nous allons commencer à étudier des programmes un peu plus longs et complexes.

Jouons un peu pour voir 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 SPACE INVADER c'est quoi ? (merci Wikipedia)

Space Invaders est un jeu vidéo développé par la société japonaise Taito, sorti en 1978 sur borne d'arcade. Il s'agit d'un shoot them up fixe. Tomohiro Nishikado conçoit et programme le jeu. Il s'inspire de plusieurs médias populaires de l'époque pour réaliser Space Invaders tels que Breakout, La Guerre des mondes et Star Wars. Considéré comme le premier archétype du shoot them up, il est aussi l'un des titres les plus influents et célèbres de l'histoire du jeu vidéo.

Le principe est de détruire des vagues d'aliens au moyen d'un canon laser en se déplaçant horizontalement sur l'écran. Il fait partie des classiques du jeu vidéo au même titre que Pac-Man et d'autres de ses contemporains. Après sa sortie au Japon, il aurait entraîné une pénurie de pièces de 100 yens. En 2007, Taito annonce que le jeu a rapporté 500 millions de dollars de recettes depuis sa parution.

Ce jeu influence beaucoup de titres et connaît plusieurs suites. Il est adapté sur de nombreux supports (Atari 2600, Atari 5200, MSX, NES, etc.). En 1980, la sortie de la version pour l'Atari 2600 permet de quadrupler les ventes de la plate-forme. De ce fait, il devient le premier titre dit « killer application » pour les consoles de jeux vidéo. La représentation par des pixels des aliens dans le jeu devient une icône médiatique pour symboliser le monde des jeux vidéo.

Le joueur contrôle un canon laser qu'il peut déplacer horizontalement, au bas de l'écran. Dans les airs, des rangées d'aliens se déplacent latéralement tout en se rapprochant progressivement du sol et en lançant des missiles. L'objectif est de détruire avec le canon laser une vague ennemie, qui se compose de cinq rangées de onze aliens chacune, avant qu'elle n'atteigne le bas de l'écran. Le joueur gagne des points à chaque fois qu'il détruit un envahisseur. Le jeu n'autorise qu'un tir à la fois et permet d'annuler ceux des ennemis en tirant dessus. La vitesse et la musique s'accélèrent au fur et à mesure que le nombre d'aliens diminue. L'élimination totale de ces derniers amène une nouvelle vague ennemie plus difficile, et ce indéfiniment. Le jeu ne se termine que lorsque le joueur perd, ce qui en fait le premier jeu sans fin.

Le score le plus élevé à Space Invaders serait détenu par Eric Furrer à l'âge de 12 ans à Toronto, Ontario, Canada. Il a obtenu le score de 1,114,020 en 38 heures et 30 minutes. Le score se remettant à zéro tous les 10 000, il a remis à zéro le compteur 111 fois durant sa partie

Je pense que nous avons toutes les informations utiles pour se lancer…

Les pré-requis

En plus des bases indispensables, pour attaquer le niveau 2 de cette petite série d'exercice il est impératif que vous ayez au moins parcouru les exercices du niveau 1. Les astuces qui y sont données ne seront plus expliquées par la suite.

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

On se regarde tout ça d'un bloc avant de se lancer dans les explications.

// Variables
var W:int = 				stage.stageWidth;
var H:int = 				stage.stageHeight;
var C:int = 				11;
var T:int = 				36;
var D:int = 				4;
var niveau:int =			1;
var vies:int;
var temps:Number;
var cadencePos:int;
var vitesse:int;
var sens:int;
var score:int;
var descend:Boolean;
var tirer:Boolean;
 
// Tableaux de stockage
var stockTirs:Array;
var stockAliens:Array;
var stockVies:Array;
 
// Objets
var base:Base = 			new Base();
var laser:Laser = 			new Laser();
var bunkers:Bunkers = 			new Bunkers();
var soucoupe:Soucoupe = 		new Soucoupe();
var explose:Explose = 			new Explose();
 
// sons
var cadence:Cadence = 			new Cadence();
var sonSoucoupe = 			new SonSoucoupe();
var sonTueSoucoupe = 			new SonTueSoucoupe();
var sonCasseBunker = 			new SonCasseBunker();
var sonTirLaser = 			new SonTirLaser();
var sonTirAlien = 			new SonTirAlien();
var pisteSoucoupe:SoundChannel;
 
// interface
var panneau:Panneaux = new Panneaux();
panneau.addEventListener(MouseEvent.CLICK, init);
panneau.buttonMode = true;
addChild(panneau);
 
// Appuyer sur une touche
function appuie(e:KeyboardEvent):void {
	if(e.keyCode==39) sens = 1;
	if(e.keyCode==37) sens = -1;
	if(e.keyCode==32) tirer = true;
}
 
// Relâcher une touche
function relache(e:KeyboardEvent):void {
	if(e.keyCode==39 || e.keyCode==37) sens = 0;
	if(e.keyCode==32) tirer = false;
}
 
// Initialisation du jeu
function init(e:MouseEvent):void{
 
	var i:int;
 
	while(numChildren>0) removeChildAt(0);
 
	vies = 				6;
	temps = 			0;
	cadencePos = 			0;
	sens =				0;
	score = 			0;
	vitesse = 			700-niveau*100;
	descend = 			false;
	tirer = 			false;
	stockTirs = 			[];
	stockAliens = 			[];
	stockVies = 			[];
 
	cadence.gotoAndStop(1);
 
	// vies
	for (i=0; i<vies; i++){
		var v:Vies = new Vies();
		v.x = (v.width+2)*(i%4)+16;
		v.y = (v.height+2)*int(i/4)+16;
		addChild(v);
		stockVies.push(v);
	}
 
	// Création des rangées d'aliens
	for (i=0; i<55; i++){
		var a:TilesAliens = new TilesAliens();
		a.x = i%C*T;
		a.y = int(i/C)*T+80;
		a.marge = a.width*.5;
		a.gotoAndStop(int(i/C)+1);
		addChild(a);
		stockAliens.push(a);
	}
 
	// remise à zéro des bunkers
	bunkers = new Bunkers();
 
	// Paramètres
	base.y = 			H-20;
	base.x = 			W*.5;
	base.marge =			base.width*.5
	bunkers.x = 			20;
	bunkers.y = 			H-110;
	soucoupe.x =			0;
	soucoupe.y = 			56;
	soucoupe.dir = 			1;
	soucoupe.visible = 		false;
	laser.visible = 		false;
	laser.dir = 			-1;
 
	// Liste d'affichage
	addChild(base);
	addChild(laser);
	addChild(explose);
	addChild(bunkers);
	addChild(soucoupe);
 
	stockTirs.push(laser);
 
	// Ecouteurs
	stage.addEventListener(Event.ENTER_FRAME,main);
	stage.addEventListener(KeyboardEvent.KEY_DOWN, appuie);
	stage.addEventListener(KeyboardEvent.KEY_UP, relache);
}
 
// Boucle principale
function main(e:Event):void{
	controles();
	gestionTirs();
	gestionAliens();
	gestionSoucoupe();
}
 
// Déplacement de la base laser
function controles():void{
	with(base){
		if (currentFrame == 1)	{
			x += sens*10;
			if (x>W-marge) 	x = W-marge;
			if (x<marge) 	x = marge;
			if (tirer && !laser.visible){
				initLaser();
				laser.visible = true;
				sonTirLaser.play();
			}
		}
	}
}
 
// Déplacement des aliens
function gestionAliens():void{
 
	// Déplace les aliens au bon tempo
	if (getTimer()-temps>=vitesse){
		temps = getTimer();
		if (vitesse<10) vitesse = 10;
 
		// Musique en rythme
		cadence.gotoAndStop(cadencePos+2);
		cadencePos++;
		cadencePos %= 4;
 
		var a:Object;
 
		// déplacement
		for each (a in stockAliens){
			a.getChildAt(0).nextFrame();// joue l'animation de l'alien
			if (a.x+a.width+D>W || a.x+D<0) descend = true;
			a.x += D;
		}
 
		if (descend){
			vitesse -=  40;
			D *= -1;
			for each(a in stockAliens){
				a.y += T*.5; 
				a.x += D;
				if (a.y > base.y){
					vies = 0;
					base.gotoAndPlay(2);
					finInvader(2,1);
				}
			}
			descend = false;
		}
 
		// Si il ne reste plus aucun alien a tuer
		if (stockAliens.length==0){
			if (vies<9)	vies++;
			niveau++>4 ? finInvader(3,1) : finInvader(4,niveau);
		}
 
		// Tir alien aléatoire en fonction de la vitesse du jeu
		if (Math.random()<.3){
			var t:TirAlien = new TirAlien();
			a = stockAliens[int(Math.random()*stockAliens.length)];
			t.x = a.x+a.marge;
			t.y = a.y+a.marge;
			t.dir = 1;
			sonTirAlien.play();
			stockTirs.push(t);
			addChild(t);
		}
	}
}
 
// Gestion de la soucoupe
function gestionSoucoupe():void{
	with(soucoupe){
		if (visible){
			x += dir*3;
			if (x<-100 || x>580){
				visible = false;
				pisteSoucoupe.stop();
				return;
			}
		} else if (Math.random()<.005) {
			dir = Math.random()<.5 ? 1:-1;
			x = dir>0 ? -50:520;
			visible = true;
			pisteSoucoupe = sonSoucoupe.play(0,10);
		} else {
			x = -50;
		}
	}
}
 
// gestion de tous les tirs
function gestionTirs():void{
	for (var i in stockTirs){
		var t:Object = stockTirs[i];
		if(i=="0"){
			if (laser.visible) {
				laser.y>0 ? laser.y -= 20 : laser.visible = false;
				for (var j:String in stockAliens){
					var a:MovieClip = stockAliens[j] as MovieClip;
					if (laser.hitTestObject(a)){
						explosion(a.x+a.marge,a.y+a.marge);
						removeChild(a);
						stockAliens.splice(j,1);
						initLaser();
						vitesse -= 5;
						score++;
					}
				}
				// Si le laser touche la soucoupe
				if (soucoupe.hitTestObject(laser)){
					explosion(soucoupe.x,soucoupe.y);
					soucoupe.visible = false;
					pisteSoucoupe.stop();
					sonTueSoucoupe.play();
					initLaser();
				}
			}
		} else if(t.y<H) {
			t.y += 6*t.dir; 
			if (base.hitTestPoint(t.x,t.y) && base.currentFrame == 1){
				killTirAlien(t,i);
				base.gotoAndPlay(2);
				retireVies();
			}
		} else {
			killTirAlien(t,i);
			return;
		}
 
		// touche un bunker
		for (var k:int = 0; k<bunkers.numChildren; k++) {
			var b = bunkers.getChildAt(k);
			if (t.hitTestObject(b)){
				t.dir>0 ? killTirAlien(t,i) : initLaser();
				b.currentFrame == 4 ? bunkers.removeChild(b) : b.nextFrame();
				sonCasseBunker.play();
				return;
			}
		}
	}
}
 
function killTirAlien(ob:Object, id:int):void{
	ob.parent.removeChild(ob);
	stockTirs.splice(id,1);
}
 
function explosion(X:Number,Y:Number):void{
	explose.x = X;
	explose.y = Y;
	explose.gotoAndPlay(2);
}
 
function initLaser():void{
	laser.x = base.x;
	laser.y = base.y;
	laser.visible = false;
}
 
function retireVies():void{
	vies--;
	removeChild(stockVies[vies]);
	stockVies.splice(vies,1);
	if (vies<=0) finInvader(2,1);
}
 
// Partie perdue
function finInvader(f:int,n:int) {
	niveau = n;
	if(pisteSoucoupe) pisteSoucoupe.stop();
	addChild(panneau);
	panneau.gotoAndStop(f);
	stage.removeEventListener(Event.ENTER_FRAME,main);
	stage.removeEventListener(KeyboardEvent.KEY_DOWN, appuie);
	stage.removeEventListener(KeyboardEvent.KEY_UP, relache);
}

Etude du programme

Pour chaque exercice, puisque nous travaillons avec Flash, je me sert des outils à ma disposition pour alléger le code, ainsi pas mal de choses passent par l'intermédiaire d'objets dynamiques et des outils graphiques de Flash. Pour Space Invader je me suis non seulement servi de ces outils, mais j'ai également fait l'impasse sur certaines petites choses comme la gestion du score, inutile pour ce que nous allons étudier, ne vous attendez donc pas à un jeu réellement complet, ce sera à vous d'ajouter ce qu'il manque.

Pour ceux qui n'utilisent pas Flash, voici les bidouilles utilisées avec les clips et que vous devrez adapter dans votre programme :

Base :

  • Le clip est composé d'une première frame qui affiche la base dans son état normal
  • Et d'une animation à partir de la frame 2 représentant une explosion
  • Le son de l'explosion est posé directement sur la timeline du clip
  • Une série de frames vide termine l'animation pour créer un temps de latence avant réapparition

Bunkers :

  • Tous les bunkers sont réunis dans un seul clip
  • Chaque bunker est composé de 10 blocs différents
  • Chaque bloc est un clip composé de 4 frames affichant une partielle destruction

Explose :

  • La première frame est vide et dotée d'un “stop()”
  • A partir de la seconde frame je pose un son et le graphisme de l'explosion
  • L'explosion reste à l'écran pendant 5 frames puis reviens à la frame 1

Panneaux :

  • Un panneau différent sur chaque frame

TilesAliens :

  • Tous les types d'aliens sont réunis dans un seul clip conteneur
  • Sur chaque frame un alien de type différent
  • Chaque type d'alien est un clip contenant deux frame d'animation

Cadence :

  • La première frame est vide et contient un code “stop()”
  • Les 4 frames suivantes contiennent un son (tone) différent

Tous les clips sont exportés pour AS depuis la bibliothèque avec un nom de classe commençant par une majuscule.

Je rappelle que toutes ces manips avec les clips ont pour but d'alléger le programme pour l'exercice, il est tout à fait possible de gérer tout ça avec des spritesheets (relisez le Taquin) et des ressources externes si vous le souhaitez.

Passons à l'étude du code, comme vous avez les pré-requis on va aller très vite sur les choses classiques.

// Variables
var W:int = 				stage.stageWidth;
var H:int = 				stage.stageHeight;
var C:int = 				11;
var T:int = 				36;
var D:int = 				4;
var niveau:int =			1;
var vies:int;
var temps:Number;
var cadencePos:int;
var vitesse:int;
var sens:int;
var score:int;
var descend:Boolean;
var tirer:Boolean;

Je déclare toutes les variables utiles, pour info :

W = largeur du jeu
H = hauteur du jeu
C = nombre de colonnes d'aliens
T = taille d'un alien
D = vitesse et direction des aliens

Tout le reste est soit suffisamment explicite, soit à découvrir au moment venu.

// Tableaux de stockage
var stockTirs:Array;
var stockAliens:Array;
var stockVies:Array;

Les tableaux de stockage pour mes objets.

// Objets
var base:Base = 			new Base();
var laser:Laser = 			new Laser();
var bunkers:Bunkers = 			new Bunkers();
var soucoupe:Soucoupe = 		new Soucoupe();
var explose:Explose = 			new Explose();

Déclaration des objets de base.

// sons
var cadence:Cadence = 			new Cadence();
var sonSoucoupe = 			new SonSoucoupe();
var sonTueSoucoupe = 			new SonTueSoucoupe();
var sonCasseBunker = 			new SonCasseBunker();
var sonTirLaser = 			new SonTirLaser();
var sonTirAlien = 			new SonTirAlien();
var pisteSoucoupe:SoundChannel;

Déclaration des sons et d'une piste son pour la gestion de la soucoupe.

// interface
var panneau:Panneaux = new Panneaux();
panneau.addEventListener(MouseEvent.CLICK, init);
panneau.buttonMode = true;
addChild(panneau);

Création et affichage du panneau d'interface.
Passage d'un événement souris pour lancer ou relancer le jeu.

// Appuyer sur une touche
function appuie(e:KeyboardEvent):void {
	if(e.keyCode==39) sens = 1;
	if(e.keyCode==37) sens = -1;
	if(e.keyCode==32) tirer = true;
}
 
// Relâcher une touche
function relache(e:KeyboardEvent):void {
	if(e.keyCode==39 || e.keyCode==37) sens = 0;
	if(e.keyCode==32) tirer = false;
}

Gestion des événements clavier, ici cela concerne la base de tir laser.

// Initialisation du jeu
function init(e:MouseEvent):void{
 
	var i:int;
 
	while(numChildren>0) removeChildAt(0);
 
	vies = 				6;
	temps = 			0;
	cadencePos = 			0;
	sens =				0;
	score = 			0;
	vitesse = 			700-niveau*100;
	descend = 			false;
	tirer = 			false;
	stockTirs = 			[];
	stockAliens = 			[];
	stockVies = 			[];
 
	cadence.gotoAndStop(1);
 
	// vies
	for (i=0; i<vies; i++){
		var v:Vies = new Vies();
		v.x = (v.width+2)*(i%4)+16;
		v.y = (v.height+2)*int(i/4)+16;
		addChild(v);
		stockVies.push(v);
	}
 
	// Création des rangées d'aliens
	for (i=0; i<55; i++){
		var a:TilesAliens = new TilesAliens();
		a.x = i%C*T;
		a.y = int(i/C)*T+80;
		a.marge = a.width*.5;
		a.gotoAndStop(int(i/C)+1);
		addChild(a);
		stockAliens.push(a);
	}
 
	// remise à zéro des bunkers
	bunkers = new Bunkers();
 
	// Paramètres
	base.y = 				H-20;
	base.x = 				W*.5;
	base.marge =				base.width*.5
	bunkers.x = 				20;
	bunkers.y = 				H-110;
	soucoupe.x =				0;
	soucoupe.y = 				56;
	soucoupe.dir = 				1;
	soucoupe.visible = 			false;
	laser.visible = 			false;
	laser.dir = 				-1;
 
	// Liste d'affichage
	addChild(base);
	addChild(laser);
	addChild(explose);
	addChild(bunkers);
	addChild(soucoupe);
 
	stockTirs.push(laser);
 
	// Ecouteurs
	stage.addEventListener(Event.ENTER_FRAME,main);
	stage.addEventListener(KeyboardEvent.KEY_DOWN, appuie);
	stage.addEventListener(KeyboardEvent.KEY_UP, relache);
}

L'initialisation du jeu, c'est ici que j'affecte les bonnes valeurs à mes variables pour un démarrage de partie.

Voyons de plus près ce qui est notable :

while(numChildren>0) removeChildAt(0);

Avant toute chose je commence par vider toute la liste d'affichage, comme cette fonction est utilisée à chaque nouveau niveau ou démarrage ou redémarrage de partie il est nécessaire de faire du nettoyage avant de recréer les objets utiles.

vitesse = 700-niveau*100;

La vitesse du jeu est inversée, plus la valeur est petite et plus le jeu est rapide. Quand on change de niveau on fait donc diminuer la valeur de départ de la vitesse pour que les aliens aillent plus vite.

 // vies
for (i=0; i<vies; i++){
	var v:Vies = new Vies();
	v.x = (v.width+2)*(i%4)+16;
	v.y = (v.height+2)*int(i/4)+16;
	addChild(v);
	stockVies.push(v);
}
 
// Création des rangées d'aliens
for (i=0; i<55; i++){
	var a:TilesAliens = new TilesAliens();
	a.x = i%C*T;
	a.y = int(i/C)*T+80;
	a.marge = a.width*.5;
	a.gotoAndStop(int(i/C)+1);
	addChild(a);
	stockAliens.push(a);
}

Je gère l'affichage des vies et des rangées d'aliens, rien de complexe si vous avez travaillé l'exercice du Frogger.

bunkers = new Bunkers();

Là ça parait simple mais il faut une explication, pour simplifier le programme j'ai créé à la main tous les blocs des bunkers et les ai posé dans un clip. Au cours du jeu les blocs vont se casser, je suis donc obligé de recréer le clip du départ si je veux réinitialiser tous les blocs. J'aurai pu effectuer une boucle pour remettre tous les clips des blocs dans leur position initiale, mais une simple récréation du conteneur suffit.

laser.dir = -1;
stockTirs.push(laser);

Vous le verrez par la suite, mais tous les tirs sont gérés à partir du même tableau, y compris celui du joueur, je le place donc en premier dans le tableau de stockage des tirs et lui donne une vitesse sur Y négative que je lui passe en paramètre.

Un fois tous les paramètres du jeu initialisés je lance les écouteurs principaux.

// Boucle principale
function main(e:Event):void{
	controles();
	gestionTirs();
	gestionAliens();
	gestionSoucoupe();
}

La fonction principale du programme, elle pilote toutes les fonctions essentielles via un événement ENTER_FRAME, pour rappel ce n'est pas forcément un bon choix d'utiliser un ENTER_FRAME, mais cela répond à nos besoins immédiats, bien que nous allons enfin parler de la gestion de la cadence d'un jeu de manière différente un peu plus loin.

// Déplacement de la base laser
function controles():void{
	with(base){
		if (currentFrame == 1)	{
			x += sens*10;
			if (x>W-marge) 	x = W-marge;
			if (x<marge) 	x = marge;
			if (tirer && !laser.visible){
				initLaser();
				laser.visible = true;
				sonTirLaser.play();
			}
		}
	}
}

La gestion de la base de tir laser du joueur, deux petites choses sont à noter, tout d'abord je regarde la frame affichée, rappellez-vous que la base est un clip qui contient une frame d'affichage classique et une animation pour son explosion, donc tant que la base n'est pas en train d'exploser elle peut bouger et tirer, sinon le joueur ne peut rien faire. D'autre part, vous remarquerez que j'utilise la propriété “visible” du laser pour savoir si le joueur peut tirer. Cela me permet d'éviter de supprimer et recréer le laser à chaque tir, comme le jeu n'accepte qu'un seul tir laser à la fois pour le joueur je peux très bien passer le laser en invisible lorsqu'il ne doit pas apparaître, c'est plus simple et plus léger pour le programme.

Tant qu'on y est on va se débarrasser des choses simples qui ont un rapport avec ce que nous venons de voir, j'ai benné en vrac à la fin du programme toutes les petites fonction utilisables partout :

function initLaser():void{
	laser.x = base.x;
	laser.y = base.y;
	laser.visible = false;
}

Quand j'ai besoin de réinitialiser le laser (il a touché un objet, est sorti de l'écran, …) j'utilise cette petite fonction toute simple.

// Déplacement des aliens
function gestionAliens():void{
 
	// Déplace les aliens au bon tempo
	if (getTimer()-temps>=vitesse){
		temps = getTimer();
		if (vitesse<10) vitesse = 10;
 
		// Musique en rythme
		cadence.gotoAndStop(cadencePos+2);
		cadencePos++;
		cadencePos %= 4;
 
		var a:Object;
 
		// déplacement
		for each (a in stockAliens){
			a.getChildAt(0).nextFrame();// joue l'animation de l'alien
			if (a.x+a.width+D>W || a.x+D<0) descend = true;
			a.x += D;
		}
 
		if (descend){
			vitesse -=  40;
			D *= -1;
			for each(a in stockAliens){
				a.y += T*.5; 
				a.x += D;
				if (a.y > base.y){
					vies = 0;
					base.gotoAndPlay(2);
					finInvader(2,1);
				}
			}
			descend = false;
		}
 
		// Si il ne reste plus aucun alien a tuer
		if (stockAliens.length==0){
			if (vies<9)	vies++;
			niveau++>4 ? finInvader(3,1) : finInvader(4,niveau);
		}
 
		// Tir alien aléatoire en fonction de la vitesse du jeu
		if (Math.random()<.3){
			var t:TirAlien = new TirAlien();
			a = stockAliens[int(Math.random()*stockAliens.length)];
			t.x = a.x+a.marge;
			t.y = a.y+a.marge;
			t.dir = 1;
			sonTirAlien.play();
			stockTirs.push(t);
			addChild(t);
		}
	}
}

Ok on attaque la partie lourde de la chose, la gestion des aliens, on s'étudie ça pas à pas :

// Déplace les aliens au bon tempo
if (getTimer()-temps>=vitesse){
	temps = getTimer();
	if (vitesse<10) vitesse = 10;

Depuis pratiquement le début des exercice je vous serine avec le fait qu'utiliser un ENTER_FRAME n'est pas une solution miracle dans un jeu vidéo et qu'il est préférable d'utiliser une cadence gérée par le temps qui passe, c'est exactement ce que nous allons faire ici pour la gestion des aliens et uniquement pour la gestion des aliens.

Pourquoi ?

Parce que j'ai besoin que les aliens puissent être gérés à une cadence différente de celle du reste du jeu, la base laser, les soucoupes, les tirs, conservent toujours la même vitesse, celle du jeu, mais les aliens eux vont aller de plus en plus vite. J'aurai pu passer par une variable “vitesse” propre aux aliens pour accélérer la cadence, mais je pense que c'est le bon moment pour vous montrer une gestion plus réaliste du temps.

getTimer() me renvoie le temps passé depuis l'ouverture du SWF.
temps est la dernière valeur de getTimer() enregistrée.
vitesse est la vitesse du jeu définie dans la fonction init() et modifiée en cours de jeu.

Je commence donc par regarder le temps passé depuis l'ouverture du SWF, je lui retire le dernier temps enregistré et je compare le résultat à la vitesse que doivent avoir les aliens. Si le temps trouvé est supérieur à la vitesse choisie, j'enregistre un nouveau temps témoins et je lance les actions des aliens.

C'est généralement cette astuce qu'on utilise pour cadencer proprement un jeu avec Flash, ceci permet d'éviter entre autre la triche qui consiste à saturer la mémoire pour ralentir le SWF. Là où un ENTER_FRAME va tenter d'atteindre la vitesse du projet en fonction de la puissance disponible, la gestion du temps passé va permettre de fixer une fois pour toute la cadence, quelle que soit la puissance disponible.

Attention, pour cet exercice je n'utilise pas la gestion du temps comme il le faudrait puisque la fonction de gestion des aliens dépend déjà d'un ENTER_FRAME, je ne m'en sert donc que pour cadencer les aliens par rapport au reste du jeu et non pour cadencer le jeu entier, mais ça me permet de vous montrer l'astuce ;-)

// Musique en rythme
cadence.gotoAndStop(cadencePos+2);
cadencePos++;
cadencePos %= 4;

Ok voilà une autre grosse astuce pour le son des aliens, nous sommes d'accord que la gestion des aliens est calé à une certaine cadence gérée par le temps qui passe. Mon clip conteneur “cadence” posséde 5 frames, la première est vide et les autres contiennent chacune un son différent. A chaque fois que le jeu met à jour la gestion des aliens, je joue une frame du clip “cadence” pour jouer un son différent. Vous pourriez très bien vous passer du clip et jouer directement un son différent, mais ça réduit le code de faire comme ça est c'est une simple astuce qui ne joue en rien sur la technique du jeu.

var a:Object;
 
// déplacement
for each (a in stockAliens){
	a.getChildAt(0).nextFrame();// joue l'animation de l'alien
	if (a.x+a.width+D>W || a.x+D<0) descend = true;
	a.x += D;
}

Dans cette première boucle je parcours le stock des aliens, pour chacun je joue l'animation de l'alien et je regarde si il touche un bord, si c'est le cas j'indique que les aliens doivent descendre d'un cran, puis je les déplaces à la vitesse/direction.

if (descend){
	vitesse -=  40;
	D *= -1;
	for each(a in stockAliens){
		a.y += T*.5; 
		a.x += D;
		if (a.y > base.y){
			vies = 0;
			base.gotoAndPlay(2);
			finInvader(2,1);
		}
	}
	descend = false;
}

Une fois tous les aliens déplacés, si ils doivent descendre, j'augmente la cadence (je réduit la valeur de la vitesse du jeu) et je change la direction/vitesse des aliens. Puis je refais une boucle pour faire descendre tous les aliens d'un cran, les déplacer dans le bon sens, et vérifier si aucun ne touche le bas de l'écran.

Vu qu'on l'utilise ici on va se débarrasser d'une autre fonction globale :

// Partie perdue
function finInvader(f:int,n:int) {
	niveau = n;
	if(pisteSoucoupe) pisteSoucoupe.stop();
	addChild(panneau);
	panneau.gotoAndStop(f);
	stage.removeEventListener(Event.ENTER_FRAME,main);
	stage.removeEventListener(KeyboardEvent.KEY_DOWN, appuie);
	stage.removeEventListener(KeyboardEvent.KEY_UP, relache);
}

Lorsque le joueur perds une vie ou fini le niveau actuel il peut se passer trois choses, soit il n'a plus de vie et la partie est terminée, soit il a tué tous les aliens et il passe au niveau suivant, soit il a terminé tous les niveaux et il gagne le jeu.

Si on suit à la lettre l'analyse du jeu, le jeu ne devrait pas avoir de fin, cependant il fini par devenir injouable au bout d'un certain temps, à vous de mieux le régler pour éviter ça si vous souhaitez coller au plus près du jeu original.

Pas grand chose à dire sur cette fonction en elle même, elle ajoute un panneau d'interface, retire les écouteurs, met à jour le niveau et coupe le son de la soucoupe si il est en cours.

// Si il ne reste plus aucun alien a tuer
if (stockAliens.length==0){
	if (vies<9)	vies++;
	niveau++>4 ? finInvader(3,1) : finInvader(4,niveau);
}

Le joueur gagne une vie par niveau complété, pour le reste vous êtes en mesure de décrypter ;-)
Je vous rappelles que nous somme au niveau 2 des exercices, je ne vais donc plus ré-expliquer ce que nous avons déjà vu dans les exercices précédents.

// Tir alien aléatoire en fonction de la vitesse du jeu
if (Math.random()<.3){
	var t:TirAlien = new TirAlien();
	a = stockAliens[int(Math.random()*stockAliens.length)];
	t.x = a.x+a.marge;
	t.y = a.y+a.marge;
	t.dir = 1;
	sonTirAlien.play();
	stockTirs.push(t);
	addChild(t);
}

Une fois de temps en temps on choisi aléatoirement un alien dans le stock disponible et on le fait tirer. Attention, plus la cadence est rapide et plus les aliens vont tirer souvent, si vous voulez conserver une cohérence ici il vous suffit de gérer ça en fonction de la vitesse du jeu et non de la cadence de rafraîchissement des aliens.

Petite pause concernant la soucoupe :

// Gestion de la soucoupe
function gestionSoucoupe():void{
	with(soucoupe){
		if (visible){
			x += dir*3;
			if (x<-100 || x>580){
				visible = false;
				pisteSoucoupe.stop();
				return;
			}
		} else if (Math.random()<.005) {
			dir = Math.random()<.5 ? 1:-1;
			x = dir>0 ? -50:520;
			visible = true;
			pisteSoucoupe = sonSoucoupe.play(0,10);
		} else {
			x = -50;
		}
	}
}

Il n'y a toujours qu'une seule soucoupe affichée à l'écran il est donc inutile de la supprimer et de la recréer, comme pour le laser je vais utiliser sa méthode “visible” pour savoir quand elle doit et peut intervenir dans le jeu. Si elle est visible elle se déplace sur l'axe X, si elle sort de la zone de jeu elle passe invisible. Sinon elle apparaît à un rythme aléatoire à un bord de l'écran avec une direction appropriée. Tant qu'elle n'est pas visible on la place hors de l'écran pour éviter la collision avec le laser.

C'est terminé pour les aliens et la soucoupe, regardons ce qu'il se passe du côté des tirs :

// gestion de tous les tirs
function gestionTirs():void{
	for (var i in stockTirs){
		var t:Object = stockTirs[i];
		if(i=="0"){
			if (laser.visible) {
				laser.y>0 ? laser.y -= 20 : laser.visible = false;
				for (var j:String in stockAliens){
					var a:MovieClip = stockAliens[j] as MovieClip;
					if (laser.hitTestObject(a)){
						explosion(a.x+a.marge,a.y+a.marge);
						removeChild(a);
						stockAliens.splice(j,1);
						initLaser();
						vitesse -= 5;
						score++;
					}
				}
				// Si le laser touche la soucoupe
				if (soucoupe.hitTestObject(laser)){
					explosion(soucoupe.x,soucoupe.y);
					soucoupe.visible = false;
					pisteSoucoupe.stop();
					sonTueSoucoupe.play();
					initLaser();
				}
			}
		} else if(t.y<H) {
			t.y += 6*t.dir; 
			if (base.hitTestPoint(t.x,t.y) && base.currentFrame == 1){
				killTirAlien(t,i);
				base.gotoAndPlay(2);
				retireVies();
			}
		} else {
			killTirAlien(t,i);
			return;
		}
 
		// touche un bunker
		for (var k:int = 0; k<bunkers.numChildren; k++) {
			var b = bunkers.getChildAt(k);
			if (t.hitTestObject(b)){
				t.dir>0 ? killTirAlien(t,i) : initLaser();
				b.currentFrame == 4 ? bunkers.removeChild(b) : b.nextFrame();
				sonCasseBunker.play();
				return;
			}
		}
	}
}

Ca peut paraître un peu lourd tout ça mais pas d'inquiétude, avant de se lancer dans le décorticage du code je vais vous toucher un mot sur la structure. J'ai fait le choix de gérer tous les tirs, qu'ils soient amis ou ennemis, dans une même boucle et dans un même tableau de stockage. J'aurai très bien pu gérer indépendamment le tir du joueur et ceux des ennemis, mais si on réfléchit un peu au fonctionnement de l'ensemble tous les tirs sont des objets réagissant de la même manière, ce qui change c'est leur direction et ce qu'ils touchent. Ca me semble donc plus approprié ici de tout gérer au même endroit, d'autant que j'ai fait l'impasse sur une autre propriété initiale du jeu d'origine : “le tir du joueur peut détruire un tir alien”, ce qui m'évite d'autres boucles inutiles pour l'exercice, mais ne vous empêche pas de les faire si vous souhaitez coller au plus près du jeu d'origine.

Je commence donc par faire une boucle sur tous les objets du stock de tirs.

if(i=="0"){
	if (laser.visible) {
		laser.y>0 ? laser.y -= 20 : laser.visible = false;
		for (var j:String in stockAliens){
			var a:MovieClip = stockAliens[j] as MovieClip;
			if (laser.hitTestObject(a)){
				explosion(a.x+a.marge,a.y+a.marge);
				removeChild(a);
				stockAliens.splice(j,1);
				initLaser();
				vitesse -= 5;
				score++;
			}
		}
		// Si le laser touche la soucoupe
		if (soucoupe.hitTestObject(laser)){
			explosion(soucoupe.x,soucoupe.y);
			soucoupe.visible = false;
			pisteSoucoupe.stop();
			sonTueSoucoupe.play();
			initLaser();
		}
	}
}

La première des choses à gérer c'est le tir du joueur, rappelez-vous que dans la fonction init() j'ai placé le laser au premier index du stock des tirs, donc à l'index 0 j'ai mon laser. Si il est visible on le gère, sinon on passe aux autres tirs. Dans sa gestion, je le déplace sur l'axe Y et je regarde si il sort en haut de l'écran, si c'est le cas il passe invisible. Ensuite je vais regarder si il touche un alien, pour ça c'est très simple, je boucle sur le stock des aliens et je teste la collision entre le laser et chaque alien du stock. Si il y a collision, j'affiche l'explosion, je retire les objets concernés de l'affichage et je met à jour mes tableaux de stockage. A chaque alien tué, j'incrémente le score et j'augmente la cadence des aliens. Enfin, si le laser touche une soucoupe on détruit la soucoupe et on réinitialise le laser.

} else if(t.y<H) {
	t.y += 6*t.dir; 
	if (base.hitTestPoint(t.x,t.y) && base.currentFrame == 1){
		killTirAlien(t,i);
		base.gotoAndPlay(2);
		retireVies();
	}

On en a terminé avec le laser, on peut donc s'occuper de chaque tir alien, je commence par regarder si il ne touche pas le bas de l'écran, si ce n'est pas le cas je le déplace et je regarde la collision avec la base laser du joueur, on va gérer les bunkers indépendamment.

else {
	killTirAlien(t,i);
	return;
}

Le tir alien est au bas de l'écran, je le détruit et sort de la fonction.

// touche un bunker
for (var k:int = 0; k<bunkers.numChildren; k++) {
	var b = bunkers.getChildAt(k);
	if (t.hitTestObject(b)){
		t.dir>0 ? killTirAlien(t,i) : initLaser();
		b.currentFrame == 4 ? bunkers.removeChild(b) : b.nextFrame();
		sonCasseBunker.play();
		return;
	}
}

La gestion des collisions avec les bunkers est gérée à part car elle concerne à la fois le laser et les tirs aliens. Elle consiste en une boucle sur tous les objets contenus dans le conteneur “bunkers”, selon le tir qui touche un bloc, on détruit le tir et on change de frame dans le bloc, on retire carrément le bloc si il est totalement détruit.

Dans cette grosse fonction vous avez certainement du remarquer la manière dont je détruit un tir alien, débarrassons-nous de la fonction générique :

function killTirAlien(ob:Object, id:int):void{
	ob.parent.removeChild(ob);
	stockTirs.splice(id,1);
}

Pas grand chose à expliquer, on détruit l'objet et on met à jour le tableau de stockage correspondant.

Vous avez aussi du remarquer la gestion de la vie si un tir alien touche la base du joueur :

function retireVies():void{
	vies--;
	removeChild(stockVies[vies]);
	stockVies.splice(vies,1);
	if (vies<=0) finInvader(2,1);
}

Rien à dire non plus à ce niveau, vous êtes en mesure de déchiffrer ce dernier bout de code.

Et voilà, nous avons terminé cet exercice.

Conclusion

Space Invader est le premier jeu un peu complexe que nous sommes amenés à faire dans cette série d'exercices, il commence à y avoir beaucoup d'objets à gérer, certains animé, d'autres non, il utilise des cadences d'animations différentes, gère l'accroissement de la difficulté en fonction des niveaux passés, etc… Il y a peu de nouvelles astuces utilisées ici, la plus importante réside dans le timing indépendant des aliens, mais la structure globale du jeu est importante pour les exercices suivants.

Les sources