722 lines
25 KiB
Text
722 lines
25 KiB
Text
[#_lesson_5_your_own_functions]
|
||
== Leçon 5 : Vos propres fonctions
|
||
|
||
image::learn/05-functions.png[La leçon terminée, 480]
|
||
|
||
La leçon quatre s'est terminée sur un avertissement à propos de votre propre
|
||
programme : `onOverlayUpdate` calcule où vont cent vingt étoiles, décide de la
|
||
luminosité de chacune, les dessine et compte la dérive, et c'est une seule
|
||
fonction qui fait quatre travaux. On est sur le point de lui en demander un
|
||
cinquième, parce qu'un champ d'étoiles sans rien devant n'est pas un jeu.
|
||
|
||
Dans cette leçon, vous le démontez. Vous allez écrire vos propres fonctions --
|
||
des fonctions dont Singe n'a jamais entendu parler et qu'il n'appellera
|
||
jamais -- et leur confier le travail. À la fin, tout le champ d'étoiles tient en une
|
||
ligne qui dit `drawStars()`, un vaisseau est revenu en bas de l'écran, et le
|
||
programme est plus facile à lire que lorsqu'il en faisait moins.
|
||
|
||
[#_start_a_new_folder_4]
|
||
=== Créez un nouveau dossier
|
||
|
||
Un dossier appelé `functions`, dedans un fichier appelé `functions.singe`, et
|
||
`Singe -R functions` qui tourne dans un terminal.
|
||
|
||
Copiez-y le programme terminé de la leçon quatre. Le voici, inchangé, et ça vaut
|
||
la peine de le lancer une fois avant de toucher à quoi que ce soit :
|
||
|
||
[source,lua]
|
||
----
|
||
LAYERS = 3
|
||
STARS_PER_LAYER = 40
|
||
ACROSS_STEP = 37
|
||
DOWN_STEP = 61
|
||
LAYER_SHIFT = 13
|
||
LAYER_SPEED = 0.5
|
||
LAYER_BRIGHT = 55
|
||
|
||
width = overlayGetWidth()
|
||
height = overlayGetHeight()
|
||
|
||
drift = 0
|
||
|
||
|
||
function onOverlayUpdate()
|
||
drift = drift + 1
|
||
overlayClear()
|
||
|
||
for layer = 1, LAYERS do
|
||
for star = 1, STARS_PER_LAYER do
|
||
starX = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
||
starY = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
||
shade = math.random(60, 90) + layer * LAYER_BRIGHT
|
||
colorForeground(shade, shade, shade)
|
||
overlayPlot(starX, starY)
|
||
end
|
||
end
|
||
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Trois couches d'étoiles qui descendent dans la fenêtre à trois vitesses. Rien
|
||
dans la première moitié de cette leçon ne change ce que vous voyez. Tout ce
|
||
qu'elle contient change la façon dont le programme se lit.
|
||
|
||
[#_one_job_one_name]
|
||
=== Un travail, un nom
|
||
|
||
Commencez par les deux lignes qui mettent une étoile à l'écran. Au-dessus de
|
||
`onOverlayUpdate`, ajoutez ceci :
|
||
|
||
[source,lua]
|
||
----
|
||
function drawStar(x, y, shade)
|
||
colorForeground(shade, shade, shade)
|
||
overlayPlot(x, y)
|
||
end
|
||
----
|
||
|
||
Puis, dans la boucle intérieure, supprimez ces deux lignes et appelez-la à la
|
||
place :
|
||
|
||
[source,lua]
|
||
----
|
||
for star = 1, STARS_PER_LAYER do
|
||
starX = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
||
starY = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
||
shade = math.random(60, 90) + layer * LAYER_BRIGHT
|
||
drawStar(starX, starY, shade)
|
||
end
|
||
----
|
||
|
||
Enregistrez. Rien ne change à l'écran.
|
||
|
||
Vous avez déjà écrit des fonctions -- `onOverlayUpdate` en est une, et
|
||
`onInputPressed` et `onInputReleased` de la leçon trois aussi -- mais ce sont
|
||
des callbacks. Vous les écrivez et Singe les appelle. `drawStar` est différente.
|
||
Singe n'en a jamais entendu parler. Elle s'exécute parce que *vous* avez écrit
|
||
son nom et des parenthèses, de la même façon que vous appelez `overlayPlot`, et
|
||
rien au monde ne l'exécutera si vous ne le faites pas.
|
||
|
||
Tout le truc est là. Désormais, quand vous vous surprenez à penser « et
|
||
maintenant, dessiner une étoile », vous pouvez écrire « dessiner une étoile », et
|
||
c'est bien ce que ça voudra dire.
|
||
|
||
[#_a_parameter_is_a_name_not_a_thing]
|
||
=== Un paramètre est un nom, pas une chose
|
||
|
||
Regardez de nouveau la première ligne de `drawStar`.
|
||
|
||
[source,lua]
|
||
----
|
||
function drawStar(x, y, shade)
|
||
----
|
||
|
||
Les trois noms entre les parenthèses sont des *paramètres*. Un paramètre est un
|
||
nom que la fonction utilise pour ce que l'appelant lui a passé, quoi que ce
|
||
soit. Quand vous écrivez `drawStar(starX, starY, shade)`, Lua met dans `x` ce
|
||
qu'il y a dans `starX`, dans `y` ce qu'il y a dans `starY`, dans `shade` ce
|
||
qu'il y a dans `shade`, puis exécute la fonction.
|
||
|
||
Les noms n'ont rien à voir entre eux. Le nombre de l'appelant s'appelle `starX`
|
||
là-bas et `x` ici, et ça marcherait exactement pareil si vous l'appeliez
|
||
`across` ou `banana`. À l'intérieur de la fonction, seuls les noms entre les
|
||
parenthèses existent, et ils contiennent des copies de ce qui est entré.
|
||
|
||
C'est ici que les parenthèses vides de la leçon un prennent enfin leur sens.
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
----
|
||
|
||
Rien entre les parenthèses veut dire aucun paramètre : le moteur n'a rien à dire
|
||
à cette fonction, donc il n'y a rien à nommer. Comparez avec le callback que
|
||
vous avez écrit à la leçon trois.
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
----
|
||
|
||
Un paramètre, appelé `what`. Le moteur a quelque chose à dire à celui-ci --
|
||
quelle commande a été enfoncée -- donc il y a un nom pour l'attraper. Vous vous
|
||
servez d'un paramètre depuis la leçon trois.
|
||
|
||
Un paramètre est vraiment la copie personnelle de la fonction, et ça vaut la
|
||
peine de vous le prouver une fois. Tapez ceci, appelez-la depuis
|
||
`onOverlayUpdate`, et retirez-la ensuite :
|
||
|
||
[source,lua]
|
||
----
|
||
function half(number)
|
||
number = number // 2
|
||
return number
|
||
end
|
||
----
|
||
|
||
Appelez-la sous la forme `half(drift)`. La fonction divise son `number` par deux
|
||
et renvoie la réponse, et `drift`, là-bas dans `onOverlayUpdate`, continue de
|
||
compter comme si de rien n'était. Une fonction ne peut pas aller changer les
|
||
variables de l'appelant à travers un paramètre. Elle reçoit une copie, et ce
|
||
qu'elle fait de la copie ne regarde qu'elle.
|
||
|
||
[#_handing_an_answer_back]
|
||
=== Renvoyer une réponse
|
||
|
||
`half` a fait quelque chose que les fonctions que vous avez écrites jusqu'ici ne
|
||
faisaient pas : elle a donné une réponse. C'est la dernière ligne.
|
||
|
||
[source,lua]
|
||
----
|
||
return number
|
||
----
|
||
|
||
`return` renvoie une valeur à celui qui a appelé la fonction, et l'appel se
|
||
transforme en cette valeur. Écrivez `smaller = half(drift)` et Lua exécute
|
||
`half`, prend ce que `half` a renvoyé et le met dans `smaller`. Vous avez
|
||
rencontré `return` à la leçon un, où `onOverlayUpdate` renvoie
|
||
`OVERLAY_UPDATED` pour répondre à la question de Singe : est-ce que quelque
|
||
chose a changé ? C'est le même mot qui fait le même travail, sauf que
|
||
maintenant, c'est vous qui posez la question.
|
||
|
||
La ligne de la luminosité ne demande qu'à devenir une fonction qui donne une
|
||
réponse. Ajoutez celle-ci :
|
||
|
||
[source,lua]
|
||
----
|
||
function shadeFor(layer)
|
||
return math.random(60, 90) + layer * LAYER_BRIGHT
|
||
end
|
||
----
|
||
|
||
Tout le corps est un `return`, ce qui est normal pour une petite fonction.
|
||
Maintenant, la boucle intérieure peut passer la réponse directement à
|
||
`drawStar`, sans nom entre les deux :
|
||
|
||
[source,lua]
|
||
----
|
||
drawStar(starX, starY, shadeFor(layer))
|
||
----
|
||
|
||
Un appel dans un appel. Lua calcule d'abord celui de l'intérieur, reçoit un
|
||
nombre de `shadeFor`, et passe ce nombre à `drawStar` comme troisième argument.
|
||
Vous avez déjà écrit quelque chose de ce genre sans le remarquer : le
|
||
`math.floor(drift * layer * LAYER_SPEED)` de la leçon quatre est un appel dont
|
||
la réponse est utilisée sur place, au milieu d'un calcul plus grand.
|
||
|
||
[#_two_answers_at_once]
|
||
=== Deux réponses à la fois
|
||
|
||
Les deux lignes de position sont plus difficiles, parce que ce sont deux nombres
|
||
calculés ensemble et qu'il faudrait en choisir un à renvoyer. Dans la plupart
|
||
des langages, une fonction peut renvoyer une valeur, et en obtenir deux est une
|
||
corvée. Lua vous laisse en renvoyer autant que vous voulez.
|
||
|
||
[source,lua]
|
||
----
|
||
function starAt(star, layer)
|
||
local x = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
||
local y = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
||
return x, y
|
||
end
|
||
----
|
||
|
||
Vous attrapez les réponses en mettant deux noms à gauche, séparés par une
|
||
virgule :
|
||
|
||
[source,lua]
|
||
----
|
||
local x, y = starAt(star, layer)
|
||
----
|
||
|
||
La première valeur dans le premier nom, la deuxième dans le deuxième. Si vous
|
||
demandez moins de noms que la fonction ne renvoie de valeurs, les valeurs en
|
||
trop sont jetées : `local x = starAt(star, layer)` vous donne le nombre
|
||
horizontal et perd le vertical, sans se plaindre. Si vous demandez plus de noms
|
||
qu'il n'y a de valeurs, les noms en trop valent `nil`.
|
||
|
||
Ignorez le mot `local` dans ces deux blocs encore une minute. Il aura bientôt sa
|
||
propre partie, et c'est la plus importante.
|
||
|
||
[#_the_star_field_in_one_line]
|
||
=== Le champ d'étoiles en une ligne
|
||
|
||
La boucle intérieure tient maintenant en trois lignes courtes, qui parlent
|
||
toutes d'une couche d'étoiles. C'est un travail qui a un nom.
|
||
|
||
[source,lua]
|
||
----
|
||
function drawLayer(layer)
|
||
for star = 1, STARS_PER_LAYER do
|
||
local x, y = starAt(star, layer)
|
||
drawStar(x, y, shadeFor(layer))
|
||
end
|
||
end
|
||
----
|
||
|
||
Et la boucle extérieure est elle aussi un travail qui a un nom :
|
||
|
||
[source,lua]
|
||
----
|
||
function drawStars()
|
||
for layer = 1, LAYERS do
|
||
drawLayer(layer)
|
||
end
|
||
end
|
||
----
|
||
|
||
Des fonctions qui appellent des fonctions qui appellent des fonctions, sur trois
|
||
niveaux, et chacune fait trois ou quatre lignes que vous pouvez garder en tête
|
||
d'un seul coup. `onOverlayUpdate` peut maintenant dire ce qu'**est** une image :
|
||
|
||
[source,lua]
|
||
----
|
||
function onOverlayUpdate()
|
||
drift = drift + 1
|
||
overlayClear()
|
||
drawStars()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Voilà la promesse faite à la fin de la leçon quatre, tenue.
|
||
|
||
[#_put_the_ship_back]
|
||
=== Remettez le vaisseau
|
||
|
||
Maintenant, le cinquième travail, celui qui aurait mis le désordre dans l'ancien
|
||
`onOverlayUpdate`. Ramenez la boîte que vous pilotiez à la leçon trois, cette
|
||
fois sous la forme d'un vaisseau posé près du bas de l'écran, qui ne bouge que
|
||
vers la gauche et la droite.
|
||
|
||
Il lui faut quelques valeurs à lui, en haut avec les autres, et les deux
|
||
callbacks d'entrée de la leçon trois, sans les branches pour le haut et le bas :
|
||
|
||
[source,lua]
|
||
----
|
||
PLAYER_WIDTH = 20
|
||
PLAYER_HEIGHT = 6
|
||
PLAYER_SPEED = 3
|
||
PLAYER_MARGIN = 6
|
||
|
||
playerX = width // 2
|
||
playerY = height - PLAYER_HEIGHT - PLAYER_MARGIN
|
||
movingLeft = false
|
||
movingRight = false
|
||
----
|
||
|
||
[source,lua]
|
||
----
|
||
function onInputPressed(what)
|
||
if what == SWITCH_LEFT then
|
||
movingLeft = true
|
||
elseif what == SWITCH_RIGHT then
|
||
movingRight = true
|
||
end
|
||
end
|
||
|
||
|
||
function onInputReleased(what)
|
||
if what == SWITCH_LEFT then
|
||
movingLeft = false
|
||
elseif what == SWITCH_RIGHT then
|
||
movingRight = false
|
||
end
|
||
end
|
||
----
|
||
|
||
Le dessiner prend deux lignes, donc c'est une fonction :
|
||
|
||
[source,lua]
|
||
----
|
||
function drawPlayer()
|
||
colorForeground(80, 255, 120)
|
||
overlayBox(playerX, playerY, playerX + PLAYER_WIDTH, playerY + PLAYER_HEIGHT)
|
||
end
|
||
----
|
||
|
||
C'est en le déplaçant que la leçon porte ses fruits. À la leçon trois, garder la
|
||
boîte à l'écran prenait quatre lignes de `if` et de `elseif` pour chaque
|
||
direction. Cette idée -- tenir un nombre entre un plancher et un plafond -- a un
|
||
nom, et ce nom, c'est *clamp* :
|
||
|
||
[source,lua]
|
||
----
|
||
function clamp(value, low, high)
|
||
if value < low then
|
||
return low
|
||
end
|
||
if value > high then
|
||
return high
|
||
end
|
||
return value
|
||
end
|
||
----
|
||
|
||
Trois sorties, et une seule au plus se produit, parce que `return` ne fait pas
|
||
que renvoyer une réponse : il arrête net la fonction. Rien de ce qui suit ne
|
||
s'exécute. Si la valeur est sous le plancher, renvoyez le plancher et arrêtez.
|
||
Si elle dépasse le plafond, renvoyez le plafond et arrêtez. Sinon, renvoyez ce
|
||
qui est entré.
|
||
|
||
[source,lua]
|
||
----
|
||
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
|
||
----
|
||
|
||
Une fonction qui se termine sans `return` ne renvoie rien, c'est-à-dire `nil`.
|
||
C'est ce qu'il faut pour `drawPlayer` et `movePlayer`, qu'on appelle pour faire
|
||
quelque chose plutôt que pour répondre à quelque chose. C'est un bug dans une
|
||
fonction comme `clamp` : oubliez ce dernier `return value` et chaque position au
|
||
milieu de la plage revient sous la forme `nil`.
|
||
|
||
[#_where_names_live]
|
||
=== Où vivent les noms
|
||
|
||
Chaque nom que vous avez créé depuis la leçon deux était un nom *global*. Un nom
|
||
global appartient à tout le programme : `drift`, `playerX` et `LAYERS` peuvent
|
||
être lus et écrits par n'importe quelle fonction du fichier, à tout moment, et
|
||
ils restent là tant que le jeu tourne.
|
||
|
||
Ça a l'air pratique, et pour six noms, ça l'est. Les ennuis commencent à
|
||
soixante. Chaque nom global est un nom que tout le programme s'est engagé à
|
||
réserver, donc quand une autre fonction veut un endroit où garder une position
|
||
horizontale et l'appelle `starX` elle aussi, les deux sont la même boîte, et
|
||
chacune écrit par-dessus le nombre de l'autre. Quand un nom global contient
|
||
quelque chose de faux, n'importe quoi dans le fichier a pu l'y mettre, et
|
||
trouver quoi veut dire tout relire.
|
||
|
||
Un nom *local* est le remède. Mettez le mot `local` devant un nom la première
|
||
fois que vous l'utilisez, et le nom n'existe qu'à l'intérieur de la `function`,
|
||
de la boucle ou du `if` où il a été écrit.
|
||
|
||
[source,lua]
|
||
----
|
||
function starAt(star, layer)
|
||
local x = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
||
local y = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
||
return x, y
|
||
end
|
||
----
|
||
|
||
Ici, `x` et `y` ne regardent personne d'autre. Une autre fonction peut avoir son
|
||
propre `x`, et les deux ne se rencontrent jamais. Quand le `x` de `starAt` est
|
||
faux, les trois lignes ci-dessus sont les trois seules qui ont pu le provoquer.
|
||
Sans le mot `local`, ces deux lignes créeraient discrètement deux noms globaux
|
||
de plus, et c'est exactement ce que `starX` et `starY` étaient depuis le début.
|
||
|
||
Maintenant, la partie qui surprend tout le monde. Tapez cette fonction, et
|
||
mettez dans `onOverlayUpdate` une ligne qui vous montre ce qu'elle renvoie :
|
||
|
||
[source,lua]
|
||
----
|
||
function countUp()
|
||
local counted = 0
|
||
counted = counted + 1
|
||
return counted
|
||
end
|
||
----
|
||
|
||
[source,lua]
|
||
----
|
||
overlayPrint(0, 0, "Count: " .. countUp())
|
||
----
|
||
|
||
`Count: 1`, et ça reste `Count: 1` tant que le jeu tourne, alors que la fonction
|
||
est appelée soixante fois par seconde et ajoute un à chaque fois. Un nom local
|
||
est créé tout neuf quand la fonction commence et disparaît quand elle se
|
||
termine, donc `counted` naît à `0`, devient `1` et meurt, soixante fois par
|
||
seconde, pour toujours. Un nom local ne se souvient pas. Retirez les deux une
|
||
fois que vous avez regardé.
|
||
|
||
Ce n'est pas un défaut, c'est le but : un nom local ne peut pas être empoisonné
|
||
par l'appel précédent, parce qu'il ne reste rien de l'appel précédent. Quand
|
||
vous avez vraiment besoin de vous souvenir de quelque chose d'un appel à
|
||
l'autre -- `drift` et `playerX` doivent se souvenir -- le nom doit vivre en
|
||
dehors de toute fonction. Mettez-le en haut du fichier, avec `local` devant :
|
||
|
||
[source,lua]
|
||
----
|
||
local drift = 0
|
||
local playerX = width // 2
|
||
----
|
||
|
||
Un `local` écrit en haut du fichier, en dehors de toutes les fonctions, vit
|
||
aussi longtemps que le script et peut être vu par toutes les fonctions en
|
||
dessous. Ce dernier mot compte : *en dessous*. Un nom local existe à partir de
|
||
sa propre ligne et vers le bas, donc une fonction écrite au-dessus de la ligne
|
||
qui le crée ne peut pas le voir. C'est vrai aussi des fonctions elles-mêmes, une
|
||
fois que vous les marquez `local`, et c'est pourquoi le script terminé est dans
|
||
cet ordre : les valeurs d'abord, puis vos propres fonctions, puis les
|
||
callbacks, en dernier.
|
||
|
||
Parcourez maintenant tout le fichier et mettez `local` devant chaque nom que
|
||
vous avez créé -- les constantes en majuscules, `width` et `height`, `drift`,
|
||
les quatre valeurs du joueur et chacune de vos propres fonctions. Trois noms n'y
|
||
ont pas droit : `onInputPressed`, `onInputReleased` et `onOverlayUpdate` doivent
|
||
rester globaux, parce que Singe les trouve par leur nom et qu'un nom local est
|
||
invisible depuis l'extérieur du fichier.
|
||
|
||
Soyons honnêtes sur ce que les noms locaux ne règlent pas. Si vous faites une
|
||
faute de frappe dans un nom auquel vous affectez une valeur --
|
||
`movinLeft = true` au lieu de `movingLeft = true` -- Lua ne se plaint pas. Il
|
||
crée un tout nouveau nom global avec votre faute de frappe pour nom, le remplit,
|
||
et laisse le vrai tranquille. Rien ne bouge, aucune erreur, et vous fixez
|
||
l'écran pendant dix minutes. Les noms locaux ne vous sauveront pas de celle-là.
|
||
Relire le nom deux fois, si.
|
||
|
||
[#_the_whole_thing_4]
|
||
=== Le tout
|
||
|
||
Voici le programme terminé, avec chaque morceau de la leçon dedans.
|
||
|
||
[source,lua]
|
||
----
|
||
local LAYERS = 3
|
||
local STARS_PER_LAYER = 40
|
||
local ACROSS_STEP = 37
|
||
local DOWN_STEP = 61
|
||
local LAYER_SHIFT = 13
|
||
local LAYER_SPEED = 0.5
|
||
local LAYER_BRIGHT = 55
|
||
|
||
local PLAYER_WIDTH = 20
|
||
local PLAYER_HEIGHT = 6
|
||
local PLAYER_SPEED = 3
|
||
local PLAYER_MARGIN = 6
|
||
|
||
local width = overlayGetWidth()
|
||
local height = overlayGetHeight()
|
||
|
||
local drift = 0
|
||
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 shadeFor(layer)
|
||
return math.random(60, 90) + layer * LAYER_BRIGHT
|
||
end
|
||
|
||
|
||
local function starAt(star, layer)
|
||
local x = (star * ACROSS_STEP + layer * LAYER_SHIFT) % width
|
||
local y = (star * DOWN_STEP + math.floor(drift * layer * LAYER_SPEED)) % height
|
||
return x, y
|
||
end
|
||
|
||
|
||
local function drawStar(x, y, shade)
|
||
colorForeground(shade, shade, shade)
|
||
overlayPlot(x, y)
|
||
end
|
||
|
||
|
||
local function drawLayer(layer)
|
||
for star = 1, STARS_PER_LAYER do
|
||
local x, y = starAt(star, layer)
|
||
drawStar(x, y, shadeFor(layer))
|
||
end
|
||
end
|
||
|
||
|
||
local function drawStars()
|
||
for layer = 1, LAYERS do
|
||
drawLayer(layer)
|
||
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
|
||
end
|
||
end
|
||
|
||
|
||
function onInputReleased(what)
|
||
if what == SWITCH_LEFT then
|
||
movingLeft = false
|
||
elseif what == SWITCH_RIGHT then
|
||
movingRight = false
|
||
end
|
||
end
|
||
|
||
|
||
function onOverlayUpdate()
|
||
drift = drift + 1
|
||
overlayClear()
|
||
movePlayer()
|
||
drawStars()
|
||
drawPlayer()
|
||
return OVERLAY_UPDATED
|
||
end
|
||
----
|
||
|
||
Deux choses méritent d'être montrées du doigt avant de passer à la suite.
|
||
|
||
`local function drawStar(x, y, shade)` marque la fonction elle-même comme
|
||
locale. Vos propres fonctions sont des noms comme les autres, et elles
|
||
n'appartiennent qu'à ce fichier. Les trois callbacks restent globaux pour que
|
||
Singe puisse les trouver.
|
||
|
||
Et `onOverlayUpdate` fait maintenant six lignes qui disent ce qu'est une image :
|
||
compter la dérive, effacer l'écran, déplacer le joueur, dessiner le ciel,
|
||
dessiner le joueur, terminé. Il fait cinq travaux en nommant cinq choses qui
|
||
font chacune un travail, et c'est la forme de toutes les boucles de jeu que vous
|
||
écrirez jamais.
|
||
|
||
[#_a_function_is_a_name_for_an_idea]
|
||
=== Une fonction est le nom d'une idée
|
||
|
||
`clamp` n'est appelée qu'une fois. La sortir n'a économisé aucune frappe --
|
||
quatre lignes en sont sorties, une seule y est entrée, et une fonction de neuf
|
||
lignes est apparue ailleurs. Ça valait quand même la peine.
|
||
|
||
Une fonction n'est pas seulement un moyen d'éviter de vous répéter. C'est un
|
||
moyen de donner un nom à une idée, pour que le code qui se sert de l'idée puisse
|
||
dire le nom et passer à autre chose. « Garder le joueur entre le bord gauche et
|
||
le bord droit » est une pensée que vous avez eue une fois.
|
||
`clamp(playerX, 0, width - PLAYER_WIDTH - 1)`, c'est cette pensée, écrite.
|
||
Quatre instructions `if`, c'est la même pensée démontée en morceaux que vous
|
||
devez remonter dans votre tête chaque fois que vous les lisez.
|
||
|
||
Quand vous vous demandez si quelque chose mérite d'être une fonction, ne vous
|
||
demandez pas combien de fois vous l'appellerez. Demandez-vous si ça a un nom.
|
||
|
||
[#_try_it_5]
|
||
=== Essayez
|
||
|
||
. *Renommez un paramètre.* Dans `drawStar`, changez `x` et `y` en `across` et
|
||
`down`, partout dans la fonction. Ne touchez à rien en dehors. Lancez.
|
||
Trouvez pourquoi rien n'a cassé.
|
||
. *Dessinez-les dans l'autre ordre.* Échangez `drawStars()` et `drawPlayer()`
|
||
dans `onOverlayUpdate` et regardez le vaisseau. Puis remettez-les. Chacune de
|
||
ces cinq lignes est un travail, et leur ordre à l'écran est leur ordre dans la
|
||
fonction.
|
||
. *Utilisez `clamp` sur une nuance.* Changez `shadeFor` en
|
||
`return clamp(math.random(60, 90) + layer * 90, 0, 200)` et regardez les trois
|
||
couches. Puis retirez le `clamp` et regardez encore : `colorForeground`
|
||
retenait discrètement les nombres pour vous depuis le début, et maintenant
|
||
vous voyez où.
|
||
. *Appelez `drawStar` avec deux arguments.* Écrivez `drawStar(100, 100)`
|
||
quelque part dans `drawLayer` et lancez. L'erreur nomme exactement ce qui
|
||
manquait. Lisez-la avant de remettre le troisième argument.
|
||
. *Déplacez un nom local.* Prenez la ligne `local drift = 0` et déplacez-la à la
|
||
fin du fichier, en dessous de tout. Lancez, lisez l'erreur, et pensez aux mots
|
||
« en dessous » dans la partie ci-dessus.
|
||
|
||
[#_break_it_on_purpose_5]
|
||
=== Cassez-le exprès
|
||
|
||
L'ordre d'un fichier qui contient des noms locaux compte, et l'erreur que vous
|
||
obtenez quand vous cassez cet ordre n'est pas celle que vous attendez.
|
||
|
||
Prenez toute la fonction `drawStar` et déplacez-la tout *en bas* du script, sous
|
||
`onOverlayUpdate`. Tout est encore là, écrit pareil. Lancez :
|
||
|
||
----
|
||
Error executing function 'onOverlayUpdate': functions/functions.singe:50: attempt to call a nil value (global 'drawStar')
|
||
stack traceback:
|
||
functions/functions.singe:50: in upvalue 'drawLayer'
|
||
functions/functions.singe:57: in upvalue 'drawStars'
|
||
functions/functions.singe:101: in function 'onOverlayUpdate'
|
||
----
|
||
|
||
Elle n'a pas la même allure que l'erreur de la leçon un, parce que c'est une
|
||
autre sorte d'erreur. L'erreur de la leçon un a été attrapée avant que le script
|
||
s'exécute. Celle-ci a été attrapée pendant qu'il s'exécutait, c'est pourquoi
|
||
Singe nomme la fonction dans laquelle il était, et pourquoi il y a un
|
||
*traceback* dessous : la liste de qui a appelé qui, le plus récent en premier.
|
||
La ligne 50 est dans `drawLayer`, qui a été appelée depuis la ligne 57, dans
|
||
`drawStars`, qui a été appelée depuis la ligne 101, dans `onOverlayUpdate`. Vos
|
||
numéros de ligne seront différents si votre fichier ne correspond pas exactement
|
||
à celui ci-dessus, et le moteur peut ajouter une ligne ou deux de son cru sous
|
||
celles-ci. Le mot `upvalue` est le nom que Lua donne à un nom local qu'une
|
||
fonction a emprunté au fichier qui l'entoure ; passez dessus pour l'instant.
|
||
|
||
La plainte elle-même vaut la peine d'être apprise par cœur. « Attempt to call a
|
||
nil value » veut dire que vous avez mis des parenthèses après un nom qui ne
|
||
contenait rien. Le mot `global` devant `drawStar`, c'est la partie qui vend la
|
||
mèche : vous avez marqué `drawStar` comme locale, donc quand Lua a lu
|
||
`drawLayer` et n'a trouvé aucun nom local qui s'appelle ainsi nulle part
|
||
au-dessus, il ne restait plus qu'à essayer un nom global -- et il n'y a pas non plus de nom global appelé
|
||
`drawStar`. Remettez la fonction au-dessus du code qui l'appelle, et l'erreur
|
||
s'en va avec.
|
||
|
||
[#_what_you_learned_5]
|
||
=== Ce que vous avez appris
|
||
|
||
* Vous pouvez écrire vos propres fonctions, et rien ne les exécute à part vos
|
||
propres appels.
|
||
* Les noms entre les parenthèses d'une fonction sont des paramètres, et ce sont
|
||
les copies, propres à la fonction, de ce que l'appelant a passé.
|
||
* `return` renvoie une valeur et arrête la fonction sur-le-champ, et une
|
||
fonction qui se termine sans en avoir un renvoie `nil`.
|
||
* Un appel peut être l'argument d'un autre appel, et celui de l'intérieur se
|
||
fait en premier.
|
||
* Lua peut renvoyer plus d'une valeur, et vous les attrapez avec plusieurs noms
|
||
séparés par des virgules.
|
||
* `local` crée un nom qui n'existe que là où il a été écrit.
|
||
* Un nom local dans une fonction est créé tout neuf à chaque appel et ne se
|
||
souvient de rien de l'appel précédent.
|
||
* Un nom local ne peut être vu que sous la ligne qui le crée, donc les valeurs
|
||
et vos propres fonctions vont au-dessus des callbacks qui s'en servent.
|
||
* Les callbacks restent globaux, parce que Singe les trouve par leur nom.
|
||
* Une fonction vaut la peine d'être écrite quand ce qu'elle fait a un nom, même
|
||
si vous ne l'appelez qu'une fois.
|
||
|
||
[#_next_time_5]
|
||
=== La prochaine fois
|
||
|
||
Vos cent vingt étoiles sont toujours cent vingt copies de trois idées. Rien dans
|
||
le programme n'est *une étoile* : il y a un nombre, et un peu d'arithmétique qui
|
||
transforme ce nombre en position. Vous ne pouvez pas dire « celle-ci est plus
|
||
proche, donc elle tombe plus vite et brille plus fort », parce qu'il n'y a pas
|
||
de « celle-ci » dont le dire, et vous ne pouvez pas laisser une étoile tomber
|
||
par le bas et être remplacée par une nouvelle, parce qu'il n'y a rien là à
|
||
retirer.
|
||
|
||
La leçon six vous donne la chose qui contient une étoile, puis un moyen d'en
|
||
contenir cent vingt et d'en ajouter une cent vingt et unième pendant que le jeu
|
||
tourne. C'est la leçon la plus utile de cette partie du livre.
|