789 lines
28 KiB
Text
789 lines
28 KiB
Text
[#_lesson_6_lists_of_things]
|
||
== Leçon 6 : Des listes de choses
|
||
|
||
image::learn/06-tables.png[La leçon terminée, 480]
|
||
|
||
Vous avez cent vingt étoiles et pas une seule n'existe. Il n'y a d'étoile nulle
|
||
part dans votre programme : il y a une boucle qui tourne cent vingt fois, et à
|
||
chaque tour elle calcule deux nombres à partir du numéro de l'étoile et de sa
|
||
couche, allume un pixel, et oublie tout. Les trois couches sont là pour simuler
|
||
la seule chose que cet arrangement ne sait pas faire, c'est-à-dire laisser une
|
||
étoile être différente de sa voisine.
|
||
|
||
Cette leçon vous donne ce qui manque. Vous allez apprendre à garder une liste
|
||
d'éléments, à fabriquer un élément qui contient plusieurs valeurs nommées à la
|
||
fois, puis à réunir les deux en une liste d'éléments qui se souviennent chacun
|
||
de leur position, de leur vitesse et de leur luminosité. C'est la plus longue
|
||
leçon de cette partie du livre, et celle sur laquelle s'appuie tout ce qui suit.
|
||
Prenez votre temps.
|
||
|
||
[#_start_a_new_folder_5]
|
||
=== Créez un nouveau dossier
|
||
|
||
Un dossier appelé `tables`, dedans un fichier appelé `tables.singe`, et
|
||
`Singe -R tables` qui tourne dans un terminal. Copiez-y le programme terminé de
|
||
la leçon cinq : le champ d'étoiles, le vaisseau, et tout le reste.
|
||
|
||
[#_a_list]
|
||
=== Une liste
|
||
|
||
Ajoutez cette ligne en haut de `tables.singe`, sous les autres valeurs, et les
|
||
trois appels à `overlayPrint` dans `onOverlayUpdate`, au-dessus du `return`.
|
||
|
||
[source,lua]
|
||
----
|
||
local names = { "Ada", "Grace", "Alan" }
|
||
----
|
||
|
||
[source,lua]
|
||
----
|
||
overlayPrint(2, 2, names[1])
|
||
overlayPrint(2, 3, names[3])
|
||
overlayPrint(2, 4, "There are " .. #names .. " names.")
|
||
----
|
||
|
||
Lancez-le. `Ada` sur une ligne, `Alan` sur la suivante, et `There are 3 names.`
|
||
en dessous.
|
||
|
||
Les accolades de la première ligne fabriquent une *table*. Une table est le seul
|
||
conteneur qu'ait Lua, et tout ce dont vous voudrez un jour garder plus d'un
|
||
exemplaire va dans une table. Ici, elle contient trois chaînes à la suite, ce
|
||
qui est la forme qu'on appelle une *liste*.
|
||
|
||
Les crochets des lignes suivantes en sortent un élément. `names[1]` est le
|
||
premier élément, `names[3]` le troisième, et le nombre entre les crochets
|
||
s'appelle l'*indice*.
|
||
|
||
Ce nombre, c'est là que Lua se sépare de presque tous les autres langages que
|
||
vous rencontrerez. *Lua compte à partir de un.* C, Java, Python, JavaScript et
|
||
la plupart des autres comptent à partir de zéro, si bien que leur premier
|
||
élément est l'élément zéro et que leur troisième élément est à l'indice deux. Le
|
||
premier élément de Lua est l'élément un, et c'est la seule chose qui fait
|
||
trébucher les programmeurs venus d'ailleurs. Vous ne venez de nulle part, alors
|
||
votre tâche est plus facile : le premier est le numéro un.
|
||
|
||
`names[0]` n'est pas une erreur, au passage. C'est `nil` -- la valeur « rien »
|
||
de la leçon trois -- parce que rien n'y est rangé. Lire un indice qui n'a jamais
|
||
été rempli donne toujours `nil`, ce qui est bien aimable jusqu'au moment où vous
|
||
essayez de vous en servir.
|
||
|
||
Le `#` devant le nom d'une table est la dernière ponctuation nouvelle avant un
|
||
bon moment. Il vous donne le nombre d'éléments de la liste, donc `#names` vaut
|
||
`3`. Comme le nombre d'éléments et le dernier indice sont le même nombre,
|
||
`names[#names]` est toujours le dernier élément, quelle que soit la longueur de
|
||
la liste.
|
||
|
||
[#_adding_and_taking_away]
|
||
=== Ajouter et retirer
|
||
|
||
Une liste qu'il faut écrire en entier ne sert pas à grand-chose dans un jeu. Ces
|
||
deux appels sont la manière dont une liste change pendant que le programme
|
||
tourne.
|
||
|
||
[source,lua]
|
||
----
|
||
table.insert(names, "Katherine")
|
||
----
|
||
|
||
Ça met `Katherine` au bout, et `#names` vaut maintenant `4`. `table.insert` est
|
||
une fonction comme une autre, et le point au milieu de son nom veut dire qu'elle
|
||
vit dans une table appelée `table` que Lua fournit -- une table de fonctions
|
||
pour travailler sur les tables. Vous n'avez pas à la créer ni à la charger. Elle
|
||
est là.
|
||
|
||
Donnez trois arguments à `table.insert` et celui du milieu dit où mettre le
|
||
nouvel élément.
|
||
|
||
[source,lua]
|
||
----
|
||
table.insert(names, 1, "Edsger")
|
||
----
|
||
|
||
`Edsger` entre en tête, et tout ce qui était déjà là se décale pour lui faire de
|
||
la place : `Ada` était l'élément un et devient l'élément deux. Rien n'est écrasé
|
||
et rien n'est perdu.
|
||
|
||
Retirer un élément, c'est l'image inverse.
|
||
|
||
[source,lua]
|
||
----
|
||
table.remove(names, 2)
|
||
----
|
||
|
||
L'élément deux s'en va, et tout ce qui le suivait recule d'un cran pour combler
|
||
le vide, si bien que la liste n'a jamais de trou au milieu. `table.remove` vous
|
||
rend aussi l'élément qu'il a retiré, donc `local gone = table.remove(names, 2)`
|
||
s'en débarrasse et vous dit ce que c'était en une seule ligne. Ne mettez pas de
|
||
nombre du tout -- `table.remove(names)` -- et il prend le dernier élément, ce
|
||
qui est le moyen le plus rapide de se servir d'une liste comme d'une pile.
|
||
|
||
Mettez ces trois appels dans votre script, un à la fois, lancez-le après chacun,
|
||
et regardez changer le nombre sur la troisième ligne.
|
||
|
||
[#_walking_a_list]
|
||
=== Parcourir une liste
|
||
|
||
Vous connaissez déjà une manière de visiter chaque élément, parce que vous vous
|
||
en servez depuis la leçon quatre :
|
||
|
||
[source,lua]
|
||
----
|
||
for i = 1, #names do
|
||
overlayPrint(2, i + 1, names[i])
|
||
end
|
||
----
|
||
|
||
Ça marche. Commencer à un, s'arrêter au nombre d'éléments, se servir du compteur
|
||
comme indice. Il y a une meilleure manière, et c'est celle que vous écrirez
|
||
désormais :
|
||
|
||
[source,lua]
|
||
----
|
||
for i, name in ipairs(names) do
|
||
overlayPrint(2, i + 1, name)
|
||
end
|
||
----
|
||
|
||
`ipairs` parcourt une liste depuis l'élément un jusqu'à ce qu'il n'y en ait
|
||
plus, et donne deux choses à votre boucle à chaque tour : l'indice et l'élément
|
||
lui-même. C'est le deuxième qui compte. Avec le `for` numérique, vous obtenez un
|
||
nombre et vous devez retourner à la table avec `names[i]` pour savoir ce que
|
||
c'est ; avec `ipairs`, l'élément est déjà dans votre main, sous le nom que vous
|
||
avez mis en second.
|
||
|
||
C'est aussi plus difficile de se tromper. `for i = 1, #names` est une belle
|
||
occasion de se tromper d'une unité -- commencez à zéro et vous affichez un
|
||
`nil`, arrêtez-vous à `#names - 1` et vous perdez le dernier -- et `ipairs` ne
|
||
contient aucun nombre sur lequel se tromper.
|
||
|
||
La plupart du temps, vous ne voulez pas l'indice, seulement l'élément. Écrivez
|
||
l'indice sous la forme d'un simple tiret bas, et n'en parlons plus :
|
||
|
||
[source,lua]
|
||
----
|
||
for _, name in ipairs(names) do
|
||
----
|
||
|
||
`_` est un nom parfaitement ordinaire, et rien dans Lua ne le traite à part.
|
||
C'est une habitude que partagent les programmeurs pour dire « quelque chose
|
||
arrive ici et je ne vais pas m'en servir ». Quiconque lit votre boucle sait au
|
||
premier coup d'œil que l'indice n'y compte pas.
|
||
|
||
Supprimez maintenant vos essais avec `names`, et les lignes `overlayPrint` avec.
|
||
Vous savez ce qu'est une liste. Il est temps d'y ranger quelque chose de mieux
|
||
qu'une chaîne.
|
||
|
||
[#_a_table_can_also_be_a_record]
|
||
=== Une table peut aussi être un enregistrement
|
||
|
||
Une étoile doit se souvenir de quatre choses : où elle se trouve de gauche à
|
||
droite, où elle se trouve de haut en bas, à quelle vitesse elle tombe, et à quel
|
||
point elle brille. Quatre listes séparées, toutes tenues à jour ensemble à la
|
||
main, ce serait l'enfer. Fabriquez plutôt une table et mettez-y des noms.
|
||
|
||
[source,lua]
|
||
----
|
||
local star = {}
|
||
star.x = 40
|
||
star.y = 0
|
||
star.speed = 2
|
||
star.shade = 180
|
||
----
|
||
|
||
Les accolades vides fabriquent une table vide. Les quatre lignes suivantes y
|
||
mettent des valeurs sous des noms au lieu de numéros, et le point est la manière
|
||
de dire quel nom vous voulez. `star.x` se lit le `x` de `star`, et ça se
|
||
comporte exactement comme n'importe quelle autre variable : on peut le lire, lui
|
||
donner une valeur, lui ajouter quelque chose.
|
||
|
||
[source,lua]
|
||
----
|
||
star.y = star.y + star.speed
|
||
----
|
||
|
||
Une table utilisée de cette manière -- un ensemble fixe de valeurs nommées qui
|
||
décrivent une seule chose -- est un *enregistrement*, et les noms qu'elle
|
||
contient sont ses *champs*. C'est le même genre de table que la liste
|
||
ci-dessus. La seule différence, c'est qu'on trouve les éléments d'une liste par
|
||
numéro et les champs d'un enregistrement par nom, et qu'une même table peut
|
||
faire les deux à la fois si un jour vous le voulez.
|
||
|
||
Le point est un raccourci. `star.x` et `star["x"]` veulent dire exactement la
|
||
même chose, et ça vaut la peine de le voir une fois : les crochets acceptent un
|
||
nom aussi volontiers qu'un nombre, donc une liste et un enregistrement sont
|
||
vraiment une seule idée sous deux casquettes. Utilisez le point. Ça se lit
|
||
mieux.
|
||
|
||
Un champ que vous n'avez jamais rempli vaut `nil`, comme un indice jamais
|
||
rempli. Il n'existe nulle part de liste des noms de champ autorisés, et rien ne
|
||
vérifie votre orthographe, donc `star.sped` n'est pas une erreur -- c'est `nil`,
|
||
et vous verrez ce qui arrive ensuite à la fin de cette leçon.
|
||
|
||
Écrire quatre lignes pour remplir quatre champs, on s'en lasse. Vous pouvez
|
||
mettre les champs à l'intérieur des accolades quand vous fabriquez la table :
|
||
|
||
[source,lua]
|
||
----
|
||
local star = {
|
||
x = 40,
|
||
y = 0,
|
||
speed = 2,
|
||
shade = 180
|
||
}
|
||
----
|
||
|
||
La même table, en une seule instruction. Les virgules séparent les champs, le
|
||
dernier n'a pas besoin de virgule après lui, et les lignes ne sont étalées que
|
||
pour se lire facilement.
|
||
|
||
[#_a_list_of_records]
|
||
=== Une liste d'enregistrements
|
||
|
||
Voici toute l'idée de cette leçon en deux lignes :
|
||
|
||
[source,lua]
|
||
----
|
||
local stars = {}
|
||
table.insert(stars, star)
|
||
----
|
||
|
||
Une liste peut contenir n'importe quoi, y compris une table. Une liste
|
||
d'enregistrements est donc une liste de choses qui se souviennent chacune de
|
||
tout ce qui les concerne. `stars[1]` est une étoile entière, et `stars[1].y` est
|
||
la distance de cette étoile depuis le haut de l'écran. Presque tous les jeux que
|
||
vous écrirez sont une poignée de listes comme celle-ci : les ennemis, les
|
||
balles, les bonus et les blocs qui tombent.
|
||
|
||
Il y a une chose à savoir sur les tables avant d'écrire le code, et elle piège
|
||
tout le monde une fois. Donner une table à une fonction ne la copie pas. Un
|
||
nombre est copié -- c'est pour ça que `half` pouvait découper son `number` à la
|
||
leçon cinq sans déranger `drift` -- mais une table est partagée. Les deux noms
|
||
désignent la même table, et un changement fait par l'un se voit par l'autre.
|
||
|
||
[source,lua]
|
||
----
|
||
local star = stars[3]
|
||
star.y = star.y + 1
|
||
----
|
||
|
||
Ça déplace la troisième étoile de la liste. Pas une copie d'elle : elle. C'est
|
||
exactement ce que vous voulez ici, et c'est ce qui fait marcher la section
|
||
suivante, mais souvenez-vous-en le jour où une fonction que vous avez écrite
|
||
change quelque chose que vous ne vous attendiez pas à la voir changer.
|
||
|
||
[#_rebuild_the_star_field]
|
||
=== Reconstruisez le champ d'étoiles
|
||
|
||
Dehors, l'arithmétique. Une étoile va devenir une chose.
|
||
|
||
Supprimez `starAt`, `shadeFor`, `drawLayer` et l'ancien `drawStar`, et avec eux
|
||
les constantes qu'ils utilisaient : `LAYERS`, `STARS_PER_LAYER`, `ACROSS_STEP`,
|
||
`DOWN_STEP`, `LAYER_SHIFT` et `LAYER_SPEED`. La variable `drift` s'en va
|
||
aussi, tout comme la ligne de `drift` en haut de `onOverlayUpdate`.
|
||
`LAYER_BRIGHT` reste, sous un nom qui ne parle plus de couches, et deux valeurs
|
||
le rejoignent :
|
||
|
||
[source,lua]
|
||
----
|
||
local STAR_COUNT = 120
|
||
local STAR_SHADE = 55
|
||
local BURST_COUNT = 10
|
||
----
|
||
|
||
[source,lua]
|
||
----
|
||
local stars = {}
|
||
----
|
||
|
||
Cent vingt, c'est ce que vous aviez : trois couches de quarante. La liste
|
||
commence vide, et voici ce qui la remplit :
|
||
|
||
[source,lua]
|
||
----
|
||
local function newStar(y)
|
||
local speed = math.random(1, 3)
|
||
local star = {
|
||
x = math.random(0, width - 1),
|
||
y = y,
|
||
speed = speed,
|
||
shade = math.random(60, 90) + speed * STAR_SHADE
|
||
}
|
||
return star
|
||
end
|
||
----
|
||
|
||
`newStar` fabrique une étoile et vous la rend. Regardez le dernier champ et
|
||
comparez-le avec `shadeFor` à la leçon cinq : le même calcul, avec la vitesse de
|
||
l'étoile là où se trouvait le numéro de couche. Les couches n'ont pas tant
|
||
disparu que fondu. Chaque étoile choisit maintenant sa propre vitesse, un, deux
|
||
ou trois pixels par image, et une étoile qui tombe plus vite est plus brillante,
|
||
ce qui fait toute l'illusion de profondeur et coûte une ligne.
|
||
|
||
Le `y` de départ est un paramètre parce que les deux appelants veulent des
|
||
choses différentes. Les étoiles fabriquées au démarrage du jeu doivent être
|
||
éparpillées sur tout l'écran ; celles fabriquées plus tard arrivent par le haut.
|
||
|
||
[source,lua]
|
||
----
|
||
local function makeStars()
|
||
for number = 1, STAR_COUNT do
|
||
table.insert(stars, newStar(math.random(0, height - 1)))
|
||
end
|
||
end
|
||
----
|
||
|
||
Rien n'appelle encore `makeStars`. Ajoutez une ligne tout en bas du fichier,
|
||
après tout le reste et en dehors de toute fonction :
|
||
|
||
[source,lua]
|
||
----
|
||
makeStars()
|
||
----
|
||
|
||
Il y a plus à dire sur cette ligne, et ça attendra la fin de la leçon. Passons
|
||
au dessin, qui raccourcit chaque fois que vous y touchez :
|
||
|
||
[source,lua]
|
||
----
|
||
local function drawStar(star)
|
||
colorForeground(star.shade, star.shade, star.shade)
|
||
overlayPlot(star.x, star.y)
|
||
end
|
||
|
||
|
||
local function drawStars()
|
||
for _, star in ipairs(stars) do
|
||
drawStar(star)
|
||
end
|
||
end
|
||
----
|
||
|
||
`drawStar` prenait trois nombres à la leçon cinq et prend maintenant une étoile.
|
||
Tout ce qu'elle a besoin de savoir est dans l'étoile, et c'est toute la raison
|
||
d'être des enregistrements : une seule chose à faire passer au lieu de quatre,
|
||
et aucun risque de les donner dans le mauvais ordre. Et `drawStars` a perdu sa
|
||
boucle sur les couches, ainsi que tout intérêt pour le nombre d'étoiles ou leur
|
||
provenance.
|
||
|
||
[#_stars_that_come_and_go]
|
||
=== Des étoiles qui vont et viennent
|
||
|
||
Maintenant, la partie que l'ancien champ d'étoiles ne savait pas faire du tout.
|
||
Les étoiles tombent chacune à sa vitesse, sortent par le bas, et sont remplacées
|
||
en haut.
|
||
|
||
[source,lua]
|
||
----
|
||
local function moveStars()
|
||
for i = #stars, 1, -1 do
|
||
local star = stars[i]
|
||
star.y = star.y + star.speed
|
||
if star.y >= height then
|
||
table.remove(stars, i)
|
||
end
|
||
end
|
||
while #stars < STAR_COUNT do
|
||
table.insert(stars, newStar(0))
|
||
end
|
||
end
|
||
----
|
||
|
||
Appelez-la depuis `onOverlayUpdate`, sur la ligne où se trouvait
|
||
`drift = drift + 1`.
|
||
|
||
Chaque étoile descend de sa propre `speed`, et c'est la ligne que la leçon cinq
|
||
ne pouvait pas écrire. Le `while` du bas, c'est le réapprovisionnement : tant
|
||
qu'il y a moins de cent vingt étoiles, en fabriquer une autre en haut de
|
||
l'écran. Il tourne autant de fois qu'il le faut puis s'arrête, et c'est le bon
|
||
outil quand vous ne savez pas combien il en manque.
|
||
|
||
La ligne `for` du haut compte à rebours, avec le pas que vous avez rencontré à
|
||
la leçon quatre. `for i = #stars, 1, -1` visite le dernier élément en premier et
|
||
remonte jusqu'à l'élément un. Ce n'est pas une fioriture. C'est la seule manière
|
||
sûre de faire ce que fait cette boucle, et la section suivante explique
|
||
pourquoi.
|
||
|
||
[#_never_remove_going_forwards]
|
||
=== Ne retirez jamais en avançant
|
||
|
||
Écrivez la même boucle dans l'autre sens et elle est cassée :
|
||
|
||
[source,lua]
|
||
----
|
||
for i = 1, #stars do
|
||
local star = stars[i]
|
||
star.y = star.y + star.speed
|
||
if star.y >= height then
|
||
table.remove(stars, i)
|
||
end
|
||
end
|
||
----
|
||
|
||
Suivez-la à la main avec quatre étoiles, et supposez que les étoiles deux et
|
||
trois aient toutes les deux atteint le bas.
|
||
|
||
* `i` vaut `1`. L'étoile un va bien.
|
||
* `i` vaut `2`. L'étoile deux est sortie par le bas, alors elle s'en va. Tout
|
||
recule d'un cran : l'étoile qui était la trois est maintenant la deux, et
|
||
celle qui était la quatre est maintenant la trois.
|
||
* `i` vaut `3`. C'est l'étoile qui était la quatre. *L'étoile trois n'a jamais
|
||
été examinée.* Retirer l'élément deux l'a fait reculer dans la case deux, et
|
||
la boucle était déjà passée par la case deux.
|
||
* `i` vaut `4`. Il n'y a plus d'élément quatre. `stars[4]` vaut `nil`.
|
||
|
||
Deux défauts distincts pour une seule petite erreur. Des éléments sont sautés,
|
||
si bien qu'une étoile qui avait atteint le bas n'est même pas examinée à ce
|
||
tour-ci. Une étoile qui survit une image de plus ne fait pas de mal, mais la
|
||
même boucle sur une liste d'ennemis, où c'est l'examen qui les tue, est un bug
|
||
que vous chercheriez pendant une heure. Le second défaut fait plus de bruit. La
|
||
liste est maintenant plus courte que le nombre avec lequel la boucle a commencé,
|
||
parce que `#stars` a été calculé une seule fois, avant tout retrait, alors les
|
||
derniers tours de la boucle vont au-delà de la fin de la liste :
|
||
|
||
----
|
||
Error executing function 'onOverlayUpdate': tables/tables.singe:60: attempt to index a nil value (local 'star')
|
||
stack traceback:
|
||
tables/tables.singe:60: in upvalue 'moveStars'
|
||
tables/tables.singe:124: in function 'onOverlayUpdate'
|
||
----
|
||
|
||
« Attempt to index a nil value » veut dire que vous avez mis un point après
|
||
quelque chose qui ne contenait rien. Le `(local 'star')` à la fin le nomme :
|
||
`star` valait `nil`, donc `star.y` n'avait nulle part où chercher.
|
||
|
||
Aller à rebours corrige les deux défauts à la fois, et ça vaut la peine de voir
|
||
pourquoi plutôt que de le croire sur parole. Quand vous retirez l'élément `i`,
|
||
tout ce qui suit `i` recule d'une case -- et en allant à rebours, tout ce qui
|
||
suit `i`, c'est tout ce que vous avez déjà visité. Ce qui reste à venir, les
|
||
éléments `i - 1` jusqu'à `1`, n'a pas bougé du tout. La liste qui rétrécit
|
||
derrière vous ne peut pas vous faire de mal non plus, parce que vous marchez
|
||
vers l'élément un et que l'élément un est toujours là.
|
||
|
||
La règle, qui vous servira toute votre vie : *quand une boucle risque de retirer
|
||
des éléments de la liste qu'elle parcourt, parcourez-la à rebours.*
|
||
|
||
`ipairs` n'est pas une exception. Retirer des éléments au milieu d'une boucle
|
||
`ipairs` tourne mal de la même façon. `ipairs` sert à regarder, et le `for` à
|
||
rebours sert à changer.
|
||
|
||
[#_add_some_yourself]
|
||
=== Ajoutez-en vous-même
|
||
|
||
Une branche de plus dans `onInputPressed` et vous pouvez regarder la liste
|
||
grandir pendant que le jeu tourne. Elle va avec les deux autres :
|
||
|
||
[source,lua]
|
||
----
|
||
elseif what == SWITCH_BUTTON1 then
|
||
addBurst()
|
||
----
|
||
|
||
[source,lua]
|
||
----
|
||
local function addBurst()
|
||
for number = 1, BURST_COUNT do
|
||
table.insert(stars, newStar(0))
|
||
end
|
||
end
|
||
----
|
||
|
||
`addBurst` va au-dessus des callbacks, avec vos autres fonctions. Et pour que
|
||
vous puissiez voir changer la longueur de la liste, affichez le nombre d'étoiles
|
||
dans `onOverlayUpdate` :
|
||
|
||
[source,lua]
|
||
----
|
||
overlayPrint(0, 0, "Stars: " .. #stars)
|
||
----
|
||
|
||
Appuyez sur le bouton -- la barre d'espace, sauf si vous avez changé ce qui
|
||
produit `SWITCH_BUTTON1` -- une douzaine de fois. Le nombre monte de dix à
|
||
chaque appui et le ciel s'épaissit, et rien ne se répète tant que vous le gardez
|
||
enfoncé : `onInputPressed` n'est prévenu qu'une fois, quand le bouton s'enfonce.
|
||
Puis regardez le nombre redescendre tout seul à cent vingt à mesure que les
|
||
étoiles en trop atteignent le bas et sont retirées, parce que le
|
||
réapprovisionnement ne fait jamais que compléter la liste jusqu'à `STAR_COUNT`
|
||
et ne la raccourcit jamais.
|
||
|
||
Rien dans le dessin, le déplacement ou le retrait n'a eu besoin d'être prévenu
|
||
que le nombre d'étoiles avait changé. Tout fonctionne à partir de la liste.
|
||
|
||
[#_the_whole_thing_5]
|
||
=== Le tout
|
||
|
||
[source,lua]
|
||
----
|
||
local STAR_COUNT = 120
|
||
local STAR_SHADE = 55
|
||
local BURST_COUNT = 10
|
||
|
||
local PLAYER_WIDTH = 20
|
||
local PLAYER_HEIGHT = 6
|
||
local PLAYER_SPEED = 3
|
||
local PLAYER_MARGIN = 6
|
||
|
||
local width = overlayGetWidth()
|
||
local height = overlayGetHeight()
|
||
|
||
local stars = {}
|
||
local playerX = width // 2
|
||
local playerY = height - PLAYER_HEIGHT - PLAYER_MARGIN
|
||
local movingLeft = false
|
||
local movingRight = false
|
||
|
||
|
||
local function clamp(value, low, high)
|
||
if value < low then
|
||
return low
|
||
end
|
||
if value > high then
|
||
return high
|
||
end
|
||
return value
|
||
end
|
||
|
||
|
||
local function newStar(y)
|
||
local speed = math.random(1, 3)
|
||
local star = {
|
||
x = math.random(0, width - 1),
|
||
y = y,
|
||
speed = speed,
|
||
shade = math.random(60, 90) + speed * STAR_SHADE
|
||
}
|
||
return star
|
||
end
|
||
|
||
|
||
local function makeStars()
|
||
for number = 1, STAR_COUNT do
|
||
table.insert(stars, newStar(math.random(0, height - 1)))
|
||
end
|
||
end
|
||
|
||
|
||
local function addBurst()
|
||
for number = 1, BURST_COUNT do
|
||
table.insert(stars, newStar(0))
|
||
end
|
||
end
|
||
|
||
|
||
local function moveStars()
|
||
for i = #stars, 1, -1 do
|
||
local star = stars[i]
|
||
star.y = star.y + star.speed
|
||
if star.y >= height then
|
||
table.remove(stars, i)
|
||
end
|
||
end
|
||
while #stars < STAR_COUNT do
|
||
table.insert(stars, newStar(0))
|
||
end
|
||
end
|
||
|
||
|
||
local function drawStar(star)
|
||
colorForeground(star.shade, star.shade, star.shade)
|
||
overlayPlot(star.x, star.y)
|
||
end
|
||
|
||
|
||
local function drawStars()
|
||
for _, star in ipairs(stars) do
|
||
drawStar(star)
|
||
end
|
||
end
|
||
|
||
|
||
local function drawPlayer()
|
||
colorForeground(80, 255, 120)
|
||
overlayBox(playerX, playerY, playerX + PLAYER_WIDTH, playerY + PLAYER_HEIGHT)
|
||
end
|
||
|
||
|
||
local function movePlayer()
|
||
if movingLeft then
|
||
playerX = playerX - PLAYER_SPEED
|
||
end
|
||
if movingRight then
|
||
playerX = playerX + PLAYER_SPEED
|
||
end
|
||
playerX = clamp(playerX, 0, width - PLAYER_WIDTH - 1)
|
||
end
|
||
|
||
|
||
function onInputPressed(what)
|
||
if what == SWITCH_LEFT then
|
||
movingLeft = true
|
||
elseif what == SWITCH_RIGHT then
|
||
movingRight = true
|
||
elseif what == SWITCH_BUTTON1 then
|
||
addBurst()
|
||
end
|
||
end
|
||
|
||
|
||
function onInputReleased(what)
|
||
if what == SWITCH_LEFT then
|
||
movingLeft = false
|
||
elseif what == SWITCH_RIGHT then
|
||
movingRight = false
|
||
end
|
||
end
|
||
|
||
|
||
function onOverlayUpdate()
|
||
overlayClear()
|
||
movePlayer()
|
||
moveStars()
|
||
drawStars()
|
||
drawPlayer()
|
||
overlayPrint(0, 0, "Stars: " .. #stars)
|
||
return OVERLAY_UPDATED
|
||
end
|
||
|
||
|
||
makeStars()
|
||
----
|
||
|
||
[#_what_just_happened_3]
|
||
=== Ce qui vient de se passer
|
||
|
||
Le programme est plus court que celui de la leçon cinq, et il fait quelque chose
|
||
que la leçon cinq ne pouvait même pas commencer à faire. Trois morceaux méritent
|
||
un second regard.
|
||
|
||
[source,lua]
|
||
----
|
||
makeStars()
|
||
----
|
||
|
||
Cette unique ligne tout en bas du fichier n'est à l'intérieur d'aucune fonction,
|
||
et on la rate facilement. Lua lit votre fichier de haut en bas quand Singe le
|
||
charge, et une ligne au niveau extérieur du fichier s'exécute sur-le-champ, une
|
||
fois, avant que le moteur ait appelé quoi que ce soit. Tout ce qui est au-dessus
|
||
n'est que définitions -- voici une valeur, voici une fonction -- et c'est la
|
||
seule instruction qui s'exécute au chargement. Elle doit être en bas, parce que
|
||
`makeStars` n'existe pas tant que la ligne qui la crée n'a pas été lue.
|
||
|
||
[source,lua]
|
||
----
|
||
local star = stars[i]
|
||
star.y = star.y + star.speed
|
||
----
|
||
|
||
`star` est une variable locale créée à neuf à chaque tour de la boucle, et elle
|
||
contient la même table que celle qui se trouve dans la liste -- pas une copie.
|
||
Changer `star.y` déplace l'étoile qui est dans la liste. Si les tables étaient
|
||
copiées, cette boucle déplacerait cent vingt copies par image et rien ne
|
||
changerait jamais à l'écran.
|
||
|
||
[source,lua]
|
||
----
|
||
for _, star in ipairs(stars) do
|
||
drawStar(star)
|
||
end
|
||
----
|
||
|
||
Comparez avec la leçon quatre : deux boucles imbriquées, deux calculs par
|
||
étoile, un reste pour garder chacune à l'écran, et trois couches pour simuler la
|
||
variété que vous obtenez maintenant gratuitement. Le dessin ne sait plus et ne
|
||
se soucie plus de savoir comment les étoiles sont arrivées là où elles sont,
|
||
combien il y en a, ni ce qui va leur arriver. Chaque partie du programme fait un
|
||
seul travail, et la liste est ce sur quoi elles sont toutes d'accord.
|
||
|
||
[#_try_it_6]
|
||
=== Essayez
|
||
|
||
. *Rendez-le plus profond.* Changez `math.random(1, 3)` en `math.random(1, 6)`
|
||
et `STAR_SHADE` en `25`. Plus de vitesses, plus de nuances, un ciel plus
|
||
profond.
|
||
. *Moins d'étoiles, plus grosse salve.* Mettez `STAR_COUNT` à `10` et
|
||
`BURST_COUNT` à `100`, puis appuyez trois ou quatre fois sur le bouton.
|
||
Regardez le nombre monter à l'écran, et chronométrez le temps qu'il met à
|
||
redescendre.
|
||
. *Donnez un autre champ à une étoile.* Ajoutez `age = 0` dans `newStar`,
|
||
ajoutez un à `star.age` dans `moveStars`, et affichez `stars[1].age` à côté du
|
||
nombre d'étoiles. Rien d'autre dans le programme n'a besoin de changer pour
|
||
porter un fait nouveau sur chaque étoile.
|
||
. *Arrêtez de les retirer.* Supprimez les trois lignes du bloc
|
||
`if star.y >= height` dans `moveStars` et lancez. Attendez dix secondes, puis
|
||
lisez le nombre à l'écran et regardez le ciel. Trouvez comment les deux
|
||
peuvent être vrais à la fois.
|
||
. *Faites-le dans l'autre sens.* Changez `for i = #stars, 1, -1` en
|
||
`for i = 1, #stars` et lancez jusqu'à ce que ça plante. Puis relisez la
|
||
section ci-dessus avec la vraie erreur sous les yeux.
|
||
|
||
[#_break_it_on_purpose_6]
|
||
=== Cassez-le exprès
|
||
|
||
Faites une faute dans un nom de champ. Dans `moveStars`, changez `star.speed`
|
||
en `star.sped` :
|
||
|
||
[source,lua]
|
||
----
|
||
star.y = star.y + star.sped
|
||
----
|
||
|
||
Lancez-le :
|
||
|
||
----
|
||
Error executing function 'onOverlayUpdate': tables/tables.singe:60: attempt to perform arithmetic on a nil value (field 'sped')
|
||
stack traceback:
|
||
tables/tables.singe:60: in upvalue 'moveStars'
|
||
tables/tables.singe:124: in function 'onOverlayUpdate'
|
||
----
|
||
|
||
Vos numéros de ligne ne seront pas exactement ceux-là, sauf si votre fichier
|
||
correspond ligne pour ligne à celui d'au-dessus. Le nom du fichier, le nombre
|
||
qui le suit et la plainte, voilà ce que vous lisez.
|
||
|
||
Lisez d'abord la fin de la première ligne. `field 'sped'`, c'est Lua qui vous
|
||
dit quel nom est revenu vide, et il vous dit que c'était un champ -- quelque
|
||
chose après un point -- plutôt qu'une variable à vous. « Attempt to perform
|
||
arithmetic on a nil value », c'est ce qu'il était en train de faire quand il
|
||
s'en est aperçu : ajouter `nil` à un nombre, ce qui est impossible.
|
||
|
||
Il n'y a eu aucune plainte quand la table a été fabriquée sans champ `sped`, et
|
||
aucune au moment où vous l'avez demandé. Une table donne `nil` pour tout nom
|
||
qu'elle n'a pas, sans commentaire. L'erreur vient plus tard, à la première
|
||
ligne qui essaie d'*utiliser* le rien qu'on vous a donné, et qui peut être très
|
||
loin de la faute de frappe. Quand vous voyez `nil value (field 'something')`,
|
||
cherchez l'endroit où ce nom a été écrit autrement.
|
||
|
||
[#_what_you_learned_6]
|
||
=== Ce que vous avez appris
|
||
|
||
* Les accolades fabriquent une table, le seul conteneur qu'ait Lua. Une table
|
||
utilisée comme liste contient des éléments à la suite, et des crochets avec un
|
||
indice en sortent un.
|
||
* Lua compte à partir de un. La plupart des autres langages comptent à partir
|
||
de zéro.
|
||
* `#list` est le nombre d'éléments qu'elle contient, et `list[#list]` est le
|
||
dernier.
|
||
* `table.insert` ajoute un élément, au bout ou à la position que vous indiquez ;
|
||
`table.remove` en retire un et comble le vide.
|
||
* Un indice ou un champ jamais rempli vaut `nil`, et rien ne vous prévient tant
|
||
que vous ne vous en servez pas.
|
||
* `ipairs` parcourt une liste et vous donne l'indice et l'élément ; `_` est
|
||
l'habitude pour un indice dont vous ne voulez pas.
|
||
* Une table utilisée comme enregistrement contient des champs nommés, qu'on
|
||
atteint avec un point.
|
||
* Une liste d'enregistrements, c'est la manière dont un jeu garde ses ennemis,
|
||
ses balles, et tout ce dont il a plus d'un exemplaire.
|
||
* Les tables sont partagées, pas copiées, quand vous les faites passer.
|
||
* Quand une boucle risque de retirer des éléments de la liste qu'elle parcourt,
|
||
parcourez-la à rebours.
|
||
|
||
[#_next_time_6]
|
||
=== La prochaine fois
|
||
|
||
Vous avez maintenant toutes les pièces d'un jeu : des choses qui bougent, un
|
||
vaisseau qui répond aux commandes, des décisions, des boucles, des fonctions, et
|
||
une liste que des choses peuvent rejoindre et quitter pendant que le jeu tourne.
|
||
|
||
La leçon sept les assemble en un jeu que vous pouvez perdre. Vous vous dirigez
|
||
le long du bas de l'écran, des blocs tombent du haut, et vous restez hors de
|
||
leur chemin -- et les blocs qui tombent sont une liste d'enregistrements,
|
||
fabriqués, déplacés et retirés exactement comme vos étoiles.
|