singe/docs/es/lessons/08-when-it-goes-wrong.adoc

508 lines
21 KiB
Text

[#_lesson_8_when_it_goes_wrong]
== Lección 8: Cuando algo sale mal
image::learn/08-when-it-goes-wrong.png[La lección terminada, 480]
Has escrito un juego. De aquí en adelante pasarás más tiempo arreglando
programas que escribiéndolos, y eso no es señal de que te pase nada malo. Es el
trabajo. La diferencia entre alguien que disfruta programando y alguien que lo
deja casi nunca es el talento; es si aprendió a leer lo que le dice el
ordenador.
Así que en esta lección no hay nada nuevo que construir. Son los cinco mensajes
que más verás durante el resto de tu vida, lo que significa de verdad cada uno,
y cuatro formas de dar caza a la equivocación que hay detrás. Ten abierto
`dodge.singe` de la lección siete, porque vas a romperlo a propósito varias
veces.
[#_two_kinds_of_stop]
=== Dos tipos de parada
Singe se para por dos razones distintas, y lo dice de dos formas distintas.
Cuando el script ni siquiera llega a arrancar, te sale algo con esta forma:
----
Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock'
----
Lua lee el archivo entero antes de ejecutar ni una palabra, y esto significa
que no ha podido entender lo que ha leído. No se ha ejecutado nada en absoluto.
Cuando el script arrancó bien pero algo salió mal después, te sale en cambio
esta forma:
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped')
stack traceback:
dodge/dodge.singe:98: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
El juego estaba funcionando. Singe llamó a tu `onOverlayUpdate`, como hace
sesenta veces por segundo, y esta vez la llamada no volvió.
Las dos formas contienen las mismas tres cosas, y son las tres cosas que
quieres: un archivo, una línea y una queja. Léelas en ese orden, siempre.
Los números de línea de esta lección son los del listado terminado de la
lección siete. Si los tuyos están desviados una o dos líneas, también lo está
tu archivo, y nada de lo que hace falta para leer un error depende de que el
número coincida.
[#_the_ones_that_stop_it_before_it_starts]
=== Los que lo paran antes de empezar
Estos son los fáciles, y también son los que peor sientan, porque la pantalla
se queda negra y no hay nada que mirar.
Ve a `updatePlaying` y borra la palabra `then` de la línea
`if spawnTimer <= 0 then`. Guarda:
----
Error running script: dodge/dodge.singe:92: 'then' expected near 'spawnBlock'
----
Lua llegó al final de la línea del `if`, quería la palabra `then` y en su lugar
encontró el principio de la línea siguiente -- que es `spawnBlock()`, y esa es
la parte `near 'spawnBlock'`. «Near» significa «aquí es donde me he dado
cuenta», y suele ser lo primero que va *después* de lo que te has dejado.
Ahora uno más feo. Vuelve a poner `then`, y en su lugar borra el `end` de la
última línea de `spawnBlock`. Guarda:
----
Error running script: dodge/dodge.singe:148: 'end' expected (to close 'function' at line 58) near <eof>
----
La línea 148 es el final del archivo, justo después de su última línea. A la
línea 148 no le pasa nada.
Lua leyó hasta el final del todo -- `<eof>` es como llama al final de un
archivo -- esperando todavía un `end` que nunca llegó, y para entonces se había
tragado todas las funciones que van después de `spawnBlock` como si fueran
parte de ella.
La mitad útil de ese mensaje es `(to close 'function' at line 58)`, que es
donde empieza `spawnBlock`. Cuando un mensaje señala el final del archivo,
créete lo que dice entre paréntesis, no el número de línea. Por esto también
importa tanto la sangría: un `end` que falta es casi invisible en una lista de
líneas, y salta a la vista en cuanto todo lo que hay debajo está sangrado un
paso de más.
Dos más que vale la pena reconocer, porque te vas a encontrar los dos esta
misma semana:
----
Error running script: dodge/dodge.singe:41: unfinished string near '"GAME OVER)'
----
Unas comillas que abriste y nunca cerraste, que ya viste en la lección uno.
----
Error running script: dodge/dodge.singe:135: 'then' expected near '='
----
Ese es `if state = "playing" then`. Un `=` da valor a una variable, dos `==`
hacen una pregunta, y Lua no te deja dar valor a una dentro de un `if`. Parece
una queja rara, y siempre significa lo mismo.
[#_attempt_to_index_a_nil_value]
=== Attempt to index a nil value
Este es el que más verás, con mucha diferencia.
Ve a `spawnBlock` y escribe mal `block` en la primera línea que lo usa, para
que quede `blocl.x = math.random(0, screenWidth - BLOCK_SIZE)`. Guarda y pulsa
la barra espaciadora:
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:60: attempt to index a nil value (global 'blocl')
stack traceback:
dodge/dodge.singe:60: in function 'spawnBlock'
dodge/dodge.singe:92: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
*Indexar* es lo que hace el punto. `blocl.x` significa «entra en la cosa
llamada `blocl` y busca la `x` que tiene dentro». Eso solo se puede hacer con
una tabla.
*Nil* es como llama Lua a un nombre que no tiene nada detrás. No lo has visto
mucho porque has tenido cuidado de darle un valor a todo, pero cualquier nombre
que no hayas usado nunca ya es `nil`, y escribir mal un nombre se inventa uno
completamente nuevo.
Así que el mensaje entero dice: *has usado un punto en algo que no existe*.
La parte entre paréntesis es el regalo. `global 'blocl'` es el nombre exacto de
la cosa que estaba vacía. Búscalo en tu archivo. Si aparece exactamente una
vez, es una falta de ortografía, y la has encontrado en diez segundos.
La palabra que va antes del nombre te dice dónde lo buscó Lua:
* `global` -- un nombre que no tiene `local` delante en ningún sitio.
* `local` -- un nombre que declaraste con `local` en la función en la que
estás.
* `field` -- un nombre detrás de un punto, como la `y` de `block.y`.
* `upvalue` -- un `local` de más afuera que esta función puede ver, que es lo
que son todas las variables de `dodge.singe`, ya que se declaran al principio
del archivo y se usan dentro de funciones.
`field` es la otra causa habitual, y no es ninguna falta de ortografía. Si
escribiste `block.hit.when` y nunca diste valor a `block.hit`, te sale
`attempt to index a nil value (field 'hit')`. El punto que falla es el segundo.
Lua te dice cuál nombrándolo.
[#_attempt_to_call_a_nil_value]
=== Attempt to call a nil value
Arregla `blocl`. Ahora escribe mal una función del motor: en `drawGame`,
cambia `overlayPrint` por `overlayPirnt`. Guarda:
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:36: attempt to call a nil value (global 'overlayPirnt')
stack traceback:
dodge/dodge.singe:36: in function 'drawGame'
dodge/dodge.singe:140: in function 'onOverlayUpdate'
----
El mismo `nil`, otro delito. *Llamar* es lo que hacen los paréntesis, y eso
solo se puede hacer con una función. No hay ninguna función llamada
`overlayPirnt`, así que `overlayPirnt` es `nil`, así que los paréntesis no
tienen nada a lo que llamar.
Esto pasa por cuatro razones, de más a menos frecuente:
. Has escrito mal una de tus propias funciones.
. Has escrito mal una de las de Singe. Nada en el motor comprueba cómo
escribes los nombres cuando se carga el archivo, así que un nombre del motor
mal escrito es solo otro nombre que resulta estar vacío, y no te cuesta nada
hasta que se ejecuta la línea.
. La función no existe en Singe en absoluto -- era una suposición razonable, y
las suposiciones razonables fallan más o menos la mitad de las veces. La
lista de funciones de la referencia es la única forma de estar seguro.
. La hiciste `local` y la llamaste desde algún sitio por encima de donde se
declara. Esa es justo la razón de que las funciones de `dodge.singe` sean
globales.
Hay una versión mucho peor de esta equivocación, y te la encontraste en la
lección uno sin saberlo: escribe mal un *callback* y no te sale ningún mensaje.
`onOverlayUpdated` no es un error. Es una función perfectamente válida de la
que Singe no ha oído hablar nunca y a la que nunca llamará, así que la pantalla
se queda negra y nada se queja. Cuando toda una parte del juego no hace
absolutamente nada, sospecha del nombre.
[#_attempt_to_perform_arithmetic_on_a_nil_value]
=== Attempt to perform arithmetic on a nil value
Vuelve a poner `overlayPrint`. En `updatePlaying`, escribe mal `blockSpeed` en
la línea de caída, para que quede `block.y = block.y + blockSped`:
----
Error executing function 'onOverlayUpdate': dodge/dodge.singe:98: attempt to perform arithmetic on a nil value (global 'blockSped')
stack traceback:
dodge/dodge.singe:98: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
La aritmética es `+`, `-`, `*` y `/`. No se puede sumar la nada a un número. De
nuevo los paréntesis nombran la cosa vacía, y de nuevo la causa más común es
una errata -- pero este tiene una segunda causa que vale la pena conocer,
porque es más difícil de ver.
Una variable a la que nunca diste un número es `nil`, no cero. Si añades un
récord al juego y escribes `local hiScore` al principio sin `= 0`, guarda `nil`
hasta que algo le dé valor, y el primer `hiScore + score` detiene el juego. Lua
no empieza los números en cero por ti. Los empiezas tú en cero.
Dos parientes cercanos, con la misma causa y la misma cura:
----
dodge/dodge.singe:37: attempt to concatenate a nil value (upvalue 'hiScore')
----
`..` uniendo una cadena a algo que no está ahí. Verás este cada vez que
imprimas una puntuación a la que se te olvidó dar valor -- aquí, `hiScore`
añadido a la línea de la puntuación en `drawGame`, que es la línea 37 ahora que
`local hiScore` está encima.
----
dodge/dodge.singe:105: attempt to compare nil with number
----
Ese es `if score > hiScore then`, puesto debajo de `state = "over"` para
guardar la mejor puntuación cuando acaba una partida, sin nada en `hiScore`.
Fíjate en que este mensaje no nombra nada entre paréntesis. Cuando Lua no puede decirte qué lado estaba
vacío, la sección que va después de la siguiente te enseña a averiguarlo.
[#_an_argument_of_the_wrong_kind]
=== Un argumento del tipo equivocado
Todos los mensajes hasta ahora venían de Lua. Este viene de Singe, y tiene un
aspecto completamente distinto, así que vale la pena conocerlo a propósito.
Supón que quieres ver qué hay en `blocks`, e intentas imprimirlo. Añade esta
línea a `drawGame`, debajo de la puntuación:
[source,lua]
----
overlayPrint(1, 3, blocks)
----
----
37:overlayPrint: Argument 3 must be a string.
----
Ni nombre de archivo, ni traza, ni ningún `attempt to` nada. Una queja del
motor empieza con el número de línea, luego el nombre de la función que se ha
negado y luego lo que quería. Aquí a `overlayPrint` le han dado una tabla donde
necesita texto, y se ha parado en lugar de dibujar algo sin sentido.
El mensaje hermano cuenta en lugar de comprobar:
----
34:overlayBox: Expected 4 argument(s), got 3.
----
Eso es un `overlayBox` al que le falta una esquina. Ese `argument(s)` tan raro
es del motor, no una errata tuya.
Cuando te salga uno de estos, la entrada de esa función en la referencia es la
respuesta, y te llevará un minuto. Cada entrada enumera los argumentos en
orden, con lo que tiene que ser cada uno. Singe los comprueba porque la
alternativa es un juego que funciona durante diez minutos y luego dibuja
basura.
[#_finding_out_what_is_really_in_there]
=== Averiguar qué hay de verdad ahí dentro
La mitad de depurar no consiste en leer errores. Es el caso en el que nada se
cae y nada funciona: bloques que nunca aparecen, un jugador que no se mueve,
una puntuación atascada en cero. Para eso necesitas ver el interior del juego
mientras se ejecuta, y hay dos formas.
La primera es `debugPrint`, que escribe una línea en la terminal desde la que
arrancaste Singe. Coge la línea que no consigues entender y pon una encima:
[source,lua]
----
debugPrint("blocks " .. #blocks .. ", timer " .. spawnTimer)
----
Ahora estás mirando lo que hay de verdad en lugar de lo que estás seguro de que
hay, y en ese hueco es donde viven los fallos. Imprime `playerX` del mismo modo
y también verás la verdad sobre él: se imprime como `160.0`, no `160`, porque dividir con
`/` en Lua siempre da un número con parte decimal, aunque esa parte sea cero.
Hay dos cosas que te van a morder. La primera es que `debugPrint` exige texto o
un número; dale cualquier otra cosa y detiene el juego igual que hizo
`overlayPrint`. `debugPrint(blocks)` no te enseñará una lista, y
`debugPrint("left " .. goingLeft)` al principio de `updatePlaying` tampoco te
enseñará un `true` ni un `false`:
----
dodge/dodge.singe:78: attempt to concatenate a boolean value (upvalue 'goingLeft')
----
`tostring` es el arreglo. Acepta cualquier cosa y te da texto para ella, así
que `tostring(goingLeft)` es `"true"` o `"false"` y la línea funciona.
La segunda es que `onOverlayUpdate` se ejecuta sesenta veces por segundo, así
que un `debugPrint` dentro de él imprime sesenta líneas por segundo y no puedes
leer ninguna. La referencia lo dice en su entrada: la llamada es barata, pero nada
la frena.
Antes que nada, pon un interruptor delante de cada línea de depuración que
añadas, para poder quitar el ruido sin tener que volver a buscarlas todas:
[source,lua]
----
local DEBUG = true
function debugLog(text)
if DEBUG then
debugPrint(text)
end
end
----
Cada `debugLog` del juego obedece a ese único `true`. Cámbialo a `false` y el
juego se queda callado; vuelve a cambiarlo y todo regresa.
Luego imprime cuando algo *pasa*, en lugar de en cada fotograma:
[source,lua]
----
debugLog("hit at y " .. math.floor(block.y) .. ", lives now " .. lives)
----
O imprime con un temporizador, que cuenta hacia atrás exactamente igual que
cuando nacen los bloques:
[source,lua]
----
reportTimer = reportTimer - 1
if reportTimer <= 0 then
debugReport()
reportTimer = REPORT_FRAMES
end
----
La carpeta `learn` tiene todo esto como `08-when-it-goes-wrong.singe`: es el
juego de la lección siete con un informe cada dos segundos más o menos, una
línea cuando te dan y una línea cuando termina la partida.
La segunda forma de ver el interior es ponerlo en pantalla, que es mejor para
cualquier cosa que cambie en cada fotograma, porque sesenta líneas por segundo
en una terminal no sirven de nada, pero un número que va cambiando en una
esquina es fácil de vigilar:
[source,lua]
----
if DEBUG then
overlayPrint(1, 3, "BLOCKS " .. #blocks .. " PLAYER " .. playerX)
end
----
[#_cutting_the_problem_in_half]
=== Partir el problema por la mitad
Cuando no ves cuál de veinte líneas está mal, deja de mirar y empieza a quitar.
Pon dos guiones al principio de una línea y Lua ignora el resto. Eso es un
*comentario*, y lo normal es usarlo para dejarte notas, pero el mejor uso es
este:
[source,lua]
----
-- overlayPrint(1, 1, "SCORE " .. score .. " LIVES " .. lives)
----
Comenta la mitad de `drawGame` y ejecútalo. Si el problema sigue ahí, está en
la mitad que has dejado. Si ha desaparecido, está en la mitad que has quitado.
Vuelve a poner esa mitad y pártela *a ella* por la mitad. Veinte líneas son
cinco rondas de esto, y cada ronda es guardar una vez y mirar una vez.
Funciona con algo más que líneas. Comenta el cuerpo de `updatePlaying` y los
bloques dejan de moverse, lo que te dice si lo que persigues está en el
movimiento o en el dibujo. Comenta la rama de `collideRects` y mira si el juego
deja de caerse. Mientras haces esto no intentas arreglar nada. Intentas
averiguar dónde no está.
[#_going_round_faster]
=== Dar vueltas más deprisa
Todo esto depende de poder probar algo en un par de segundos, y para eso está
`-R`. Lo usas desde la lección uno; esto es lo que hace de verdad, y vale la
pena saberlo ahora que vas a apoyarte en él.
Singe vigila cada archivo de script que ha cargado tu juego. Cuando guardas
uno, el juego vuelve a empezar desde el principio: todo lo que había creado el
script se tira y tu archivo se ejecuta de nuevo, en lo que se tarda en leer el
archivo y no en lo que se tarda en arrancar el motor. `F5` hace lo mismo cuando
quieras, sin guardar nada.
Una tecla que mantienes pulsada durante una recarga se ignora hasta que la
sueltas, así que el jugador no se irá corriendo él solo después de una recarga.
Los dos tipos de parada se comportan de forma distinta aquí, y es útil saber
cuál estás viendo sin leer el mensaje. Un script que no compila *se imprime y
Singe sigue vivo*: la ventana sigue abierta y vacía, el archivo sigue vigilado,
y guardar el arreglo devuelve el juego. Un fallo dentro de un callback -- todos
los mensajes `attempt to` y las quejas del motor sobre los argumentos -- tumba
a Singe, y lo vuelves a arrancar a mano.
[#_reading_a_traceback]
=== Leer una traza
Las líneas que hay debajo de `stack traceback:` son la lista de funciones que
estaban en marcha cuando todo se paró, empezando por la más interna. Aquí está
otra vez la de `blocl`:
----
dodge/dodge.singe:60: in function 'spawnBlock'
dodge/dodge.singe:92: in function 'updatePlaying'
dodge/dodge.singe:136: in function 'onOverlayUpdate'
----
Léela de abajo arriba y es una frase. Singe llamó a `onOverlayUpdate`. En la
línea 136, `onOverlayUpdate` llamó a `updatePlaying`. En la línea 92,
`updatePlaying` llamó a `spawnBlock`. En la línea 60, `spawnBlock` se rindió.
La línea de arriba es donde se rompió. Las líneas de debajo son cómo llegó ahí,
y son lo que necesitas cuando la línea de arriba parece inocente. Una función
que suma un número a `block.y` no está mal; una función que metió un bloque en
la lista sin una `y` sí lo está. Solo el rastro une las dos, y el rastro se te
imprime todas y cada una de las veces.
[#_the_habit]
=== La costumbre
Todo lo de arriba es técnica. Esto es la costumbre, y importa más:
*Cambia una cosa. Ejecútalo. Mira lo que ha pasado.*
Cuando algo no funciona, la tentación es cambiar cuatro cosas a la vez, porque
seguro que una de ellas es la buena. Hazlo y dejará de funcionar de una forma
nueva, y ahora no sabrás cuál de las cuatro ha sido, ni si dos de ellas se
están anulando entre sí. Has hecho el problema más grande y lo has escondido
mejor.
Un cambio. Ejecutar. Mirar. Parece más lento. Es lo más rápido que hay, y
todos los programadores con experiencia que conozcas lo hacen, exactamente por
la razón que estás a punto de descubrir.
[#_try_it_8]
=== Pruébalo
. *Conócelos a todos.* Comete a propósito en `dodge.singe` cada una de las
cinco equivocaciones de esta lección, de una en una, y lee el mensaje antes
de arreglarla. Nunca más te darán miedo.
. *Vigila dónde nacen.* Pon dentro de `spawnBlock` un `debugPrint` que imprima
la `x` del bloque nuevo. Juega treinta segundos y luego mira los números.
¿Están repartidos por toda la pantalla, o prefieren un extremo?
. *Apágalo.* Cambia `DEBUG` a `false` en el script de la lección, y comprueba
por ti mismo que no se imprime nada y que el juego no ha cambiado.
. *Un fallo sin error.* En `updatePlaying`, cambia `if spawnTimer <= 0 then`
por `if spawnTimer == 0 then`, que parece querer decir lo mismo. Ejecútalo y
empieza una partida. No hay ningún error, nada se cae y no cae ni un solo
bloque, así que ni siquiera puedes perder. Pon un `debugLog` dentro de
`spawnBlock`, añade `spawnTimer` a la primera línea que imprime
`debugReport` y vuelve a jugar. Averigua por lo que dicen, y por lo que uno
de ellos no dice nunca, por qué `==` era la pregunta equivocada.
. *Rompe el nombre de un callback.* Cambia el nombre de `onInputPressed` por
`onInputPress` y ejecútalo. No se imprime ningún error, nada se cae, el jugador no se
mueve y la barra espaciadora no empieza una partida. Luego explícate a ti
mismo por qué esa es la equivocación más peligrosa de esta lección.
[#_what_you_learned_8]
=== Lo que has aprendido
* Los errores tienen un archivo, una línea y una queja. Léelos en ese orden.
* `Error running script` significa que no se ha ejecutado nada; la forma con
traza significa que estaba funcionando y se paró.
* `nil` es un nombre sin nada detrás, y está detrás de la mayoría de los
mensajes de error que verás en tu vida.
* La palabra entre paréntesis nombra la cosa vacía, y dice si era un global, un
local, un campo o un upvalue.
* Un `end` que falta se señala al final del archivo, y los paréntesis te dicen
dónde está de verdad la equivocación.
* Las quejas del motor tienen otro aspecto: un número de línea, un nombre de
función y lo que quería en su lugar.
* `debugPrint` te enseña lo que guarda de verdad una variable. `tostring` hace
que cualquier cosa se pueda imprimir, y un interruptor como `DEBUG` quita el
ruido.
* Comenta una mitad con `--` para averiguar en qué mitad está el problema.
* Un callback mal escrito no produce ningún error, y eso lo hace peor que uno
que sí lo produce.
* Cambia una cosa, ejecútalo, mira.
[#_next_time_8]
=== La próxima vez
Ese es el final de la primera parte. Sabes escribir un juego, y ahora sabes
arreglarlo, lo que significa que todo lo que viene a partir de aquí es añadir y
no poner cimientos. La segunda parte empieza cambiando esas cajas de colores
por imágenes de verdad, y a partir de ahí el juego que has estado construyendo
empieza a parecer un juego al que otra persona querría jugar.