Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

• Glissé-Lâché (drag and drop)(ici)
Collisions et superpositions

Glissé-lâché (drag&drop) et pointeurs personnalisés

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par Nataly, le 28 août 2012

startDrag/stopDrag, le net regorge de tutos, billets et autres documents à vocation didactique répétant à l'envi les mêmes approximations.
Je vais donc m'attacher ici, non pas à détailler le principe que vous connaissez forcément tant c'est un incontournable de l'interaction, mais à préciser les choses et lever les confusions.
Outre les traditionnelles utilisations interactives, les méthodes startDrag et stopDrag peuvent être utilisées pour substituer un clip de votre choix au pointeur. Ce sera l'objet de la deuxième partie de ce tuto.

Et comme qui dit déplacer dit le plus souvent tester les superpositions ou collisions, dans l'élan nous allons régler le sort des différents “hitTests”[page suivante à venir].

Je considère que vous disposez des connaissances fondamentales détaillées dans la Saga Premier Pas.
Si vous voulez jouer à la récrée, il vous faudra savoir faire usage d'une classe perso et de méthodes statiques, le mieux pour la pratique serait de l'écrire ;)

Déplacer un objet à la souris

Traditionnellement, le glissé-lâché est initié quand on enfonce la souris et prend fin quand on relâche le bouton.

Le plus souvent le principe est illustré des quelques lignes suivantes :

leClip.addEventListener(MouseEvent.MOUSE_DOWN, qdEnfonce)
leClip.addEventListener(MouseEvent.MOUSE_UP, qdLache)
 
function qdEnfonce (me:MouseEvent):void {
/*C'est la méthode startDrag de la classe Sprite qui se charge de subordonner la position d'un clip au pointeur.
Elle admet un paramètre facultatif qui, s'il est valorisé à true, place l'origine du clip sous le pointeur. 
Dans le cas contraire c'est le point cliqué qui est utilisé.
*/
	me.target.startDrag(true)
}
function qdLache (me:MouseEvent):void {
	me.target.stopDrag()
}

Ici, on a choisi d'écouter l'événement MouseDown sur ce clip pour initier le déplacement et d'arrêter le déplacement au relâchement de la souris.

Et ça marche, non ? Qu'est-ce qu'elle râle encore, la Tartine ? S'insurge le Ronchon de service, jaloux de son statut. D'autant qu'on a pris soin d'initier le startDrag sur la cible de l'événement (target) utilisant là un couplage faible, ce qui est quand même classe.

Pointeur relâché en dehors du clip

D'accord, pour la démo ça le fait. Je ne peux qu'en convenir. Mais, juste pour de rire, essaie donc de réduire le déplacement du clip sur l'horizontale (à l'aide du troisième paramètre).
En testant, relâche le bouton de la souris quand le pointeur n'est plus sur le clip déplacé (dessous par exemple puisqu'il est bloqué sur une trajectoire horizontale)…

leClip.addEventListener(MouseEvent.MOUSE_DOWN, debutDeplacement);
leClip.addEventListener(MouseEvent.MOUSE_UP, finDeplacement);
 
function debutDeplacement(me:MouseEvent):void {
	var c:MovieClip=MovieClip(me.target);
	trace(c)
//le périmètre de restriction : new Rectangle(x,y,largeur,hauteur)
	c.startDrag(true,new Rectangle(0,c.y,stage.stageWidth,0));
}
function finDeplacement(me:MouseEvent):void {
	me.target.stopDrag();
}

Ah bah oui, ça le fait moins déjà ;)
Normal : on écoute le relâchement de la souris sur le clip, si son périmètre de déplacement est restreint il se peut qu'il ne soit plus sous le pointeur quand on lâche le bouton de la souris, et par conséquent l'écouteur n'entend plus rien :(

Plutôt que se lancer dans les grandes manœuvres pour reconstituer un événement “releaseOutside” qui n'existe plus, décidons plus simplement d'écouter l'événement MouseEvent.MOUSE_UP de “plus haut”, depuis la scène par exemple (rappelez vous le flux de propagation à travers la liste d'affichage) :

stage.addEventListener(MouseEvent.MOUSE_UP, qdLache);



Si vous testez en l'état, la fenêtre de sortie vous rappelle à l'ordre :

ReferenceError: Error #1069: La propriété stopDrag est introuvable sur flash.display.Stage


De fait, la fonction de rappel invoque la méthode stopDrag sur me.target qui maintenant est la scène - une instance de Stage - qui n'expose pas plus de méthode stopDrag que starDrag.

Vérifiez sur la doc et pendant que vous y êtes, un œil sur l'arbre d'héritage finira de vous mettre au clair :

La classe Stage

Héritage : Stage hérite de DisplayObjectContainer hérite de InteractiveObject hérite de DisplayObject hérite de EventDispatcher hérite de Object

La classe Sprite

Héritage : Sprite hérite de DisplayObjectContainer hérite de InteractiveObject hérite de DisplayObject hérite de EventDispatcher hérite de Object
La scène n'hérite pas de Sprite, or c'est la classe Sprite qui expose les propriétés startDrag et stopDrag

stopDrag, comment ça fonctionne vraiment

Voilà qui n'arrange pas nos velléités de couplage faible et on s'en moque puisqu'il n'y a rien à coupler ;)

Retour sur la doc à l'entrée stopDrag :

Met fin à la méthode startDrag(). Un sprite qu’il est possible de déplacer grâce à la méthode startDrag() reste déplaçable jusqu’à ce qu’une autre méthode stopDrag() soit ajoutée, ou jusqu’à ce qu’un autre sprite devienne déplaçable. Vous ne pouvez déplacer qu’un seul sprite à la fois.

Met fin à la méthode startDrag(), TOUT COURT…
Point n'est besoin de préciser le clip en cours de déplacement ;) Il suffit d'appeler stopDrag depuis n’importe quelle instance de Sprite (ou MovieClip qui en hérite) pour que l'objet en cours de déplacement cesse de suivre le pointeur. En plus pas d'erreur si la méthode est invoquée alors qu'aucun déplacement n'est en cours :)

Du coup, pourquoi se compliquer à récupérer une cible dans la fonction de rappel ? Invoquons stopDrag sur le premier clip que nous ayons sous la main…
… et le clip qui existe quoiqu'il arrive, c'est, c'est ? Le scenario principal :idea:

Dans l'immense majorité des cas on écrit le code sur l'image 1 du scenario principal. En conséquence, l'objet renvoyé par 'this' c'est lui (MainTimeLine). Pour arrêter un déplacement, on peut donc tout bonnement écrire sans se préoccuper de rien :

function finDeplacement(me:MouseEvent):void {
	this.stopDrag();
}

Ce qui se transforme immanquablement en :

function finDeplacement(me:MouseEvent):void {
	stopDrag();
}

this étant l'objet par défaut1), et la fainéantise étant le notre, de défaut :mrgreen:

En synthèse : le principe Passe-Partout

Un seul objet, on ne se complique pas :

leClip.addEventListener(MouseEvent.MOUSE_DOWN, debutDeplacement);
stage.addEventListener(MouseEvent.MOUSE_UP, finDeplacement);
 
function debutDeplacement(me:MouseEvent):void {
        me.target.startDrag()
      /* avec un périmètre de limitation
	var c:MovieClip=MovieClip(me.target);
	c.startDrag(true,new Rectangle(0,c.y,stage.stageWidth,0));
      */
}
 
 
function finDeplacement(me:MouseEvent):void {
	stopDrag();
}




Plusieurs objets, on ne se complique pas non plus : on les enferme ds un clip contenant.

contenant.addEventListener(MouseEvent.MOUSE_DOWN, qdEnfonceSurContenant);
stage.addEventListener(MouseEvent.MOUSE_UP, finDeplacement);
contenant.buttonMode=true; // on aura une jolie main comme pointeur au survol
 
function qdEnfonceSurContenant(me:MouseEvent):void {
	var c:MovieClip=MovieClip(me.target)
        MovieClip(c.parent).addChild(c); // premier plan, le cas échéant
        c.startDrag()
}
 
function finDeplacement(me:MouseEvent):void {
       stopDrag();
}

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

Pointeur personnalisé

Aujourd'hui, je suis vissée à l'envers - vous l'avez noté - et mon esprit de contradiction est en pleine forme. Je continue donc sur ma lancée et vous propose à titre d'entrainement de jouer avec les méthode starDrag et stopDrag non pas depuis les événements souris, mais depuis le clavier.
Si, si ! :D

Imaginons donc qu'on se propose un (microscopique) jeu du type “je bute des ennemis (à la souris)” et je peux changer d'arme.
Du coup le pointeur doit pouvoir prendre l'aspect d'une arme ou d'une autre, voire reprendre son visuel standard.
On choisit l'arme au clavier. Ici j'ai décidé w : pistolet, x : lance-pierre

Je fais appel à votre imagination démesurée pour reconnaitre un pistolet dans la pastille rouge et un lance-pierre au travers le rectangle noir (mal vissée, j'ai prévenu) :mrgreen:

cliquez pour donner le focus
L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

Sur la scène j'ai nommé le pistolet : pistolet et le lance-pierre : lancePierre.
Vous savez tout ce qu'il faut pour vous y lancer, sauf peut-être la gestion des touches du clavier, un tour sur les premières lignes du |tuto de Billy vous sortira d’embarras. J'y suis d'ailleurs moi même allée de mon copier coller2), ce qui donne pour ce qui nous intéresse et qui n'a pas à voir directement avec l'objet du tuto :

stage.addEventListener(KeyboardEvent.KEY_DOWN, qdEnfonceTouche);
 
function qdEnfonceTouche(ke:KeyboardEvent):void {
	trace("une touche a été enfoncée - keyCode="+ke.keyCode+"  - charCode="+ke.charCode);
	[]
        // w : 87, x : 88
	switch (ke.keyCode) {
        []

Une autre chose que vous n'avez peut-être pas en tête, c'est l'instruction pour afficher ou masquer le pointeur. Il y en a deux, les voici :

	Mouse.hide();// cacher
	Mouse.show();// afficher
Notez le : startDrag ne fait que souscrire un écouteur au mouseMove (du moins l'équivalent), il ne place pas l'objet sous concerné sous le pointeur, il faut attendre qu'il y ait mouvement de la souris.

Du coup si vous n'y prenez garde vous risquez de vous retrouver avec ce type de buguouillerie :

cliquez pour donner le focus
L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.

 
stage.addEventListener(KeyboardEvent.KEY_DOWN, qdEnfonceTouche);
function qdEnfonceTouche(ke:KeyboardEvent):void {
//	trace("une touche a été enfoncée - keyCode="+ke.keyCode+"  - charCode="+ke.charCode);
	Mouse.hide();
	switch (ke.keyCode) {
		case 87 :
			affichePointeur(pistolet);
			break;
		case 88 :
			affichePointeur(lancePierre);
			break;
		default :
			Mouse.show();// on récupère le visuel du pointeur
			stopDrag();
			affichePointeur();
	}
 
}
 
function affichePointeur(c:MovieClip=null) {
	pistolet.visible=lancePierre.visible=false;
	if (c) {
		c.visible=true;
		c.startDrag(true);
		c.x=mouseX; // penser à disposer le clip sous le pointeur
		c.y=mouseY;
	}
 
}

Le pointeur perso ne clique plus :(

Alors voilà ! :)
On dirait qu'on pourrait cliquer sur les nuages et les étoiles, soit avec un gourdin, soit avec un lance pierre, soit avec rien.
On dirait aussi que le gourdin supprime 4 points de vie et le lance-pierre un seul.
On dirait pour finir que les étoiles et les nuages 3) auraient des longueur de vie différentes (ici les étoiles n'ont que 8 vies).

On garde la règle des touches 'x' et 'y' pour changer d'arme.

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

Si comme moi vous avez dessiné un gourdin plein, il y a toutes les chances que dans un premier temps vous ayez l'impression que l'événement CLICK n'est plus entendu. En tous cas, la fonction de rappel associée au contenant4) n'est pas invoquée.
Logique : ce n'est pas parce qu'on ne voit plus le pointeur qu'il n'existe plus… Du coup, le clip qui reçoit le clic, c'est le visuel soumis au startDrag ;)
Il faut donc faire en sorte qu'il ne soit plus réactif, accessible au clic, à la souris…
Allez, je vous le dit : c'est la propriété mouseEnabled qui “désensibilise” les clips si on la valorise à faux :)

Pour le reste, il y a mille façons de s'y prendre. J'ai choisi le parti-pris “on se complique pas”.
Je considère que je peux mettre dans le contenant tous les clips que je veux, sans toucher au code. Par conséquent l'usure des vies se fait au scénario, le code se contente de déplacer la tête de lecture de une ou plusieurs images.


 
var _arme_c:MovieClip;
 
gourdin.mouseEnabled=lancePierre.mouseEnabled=false;
 
contenant.addEventListener(MouseEvent.CLICK, qdClic);
 
function qdClic(me:MouseEvent):void {
	txtSortie.text="pas d'arme";
	if (!_arme_c) {
		return; // pas d'arme on sort
	}
	var cible:MovieClip=MovieClip(me.target);
	if (_arme_c==lancePierre) {
		cible.nextFrame();
		txtSortie.text="Lance pierre : perte d'un point de vie pour la cible";
	} else {
		cible.gotoAndStop(cible.currentFrame+4);
		txtSortie.text="Gourdin : perte de 4 points de vie pour la cible";
	}
	if (cible.totalFrames==cible.currentFrame) {
		txtSortie.text="cible abattue";
		contenant.removeChild(cible);
	}
}
stage.addEventListener(KeyboardEvent.KEY_DOWN, qdEnfonceTouche);
function qdEnfonceTouche(ke:KeyboardEvent):void {
	//trace("une touche a été enfoncée - keyCode="+ke.keyCode+"  - charCode="+ke.charCode);
	Mouse.hide();
	switch (ke.keyCode) {
		case 87 :
			affichePointeur(gourdin);
			break;
		case 88 :
			affichePointeur(lancePierre);
			break;
		default :
			Mouse.show();
			stopDrag();
			affichePointeur();
	}
 
}
 
function affichePointeur(c:MovieClip=null) {
	gourdin.visible=lancePierre.visible=false;
	_arme_c=null;
	if (c) {
		c.visible=true;
		c.startDrag(true);
		c.x=mouseX;
		c.y=mouseY;
		_arme_c=c;
	}
 
}

Récrée !

Du coup, cette histoire de pointeur personnalisé, ça m'a donné une idée :

L"extension Adobe Flash Plugin est nécessaire pour afficher ce contenu.
Je l'ai mis en œuvre avec deux pointeurs pour deux types d'interactions, on aurait pu en avoir autant qu'on veut.

Consignes :
• Les pastilles sont déplaçables, c'est un pointeur main qui le figure.
• Le bol, la caisse, l'oeuf et Tartine peuvent recevoir un coup de marteau, c'est un pointeur marteau qui le figure.
• Bol et caisse une fois cassés sont inactifs, plus de pointeur spécifique.
• L'œuf une fois cassé devient déplaçable, le pointeur change.

Quand on envisage ce genre de chose, où il peut y avoir quantité d'objets interactifs avec pointeur perso, on se dit vite que gérer le survol de chacun d'eux, ça va virer galère avant même de commencer.
A y bien réfléchir ils ont un comportement commun en plus de leurs spécificités propres, une classe devrait résoudre le problème.

Une classe à associer aux clips ? Je trouve ça dommage : autant se garder cette possibilité sous le coude. D'accord on peut envisager d'éventuelles classes de bases étendant une classe GestionPointeur, ou composant avec… bref… j'ai pas réfléchi plus avant, j'ai opté pour une bête classe statique.

Elle expose une méthode associeVisuel et une autre supprimePointeur et se charge d'associer pointeur perso et objet considéré (gestion du survol donc).

associeVisuel

public function associeVisuel(pointeur:MovieClip,tbClips:Array):void


pointeur : un clip présent sur la scène
tbClips : un tableau des clips concernés

supprimePointeur

public function supprimePointeur(c:MovieClip) :void

c : le clip à dissocier (plus de gestion du survol)

Voici donc les lignes d'initialisation, pour le swf de demo :

// le visuel est associé aux clips spécifiés, la gestion des survol assurée
GestionPointeur.associeVisuel(mainPrend, new Array(pastille1, pastille2));// une main pour les pastilles
GestionPointeur.associeVisuel(marteau, new Array(bol, caisse, oeuf,tartine));//un marteau pour les outils




Sources

les limites du glissé-lâché

Vous l'avez constaté : sur un glisser-lâcher on ne s'occupe que du début et de la fin (start/stop), ce qu'il se passe pendant, on n'a pas la main dessus. Donc si vous voulez “faire des choses” pendant le déplacement vous devrez passer par l'écoute du MOUSE_MOVE.
C'est illustré dans le blog AnnexeAS3 avec le jeu de mots en vrac .

Trainer un objet le long d'un trajet complexe est aussi une consigne qui ressemble à du startDrag mais n'en est pas, vous trouverez les quatre lignes de triche dans ce sujet.

Et puisque quand on lâche on se pose souvent la question de savoir où, la suite logique c'est le test de collision (hitTest), ce que je me propose d'aborder des le prochain tuto

1) quand on n'écrit rien, on écrit this
2) en modifiant les noms à mon goût
3) et d'éventuels autres
4) vous n'avez pas ajouté un écouteur par cible quand même ? ;)