715 lines
24 KiB
Text
715 lines
24 KiB
Text
[#_lesson_5_your_own_functions]
|
|
== Lección 5: Tus propias funciones
|
|
|
|
image::learn/05-functions.png[La lección terminada, 480]
|
|
|
|
La lección cuatro terminó con un aviso sobre tu propio programa:
|
|
`onOverlayUpdate` calcula dónde van ciento veinte estrellas, decide lo brillante
|
|
que es cada una, las dibuja y cuenta la deriva, y es una sola función haciendo
|
|
cuatro trabajos. Y está a punto de recibir un quinto, porque un campo de
|
|
estrellas sin nada delante no es un juego.
|
|
|
|
En esta lección lo desmontas. Escribirás tus propias funciones -- funciones de
|
|
las que Singe nunca ha oído hablar y que nunca llamará -- y les pasarás el
|
|
trabajo. Al final, todo el campo de estrellas es una línea que dice
|
|
`drawStars()`, vuelve a haber una nave en la parte de abajo de la pantalla, y el
|
|
programa se lee mejor que cuando hacía menos.
|
|
|
|
[#_start_a_new_folder_4]
|
|
=== Empieza una carpeta nueva
|
|
|
|
Una carpeta llamada `functions`, dentro un archivo llamado `functions.singe`, y
|
|
`Singe -R functions` ejecutándose en una terminal.
|
|
|
|
Copia en él el programa terminado de la lección cuatro. Aquí lo tienes, sin
|
|
cambios, y vale la pena ejecutarlo una vez antes de tocar nada:
|
|
|
|
[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
|
|
----
|
|
|
|
Tres capas de estrellas bajando por la ventana a tres velocidades. Nada de la
|
|
primera mitad de esta lección cambia lo que ves. Todo cambia cómo se lee el
|
|
programa.
|
|
|
|
[#_one_job_one_name]
|
|
=== Un trabajo, un nombre
|
|
|
|
Empieza por las dos líneas que ponen una estrella en la pantalla. Encima de
|
|
`onOverlayUpdate`, añade esto:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawStar(x, y, shade)
|
|
colorForeground(shade, shade, shade)
|
|
overlayPlot(x, y)
|
|
end
|
|
----
|
|
|
|
Luego, en el bucle de dentro, borra esas dos líneas y llama a la función en su
|
|
lugar:
|
|
|
|
[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
|
|
----
|
|
|
|
Guarda. La imagen es la misma.
|
|
|
|
Ya habías escrito funciones antes -- `onOverlayUpdate` es una, y también lo son
|
|
`onInputPressed` y `onInputReleased` de la lección tres -- pero son callbacks. Tú
|
|
las escribes y Singe las llama. `drawStar` es distinta. Singe no ha oído hablar
|
|
de ella nunca. Se ejecuta porque *tú* has escrito su nombre y los paréntesis,
|
|
igual que llamas a `overlayPlot`, y nada en el mundo la ejecutará si no lo haces
|
|
tú.
|
|
|
|
Ese es todo el truco. A partir de aquí, cuando te descubras pensando «y ahora
|
|
dibuja una estrella», puedes escribir «dibuja una estrella» y decirlo en serio.
|
|
|
|
[#_a_parameter_is_a_name_not_a_thing]
|
|
=== Un parámetro es un nombre, no una cosa
|
|
|
|
Vuelve a mirar la primera línea de `drawStar`.
|
|
|
|
[source,lua]
|
|
----
|
|
function drawStar(x, y, shade)
|
|
----
|
|
|
|
Los tres nombres que hay entre los paréntesis son *parámetros*. Un parámetro es
|
|
un nombre que la función usa para lo que le haya dado quien la llama. Cuando
|
|
escribes `drawStar(starX, starY, shade)`, Lua pone en `x` lo que haya en
|
|
`starX`, en `y` lo que haya en `starY`, en `shade` lo que haya en `shade`, y
|
|
luego ejecuta la función.
|
|
|
|
Los nombres no tienen nada que ver entre sí. El número de quien llama se llama
|
|
`starX` ahí fuera y `x` aquí dentro, y funcionaría exactamente igual si lo
|
|
llamaras `across` o `banana`. Dentro de la función solo existen los nombres que
|
|
hay entre los paréntesis, y guardan copias de lo que ha entrado.
|
|
|
|
Aquí es donde por fin tienen sentido los paréntesis vacíos de la lección uno.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
----
|
|
|
|
Nada entre los paréntesis significa que no hay parámetros: el motor no tiene
|
|
nada que decirle a esta función, así que no hay nada a lo que poner nombre.
|
|
Compárala con el callback que escribiste en la lección tres.
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
----
|
|
|
|
Un parámetro, llamado `what`. El motor tiene algo que decirle a esta -- qué
|
|
control se ha pulsado -- así que hay un nombre donde recogerlo. Llevas usando un
|
|
parámetro desde la lección tres.
|
|
|
|
Un parámetro es de verdad la copia propia de la función, y vale la pena
|
|
comprobarlo por ti mismo una vez. Escribe esto, llámalo desde
|
|
`onOverlayUpdate` y quítalo otra vez después:
|
|
|
|
[source,lua]
|
|
----
|
|
function half(number)
|
|
number = number // 2
|
|
return number
|
|
end
|
|
----
|
|
|
|
Llámala como `half(drift)`. La función divide entre dos su `number` y devuelve
|
|
el resultado, y `drift`, ahí fuera en `onOverlayUpdate`, sigue contando como si
|
|
no hubiera pasado nada. Una función no puede alargar la mano y cambiar las
|
|
variables de quien la llama a través de un parámetro. Recibe una copia, y lo que
|
|
haga con la copia es asunto suyo.
|
|
|
|
[#_handing_an_answer_back]
|
|
=== Devolver una respuesta
|
|
|
|
`half` ha hecho algo que no hacían las funciones que has escrito hasta ahora: ha
|
|
dado una respuesta. Es la última línea.
|
|
|
|
[source,lua]
|
|
----
|
|
return number
|
|
----
|
|
|
|
`return` devuelve un valor a quien llamó a la función, y la llamada se convierte
|
|
en ese valor. Escribe `smaller = half(drift)` y Lua ejecuta `half`, coge lo que
|
|
`half` ha devuelto y lo mete en `smaller`. Conociste `return` en la lección uno,
|
|
donde `onOverlayUpdate` devuelve `OVERLAY_UPDATED` para responder a la pregunta
|
|
de Singe sobre si ha cambiado algo. Es la misma palabra haciendo el mismo
|
|
trabajo, solo que ahora el que pregunta eres tú.
|
|
|
|
La línea del brillo está pidiendo a gritos ser una función con respuesta. Añade
|
|
esta:
|
|
|
|
[source,lua]
|
|
----
|
|
function shadeFor(layer)
|
|
return math.random(60, 90) + layer * LAYER_BRIGHT
|
|
end
|
|
----
|
|
|
|
Todo el cuerpo es un `return`, lo que es normal en una función pequeña. Ahora el
|
|
bucle de dentro puede darle la respuesta directamente a `drawStar` sin un nombre
|
|
por medio:
|
|
|
|
[source,lua]
|
|
----
|
|
drawStar(starX, starY, shadeFor(layer))
|
|
----
|
|
|
|
Una llamada dentro de otra llamada. Lua calcula primero la de dentro, recibe un
|
|
número de `shadeFor` y le pasa ese número a `drawStar` como tercer argumento. Ya
|
|
has escrito algo así sin darte cuenta: el
|
|
`math.floor(drift * layer * LAYER_SPEED)` de la lección cuatro es una llamada
|
|
cuya respuesta se usa en el acto, en medio de una cuenta más grande.
|
|
|
|
[#_two_answers_at_once]
|
|
=== Dos respuestas a la vez
|
|
|
|
Las dos líneas de la posición son más difíciles, porque son dos números que se
|
|
calculan juntos y tendrías que elegir uno para devolverlo. En la mayoría de los
|
|
lenguajes una función puede devolver un valor, y sacarle dos es un engorro. Lua
|
|
te deja devolver tantos como quieras.
|
|
|
|
[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
|
|
----
|
|
|
|
Recoges las respuestas poniendo dos nombres a la izquierda, separados por una
|
|
coma:
|
|
|
|
[source,lua]
|
|
----
|
|
local x, y = starAt(star, layer)
|
|
----
|
|
|
|
El primer valor va al primer nombre, el segundo al segundo. Si pides menos
|
|
nombres de los que devuelve la función, los valores que sobran se tiran:
|
|
`local x = starAt(star, layer)` te da el número horizontal y pierde el vertical,
|
|
sin ninguna queja. Si pides más nombres que valores hay, los que sobran son
|
|
`nil`.
|
|
|
|
Olvídate de la palabra `local` de esos dos bloques un minuto más. Tiene una
|
|
sección propia más adelante, y es la importante.
|
|
|
|
[#_the_star_field_in_one_line]
|
|
=== El campo de estrellas en una línea
|
|
|
|
El bucle de dentro tiene ahora tres líneas cortas, y todas tratan de una capa de
|
|
estrellas. Eso es un trabajo con nombre.
|
|
|
|
[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
|
|
----
|
|
|
|
Y el bucle de fuera también es un trabajo con nombre:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawStars()
|
|
for layer = 1, LAYERS do
|
|
drawLayer(layer)
|
|
end
|
|
end
|
|
----
|
|
|
|
Funciones que llaman a funciones que llaman a funciones, tres niveles, y cada
|
|
una son tres o cuatro líneas que puedes tener en la cabeza de una vez. Ahora
|
|
`onOverlayUpdate` puede decir lo que *es* un fotograma:
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
drift = drift + 1
|
|
overlayClear()
|
|
drawStars()
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Esa es la promesa que hizo la lección cuatro al final, cumplida.
|
|
|
|
[#_put_the_ship_back]
|
|
=== Vuelve a poner la nave
|
|
|
|
Ahora el quinto trabajo, el que habría puesto patas arriba el antiguo
|
|
`onOverlayUpdate`. Recupera la caja que manejabas en la lección tres, esta vez
|
|
como una nave cerca de la parte de abajo de la pantalla que solo se mueve a
|
|
izquierda y derecha.
|
|
|
|
Necesita algunos valores propios, arriba con los demás, y los dos callbacks de
|
|
entrada de la lección tres sin las ramas de arriba y abajo:
|
|
|
|
[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
|
|
----
|
|
|
|
Dibujarla son dos líneas, así que es una función:
|
|
|
|
[source,lua]
|
|
----
|
|
function drawPlayer()
|
|
colorForeground(80, 255, 120)
|
|
overlayBox(playerX, playerY, playerX + PLAYER_WIDTH, playerY + PLAYER_HEIGHT)
|
|
end
|
|
----
|
|
|
|
Moverla es donde la lección da sus frutos. En la lección tres, mantener la caja
|
|
en la pantalla costaba cuatro líneas de `if` y `elseif` por cada dirección. Esa
|
|
idea -- mantener un número entre un mínimo y un máximo -- tiene nombre, y el
|
|
nombre es *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
|
|
----
|
|
|
|
Tres returns, y como mucho ocurre uno, porque `return` no solo devuelve una
|
|
respuesta: detiene la función en seco. No se ejecuta nada de lo que va después.
|
|
Si el valor está por debajo del mínimo, devuelve el mínimo y para. Si está por
|
|
encima del máximo, devuelve el máximo y para. Si no, devuelve lo que ha entrado.
|
|
|
|
[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
|
|
----
|
|
|
|
Una función que termina sin un `return` no devuelve nada, que es `nil`. Eso es
|
|
lo correcto para `drawPlayer` y `movePlayer`, a las que se llama para hacer algo
|
|
y no para responder algo. Es un fallo en una función como `clamp`: quita ese
|
|
último `return value` y todas las posiciones del medio del rango vuelven como
|
|
`nil`.
|
|
|
|
[#_where_names_live]
|
|
=== Dónde viven los nombres
|
|
|
|
Todos los nombres que has creado desde la lección dos han sido *globales*. Un
|
|
nombre global pertenece a todo el programa: `drift`, `playerX` y `LAYERS` los
|
|
puede leer y escribir cualquier función del archivo, en cualquier momento, y
|
|
siguen ahí mientras dure el juego.
|
|
|
|
Eso suena cómodo, y con seis nombres lo es. El problema empieza con sesenta.
|
|
Cada nombre global es un nombre que todo el programa ha acordado reservar, así
|
|
que cuando alguna otra función quiere un sitio donde guardar una posición
|
|
horizontal y también lo llama `starX`, los dos son la misma caja, y cada uno
|
|
escribe encima del número del otro. Cuando un nombre global guarda algo que está
|
|
mal, cualquier cosa del archivo podría haberlo puesto ahí, y averiguar cuál
|
|
significa leerlo entero.
|
|
|
|
La cura es un nombre *local*. Pon la palabra `local` delante de un nombre la
|
|
primera vez que lo uses, y el nombre solo existirá dentro de la `function`, el
|
|
bucle o el `if` donde se escribió.
|
|
|
|
[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
|
|
----
|
|
|
|
Aquí, `x` e `y` no son asunto de nadie más. Otra función puede tener su propia
|
|
`x` y las dos no se encuentran nunca. Cuando la `x` de `starAt` está mal, las
|
|
tres líneas de arriba son las únicas tres que pueden haberlo hecho. Sin la
|
|
palabra `local`, esas dos líneas estarían creando sin hacer ruido dos nombres
|
|
globales más, que es exactamente lo que han sido `starX` y `starY` todo este
|
|
tiempo.
|
|
|
|
Ahora viene la parte que sorprende a todo el mundo. Escribe esta función y pon
|
|
una línea en `onOverlayUpdate` que te enseñe lo que devuelve:
|
|
|
|
[source,lua]
|
|
----
|
|
function countUp()
|
|
local counted = 0
|
|
counted = counted + 1
|
|
return counted
|
|
end
|
|
----
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "Count: " .. countUp())
|
|
----
|
|
|
|
`Count: 1`, y se queda en `Count: 1` mientras dura el juego, aunque a esa
|
|
función se la llama sesenta veces por segundo y suma uno cada vez. Un nombre
|
|
local se crea de nuevo cuando empieza la función y desaparece cuando la función
|
|
termina, así que `counted` nace valiendo `0`, pasa a `1` y muere, sesenta veces
|
|
por segundo, para siempre. Un nombre local no recuerda. Vuelve a quitar las dos
|
|
cosas cuando lo hayas visto.
|
|
|
|
No es un defecto, es justo la idea: a un nombre local no lo puede envenenar la
|
|
llamada anterior, porque de la llamada anterior no queda nada. Cuando sí
|
|
necesitas recordar algo entre llamadas -- `drift` y `playerX` tienen que
|
|
recordar -- el nombre tiene que vivir fuera de todas las funciones. Ponlo arriba
|
|
del archivo con `local` delante:
|
|
|
|
[source,lua]
|
|
----
|
|
local drift = 0
|
|
local playerX = width // 2
|
|
----
|
|
|
|
Un `local` escrito arriba del archivo, fuera de todas las funciones, vive lo
|
|
mismo que el script y lo pueden ver todas las funciones que hay debajo. Esa
|
|
última palabra importa: *debajo*. Un nombre local existe desde su propia línea
|
|
hacia abajo, así que una función escrita encima de la línea que lo crea no lo
|
|
puede ver. Lo mismo pasa con las propias funciones en cuanto las marcas como
|
|
`local`, y por eso el script terminado está en el orden en que está: primero los
|
|
valores, luego tus funciones, y los callbacks al final.
|
|
|
|
Recorre ahora todo el archivo y pon `local` delante de cada nombre que hayas
|
|
creado -- las constantes en mayúsculas, `width` y `height`, `drift`, los cuatro
|
|
valores del jugador y cada una de tus funciones. Tres nombres no lo llevan:
|
|
`onInputPressed`, `onInputReleased` y `onOverlayUpdate` tienen que seguir siendo
|
|
globales, porque Singe los encuentra por su nombre y un nombre local es
|
|
invisible desde fuera del archivo.
|
|
|
|
Sé sincero sobre lo que los nombres locales no arreglan. Si escribes mal un
|
|
nombre al que le estás asignando algo -- `movinLeft = true` en lugar de
|
|
`movingLeft = true` -- Lua no se queja. Crea un nombre global nuevecito con tu
|
|
errata como nombre, le da valor y deja en paz el de verdad. No se mueve nada, no
|
|
sale ningún error, y te pasas diez minutos mirándolo. Los nombres locales no te
|
|
salvarán de esa. Leer el nombre dos veces, sí.
|
|
|
|
[#_the_whole_thing_4]
|
|
=== Todo junto
|
|
|
|
Aquí está el programa terminado, con todas las piezas de la lección.
|
|
|
|
[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
|
|
----
|
|
|
|
Hay dos cosas que vale la pena señalar antes de seguir.
|
|
|
|
`local function drawStar(x, y, shade)` marca la propia función como local. Tus
|
|
funciones son nombres como cualquier otro, y pertenecen solo a este archivo. Los
|
|
tres callbacks siguen siendo globales para que Singe pueda encontrarlos.
|
|
|
|
Y `onOverlayUpdate` son ahora seis líneas que dicen lo que es un fotograma:
|
|
contar la deriva, limpiar la pantalla, mover al jugador, dibujar el cielo,
|
|
dibujar al jugador, listo. Hace cinco trabajos nombrando cinco cosas que hacen
|
|
un trabajo cada una, que es la forma de todos los bucles de juego que escribirás.
|
|
|
|
[#_a_function_is_a_name_for_an_idea]
|
|
=== Una función es el nombre de una idea
|
|
|
|
A `clamp` se la llama una vez. Sacarla no ha ahorrado nada de escritura --
|
|
salieron cuatro líneas, entró una, y en otro sitio apareció una función de nueve
|
|
líneas. Aun así valía la pena.
|
|
|
|
Una función no es solo una forma de no repetirte. Es una forma de darle nombre a
|
|
una idea, para que el código que usa la idea pueda decir el nombre y seguir
|
|
adelante. «Mantén al jugador entre el borde izquierdo y el borde derecho» es un
|
|
pensamiento que tuviste una vez. `clamp(playerX, 0, width - PLAYER_WIDTH - 1)`
|
|
es ese pensamiento, por escrito. Cuatro sentencias `if` son el mismo pensamiento
|
|
desmontado en piezas que tienes que volver a montar en la cabeza cada vez que
|
|
las lees.
|
|
|
|
Cuando estés decidiendo si algo merece ser una función, no te preguntes cuántas
|
|
veces la llamarás. Pregúntate si tiene nombre.
|
|
|
|
[#_try_it_5]
|
|
=== Pruébalo
|
|
|
|
. *Cambia el nombre de un parámetro.* En `drawStar`, cambia `x` e `y` por
|
|
`across` y `down`, en todos los sitios de dentro de la función. No toques nada
|
|
de fuera. Ejecútalo. Averigua por qué no se ha roto nada.
|
|
. *Dibújalos al revés.* Intercambia `drawStars()` y `drawPlayer()` en
|
|
`onOverlayUpdate` y mira la nave. Luego vuelve a dejarlos como estaban. Cada
|
|
una de esas cinco líneas es un trabajo, y su orden en la pantalla es su orden
|
|
en la función.
|
|
. *Usa `clamp` con un tono.* Cambia `shadeFor` por
|
|
`return clamp(math.random(60, 90) + layer * 90, 0, 200)` y mira las tres
|
|
capas. Luego quita el `clamp` y vuelve a mirar: `colorForeground` te estaba
|
|
conteniendo los números sin decir nada todo este tiempo, y ahora puedes ver
|
|
dónde.
|
|
. *Llama a `drawStar` con dos argumentos.* Escribe `drawStar(100, 100)` en algún
|
|
sitio de `drawLayer` y ejecútalo. El error nombra exactamente lo que falta.
|
|
Léelo antes de volver a poner el tercer argumento.
|
|
. *Mueve una variable local.* Coge la línea `local drift = 0` y llévala al final
|
|
del archivo, debajo de todo. Ejecútalo, lee el error y piensa en la palabra
|
|
«debajo» de la sección anterior.
|
|
|
|
[#_break_it_on_purpose_5]
|
|
=== Rómpelo a propósito
|
|
|
|
El orden de un archivo con nombres locales importa, y el error que obtienes
|
|
cuando rompes ese orden no es el que esperas.
|
|
|
|
Coge la función `drawStar` entera y llévala al *final* del script, debajo de
|
|
`onOverlayUpdate`. Todo sigue ahí, escrito igual. Ejecútalo:
|
|
|
|
----
|
|
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'
|
|
----
|
|
|
|
Esto tiene un aspecto distinto del error de la lección uno porque es otro tipo
|
|
de error. El fallo de la lección uno se detectó antes de que se ejecutara el
|
|
script. Este se detectó mientras se ejecutaba, y por eso Singe nombra la función
|
|
en la que estaba a medias, y por eso hay un *traceback* debajo: la lista de
|
|
quién llamó a quién, lo más reciente primero. La línea 50 está dentro de
|
|
`drawLayer`, a la que se llamó desde la línea 57 dentro de `drawStars`, a la que
|
|
se llamó desde la línea 101 dentro de `onOverlayUpdate`. Tus números de línea
|
|
serán otros si tu archivo no coincide exactamente con el de arriba, y puede que
|
|
el motor añada debajo una línea o dos por su cuenta. La palabra `upvalue` es
|
|
como llama Lua a un nombre local que una función ha tomado prestado del archivo
|
|
que la rodea; de momento, sáltatela.
|
|
|
|
Vale la pena aprenderse de memoria la queja en sí. «Attempt to call a nil value»
|
|
significa que has puesto paréntesis después de un nombre que no guardaba nada.
|
|
La palabra `global` delante de `drawStar` es la parte que lo delata: marcaste `drawStar` como local,
|
|
así que cuando Lua leyó `drawLayer` y no encontró ningún nombre local así por
|
|
encima, lo único que le quedaba por probar era un nombre global -- y tampoco hay
|
|
ningún nombre global llamado `drawStar`. Vuelve a poner la función encima del
|
|
código que la llama y el error se va con ella.
|
|
|
|
[#_what_you_learned_5]
|
|
=== Lo que has aprendido
|
|
|
|
* Puedes escribir tus propias funciones, y nada las ejecuta salvo tus propias
|
|
llamadas.
|
|
* Los nombres que hay entre los paréntesis de una función son parámetros, y son
|
|
las copias propias de la función de lo que le pasó quien la llamó.
|
|
* `return` devuelve un valor y detiene la función ahí mismo, y una función que
|
|
termina sin él devuelve `nil`.
|
|
* Una llamada puede ser el argumento de otra llamada, y la de dentro ocurre
|
|
primero.
|
|
* Lua puede devolver más de un valor, y los recoges con varios nombres separados
|
|
por comas.
|
|
* `local` crea un nombre que solo existe donde se escribió.
|
|
* Un nombre local dentro de una función se crea de nuevo en cada llamada y no
|
|
recuerda nada de la anterior.
|
|
* Un nombre local solo se ve por debajo de la línea que lo crea, así que los
|
|
valores y tus funciones van encima de los callbacks que los usan.
|
|
* Los callbacks siguen siendo globales, porque Singe los encuentra por su
|
|
nombre.
|
|
* Vale la pena escribir una función cuando lo que hace tiene nombre, aunque la
|
|
llames una sola vez.
|
|
|
|
[#_next_time_5]
|
|
=== La próxima vez
|
|
|
|
Tus ciento veinte estrellas siguen siendo ciento veinte copias de tres ideas. En
|
|
el programa no hay nada que sea *una estrella*: hay un número y algo de
|
|
aritmética que convierte el número en una posición. No puedes decir «esta está
|
|
más cerca, así que cae más deprisa y brilla más», porque no hay ninguna «esta»
|
|
de la que decirlo, y no puedes dejar que una estrella se caiga por abajo y la
|
|
sustituya otra nueva, porque ahí no hay nada que quitar.
|
|
|
|
La lección seis te da lo que guarda una estrella, y luego una forma de guardar
|
|
ciento veinte y añadir la número ciento veintiuno mientras el juego se ejecuta.
|
|
Es la lección más útil de esta parte del libro.
|