429 lines
16 KiB
Text
429 lines
16 KiB
Text
[#_lesson_4_doing_it_again]
|
|
== Lección 4: Otra vez
|
|
|
|
image::learn/04-repeating.png[La lección terminada, 480]
|
|
|
|
Has dibujado una palabra y una caja. Un juego necesita cien cosas:
|
|
estrellas, ladrillos, balas, las filas de un menú. Escribir cien líneas no es la
|
|
respuesta, y si lo fuera, tendrías que escribir otras cien en cuanto quisieras
|
|
ciento una.
|
|
|
|
Esta lección es la respuesta. Al final de ella, un campo de estrellas irá
|
|
bajando lentamente por tu ventana, y todas y cada una las dibujarán las mismas
|
|
cinco líneas de código.
|
|
|
|
[#_start_a_new_folder_3]
|
|
=== Empieza una carpeta nueva
|
|
|
|
Una carpeta llamada `repeating`, dentro un archivo llamado `repeating.singe`, y
|
|
`Singe -R repeating` ejecutándose en una terminal.
|
|
|
|
[#_forty_dots]
|
|
=== Cuarenta puntos
|
|
|
|
Escribe esto y ejecútalo.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
colorForeground(255, 255, 255)
|
|
|
|
for star = 1, 40 do
|
|
overlayPlot(star * 8, 100)
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Una fila de cuarenta puntos blancos que cruza el centro de la ventana.
|
|
|
|
`overlayPlot` es la llamada de dibujo más sencilla que hay: pone un píxel en el
|
|
color de primer plano actual, y es justo lo que pide una estrella. Todo lo
|
|
interesante aquí son las tres líneas que la rodean.
|
|
|
|
[source,lua]
|
|
----
|
|
for star = 1, 40 do
|
|
----
|
|
|
|
Eso dice: haz lo que sigue cuarenta veces, y en cada vuelta, `star` guarda un
|
|
número distinto -- uno la primera vez, dos la segunda, cuarenta la última. Los
|
|
dos extremos cuentan, así que `1, 40` son de verdad cuarenta vueltas y no
|
|
treinta y nueve. Esta forma es un *bucle*, y este en concreto es un *bucle for
|
|
numérico*.
|
|
|
|
`star` es un nombre que te inventas, igual que te inventaste `frames` en la
|
|
lección dos, y el bucle lo rellena por ti. No le das valor tú, y no deberías
|
|
cambiarlo dentro del bucle. Solo existe entre el `for` y su `end`; pide su valor
|
|
después y obtendrás `nil`.
|
|
|
|
El `do` es obligatorio y es fácil olvidarlo. Si lo quitas, Lua dice
|
|
`'do' expected near`, seguido de lo que haya encontrado en su lugar, que será lo
|
|
primero de la línea siguiente.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPlot(star * 8, 100)
|
|
----
|
|
|
|
El cuerpo. Esto es lo que se ejecuta cuarenta veces, y es la misma línea cada
|
|
vez -- lo que cambia es `star`, así que el primer punto cae en una x de 8, el
|
|
segundo en 16 y el último en 320. Un bucle solo sirve de algo cuando el cuerpo
|
|
hace algo con el número del bucle, y lo primero que buscarás siempre es hacer
|
|
cuentas con él.
|
|
|
|
El cuerpo va sangrado un paso más que el `for`, por el mismo motivo por el que
|
|
has sangrado todo lo demás hasta ahora.
|
|
|
|
[source,lua]
|
|
----
|
|
end
|
|
----
|
|
|
|
Cierra el bucle. Ya conoces tres cosas que necesitan un `end`: `function`, `if`
|
|
y `for`. Todas funcionan igual, se anidan unas dentro de otras, y la sangría es
|
|
cómo llevas la cuenta de qué `end` va con qué.
|
|
|
|
[#_counting_by_more_than_one]
|
|
=== Contar de más de uno en uno
|
|
|
|
El bucle cuenta de uno en uno salvo que le digas otra cosa. Añade un tercer
|
|
número y contará de ese en ese.
|
|
|
|
[source,lua]
|
|
----
|
|
for x = 0, overlayGetWidth() - 1, 10 do
|
|
overlayPlot(x, 100)
|
|
end
|
|
----
|
|
|
|
Léelo como *desde, hasta, de cuánto en cuánto*. Este va 0, 10, 20, y así
|
|
sucesivamente, y se para en el último píxel de la superficie o antes, lo que
|
|
dibuja una línea de puntos de lado a lado sea cual sea el tamaño de la ventana.
|
|
Es la misma fila de puntos de antes, descrita según dónde van y no según
|
|
cuántos hay.
|
|
|
|
El paso puede ser negativo, y entonces cuenta hacia atrás.
|
|
|
|
[source,lua]
|
|
----
|
|
for countdown = 10, 1, -1 do
|
|
----
|
|
|
|
Diez, nueve, ocho, hasta uno. Con un paso negativo el segundo número es el suelo
|
|
en vez del techo, que es lo que esperarías, y equivocarse de signo da un bucle
|
|
que no se ejecuta nunca: `for i = 10, 1 do` sin paso cuenta hacia arriba desde
|
|
diez hacia uno, ya ha pasado el final antes de empezar y no hace nada. Ningún
|
|
error, ninguna salida. Si un bucle tuyo parece no ejecutarse nunca, comprueba
|
|
primero la dirección.
|
|
|
|
El paso también puede ser una fracción, aunque lo querrás menos a menudo de lo
|
|
que crees.
|
|
|
|
[#_one_loop_inside_another]
|
|
=== Un bucle dentro de otro
|
|
|
|
El cuerpo de un bucle es código normal, y el código normal puede contener un
|
|
bucle.
|
|
|
|
[source,lua]
|
|
----
|
|
for row = 0, overlayGetHeight() - 1, 20 do
|
|
for column = 0, overlayGetWidth() - 1, 20 do
|
|
overlayPlot(column, row)
|
|
end
|
|
end
|
|
----
|
|
|
|
Eso dibuja una cuadrícula de puntos por toda la ventana. Ponlo en tu
|
|
`onOverlayUpdate` en lugar de la fila y míralo, porque vale la pena tener claro
|
|
cómo se ejecuta ahora, y no en la lección once, cuando algo dependa de ello.
|
|
|
|
El bucle de fuera se ejecuta una vez con `row` a 0. Dentro de eso, todo el bucle
|
|
de dentro se ejecuta de principio a fin -- dieciocho columnas, dieciocho puntos,
|
|
una fila de la cuadrícula. Solo cuando el bucle de dentro ha terminado pasa el
|
|
de fuera a `row` a 20 y vuelve a ejecutar entero el bucle de dentro.
|
|
|
|
Así que el cuerpo del bucle de dentro se ejecuta dieciocho por doce veces,
|
|
que son doscientas dieciséis, en doce vueltas del de fuera. Esa
|
|
multiplicación es la razón de que los bucles anidados sean la forma de llenar
|
|
un rectángulo de cualquier cosa, y también la razón de que debas echar un
|
|
vistazo a los números antes de anidar tres.
|
|
|
|
Fíjate en que la posición de cada punto necesita las dos variables del bucle:
|
|
`column` para lo horizontal y `row` para lo vertical. Ese es el patrón. Si el
|
|
cuerpo de dentro solo usa la variable de dentro, el bucle de dentro está
|
|
dibujando lo mismo una y otra vez en el mismo sitio, y has escrito una forma
|
|
lenta de hacerlo una vez.
|
|
|
|
[#_when_you_do_not_know_how_many]
|
|
=== Cuando no sabes cuántas veces
|
|
|
|
Un bucle `for` necesita saber cuántas veces antes de empezar. A veces no lo
|
|
sabes, y entonces quieres el otro tipo.
|
|
|
|
[source,lua]
|
|
----
|
|
x = 0
|
|
while x < overlayGetWidth() do
|
|
overlayPlot(x, 100)
|
|
x = x + 10
|
|
end
|
|
----
|
|
|
|
Un bucle `while` comprueba una pregunta antes de cada vuelta, exactamente la
|
|
pregunta que haría un `if`, y sigue mientras la respuesta sea verdadera. Cuando
|
|
la respuesta es falsa, se para y continúa con la línea que va después del
|
|
`end`. Si la respuesta es falsa ya la primera vez, el cuerpo no se ejecuta
|
|
nunca.
|
|
|
|
Ese ejemplo dibuja la misma línea de puntos que el `for` con un paso de diez, y
|
|
necesita tres líneas para hacer lo que `for` hacía en una, así que usa `for`
|
|
para eso. Esta es la diferencia que decide:
|
|
|
|
* Usa `for` cuando sabes el número de vueltas antes de empezar. Cuarenta
|
|
estrellas. Uno de cada diez píxeles a lo ancho. Cada fila de una cuadrícula.
|
|
* Usa `while` cuando estás esperando a que algo se haga verdadero y no puedes
|
|
decir de antemano cuánto tardará. Repartir cartas hasta que se acabe la
|
|
baraja. Seguir preguntando hasta que el jugador escriba algo válido. Recorrer
|
|
una lista hasta encontrar lo que buscabas.
|
|
|
|
Un `while` tiene tres partes y tienes que escribir las tres tú: preparar la
|
|
variable antes del bucle, comprobarla en el `while` y cambiarla dentro del
|
|
cuerpo. El bucle `for` hace las tres por ti, y por eso es el que debes usar
|
|
cuando encaja. Olvida la tercera parte en un `while` y la pregunta no deja nunca
|
|
de ser verdadera, que es lo que harás a propósito al final de esta lección.
|
|
|
|
[#_a_star_made_out_of_its_own_number]
|
|
=== Una estrella hecha de su propio número
|
|
|
|
Ahora el campo de estrellas, y un problema que da forma al resto de la primera
|
|
parte.
|
|
|
|
Cada estrella necesita su propia x y su propia y, y no tienes forma de guardar
|
|
cien variables separadas -- escribir `star1X`, `star2X` y así hasta cien es peor
|
|
que escribir las cien líneas que querías evitar. Así que no las guardes. Calcula
|
|
cada una a partir de lo único que hace a una estrella distinta de sus vecinas:
|
|
su número.
|
|
|
|
[source,lua]
|
|
----
|
|
starX = (star * 37) % width
|
|
----
|
|
|
|
El `%` es el último de los operadores aritméticos y es el *resto*: lo que sobra
|
|
después de dividir. Quince entre cuatro es tres y sobran tres, así que `15 % 4`
|
|
es 3. Su propiedad útil es que el resultado nunca puede ser tan grande como el
|
|
número de la derecha, así que el resto entre `width` siempre es una posición en
|
|
algún sitio de la pantalla, por mucho que crezca el lado izquierdo.
|
|
|
|
La estrella 1 va a 37, la 5 a 185, la 10 a 10 -- 370 ha pasado de 360, ha dado
|
|
la vuelta y ha vuelto a aparecer por la izquierda. Se elige treinta y siete
|
|
porque no comparte ningún factor con 360, que es lo que impide que las estrellas
|
|
caigan en rayas ordenadas; prueba con 36 y verás el problema enseguida.
|
|
|
|
La deriva hacia abajo es el mismo truco con un número que crece con el tiempo.
|
|
|
|
[source,lua]
|
|
----
|
|
drift = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
drift = drift + 1
|
|
...
|
|
starY = (star * 61 + math.floor(drift * layer * 0.5)) % height
|
|
----
|
|
|
|
`drift` vive fuera de la función y sube en cada fotograma, exactamente igual que
|
|
`frames` en la lección dos. Sumarlo a la y de cada estrella mueve todo el campo
|
|
hacia abajo a la vez, y el `%` devuelve arriba una estrella que se cae por
|
|
abajo, para siempre, gratis.
|
|
|
|
`math.floor` tira la fracción de un número y devuelve la parte entera que queda
|
|
por debajo: `math.floor(7.9)` es 7. La lección dos señaló que Singe hace eso por
|
|
ti cuando dibuja, y es verdad, pero hacerlo tú significa que el número con el
|
|
que trabajas es el número que aparece en pantalla. Eso importará en la lección
|
|
siete, cuando dos cosas tengan que ponerse de acuerdo sobre si se están tocando.
|
|
|
|
Fíjate en que ese `layer` es una segunda variable de bucle. Las estrellas se
|
|
dibujan con un bucle dentro de otro, y el de fuera no es una fila de una
|
|
cuadrícula sino una *distancia*: tres capas de estrellas, cada una con su propia
|
|
velocidad y su propio brillo, que es lo que hace que un campo plano de puntos
|
|
parezca tener profundidad.
|
|
|
|
[#_chance]
|
|
=== El azar
|
|
|
|
Las estrellas titilan. Titilar significa un brillo un poco distinto en cada
|
|
fotograma, y «un poco distinto» es `math.random`.
|
|
|
|
[source,lua]
|
|
----
|
|
shade = math.random(60, 90) + layer * 55
|
|
colorForeground(shade, shade, shade)
|
|
----
|
|
|
|
`math.random(60, 90)` devuelve un número entero de 60 a 90, los dos incluidos,
|
|
elegido de nuevo cada vez que la llamas. Llámala sin ningún argumento y
|
|
obtendrás en cambio una fracción entre 0 y 1, que es la forma que debes usar
|
|
cuando quieres que algo pase una vez de cada cinco. No tienes que preparar el
|
|
generador; Lua lo hace cuando arranca Singe.
|
|
|
|
Sumar `layer * 55` saca las capas cercanas del rango oscuro, así que las
|
|
estrellas lejanas siguen débiles y las cercanas son casi blancas. El mismo rojo,
|
|
verde y azul, como explicaba la lección tres, es un tono de gris.
|
|
|
|
Como `shade` se elige otra vez en cada fotograma para cada estrella, cada
|
|
estrella parpadea por su cuenta, lo que a sesenta fotogramas por segundo se ve
|
|
como un titileo y no como un fallo.
|
|
|
|
[#_the_whole_thing_3]
|
|
=== Todo junto
|
|
|
|
[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
|
|
----
|
|
|
|
Ciento veinte estrellas, dibujadas por cinco líneas, y el archivo es más corto
|
|
que el de la lección tres.
|
|
|
|
También es hasta donde llega este enfoque, y vale la pena ser sincero sobre el
|
|
porqué mientras lo tienes delante. Cada estrella de ahí es una fórmula. De una
|
|
estrella no se *recuerda* nada: no puedes hacer una más brillante que su vecina
|
|
y que se quede así, no puedes derribar una del cielo y no puedes hacer que el
|
|
jugador recoja una, porque no hay ningún sitio donde apuntar que le ha pasado
|
|
algo. La estrella diecisiete no es una cosa. Es un número que pasa un momento
|
|
por una variable llamada `star`.
|
|
|
|
En cuanto quieres cien cosas que recuerden cada una algo propio, necesitas un
|
|
sitio donde poner cien grupos de datos. Eso es la lección seis, y el campo de
|
|
estrellas es la razón de que exista.
|
|
|
|
[#_try_it_4]
|
|
=== Pruébalo
|
|
|
|
. *Más y menos.* Prueba `STARS_PER_LAYER = 200`, luego `4`. Luego
|
|
`LAYERS = 8` y fíjate en lo que hace `layer * LAYER_BRIGHT` con `shade` cuando
|
|
el número de capa se hace grande. Los números que admite `colorForeground`
|
|
llegan hasta 255.
|
|
. *Cambia el espaciado.* Pon `ACROSS_STEP` a 36 y ejecútalo. Las estrellas caen
|
|
en rayas, porque 36 cabe en 360 exactamente diez veces. Vuelve a ponerlo a 37
|
|
y prueba 71 y 90.
|
|
. *Quita el titileo.* Cambia la línea de `math.random` por un simple
|
|
`shade = layer * 70` y compara. Decide cuál te gusta más; no hay respuesta
|
|
correcta, y darte cuenta de que tienes una preferencia es parte del trabajo.
|
|
. *Haz que caigan hacia arriba.* Un carácter.
|
|
. *Dibuja también la cuadrícula.* Vuelve a poner el bucle de la cuadrícula
|
|
anidada de antes, después de las estrellas, en un color oscuro. Dos bucles
|
|
anidados en una función, dibujando dos cosas distintas, y el segundo no
|
|
molesta al primero.
|
|
|
|
[#_break_it_on_purpose_4]
|
|
=== Rómpelo a propósito
|
|
|
|
Hasta ahora, todos los errores han sido un mensaje. Este no, y precisamente por
|
|
eso deberías conocerlo cuando lo estás esperando.
|
|
|
|
Pon esto en tu `onOverlayUpdate`, justo debajo de `overlayClear()`, ejecútalo y
|
|
lee el párrafo siguiente antes de hacer nada más.
|
|
|
|
[source,lua]
|
|
----
|
|
x = 0
|
|
while x < width do
|
|
overlayPlot(x, 100)
|
|
end
|
|
----
|
|
|
|
Falta la línea que hace avanzar `x`, así que `x` se queda en cero, así que
|
|
`x < width` no deja nunca de ser verdadero. El bucle da vueltas para siempre,
|
|
pintando el mismo píxel, y está dentro de `onOverlayUpdate`, así que
|
|
`onOverlayUpdate` no vuelve nunca. Singe está esperando a tu función. No puede
|
|
dibujar, no puede leer el teclado y no puede salir.
|
|
|
|
Lo que ves es una ventana que deja de moverse. Esc no hace nada. Q no hace
|
|
nada. Tampoco se imprime nada.
|
|
|
|
Ve a la terminal desde la que arrancaste Singe y pulsa *Ctrl* y *C* a la vez.
|
|
Singe detiene tu bucle y te dice dónde estaba:
|
|
|
|
----
|
|
Stopped: the script did not return, and Singe was asked to quit.
|
|
stack traceback:
|
|
repeating/repeating.singe:20: in function 'onOverlayUpdate'
|
|
----
|
|
|
|
Puede que lo diga dos veces, con unos segundos de diferencia, antes de salir.
|
|
Con una basta. La línea 20 es el `end` de tu `while`, que es donde el bucle
|
|
estaba dando vueltas cuando lo detuviste. Quita el bucle, guarda y vuelve a
|
|
arrancar Singe.
|
|
|
|
Eso es un informe, no un error. Nada se quejó hasta que lo pediste, porque, para
|
|
el ordenador, no ha ido nada mal. Pediste un bucle que se ejecutara mientras `x`
|
|
fuera menor que el ancho, y eso es exactamente lo que tienes. Este es el único
|
|
tipo de error en el que los mensajes de error no te pueden ayudar, y la cura es
|
|
una costumbre: cada vez que escribas un `while`, escribe la línea que cambia la
|
|
variable antes de escribir nada más en el cuerpo.
|
|
|
|
[#_what_you_learned_4]
|
|
=== Lo que has aprendido
|
|
|
|
* `for name = first, last do ... end` ejecuta su cuerpo una vez por cada número
|
|
desde el primero hasta el último, los dos incluidos.
|
|
* Un tercer número es el paso, y puede ser negativo para contar hacia atrás.
|
|
* La variable del bucle solo existe dentro del bucle, y el cuerpo debería
|
|
usarla.
|
|
* `for` necesita un `do`, y todo bucle necesita un `end`.
|
|
* Un bucle dentro de otro ejecuta el bucle de dentro entero en cada vuelta del
|
|
de fuera, y así es como llenas un rectángulo.
|
|
* `while question do ... end` repite mientras la pregunta sea verdadera, y
|
|
tienes que cambiar algo en el cuerpo tú mismo.
|
|
* Usa `for` cuando sabes cuántas veces, y `while` cuando estás esperando a que
|
|
algo se haga verdadero.
|
|
* `%` es el resto, y mantiene un número que crece dentro de un rango.
|
|
* `math.random(a, b)` da un número entero de a a b; `math.floor` tira la
|
|
fracción.
|
|
* Un bucle que no termina nunca congela todo el juego sin ningún mensaje de
|
|
error, y *Ctrl* y *C* en la terminal es como sales.
|
|
|
|
[#_next_time_4]
|
|
=== La próxima vez
|
|
|
|
Vuelve a mirar ese `onOverlayUpdate`. Dibuja estrellas, calcula posiciones,
|
|
elige colores, y empieza a ser una función que hace cuatro trabajos en lugar de
|
|
uno. En la lección cinco escribes tus propias funciones, les das argumentos
|
|
igual que los reciben las funciones de Singe y les sacas respuestas -- y el
|
|
campo de estrellas se convierte en una línea que dice `drawStars()`.
|