%@LANGUAGE="VBSCRIPT" CODEPAGE="1252"%>
| Télécharger
le .dir commenté correspondant à cette dernière étape
de l'exercice (bouton droit -> enregistrer la cible) |
Dans cette dernière étape, nous allons terminer l'implémentation des fonctionnalités que nous nous étions fixées au départ.
Si nous analysons la situation que nous avons à l'issue de cette étape, elle devrait rappeler, dans son fonctionnement, des notions que nous avons déjà vues à l'exercice précédent.
En effet, on s'aperçoit,
en décrivant l'animation, qu'en réalité, celle-ci a 4 manières
de fonctionner : en automatique, en arrêt sur image, en boucle sur une
petite séquence, ou en manuel.
Nous avons rencontré une situation strictement similaire, bien que graphiquement
totalement différente dans la dernière étape du pendule,
où nous pouvions soit l'attraper pour l'écarter, soit le laisser
osciller en s'amortissant.
Et nous avions résolu le problème en utilisant la notion de mode de fonctionnement.
Dans le cas de la dernière étape de l'avion, nous allons utiliser exactement le même concept : en effet, chacun des 4 fonctionnements de l'animation correspond à un "mode" de fonctionnement.
Rappelons ici comment
nous avions traduit cela en terme de modélisation : les seules modifications
que nous avions apportées se trouvaient dans les méthodes du sprite,
à la (presque) exclusion de tout le reste.
A savoir que nous avions écrit une méthode "d'entrée"
sur le sprite, que nous avions appelée fonctionner(), que nous
avions créé la propriété leMode, et que
fonctionner() aiguillait sur la méthode convenable en fonction
de la valeur de leMode.
Parallèlement, le script d'animation moteur, lui, envoyait au sprite
l'ordre d'exécuter fonctionner().
Quant à la propriété leMode, nous avions vu que nous pouvions faire changer sa valeur depuis n'importe quel élément d'interface, explicite ou non.
Nous allons faire la même chose ici !
Commençons par modifier notre script moteur, pour donner l'ordre au sprite d'exécuter fonctionner(). Moteur devient donc :
on
moteur
sprite(1).fonctionner()
updateStage
end
Ensuite de quoi, toujours fidèle à notre habitude de créer désormais les structures avant de les remplir, nous pouvons écrire, dans les méthodes du sprite, quatre méthodes, vides de contenu, correspondant chacune à un des modes de fonctionnement. Dans l'exercice, nous les avons appelées :
on
jouer me
end
(celle-ci est la même que celle que nous avions jusque
là)
on
figer me
end
(celle-ci mérite un commentaire particulier)
on
boucler me
end
et enfin
on
jouerManuel me
end
Puis nous avons écrit la méthode fonctionner (qu'il est inutile de commenter tant le code en est simple) :
on
fonctionner me
if leMode
= 1 then
![]()
sprite(spriteNum).jouer()
else if
leMode = 2 then
![]()
sprite(spriteNum).figer()
else if
leMode = 3 then
![]()
sprite(spriteNum).boucler()
else if
leMode = 4 then
![]()
sprite(spriteNum).jouerManuel()
end if
end
Rappelons seulement
que dans une architecture à mode unique, on se trouvait dans la situation
suivante : un timeOut déclenche un script d'animation, qui lui
même fait exécuter une méthode à un objet.
C'est le schéma : timeOut -> moteur -> méthode d'objet
Ici, nous avons simplement rajouté une étape supplémentaire
: un timeOut déclenche un script d'animation, qui lui-même fait
exécuter une méthode à un objet, cette méthode elle-même
appelant une autre méthode en fonction d'un paramètre.
Le schéma est le suivant : timeOut -> moteur -> méthode
d'objet "décisionnaire" (fonction du paramètre)
-> méthode d'objet qui "fait le travail"
Il s'agit là de la description d'une architecture : rien n'empêcherait de se trouver en présence d'un modèle plus complexe, dans lequel plusieurs sprites seraient concernés, où le choix d'un fonctionnement serait le résultat non plus d'un paramètre mais de plusieurs... Lesquels pourraient évoluer de manière dépendante ou indépendante, peu importe...
Nous verrons également dans d'autres exercices que les "méthodes qui font le travail" peuvent elles-mêmes appeler d'autres méthodes qui "font un sous-travail", elles-mêmes avec des paramètres éventuels. On peut empiler les briques autant qu'on le veut. Ce n'est qu'une complexification apparente : dans ce dernier cas, on reparle de code réutilisable quand le "sous-travail" est susceptible d'intervenir à plusieurs endroits, ou encore d'être utilisé par plusieurs entités ou objets différents dans l'ensemble du programme. Si ce code n'est pas susceptible d'être utilisé par plusieurs entités ou en plusieurs endroits du programme général, on parlera seulement de "fractionnement" du code. L'utilité de ce fractionnement est la mise au point, le déboguage, et surtout la maintenance ultérieure. Il est en effet beaucoup plus facile de lire un petit morceau de code (et de comprendre ce qu'il fait), plutôt qu'un long listing.
Maintenant que nous avons écrit notre structure, nous pouvons à nouveau constater que si nous testons notre animation, elle ne fait certes pas encore ce que nous souhaitons (et pour cause, nous n'avons pas encore écrit le code qui constituera les méthodes), mais elle ne fait pas d'erreur.
Il ne nous reste
plus qu'à nous occuper de deux choses :
- remplir les méthodes qui "font le travail" dans
chacun des modes,
- s'occuper d'écrire le dispositif qui fera changer la valeur de leMode
de la manière dont on le souhaite.
La méthode jouer() est la même que précédemment : inutile donc d'y revenir.
La méthode figer() peut paraître curieuse et elle fera sourire certains :
on
figer me
nothing
end
En effet, on peut
se demander quelle est l'utilité d'écrire une ligne de code qui
donne l'ordre de ne rien faire.
On aurait pu tout à fait laisser la méthode vide : notre animation
aurait fonctionné de la manière souhaitée.
Encore plus simple, on aurait pu ne pas écrire la méthode du tout,
et enlever de la méthode fonctionner() les lignes
else
if leMode = 2 then
![]()
sprite(spriteNum).figer()
Là encore,
notre animation aurait fonctionné selon nos souhaits puisque, au moment
où leMode vaut 2, la méthode fonctionner() n'aurait
rien appelé du tout, et donc notre animation aurait été
figée.
Alors pourquoi écrire cette méthode figer(), avec cette
instruction nothing ?
Pour deux raisons :
- pour une question de compréhension humaine à la lecture de l'ensemble
du code. Lorsqu'on lit ce code, on sait qu'il existe un mode dans lequel l'animation
est "en panne". Il n'y a pas d'oubli, on lit clairement qu'on a prévu
un fonctionnement figé.
- pour une question de cohérence logique dans la modélisation.
Le mode figé ne correspond pas à un vide ni à une absence.
C'est un mode qui est prévu par le concepteur, dans
lequel il est prévu qu'il ne se passe rien. Même
si une absence de code aurait abouti au résultat souhaité, cette
absence de code n'aurait pas été conforme à la réalité
de la conception du dispositif, laquelle prévoit explicitement
un cas où il ne se passe rien. Il faut donc l'écrire pour indiquer
là aussi explicitement que l'on traite ce cas.
Cette exigence peut paraître tatillonne ou exagérée : il
n'en est rien. Il est important que ce que l'on écrit reflète
pleinement le dispositif que l'on a prévu. Et ceci d'autant plus que
le dispositif est complexe.
Ce n'est pas la
première fois que l'on rencontre cette problématique de plusieurs
solutions arrivant au même résultat. D'une manière générale,
la programmation a ceci de similaire à la cuisine que la même recette
sera exécutée différemment par différents cuisiniers.
On aura toujours du cassoulet, mais il ne sera jamais tout à fait le
même (ni tout à fait un autre, aurait rajouté le poète…).
Lorsqu'on écrit un traitement informatique, on devra toujours choisir,
outre des considérations sur l'éléganceet le concision,
sur l'économie des ressources de la machine, la solution qui est la plus
en cohérence avec le déroulement réel du traitement, et
avec la signification du discours présenté par l'exécution
du programme final.
C'est le même souci qui nous a fait écarter le choix de lire en
avant une séquence inversée pour simuler une lecture en arrière,
qui nous a fait appliquer le code du pendule à la masselotte plutôt
qu'au point fixe, etc...
Voyons maintenant
la méthode boucler().
Elle ne pose pas de difficulté non plus.
Il nous suffit de remarquer que, dans ce mode, nous lisons en avant, puis en
arrière, puis en avant, puis en arrière, etc... une petite séquence
de 10 images.
La méthode boucler() est donc exactement la même que jouer(),
avec une liste d'acteurs différente.
Il nous suffira donc de modifier notre liste d'acteurs quand nous passons dans
ce mode, de manière à substituer les 10 images de la boucle à
la séquence complète utilisée dans jouer().
Ceci se fera dans le dispositif qui nous permet de changer la valeur de
leMode (voir ci-dessous). C'est dans ce dispositif qu'au moment où
on mettra leMode à 3, nous changerons le contenu de la propriété
lstActeurs de notre sprite pour lui faire jouer une sous-boucle de
la séquence complète.
On pourrait remarquer
ici que dans fonctionner(), quand leMode vaut 3, on pourrait
aussi appeler la méthode jouer(). A ceci près que :
- de la même manière que pour figer(), on ne décrirait
pas fidèlement le fait qu'il existe un mode en boucle,
- la méthode boucler() est la même que jouer()
à un détail près : dans jouer(), on déplace
le sprite en fonction du rang de l'image affichée, on ne le fait pas
dans la boucle. boucler() a donc une ligne de code de moins que jouer()
(On pourrait le faire, mais alors on change le cahier des charges).
Il nous reste à écrire la méthode jouerManuel().
Dans cette méthode,
nous avons deux choses à faire :
- assurer le déplacement du sprite contenant l'image pour lui faire suivre
la souris,
- afficher le bon acteur, fonction de la position.
Déplacer
le sprite :
C 'est une manipulation que nous avons déjà faite, dans l'exercice
du pendule, pour assurer le déplacement de la poignée du curseur.
Rappelons en le principe :
si la souris est à droite de la position gauche extrême
ou si la souris est à gauche de la position droite extrême alors
(c'est à dire entre les deux positions extrêmes)
la position du sprite est
égale à la position de la souris
Sinon si la souris est à gauche de la position gauche extrême alors
le sprite est en butée
sur sa position gauche extrême
sinon si la souris est à droite de la position droite extrême alors
le sprite est en butée
sur sa position droite extrême
fin si
Ce qui donne en
code :
on jouerManuel me
if
the mouseH > (sprite(spriteNum).member.width / 2) and the mouseH < 900
- (sprite(spriteNum).member.width / 2) then
![]()
sprite(spriteNum).locH
= the mouseH
else
if the mouseH < (sprite(spriteNum).member.width / 2) then
![]()
sprite(spriteNum).locH
= (sprite(spriteNum).member.width / 2)
else
if the mouseH > 900 - (sprite(spriteNum).member.width / 2) then
![]()
sprite(spriteNum).locH
= 900 - (sprite(spriteNum).member.width / 2)
end
if
end
Les positions extrêmes étant, en effet, les bords gauche et droit de la scène, augmentés et diminués respectivement d'une demie-largeur de l'acteur.
Trouver
le bon acteur à afficher :
C'est le problème inverse de celui que nous avons traité dans
jouer(). Dans jouer(), nous connaissions l'acteur qui était
affiché (et son rang dans la liste), et nous en déduisions la
position que devait occuper le sprite sur la scène. Ici, nous connaissons
la position, il faut en déduire l'acteur. C'est également une
règle de trois.
La course totale est de 580 pixels, sur lesquels doivent se répartir
200 images, soit 2,9 pixels par image.
Donc, si l'image se trouve à n pixels de distance de la position
de départ, le numéro de l'acteur vaut : valeur entière
de (n / 2.9)
Valeur entière parce que le numéro d'un élément
d'une liste ne peut pas être décimal.
D'autre part, il faut faire attention au point suivant : si le sprite est en
butée gauche, alors n vaut 0 et le numéro calculé
vaut 0 également. Or il n'y a pas d'élément numéro
0 dans une liste Lingo. Si on ajoute 1 à la valeur trouvée, on
aura un problème à l'autre bout de la course, à savoir
que si n vaut 580, n/2,9 vaut 200, et que si on ajoute 1 on
trouvera 201. Comme il n'y a que 200 éléments dans la liste, on
aura une erreur.
La solution est donc la suivante : rajouter une condition après le calcul
du numéro pour dire que si le numéro trouvé vaut 0, alors
on le fixe arbitrairement à 1.
Ce qui donne le code suivant pour l'ensemble de notre méthode jouerManuel()
:
on
jouerManuel me
if
the mouseH > (sprite(spriteNum).member.width / 2) and the mouseH < 900
- (sprite(spriteNum).member.width / 2) then
![]()
sprite(spriteNum).locH
= the mouseH
else
if the mouseH < (sprite(spriteNum).member.width / 2) then
![]()
sprite(spriteNum).locH
= (sprite(spriteNum).member.width / 2)
else
if the mouseH > 900 - (sprite(spriteNum).member.width / 2) then
![]()
sprite(spriteNum).locH
= 900 - (sprite(spriteNum).member.width / 2)
end
if
numero
=integer( (sprite(spriteNum).locH - (sprite(spriteNum).member.width/2))/2.9)
if
numero <= 0 then
![]()
numero
= 1
end
if
sprite(spriteNum).member
= lstActeurs[numero]
end
Une remarque à
propos de la fonction integer(nombre) : elle s'énonce "partie
entière du nombre". Mais il y a une petite ambiguïté.
Normalement, une partie entière désigne la valeur qui est à
gauche de la virgule. C'est à dire que partie entière de 4,35
vaut 4 et partie entière de 7,68 devrait valoir 7.
Or, en lingo, integer(7,68) = 8.
C'est à dire que la fonction integer(nombre) devrait en fait
se traduire par "arrondi de nombre". Ce qui donne : integer(3,4)
= 3, integer(3,5) = 4 et integer(3,8) = 4.
D'autres langages comme javascript ou actionScript possèdent les fonctions
round(nombre), qui donne l'arrondi, floor(nombre) qui donne
la partie entière (cette fois au sens propre), ceil(nombre)
- comme ceiling, plafond - qui donne l'entier immédiatement
supérieur quelle que soit la valeur de la décimale.
Cela manque en lingo et si on veut floor() et ceil(), il faut
les écrire(*).
Pour terminer notre exercice, il nous reste à implémenter le dispositif qui va permettre de faire varier la propriété leMode.
Dans le fonctionnement
que nous nous sommes donnés, nous avons dit qu'on passait d'un mode de
fonctionnement à un autre, en séquence, et en cliquant sur l'image.
L'événement qui va donc déclencher un changement de valeur
de leMode sera donc un mouseUp sur le sprite.
Comme nous avons également décidé que le changement se
ferait séquentiellement, nous écrirons donc que lorsqu'on clique
sur le sprite, leMode sera incrémenté de 1, et ceci à
condition que leMode soit inférieur à 4, puisque nous
avons 4 modes distincts. Quand nous serons dans le dernier mode, nous reviendrons
au premier mode.
Ce qui donnera
:
on mouseUp
si
leMode est inférieur à 4 alors
![]()
leMode
= leMode + 1
sinon
si leMode = 4 alors
![]()
leMode
= 1
fin
si
fin
et en code :
on
mouseUp me
if
leMode < 4 then
![]()
leMode
= leMode + 1
else
![]()
leMode
= 1
end
if
end
Nous avons presque
terminé l'exercice.
Nous avions signalé, au moment d'écrire la méthode boucler(),
qu'il fallait modifier la propriété lstActeurs, de manière
que dans ce mode, l'animation tourne sur une petite boucle.
C'est simple. Il suffit de regarder, au moment du clic, à quel acteur
se trouve l'animation, de regarder le rang de cet acteur dans la liste générale,
et de prendre les 10 suivants, qu'on stocke dans une autre liste temporaire.
Puis on vide la liste des acteurs, et on la réinitialise avec les 10
acteurs sélectionnés et inscrits dans la liste temporaire.
Il faut juste faire attention à une chose : si, au moment du clic, on
est presque à la fin de la liste des acteurs, et que le rang est éloigné
de moins de 10 unités de la fin de la liste, alors on aura une erreur
lorsqu'on voudra prendre les 10 acteurs suivant l'acteur courant.
Il y aura donc une condition qui prévoira :
si l'acteur courant est supérieur à (nombre d'acteurs
de la liste - 10) alors
l'acteur
courant = nombre d'acteurs dans la liste - 10
fin si
Par ailleurs, quand
on recliquera pour passer au mode suivant, il ne faudra pas oublier de réinitialiser
la liste des acteurs avec la totalité de la séquence.
Notre script mouseUp devient donc :
on
mouseUp me
if
leMode < 4 then
![]()
leMode
= leMode + 1
else
![]()
leMode
= 1
end
if
if
leMode = 3 then
![]()
acteurCourant
= sprite(spriteNum).member.name
![]()
leRang
= lstActeurs.getPos(acteurCourant)
![]()
if
leRang > lstActeurs.count() - 10 then
![]()
![]()
leRang
= lstActeurs.count() -10
![]()
end
if
![]()
lstBoucle
= []
![]()
repeat
with i = 1 to 10
![]()
![]()
lstBoucle.add(lstActeurs[leRang
+ (i-1)])
![]()
end
repeat
![]()
lstActeurs
= []
![]()
repeat
with i = 1 to lstBoucle.count()
![]()
![]()
lstActeurs.add(lstBoucle[i])
![]()
end
repeat
else
![]()
lstActeurs
= []
![]()
repeat
with i = 1 to 200
![]()
![]()
lstActeurs.add(member(i,
2).name)
![]()
end
repeat
end
if
end
Nous avons maintenant
terminé notre exercice.
L'exercice suivant, le memory, permettra de réviser les listes.
(*) - Voici les
deux scripts qui donnent les plancher et plafond d'un nombre décimal
:
on
floor(nombre)
laDifference
= nombre - integer(nombre)
--
si la partie décimale de nombre est inférieure à .5, alors
laDifference est positif
--
si la partie décilmale de nombre est supérieure ou égale
à 0.5 alors laDifference est négatif
if
laDifference >= 0 then
![]()
nombre
= nombre - laDifference
else
![]()
nombre
= integer(nombre) - 1
end
if
return
nombre
end
on
ceil(nombre)
laDifference
= nombre - integer(nombre)
--
si la partie décimale de nombre est inférieure à .5, alors
laDifference est positif
--
si la partie décimale de nombre est supérieure ou égale
à 0.5 alors laDifference est négatif
if
laDifference >= 0 then
![]()
nombre
= integer(nombre) + 1
else
![]()
nombre
= integer(nombre)
end
if
return
nombre
end
| Retour à la cinquième étape |