775 lines
26 KiB
Text
775 lines
26 KiB
Text
[#_lesson_6_lists_of_things]
|
|
== Lección 6: Listas de cosas
|
|
|
|
image::learn/06-tables.png[La lección terminada, 480]
|
|
|
|
Tienes ciento veinte estrellas y ninguna de ellas existe. No hay ninguna
|
|
estrella en ningún sitio de tu programa: hay un bucle que da ciento veinte
|
|
vueltas, y en cada vuelta calcula dos números a partir del número de la estrella
|
|
y de su capa, pinta un píxel y lo olvida todo. Las tres capas están ahí para
|
|
fingir lo único que ese montaje no puede hacer, que es dejar que una estrella
|
|
sea distinta de su vecina.
|
|
|
|
Esta lección te da lo que falta. Aprenderás a guardar una lista de elementos, a
|
|
hacer un elemento que guarda varios valores con nombre a la vez, y luego a
|
|
juntar las dos cosas en una lista de elementos que recuerdan cada uno su propia
|
|
posición, su velocidad y su brillo. Es la lección más larga de esta parte del
|
|
libro y la que sostiene todo lo que viene después. Tómatela con calma.
|
|
|
|
[#_start_a_new_folder_5]
|
|
=== Empieza una carpeta nueva
|
|
|
|
Una carpeta llamada `tables`, dentro un archivo llamado `tables.singe`, y
|
|
`Singe -R tables` ejecutándose en una terminal. Copia dentro el programa
|
|
terminado de la lección cinco: el campo de estrellas, la nave y todo lo demás.
|
|
|
|
[#_a_list]
|
|
=== Una lista
|
|
|
|
Añade esta línea al principio de `tables.singe`, debajo de los otros valores, y
|
|
las tres llamadas a `overlayPrint` a `onOverlayUpdate`, encima del `return`.
|
|
|
|
[source,lua]
|
|
----
|
|
local names = { "Ada", "Grace", "Alan" }
|
|
----
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(2, 2, names[1])
|
|
overlayPrint(2, 3, names[3])
|
|
overlayPrint(2, 4, "There are " .. #names .. " names.")
|
|
----
|
|
|
|
Ejecútalo. `Ada` en una línea, `Alan` en la siguiente, y `There are 3 names.`
|
|
debajo.
|
|
|
|
Las llaves de la primera línea crean una *tabla*. Una tabla es el único
|
|
contenedor que tiene Lua, y todo lo que guardes alguna vez en cantidad de más de
|
|
uno va en una. Aquí guarda tres cadenas en fila, que es la forma a la que la
|
|
gente llama *lista*.
|
|
|
|
Los corchetes de las líneas siguientes sacan un elemento. `names[1]` es el
|
|
primer elemento, `names[3]` es el tercero, y el número entre corchetes se llama
|
|
*índice*.
|
|
|
|
Ese número es donde Lua se separa de casi todos los demás lenguajes que te vas a
|
|
encontrar. *Lua cuenta desde uno.* C, Java, Python, JavaScript y casi todos los
|
|
demás cuentan desde cero, así que su primer elemento es el elemento cero y su
|
|
tercer elemento tiene el índice dos. El primer elemento de Lua es el elemento
|
|
uno, y esto es lo único que hace tropezar a los programadores que llegan de otro
|
|
sitio. Tú no llegas de ningún sitio, así que lo tienes más fácil: el primero es
|
|
el número uno.
|
|
|
|
Por cierto, `names[0]` no es un error. Es `nil` -- el valor de nada de la
|
|
lección tres -- porque ahí no hay nada guardado. Leer un índice que nunca se ha
|
|
llenado siempre da `nil`, lo cual es todo un detalle justo hasta que intentas
|
|
usarlo.
|
|
|
|
El `#` delante del nombre de una tabla es el último signo de puntuación nuevo
|
|
en una temporada. Te da cuántos elementos hay en la lista, así que `#names` es
|
|
`3`. Como la cuenta y el último índice son el mismo número, `names[#names]` es
|
|
siempre el último elemento, por larga que se haga la lista.
|
|
|
|
[#_adding_and_taking_away]
|
|
=== Añadir y quitar
|
|
|
|
Una lista que tienes que escribir entera no le sirve de mucho a un juego. Estas
|
|
dos llamadas son como cambia una lista mientras se ejecuta el programa.
|
|
|
|
[source,lua]
|
|
----
|
|
table.insert(names, "Katherine")
|
|
----
|
|
|
|
Eso pone `Katherine` al final, y `#names` ahora es `4`. `table.insert` es una
|
|
función como cualquier otra, y el punto en medio de su nombre significa que vive
|
|
dentro de una tabla llamada `table` que pone Lua -- una tabla de funciones para
|
|
trabajar con tablas. No tienes que crearla ni cargarla. Está ahí.
|
|
|
|
Dale a `table.insert` tres argumentos y el del medio es dónde poner el elemento
|
|
nuevo.
|
|
|
|
[source,lua]
|
|
----
|
|
table.insert(names, 1, "Edsger")
|
|
----
|
|
|
|
`Edsger` entra por delante, y todo lo que ya estaba se corre para hacerle sitio:
|
|
`Ada` era el elemento uno y ahora es el elemento dos. No se sobrescribe nada y
|
|
no se pierde nada.
|
|
|
|
Sacar un elemento es lo mismo, pero al revés.
|
|
|
|
[source,lua]
|
|
----
|
|
table.remove(names, 2)
|
|
----
|
|
|
|
El elemento dos se va, y todo lo que había detrás se corre hacia atrás para
|
|
cerrar el hueco, así que la lista nunca tiene un agujero en medio.
|
|
`table.remove` también te devuelve el elemento que ha sacado, así que
|
|
`local gone = table.remove(names, 2)` lo quita y te dice qué era en una sola
|
|
línea. Si no pones el número -- `table.remove(names)` -- saca el último
|
|
elemento, que es la forma más rápida de usar una lista como una pila.
|
|
|
|
Pon esas tres llamadas en tu script, de una en una, ejecútalo después de cada
|
|
una y mira cómo cambia la cuenta de la tercera línea.
|
|
|
|
[#_walking_a_list]
|
|
=== Recorrer una lista
|
|
|
|
Ya conoces una forma de visitar cada elemento, porque la usas desde la lección
|
|
cuatro:
|
|
|
|
[source,lua]
|
|
----
|
|
for i = 1, #names do
|
|
overlayPrint(2, i + 1, names[i])
|
|
end
|
|
----
|
|
|
|
Funciona. Empieza en uno, para en la cuenta, usa el contador como índice. Hay
|
|
una forma mejor, y es la que escribirás de ahora en adelante:
|
|
|
|
[source,lua]
|
|
----
|
|
for i, name in ipairs(names) do
|
|
overlayPrint(2, i + 1, name)
|
|
end
|
|
----
|
|
|
|
`ipairs` recorre una lista desde el elemento uno hasta que se acaba, y le da a
|
|
tu bucle dos cosas en cada vuelta: el índice y el propio elemento. Lo que
|
|
importa es lo segundo. Con el `for` numérico obtienes un número y tienes que
|
|
volver a la tabla con `names[i]` para saber qué es; con `ipairs` el elemento ya
|
|
lo tienes en la mano, con el nombre que hayas puesto en segundo lugar.
|
|
|
|
También es más difícil equivocarse. `for i = 1, #names` es un sitio donde
|
|
equivocarte por uno -- empieza en cero e imprimes un `nil`, para en
|
|
`#names - 1` y pierdes el último -- y `ipairs` no tiene ni un número con el que
|
|
equivocarse.
|
|
|
|
La mayoría de las veces no quieres el índice, solo el elemento. Escribe el
|
|
índice como un solo guion bajo y listo:
|
|
|
|
[source,lua]
|
|
----
|
|
for _, name in ipairs(names) do
|
|
----
|
|
|
|
`_` es un nombre perfectamente normal, y nada en Lua lo trata de forma
|
|
especial. Es una costumbre que comparten los programadores para decir «aquí
|
|
llega algo y no lo voy a usar». Cualquiera que lea tu bucle sabe de un vistazo
|
|
que en él el índice no importa.
|
|
|
|
Borra ahora los experimentos con `names`, y con ellos las líneas de
|
|
`overlayPrint`. Ya sabes lo que es una lista. Es hora de meter en una algo mejor
|
|
que una cadena.
|
|
|
|
[#_a_table_can_also_be_a_record]
|
|
=== Una tabla también puede ser un registro
|
|
|
|
Una estrella necesita recordar cuatro cosas: dónde está en horizontal, dónde
|
|
está en vertical, lo rápido que cae y lo brillante que es. Cuatro listas
|
|
separadas, todas sincronizadas a mano, serían un suplicio. En lugar de eso, crea
|
|
una tabla y pon nombres dentro.
|
|
|
|
[source,lua]
|
|
----
|
|
local star = {}
|
|
star.x = 40
|
|
star.y = 0
|
|
star.speed = 2
|
|
star.shade = 180
|
|
----
|
|
|
|
Las llaves vacías crean una tabla vacía. Las cuatro líneas que siguen meten
|
|
valores en ella con nombres en lugar de números, y el punto es como dices a qué
|
|
nombre te refieres. `star.x` se lee «la `x` de `star`», y se comporta
|
|
exactamente como cualquier otra variable: léela, dale un valor, súmale algo.
|
|
|
|
[source,lua]
|
|
----
|
|
star.y = star.y + star.speed
|
|
----
|
|
|
|
Una tabla usada así -- un conjunto fijo de valores con nombre que describen una
|
|
cosa -- es un *registro*, y los nombres que tiene dentro son sus *campos*. Es el
|
|
mismo tipo de tabla que la lista de antes. La única diferencia es que los
|
|
elementos de una lista se encuentran por número y los campos de un registro se
|
|
encuentran por nombre, y una misma tabla puede hacer las dos cosas a la vez si
|
|
algún día lo quieres.
|
|
|
|
El punto es una abreviatura. `star.x` y `star["x"]` significan exactamente lo
|
|
mismo, y vale la pena verlo una vez: los corchetes aceptan un nombre con la
|
|
misma alegría que un número, así que una lista y un registro son de verdad una
|
|
sola idea con dos sombreros. Usa el punto. Se lee mejor.
|
|
|
|
Un campo al que nunca has dado valor es `nil`, igual que un índice que nunca has
|
|
llenado. No hay en ningún sitio una lista de nombres de campo permitidos, y nada
|
|
comprueba cómo los escribes, así que `star.sped` no es un error -- es `nil`, y
|
|
verás lo que pasa después al final de esta lección.
|
|
|
|
Escribir cuatro líneas para rellenar cuatro campos acaba cansando. Puedes poner
|
|
los campos dentro de las llaves al crear la tabla:
|
|
|
|
[source,lua]
|
|
----
|
|
local star = {
|
|
x = 40,
|
|
y = 0,
|
|
speed = 2,
|
|
shade = 180
|
|
}
|
|
----
|
|
|
|
La misma tabla, una sola instrucción. Las comas separan los campos, el último no
|
|
necesita coma detrás, y las líneas están repartidas solo para que se lean con
|
|
facilidad.
|
|
|
|
[#_a_list_of_records]
|
|
=== Una lista de registros
|
|
|
|
Aquí está toda la idea de esta lección en dos líneas:
|
|
|
|
[source,lua]
|
|
----
|
|
local stars = {}
|
|
table.insert(stars, star)
|
|
----
|
|
|
|
Una lista puede guardar cualquier cosa, y eso incluye una tabla. Así que una
|
|
lista de registros es una lista de cosas, y cada una recuerda todo sobre sí
|
|
misma. `stars[1]` es una estrella entera, y `stars[1].y` es lo que ha bajado esa
|
|
estrella por la pantalla. Casi todos los juegos que escribas serán un puñado de
|
|
listas como esta: los enemigos, las balas, los objetos que se recogen y los
|
|
bloques que caen.
|
|
|
|
Hay una cosa sobre las tablas que tienes que saber antes de escribir el código,
|
|
y pilla a todo el mundo una vez. Pasarle una tabla a una función no la copia. Un
|
|
número sí se copia -- por eso `half` pudo trocear su `number` en la lección
|
|
cinco sin molestar a `drift` -- pero una tabla se comparte. Los dos nombres se
|
|
refieren a la misma tabla, y un cambio hecho a través de uno se ve a través del
|
|
otro.
|
|
|
|
[source,lua]
|
|
----
|
|
local star = stars[3]
|
|
star.y = star.y + 1
|
|
----
|
|
|
|
Eso mueve la tercera estrella de la lista. No una copia de ella: a ella. Esto es
|
|
exactamente lo que quieres aquí, y es lo que hace que funcione la sección
|
|
siguiente, pero recuérdalo cuando una función que has escrito cambie algo que no
|
|
esperabas que cambiara.
|
|
|
|
[#_rebuild_the_star_field]
|
|
=== Reconstruye el campo de estrellas
|
|
|
|
Fuera la aritmética. Una estrella está a punto de convertirse en una cosa.
|
|
|
|
Borra `starAt`, `shadeFor`, `drawLayer` y el viejo `drawStar`, y con ellos las
|
|
constantes que usaban: `LAYERS`, `STARS_PER_LAYER`, `ACROSS_STEP`,
|
|
`DOWN_STEP`, `LAYER_SHIFT` y `LAYER_SPEED`. La variable `drift` también se va,
|
|
y con ella la línea de `drift` del principio de `onOverlayUpdate`.
|
|
`LAYER_BRIGHT` se queda, con un nombre que ya no habla de capas, y se le unen
|
|
dos valores:
|
|
|
|
[source,lua]
|
|
----
|
|
local STAR_COUNT = 120
|
|
local STAR_SHADE = 55
|
|
local BURST_COUNT = 10
|
|
----
|
|
|
|
[source,lua]
|
|
----
|
|
local stars = {}
|
|
----
|
|
|
|
Ciento veinte es lo que tenías: tres capas de cuarenta. La lista empieza vacía,
|
|
y esto es lo que la llena:
|
|
|
|
[source,lua]
|
|
----
|
|
local function newStar(y)
|
|
local speed = math.random(1, 3)
|
|
local star = {
|
|
x = math.random(0, width - 1),
|
|
y = y,
|
|
speed = speed,
|
|
shade = math.random(60, 90) + speed * STAR_SHADE
|
|
}
|
|
return star
|
|
end
|
|
----
|
|
|
|
`newStar` crea una estrella y te la devuelve. Mira el último campo y compáralo
|
|
con `shadeFor` de la lección cinco: la misma suma, con la velocidad de la propia
|
|
estrella donde antes estaba el número de capa. Las capas no han desaparecido;
|
|
más bien se han disuelto. Ahora cada estrella elige su propia velocidad, uno,
|
|
dos o tres píxeles por fotograma, y una estrella que cae más rápido es más
|
|
brillante, que es toda la ilusión de profundidad y cuesta una línea.
|
|
|
|
La `y` inicial es un parámetro porque los dos sitios que llaman a la función
|
|
quieren cosas distintas. Las estrellas que se crean al empezar el juego deben
|
|
quedar repartidas por toda la pantalla; las que se crean después entran por
|
|
arriba.
|
|
|
|
[source,lua]
|
|
----
|
|
local function makeStars()
|
|
for number = 1, STAR_COUNT do
|
|
table.insert(stars, newStar(math.random(0, height - 1)))
|
|
end
|
|
end
|
|
----
|
|
|
|
Nada llama todavía a `makeStars`. Añade una línea al final del todo del archivo,
|
|
después de todo lo demás y fuera de cualquier función:
|
|
|
|
[source,lua]
|
|
----
|
|
makeStars()
|
|
----
|
|
|
|
Hay más que decir sobre esa línea, y tendrá que esperar al final de la lección.
|
|
Ahora el dibujo, que se acorta cada vez que lo tocas:
|
|
|
|
[source,lua]
|
|
----
|
|
local function drawStar(star)
|
|
colorForeground(star.shade, star.shade, star.shade)
|
|
overlayPlot(star.x, star.y)
|
|
end
|
|
|
|
|
|
local function drawStars()
|
|
for _, star in ipairs(stars) do
|
|
drawStar(star)
|
|
end
|
|
end
|
|
----
|
|
|
|
`drawStar` recibía tres números en la lección cinco y ahora recibe una estrella.
|
|
Todo lo que necesita saber está dentro de la estrella, que es la razón de tener
|
|
registros: una sola cosa que pasar de un lado a otro en lugar de cuatro, y
|
|
ninguna posibilidad de entregarlas en el orden equivocado. Y `drawStars` ha
|
|
perdido su bucle sobre las capas, junto con cualquier interés en cuántas
|
|
estrellas hay o de dónde han salido.
|
|
|
|
[#_stars_that_come_and_go]
|
|
=== Estrellas que vienen y van
|
|
|
|
Ahora la parte que el viejo campo de estrellas no podía hacer de ninguna manera.
|
|
Las estrellas caen cada una a su velocidad, se van por abajo y otras las
|
|
sustituyen por arriba.
|
|
|
|
[source,lua]
|
|
----
|
|
local function moveStars()
|
|
for i = #stars, 1, -1 do
|
|
local star = stars[i]
|
|
star.y = star.y + star.speed
|
|
if star.y >= height then
|
|
table.remove(stars, i)
|
|
end
|
|
end
|
|
while #stars < STAR_COUNT do
|
|
table.insert(stars, newStar(0))
|
|
end
|
|
end
|
|
----
|
|
|
|
Llámala desde `onOverlayUpdate`, en la línea donde antes estaba
|
|
`drift = drift + 1`.
|
|
|
|
Cada estrella baja según su propia `speed`, que es la línea que la lección cinco
|
|
no podía escribir. El `while` de abajo es el relleno: mientras haya menos de
|
|
ciento veinte estrellas, crea otra en la parte de arriba de la pantalla. Se
|
|
ejecuta tantas veces como haga falta y luego se para, que es la herramienta
|
|
adecuada cuando no sabes cuántas faltan.
|
|
|
|
La línea `for` de arriba cuenta hacia atrás, con el paso que viste en la lección
|
|
cuatro. `for i = #stars, 1, -1` visita primero el último elemento y va hacia
|
|
atrás hasta el elemento uno. No es un adorno. Es la única forma segura de hacer
|
|
lo que hace este bucle, y la sección siguiente explica por qué.
|
|
|
|
[#_never_remove_going_forwards]
|
|
=== Nunca quites hacia delante
|
|
|
|
Escribe el mismo bucle hacia delante y está roto:
|
|
|
|
[source,lua]
|
|
----
|
|
for i = 1, #stars do
|
|
local star = stars[i]
|
|
star.y = star.y + star.speed
|
|
if star.y >= height then
|
|
table.remove(stars, i)
|
|
end
|
|
end
|
|
----
|
|
|
|
Síguelo a mano con cuatro estrellas, y supón que las estrellas dos y tres han
|
|
llegado las dos abajo.
|
|
|
|
* `i` es `1`. La estrella uno está bien.
|
|
* `i` es `2`. La estrella dos se ha salido por abajo, así que fuera. Todo se
|
|
corre hacia abajo: la estrella que era la tres ahora es la dos, y la que era
|
|
la cuatro ahora es la tres.
|
|
* `i` es `3`. Esa es la estrella que antes era la cuatro. *La estrella tres no
|
|
se ha mirado nunca.* Quitar el elemento dos la ha bajado al hueco dos, y el
|
|
bucle ya había pasado por el hueco dos y se había ido.
|
|
* `i` es `4`. Ya no hay elemento cuatro. `stars[4]` es `nil`.
|
|
|
|
Dos fallos distintos por un error pequeño. Se saltan elementos, así que una
|
|
estrella que había llegado abajo ni siquiera se mira en esta vuelta. Que una
|
|
estrella sobreviva un fotograma de más no hace daño, pero el mismo bucle sobre
|
|
una lista de enemigos, donde lo que los mata es que el bucle los mire, es un
|
|
fallo que perseguirías durante una hora. El segundo fallo hace más ruido. La lista ahora
|
|
es más corta que la cuenta con la que empezó el bucle, porque `#stars` se
|
|
calculó una vez, antes de quitar nada, así que las últimas vueltas del bucle se
|
|
pasan del final de la lista:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': tables/tables.singe:60: attempt to index a nil value (local 'star')
|
|
stack traceback:
|
|
tables/tables.singe:60: in upvalue 'moveStars'
|
|
tables/tables.singe:124: in function 'onOverlayUpdate'
|
|
----
|
|
|
|
«Attempt to index a nil value» significa que has puesto un punto detrás de algo
|
|
que no guardaba nada. El `(local 'star')` del final lo nombra: `star` era `nil`,
|
|
así que `star.y` no tenía dónde mirar.
|
|
|
|
Ir hacia atrás arregla los dos fallos a la vez, y vale la pena ver por qué en
|
|
lugar de creértelo sin más. Cuando quitas el elemento `i`, todo lo que va
|
|
después de `i` baja un hueco -- y yendo hacia atrás, todo lo que va después de
|
|
`i` es todo lo que ya has visitado. Lo que aún está por venir, los elementos
|
|
desde `i - 1` hasta `1`, no se ha movido en absoluto. Que la lista encoja a tu
|
|
espalda tampoco te puede hacer daño, porque vas hacia el elemento uno y el
|
|
elemento uno siempre está ahí.
|
|
|
|
La regla, que usarás el resto de tu vida: *cuando un bucle pueda quitar
|
|
elementos de la lista que está recorriendo, recórrela hacia atrás.*
|
|
|
|
`ipairs` no es una excepción. Quitar elementos en medio de un bucle `ipairs`
|
|
sale mal de la misma forma. `ipairs` es para mirar, y el `for` hacia atrás es
|
|
para cambiar.
|
|
|
|
[#_add_some_yourself]
|
|
=== Añade algunas tú
|
|
|
|
Una rama más en `onInputPressed` y podrás ver cómo crece la lista mientras se
|
|
ejecuta el juego. Va con las otras dos:
|
|
|
|
[source,lua]
|
|
----
|
|
elseif what == SWITCH_BUTTON1 then
|
|
addBurst()
|
|
----
|
|
|
|
[source,lua]
|
|
----
|
|
local function addBurst()
|
|
for number = 1, BURST_COUNT do
|
|
table.insert(stars, newStar(0))
|
|
end
|
|
end
|
|
----
|
|
|
|
`addBurst` va encima de los callbacks, con tus otras funciones. Y para que
|
|
puedas ver cómo cambia la longitud de la lista, pon la cuenta en pantalla en
|
|
`onOverlayUpdate`:
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(0, 0, "Stars: " .. #stars)
|
|
----
|
|
|
|
Pulsa el botón -- la barra espaciadora, a menos que hayas cambiado de dónde
|
|
viene `SWITCH_BUTTON1` -- una docena de veces. La cuenta sube de diez en diez
|
|
con cada pulsación y el cielo se llena, y no se repite mientras lo mantienes
|
|
pulsado: a `onInputPressed` se le avisa una vez, cuando el botón baja. Luego
|
|
mira cómo la cuenta vuelve sola a ciento veinte a medida que las estrellas de
|
|
más llegan abajo y se quitan, porque el relleno solo completa la lista hasta
|
|
`STAR_COUNT` y nunca la recorta.
|
|
|
|
Ni el dibujo, ni el movimiento, ni el código que las quita han tenido que
|
|
enterarse de que el número de estrellas había cambiado. Todos trabajan a partir
|
|
de la lista.
|
|
|
|
[#_the_whole_thing_5]
|
|
=== Todo junto
|
|
|
|
[source,lua]
|
|
----
|
|
local STAR_COUNT = 120
|
|
local STAR_SHADE = 55
|
|
local BURST_COUNT = 10
|
|
|
|
local PLAYER_WIDTH = 20
|
|
local PLAYER_HEIGHT = 6
|
|
local PLAYER_SPEED = 3
|
|
local PLAYER_MARGIN = 6
|
|
|
|
local width = overlayGetWidth()
|
|
local height = overlayGetHeight()
|
|
|
|
local stars = {}
|
|
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 newStar(y)
|
|
local speed = math.random(1, 3)
|
|
local star = {
|
|
x = math.random(0, width - 1),
|
|
y = y,
|
|
speed = speed,
|
|
shade = math.random(60, 90) + speed * STAR_SHADE
|
|
}
|
|
return star
|
|
end
|
|
|
|
|
|
local function makeStars()
|
|
for number = 1, STAR_COUNT do
|
|
table.insert(stars, newStar(math.random(0, height - 1)))
|
|
end
|
|
end
|
|
|
|
|
|
local function addBurst()
|
|
for number = 1, BURST_COUNT do
|
|
table.insert(stars, newStar(0))
|
|
end
|
|
end
|
|
|
|
|
|
local function moveStars()
|
|
for i = #stars, 1, -1 do
|
|
local star = stars[i]
|
|
star.y = star.y + star.speed
|
|
if star.y >= height then
|
|
table.remove(stars, i)
|
|
end
|
|
end
|
|
while #stars < STAR_COUNT do
|
|
table.insert(stars, newStar(0))
|
|
end
|
|
end
|
|
|
|
|
|
local function drawStar(star)
|
|
colorForeground(star.shade, star.shade, star.shade)
|
|
overlayPlot(star.x, star.y)
|
|
end
|
|
|
|
|
|
local function drawStars()
|
|
for _, star in ipairs(stars) do
|
|
drawStar(star)
|
|
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
|
|
elseif what == SWITCH_BUTTON1 then
|
|
addBurst()
|
|
end
|
|
end
|
|
|
|
|
|
function onInputReleased(what)
|
|
if what == SWITCH_LEFT then
|
|
movingLeft = false
|
|
elseif what == SWITCH_RIGHT then
|
|
movingRight = false
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
overlayClear()
|
|
movePlayer()
|
|
moveStars()
|
|
drawStars()
|
|
drawPlayer()
|
|
overlayPrint(0, 0, "Stars: " .. #stars)
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
makeStars()
|
|
----
|
|
|
|
[#_what_just_happened_3]
|
|
=== Qué acaba de pasar
|
|
|
|
El programa es más corto que el de la lección cinco y hace algo que la lección
|
|
cinco ni siquiera podía empezar a hacer. Tres partes merecen una segunda mirada.
|
|
|
|
[source,lua]
|
|
----
|
|
makeStars()
|
|
----
|
|
|
|
Esa única línea al final del todo del archivo no está dentro de ninguna función,
|
|
y es fácil pasarla por alto. Lua lee tu archivo de arriba abajo cuando Singe lo
|
|
carga, y una línea en el nivel exterior del archivo se ejecuta en ese mismo
|
|
momento, una vez, antes de que el motor haya llamado a nada. Todo lo que hay
|
|
encima son definiciones -- aquí hay un valor, aquí hay una función -- y esta es
|
|
la única instrucción que ocurre al cargar. Tiene que ir al final, porque
|
|
`makeStars` no existe hasta que se ha leído la línea que la crea.
|
|
|
|
[source,lua]
|
|
----
|
|
local star = stars[i]
|
|
star.y = star.y + star.speed
|
|
----
|
|
|
|
`star` es una variable local que se crea de nuevo en cada vuelta del bucle, y
|
|
guarda la misma tabla que está en la lista -- no una copia. Cambiar `star.y`
|
|
mueve la estrella que está en la lista. Si las tablas se copiaran, este bucle
|
|
movería ciento veinte copias por fotograma y nada cambiaría nunca en pantalla.
|
|
|
|
[source,lua]
|
|
----
|
|
for _, star in ipairs(stars) do
|
|
drawStar(star)
|
|
end
|
|
----
|
|
|
|
Compáralo con la lección cuatro: dos bucles anidados, dos cuentas por estrella,
|
|
un resto para mantener cada una en la pantalla, y tres capas para fingir la
|
|
variedad que ahora tienes gratis. El dibujo ya no sabe ni le importa cómo
|
|
llegaron las estrellas adonde están, cuántas hay ni qué les pasa después. Cada
|
|
parte del programa hace un trabajo, y la lista es en lo que todas se ponen de
|
|
acuerdo.
|
|
|
|
[#_try_it_6]
|
|
=== Pruébalo
|
|
|
|
. *Hazlo más profundo.* Cambia `math.random(1, 3)` por `math.random(1, 6)` y
|
|
`STAR_SHADE` por `25`. Más velocidades, más tonos, un cielo más profundo.
|
|
. *Menos estrellas, ráfaga más grande.* Pon `STAR_COUNT` a `10` y `BURST_COUNT`
|
|
a `100`, y luego pulsa el botón tres o cuatro veces. Mira cómo sube la cuenta
|
|
en pantalla, y cronometra cuánto tarda en vaciarse.
|
|
. *Dale a una estrella otro campo.* Añade `age = 0` a `newStar`, suma uno a
|
|
`star.age` en `moveStars` y pon `stars[1].age` en pantalla junto a la cuenta.
|
|
No hace falta cambiar nada más del programa para que cada estrella lleve un
|
|
dato nuevo.
|
|
. *Deja de quitarlas.* Borra las tres líneas del bloque `if star.y >= height`
|
|
de `moveStars` y ejecútalo. Espera diez segundos, luego lee la cuenta en
|
|
pantalla y mira el cielo. Averigua cómo pueden ser ciertas las dos cosas a la
|
|
vez.
|
|
. *Hazlo hacia delante.* Cambia `for i = #stars, 1, -1` por `for i = 1, #stars`
|
|
y ejecútalo hasta que se caiga. Luego vuelve a leer la sección de antes con el
|
|
error de verdad delante.
|
|
|
|
[#_break_it_on_purpose_6]
|
|
=== Rómpelo a propósito
|
|
|
|
Escribe mal el nombre de un campo. En `moveStars`, cambia `star.speed` por
|
|
`star.sped`:
|
|
|
|
[source,lua]
|
|
----
|
|
star.y = star.y + star.sped
|
|
----
|
|
|
|
Ejecútalo:
|
|
|
|
----
|
|
Error executing function 'onOverlayUpdate': tables/tables.singe:60: attempt to perform arithmetic on a nil value (field 'sped')
|
|
stack traceback:
|
|
tables/tables.singe:60: in upvalue 'moveStars'
|
|
tables/tables.singe:124: in function 'onOverlayUpdate'
|
|
----
|
|
|
|
Tus números de línea no serán exactamente estos a menos que tu archivo coincida
|
|
línea a línea con el de arriba. Lo que lees es el nombre del archivo, el número
|
|
que va detrás y la queja.
|
|
|
|
Lee primero el final de la primera línea. `field 'sped'` es Lua diciéndote qué
|
|
nombre ha salido vacío, y te está diciendo que era un campo -- algo detrás de
|
|
un punto -- y no una variable tuya. «Attempt to perform arithmetic on a nil
|
|
value» es lo que estaba haciendo cuando se dio cuenta: sumar `nil` a un número,
|
|
cosa que no se puede hacer.
|
|
|
|
No hubo ninguna queja cuando la tabla se creó sin un campo `sped`, ni en el
|
|
momento en que lo pediste. Una tabla te da `nil` por cualquier nombre que no
|
|
tenga, sin decir nada. El error llega más tarde, en la primera línea que intenta
|
|
*usar* la nada que te dieron, y eso puede estar muy lejos de la errata. Cuando
|
|
veas `nil value (field 'something')`, busca el sitio donde ese nombre está
|
|
escrito de otra forma.
|
|
|
|
[#_what_you_learned_6]
|
|
=== Lo que has aprendido
|
|
|
|
* Las llaves crean una tabla, el único contenedor que tiene Lua. Una tabla usada
|
|
como lista guarda elementos en fila, y los corchetes con un índice sacan uno.
|
|
* Lua cuenta desde uno. Casi todos los demás lenguajes cuentan desde cero.
|
|
* `#list` es cuántos elementos tiene, y `list[#list]` es el último.
|
|
* `table.insert` añade un elemento, al final o en la posición que digas;
|
|
`table.remove` saca uno y cierra el hueco.
|
|
* Un índice o un campo que nunca se ha llenado es `nil`, y nada te avisa hasta
|
|
que lo usas.
|
|
* `ipairs` recorre una lista y te da el índice y el elemento; `_` es la
|
|
costumbre para un índice que no quieres.
|
|
* Una tabla usada como registro guarda campos con nombre, a los que se llega con
|
|
un punto.
|
|
* Una lista de registros es como un juego guarda sus enemigos, sus balas y todo
|
|
lo demás de lo que hay más de uno.
|
|
* Las tablas se comparten, no se copian, cuando las pasas de un lado a otro.
|
|
* Cuando un bucle pueda quitar elementos de la lista que recorre, recórrela
|
|
hacia atrás.
|
|
|
|
[#_next_time_6]
|
|
=== La próxima vez
|
|
|
|
Ya tienes todas las piezas de un juego: cosas que se mueven, una nave que
|
|
responde a los controles, decisiones, bucles, funciones y una lista en la que
|
|
las cosas pueden entrar y de la que pueden salir mientras el juego se ejecuta.
|
|
|
|
La lección siete las junta en un juego en el que puedes perder. Te mueves por la
|
|
parte de abajo de la pantalla, caen bloques desde arriba y tú te apartas de su
|
|
camino -- y los bloques que caen son una lista de registros, creados, movidos y
|
|
quitados exactamente igual que tus estrellas.
|