EPOOL - Environnement de Programmation Orientée Objet en Lingo par Irv Kalb
Plus d'exemples d'objets
Dans ce chapitre nous allons regarder davantage d'exemples d'objets. Nous allons regarder un exemple d'objet d'intervalle aléatoire, d'objet Tableau et d'objet Manager de Sprite.
Souvent dans le développement d'un jeu vous avez besoin d'obtenir des nombres aléatoires compris dans un certain intervalle. Vous pouvez souhaiter présenter dix éléments à l'utilisateur (par exemple dix questions ou dix illustrations), mais vous souhaitez que cette présentation se fasse dans un ordre aléatoire. Dans un quizz vous pouvez vouloir rendre aléatoire l'ordre des questions, et aussi l'ordre des réponses proposées. Afin de disposer d'une solution générique pour ces cas de figure, vous pouvez créer un objet d'intervalle aléatoire. Vous lui donnez le nombre maximum d'éléments puis vous pouvez lui demander de vous donner n'importe quel élément compris dans cet intervalle. Il ne devra pas vous renvoyer deux fois le même élément avant d'avoir envoyés tous les éléments de l'intervalle. Comme algorithme de base, nous commencerons par créer une liste d'entiers de 1 à n:
-- Création
de la liste
lNombre = []
repeat with i = 1 to n
append(lNombre, i)
end repeat
Lorsque nous voulons un nombre aléatoire, nous choisissons n'importe quel élément de la liste, le renvoyons comme réponse, et l'enlevons de la liste. De plus lorsque nous avons épuisé la liste des éléments à envoyer nous devons recréer une liste pour pouvoir continuer de répondre aux requêtes. Maintenant que nous avons décrit notre approche, nous devons implémenter cet algorithme de base et écrire un objet qui remplit ces fonctions. Voici un script parent qui répond à nos attentes:
--
Script d'Intervalle Aleatoire
property plNombres -- liste des
nombres
property pQuantiteMax -- quantité
maximum de nombres
on new me, combien
pQuantiteMax = combien
me.mInit()
return me
end new
on
mInit me
-- Création de la liste
plNombres = []
repeat with i = 1 to pQuantiteMax
append(plNombres, i)
end repeat
end
on mGetAuHasard me
nElements = count(plNombres)
if nElements
= 0 then -- il est
temps de recréer la liste
me.mInit()
nElements = pQuantiteMax
end if
indexAleatoire = random(nElements)
-- choisit un élément de la liste au hasard
valeurARetourner = plNombres[indexAleatoire]
-- lit sa valeur
deleteAt(plNombres,
indexAleatoire)
-- enlève cet élément de la liste
return valeurARetourner
end mGetAuHasard
L'objet a deux propriétés: plNombres qui
est la liste complète des nombres, et pQuantiteMax qui mémorise
le nombre maximum d'éléments contenus dans la liste. Il y a trois
méthodes: la méthode habituelle "new", mInit et mGetAuHasard.
A l'instanciation de l'objet vous lui passez en paramètre le nombre maximum
d'éléments que vous désirez, et l'objet retient ce nombre
dans sa propriété pQuantiteMax. Ensuite il appelle sa méthode
mInit pour initialiser la liste. La méthode "mInit" se contente
de créer une liste d'entiers de un au nombre maximum d'éléments.
Nous avons vu précédemment
que pour appeller une méthode d'un objet, vous deviez écrire une
ligne du type:
goUnObjet.mUnNomDeMethode()
Mais ici nous nous trouvons à l'intérieur de l'objet, et nous appelons une autre méthode du même objet. Dans ce cas - très fréquent - là où nous utiliserions normalement une référence d'objet, nous utilisons le mot-clé "me". Ce mot-clé "me" est une référence d'objet particulière: elle réfère toujours à l'instance du script courant. C'est pourquoi "me" permet d'appeler une autre méthode du même objet:
me.mUnNomDeMethode()
A chaque fois que vous voulez obtenir un nombre aléatoire, vous faites appel à la méthode mGetAuHasard. Cette méthode choisit un élément quelconque de la liste et l'y supprime. Au début de la méthode, nous vérifions toujours que la liste n'est pas vide, c'est-à-dire que le nombre d'éléments restants dans la liste est supérieur à zéro. Si la liste ne contient plus d'éléments nous appelons la méthode mInit de l'objet pour recréer la liste.
Il y a autre chose d'intéressant à remarquer dans ce script. Cet objet a trois méthodes, mais deux seulement de ces méthodes sont vraiment accessibles "de l'extérieur" de l'objet; les méthodes "new" et mGetAuHasard. La méthode mInit fournit un traitement interne à l'objet qui est appelé depuis deux endroits - elle est appelée depuis les méthodes new et mGetAuHasard - mais elle n'est pas sensée être appelée de l'extérieur de l'objet. Les méthodes ainsi conçues pour n'être appelées que par d'autres méthodes du même objet (que de l'intérieur du script où elle sont définies) sont appelées des "méthodes privées". Les méthodes susceptibles d'être appelées depuis l'intérieur ou l'extérieur de l'objet sont identifiées comme "méthodes publiques".
En fait d'autres langages utilisent des mots-clé "PUBLIC" et "PRIVATE" pour s'assurer de la façon dont une méthode est appelée. Lingo n'offre pas de telles restrictions. Si vous publiez une documentation décrivant l'API de l'objet IntervalleAleatoire aux autres programmeurs, vous n'y ferez figurer que les méthodes new et mGetAuHasard. Vous omettrez intentionnellement la méthode mInit puisqu'elle n'est pas sensée être appelées par d'autres gestionnaires que ceux de l'objet. (Une très bonne alternative est d'étendre la convention de nommage pour apporter la distinction entre méthodes publiques et privées. Un préfixe en "m" suffisant pour les méthodes publiques, et un préfixe "mi" - méthode interne - pour les méthodes privées. Avec une telle convention la méthode Init interne serait nommée "miInit".)
Si vous regardez dans le détail le code de l'objet IntervalleAleatoire, vous pourrez remarquez un éventuel défaut. Supposons que vous instanciez un objet IntervalleAleatoire qui gère des nombres de 1 à 10.Au cours de votre programme vous y faites appel 10 fois et il vous renvoie les valeurs dans un ordre aléatoire. Pour clarifier l'exemple, imaginons que le dernier numéro retourné est 5. Si vous faites à nouveau appel à mGetAuHasard, l'objet va régénérer la liste des nombres et en renvoyer un au hasard. Il est tout à fait possible que le premier nombre renvoyé soit encore 5. Du point de vue de l'utilisateur, cela correspond à un 5 sortant deux fois d'affilée. Voici un défaut de notre implémentation. il serait mieux d'éviter la sortie du même nombre deux fois consécutives, même lorsque l'objet régénère sa liste d'éléments. Voici une version modifiée du script qui prend en compte ce problème.
--
Script d'Intervalle Aleatoire
property plNombres -- liste des
nombres
property pQuantiteMax -- quantité
maximum de nombres
property
pDerniereValeur
on new me, combien
pQuantiteMax = combien
me.mInit()
return me
end new
on mInit me
-- Création de la liste
plNombres = []
repeat with i = 1 to pQuantiteMax
append(plNombres, i)
end repeat
end
on mGetAuHasard me
nElements = count(plNombres)
if nElements
= 0 then -- il est
temps de recréer la liste
me.mInit()
nElements = pQuantiteMax
end if
-- Vérifie que la valeur choisie n'est
pas la même que la précédente
repeat while
TRUE
indexAleatoire = random(nElements) -- choisit un élément
de la liste au hasard
valeurARetourner = plNombres[indexAleatoire] -- lit sa valeur
if valeurARetourner <> pDerniereValeur then
exit repeat
end if
end repeat
deleteAt(plNumbers, indexAleatoire)
-- enlève cet élément
de la liste
pDerniereValeur
= valeurARetourner
return valeurARetourner
end mGetAuHasard
Remarquez que nous avons ajouté une propriété nommée pDerniereValeur qui conserve la valeur du dernier élément retourné. Ensuite dans la méthode mGetAuHasard, lorsque nous choisissons un élément au hasard, nous vérifions qu'il est différent du dernier élément retourné. S'il est le même que le dernier élément retourné nous restons dans la boucle jusqu'à en choisir un différent.
Nous venons juste de démontrer un concept important de programmation orientée objet. Alors que nous avons modifié l'implémentation du script IntervalleAleatoire pour corriger un défaut, nous n'avons pas modifier l'API de l'objet. Cela signifie qu'aucun changement n'est à effectuer où que ce soit dans le reste du programme. Plus particulièrement les clients de l'objet IntervalleAleatoire ne subissent aucune modification. Nous avons modifié une petite portion du code d'un script parent, mais l'amélioration sera ressentie par n'importe quel gestionnaire faisant appel à un objet IntervalleAleatoire quelque part dans le programme. Mettre le code dans un script parent l'a isolé du reste du programme. Si par erreur nous avions introduit un bug en faisant ces modifications, nous saurions que le bug serait dans le script parent IntervalleAleatoire puisque nous n'avons rien modifié aux appels à l'objet.
De nombreux autres langages de programmation possèdent une structure de base appelée table (array). Un tableau est un bon exemple d'une simple table à deux dimensions - un ensemble de données qui peuvent être représentées en lignes et en colonnes. On parle de table de n sur m, par exemple une table de 6 sur 4, où 6 représente le nombre de lignes et 4 le nombre de colonnes. Chaque item du tableau est appelé une cellule ou un élément. Vous faites référence à une cellule par ses indices de lignes et de colonnes. La cellule de la troisième ligne et de la deuxième colonne peut être indiquée table(3,2). La cellule de la ligne 1 et de la colonne 5 serait table(1,5).
Avec Director 7, Director offre une nouvelle syntaxe pour accéder aux éléments de listes imbriquées. Supposons que nous voulons une table de 4 sur 5 où les valeurs seraient définies à 7. Le code suivant nous permet de créer cette table:
on creationTable
global glTable
glTable = [[7, 7, 7, 7, 7],[7, 7, 7, 7, 7],[7, 7, 7, 7, 7],[7, 7, 7, 7, 7],[7, 7, 7, 7, 7]]
Lorsque nous voulons accéder à une donnée, nous pouvons récupérer la valeur d'une cellule avec la syntaxe:
glTable[numeroLigne][numeroColonne]
La syntaxe de la définition de listes imbriquées ci-dessus est plutôt énigmatique. De plus, l'approche en listes imbriquées n'est pas envisageable dans tous les cas. Les listes Lingo sont "basées sur 1", c'est-à-dire que le premier élément est à l'indice 1. Comment faire si nous voulons une table "basée sur 0" où les indices commenceraient à zéro ? Et comment faire si nous voulions être sûrs que les indices de ligne et de colonne auxquels nous nous référons sont valides avant d'y accéder ?
Une utilisation importante des objets est de modeler les données si bien que les clients de l'objet peuvent utiliser les données comme ils les imaginent, sans se soucier des détails nécessaires de l'implémentation. En interne, nous pouvons utiliser une liste linéaire pour stocker les données, et écrire les algorithmes pour accéder à cette liste comme si il s'agissait vraiment d'une table à deux dimensions. Nous pouvons alors fournir une interface qui permette au client de l'objet de l'utiliser de manière évidente et intuitive. Voici un script parent Table qui réalise ce travail:
-- script Table
property pPremiereLigne -- limite
de la première ligne
property pDerniereLigne -- limite de
la dernière ligne
property pPremiereCol -- limite de la première colonne
property pDerniereCol -- limite de la
dernière colonne
property pValeurInitiale -- valeur initial (pour initialisation)
property pnCols -- nombre de colonnes,
(stocké ici pour la vitesse)
property plCelulles -- les données de la table
property pfVerifDepassement -- booléen
(flag) utilisé pour débugger
property pnCelulles -- le nombre de cellules
-- Création de la table en passant les limites supérieures
et inférieures
-- des lignes et colonnes ainsi que la valeur initiale
on new me, premierLigne, derniereLigne,
premiereCol, derniereCol, valeurInitiale
pPremiereLigne = premierLigne
pDerniereLigne = derniereLigne
nLignes = pDerniereLigne - pPremiereLigne + 1
pPremiereCol = premiereCol
pDerniereCol = derniereCol
pValeurInitiale = valeurInitiale
pnCols = pDerniereCol - pPremiereCol + 1
pnCelulles = nLignes * pnCols
-- Création du tableau sous forme de
liste linéaire
plCelulles = []
repeat with i = 1 to pnCelulles
add(plCelulles, valeurInitiale)
end repeat
pfVerifDepassement = FALSE
return me
end birth
-- Définition de la valeur d'une cellule de la table
on mSet me, uneLigne, uneCol, uneValeur
if pfVerifDepassement then
if (uneLigne < pPremiereLigne) or
(uneLigne > pDerniereLigne) then
alert("Indice de ligne non valide dans
la méthode mSet, valeur :" &&
uneLigne & RETURN & \
"Les indices valides
vont de" && pPremiereLigne &&
"à" && pDerniereLigne)
exit
end if
if (uneCol < pPremiereCol)
or (uneCol > pDerniereCol) then
alert("Indice
de colonne non valide dans la méthode mSet, valeur :"
&& uneCol & RETURN & \
"Les
indices valides vont de" &&
pPremiereCol && "à" && pDerniereCol)
exit
end if
end if
laCellule = ((uneLigne - pPremiereLigne) * pnCols) + (uneCol - pPremiereCol)
+ 1
plCelulles[laCellule] = uneValeur
end mSet
-- Récupération de la valeur d'une cellule
de la table
on mGet me, uneLigne,
uneCol
if pfVerifDepassement then
if (uneLigne < pPremiereLigne) or
(uneLigne > pDerniereLigne) then
alert("Indice
de ligne non valide dans la méthode mGet,
valeur :" && uneLigne & RETURN & \
"Les indices valides vont
de" && pPremiereLigne &&
"à" && pDerniereLigne)
exit
end if
if (uneCol < pPremiereCol)
or (uneCol > pDerniereCol) then
alert("Indice
de colonne non valide dans la méthode mGet,
valeur :" &&
uneCol & RETURN & \
"Les indices valides vont
de" && pPremiereCol &&
"à" && pDerniereCol)
exit
end if
end if
laCellule = ((uneLigne - pPremiereLigne) * pnCols) + (uneCol - pPremiereCol)
+ 1
return plCelulles[laCellule]
end mGet
-- Active ou désactive la vérification de dépassement
on mSetVerifDepassement me, vraiOuFaux
pfVerifDepassement = vraiOuFaux
end mSetVerifDepassement
-- Ecrit le contenu de la table dans la fenêtre de
message
on mDebug me
repeat with
i = pPremiereLigne to pDerniereLigne
cetteLigne = ""
repeat with j = pPremiereCol to pDerniereCol
cetteLigne = cetteLigne && mGet(me,
i, j)
end repeat
put cetteLigne
end repeat
end mDebug
Même si ce script parent peut sembler compliqué, il est en fait assez simple. Si nous voulons à nouveau créer une table de 4 par 5 avec les valeurs initialisées à 7, nous le ferions avec le code :
global
goArray
goArray = new(script
"Array", 1, 4, 1, 5, 7)
Dans le gestionnaire "new", quelques calculs sont directement faits pour trouver le nombre d'éléments nécessaires pour représenter la table sous forme de liste linéaire. Avec notre exemple, nous avons besoin de 20 cellules (4 fois 5) pour représenter la table. Même si ce cas est trivial, le code écrit gère n'importe quel indice de départ. Si nous avions voulu une table avec des indices basés à 0, alors le code aurait trouvé que nous aurions eu besoin de 30 cellules (5 fois 6). Le code enregistre les limites inférieures et supérieures des indices des deux dimensions de la table dans des propriétés. Ensuite le code parcoure une boucle autant de fois qu'il y a de cellules, définissant la valeur initiale de chacune d'entre elles. La cellule 1 stocke la ligne 1 de la colonne 1. La cellule 2 stocke la ligne 1 de la colonne 2. Etc.
La fonction principale de cet objet et de lire et définir la valeur d'une cellule du tableau. Si nous oublions pour un instant le code de vérification de non-dépassement des limites, nous pouvons voir que les méthodes mSet et mGet ont une ligne commune. Pour accéder à une cellule elles utilisent toutes les deux la formule:
laCellule = ((uneLigne - pPremiereLigne) * pnCols) + (uneCol - pPremiereCol) + 1
Cette ligne calcule l'indice dans la liste linéaire qui représente la cellule visée dans la table. Dans notre exemple de table de 4 sur 5 (avec des indices démarrant à 1) supposons que nous voulons accéder à la cellule de la troisième ligne et de la troisième colonne. En suivant cette formule, nous trouvons la celulle 11. La méthode mGet fait ce calcul puis retourne la valeur lue à cet indice dans la liste linéaire. La méthode mSet permet à l'appelant de définir la valeur à cet indice dans la liste linéaire.
Intéressons-nous maintenant à la propriété pfVerifDepassement. Il s'agit d'un booléen qui précise à l'objet Table s'il doit ou non vérifier les dépassements d'indice, c'est-à-dire que les indices demandés sont bien dans la table et sont donc valides. Pendant le développement, c'est une fonctionnalité très pratique pour vous permettre de repérer des bugs dans le code qui appelle l'objet Table. Dans la méthode "new", nous initialisons cette propriété à FALSE. Cependant l'objet dispose d'une méthode mSetVerifDepassement qui vous permet de définir la valeur de cette propriété à TRUE ou FALSE. (Bien entendu de l'extérieur de l'objet, l'utilisateur ne sais pas précisément ce que cela change, il sait juste que ça active ou désactive la vérification du dépassement d'indice.) Pendant le développement, juste après avoir instancié l'objet, si vous voulez activer la vérification de dépassement vous devez ajouter la ligne:
goTable.mSetVerifDepassement(TRUE)
Cette méthode se contente de définir la propriété pfVerifDepassement. Au début des méthodes mSet et mGet, si pfVerifDepassement est activé, ces routines vérifient que les indices passés en argument sont bien dans l'intervalle des indices valides. Lorsque vous êtes convaincus que votre programme ne contient aucune erreur susceptible de générer des indices non valides, vous pouvez supprimer cet appel pour améliorer les performances.
Enfin il y a une méthode de débuggage appelée mDebug. Lorsque vous voulez voir les données de la table, vous pouvez appeler la méthode mDebug. mDebug exporte les données dans la fenêtre de message sous forme de tableau tel que l'utilisateur se le représente, et non pas sous sa forme de liste linéaire interne.
goTable.mDebug()
Dans certains jeux vous pouvez être amenés à ajouter ou retirer dynamiquement des éléments de l'écran. Au cours du jeu, les nombre de personnages, de missiles, de balles, de papillons, etc. peuvent varier. Une façon classique d'implémenter ceci est d'attribuer une piste pour chacun de ces éléments. Mais comment conserver une trace de ces pistes utilisées ? Une bonne solution est l'utilisation d'un gestionnaire de sprite. C'est un objet qui conserve une liste des sprites et de l'état de chacun d'eux, c'est-à-dire si la piste est allouée ou non. Lorsque vous avez besoin d'une nouvelle piste, vous pouvez demander au gestionnaire de sprite, qui cherche la première piste libre et vous retourne son numéro. Lorsque vous n'avez plus besoin d'une piste, vous dites au gestionnaire de sprite que vous en avez fini avec telle piste. Voici le code d'un gestionnaire de sprite qui implémente ces fonctions :
-- GestSprite
property plAllocation -- liste des états
de sprites
property pchPremiere -- numéro
de la première piste à gérer
property pchDerniere -- numéro
de la dernière piste à gérer
on new me,
chPremiere, chDerniere
pchPremiere = chPremiere
pchDerniere = chDerniere
mInit(me)
return me
end
on mInit me
plAllocation = [:]
repeat with ch = pchPremiere to pchDerniere
addProp(plAllocation, ch, FALSE)
-- initialise tout à FALSE (non alloué)
end repeat
end
on mAllouePiste me
-- cherche la premier piste libre
repeat with ch = pchPremiere to pchDerniere
fAlloue = getAProp(plAllocation, ch)
if not(fAlloue) then
-- on en a trouvé une
setProp(plAllocation, ch, TRUE)
-- on la marque comme allouée
return ch -- et on retourne son numéro
end if
end repeat
alert("Essai d'allocation de piste, mais
plus aucune de disponible")
end
on mLiberePiste me, chALiberer
if (chALiberer < pchPremiere)
or (chALiberer > pchDerniere)
then
alert("Demande de libération de
la piste" && chALiberer &&
"qui n'est pas gérée.")
return
end if
fAllocated = getProp(plAllocation, chALiberer)
if not(fAllocated) then
alert("Demande de libération de
la piste" && chALiberer &&
"qui n'a pas été allouée.")
return
end if
setProp(plAllocation, chALiberer,
FALSE) -- on remet l'indicateur
d'allocation à FALSE (non alloué)
end
on mDebug me
put "Allocation de
piste:"
repeat with
ch = pchPremiere to pchDerniere
fAlloue = getProp(plAllocation, ch)
if fAlloue then
put ch && ": alloué"
else
put ch && ": non alloué"
end if
end repeat
end
Lorsque vous instanciez le gestionnaire de sprite, vous lui passez en paramètre les numéros des pistes qui bornent l'intervalle de pistes à gérer. Ensuite vous disposez de deux méthodes de base: mAllouePiste vous sert à connaître la première piste libre, et mLiberePiste permet de libérer une piste précédemment allouée. mDebug vous permet d'avoir un aperçu dans le fenêtre de message de l'état de chacune des pistes gérées par le gestionnaire de sprite.
En interne le gestionnaire de sprite travaille sur une liste de propriétés. Chaque élément de la liste de propriétés est du type numeroDePiste : fEstAllouee . Par exemple si vous avez créé un gestionnaire de sprite qui s'occupe des pistes 20 à 24, la propriété plAllocations sera initialement:
[20:FALSE, 21:FALSE, 22:FALSE, 23:FALSE, 24:FALSE]
Lorsqu'un client appelle mAllouePiste pour allouer une piste, le booléen FALSE approprié passera à TRUE. Et inversement lors de l'appel à mLiberePiste.