<%@LANGUAGE="VBSCRIPT" CODEPAGE="1252"%> 3e exo - L'avion - étape 6 Sixième étape
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
Retour au sommaire