EPOOL - Environnement de Programmation Orientée Objet en Lingo par Irv Kalb

Chapitre précédent

Table des matières

Chapitre suivant

 

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.

Intervalle aléatoire

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.


Table

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()

 

Gestionnaire de Sprite

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.

 

Chapitre précédent

Table des matières

Chapitre suivant