Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Exercice pratique : la COURSE

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par Monsieur Spi, le 01 août 2013

Bonjour,

Aujourd'hui on s'attaque aux jeux de courses, un exercice particulier qui change de la méthode habituelle utilisée dans les exercices précédents, aussi je vais également adapter mon plan de rédaction et toute l'étude va se faire pas à pas.

Comme vous allez le voir il s'agit plus ici de dessiner et de partitionner l'espace que d'utiliser des formules complexes, je vais utiliser des artifices pour éviter de longues formules de maths mais le résultat est le même pour la méthode générale.

Vous allez également constater que le comportement des voitures contrôlées par l'ordinateur n'est pas des plus précis, c'est normal puisque je n'ai pas pris le temps de chercher les trajectoires optimales en fonction des réactions des voitures, mais ça ne change rien à la technique qui est derrière et ce sera à vous d'ajouter en fonction de vos préférences.

Enfin, la méthode exposée ci-après est à prendre de manière globale, le parti pris d'utiliser des Bitmaps est uniquement pour simplifier les explications, la technique fonctionnerait tout aussi bien avec des tuiles ou des formules de math si le cœur vous en 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

Les pré-requis

Etude préliminaire

Tout d'abord le JEU DE COURSE c'est quoi ?

On ne parle pas ici d'un jeu en particulier, mais d'une catégorie de jeux. Tout le monde sait en quoi consiste un jeu de course, les joueurs s'affrontent sur un circuit avec un véhicule et le premier arrivé à gagné.

En matière de jeux vidéo de course il existe différents rendus : la vue top down, c'est à dire vu du dessus tel un oiseau, la vue latérale, très peu utilisée, la vue isométrique, très employée dans les vieux jeux vidéo, les vue à la troisième personne ou subjective, utilisée dans les jeux plus récents utilisant la 3D.

Si vous avez déjà tenté de réaliser un jeu de course vous vous êtes sûrement confronté au problème de la gestion des collisions et de la reconnaissance du terrain par les véhicules, quelle que soit le rendu choisi. Comment obtenir une détection très fine de la forme de la piste, comment faire réagir un véhicule en fonction du terrain sur lequel il roule, comment indiquer à l'ordinateur qu'il doit suivre un chemin dans le cas des adversaires contrôlés par l'ordinateur, comment gérer une conduite plus ou moins réaliste, … ?

Ce sont autant de questions que nous allons aborder avec cet exercice.

Préparation - partie 1

Avant de commencer nous allons devoir mettre quelques petites choses au point, si vous avez déjà créé quelques jeux (ou fait les exercices précédent de cette série), vous avez sans doute pris l'habitude de vous servir de grilles, de tuiles, de tableaux à deux dimensions, et de formules de collisions entre une hitbox et tout un tas de choses (point, grille, cercle, droite, segment, …).

Si le principe général reste le même, les outils dont nous allons nous servir sont bien différent de ce à quoi on est habitué. Ici les maîtres mots seront “Bitmap”, “BitmapData” et “Point”.

Notre premier problème concerne le circuit lui même :

Comme vous pouvez le constater, le circuit (ici un circuit de Mario Kart que je vais utiliser pour l'exercice), est plutôt grand. La première option à laquelle on pense c'est d'utiliser une grille comme pour du Tile Based, et tracer chaque tuile une par une, sauf que pour obtenir une précision suffisante il faudrait des tuiles mesurant presque un pixel de côté, or pour un terrain de 800*800 pixels cela nous fait 640 000 tuiles à se dessiner à la main puis à placer dans un tableau, ça fait beaucoup et ce n'est pas efficace. Une autre option est de s'orienter vers la technique du flipper, c'est à dire utiliser des formules de math pour déterminer chaque courbe, chaque ligne droite, chaque segment, chaque mur, etc…, c'est compliqué et là encore assez peu efficace car si cela nous permet de tracer un circuit cela ne nous permet pas de détecter le terrain et ses variations. Pour afficher le terrain nous allons donc nous servir tout simplement d'un Bitmap, de l'image du terrain elle même tout simplement, et nous allons gérer les collisions d'une autre manière, c'est encore la méthode la plus simple. Notez qu'un Bitmap est par définition un tableau (donc une grille) rempli de pixels de couleur, ce qui nous ramène au Tile Based. En ce qui concerne la préparation de l'affichage du terrain c'est donc tranquille, on se sert juste de l'image du terrain à afficher, plutôt chouette non ?

Passons à la méthode pour détecter les collisions et les variations de terrain. On a vu plus haut que les méthodes habituelles étaient peu pratiques ou imprécises, il nous faut donc trouver un autre artifice qui fera l'affaire. Une solution simple, là encore, va être d'utiliser une ColorMap, une carte des couleurs si vous préférez. Il en existe plusieurs types, celle que nous allons utiliser pour les collisions sera de la forme la plus simple histoire de ne pas compliquer d'avantage la mécanique :

Cette image fait exactement la taille du circuit, elle reprend les zones importantes comme le bitume, l'herbe et les murs mais cette fois avec une palette de couleurs limitée. Ici nous avons besoin de différentier 3 choses, nous allons donc utiliser trois couleurs et uniquement trois couleurs, méfiez vous des formats d'images compressés et utilisez un PNG limité à 3 couleurs pour être sur de ne pas avoir de couleurs intermédiaires liées à la compression d'un JPEG par exemple. Pour savoir ce que le véhicule rencontre sur son chemin il nous suffit de vérifier la couleur de la ColorMap d'un point situé sur le véhicule ou autour. Imaginons un point au centre du véhicule, si le point correspond à du noir sur la ColorMap alors on est sur du bitume, du bleu il s'agit d'un mur, du rose c'est de l'herbe, etc… Bien sur vous pouvez ajouter autant d'obstacles et de types de terrain que vous le souhaitez, il suffit d'attribuer une couleur à chaque chose : tâche d'huile, paille, eau, turbo, bonus, …

Nous avons réglé pas mal de problèmes, d'une part l'affichage du circuit, facile il s'agit juste d'une image, d'autre part la reconnaissance des surfaces, là aussi très simple à l'aide d'une ColorMap de la taille du circuit, reste à présent à gérer les déplacements des véhicules contrôlés par l'ordinateur. Partis comme on l'est vous devez vous douter qu'il y aura encore une astuce avec un Bitmap, mais je ne souhaite pas vous en parler tout de suite, nous verrons cela en temps voulu, pour le moment nous avons assez d'infos préparatoires pour commencer à programmer notre jeu, limité au seul véhicule du joueur.

Résumons la préparation :

Dessinez votre circuit et sa ColorMap, dans la bibliothèque de Flash exportez ces deux Bitmap pour une utilisation avec AS (cela se fait exactement comme pour les MovieClip), le circuit portera le nom de “Terrain” et la ColorMap le nom de “ColormapCollisions”.

Créez ensuite un MovieClip nommé “Joueur” dans lequel vous allez placer la voiture, faites en sorte que le repère de position du clip soit au centre de la voiture, ce sera plus pratique pour les calculs, et exportez le pour AS.

Créez également un MovieClip nommé “Marqueur” qui contiendra uniquement un point de la taille que vous souhaitez et dont le repère de position est lui aussi au centre du point, et exportez le pour AS. Ce marqueur ne nous servira que pour vous montrer visuellement les points qu'on va utiliser au cours de l'exercice, il est donc inutile pour une version définitive de votre jeu.

C'est tout pour la préparation, il y aura d'autres choses à créer par la suite mais on verra ça en temps et heure. Pour le moment notre objectif va être d'afficher le terrain, d'y poser la voiture, de permettre au joueur de la diriger et de gérer les interactions avec les différentes surfaces, on s'occupera des adversaires dans un second temps.

Programme principal

Notre programme va pour le moment comporter une partie principale au sein de votre FLA, et les classes “Marqueur” pour afficher les points de repère, “Joueur” pour toute la gestion du joueur et “Collisions” pour toute la gestion des collisions, voici ce que cela donne.

Sur la première frame de votre FLA écrivez :

var mapCollisions:BitmapData;
var terrain:Bitmap;
var joueur:Joueur;
 
init();
 
function init():void{
 
	mapCollisions = new ColormapCollisions();
	terrain = new Bitmap(new Terrain());
	joueur = new Joueur(75,370,mapCollisions);
 
	addChild(terrain);
	addChild(joueur);
}

On crée tout simplement tout le matériel dont on a besoin, à savoir la map pour les collisions, le terrain pour l'affichage et le joueur. Vous noterez qu'il n'y a pas besoin d'ajouter la map des collisions à la liste d'affichage, on a juste besoin qu'elle existe.

Classe Joueur

Voyons la classe “Joueur”, à la racine de votre projet créez un nouveau document AS nommé “Joueur” et écrivez :

package {
 
	import flash.display.MovieClip;	
	import flash.events.Event;
	import flash.events.KeyboardEvent;
	import flash.geom.Point;
	import flash.display.BitmapData;
 
	public class Joueur extends MovieClip { 
 
		private var W:Number = width/2;
		private var H:Number = height/2;
 
		private var angle:Number;
		private var dir:Number;
		private var acc:Number;
		private var vitMax:Number;
		private var dirMax:Number;
 
		public var dx:int;
		public var dy:int;
		public var vit:Number;
		public var rebond:Boolean;
		public var fri:Number;
		public var freine:Boolean;
		public var sante:int;
 
		private var idealX:Number;
		private var idealY:Number;
		private var moveX:Number;
		private var moveY:Number;
		private var glisse:Number;
 
		public var p1:Point = new Point(W,  -H);
		public var p2:Point = new Point(-W, -H);
		public var p3:Point = new Point(W,   H);
		public var p4:Point = new Point(-W,  H);	
		public var p5:Point = new Point(0,   H);	
		public var p6:Point = new Point(0,  -H);
 
		private var pt1:Marqueur = new Marqueur(p1.x, p1.y);
		private var pt2:Marqueur = new Marqueur(p2.x, p2.y);
		private var pt3:Marqueur = new Marqueur(p3.x, p3.y);
		private var pt4:Marqueur = new Marqueur(p4.x, p4.y);
		private var pt5:Marqueur = new Marqueur(p5.x, p5.y);
		private var pt6:Marqueur = new Marqueur(p6.x, p6.y);
 
		private var col:Collisions;
		private var mapCollisions:BitmapData;
 
		private var gauche:int;	
		private var droite:int;
		private var haut:int;
		private var bas:int;
 
		public function Joueur (X:int, Y:int, mapColl:BitmapData) {
 
			x = X;
			y = Y;
 
			addChild(pt1);
			addChild(pt2);
			addChild(pt3);
			addChild(pt4);
			addChild(pt5);
			addChild(pt6);
 
			mapCollisions = mapColl;
			col = new Collisions();
 
			addEventListener(Event.ADDED_TO_STAGE, init);
		}
 
		private function init(e:Event):void {
			sante = 	100;
			glisse = 	.12;
			moveX = 	0;
			moveY = 	0;
			angle = 	0;
			dir = 		0;
			acc = 		.30;
			vitMax = 	5;
			dirMax = 	1.5;
			W = 		width/2;
			H = 		height / 2;
			dx = 		0;
			dy = 		0;
			vit = 		0;
			rebond = 	false;
			fri = 		.95;
			freine = 	false;
			addEventListener(Event.ENTER_FRAME, update);
			stage.addEventListener(KeyboardEvent.KEY_DOWN, appuyer);
			stage.addEventListener(KeyboardEvent.KEY_UP, relacher);	
			removeEventListener(Event.ADDED_TO_STAGE, init);
		}
 
		// appuyer sur une touche
		public function appuyer (e:KeyboardEvent):void{
			if (e.keyCode == 37) 	gauche = 1;
			if (e.keyCode == 39) 	droite = 1;
			if (e.keyCode == 38)	haut = 1;
			if (e.keyCode == 40)	bas = 1;	
		}
 
		// Relâcher une touche
		public function relacher (e:KeyboardEvent):void{
			if (e.keyCode == 37) 	gauche = 0;
			if (e.keyCode == 39) 	droite = 0;
			if (e.keyCode == 38)	haut = 0;
			if (e.keyCode == 40)	bas = 0;
		}	
 
		private function update(e:Event):void {
 
			col.hitTest(this,mapCollisions);
 
			if (!rebond) {
				dx = droite-gauche;
				dy = haut-bas;
				angle = Math.atan2(dy, dx);
			}
 
			if (freine) vit *= 0.9;
 
			vit += acc*dy;
			dir += dx*0.2;
			dir *= fri;
 
			if (Math.abs(vit) > vitMax) vit = vitMax*dy;
			if (Math.abs(dir) > dirMax) dir = dirMax*dx;
			if (Math.abs(dir) < 0.05) dir = 0
 
			vit *= fri;
			if (0.1 > Math.abs(vit) > 0)  vit = 0;
 
			rotation += 	dir*vit;
			angle = 		(rotation - 90) * Math.PI / 180;
			idealX = 		Math.cos(angle)*vit;
			idealY = 		Math.sin(angle)*vit;
			moveX += 		(idealX-moveX)*glisse;
			moveY += 		(idealY-moveY)*glisse;
			if (rebond) {
				x -= moveX;
				y -= moveY;
			} else {
				x += moveX;
				y += moveY;
			}
 
			dir -= (dir * 0.1);
		}
	}
}

Je considère que vous avez lu les pré-requis, on va donc estimer que vous savez créer et utiliser une classe et avancer un peu plus vite.

On a beaucoup de variables, je ne vais pas les expliquer une par une mais on va les passer en revue par blocs utiles.

private var W:Number = width/2;
private var H:Number = height/2;
 
private var angle:Number;
private var dir:Number;
private var acc:Number;
private var vitMax:Number;
private var dirMax:Number;
 
public var dx:int;
public var dy:int;
public var vit:Number;
public var rebond:Boolean;
public var fri:Number;
public var freine:Boolean;
public var sante:int;
 
private var idealX:Number;
private var idealY:Number;
private var moveX:Number;
private var moveY:Number;
private var glisse:Number;

Tout ceci nous sert pour diriger le véhicule, angle, accélération, direction, vitesse max, friction, etc….

public var p1:Point = new Point(W,  -H);
public var p2:Point = new Point(-W, -H);
public var p3:Point = new Point(W,   H);
public var p4:Point = new Point(-W,  H);	
public var p5:Point = new Point(0,   H);	
public var p6:Point = new Point(0,  -H);

Ces variables publiques sont des points de contrôle que l'on va placer sur le véhicule, comme en formule 1, il s'agit de petits capteurs placés à des endroits importants qui vont nous servir pour obtenir les informations sur le terrain. J'ai fait le choix de créer 6 points de contrôle, vous pouvez en mettre plus ou moins selon la précision que vous souhaitez obtenir. J'ai choisi d'en placer un sur chaque angle de la voiture, plus un au centre du pare choc avant et un au centre du pare choc arrière, ce devrait être suffisant pour nos besoins.

private var pt1:Marqueur = new Marqueur(p1.x, p1.y);
private var pt2:Marqueur = new Marqueur(p2.x, p2.y);
private var pt3:Marqueur = new Marqueur(p3.x, p3.y);
private var pt4:Marqueur = new Marqueur(p4.x, p4.y);
private var pt5:Marqueur = new Marqueur(p5.x, p5.y);
private var pt6:Marqueur = new Marqueur(p6.x, p6.y);

Ces marqueurs ne sont là que pour vous indiquer visuellement les points de contrôle qu'on à posé, ils ne servent strictement à rien dans le jeu lui même et vous pouvez vous en passer. Ils dépendent qu'une classe “Marqueur” qu'on étudiera plus tard mais qui ne contient que le positionnement du marqueur et son lien avec le MovieClip “Marqueur”.

private var col:Collisions;
private var mapCollisions:BitmapData;

Ici nous avons la gestion des collisions qui correspond à l'objet privé “col” et la ColorMap des collisions qui correspond à l'objet “mapCollisions”.

private var gauche:int;
private var droite:int;
private var haut:int;
private var bas:int;

Et on termine cette longue liste de variables avec celles qui servent au joueur à contrôler son véhicule.

public function Joueur (X:int, Y:int, mapColl:BitmapData) {
 
	x = X;
	y = Y;
 
	addChild(pt1);
	addChild(pt2);
	addChild(pt3);
	addChild(pt4);
	addChild(pt5);
	addChild(pt6);
 
	mapCollisions = mapColl;
	col = new Collisions();
 
	addEventListener(Event.ADDED_TO_STAGE, init);
}

Le constructeur du joueur, on lui passe trois paramètres qui sont sa position sur X, sa position sur Y et la ColorMap des collisions. On place le joueur, on ajoute les Marqueurs (inutiles sauf pour déboguer), on initialise la map des collisions et le calcul des collisions (que nous étudierons un peu plus tard), et pour finir on attend que le joueur soit ajouté à la liste d'affichage avant de faire quoi que ce soit d'autre.

private function init(e:Event):void {
	sante = 	100;
	glisse = 	.12;
	moveX = 	0;
	moveY = 	0;
	angle = 	0;
	dir = 		0;
	acc = 		.30;
	vitMax = 	5;
	dirMax = 	1.5;
	W = 		width/2;
	H = 		height / 2;
	dx = 		0;
	dy = 		0;
	vit = 		0;
	rebond = 	false;
	fri = 		.95;
	freine = 	false;
	addEventListener(Event.ENTER_FRAME, update);
	stage.addEventListener(KeyboardEvent.KEY_DOWN, appuyer);
	stage.addEventListener(KeyboardEvent.KEY_UP, relacher);	
	removeEventListener(Event.ADDED_TO_STAGE, init);
}

Le joueur a été ajouté à la liste d'affichage, on va donc initialiser toutes les variables utiles, retirer l'écouteur d'ajout à la liste d'affichage, ajouter les écouteurs clavier et un écouteur pour mettre à jour le joueur.

// appuyer sur une touche
public function appuyer (e:KeyboardEvent):void{
	if (e.keyCode == 37) 	gauche = 1;
	if (e.keyCode == 39) 	droite = 1;
	if (e.keyCode == 38)	haut = 1;
	if (e.keyCode == 40)	bas = 1;	
}
 
// Relâcher une touche
public function relacher (e:KeyboardEvent):void{
	if (e.keyCode == 37) 	gauche = 0;
	if (e.keyCode == 39) 	droite = 0;
	if (e.keyCode == 38)	haut = 0;
	if (e.keyCode == 40)	bas = 0;
}

Bon, là rien de bien neuf, on voit ce genre de chose depuis le début des exercices, on passe donc tout de suite à ce qui nous intéresse vraiment, la gestion du joueur.

private function update(e:Event):void {
 
	col.hitTest(this,mapCollisions);
 
	if (!rebond) {
		dx = droite-gauche;
		dy = haut-bas;
		angle = Math.atan2(dy, dx);
	}
 
	if (freine) vit *= 0.9;
 
	vit += acc*dy;
	dir += dx*0.2;
	dir *= fri;
 
	if (Math.abs(vit) > vitMax) vit = vitMax*dy;
	if (Math.abs(dir) > dirMax) dir = dirMax*dx;
	if (Math.abs(dir) < 0.05) dir = 0
 
	vit *= fri;
	if (0.1 > Math.abs(vit) > 0)  vit = 0;
 
	rotation += 		dir*vit;
	angle = 		(rotation - 90) * Math.PI / 180;
	idealX = 		Math.cos(angle)*vit;
	idealY = 		Math.sin(angle)*vit;
	moveX += 		(idealX-moveX)*glisse;
	moveY += 		(idealY-moveY)*glisse;
	if (rebond) {
		x -= moveX;
		y -= moveY;
	} else {
		x += moveX;
		y += moveY;
	}
 
	dir -= (dir * 0.1);
}

Ok, le joueur est mis à jour à chaque frame du programme.

col.hitTest(this,mapCollisions);

On commence par regarder les collisions (on étudie la classe Collisions juste après rassurez-vous).

if (!rebond) {
	dx = droite-gauche;
	dy = haut-bas;
	angle = Math.atan2(dy, dx);
}

Lorsque le joueur tape un mur il rebondit, avant de modifier ses paramètre on regarde donc si il n'est pas en train de rebondir, en effet quand le joueur rebondit on considère qu'il n'a plus la maîtrise de son véhicule. Si il ne rebondit pas donc, on récupère la direction sur chaque axe et l'orientation qui en résulte.

if (freine) vit *= 0.9;

Si le joueur freine ou est freiné par quelque chose, sa vitesse se réduit progressivement, ça il peut le faire même quand il rebondit sur un mur, c'est un réflexe de pilote ;-)

vit += acc*dy;
dir += dx*0.2;
dir *= fri;

On calcule ensuite la vitesse (dépend de l'accélération et de l'action choisie par le joueur, accélérer ou freiner), la direction (dépend de la direction choisie par le joueur, droite ou gauche, et de l'angle de braquage, ici je ne l'ai pas placé dans une variable, faites le si cela vous dérange). La direction est également influencée par la friction du sol, qui pourra bien sur varier selon nos besoins.

if (Math.abs(vit) > vitMax) vit = vitMax*dy;
if (Math.abs(dir) > dirMax) dir = dirMax*dx;
if (Math.abs(dir) < 0.05) dir = 0

On limite la vitesse et la direction aux valeurs maximales que le véhicule est capable de supporter, et on ajoute une valeur minimale à la direction, chose qui nous est utile pour lisser un peu le comportement de la voiture.

vit *= fri;
if (0.1 > Math.abs(vit) > 0)  vit = 0;

La vitesse est influencée par la friction du sol, et là aussi on lisse un peu le comportement en évitant que la vitesse soit une valeur qui n'atteigne jamais zéro, donc toujours active.

rotation += 		dir*vit;
angle = 		(rotation - 90) * Math.PI / 180;
idealX = 		Math.cos(angle)*vit;
idealY = 		Math.sin(angle)*vit;
moveX += 		(idealX-moveX)*glisse;
moveY += 		(idealY-moveY)*glisse;

La rotation de la voiture dépend de la direction et de la vitesse, son angle est égal à sa rotation (en radians) auquel je retire 90 degrès afin de placer ma voiture dans le sens de la marche. Je détermine ensuite deux choses, tout d'abord les mouvements idéaux sur X et Y, à savoir le mouvement normal que devrait avoir la voiture si on ne cherchais pas à rendre la conduite plus souple, comprenez par là qu'il s'agit du mouvement que le pilote cherche à faire. Puis le mouvement réel, c'est à dire ce que fait vraiment la voiture suite aux instructions du pilote, ce mouvement dépend du mouvement actuel, du mouvement idéal et du facteur de glisse. Ce que j'ai appelé “facteur de glisse” est en fait l'adhérence de la voiture en fonction du terrain, vous pouvez faire fluctuer cette valeur en fonction de la surface avec laquelle le véhicule est en contact, par exemple du bitume réagira mieux que de l'herbe et encore mieux que de l'huile, etc… Cette petite astuce va nous permettre de gérer le comportement de la voiture sans entrer dans des notions de forces et de physique bien complexes et que l'on devrait utiliser si on cherchait à faire un jeu de course 3D dit de simulation, fort heureusement ce n'est pas notre cas.

if (rebond) {
	x -= moveX;
	y -= moveY;
} else {
	x += moveX;
	y += moveY;
}

Il ne nous reste plus qu'à déplacer la voiture, mais tout dépend si elle rebondit suite à une collision ou pas, si elle rebondit elle repart en sens inverse du mouvement actuel.

dir -= (dir * 0.1);

Et enfin, dans tous les cas la direction subit un effet de friction, tant que le joueur tourne cela n'influence que très peu le comportement du véhicule, mais dès qu'il arrête de tourner la direction revient peu à peu au centre, ce qui est le comportement normal de tout véhicule ayant une certaine vitesse, si vous lâchez le volant les roues reviennent dans le sens de la marche.

C'est terminé pour le joueur, rien de très complexe normalement, il nous reste quand même à voir comment son gérées les collisions.

Classe Collisions

A la racine de votre projet créez un nouveau document AS nommé “Collisions” et écrivez :

package {
 
	import flash.display.BitmapData;
	import flash.geom.Point;	
 
	public class Collisions { 
 
		public function Collisions () {}		
 
		public function hitTest(ob:Object, map:BitmapData):void {
 
			var terre:int = 16711935;
			var mur:int = 6750207;
 
			// converti les coordonnées des points 
			var p1:Point = ob.localToGlobal(ob.p1);
			var p2:Point = ob.localToGlobal(ob.p2);
			var p3:Point = ob.localToGlobal(ob.p3);
			var p4:Point = ob.localToGlobal(ob.p4);
			var p5:Point = ob.localToGlobal(ob.p5);
			var p6:Point = ob.localToGlobal(ob.p6);
 
			// trouve les couleurs sous les points
			var c1:int = map.getPixel(p1.x, p1.y);
			var c2:int = map.getPixel(p2.x, p2.y);
			var c3:int = map.getPixel(p3.x, p3.y);
			var c4:int = map.getPixel(p4.x, p4.y);
			var c5:int = map.getPixel(p5.x, p5.y);
			var c6:int = map.getPixel(p6.x, p6.y);
 
			with (ob) {
				ob.freine = false; 
				if(c1==terre || c2==terre ||c3==terre || c4==terre) ob.freine = true;
 
				if(rebond){
					if (Math.abs(vit) > 0.1) {
						vit *= fri;
					} else {
						rebond = false;
						vit = 0;
					}
				} else {
					if (vit>0) {
						if(c6 == mur){
							vit = -vit/2;
							dy = 0;
							rebond = true;
							sante -= 25;
							return;
						}
						if(c1 == mur) rotation -= vit*vit;
						if(c2 == mur) rotation += vit*vit;
					}
					if (vit<0) {
						if(c5 == mur){
							vit = -vit/2;
							dy = 0;	
							rebond = true;
							sante -= 25;
							return;
						} 
						if(c3 == mur) rotation += vit*vit;
						if(c4 == mur) rotation -= vit*vit;
					}
				}
			}
		}
	}
}

Comme vous pouvez le voir dans les imports on n'utilisera dans cette classe que deux choses essentielles, un BitmapData et des Points.

public function hitTest(ob:Object, map:BitmapData):void { ... }

Cette fonction sert pour le test de collision entre un objet et la ColorMap des collisions, on lui passe donc ces deux paramètres.

var terre:int = 16711935;
var mur:int = 6750207;

Je défini deux variables, l'une représente la couleur que doit avoir la terre sur la ColorMap, l'autre la couleur que doivent avoir les murs.

// converti les coordonnées des points 
var p1:Point = ob.localToGlobal(ob.p1);
var p2:Point = ob.localToGlobal(ob.p2);
var p3:Point = ob.localToGlobal(ob.p3);
var p4:Point = ob.localToGlobal(ob.p4);
var p5:Point = ob.localToGlobal(ob.p5);
var p6:Point = ob.localToGlobal(ob.p6);

Notre voiture possède 6 points de contrôle, chacun placé dans la voiture et ayant donc des coordonnées locales à cette voiture, j'ai besoin de récupérer ces points mais de les convertir en coordonnées globales pour savoir où chacun se situe sur la ColorMap et non juste au sein de la voiture.

// trouve les couleurs sous les points
var c1:int = map.getPixel(p1.x, p1.y);
var c2:int = map.getPixel(p2.x, p2.y);
var c3:int = map.getPixel(p3.x, p3.y);
var c4:int = map.getPixel(p4.x, p4.y);
var c5:int = map.getPixel(p5.x, p5.y);
var c6:int = map.getPixel(p6.x, p6.y);

Pour chaque point je cherche la couleur qu'il survole sur la ColorMap.

with (ob) { ... }

Nous allons à présent travailler directement dans notre objet, la voiture, et modifier les paramètres en fonction du terrain.

ob.freine = false; 
if(c1==terre || c2==terre || c3==terre || c4==terre) ob.freine = true;

Par défaut la voiture ne freine pas, cependant si un des points de contrôle se trouve sur de la terre la voiture est ralentie.

if(rebond){
	if (Math.abs(vit) > 0.1) {
		vit *= fri;
	} else {
		rebond = false;
		vit = 0;
	}
}

Si la voiture est en train de rebondir suite à une précédente collision (c'est important), si sa vitesse est supérieur à une vitesse minimale la voiture est ralentie, sinon on considère que l'action du rebond est terminée et que la voiture est arrêtée.

} else {

Si elle n'est pas en train de rebondir…

if (vit>0) {
	if(c6 == mur){
		vit = -vit/2;
		dy = 0;
		rebond = true;
		sante -= 25;
		return;
	}
	if(c1 == mur) rotation -= vit*vit;
	if(c2 == mur) rotation += vit*vit;
}

Si la voiture est en train d'avancer, si le point central du pare choc avant rencontre un mur elle rebondit et elle prend du dégât, sa vitesse s'inverse et se divise par deux (effet du choc) et sa direction est figée au centre. J'ai ajouté une notion de dégâts (sante), on ne s'en servira pas pour le joueur mais pour les adversaires, vous verrez dans quel contexte. Si le point avant gauche ou le point avant droit touche un mur, la voiture racle contre le mur on simule ça avec une petite rotation de la voiture dans un sens ou dans l'autre, ceci évite de se retrouver bloqué face à un mur.

if (vit<0) {
	if(c5 == mur){
		vit = -vit/2;
		dy = 0;	
		rebond = true;
		sante -= 25;
		return;
	} 
	if(c3 == mur) rotation += vit*vit;
	if(c4 == mur) rotation -= vit*vit;
}

Si la voiture est en train de reculer, on réitère la même opération mais cette fois avec les points de contrôle situés à l'arrière du véhicule. Cette opération dans un sens puis dans l'autre selon si la voiture avance ou recule va nous permettre de mieux gérer des collisions multiples, par exemple lorsque la voiture se trouve coincée entre deux murs proches, on ne regarde la collision que dans le sens où la voiture se déplace et on évite ainsi les situations où la voiture se retrouve bloquée à rebondir à l'infini entre les deux murs.

C'est terminé pour les collisions, bien sur vous pouvez affiner et ajouter des facteurs qui influencent le comportement lors d'une collision, par exemple les tâches d'huiles, les turbos, les autres véhicules, etc….

Nous avons vu l'essentiel, comment conduire le véhicule et comment gérer facilement les collisions. Je ne vous ai pas parlé de la classe “Marqueur”, on va s'en débarrasser tout de suite avant de passer à la seconde partie de l'exercice concernant les adversaires.

Classe Marqueur

A la racine de votre projet créez un nouveau document AS nommé “Marqueur” et écrivez :

package {
 
	import flash.display.Sprite;	
 
	public class Marqueur extends Sprite { 
 
		public function Marqueur (X:int, Y:int) {
			x = X;
			y = Y;
		}
	}
}

Bon là pas grand chose à dire, les marqueurs ne sont que des repères visuels aidant au débogage, cette classe ne sert qu'à placer chaque marqueur au moment de sa création, vous pouvez très bien vous en passer.

Allez on attaque la seconde partie, les adversaires.

Préparation - partie 2

Si vous avez compris tout ce que nous venons de faire dans la première partie vous devriez vous en sortir haut la main dans celle-ci. Cette fois on s'attaque aux adversaires, la réaction des véhicules et les collisions ne devraient pas trop poser de problème car sensiblement identiques à celles du joueur à quelques détails prés. Ce qui va nous intéresser en revanche c'est l'IA, l'intelligence de chaque adversaire pour suivre le parcours du circuit sans avoir une trajectoire trop parfaite, ni complètement aléatoire.

Avant toute chose il nous faut une nouvelle voiture à afficher pour représenter les adversaires, créez donc un nouveau MovieClip nommé “Ordinateur” et exportez-le pour AS. Dedans mettre la même voiture que pour le joueur (avec les mêmes contraintes et la même taille) d'une couleur différente.

Il nous faudra bien sur une classe “Ordinateur” pour piloter les voitures mais on verra ça plus tard.

Si vous avez déjà joué à un jeu de voiture vous savez que le secret d'une course réussie réside dans la trajectoire que vous choisissez d'emprunter, un même virage pouvant se passer à fond si vous l'engagez correctement, ou pas si vous y entrez n'importe comment. Certains jeux dit “réalistes” vous affichent sur la piste une traîné lumineuse qui vous indique la meilleure trajectoire possible pour réaliser un record. Tous vos adversaires vont essayer de suivre au plus près cette marque afin d'optimiser leur conduite, et c'est précisément ce dont nous allons nous servir pour donner un chemin à suivre à l'ordinateur. Mais si l'on se contentait de tracer un chemin et posions les voitures dessus, elle se déplaceraient comme sur un rail et il n'y aurait plus de défit, on va donc n'utiliser que des points de repères à atteindre en fonction de chaque virage, un point placé à un endroit stratégique devient une cible à atteindre par la voiture qui va le viser. Mais cela entraîne un autre problème, comment déterminer quel point la voiture doit viser en fonction de sa position sur le circuit ?

La solution passe une nouvelle fois par un Bitmap qui va nous permettre de déterminer des zones, tant que la voiture se trouve dans une zone elle dispose d'un point unique à atteindre pour cette zone. On va donc créer une nouvelle ColorMap, mais cette fois elle ne servira que pour découper le circuit en zones plus petites. Pour bien vous montrer le découpage voici une capture où je superpose le terrain et la ColorMap :

Si on retire le terrain voici ce que nous obtenons :

Chaque couleur représente une zone, on déterminera les points à atteindre plus tard. Lorsqu'un adversaire contrôlé par l'ordinateur se déplace, il regarde sur quelle zone il se trouve sur la ColorMap et réagit en conséquence, exactement de la même manière que nous l'avons fait pour déterminer les surfaces du terrain lors des collisions. Attention, une fois de plus la ColorMap doit être un PNG avec une gamme de couleurs limitée pour s'assurer qu'il n'y ait pas de couleurs parasites liées à la compression, de plus vous ne devez laisser aucun trou entre les zones sinon les voitures pourraient se retrouver bloquées sans savoir quoi faire.

Créez donc cette nouvelle ColorMap, un simple Bitmap sous Photoshop par exemple, donnez lui le nom de “ColormapTrajets” et exportez le pour AS.

C'est tout pour la préparation, passons au code.

Modifications

On va commercer par modifier légèrement le programme principal, éditez votre projet et écrivez :

var zones:Array = [14211288, 11645361, 10395294, 8750469, 7500402, 6381921, 4934475, 3552822, 2039583,12698111, 9671679, 7105791, 5197823, 2829311, 251, 223, 179, 151, 119, 91, 66, 16777215];
 
var cibles:Array = [new Point(54,144), new Point(162,33), new Point(254,86), new Point(339,224), new Point(483,214), new Point(563,82), new Point(677,65), new Point(744,208), new Point(717,312), new Point(312,424), new Point(294,457), new Point(337,488), new Point(659,482), new Point(756,550), new Point(723,672), new Point(632,727), new Point(512,702), new Point(438,658), new Point(369,642), new Point(267,715), new Point(104,727), new Point(64,595)];
 
var i:int;
var X:int;
var Y:int;
 
var mapCollisions:BitmapData;
var mapTrajets:BitmapData;
var terrain:Bitmap;
var joueur:Joueur;
var ordi:Ordinateur;
var p:Point;
 
init();
 
function init():void{
 
	mapCollisions = new ColormapCollisions();
	mapTrajets = new ColormapTrajets();
	terrain = new Bitmap(new Terrain());
	joueur = new Joueur(75,370,mapCollisions);
 
	addChild(terrain);
	addChild(joueur);
 
	for (i=0; i<7;i++){
		X = (i%2+1)*25+25;
		Y = i*20+370;
		ordi = new Ordinateur(X,Y, mapTrajets,zones,cibles,mapCollisions);
		addChild(ordi);
	}
 
	for each(p in cibles){
		addChild(new Marqueur(p.x,p.y));
	}
}

Le premier tableau représente les couleurs de chaque zone du découpage du terrain, le second contient chaque point cible à atteindre pour une zone précise. Les deux tableaux correspondent exactement, c'est à dire que pour la zone 1 vous avez une couleur qui correspond au point 1 du tableau des cibles. Chaque point est placé à la main, c'est à dire que ça va être à vous de minutieusement placer les points sur le circuit et d'étudier la trajectoire idéale en prenant en compte le comportement des voitures. Les points se placent à l'intersection de deux zones et à la fin de la zone concernée afin que le joueur cible ce point tant qu'il se trouve sur la zone.

On crée ensuite la map des trajets qu'on vient de dessiner, ainsi qu'une variable “ordi” qui servira à placer les adversaires, et un point “p” qui servira uniquement à placer des marqueurs sur chaque point référencé dans le tableau des cibles, cela va vous permettre de voir où se trouvent les points que vous avez posé.

Dans la fonction “init”, à part le fait d'initialiser la nouvelle ColorMap, on crée une boucle pour poser 7 adversaires et les placer, vous noterez que chaque adversaire dépend d'une classe “Ordinateur” à laquelle on passe des paramètres qui sont : la position, la ColorMap des trajets, les zones, les cibles et la ColorMap des collisions.

Enfin, pour chaque cible on pose un marqueur sur le circuit afin de mieux les voir tant qu'on travaille.

Allez on attaque le code…

Classe Ordinateur

A la racine de votre projet créez un nouveau document AS nommé “Ordinateur” et écrivez :

package {
 
	import flash.display.MovieClip;	
	import flash.events.Event;
	import flash.geom.Point;
	import flash.display.BitmapData;
 
	public class Ordinateur extends MovieClip { 
 
		private const W:Number = width/2;
		private const H:Number = height/2;
 
		private var p:Point;
		private var cible:Point
		private var cibles:Array;
		private var portions:Array;
		private var col:Collisions;
		private var mapTrajets:BitmapData;
		private var mapCollisions:BitmapData;
 
		private var dx:int;
		private var dy:int;
		private var zone:int;
		private var _X:Number;
		private var _Y:Number;
		private var dir:Number;
		private var acc:Number;
		private var diff:Number;
		private var vitMax:Number;
		private var dirMax:Number;
		private var moveX:Number;
		private var moveY:Number;
		private var angle:Number;
		private var idealX:Number;
		private var idealY:Number;
		private var glisse:Number;
		private var angleActuel:Number;
		private var angleCible:Number;
 
		public var sante:int;
		public var vit:Number;
		public var fri:Number;
		public var rebond:Boolean;		
		public var freine:Boolean;
 
		public var p1:Point = new Point(W,  -H);
		public var p2:Point = new Point(-W, -H);
		public var p3:Point = new Point(W,   H);
		public var p4:Point = new Point(-W,  H);	
		public var p5:Point = new Point(0,   H);	
		public var p6:Point = new Point(0,  -H);
		public var p7:Point = new Point(0,  0);
 
		public function Ordinateur (X:int, Y:int, mapTrajectoires:BitmapData, zones:Array, cib:Array, mapColl:BitmapData) {
			mapTrajets = mapTrajectoires;
			mapCollisions = mapColl;
			col = new Collisions();
			portions = zones;
			cibles = cib;
			_X = X;
			_Y = Y;
			init(X, Y);
		}		
 
		function init(X:int, Y:int):void {
			x = X;
			y = Y;
			dx = 0;
			dy = 0;
			vit = 0;
			dir = 0;
			fri = .95;
			acc = .30;
			angle = 0;
			moveX = 0;
			moveY = 0;
			sante = 100;
			vitMax = 5;
			dirMax = 1.5;
			glisse = .12;
			rotation = 0;
			rebond = false;
			freine = false;
			angle = -Math.PI / 2;
			addEventListener(Event.ENTER_FRAME, update);
		}
 
		private function update(e:Event):void {
 
			// détecte le sol
			p = this.localToGlobal(this.p7);// converti les coordonnées des points 
			zone = mapTrajets.getPixel(p.x, p.y);// trouve les couleurs sous les points
			cible = cibles[portions.indexOf(zone)];// trouve le point à cibler
 
			// gére les collisions
			col.hitTest(this, mapCollisions);
 
			// limitation de vitesse selon les zones
			if (zone == 6381921) vitMax = 3;
			else if (zone == 179) vitMax = 2;
			else if (zone == 151) vitMax = 3;
			else vitMax = 5;
 
			// déplacements
			if (!rebond) {
				dx = 			cible.x-x;
				dy = 			cible.y-y;
				angleCible = 		angle360(Math.atan2(dy, dx) / Math.PI*180);
				angleActuel = 		angle360(angle / Math.PI*180);
				diff = 			angleActuel - angleCible;
				if (diff <= 0) 		diff += 360 else diff -= 360;
				diff = 			angle360(diff);			
				if (diff < 180) 	dir -= 0.2;
				if (diff > 180) 	dir += 0.2;
				if (dir > 1.5)  	dir =  1.5;
				if (dir < -1.5) 	dir = -1.5;
				if (vit < vitMax) 	vit += 0.5;
				if (freine) 		vit *= 0.9;
				vit *= 			fri;
				idealX = 		Math.cos(angle)*vit;
				idealY = 		Math.sin(angle)*vit;
				moveX += 		(idealX-moveX)*glisse;
				moveY += 		(idealY-moveY)*glisse;
				x +=			moveX;
				y += 			moveY;
			} else {
				moveX *= 0.7;
				moveY *= 0.7;
				x -= moveX;
				y -= moveY;
			}
 
			// tourner
			angle += dir*vit*.030;
			if(diff>3 && diff<357) rotation = angle * 180/Math.PI+90;
			dir -= (dir * 0.1);
 
			// gestion des degats
			if (sante <= 0) {
				removeEventListener(Event.ENTER_FRAME, update);
				init(_X, _Y);
			}
		}
 
		// convertir un angle sur 360 degres
		private function angle360(t:Number):Number {
			t %= 360;
			if(t < 0) t+= 360;
			return t
		}
	}
}

La construction est globalement la même que pour le joueur, on va donc se passer d'étudier les variables, vous verrez les nouvelles au fur et à mesure. Certains se demanderont peut-être pourquoi je ne crée pas une classe générique “Véhicule” et deux classes “Joueur” et “Ordinateur” qui étendent la classe “Véhicule” afin de clarifier mon code. Tout simplement parce que nous ne sommes pas en train d'apprendre à faire de la POO propre, mais de voir une méthode pour créer un jeu de course. C'est moins compliqué pour moi de réécrire tout le code et de bien séparer les choses lorsque je tente de vous expliquer l'exercice, que de faire des modifications constantes avec des retours dans des classes qu'on à déjà vues auparavant. Bref, ce n'est pas le sujet qui nous intéresse, le but des exercices n'est pas de vous donner un moteur tout prêt, mais des sujets de réflexions autour de techniques dédiées au jeu vidéo, mais sachez que vous pouvez bien sur simplifier tout ça selon votre niveau en programmation et votre maîtrise de la POO.

On refait donc ce qu'on à déjà fait avec le joueur, c'est à dire créer les différentes variables (friction, vitesse, etc….), et en ajouter de nouvelles que l'on détaillera en cours de route, puis placer non plus 6 mais 7 points de contrôle. En effet pour les ordinateurs j'ai ajouté un point central à la voiture, vous verrez pourquoi un peu plus tard.

public function Ordinateur (X:int, Y:int, mapTrajectoires:BitmapData, zones:Array, cib:Array, mapColl:BitmapData) {
	mapTrajets = mapTrajectoires;
	mapCollisions = mapColl;
	col = new Collisions();
	portions = zones;
	cibles = cib;
	_X = X;
	_Y = Y;
	init(X, Y);
}

Lorsqu'un adversaire est créé, on lui passe les différents maps dont il a besoin pour les collisions et le trajet ainsi que les points cibles, et on enregistre sa position sur la grille de départ. Notez ici que pour bien faire il faudrait ajouter un écouteur pour savoir quand la voiture est ajoutée à la liste d'affichage, mais on va s'en passer pour l'exercice.

function init(X:int, Y:int):void {
	x = X;
	y = Y;
	dx = 0;
	dy = 0;
	vit = 0;
	dir = 0;
	fri = .95;
	acc = .30;
	angle = 0;
	moveX = 0;
	moveY = 0;
	sante = 100;
	vitMax = 5;
	dirMax = 1.5;
	glisse = .12;
	rotation = 0;
	rebond = false;
	freine = false;
	angle = -Math.PI / 2;
	addEventListener(Event.ENTER_FRAME, update);
}

On initialise toutes les variables et on place la voiture sur le circuit.

private function update(e:Event):void { ... }

La fonction qui met à jour l'adversaire.

// détecte le sol
p = this.localToGlobal(this.p7);// converti les coordonnées des points 
zone = mapTrajets.getPixel(p.x, p.y);// trouve les couleurs sous les points
cible = cibles[portions.indexOf(zone)];// trouve le point à cibler

La voiture à besoin de détecter la zone qu'elle est en train de traverser, c'est à ça que sert le point central que j'ai ajouté. Je commence donc par convertir les coordonnées du point en coordonnées globales, puis je récupère la couleur de la zone que la voiture traverse, et le point qu'elle doit cibler pour cette zone.

// gére les collisions
col.hitTest(this, mapCollisions);

La gestion des collisions est exactement la même que pour le joueur.

// limitation de vitesse selon les zones
if (zone == 6381921) vitMax = 3;
else if (zone == 179) vitMax = 2;
else if (zone == 151) vitMax = 3;
else vitMax = 5;

L'autre intérêt des zones c'est de pouvoir influer sur la vitesse de la voiture, l'ordinateur n'ayant pas appris les notions de vitesse relative, de courbe, de forces, et globalement de physique, il ne sait pas quand il doit ralentir et fonce tout le temps à fond. Grâce aux zones nous pouvons lui signifier que dans une certaine zone sa vitesse doit être limitée, ce qui lui permet de mieux aborder le virage suivant. Là aussi ce sera à vous de tâtonner pour trouver les bonnes vitesses pour chaque zone en fonction du comportement de la voiture. Méfiez-vous également des couleurs, vous n'aurez sans doute pas le même nombre de couleurs pour tous vos circuits et une même couleur n'aura pas forcément la même action sur le comportement de la voiture d'un circuit à l'autre. Là j'ai codé ça comme un sauvage en dur directement dans la classe “Ordinateur” pour vous montrer le principe en trois lignes, mais bien sur c'est à vous de créer un tableau par circuit comprenant toutes les zones de freinage, voire de créer une nouvelle ColorMap par circuit pour y coller des zones de freinages qui peuvent être différentes des zones utilisées pour le trajet, les deux n'étant pas forcément liées, bref sur le même principe vous pouvez faire un peu ce que vous voulez pour obtenir une plus grande précision.

Revenons à nous moutons et voyons de plus près comment la voiture doit se déplacer.

// déplacements
if (!rebond) {
	dx = 			cible.x-x;
	dy = 			cible.y-y;
	angleCible = 		angle360(Math.atan2(dy, dx) / Math.PI*180);
	angleActuel = 		angle360(angle / Math.PI*180);
	diff = 			angleActuel - angleCible;
	if (diff <= 0) 		diff += 360 else diff -= 360;
	diff = 			angle360(diff);			
	if (diff < 180) 	dir -= 0.2;
	if (diff > 180) 	dir += 0.2;
	if (dir > 1.5)  	dir =  1.5;
	if (dir < -1.5) 	dir = -1.5;
	if (vit < vitMax) 	vit += 0.5;
	if (freine) 		vit *= 0.9;
	vit *= 			fri;
	idealX = 		Math.cos(angle)*vit;
	idealY = 		Math.sin(angle)*vit;
	moveX += 		(idealX-moveX)*glisse;
	moveY += 		(idealY-moveY)*glisse;
	x +=			moveX;
	y += 			moveY;
} else {
	moveX *= 0.7;
	moveY *= 0.7;
	x -= moveX;
	y -= moveY;
}

La grosse différence avec le joueur c'est les touches de direction, le joueur choisi de lui même la direction qu'il doit suivre, mais l'ordinateur va suivre un chemin qu'on lui a balisé avec des cibles à atteindre. On commence donc par regarder la direction à suivre sur chaque axe en fonction de la position actuelle et de la position de la cible à atteindre.

Un premier problème lié à l'Actionscript survient, tous les angles sont compris entre 0 et 180 ou 0 et -180, AS ne travaillant pas avec des angles sur 360 degrés. Or pour savoir où se trouve une cible par rapport à l'orientation actuelle de la voiture il nous faut raisonner sur des angles à 360 degrés. Pour ce faire on va utiliser une petite fonction que je vous détaille tout de suite et qui a pour but de convertir un angle sur 360 degrés.

// convertir un angle sur 360 degres
private function angle360(t:Number):Number {
	t %= 360;
	if(t < 0) t+= 360;
	return t
}

C'est tout bête mais ça va nous rendre un immense service, revenons aux déplacements de notre voiture.

On cherche l'angle de la cible par rapport à la voiture et l'angle actuel de la voiture, les deux sur un cercle de 360 degrés. On regarde la différence entre ces deux angles, on fait une petite correction pour inverser l'angle trouvé, puis on le recalcule sur 360 degrés (pour le cas où il serait négatif). C'est de la trigo et du calcul d'angle, on ne va pas entrer dans les détails car ce n'est pas ce qui nous intéresse vraiment ici, le but est juste de trouver l'angle résultant entre l'angle actuel et l'angle ciblé.

On va ensuite faire tourner la voiture selon l'angle qu'on à trouvé, s'il est inférieur à 180 on tourne vers la gauche et s'il est supérieur on tourne à droite, 180 étant donc l'angle précis à atteindre sur notre cercle, si la voiture est orientée dessus on ne tourne pas bien sur. Plutôt que de dire “tourner” je devrait dire “corriger la trajectoire” car c'est en fait ce qu'il se passe ici, on corrige la trajectoire pour essayer d'atteindre l'angle optimal.

On limite l'angle de braquage, ça c'est le comportement normal de la voiture, et on stipule à l'ordinateur que tant qu'il na pas atteint sa vitesse maximale il doit accélérer, il accélère donc toujours à fond selon la vitesse limite de chaque zone. Comme pour le joueur on fait entrer en jeu le freinage et la friction, les comportements normaux de la voiture. Puis on cherche le mouvement idéal et le mouvement réel, qui dépend du “facteur d'adhérence” et enfin on déplace la voiture. Notez que cette fois aussi on regarde si la voiture est en train de rebondir suite à une collision ou pas, j'ai légèrement augmenté le freinage car un être humain aurait le réflexe de rester appuyé sur l'accélérateur pour repartir plus vite alors que l'ordinateur ne fait rien tant qu'il n'a pas reçut d'instructions dans ce sens, un petit artifice qui en vaut un autre…

// tourner
angle += dir*vit*.030;
if(diff>3 && diff<357) rotation = angle * 180/Math.PI+90;
dir -= (dir * 0.1);

Cette petite partie est importante, l'angle actuel de la voiture dont on se sert pour déterminer le mouvement, dépend de la direction, de la vitesse et du temps de réaction du pilote, plus ce temps est long et plus le pilote tournera tard, plus il est court et plus le pilote corrigera vite sa direction. Dans les faits ce n'est pas exactement ce que fait cette formule, mais c'est la meilleure manière que j'ai trouvé de vous l'expliquer simplement. Immédiatement après on va vérifier que la différence entre l'angle actuel de la voiture et l'angle de la cible est supérieure à 3 degrés, ceci va éviter des corrections permanentes de la trajectoire pour des angles assez petits que la voiture n'arrivera jamais a atteindre. Et enfin comme pour le joueur, la direction revient lentement au centre si on cesse de tourner.

// gestion des degats
if (sante <= 0) {
	removeEventListener(Event.ENTER_FRAME, update);
	init(_X, _Y);
}

Dernier petit artifice, les dégâts, ils me servent essentiellement à éviter qu'une voiture reste bloquée derrière un mur trop longtemps. En effet il se peut qu'un mur empêche la voiture d'atteindre sa cible, pour m'éviter de nombreuses lignes de code pour palier à cet inconvénient, je vais tout simplement réduire la santé du véhicule quand il tape un mur, lorsque sa santé est à zéro la voiture est repositionnée sur la grille de départ.

Et nous avons terminé ce petit exercice.

Conclusion

Pour cet exercice j'ai utilisé une technique qui s'appuie sur des Bitmaps, mais sachez que vous pouvez les remplacer par des tuiles ou tout autre construction de votre cru si vous trouvez plus simple. Mon but n'était encore une fois de vous montrer LA solution parfaite, mais une méthode utilisée dans beaucoup de jeux pour créer une IA pour un jeu de course. Si vous souhaitez affiner le comportement des voitures pilotées par l'ordinateur vous pouvez ajouter d'autres points de contrôle, même si ils se trouvent en dehors de la voiture, par exemple un point placé à une cinquantaine de pixels devant la voiture pourrait permettre de détecter des obstacles avant d'être dessus et ainsi de corriger la trajectoire pour les éviter ou au contraire aller dessus (bonus, turbos, …). Ce même point pourrait servir pour anticiper les collisions, voire détecter une sortie de piste si la trajectoire n'est pas corrigée rapidement. Bref, plus vous aurez de points de contrôles bien placés et plus vous pourrez affiner les comportements de l'ordinateur et ajouter des facteurs aléatoire pour que chaque voiture réagisse différemment des autres.

Les sources

course_mb.zip version CS6
course_mb_cs5.zip version CS5