Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Réalisation de cartes à gratter

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Compatible Flash CS5. Cliquer pour en savoir plus sur les compatibilités.Par Stefbuet, le 16 juillet 2011

Lors de cet article je vais aborder une technique parmi tant d’autres pour réaliser une carte à gratter où l’utilisateur va gratter des cases pour découvrir des symboles cachés. Je m’efforcerai dans cet article de vous présenter la méthode la plus adaptée à chaque cas d’utilisation possible, c'est-à-dire avec un certain niveau d’abstraction concernant les formes graphiques des cases à gratter grâce à l’AS3. Une première partie sera consacrée à la simulation du grattage avec la souris, puis nous verrons comment détecter les cases terminées et enfin comment rajouter quelques effets graphiques pour donner une touche de réalisme à vos animations de cartes à gratter.

Cet article vous permettra de réaliser des cartes à gratter comme celle-ci:

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

I. Création de la case et gestion de l'effacement

L’initialisation de notre scène, ainsi que le paramétrage de diverses options des cases à gratter se fera dans la classe du document que nous nommerons Main.
Mais intéressons nous d’abord à une case à gratter à proprement parler. Nous allons commencer par créer une classe de base ScratchCard qui hérite de la classe MovieClip afin de pouvoir écouter des événements par la suite. Nous aurons besoin de deux données de base, à savoir les classes de type BitmapData du fond de la case après grattage et de la texture qui sera grattée exportées depuis la bibliothèque de Flash.

Pour créer un effet de masquage, j’ai décidé d’afficher tout d’abord un bitmap de l’image de fond (avec gestion transparence) puis d'afficher par-dessus un bitmap avec la texture de recouvrement. Afin d’épouser la forme de la case à gratter, on crée un deuxième bitmap de l’image de fond et on le définit comme masque de la surface grattable. Par la suite, pour simuler un effet de grattage nous modifierons directement les pixels du bitmap de la surface supérieure masquée car nous ne pouvons utiliser qu’un seul masque par DiplayObject.

Il faut donc créer les Bitmap mais aussi leurs BitmapData respectifs, grâce aux paramètres fournis dans le constructeur de notre classe. Nous allons aussi centrer tous ces bitmap par rapport à l’origine de notre conteneur, ce qui nous donne :

package  {
 
	import flash.display.MovieClip;
	import flash.events.Event;
	import flash.display.BitmapData;
	import flash.display.Bitmap;
 
	public class ScratchCard extends MovieClip{
 
		private var back:Bitmap;
		private var backData:BitmapData;
		private var coverMask:Bitmap;
		private var coverBmp:Bitmap;
		private var coverBmpData:BitmapData;
 
		public function ScratchCard(CardBmpPattern:Class, CoverBmpPattern:Class) {
 
			//le fond de la case
			backData=new CardBmpPattern(0,0);
			back=new Bitmap(backData);
			back.x-=back.width/2;
			back.y-=back.height/2;
			addChild(back);
 
			//la matiere a gratter
			var temp:BitmapData=new CoverBmpPattern(0,0);
			coverBmpData=new BitmapData(back.width, back.height);
			coverBmpData.draw(temp);
			coverBmp=new Bitmap(coverBmpData, "never", true);
			coverBmp.x-=coverBmp.width/2;
			coverBmp.y-=coverBmp.height/2;
			coverBmp.cacheAsBitmap=true;
			addChild(coverBmp);
 
			//le masque de la matiere a gratter
			coverMask=new Bitmap(new CardBmpPattern(0,0));
			coverMask.x-=coverMask.width/2;
			coverMask.y-=coverMask.height/2;
			coverMask.cacheAsBitmap=true;
			addChild(coverMask);
			coverBmp.mask=coverMask;
 
		}
 
	}
}//fin package

Maintenant que nous avons l’aspect graphique avec masquage initialisé, il faut pouvoir retirer de la matière de la surface supérieure. Nous allons offrir au développeur (vous) le choix d’utiliser la forme d’outil de suppression (brush) qu’il veut : rond, carré ou triangle, peu importe. Pour cela, on demandera un objet de type DisplayObject correspondant à la brosse en cours quand on voudra commencer à gratter la surface.
Dès le début du grattage, nous allons appeler - à chaque cycle d’affichage - une fonction1) qui va effacer la zone parcourue par la brosse entre sa position précédente et sa position actuelle.
Pour effacer de la matière, après avoir préalablement stocké l’image de la brosse dans un BitmapData, nous afficherons le contenu de ce bitmapData dans celui de la surface supérieure (méthode draw).
Ici, nous aurons recours au mode de dessin de type BlendMode ::ERASE pour rendre la matière transparente.

Nous avons alors les deux fonctions et propriétés suivantes :

package {
 
	//imports...
 
	public class ScratchCard extends MovieClip {
 
		private var isDrawing:Boolean=false;
		private var mouseBrush:DisplayObject;
		private var brushBmpData:BitmapData;
 
 
		public function ScratchCard(CardBmpPattern:Class, CoverBmpPattern:Class) {
 
			// [...]
 
			isDrawing=false;
 
		}
 
		function activateDrawing(state:Boolean, brush:DisplayObject=null):void {
 
			if(state&&!isDrawing) {
				addEventListener(Event.ENTER_FRAME, destroyCover);
				mouseBrush=brush;
				brushBmpData=new BitmapData(brush.width, brush.height, true, 0);
				var c:ColorTransform=new ColorTransform(0,0,0,0,0,0,0,255);
				var m:Matrix=new Matrix();
				m.tx=brush.width/2;
				m.ty=brush.height/2;
				brushBmpData.draw(brush,m,c);
			}
			else {
				if(isDrawing) removeEventListener(Event.ENTER_FRAME, destroyCover);
			}
			isDrawing=state;
			previousVelocity=0;
 
		}
 
		function destroyCover(e:Event):void {
 
			var currentPos:Point=new Point(mouseBrush.x-this.x, mouseBrush.y-this.y);
 
			var pos:Matrix=new Matrix();
			pos.tx=currentPos.x+coverBmpData.width/2;
			pos.ty=currentPos.y+coverBmpData.height/2;
 
			var col:ColorTransform=new ColorTransform(0,0,0,0,0,0,0,255);
 
			coverBmpData.draw(mouseBrush, pos, col, BlendMode.ERASE);
 
		}
	}
 
 
}


Cependant, si l’utilisateur bouge la souris rapidement, on remarquera que les traces d’effacement sur la matière sont discontinues. En effet, la fonction d’effacement est appelée à chaque cycle d’affichage qui peut être plus ou moins long (en fonction des autres éléments graphiques de la scène et de la puissance de la machine de l’utilisateur).

Pour palier à ce problème, nous allons interpoler linéairement la position de la brosse entre sa dernière position et sa position actuelle afin d’effectuer plusieurs effacements intermédiaires. Nous obtiendrons alors une trace continue.
Pour être sûrs d’avoir une trace sans trous, quelle que soit la vitesse, on définit le nombre d’itérations (lors de l’interpolation à effectuer) selon la vélocité du déplacement de la souris.

Il faut donc calculer la vitesse de la brosse à chaque cycle d’affichage et effectuer les interpolations dans une boucle.



Le code correspondant aux deux fonctions de mise à jour, ainsi que la déclaration des propriétés nécessaires pour un affichage sans discontinuité est alors le suivant :

package {
 
	//imports...
 
	public class ScratchCard extends MovieClip {
 
		private var isDrawing:Boolean=false;
		private var mouseBrush:DisplayObject;
		private var brushBmpData:BitmapData;
		private var lastPos:Point;
 
		public function ScratchCard(CardBmpPattern:Class, CoverBmpPattern:Class) {
 
			// [...]
 
			isDrawing=false;
			lastPos=new Point();
 
 
		}
 
		function activateDrawing(state:Boolean, brush:DisplayObject=null):void {
 
			if(state&&!isDrawing) {
				addEventListener(Event.ENTER_FRAME, destroyCover);
				mouseBrush=brush;
				lastPos.x=brush.x-this.x;
				lastPos.y=brush.y-this.y;
				brushBmpData=new BitmapData(brush.width, brush.height, true, 0);
				var c:ColorTransform=new ColorTransform(0,0,0,0,0,0,0,255);
				var m:Matrix=new Matrix();
				m.tx=brush.width/2;
				m.ty=brush.height/2;
				brushBmpData.draw(brush,m, c);
			}
			else {
				if(isDrawing) removeEventListener(Event.ENTER_FRAME, destroyCover);
			}
			isDrawing=state;
			previousVelocity=0;
 
		}
 
		function destroyCover(e:Event):void {
 
			var nowPos:Point=new Point(mouseBrush.x-this.x, mouseBrush.y-this.y);
			var currentPos:Point=new Point();
 
			var  currentVelocity:Number=Math.sqrt(Math.pow(nowPos.x-lastPos.x,2)+Math.pow(nowPos.y-lastPos.y,2));
 
			var passNumber:Number=Math.max(5,Math.round(currentVelocity/2));
 
			for(var i:uint=0; i<passNumber; i++) {
 
				currentPos.x=(i/(passNumber-1))*nowPos.x+(1-i/(passNumber-1))*lastPos.x;
				currentPos.y=(i/(passNumber-1))*nowPos.y+(1-i/(passNumber-1))*lastPos.y;
 
				var pos:Matrix=new Matrix();
				pos.tx=currentPos.x+coverBmpData.width/2;
				pos.ty=currentPos.y+coverBmpData.height/2;
				var col:ColorTransform=new ColorTransform(0,0,0,0,0,0,0,255);
				coverBmpData.draw(mouseBrush, pos, col, BlendMode.ERASE);
			}
 
			lastPos.x=mouseBrush.x-this.x;
			lastPos.y=mouseBrush.y-this.y;
 
 
		}
	}
 
 
}

Notre classe est maintenant capable d’afficher la case avec les masquages nécessaires et d'effacer de la matière avec une brosse personnalisée.
Mettons en place la classe Document pour faire un exemple de base.

Je vous propose d’afficher 3 cases à gratter avec le logo Flash comme image de fond et une texture de métal granulaire comme surface à gratter. Ensuite, on créera une forme simple en guise de brosse et dans une fonction de mise à jour 2) nous placerons cette forme aux coordonnées de la souris.

Attention à bien centrer la brosse sur son origine, ainsi elle suivra préisément le mouvement du pointeur.
Par ailleurs, dans la suite du tuto je considère la brosse ainsi disposée…


Voici le code de la classe Main de base, que nous compléterons plus tard :

package  {
 
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.ui.Mouse;
	import flash.events.MouseEvent;
 
	import ScratchCard;
	import flash.geom.Point;
 
	public class Main extends Sprite {
 
		private var cards:Vector.<ScratchCard>;
		private var brush:Brush;
 
 
		public function Main() {
			addEventListener(Event.ADDED_TO_STAGE, initScene);
		}
 
		private function initScene(e:Event):void {
 
			removeEventListener(Event.ADDED_TO_STAGE, initScene);
 
			cards=new Vector.<ScratchCard>();
 
			for(var i:uint=0; i<3; i++) {
				var rnd:Number=Math.random();
				var rnd2:Number=Math.random()*0.2+0.4;
				cards.push(new ScratchCard(rnd>rnd2?FlashFailBmp:FlashBmp, MetalBmp, rnd>rnd2?"FAIL":"WIN"));
				cards[i].x=(i+1)*stage.stageWidth/4.0;
				cards[i].y=stage.stageHeight/2.0;
				addChild(cards[i]);
			}
 
			brush=new Brush();
			addChild(brush);
			Mouse.hide();
			addEventListener(Event.ENTER_FRAME, moveBrush);
 
			addEventListener(MouseEvent.MOUSE_DOWN, destroyingOn);
			addEventListener(MouseEvent.MOUSE_UP, destroyingOff);
 
			end_mc.visible=false;
 
		}
 
		private function moveBrush(e:Event):void {
			brush.x=mouseX;
			brush.y=mouseY;
 
		}
 
		private function destroyingOn(e:MouseEvent):void {
			for(var i:uint=0; i<3; i++)
				cards[i].activateDrawing(true, brush);
		}
 
		private function destroyingOff(e:MouseEvent):void {
			for(var i:uint=0; i<3; i++)
				cards[i].activateDrawing(false);
		}
 
	}
 
}

II. Détection d’une case terminée

La première idée qui vient à l’esprit pour détecter quand une case est terminée, à savoir considérer que l’utilisateur voit clairement le symbole de ladite case, même si elle n’est pas complètement effacée, est de tester le pourcentage de pixels de la surface à gratter qui sont effacés.
Grave erreur et voici pourquoi :

Imaginons une case en forme de sphère dans le meilleur des cas. Si l’utilisateur efface le bord extérieur de ce cercle (disque en fait) il ne verra pas le symbole et s’il efface à partir de la moitié du rayon du disque la surface - comme sur l’image ci-dessous qui montre que le symbole est encore totalement invisible - et bien on détectera un pourcentage d’effacement de 75% ! Alors que si on affiche uniquement le plus important : le centre, on n’aura que 25% de détection.
La détection au pourcentage est donc très peu adaptée au problème.




A la place, je vous propose de donner un poids à chaque pixel qui sera de plus en plus faible quand on s’éloigne du centre.
Pour cela une fonction sinusoïdale partant de 1 à 0 est parfaite, elle s’adapte très bien au comportement de l’utilisateur selon moi.

Plutôt que prendre un seul poids en fonction de la distance au centre du pixel courant - ce qui serait optimal pour les disques mais ne serait pas pratique pour les formes de type rectangle ou autre non circulaire - on va donner un poids à chaque pixel selon les axes X et Y puis faire un produit du résultat.



Si le poids total accumulé divisé par la surface totale à gratter est supérieur à 0.11 (valeur choisie après pas mal d’expérimentation, à régler selon vos désirs) alors on envoie un événement de type Event.COMPLETE pour annoncer que la case est enfin découverte. Dans le cas contraire on ne fait rien !

La fonction qui détecte si la case est ouverte ou pas, sera appelée à chaque cycle d’affichage par la fonction qui gère l’effacement de la surface grattable, si la case n’à pas encore été découverte ! (tout à la fin de cette dernière)
Voici la fonction de détection de fin de grattage :

package {
 
	// imports...
 
	public class ScratchCard extends MovieClip {
 
		private var isOpened:Boolean;
 
		public function ScratchCard(...) {
 
			// [...]
			isOpened=false;
 
		}
 
		function checkForCompletion():void {
 
			var count:Number = 0;
 
			for(var i:Number=analysedWidth; i<coverBmpData.width; i++) {
 
				for (var j:Number=0; j<coverBmpData.height; j++) {
 
					var a:Number=(coverBmpData.getPixel32(i,j)>>24)&0xff;
					if (a < 10)
					{
						var px:Number=Math.pow(1-(2*Math.abs(i-coverBmpData.width/2)/coverBmpData.width),1.5);
						var py:Number=Math.pow(1-(2*Math.abs(j-coverBmpData.height/2)/coverBmpData.height),1.5);
						count +=  px * py;
					}
				}
			}
 
			count/=coverBmpData.height*coverBmpData.width;
 
			if(accumulatedCount > 0.11) {
				dispatchEvent(new Event(Event.COMPLETE));
				isOpened = true;
			}
 
 
		}
 
	}//end of class
 
}//end of package


Cependant, cette fonction qui analyse tous les pixels de notre case est assez coûteuse en temps de calcul, et il est possible de l'optimiser.
Pour cela, au lieu de traiter tout le bitmap en une fois, nous allons à chaque cycle traiter uniquement 10 rangées de pixels, et attendre le cycle suivant pour continuer. L'analyse s’étendra sur plusieurs cycles mais la fréquence d'actualisation étant assez rapide cela ne sera même pas remarquable par l'utilisateur, alors autant utiliser cette technique, qui peut s’appliquer à de nombreux procédés, en particulier l'encodage de sons/images avec l'AS3.

Voilà comment étendre l'analyse de plusieurs cycles, en gardant en mémoire la derniere rangée de pixels traités :

package {
 
	// imports...
 
	public class ScratchCard extends MovieClip {
 
		private var isOpened:Boolean;
		private var analysedWidth:Number;
		private var accumulatedCount:Number;
 
		public function ScratchCard(...) {
 
			// [...]
			isOpened=false;
			analysedWidth=0;
			accumulatedCount=0;
 
		}
 
		function checkForCompletion():void {
 
		    var count:Number=0;
			var analysedWidthStart=analysedWidth;
			const step:Number=10;
			var processFinished=false;
 
			for(var i:Number=analysedWidth; (i<coverBmpData.width)&&(analysedWidth-analysedWidthStart<step); i++) {
				analysedWidth++;
				if(i==coverBmpData.width-1) processFinished=true;
				for(var j:Number=0; j<coverBmpData.height; j++) {
 
					var a:Number=(coverBmpData.getPixel32(i,j)>>24)&0xff;
					if(a<10) {
						var px:Number=Math.pow(1-(2*Math.abs(i-coverBmpData.width/2)/coverBmpData.width),1.5);
						var py:Number=Math.pow(1-(2*Math.abs(j-coverBmpData.height/2)/coverBmpData.height),1.5);
						accumulatedCount+=px*py;
					}
				}
			}
 
			if(processFinished) {
				trace("ok");
				analysedWidth=0;
				accumulatedCount/=coverBmpData.height*coverBmpData.width;
				if(accumulatedCount>0.11) {
					dispatchEvent(new Event(Event.COMPLETE));
					isOpened=true;
				}
				accumulatedCount=0;
			}
 
		}
 
	}//end of class
 
}//end of package

III. Effets réalistes

Particules

Afin de donner l’impression à l’utilisateur qu’il est réellement en train de gratter une carte, nous allons rajouter un effet de particules en forme de petits filaments représentant les morceaux de la surface supérieure qui a été grattée. Physiquement, quand on gratte une surface suffisamment vite, les petits morceaux arrachés restent accrochés à l’objet qui gratte et lors d’une décélération on peut les voir continuer leur trajet jusqu'à l'arrêt final.
Nous allons faire exactement la même chose en AS3, à savoir détecter quand il y a une grande décélération dans le mouvement, et à ce moment, générer une certaine quantité de particules avec la même direction que le mouvement initié par l'utilisateur.

A chaque cycle de rendu, depuis la fonction d’effacement, on calcule la vitesse de déplacement de la brosse et si celle-ci à diminué, tout en restant non nulle, alors on va émettre un certain nombre de particules. Les particules sont définies dans leur propre classe Particle héritant de la classe Sprite. Ce sont de simples ‘traits’ de couleur avec une vitesse initiale qui va diminuer au cours du temps pour arriver à zéro3). Dans le constructeur des particules, on définit aussi un boolean pour savoir si la particule sera effacée quand sa vélocité atteindra zéro. En effet, garder toutes les particules à l’etat de repos rend l’effet moins réaliste et on préfère alors n’en garder que la moitié en permanence mais un maximum en mouvement pour donner un effet plus intense.

Voici le code de la classe Particle. Vous remarquerez que si la particule est censée ne pas rester après arrêt, elle va s’enlever automatiquement de son conteneur et arrêter d’écouter l’événement ENTER_FRAME :


package  {
 
	import flash.display.Sprite;
	import flash.display.DisplayObjectContainer;
	import flash.events.Event;
	import flash.utils.getTimer;
	import flash.display.Graphics;
	import flash.geom.Point;
 
	public class Particle extends Sprite {
 
		private var lastTime:Number, now:Number, timeFactor:Number;
		private var creationTime:Number;
		private var _c:DisplayObjectContainer; //parent de la particule
		private var lifeTime:Number;
		private var velocity:Point;
		private var willLive:Boolean;
 
		public function Particle(initPos:Point, velocity:Point, lifeTime:Number, color:Number, c:DisplayObjectContainer) {
			addEventListener(Event.ENTER_FRAME, update);
			lastTime=creationTime=getTimer();
			_c=c;
			this.x=initPos.x;
			this.y=initPos.y;
			_c.addChild(this);
			this.lifeTime=lifeTime;
			this.velocity=velocity;
 
			//simple ligne basique
			var g:Graphics=graphics;
			g.lineStyle(1,color);
			g.moveTo(Math.random()*4-2,Math.random()*4-2);
			g.lineTo(Math.random()*4-2,Math.random()*4-2);
 
			willLive=Math.random()>0.5?true:false;
		}
 
		private function update(e:Event):void {
 
			now=getTimer();
 
			if((now-creationTime>lifeTime)&&(!willLive)) {
				_c.removeChild(this);
				removeEventListener(Event.ENTER_FRAME, update);
				return;
			}
 
			timeFactor=(lastTime-now)/30;
			lastTime=now;
 
			x-=velocity.x*timeFactor;
			y-=velocity.y*timeFactor;
 
			var lifeFactor:Number=(now-creationTime)/lifeTime; //0 to 1
 
			var sign=velocity.x/Math.abs(velocity.x);
			velocity.x+=sign*(0.2+lifeFactor*3)*timeFactor;
			if(velocity.x/Math.abs(velocity.x)!=sign) velocity.x=0;
			sign=velocity.y/Math.abs(velocity.y);
			velocity.y+=sign*(0.2+lifeFactor*3)*timeFactor
			if(velocity.y/Math.abs(velocity.y)!=sign) velocity.y=0;
 
			if(!willLive) alpha=Math.sin((1-(now-creationTime)/lifeTime)*Math.PI/2);
		}
 
	}//fin classe
 
}//fin package


Pour ce qui est de la génération de ces particules, dans la fonction de suppression de matière de la case à gratter, on calcule la nouvelle vitesse de la brosse et, au besoin, on appelle une fonction pour émettre ces particules. Le détail des arguments de la fonction emit particle sera détaillé juste après :

package {
 
	//imports
 
	public class ScratchCard extends MovieClip {
 
		private var previousVelocity:Number;
 
		public function ScratchCard(...) {
 
			// [...]
 
			previousVelocity=0;
 
		}
 
		function destroyCover(e:Event):void {
 
			//code deja présent
			var nowPos:Point=new Point(mouseBrush.x-this.x, mouseBrush.y-this.y);
			var currentPos:Point=new Point();
			var  currentVelocity:Number=Math.sqrt(Math.pow(nowPos.x-lastPos.x,2)+Math.pow(nowPos.y-lastPos.y,2));
 
			//nouveau:
			if((currentVelocity<previousVelocity)&&(currentVelocity>0.1)) emitParticles(new Point(nowPos.x-lastPos.x, nowPos.y-lastPos.y));
			previousVelocity=currentVelocity;
 
			// [...]
 
		}
 
	}//fin classe
 
}//fin package

La génération des particules avec la fonction emitParticles est très précise. En effet, on va émettre 20 particules et il faut trouver leurs points de départ. Pour cela on choisit un point arbitrairement situé dans la bounding box de l'objet brush. Pour que ce point soit validé comme départ plausible d’une particule, il faut qu’il soit situé sur un pixel visible du brush, sur un pixel non effacé de la surface a gratter, et enfin sur un pixel non vide de l’image de fond de la carte à gratter.

Une fois toutes ces conditions réunies, on crée une particule avec la classe décrite précédemment.

Attention, plus le ‘grattage’ est avancé, moins la probabilité de trouver des pixels pour émettre les particules est grande. Pour éviter de se retrouver dans une boucle infinie, on limite le nombre d’essais de points par particule à émettre à 20, donc si il n’y a rien a gratter il n’y aura pas de particules générées !


La fonction est alors la suivante :

function emitParticles(velocity:Point):void {
 
	for(var i:Number=0; i<20; i++) {
		var foundPoint:Boolean=false;
		var trials:Number=0;
		while((!foundPoint)&&(trials<20)) {
			trials++;
			var offset:Point=new Point(Math.round(Math.random()*brushBmpData.width), Math.round(Math.random()*brushBmpData.height));
			if((brushBmpData.getPixel32(offset.x,offset.y)>>24)&0xff>10) {
				if((coverBmpData.getPixel32(mouseBrush.x-this.x+offset.x-brushBmpData.width/2+coverBmpData.width/2, mouseBrush.y-this.y+offset.y-brushBmpData.height/2+coverBmpData.height/2)>>24)&0xff>10) {
					if((backData.getPixel32(mouseBrush.x-this.x+offset.x-brushBmpData.width/2+coverBmpData.width/2, mouseBrush.y-this.y+offset.y-brushBmpData.height/2+coverBmpData.height/2)>>24)&0xff>10) {
						foundPoint=true;
						new Particle(new Point(mouseBrush.x-this.x+offset.x-brushBmpData.width/2,mouseBrush.y-this.y+offset.y-brushBmpData.height/2), new Point(velocity.x-1+Math.random()*2, velocity.y-1+Math.random()*2), 500, 0x444444, this);
					}
				}
			}
		}//fin while
	}//fin for
}//fin fonction

Génération de son

A partir de maintenant, nous avons une application de grattage assez réaliste, mais nous pouvons aller plus loin, et jouer le son du grattage. Plutôt que de jouer un son pré-enregistré nous allons le générer nous même, ce qui permet d’avoir un son qui est adapté parfaitement au comportement de l’utilisateur (fonction de la vitesse de grattage).

Le son à générer sera du bruit, à savoir un signal aléatoire. Cependant pour donner un effet de vitesse à notre simulation, on va faire varier la tonalité du signal : grave pour des déplacements lents et aigu pour une vitesse rapide.



Dans la fonction appelée par l’objet Sound pour récupérer les données à jouer, nous allons envoyer 2048 samples. Tous les k samples, on va changer le sample d’une façon aléatoire. Plus k est faible plus le son sera aigu, plus k est grand plus le son sera grave : k est assimilable à une pseudo pulsation de signal. On calcule alors k en fonction de la vitesse de déplacement du pointeur, mais ce n’est pas suffisant pour avoir un son ’agréable’ car les variations de vitesse sont brusques.
Au lieu de définir k directement en fonction de la vitesse, nous le ferons tendre vers cette valeur par un asservisement : k = k_precedent + (k_voulue – k_precedent) / x, avec x choisi comme on le souhaite.

Pour finir, j’ai rajouté à ce coefficient k une puissance de 0.75 afin de ne pas avoir de son super aigu à vitesse faible, comme vous pouvez le constater sur ce graph (différence avec coefficient linéaire) :



La fonction pour générer le signal du son de grattage est alors la suivante :

private function generateScrachSound(e:SampleDataEvent):void {
	var n:Number=500;
	var sample:Number;
	soundSpeed+=(100/(previousVelocity+1)-soundSpeed);
 
	for (var i:int=0; i<2048; i++ ) {
 
		if(soundSpeed==100) {
			e.data.writeFloat(0);
			e.data.writeFloat(0);
		}
		else {
 
			if(n>Math.pow(soundSpeed,0.75)) {
				sample = -0.25+0.25*(Math.random()); 
				n=0;
			}
			n++;
			e.data.writeFloat(sample);
			e.data.writeFloat(sample);
 
		}
	}
}


Bien entendu à l’initialisation de notre case à gratter il faut créer un objet Sound et lui faire écouter l’événement SAMPLE_DATA pour lui fournir les données calculées par la fonction précédente :

package {
 
	//imports
 
	public class ScratchCard extends MovieClip {
 
		private var scrachSound:Sound;
		private var soundSpeed:Number;
 
		public function ScratchCard(...) {
 
			// [...]
 
			scrachSound=new Sound();
			soundSpeed=75;
			scrachSound.addEventListener(SampleDataEvent.SAMPLE_DATA, generateScrachSound);
			scrachSound.play();
 
		}
 
	}//fin classe
 
}//fin package


Nous arrivons à la fin de cet article sur les cartes à gratter. De nombreuses méthodes pour créer ce type d’applications existent, cependant j’espère vous avoir donné la technique la plus polyvalente possible, s’adaptant donc à toutes les situations.

Vous retrouverez l’application d’exemple du début de l’article, ainsi que toutes les sources en téléchargement ci-dessous.

Stefbuet

Les sources

1) sur ENTER_FRAME donc
2) appelée à chaque rendu graphique Event.ENTER_FRAME, toujours
3) mise à jour à coups d'ENTER_FRAME