426 lines
16 KiB
Text
426 lines
16 KiB
Text
[#_lesson_2_numbers_that_change]
|
|
== Lección 2: Números que cambian
|
|
|
|
image::learn/02-numbers.png[La lección terminada, 480]
|
|
|
|
En la lección uno pusiste una palabra en la pantalla y allí se quedó. Al final
|
|
de esta lección, esa misma palabra irá deslizándose por la ventana, dará la
|
|
vuelta cuando llegue a un borde, y lo hará gracias a cuatro números que el
|
|
programa cambia sesenta veces por segundo.
|
|
|
|
Por el camino conocerás la idea que hace tropezar a más principiantes que
|
|
ninguna otra: dónde pones algo decide cuánto tiempo vive.
|
|
|
|
[#_start_a_new_folder]
|
|
=== Empieza una carpeta nueva
|
|
|
|
Crea una carpeta llamada `numbers` junto a tu carpeta `hello`, y dentro de ella
|
|
un archivo llamado `numbers.singe`. Escribe en él el programa de la lección uno,
|
|
o copia `hello.singe` y cámbiale el nombre -- cualquiera de las dos cosas vale,
|
|
y escribirlo otra vez no es tiempo perdido.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Ejecútalo con `Singe -R numbers` y déjalo funcionando. El `-R` del final de la
|
|
lección uno significa que no tendrás que volver a arrancarlo durante el resto de
|
|
esta lección; guarda el archivo y la ventana se pone al día sola.
|
|
|
|
[#_count_the_frames]
|
|
=== Cuenta los fotogramas
|
|
|
|
La lección uno decía que Singe llama a `onOverlayUpdate` unas sesenta veces por
|
|
segundo. Es fácil de decir y difícil de creer, así que cuéntalas.
|
|
|
|
[source,lua]
|
|
----
|
|
function onOverlayUpdate()
|
|
frames = 0
|
|
frames = frames + 1
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Guárdalo. En la esquina superior izquierda aparece `frame 1`, y se queda en
|
|
`frame 1` para siempre.
|
|
|
|
Es decepcionante, y también es lo más útil de esta lección, así que no lo
|
|
arregles todavía. Mira lo que has escrito y piensa en el orden de las líneas,
|
|
que según la lección uno era lo más importante de un programa.
|
|
|
|
Singe llama a `onOverlayUpdate`. La primera línea pone `frames` a cero. La
|
|
segunda le suma uno, y queda en uno. Lo imprimes, devuelves la respuesta y la
|
|
función se acaba. Luego Singe la llama otra vez, y lo primero que pasa es que
|
|
`frames` vuelve a ponerse a cero.
|
|
|
|
No estás contando fotogramas. Estás contando hasta uno, sesenta veces por
|
|
segundo.
|
|
|
|
[#_move_the_line_out]
|
|
=== Saca la línea
|
|
|
|
Saca la línea `frames = 0` de la función y ponla encima, ella sola, antes de la
|
|
palabra `function`.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
frames = frames + 1
|
|
overlayClear()
|
|
overlayPrint(2, 2, "Hello!")
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Guárdalo. El número sube, deprisa, y no para.
|
|
|
|
No ha cambiado nada más. La única diferencia es *dónde* está la línea, y esa es
|
|
toda la idea. Las líneas que están dentro de una función ocurren cada vez que se
|
|
llama a la función. Las líneas que están fuera de todas las funciones ocurren
|
|
una vez, cuando Singe lee tu archivo, antes de que empiece el juego.
|
|
|
|
`frames = 0` tenía que ocurrir una vez. Estaba ocurriendo sesenta veces por
|
|
segundo, y borraba el trabajo de la línea de debajo.
|
|
|
|
Todo lo que se mueva que escribas tendrá esta forma. Prepara el número fuera,
|
|
cámbialo dentro.
|
|
|
|
[#_what_just_happened_2]
|
|
=== Qué acaba de pasar
|
|
|
|
Ahí dentro hay tres piezas de código nuevas. Aquí están por separado.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = 0
|
|
----
|
|
|
|
Esto se inventa un nombre, `frames`, y pone el número cero detrás de él. Un
|
|
nombre con un valor detrás es una *variable*, y se llama así porque el valor
|
|
puede variar -- cambiar mientras el programa se ejecuta.
|
|
|
|
El `=` no es el signo igual del colegio. No dice que `frames` sea igual a cero;
|
|
dice *mete cero en `frames`*, y siempre funciona en esa dirección, de derecha a
|
|
izquierda. Los programadores lo llaman *asignación*, y vale la pena decir la
|
|
línea en voz alta de esa forma mientras la escribes: «frames recibe cero».
|
|
|
|
El nombre lo eliges tú. Puede ser casi cualquier cosa que empiece por una letra
|
|
y no tenga espacios, y debería decir qué es la cosa. `frames` es un buen nombre.
|
|
`f` es uno malo, y dentro de una semana no te lo agradecerás.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = frames + 1
|
|
----
|
|
|
|
Esta es la línea que hace que la gente se pare y se quede mirando, porque como
|
|
afirmación aritmética no tiene sentido: nada es igual a sí mismo más uno.
|
|
|
|
No es una afirmación aritmética. Recuerda que `=` mete el lado derecho en el
|
|
nombre de la izquierda, y léela en ese orden, primero la derecha. Calcula
|
|
`frames + 1` con lo que haya en `frames` ahora mismo. Luego vuelve a meter el
|
|
resultado en `frames`. Si `frames` tenía seis, el lado derecho da siete, y el
|
|
siete va a `frames`.
|
|
|
|
Sumarle uno a una variable así es algo que escribirás miles de veces.
|
|
|
|
El `+` es lo que esperas, y también los otros tres: `-` resta, `*` multiplica y
|
|
`/` divide. No hay ninguna tecla `x` para multiplicar, y por eso el asterisco
|
|
hace ese trabajo.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
----
|
|
|
|
`overlayPrint` quiere texto, y `frames` es un número, no texto. Los dos puntos
|
|
unen los dos y entregan el resultado como una sola cadena: los cinco
|
|
caracteres `frame`, un espacio, y luego tantas cifras como necesite el número.
|
|
|
|
Esos dos puntos son el operador de unión, y vale la pena conocer su nombre de
|
|
verdad porque los mensajes de error lo usan: unir cadenas se llama
|
|
*concatenación*.
|
|
|
|
Fíjate en el espacio dentro de las comillas, antes de la de cierre. Sin él te
|
|
sale `frame6`. Lua une exactamente lo que le das y no añade nada por su cuenta.
|
|
|
|
Lua convierte un número en texto por ti cuando lo unes así a una cadena, y por
|
|
eso no has tenido que hacer nada especial. No lo hace con todo. Si alguna vez
|
|
intentas unir algo que no es ni texto ni número, Lua se detiene con un error, y
|
|
la forma de evitarlo es `tostring`, que convierte cualquier cosa en texto:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " .. tostring(frames))
|
|
----
|
|
|
|
Esa línea hace exactamente lo mismo que la anterior. No necesitas `tostring`
|
|
para los números. Lo necesitarás en la lección tres.
|
|
|
|
[#_make_it_move]
|
|
=== Haz que se mueva
|
|
|
|
Ya tienes todo lo que necesitas para mover la palabra. La palabra se dibuja
|
|
donde tú digas, tienes una forma de guardar un número de un fotograma a otro, y
|
|
tienes una forma de cambiarlo.
|
|
|
|
Dale a la columna su propia variable fuera de la función, y súmale algo dentro.
|
|
|
|
[source,lua]
|
|
----
|
|
frames = 0
|
|
wordX = 0
|
|
|
|
|
|
function onOverlayUpdate()
|
|
frames = frames + 1
|
|
wordX = wordX + 1
|
|
overlayClear()
|
|
overlayPrint(wordX, 2, "Hello!")
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
Guárdalo. La palabra sale disparada por el lado derecho y no vuelve nunca.
|
|
|
|
Se movía una columna en cada fotograma, sesenta veces por segundo, así que cruzó
|
|
toda la ventana en más o menos un segundo. Hazla más lenta sumando menos de una
|
|
columna entera:
|
|
|
|
[source,lua]
|
|
----
|
|
wordX = wordX + 0.25
|
|
----
|
|
|
|
Ahora tarda cuatro fotogramas en moverse una columna, y la palabra avanza
|
|
despacito. `wordX` casi siempre es un número como `12.75`, que no es ninguna
|
|
columna, y Singe lo resuelve tirando todo lo que va después del punto cuando
|
|
dibuja: `12.75` se dibuja en la columna doce. Guardar la fracción en la variable
|
|
es justo lo que te permite moverte más despacio que una columna por fotograma.
|
|
El número es más fino que la pantalla.
|
|
|
|
[#_where_is_the_edge]
|
|
=== ¿Dónde está el borde?
|
|
|
|
La palabra sigue marchándose. Para darle la vuelta tienes que saber dónde está
|
|
el borde derecho, y podrías averiguarlo contando: un juego sin vídeo recibe por
|
|
defecto un mundo de 720 por 480, el overlay en el que dibujas es la mitad, 360
|
|
por 240, y las letras de la fuente de consola integrada miden seis píxeles de
|
|
ancho, así que hay sesenta columnas.
|
|
|
|
No lo hagas. Pregúntale al motor.
|
|
|
|
[source,lua]
|
|
----
|
|
lastColumn = overlayGetWidth() // CELL_WIDTH - string.len(word)
|
|
----
|
|
|
|
`overlayGetWidth` y `overlayGetHeight` son otras dos funciones que proporciona
|
|
Singe, como `overlayPrint`. No reciben nada y devuelven un número: el ancho y el
|
|
alto de la superficie de dibujo. En la lección uno viste a `return` devolver un
|
|
valor, y este es el otro extremo -- una función que llamas puede darte algo, y
|
|
aquí estás haciendo cuentas con lo que te ha dado.
|
|
|
|
Los números que te dan están en píxeles, y `overlayPrint` cuenta en celdas de
|
|
carácter, así que tienes que dividir. `overlayGetFontWidth` te dice cuántos
|
|
píxeles de ancho tiene una celda, y `overlayGetFontHeight` cuántos de alto, así
|
|
que nunca tienes que saberte la respuesta. Las dos barras significan *divide y
|
|
tira la fracción*, porque un tercio de celda no es un sitio donde se pueda
|
|
imprimir. La barra simple de siempre te daría una fracción, y una fracción de
|
|
celda tampoco es un sitio.
|
|
|
|
`string.len` cuenta los caracteres de una cadena, y está ahí para que la palabra
|
|
dé la vuelta cuando su *última* letra llegue al borde, y no la primera. Pruébalo
|
|
sin él y verás seis caracteres deslizarse hacia la nada.
|
|
|
|
Las mayúsculas de `CELL_WIDTH`, que guarda lo que te da `overlayGetFontWidth`,
|
|
son una costumbre, no una regla. A Lua le da igual, pero un nombre en mayúsculas es como los programadores dicen «este es un
|
|
número que pongo una vez arriba y no vuelvo a tocar», y es mucho mejor que tener
|
|
un `6` suelto a mitad de un archivo que nadie sabe explicar un mes después.
|
|
|
|
[#_turn_it_round]
|
|
=== Dale la vuelta
|
|
|
|
Moverse en el otro sentido no cuesta nada: en lugar de sumar `0.25` en cada
|
|
fotograma, suma `-0.25`. Así que guarda la cantidad en una variable propia y
|
|
cámbiale el signo cuando la palabra llegue a un borde.
|
|
|
|
[source,lua]
|
|
----
|
|
if wordX > lastColumn then
|
|
wordX = lastColumn
|
|
speedX = -speedX
|
|
end
|
|
----
|
|
|
|
Eso es una decisión, y las decisiones son toda la lección tres, así que
|
|
créetelo sin más durante una lección. Léelo en voz alta: *si* `wordX` ha pasado
|
|
de `lastColumn`, *entonces* haz las dos líneas sangradas; si no, sáltatelas. El
|
|
`end` lo cierra, igual que `end` cierra una función.
|
|
|
|
La primera línea clava la palabra justo en el borde, para que una palabra rápida
|
|
no se lo salte. La segunda es el cambio de signo. `-speedX` es menos lo que
|
|
tenga `speedX`, así que si tenía `0.25` ahora tiene `-0.25`, y sumar `-0.25` en
|
|
cada fotograma lleva la palabra de vuelta por donde vino. Cuando llega al borde
|
|
izquierdo, el `if` correspondiente le vuelve a cambiar el signo, y vuelve a ser
|
|
`0.25`.
|
|
|
|
Dos más como esos para los bordes de arriba y de abajo, y la palabra rebota por
|
|
toda la ventana.
|
|
|
|
[#_the_whole_thing]
|
|
=== Todo junto
|
|
|
|
[source,lua]
|
|
----
|
|
CELL_WIDTH = overlayGetFontWidth()
|
|
CELL_HEIGHT = overlayGetFontHeight()
|
|
TOP_ROW = 2
|
|
|
|
word = "Hello!"
|
|
wordX = 0
|
|
wordY = TOP_ROW
|
|
speedX = 0.25
|
|
speedY = 0.125
|
|
frames = 0
|
|
|
|
lastColumn = overlayGetWidth() // CELL_WIDTH - string.len(word)
|
|
lastRow = overlayGetHeight() // CELL_HEIGHT - 1
|
|
|
|
|
|
function onOverlayUpdate()
|
|
frames = frames + 1
|
|
wordX = wordX + speedX
|
|
wordY = wordY + speedY
|
|
|
|
if wordX < 0 then
|
|
wordX = 0
|
|
speedX = -speedX
|
|
end
|
|
|
|
if wordX > lastColumn then
|
|
wordX = lastColumn
|
|
speedX = -speedX
|
|
end
|
|
|
|
if wordY < TOP_ROW then
|
|
wordY = TOP_ROW
|
|
speedY = -speedY
|
|
end
|
|
|
|
if wordY > lastRow then
|
|
wordY = lastRow
|
|
speedY = -speedY
|
|
end
|
|
|
|
overlayClear()
|
|
overlayPrint(0, 0, "frame " .. frames)
|
|
overlayPrint(wordX, wordY, word)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
----
|
|
|
|
`TOP_ROW` mantiene la palabra fuera de las dos líneas de arriba, para que nunca
|
|
garabatee encima del contador de fotogramas. `lastRow` lleva un `- 1` en lugar
|
|
de un `string.len`, porque una palabra tiene muchos caracteres de ancho pero
|
|
siempre una sola línea de alto.
|
|
|
|
Fíjate en que toda la mitad de arriba del archivo se ejecuta una vez y toda la
|
|
mitad de abajo sesenta veces por segundo, y en que puedes distinguir cuál es
|
|
cuál mirando la sangría. Para eso eran los espacios.
|
|
|
|
[#_try_it_2]
|
|
=== Pruébalo
|
|
|
|
. *Cambia las velocidades.* Prueba `speedX = 1` y `speedY = 1`. Luego prueba
|
|
`0.05` para las dos. Luego haz una mucho mayor que la otra.
|
|
. *Cambia la palabra.* Pon una palabra mucho más larga entre las comillas -- lo
|
|
bastante larga para llenar un tercio de la ventana -- y fíjate en que el
|
|
rebote de la derecha sigue ocurriendo en el sitio correcto. Es `string.len`
|
|
haciendo su trabajo.
|
|
. *Quita la limpieza.* Borra la línea `overlayClear()`. La lección uno prometió
|
|
que esto importaría algún día, y hoy es ese día: la palabra deja un rastro por
|
|
toda la ventana y no borra nada, porque dibujar siempre ha dibujado encima de
|
|
lo que había. Vuelve a ponerla.
|
|
. *Que no vaya de lado.* Pon `speedX = 0` y ejecútalo. Averigua por qué ahora la
|
|
palabra no sale nunca de la columna cero, y por qué el `if` del borde
|
|
izquierdo no salta nunca.
|
|
. *Rompe el contador de fotogramas a propósito.* Vuelve a meter la línea
|
|
`frames = 0` dentro de la función, encima de `frames = frames + 1`. Mira cómo
|
|
el contador se queda en uno mientras la palabra sigue moviéndose
|
|
perfectamente. Luego averigua por qué a la palabra no le afecta.
|
|
|
|
[#_break_it_on_purpose_2]
|
|
=== Rómpelo a propósito
|
|
|
|
Cambia los dos puntos de la línea del contador de fotogramas por un signo más,
|
|
para que quede así:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "frame " + frames)
|
|
----
|
|
|
|
Guárdalo. La ventana se cierra, Singe se detiene y la terminal dice:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': numbers/numbers.singe:42: attempt to add a 'string' with a 'number'
|
|
----
|
|
|
|
seguido de varias líneas que empiezan por `stack traceback:`, que de momento
|
|
puedes ignorar.
|
|
|
|
Léelo como te enseñó la lección uno: el archivo, la línea y luego la queja. La
|
|
línea 42 es la que has cambiado. La queja dice que a Lua le pidieron *sumar* una
|
|
cadena a un número, y sumar es cosa de números. `+` y `..` se parecen sobre el
|
|
papel y hacen trabajos totalmente distintos: el signo más es aritmética, `..`
|
|
es pegamento.
|
|
|
|
Vale la pena conocer este por el sitio donde ha aparecido. El error de la
|
|
lección uno detuvo Singe antes de que tu juego llegara a arrancar, porque Lua no podía
|
|
ni leer el archivo. Este es una línea perfectamente legible que solo falla al
|
|
ejecutarse, así que la ventana se abrió, el juego funcionó y murió en el primer
|
|
fotograma. Los dos tipos te dicen el archivo y la línea. En cualquier caso,
|
|
empieza por ahí.
|
|
|
|
[#_what_you_learned_2]
|
|
=== Lo que has aprendido
|
|
|
|
* Una variable es un nombre con un valor detrás, y el valor puede cambiar.
|
|
* `=` significa «mete el lado derecho en el nombre de la izquierda», no «es
|
|
igual a».
|
|
* `frames = frames + 1` calcula primero el lado derecho y luego lo guarda.
|
|
* `+`, `-`, `*` y `/` hacen la aritmética que esperas; `//` divide y tira la
|
|
fracción.
|
|
* `..` une texto, y de paso convierte un número en texto.
|
|
* Una línea dentro de `onOverlayUpdate` se ejecuta en cada fotograma. Una línea
|
|
fuera de todas las funciones se ejecuta una vez, antes de que empiece el
|
|
juego.
|
|
* Todo lo que tenga que recordar algo de un fotograma a otro tiene que vivir
|
|
fuera.
|
|
* `overlayGetWidth` y `overlayGetHeight` te dicen el tamaño de la superficie de
|
|
dibujo, así que nunca tienes que adivinarlo.
|
|
* `overlayClear` importa en cuanto algo se mueve.
|
|
|
|
[#_next_time_2]
|
|
=== La próxima vez
|
|
|
|
La palabra se mueve, pero se mueve sola y nada de lo que hagas le afecta. En la
|
|
lección tres coges los cuatro bloques `if` que usaste sin más, aprendes lo que
|
|
dicen de verdad y los apuntas a las teclas de flecha en lugar de a los bordes de
|
|
la pantalla. La caja que manejas por la ventana al final de la lección es el
|
|
jugador del juego que construyes en la lección siete.
|