EPOOL - Environnement de Programmation Orientée Objet par Irv Kalb
Chapitre suivant
Gérer les sons nous donne une bonne occasion d'illustrer la programmation orientée objet. Nous verrons comment un gestionnaire de sons peut être un excellent exemple d'encapsulation, puisqu'un seul objet peut être écrit pour gérer tous les aspects du son dans un programme. Lors de la modélisation de la gestion globale du son pour un projet, il y a de nombreux cas à prendre en compte. Un gestionnaire de son peut être utilisé pour s'occuper de tous les détails d'administration des ressources, tandis qu'il fournit une interface claire à ses clients.
Dans ce chapitre nous verrons comment concevoir différents gestionnaires de sons, en fonction de différentes contraintes. Comme nous avancerons dans ce développement, nous verrons comment faire d'un simple objet un objet gestionnaire d'objets - un seul objet qui en administre d'autres.
Un gestionnaire de son simple
Imaginons un projet éducatif qui comporte deux types de sons, par exemple les sons de narration et des effets sonores. Les sons de narration sont de longue durée, et joués à l'arrivée à une certaine "page" définie dans le scénario. Les effets sonores sont de courte durée et joués en réponse à des événements - clic sur un boutton, réponse à la question, etc. La création d'un gestionnaire de son serait un moyen idéal pour s'adapter à ces caractéristiques. Le code pour jouer et contrôler les différents types de sons pourrait être encapsulé dans un objet de gestion du son accessible globalement. Voici le code pour un gestionnaire de son simple qui satisfait ces critères:
-- script GestionnaireSonSimple
property pDelim
property pPisteSonNarration
property pPisteSonEffets
on new me
pDelim = the last char of the
moviePath
-- On utilise la piste 1 pour les sons de narration
-- et la piste 2 pour les effets sonores
pPisteSonNarration = 1
pPisteSonEffets = 2
return me
end
on mLectureSonNarration me,
nomFichierSon
sound stop pPisteSonNarration --
au cas où un son est déjà en cours de lecture
cheminCompletSon = the moviePath
& "Sons" & pDelim & nomFichierSon
sound playFile pPisteSonNarration, cheminCompletSon
end
on mPauseSonNarration me
sound(pPisteSonNarration).pause()
end
on mContinueSonNarration me
sound(pPisteSonNarration).play()
end
on mLectureEffet me, nomSon
sound stop pPisteSonEffets -- au cas où un
son est déjà en cours de lecture
puppetSound pPisteSonEffets, nomSon
end
La démarche est très directe. Pour instancier
un objet global de gestion du son nous faisons (par exemple dans un gestionnaire
prepareMovie):
global
goGestionnaireSonSimple
goGestionnaireSonSimple = new(script
"GestionnaireSonSimple")
A l'instanciation, le gestionnaire de son définit une propriété, pDelim, pour identifier le caractère de délimitation des chemins ('\' ou ':') .On le fait une fois ici, puis on l'utilisera plus tard. Ensuite nous définissons pPisteSonNarration et pPisteSonEffets à 1et 2 pour correspondre aux pistes son 1 et 2. En plaçant ces valeurs à l'intérieur de l'objet, le GestionnaireSonSimple masque les détails de l'implémentation au client. Cette technique est appelée masquage d'information. De l'extérieur de l'objet, aucun client ne sait - ni n'a besoin de savoir - quelles pistes sont utilisées. En fait les clients du gestionnaire de son n'ont même pas besoin de connaître le concept de piste sonore.
|
D'autres langages de programmation ont un type de données appelée constante. L'idée est que vous définissez une valeur qui ne peut varier, mais dont vous pouvez utiliser le nom plutôt que la valeur, puisqu'un nom porte plus d'information qu'une valeur. Dans d'autres langages vous pourriez écrire quelque chose comme: constant cPiseSonNarration = 1 constant cPisteSonEffets = 2 Et vous pourriez utiliser cette constante partout. Mais Lingo ne possède pas de telle constante. Un moyen de le simuler et de faire comme nous avons fait ici. Si vous êtes à l'intérieur d'un objet, vous utilisez une propriété, définie une fois pour toute dans le gestionnaire "new". Une autre méthode est d'utiliser une globale et de l'initialiser dans le gestionnaire prepareMovie. |
Puisque les sons de narration sont longs (ie, sont des fichiers de taille importante), nous avons choisi un modèle où les sons de narration sont des fichiers externes. De plus, pour avoir une structure claire, nous plaçons tous les sons de narration dans un répertoire nommé "Sons" à côté du programme. Lorsque nous voulons lire un son de narration, nous utilisons "sound play file" et construisons à la volée le chemin complet vers le fichier son. C'est ce qui ce passe dans la méthode mLectureSonNarration. La première ligne arrête la lecture éventuelle d'un son sur cette piste. Ensuite, en utilisant le nomFichierSon passé en argument et le délimiteur que nous avons défini et enregistré plus tôt, nous créons le chemin complet du fichier. Enfin nous lançons la lecture du son. De l'extérieur de l'objet, dès que nous voulons jouer un son de narration il nous suffit d'écrire quelque chose comme:
global goGestionnaireSonSimple
goGestionnaireSonSimple.mLectureSonNarration("unNomDeFichierSon")
Nous avons ensuite défini de simples méthodes de contrôle qui permettent de mettre le son en pause et de le relancer si l'utilisateur le souhaite (regardez les méthodes mPauseSonNarration et mContinueSonNarration qui utilisent des nouvelles fonctions de Director 8). Puisque nous avons sauvegardé la piste utilisée pour les sons de narration, ces méthodes sont triviales.
Les effets sonores sont généralement courts (ie des fichiers de petite taille). Nous avons choisi que les effets sonores étaient des acteurs de l'animation. Lorsque le GestionnaireSonSimple veut lire un effet sonore, il se contente d'un puppetSound pour le jouer. Si vous regardez le code de la méthode mLectureEffet method, vous verrez cette implémentation simple. Quand nous voulons jouer un effet sonore, il nous suffit d'écrire une ligne du style:
global goGestionnaireSonSimple
goGestionnaireSonSimple.mLectureEffet("unNomDActeurSon")
Un gestionnaire de son plus compliqué
Maintenant attaquons nous à des contraintes plus fortes. Supposons que nous voulons être capable de lire un nombe quelconque de sons (toujours internes ou externes) en simultané, sans avoir à se soucier de la gestion des pistes audio. Nous pouvons écrire un gestionnaire de son qui cherche dynamiquement quelle piste utiliser. Une autre contrainte pourrait être l'ajout de la possibilité de modifier un son pendant sa lecture (stop, fondu in ou out, augmenter ou diminuer le volume, etc.).
Pour implémenter cette variation, le gestionnaire son générera un indentifiant unique pour un son - que nous appellerons soundID - à chaque requête de lecture de son. Lorsque le gestionnaire de son veut lire un son, il trouve une piste non utilisée, génére un soundID unique, lance le son approprié, et stocke le soundID dans une liste les regroupant tous. Par exemple si nous voulons utiliser 4 pistes, nous commencerons par créer une liste (plSounsIDs) de 4 éléments. Nous initialiserons toutes ses valeurs à zéro, chaque zéro indiquant qu'aucun son n'est joué dans la piste correspondante. A l'arrivée de la première requête de lecture d'un son, nous incrémenterons une propriété (pSoundIDCourant) pour trouver un identifiant unique, nous chercherons une piste libre et lancerons la lecture du son sur cette piste. Enfin nous stockerons le nouvel identifiant soundID dans l'enregistrement approprié de la liste plSoundIDs.
Cette version du gestionnaire de son permet aussi n'importe quel nombre de méthodes "d'action". Chaque méthode représente une action qui peut être effectuée sur un son en cours de lecture. Dans le code ci-après, nous n'implémenterons que la méthode mStopSon. L'identifiant soundID créé par le gestionnaire de son à chaque requête de lecture d'un son est unique - l'identifiant permet de retrouver le son de manière certaine. Le concept est le même que celui que nous avons vu avec les références d'objet en Lingo. Lorsque vous créez un objet dans Director, Director vous retourne une référence vers cet objet. Lorsque vous appellez les méthodes d'un objet, vous avez besoin d'identifier grâce à la référence d'objet l'instance sur laquelle vous appliquez ces méthodes. Dans ce gestionnaire de son, à chaque fois que vous voulez effectuer une action sur un son, vous appellez la méthode appropriée du gestionnaire de son en lui passant l'identifiant soundID en paramètre.
Utilisons une analogie. Supposons que vous achetez un article sur internet et que le vendeur décide de vous le livrer par UPS. Lorsque le vendeur donne le colis à UPS, UPS créé un numéro de suivi et le donne au vendeur, qui peut vous le transmettre. Une fois que vous avez ce numéro de suivi, à chaque fois que vous voulez connaître une propriété du colis (sans doute souhaitez vous savoir où il se trouve) vous devez contactez UPS et leur donner votre numéro de suivi. Le numéro de suivi est l'identifiant qui vous est nécessaire pour effectuer n'importe quelle opération sur le colis. Si vous souhaitez modifier l'adresse de livraison ou définir un horaire, vous avez besoin de ce numéro. Ce numéro de suivi est dans un format interne généré par UPS (lors d'un récent achat j'ai eu un numéro qui ressemblait à: 1ZV44V58034409782). Cette valeur a sans doute un sens pour UPS puisqu'elle leur permet d'identifier de manière unique votre colis. Mais en tant que numéro hors du système de suivi d'UPS, il ne contient aucune information. De la même manière, l'identifiant soudID généré par le gestionnaire de son a un sens à l'intérieur de l'objet, mais pour un client (a l'extérieur du gestionnaire) Il ne contient aucune information.
Le code commence à se compliquer, mais accompagné de commentaire et d'explications vous devriez l'assimiler facilement:
-- GestionnaireSonDynamique
property pDelim -- délimiteur
de chemin
property pnPistes -- nombre de pistes
property pSoundIDCourant -- identifiant
soundID courant
property plSoundIDs -- liste des soundIDs par piste
on new me, nPistes
pnPistes = nPistes
pDelim = the last char
of the moviePath
plSoundIDs = []
repeat with ceNumeroDePiste = 1 to pnPistes
append(plSoundIDs, 0)
end repeat
pSoundIDCourant = 0
return me
end new
on mLectureSonExterne me, nomSon
soundID = me.mLectureSon(nomSon, #externe)
return soundID
end mLectureSonExterne
on mLectureSonInterne me, nomSon
soundID = me.mLectureSon(nomSon, #interne)
return soundID
end mLectureSonInterne
on mLectureSon me, nomSon, symInterneOuExterne
numeroPiste = me.mAffectePiste()
pSoundIDCourant = pSoundIDCourant + 1 -- crée un
soundID unique à chaque lecture de son
plSoundIDs[numeroPiste] = pSoundIDCourant
if symInterneOuExterne = #externe
then
cheminCompletSon = the moviePath
& "Sons" & pDelim & nomSon
sound playFile numeroPiste,
cheminCompletSon
else
puppetSound numeroPiste,
nomSon
end if
return pSoundIDCourant --
retourne l'identifiant au client
end
on mAffectePiste me
-- Vérifie si tous les sons en cours de lecture sont toujours
en train d'être lus
-- Si la lecture est terminée on remet
le soundID à zéro.
repeat with ceNumeroPiste = 1 to pnPistes
if plSoundIDs[ceNumeroPiste] > 0 then
if not(soundBusy(ceNumeroPiste)) then
plSoundIDs[ceNumeroPiste] = 0
end if
end if
end repeat
-- Maintenant on cherche une piste libre
quellePiste = getOne(plSoundIDs, 0)
if quellePiste > 0 then -- on en a trouvé
une, on la retourne
return quellePiste
end if
-- Toutes les pistes sont occupées
-- On cherche la piste avec le plus vieux son
(le plus petit soundID)
-- On arrête ce son pour libérer la piste
-- on redéfinit ce soundID et on retourne
le numéro de piste
idDoyen = min(plSoundIDs)
pisteAvecIdDoyen = getOne(plSoundIDs, idDoyen)
sound stop pisteAvecIdDoyen
plSoundIDs[pisteAvecIdDoyen]
= 0
return pisteAvecIdDoyen
end mAffectePiste
on mArretSon me, soundID
numeroPiste = me.mTrouvePisteDuSoundID(soundID)
if numeroPiste
= 0 then -- piste
ou soundID non valide
return
end if
sound stop numeroPiste
end mArretSon
on mTrouvePisteDuSoundID me,
soundID
if soundID = 0 then
return 0
end if
-- Cherche l'identifiant. Si aucun trouvé,
la valeur retournée sera 0.
quellePiste = getOne(plSoundIDs, soundID)
return quellePiste
end mTrouvePisteDuSoundID
Dans la méthode "new" nous faisons l'initialisation des propriétés. Nous sauvegardons le nombre de pistes dans une propriété, stockons le délimiteur de chemin, et initialisons la liste des soundIDs et la valeur du soundID courant à zéro.
mLectureSonExterne et mLectureSonInterne sont les deux méthodes principales pour le client lorsqu'il veut lire un son. Ces deux méthodes appellent la méthode privée mLectureSon (qui n'est pas sensée être appelée de l'extérieur de l'objet). Après l'exécution de mLectureSon, ces deux méthodes principales retournent au client un soundID unique pour le son en réponse à leur requête. Si le client veut plus tard agir sur ce son, il devra utiliser ce soundID.
La méthode mLectureSon appelle mAffectePiste pour récupérer une piste dans laquelle lire le son. Ensuite elle incrémente pSoundIDCourant pour créer un soundID unique pour le son courant, et le sauvegarde à l'indice approprié dans la liste plSoundIDs. En fonction de l'origine du son (interne ou externe) nous exécutons playFile ou puppetSound pour lire le son. Enfin nous retournons l'identifiant soundID à l'appelant.
La méthode mAffectePiste choisit dynamiquement la piste. Voici la logique utilisée dans ce choix. D'abord nous parcourons la liste plSoundIDs, et pour toutes les pistes en train de lire un son (la valeur est supérieure à zéro) nous vérifions si la piste est encore utilisée. Si la lecture sur cette piste est terminée, nous réinitialisons à zéro cette piste dans la liste plSoundIDs. Ensuite nous cherchons s'il reste une piste libre pour lire le nouveau son - si oui nous retournons son numéro de piste. Si aucune piste libre n'est trouvée, nous devons décider autoritairement laquelle couper. Nous décidons que si toutes les pistes sont occupées, nous coupons le "plus vieux" son en train d'être joué. Pour trouver ce son il suffit de chercher le plus vieux soundID, c'est à dire le plus petit. Une fois trouvé, nous arrêtons la lecture de ce son, marquons la piste comme libre et retournons son numéro. Puisque ce choix est implémenté une seule fois à cet endroit (encapsulé dans une méthode du gestionnaire de son), si pour une raison quelconque nous décidons plus tard de changer cette règle, il nous suffit de modifier cette seule méthode pour propager la mise à jour à l'ensemble du programme. Il n'y a aucun changement à faire dans les appels des clients aux méthodes du gestionnaire de son.
Comme nous l'avons annoncé, nous pouvons ajouter autant de méthodes d'action pour modifier le son en train d'être lu. Pour illustrer ces méthodes par un exemple nous avons déjà codé mArretSon. Cette méthode commence par appeller mTrouvePisteDuSoundID pour connaître le numéro de la piste dans laquelle est joué le son correspondant à l'identifiant soundID. Ensuite il arrête ce son. D'autres méthodes "d'action" telle que pause, continue, restart, augmenter le volume, le diminuer, fondu in ou out, etc. peuvent facilement être ajoutées à cette architecture en ajoutant simplement des méthodes dans le gestionnaire de son. Le client appellerait alors la méthode d'action appropriée en lui passant le soundID en paramètre.
Comme son nom l'indique, mTrouvePisteDuSoundID prend comme paramètre un identifiant soundID et retourne le numéro de la piste dans laquelle est joué le son correspondant à cet identifiant. C'est une méthode privée qui peut être appelée par toutes les méthodes d'action. Il parcoure la liste plSoundIDs à la recherche du soundID passé en paramètre. Si cet identifiant n'est pas trouvé (eg, si la lecture du son est terminée), la méthode retourne zéro.
Cette version du gestionnaire de son fait aussi du masquage d'information. Un client du gestionnaire de son (c'est à dire n'importe quelle ligne du programme qui souhaite jouer un son) se contente de demander pour jouer tel son, sans savoir ni avoir à savoir quelle piste est utilisée. De plus, l'identifiant soundID qui permet de dialoguer avec le gestionnaire de son est dans un format interne à notre objet, et seulement compréhensible par lui. Puisque le soundId est retourné au client, celui-ci peut regarder sa valeur, mais elle n'a aucune signification hors de l'objet gestionnaire de son. Si après s'être adapté à d'autres contraintes, nous décidons de changer l'algorithme de génération de l'identifiant soundID, le code des clients resterait inchangé.
Repensons l'architecture du gestionnaire de son
Avec l'ajout de nouvelles fonctions au gestionnaire de son, nous pouvons penser que notre objet se complexifie. Du point de vue de l'exécution, grouper tout ce dont nous avons besoin dans un seul script est peut-être plus efficace. Cependant, d'un point de vue humain, avoir beaucoup de code dans un seul script peut commencer à brouiller notre vision de la chose. Arrivé à ce point, nous pouvons avoir envie de modifier légèrement l'architecture de notre objet pour séparer le code en plusieurs couches. C'est une approche évolutive.
Si nous prêtons attention à ce que fait le gestionnaire de son, nous nous apercevons qu'il exécute deux fonctions distinctes ; 1) attribuer les pistes, 2) gérer les opérations sur chaque piste (eg, lancer la lecture du son, l'arrêter, faire un fondu, etc.). Nous pouvons utiliser cette distinction des fonctions pour séparer les fonctionnalités et déplacer certaines portions de code dans un nouvel objet. Le gestionnaire de son pourrait continuer à attribuer les pistes, mais il pourrait aussi instancier un objet PisteSon pour chaque piste utilisée. Ainsi chaque objet PisteSon gérerait tous les détails du son pour sa propre piste. C'est la même chose que la délégation de responsablilités dans votre environement de travail. Au travail, le rôle du chef d'équipe est de prendre les décisions qui affectent tous les membres de l'équipe. Ici le travail du gestionnaire de son sera d'assigner les pistes, mais il transférera les requêtes pour agir sur les sons à l'objet pisteSon de son choix.
Puisque nous suivons une approche orientée objet, les clients utiliseront toujours les même méthodes que dans notre gestionnaire de son dynamique précédent. Encore une fois, les clients ne connaissent pas et n'ont pas besoin de connaître les détails de l'implémentation de notre gestionnaire de son, ils veulent juste lire des sons et agir dessus. Le fait que notre gestionnaire utilise d'autres objets ne concerne pas le client. Voici encore un exemple du monde réel. Si vous appellez quelqu'un pour réparer votre maison. S'il s'agit d'une très petite compagnie, la personne qui vous répond au téléphone sera peut-ête celle qui viendra effectuer la réparation. Mais lorsque l'enreprise grandira, il est plus probable que la personne qui vous répondra au téléphone attribuera la réparation à quelqu'un d'autre (un technicien spécialisé).
Dans notre nouveau gestionnaire de son éclaté, supposez qu'on veuille lire deux sons A et B, puis arrêter plus tard le son A. Nous appellerons le gestionnaire de son pour démarrer les deux sons, et il nous retournera deux identifiants soundID uniques. Cependant en interne, le gestionnaire de son attribue ces deux sons à deux instances différentes d'objet PisteSon. Puis plus tard nous appellons le gestionnaire pour lui demander de stopper le son A (en lui passant le soundID du son A) et le son s'arrête. A l'intérieur du gestionnaire de son, celui-ci cherche à quelle piste correspond le soundID passé, et transfère la requête à l'objet PisteSon qui gère cette piste. Voici un squelette de ce type de gestionnaire de son. Remarquez que certaines actions et propriétés ont été déplacées de l'objet gestionnaire de son vers l'objet PisteSon.
-- GestionnaireSonEclate
property pnPistes -- nombre de pistes
property pSoundIDCourant -- identifiant soundID courant
property ploPisteSon -- liste
des objets PisteSon
on new me, nPistes
pnPistes = nPistes
theDelim = the last char of the moviePath
ploPisteSon = []
repeat with ceNumeroPiste = 1 to pnPistes
oPisteSon = new(script "PisteSon", theDelim, ceNumeroPiste)
append(ploPisteSon, oPisteSon)
end repeat
pSoundIDCourant = 0
return me
end new
on mLectureSonExterne me, nomSon
soundID = me.imLectureSon(nomSon, #externe)
return soundID
end mLectureSonExterne
on mLectureSonInterne me, nomSon
soundID = me.imLectureSon(nomSon, #interne)
return soundID
end mLectureSonInterne
-- Le nom "im" LectureSon précise qu'il s'agit
d'une méthode
-- interne de l'objet GestionnaireSonEclate. C'est à
dire que
-- cette méthode ne doit être appelée
que de l'intérieur de l'objet
on imLectureSon me, nomSon, symInterneOuExterne
numeroPiste = me.mAffectePiste()
pSoundIDCourant = pSoundIDCourant + 1
-- crée un identifiant unique soundID pour chaque nouveau
son
-- Cherche l'objet PisteSon approprié
-- et lui demande de démarrer la lecture
du son.
oPisteSon = ploPisteSon[numeroPiste]
oPisteSon.mLectureSon(nomSon, symInterneOuExterne, pSoundIDCourant)
return pSoundIDCourant -- retourne l'identifiant
au client
end
on mAffectePiste me
-- Vérifie si tous les sons en cours de lecture sont toujours
en train d'être lus
-- Si la lecture est terminée on remet
le soundID à zéro.
repeat with ceNumeroPiste = 1 to pnPistes
oPisteSon = ploPisteSon[ceNumeroPiste]
ceSoundID = oPisteSon.mGetSoundID()
if ceSoundID > 0 then
if not(soundBusy(ceNumeroPiste)) then
oPisteSon.mLibereSoundID()
end if
end if
end repeat
--
Maintenant on cherche une piste libre
repeat with ceNumeroPiste = 1 to pnPistes
oPisteSon = ploPisteSon[ceNumeroPiste]
soundIDDeLaPiste = oPisteSon.mGetSoundID()
if soundIDDeLaPiste = 0
then --
on en a trouvé une, on la retourne
return ceNumeroPiste
end if
end repeat
--
Toutes les pistes sont occupées
-- On cherche la piste avec le plus vieux son
(le plus petit soundID)
-- On arrête ce son pour libérer la piste
-- on redéfinit ce soundID et on retourne
le numéro de piste
idDoyen = the maxInteger -- initialisation
de la variable
repeat with ceNumeroPiste = 1 to pnPistes
oPisteSon = ploPisteSon[ceNumeroPiste]
soundIDDeLaPiste = oPisteSon.mGetSoundID()
if soundIDDeLaPiste < idDoyen then -- on en a trouvé
un plus ancien
idDoyen = soundIDDeLaPiste
pisteAvecIdDoyen = ceNumeroPiste
end if
end repeat
-- on arrête la lecture du son sur la
piste "pisteAvecIdDoyen"
-- pour que le nouveau son puisse être
lu
oPisteSon = ploPisteSon[pisteAvecIdDoyen]
oPisteSon.mArretSon()
return pisteAvecIdDoyen
end mAffectePiste
on mArretSon me, soundID
numeroPiste = me.mTrouvePisteDuSoundID(soundID)
if numeroPiste = 0 then -- piste
non valide
return
end if
oPisteSon = ploPisteSon[numeroPiste]
oPisteSon.mArretSon()
end mArretSon
on mTrouvePisteDuSoundID me,
soundID
if soundID = 0 then
return 0
end if
-- demande à chaque piste son soundID
courant
-- si nous trouvons la valeur demandée,
on retourne ce numéro de piste
repeat with ceNumeroPiste = 1 to pnPistes
oPisteSon = ploPisteSon[ceNumeroPiste]
soundIDDeLaPiste = oPisteSon.mGetSoundID
if soundID = soundIDDeLaPiste then -- trouvé!
return ceNumeroPiste
end if
end repeat
-- pas trouvé, on retourne 0
return 0
end mTrouvePisteDuSoundID
on mLibere me
repeat with
ceNumeroPiste = 1 to pnPistes
oPisteSon = ploPisteSon[ceNumeroPiste]
oPisteSon.mLibere()
ploPisteSon[ceNumeroPiste] = VOID
end repeat
end mLibere
Voici le code du script parent PisteSon. Dans sa méthode
"new", le gestionnaire instancie un objet PisteSon pour chaque piste
son réelle que le programme veut pouvoir gérer.
-- PisteSon
property pDelim -- delimiteur de chemin
property pSoundID -- identifiant sound ID courant
property pNumeroPiste -- numéro
de la piste son gérée par cet objet
on new me, theDelim, numeroPiste
pDelim =
theDelim
pNumeroPiste = numeroPiste
pSoundID = 0
return me
end new
on mGetSoundID me
return
pSoundID
end
on
mLibereSoundID me
pSoundID = 0
end
on mLectureSon me, nomSon,
symInterneOuExterne, soundID
pSoundID = soundID
if symInterneOuExterne = #externe then
cheminCompletSon = the moviePath
& "Sons" & pDelim & nomSon
sound playFile pNumeroPiste, cheminCompletSon
else
puppetSound pNumeroPiste, nomSon
end if
end
on mArretSon me
sound
stop pNumeroPiste
pSoundID = 0
end mArretSon
Mainetant que nous avons créé cette structure,
il est facile d'ajouter des fonctionnalités. Un exemple serait de faire
retenir facilement à chaque objet PisteSon le nom du son actuellement
joué si bien qu'on pourrait ajouter des méthodes pour retrouver
le nom du son lu. Un autre exemple serait l'ajout d'un système de file
d'attente à chaque objet PisteSon, ainsi lorsqu'un son est terminé,
un autre son démarre aussitôt. Juste pour illustrer le cas par
du code, supposons que nous voulions ajouter "faire un fondu out du son".
Nous utiliserons la méthode mArretSon du gestionnaire de son et de l'objet
PisteSon comme prototypes. Dans le gestionnaire de son, nous devrions ajouter
la méthode suivante:
on mFonduOut
me, soundID
numeroPiste = me.mTrouvePisteDuSoundID(soundID)
if numeroPiste = 0 then -- piste
non valide
return
end if
oPisteSon = ploPisteSon[numeroPiste]
oPisteSon.mFonduOut()
end mFonduOut
Et dans le script de l'objet PisteSon, nous ajoutons:
on mFonduOut me
sound fadeOut pNumeroPiste
end mFonduOut
Soyons propres
Dans ce chapitre nous avons vu comment un objet simple peut être transformé en un objet gestionnaie d'objets pour créer et transmettre des traitements à des objets subordonnés. Dans notre introduction aux objets, nous avons vu comment, lors de l'instanciation d'un objet, Director alloue de la mémoire pour les propriétés de l'objet. Nous avons aussi vu que lorsque nous n'avons plus besoin d'un objet, nous devons le libérer (ie, libérer la mémoire utilisée par l'objet et remettre la référence vers l'objet à VOID). Si nous ne le faisons pas, la mémoire allouée à un objet ne peut jamais être récupérée par Director. Et si nous perdons ainsi de la mémoire de façon répétée, nous pouvons épuiser la mémoire disponible et planter le programme.
Mais un objet gestionnaire d'objet est un cas particulier. Nous devons être très prudents lorsque nous voulons libérer un objet gestionnaire d'objet ou arrêter un programme qui contient de tels objets. Si un objet instancie un ou plusieurs objets (se transformant ainsi en objet gestionnaire d'objets), alors lorsqu'il doit être détruit, il a la responsabilité de libérer le ou les objets qu'il a instancié.
En Lingo, lorsque vous créez un objet, vous appellez explicitement la méthode "new" du script parent. Dans d'autres langages de programmation cette méthode est appelée constructeur puisqu'elle vous permet de construire un nouvel objet. Et dans d'autres langages que Lingo il existe une méthode générique pour se débarrasser d'un objet, appelée destructeur.
Pour combler ce défaut de Director, nous pouvons adopter une convention. Cette convention est que dans chaque script parent que vous écrivez vous incluez une méthode de destruction utilisant toujours un nom générique. Si nous respectons cette convention, alors nous avons une structure qui permet aux objets de libérer la mémoire correctement. Pour être plus précis, chaque parent doit inclure une méthode nommée "mLibere" (n'importe quel autre nom irait aussi bien, le plus important est de suivre un consensus). mLibere est un nom simple qui définit bien le fonction de la méthode. Dans le code de mLibere nous écrivons le code nécessaire à la libération de tous les objets que nous avons instancié, et n'importe quelle autre instruction que nous souhaitons exécuter avant la disparition de l'objet.
Voici un exemple basé sur le gestionnaire de son "éclaté". Habituellement nous instancierons un objet accessible globalement comme notre gestionnaire de son dans un gestionnaire prepareMovie ou startMovie comme ceci :
global goGestionnaireSon
on prepareMovie
goGestionnaireSon = new(script "GestionnaireSonEclate", 4)
end
Dans notre gestionnaire stopMovie, nous devons inclure
le code pour libérer tous les objets de portée globale comme celui
que nous avons créé. Comme nous l'avons déjà dit,
se contenter de mettre la variable à VOID libérera la mémoire
allouée à cet objet. Mais si par exemple nous mettons goGestionnaireSon
à VOID, nous ne donnons aucune chance au gestionnaire de son de "se
nettoyer". Le résultat sera la libération effective de l'objet
gestionnaire de son, mais tous les objets PisteSon créés par cet
objet ne le seront pas et la mémoire qu'ils occupent sera perdue. Donc
juste avant d'attribuer VOID à goGestionnaireSon, nous appellons la méthode
mLibere de cet objet gestionnaire de son. Voici le code:
on stopMovie
goGestionnaireSon.mLibere()
goGestionnaireSon =
VOID
end
Ainsi la méthode mLibere du gestionnaire de son lui donne l'opportunité de nettoyer son monde. Son monde est constitué d'un certain nombre d'instances d'objets PisteSon. Dans le code de la méthode mLibere du gestionnaire de son, nous appellons la méthode mLibere de chaque objet PisteSon puis définissons la variable appropriée àVOID. Voici comment nous faisons:
on mLibere me
repeat with ceNumeroPiste = 1 to pnPistes
oPisteSon =
ploPisteSon[ceNumeroPiste]
oPisteSon.mLibere()
ploPisteSon[ceNumeroPiste] = VOID
end repeat
end
mLibere
En respectant cette convention, nous devons aussi ajouter la méthode
mLibere à l'objet PisteSon. Si un script parent n'a pas de nettoyage
particulier à faire dans sa méthode mLibere, nous pouvons nous
limiter à la méthode "dégénérée"
qui n'exécute rien. Ou pour être plus explicite, nous pouvons y
mettre une commande "nothing" comme ci dessous:
on mLibere me
nothing
end mLibere
Si l'objet PisteSon avait aussi instancié d'autres objets, il devrait appeller mLibere sur ces instances puis mettre leurs références à VOID.
Dans le cas de notre gestionnaire d'objets, voici la séquence complète des évenements. Dans le gestionnaire stopMovie nous appellons la méthode mLibere du gestionnaire de son. Celui-ci sait qu'il a créé des instances d'objets PisteSon, et qu'il doit donc appeller la méthode mLibere sur chacune de ces instances. la méthode de chaque objet PisteSon est executée mais ne fait rien. Au retour des fonctions mLibere des objets PisteSon, le gestionnaire de son définit la référence d'objet pour chacune de ces instances à VOID, libérant ainsi l'objet et la mémoire. Lorsque mLibere du gestionnaire de son a fini son exécution, nous retournons dans notre gestionnaire stopMovie pour définir la variable globale goGestionnaireSon à VOID, libérant ainsi l'objet gestionnaire de son.
Les clés du processus de libération sont ; 1) une
convention qui définit un nom commun pour le méthode de destruction,
et 2) dans cette méthode de destruction, chaque objet appelle la méthode
de destruction des objets qu'il a créé, puis définit ses
variables de référence d'objet à VOID. Un appel à
la méthode mLibere d'un objet gestionnaire d'objets lance une séquence
pseud-récursive d'appels à la méthode mLibere pour s'assurer
que chaque objet peut nettoyer son monde. Dans l'exemple du gestionnaire de
son, la hiérarchie n'est profonde que de deux niveaux. Cependant à
tous les niveaux chaque objet gestionnaire d'objets étant responsable
de la libération des objets qu'il a instanciés, une profondeur
quelconque de la hiérarchie peut être nettoyée en suivant
cette approche.
De plus, puisque vous savez que la méthode mLibere est
un destructeur, vous savez qu'elle sera la dernière méthode appelée
et exécutée de votre objet avant sa disparition, si vous développez
un objet de suivi du parcours de l'utilisateur à travers un programme,
vous pouvez avoir envie de placer le code qui sauvegarde l'état de progression
de l'utilisateur dans un fichier à cet endroit. Un autre exemple peut
être un objet de communication. Sachant lors de l'exécution de
sa méthode mLibere qu'il va disparaître, nous pouvons y inclure
du code pour fermer le canal de communication.
Chapitre suivant