Société de service et de conseil multimédia en Suisse
romande. Prossel Software offre des services de création, conception,
réalisation, développement et
programmation pour sites Web et applications mobiles.
Nous sommes basés à Senarclens, près de Cossonay, entre Yverdon-les-Bains et Lausanne, dans canton de Vaud depuis 2020. Auparavant, nous étions à Cheyres, dans le canton de Fribourg.
Cet article n'est pas une traduction, mais il est largement inspiré
d'une publication
en anglais de James Newton sur le même sujet.
1. Introduction
Il y a communication inter-sprites quand le comportement d'un
sprite agit sur un ou plusieurs autres sprites. Par exemple, le
clic sur un bouton qui déplace des objets sur la scène. Le codage
"en dur" permet de développer rapidement une animation
avec Director qui nécessite de faire communiquer des sprites entre
eux. Coder "en dur" dans le cas de la communication
inter-sprites signifie que le numéro du sprite sur lequel on veut
agir est directement écrit dans le code. Tôt ou tard, cette manière
de faire finira par poser problème. En effet, dès qu'il faudra
déplacer un sprite sur une autre piste du scénario, il sera aussi
nécessaire d'ajuster son numéro dans le code Lingo. Cela devient
vite fastidieux et difficile à maintenir quand l'animation devient
plus conséquente.
Nous allons examiner quelques techniques de communication inter-sprites
pour montrer leurs avantages et inconvénients. On verra qu'il
est possible d'écrire relativement simplement des comportements
qui sont indépendants de la position des sprites dans le scénario.
2. N° de sprite en dur dans le Lingo
Donner le numéro du sprite que l'on veut manipuler directement
dans le code lingo est la méthode la plus rapide, mais aussi la
pire. Dans l'exemple 1, les comportements des deux flèches envoient
chacun un message au sprite numéro 1.
-- Comportement de la
flèche haut:
on mouseDown me
sendsprite 1,
#Monte
end
La balle qui est le sprite 1 traite ces messages en changeant
son opacité.
-- Comportement de la
balle:
on Monte me
sprite(me.spritenum).blend
= min(sprite(me.spritenum).blend
+ 10, 100)
end
S'il fallait maintenant ajouter une image de fond à notre animation,
il faudrait décaler nos 3 sprites vers le bas, ce qui fait que
la balle se trouverait par exemple en 2. Sans rien toucher au
code, l'animation s'exécute toujours. Par contre étant donné que
les messages sont envoyés au sprite 1, la balle ne les reçoit
plus. Le comportement de la balle doit donc être adapté au nouveau
numéro de sprite de la balle pour que l'animation continue de
fonctionner.
Le fait qu'aucune erreur ne soit générée lorsque l'on clique
sur les flèches peut être considéré comme un avantage ou un inconvénient.
Un avantage car justement l'animation continue de s'exécuter,
mais un inconvénient car on ne se rend pas forcément compte que
quelque chose ne fonctionne plus comme prévu.
On peut palier à cet inconvénient en utilisant la syntaxe:
-- Comportement de la
flèche haut:
on mouseDown me
Monte sprite 1
end
Si le gestionnaire Monte n'est pas défini pour le sprite
1, Director génère une erreur (Handler not defined) et l'animation
s'arrête.
3. N° de sprite dans un paramètre de comportement
Pour éviter d'avoir le numéro de sprite dans le lingo, on peut
le définir en tant que propriété du comportement. Modifions le
comportement d'une flèche pour qu'il utilise une propriété contenant
le numéro du sprite à piloter.
property iSpriteToCall
on getPropertyDescriptionList
return [ #iSpriteToCall
[ #comment
"Sprite à appeler",¬
#format #integer, #default
1 ]]
end
on mouseDown
me
sendsprite iSpriteToCall,
#Monte
end
Maintenant, pour configurer le numéro de sprite qui doit recevoir
le message, il suffit de sélectionner le sprite sur la scène,
ouvrir l'inspecteur de comportement et modifier le numéro dans
la liste de propriété. Ca fait déjà plus professionnel, mais ça
ne résout pas le problème du changement de position du sprite,
ça ne fait que le déplacer. Il faut toujours aller mettre à jour
le numéro quelque part. Au lieu d'aller dans le code, il faut
aller dans le dialogue des paramètres du comportement. Maigre
consolation, le même comportement peut être utilisé pour plusieurs
instances devant envoyer le même message à des sprites différents,
sans avoir à récrire un script pour chaque numéro.
4. SendAllSprites: envoi du message à tous les sprites
Pour ne pas avoir à se soucier du numéro du sprite qui doit recevoir
le message, on peut l'envoyer à tous les sprites. Le sprite concerné
traite le message, les autres l'ignorent.
on
mouseDown me
sendAllSprites #Monte
end
De cette manière, quelque soit l'emplacement du sprite qui doit
traiter le message, il le recevra. Voilà donc déjà une nette amélioration
de la communication. Mais attention, cette manière de faire n'est
pas sans inconvénient. En effet, si chaque sprite d'une animation
envoie en message à tous les autres lors de chaque entrée dans
une image, cela peut faire beaucoup de messages et influencer
la vitesse de déroulement de l'animation. Le nombre de messages
envoyés est proportionnel au carré du nombre de sprites et au
nombre de comportements par sprite.
Illustrons ce principe avec la métaphore suivante: Une équipe
de nettoyage se répartit les bureaux d'une grande entreprise.
Chaque nettoyeur est assigné à un bureau. Une fois que le travail
à commencé, le chef, qui est resté à la réception, doit rappeler
un de ses employé (appelons le A) pour une raison quelconque.
Il décide d'utiliser le téléphone, car chaque bureau en est équipé.
Il appelle chaque numéro, donne le message suivant: "Si tu
est A, reviens à la réception" et raccroche. A finit par
recevoir le message et revient. Un peu plus tard, c'est au tour
de B d'être demandé par son chef. Celui-ci n'a donc plus qu'à
recommencer ses téléphones. On a bien compris que cette méthode
n'est pas très efficace. Nous allons voir comment l'améliorer.
5. Call sur liste de pointeurs de scripts
Continuons avec notre histoire de nettoyeurs. Le chef peut faire
une seule fois sa série de téléphone. En demandant le nom de la
personne qui répond, il établit une liste gardant la relation
nom-numéro de téléphone. Ensuite, lorsqu'il doit atteindre quelqu'un
il n'a plus qu'à consulter sa liste et appeler directement le
bon numéro.
Pour traduire ce principe de liste en Lingo, nous allons envoyer,
depuis le beginSprite de la flèche haut, un message d'initialisation
à tous les sprites, ainsi qu'une liste vide.
-- Comportement de la
flèche haut
property m_lstBalles
on beginSprite
me
m_lstBalles = []
SendAllSprites
#InitListBalles, m_lstBalles
end
En réponse à ce message, le comportement de la balle s'ajoute
à la liste.
-- Comportement de la
balle
on InitListBalles me,
lstBalles
lstBalles.add(me)
end
Lorsque la flèche doit envoyer un message à la balle elle n'a
plus qu'à le faire pour les membres de sa liste de balles.
on
mouseDown me
call #Monte, m_lstBalles
end
Nous avons introduit ici plusieurs éléments nouveaux. Étudions
les un par un.
Dans le cas d'un sendAllSprites, le gestionnaire du message
ne peut pas utiliser le mot clé return pour retourner une
valeur à l'appelant. En effet, le message est envoyé à tous les
sprites. Si chacun retourne une valeur, que devrait retourner
la fonction sendAllSprites ? En fait son comportement n'est
pas défini, on ne peut donc pas utiliser le retour de fonction
pour situer la balle.
La propriété m_lstBalles du comportement de la flèche
garde la liste des balles qui désirent recevoir le message de
la flèche. Cette propriété est initialisée avec une liste vide
dans le beginSprite. Pourquoi une liste ? Pour l'expliquer,
il faut comprendre comment fonctionnent les passages de paramètres
en Lingo.
5.1. Paramètres par valeur
Plusieurs langages de programmation, (tels que C, C++, Pascal,
Visual Basic, ...) offrent la possibilité de passer les paramètre
par valeur ou par référence. En Lingo tous les paramètres sont
passés par valeur.
Dans le passage de paramètre par valeur, comme son nom l'indique,
c'est une valeur qui est passée à la fonction. Même si on donne
un nom de variable à l'appel, ce n'est qu'une copie de son contenu
(la valeur) qui est reçue par le destinataire du message. Si celui-ci
modifie la valeur de la variable reçue, il ne fait que de modifier
sa copie. La variable utilisée à l'appel n'est pas modifiée.
Considérons le script d'animation suivant:
on test
var1 = 10
put "Début du test
" & var1
fonction1 var1
put "fin du test
" & var1
end
on fonction1 param1
put "entrée de fonction1
" & param1
param1 = param1 * 2
put "sortie de fonction1
" & param1
end
Dans la fenêtre de message, on appelle la fonction test
et on obtient le résultat suivant:
test
-- "Début du test: 10"
-- "entrée de fonction1: 10"
-- "sortie de fonction1: 20"
-- "fin du test: 10"
On constate bien que la variable var1 contient toujours
la valeur 10 après le retour de fonction1 alors qu'elle
été modifiée dans la fonction. Une fonction ne peut donc pas utiliser
ses paramètres pour retourner une valeur à son appelant.
5.2. Cas spécial: les listes
Une variable peut contenir une liste. Si cette variable est copiée
dans une autre, il n'y a toujours qu'une liste en mémoire, mais
les deux variables permettent de la modifier. Dans le cas des
listes, les variables sont en fait des pointeurs. Exemple:
Si on passe en paramètre une variable contenant une liste, on
ne passe pas vraiment la liste, mais son pointeur. Il n'existe
toujours qu'une liste, mais deux pointeurs: l'original et la copie
passée à fonction. Celle ci peut modifier la liste grâce à sa
copie du pointeur. Les modifications de la liste depuis l'intérieur
de la fonction agissent sur la même liste qui est visible à l'extérieur
de la fonction.
On comprend mieux maintenant, pourquoi on passe un paramètre
de type liste avec le message #InitListBalles.
5.3. Me: instance de script
Le comportement de la balle réagit au message #InitListBalles
en ajoutant la variable me à la liste des balles. Le paramètre
me est automatiquement renseigné par Director lorsqu'on
envoie un message avec SendSprite, SendAllSprites
ou call. Il s'agit encore une fois, comme pour une liste,
d'un pointeur. Pour un comportement, le me représente l'instance
du script qui est associé au sprite.
5.4. Fonction call avec une liste
Le comportement de la flèche envoie son message à la liste de
balles avec la fonction call. La fonction call permet d'envoyer
un message à une instance de script ou à une liste d'instances
de scripts. Dans notre cas, la liste de balles contient un pointeur
sur l'instance du comportement Balle du sprite de la balle.
5.5 Résumé
Grâce à un seul SendAllSprites et à la liste des balles,
nous pouvons maintenant n'envoyer plus qu'un seul message à la
balle au lieu d'inonder de messages tous les sprites de l'animation.
Il y a cependant un problème si la balle est située sur une piste
plus élevée que la flèche. Dans ce cas, lorsque la flèche envoie
son message d'initialisation depuis son beginSprite, la
balle n'existe pas encore et ne peut donc pas répondre au message.
La liste de balles reste vide et l'animation ne fonctionne pas.
6. L'aller-retour
Nous avons vu que lorsqu'un sprite apparaît sur la scène, le
gestionnaire beginSprite est appelé. Il peut envoyer un
message à tous les sprites avec la fonction SendAllSprites,
mais seuls les sprites dont le beginSprite a déjà été appelé
peuvent répondre au message. Si un lien doit être établi avec
un sprite qui n'a pas encore été initialisé, il faut prévoir un
double mécanisme. Le premier si le sprite à atteindre est situé
sur une piste inférieure et le deuxième s'il se situe sur une
piste supérieure.
Dans notre exemple, il faut qu'une balle initialisée après la
flèche lui envoie un message pour lui signaler sa présence. On
ajoute un SendAllSprites au comportement de la balle:
-- Comportement de la
balle
on beginSprite me
-- Permet
à la flèche d'acquérir une référence sur la balle
SendAllSprites
#EnregistreBalle, me
end
La flèche peut ainsi ajouter la balle "retardataire"
à sa liste:
-- Enregistre les balles
"retardataires"
on EnregistreBalle me,
uneBalle
m_lstBalles.add(uneBalle)
end
Nous avons maintenant tenu compte des 2 cas possibles, selon
qu'un sprite est initialisé avant ou après l'autre, et notre animation
fonctionne de nouveau correctement, quelque soit l'emplacement
des sprites dans le scénario.
L'exemple 2 illustre l'établissement de la communication grâce
au SendAllSprites depuis le beginSprite, l'utilisation
d'une liste pour garder des références sur des instances de scripts
et l'aller-retour pour terminer l'initialisation dans tous les
cas.
Il y a quand même un inconvénient: il a fallu écrire 2 fois du
code pour arriver au même résultat. Ce n'est peut-être pas grand
chose de plus, mais pour la maintenance du code, il vaudrait mieux
l'éviter. En effet, si quelque chose devait être modifié, il faudrait
le faire à deux endroits et le risque d'en oublier une partie
n'est pas négligeable. L'animation pourrait très bien fonctionner,
jusqu'au jour où l'un des protagonistes doit être déplacé sur
le scénario, ce qui mettrait en évidence la correction manquante
dans l'autre partie du code.
Alors comment faire pour éviter cette redondance de code ? La
réponse est simple: attendre que tous les sprites soient initialisés
pour établir les liens entre eux.
7. Retarder l'initialisation
Il y a plusieurs possibilités pour retarder l'initialisation
des liens entre les sprites. Le but étant d'attendre que tous
les sprites aient été initialisés, on peut attendre le premier
prepareFrame, enterFrame, exitFrame, stepFrame,
ou même la première fois qu'il est nécessaire de communiquer.
Le principe est le même à chaque fois: tester si l'initialisation
à déjà eu lieu et initialiser si ce n'est pas le cas.
7.1. Attendre le premier prepareFrame
Tant que l'initialisation n'a pas été faite, la liste a la valeur
void. Lors des passages suivants dans la fonction prepareFrame,
la liste existe et il n'y a plus rien à faire.
-- Comportement de la
flèche haut
property m_lstBalles
on prepareFrame
me
if voidP(m_lstBalles)
then
m_lstBalles = []
SendAllSprites #InitListBalles,
m_lstBalles
end if
end
on mouseDown me
call
#Monte, m_lstBalles
end
La balle n'a plus qu'à répondre au message d'initialisation:
-- Dans le comportement
de la balle
on InitListBalles me,
lstBalles
lstBalles.add(me)
end
on Monte me
valeur = sprite(me.spritenum).blend
valeur = valeur + 10
if valeur > 100
then valeur = 0
sprite(me.spritenum).blend
= valeur
end
Cette manière de faire est probablement celle qui a le meilleur
rapport efficacité/complexité pour établir des liens entre des
sprites.
On a cependant introduit un test supplémentaire qui est effectué
lors de chaque changement d'image. Dans l'immense majorité des
cas d'application, ce ne sera jamais un problème et l'effet peut
être complètement ignoré. Pour la beauté de l'art, nous allons
encore voir qu'il est possible de le supprimer.
7.2. Utiliser the actorList
Sans entrer dans les détails, un script peut s'ajouter dans une
liste spéciale définie par Director: the actorList. Les
objets dans actorList reçoivent un message stepFrame
à chaque passage de la tête de lecture sur une image.
Le comportement suivant utilise cette propriété pour, lors du
premier stepFrame, envoyer un message d'initialisation
au sprite auquel il est affecté. Une fois sa tâche accomplie,
il se supprime lui-même de la liste pour ne plus recevoir les
messages suivants. De cette manière il n'y a plus besoin de tester
si l'initialisation a été faite. Le comportement se supprime aussi
de la liste des comportements du sprite, même si cela n'est pas
indispensable. A noter: l'astuce qui permet de ne pas perdre un
message quand un script est supprimé de scriptInstanceList.
-- Le sprite qui possède
ce comportement reçoit
-- le message #InitSprite dès que tous les sprites
-- présents sur l'image sont initialisés. Le but
-- est de pouvoir à ce moment envoyer un message
-- à tous les sprites avec un SendAllSprites.
--
-- Ecrit par Pierre Rossel le 23 mars 2000 pour illustrer
-- un article sur la communication inter-sprites
property spriteNum
on beginSprite
me
(the
actorlist).add(me)
end
on endSprite
me
-- Au cas ou on est encore dedans
(Par exemple en cas
-- d'erreur en mode auteur avant le prepareFrame)
(the
actorlist).deleteone(me)
end
on stepFrame
me
-- C'est maintenant qu'on envoie
le message à tous les
-- comportements du sprite
call(#InitSprite,
sprite(spriteNum).scriptInstanceList)
end
on prepareFrame
me
-- Pour ne plus recevoir les
prochains #stepFrame
(the
actorlist).deleteone(me)
-- Le fait d'enlever un script
de la liste empèche le
-- suivant de recevoir son message.
Donc on l'envoie nous même.
lst = sprite(spriteNum).scriptInstanceList
pos = lst.getPos(me)
lst.deleteAt(pos)
if pos <= lst.count
then
call(#prepareFrame,
[lst[pos]])
end if
end
Si le comportement InitSprite est ajouté à la flèche,
son comportement flèche peut se simplifier ainsi:
-- Comportement de la
flèche haut
property m_lstBalles
-- Message envoyé par le comportement InitSprite
on InitSprite
me
m_lstBalles = []
SendAllSprites #InitListBalles, m_lstBalles
end
on mouseDown me
call
#Monte, m_lstBalles
end
8. Conclusion
Faire communiquer les sprites entre eux n'est pas très compliqué.
Le moyen le plus simple est à éviter car il rend le code Lingo
dépendant de la position des sprites sur le scénario. Avec un
minimum d'effort, il est possible de s'affranchir de cette dépendance.
Le tableau suivant résume les avantages et inconvénients de chaque
méthode. Celles qui sont dépendantes d'un numéro de sprite
ont été volontairement omises.
SendAllSprites chaque fois
que nécessaire
Simple
Pas efficace s'il doit être effectué souvent avec beaucoup
de sprites sur la scène.
SendAllSprites depuis le
beginSprite pour remplir une liste de scripts, puis
utilisation de la fonction call
Efficace pour appels fréquents
Nécessite du code à double pour tenir compte de tous les
cas.
SendAllSprites depuis le
prepareFrame pour remplir une liste de scripts, puis
utilisation de la fonction call
Efficace pour appels fréquents
Nécessite un test à chaque prepareFrame
SendAllSprites depuis le
premier stepFrame pour remplir une liste de scripts,
puis utilisation de la fonction call