singe/docs/es/lessons/09-pictures.adoc

789 lines
28 KiB
Text

[#_lesson_9_pictures]
== Lección 9: Imágenes
image::learn/09-pictures.png[La lección terminada, 480]
Todo lo que has dibujado hasta ahora lo dibujaste con cajas, líneas y el texto
sencillo del propio motor. Fue a propósito: ocho lecciones sin arte eran ocho
lecciones en las que nada podía salir mal salvo tu programa. Ahora las cajas se
convierten en una nave y unas rocas.
Una imagen que el motor dibuja en el overlay se llama *sprite*. Esta lección
los carga, los dibuja, los mide, los pone en el orden correcto y recorre una
imagen que guarda varios fotogramas de una animación. Al final, el juego de la
lección siete parece un juego.
[#_the_art_kit]
=== El kit de arte
No tienes que dibujar nada. Este libro trae una pequeña carpeta de arte, y todo
lo de esta lección y de la siguiente la usa.
Busca la carpeta `art` que vino con el libro y copia la carpeta entera en la
carpeta de esta lección, junto a tu script. Deberías acabar con esto:
----
singe/ <- tu carpeta de trabajo
Singe/
data/
rocks/
rocks.singe
art/
spaceship.png
asteroid.png
bullet.png
star.png
walk.png
shoot.wav
boom.wav
----
Tu arte va dentro de la propia carpeta de tu juego, y la carpeta `Singe` del
motor se queda donde está, fuera, en la carpeta de trabajo. Eso importa dentro
de un momento, porque decide cómo nombras un archivo cuando lo cargas.
La carpeta `Singe` es la que el motor desempaquetó para ti la primerísima vez
que lo ejecutaste, allá en la lección uno. Vale la pena saber qué hay dentro,
porque esos archivos también puedes usarlos tú y todos los lectores de este
libro los tienen:
* `Singe/click.wav` -- un clic corto, que el menú del propio motor usa para su
prueba de sonido.
* `Singe/FreeSansBold.ttf` -- una fuente TrueType, para la lección diecinueve.
* `Singe/menuIntro.flac` -- la música que suena en el menú del motor. La usarás
en la lección diez.
* `Singe/menuBackground.mkv` -- un vídeo, para la lección catorce.
* `Singe/DragonModel.glb` y `Singe/SingeText.glb` -- dos modelos 3D, para la
lección veintitrés.
* `Singe/missing.png` -- la imagen que dibuja el motor cuando un juego pide una
que no encuentra.
Las imágenes del kit son pequeñas a propósito. La nave mide 32 píxeles de ancho
y 24 de alto, una roca 24 por 24, un disparo 4 por 10 y una estrella 8 por 8.
Suena diminuto hasta que recuerdas lo grande que es en realidad la pantalla,
que es lo siguiente que hay que aclarar.
[#_how_big_the_screen_is]
=== Cuánto mide la pantalla
Sin un vídeo que reproducir, Singe le da a tu juego un lienzo de 720 píxeles de
ancho y 480 de alto, y la superficie en la que dibujas -- el *overlay* -- mide
la mitad en cada dirección: *360 por 240*. Singe lo estira para que llene la
ventana, así que una nave de 32 píxeles ocupa más o menos una décima parte del
ancho de la pantalla, que es aproximadamente lo que ocupa una nave en un juego
de recreativa.
Nunca escribas 360 y 240 en tu programa. Pregunta:
[source,lua]
----
local screenWidth = overlayGetWidth()
local screenHeight = overlayGetHeight()
----
Esas dos te dan el tamaño del overlay, y un juego que pregunta es un juego que
sigue funcionando cuando alguien lo ejecuta a otro tamaño.
Hay una cosa que no cuenta en esos píxeles: `overlayPrint`, de la lección uno,
cuenta en celdas de carácter. Cada celda mide 6 píxeles de ancho y 13 de alto,
así que un overlay de 360 por 240 son 60 columnas por 18 filas de texto. Los
sprites van en píxeles, `overlayPrint` va en celdas, y confundir las dos cosas
te puede costar una tarde de desconcierto si te dejas.
[#_one_picture_on_screen]
=== Una imagen en pantalla
Empieza un archivo nuevo junto a tu carpeta `art` y escribe esto.
[source,lua]
----
dofile("Singe/Framework.singe")
local ship = spriteLoad(DIR .. "art/spaceship.png")
function onOverlayUpdate()
overlayClear()
spriteDraw(ship, 160, 100)
return OVERLAY_UPDATED
end
function onShutdown()
spriteUnload(ship)
end
----
Ejecútalo. Una pequeña nave azul aparece cerca del centro de una pantalla
negra, apuntando hacia arriba.
[#_what_just_happened_5]
=== Qué acaba de pasar
Cuatro cosas nuevas, de una en una.
[source,lua]
----
local ship = spriteLoad(DIR .. "art/spaceship.png")
----
`spriteLoad` lee un archivo de imagen del disco, lo decodifica y guarda la
imagen en memoria. Lo que te devuelve no es la imagen. Es un *handle*: un
número normal y corriente, un resguardo, la forma que tiene el motor de decir
«ese». Le devuelves ese número al motor cada vez que quieres hacer algo con la
imagen, y nunca necesitas saber ni preocuparte de qué número es.
Imprímelo si quieres -- `debugPrint(ship)` de la lección ocho te enseñará un
número pequeño -- pero el número en sí no es asunto tuyo, y nada en tu programa
debería depender nunca de cuál resulte ser. La única regla es que lo guardes en
algún sitio donde puedas volver a cogerlo, que es justo para lo que sirve una
variable.
`DIR` también es nuevo, y resuelve un problema que crea la organización que
usas desde la lección uno. Singe busca los archivos a partir de la carpeta en
la que estabas cuando lo arrancaste, *no* a partir de tu script. Lo arrancas
desde tu carpeta de trabajo, así que un simple `art/spaceship.png` hace que el
motor busque `singe/art/spaceship.png`, fuera, junto a la carpeta `Singe`. Tu
arte no está ahí. Está un nivel más abajo, en `rocks/art/spaceship.png`, con tu
script.
`DIR` cierra ese hueco. Es un trozo de texto que guarda la carpeta en la que
está tu script, con la barra ya puesta al final, así que
`DIR .. "art/spaceship.png"` -- unir dos trozos de texto con `..`, cosa que
haces desde la primera parte -- nombra la imagen correctamente sea cual sea la
forma en que se arrancó el juego, y la sigue nombrando correctamente cuando
mueves la carpeta entera a otro sitio.
`DIR` es uno de los nombres que define `Singe/Framework.singe`, que es para lo
que está la línea `dofile` del principio de tu script. Conociste esa línea y lo
que contiene en la lección siete, y este es el momento en que se gana su sitio:
sin ella no hay `DIR`, y sin `DIR` el motor no encuentra tu nave.
Usa `DIR` en cada archivo que cargue tu juego, desde aquí hasta el final del
libro.
[source,lua]
----
spriteDraw(ship, 160, 100)
----
Dibuja el sprite de ese handle, en esa posición. Como todas las llamadas de
dibujo, solo funciona dentro de `onOverlayUpdate`.
La posición es la *esquina superior izquierda* de la imagen, no su centro. Pide
`(160, 100)` y el píxel de más arriba a la izquierda de la nave cae ahí, y el
resto cuelga hacia abajo y hacia la derecha. Esto le pasa a todo el mundo una
vez, normalmente cuando intenta centrar algo y le sale desplazado hacia abajo y
a la derecha la mitad de su propio tamaño.
Si prefieres dar el centro, dilo:
[source,lua]
----
spriteDraw(ship, 160, 100, true)
----
Un cuarto argumento `true` significa «trata ese punto como el centro». Las dos
formas están en la entrada de `spriteDraw` en la referencia, junto con otras
dos que estiran la imagen hasta llenar un rectángulo.
[source,lua]
----
function onShutdown()
spriteUnload(ship)
end
----
`onShutdown` es un callback, como `onOverlayUpdate` y los dos callbacks de
entrada de la lección tres. El motor lo llama una vez, cuando tu juego está
terminando. Es donde devuelves lo que cogiste: `spriteUnload` libera la imagen
y tira el handle.
En rigor, podrías no ponerlo. El motor libera todo lo que cargó tu script
cuando termina el juego, así que un `spriteUnload` olvidado no es una fuga.
Escríbelo de todos modos. Cuesta una línea, dice qué cosas son de tu programa,
y dentro de un momento verás el caso en el que olvidarse de descargar sí
importa de verdad.
Un aviso sobre ese handle: una vez que has descargado un sprite, el número está
muerto. Dibujar con él no se limita a no dibujar nada: termina tu juego con un
error.
[#_transparency_and_why_it_is_a_png]
=== La transparencia, y por qué es un PNG
La nave es un triángulo con dos aletas. El archivo en el que vive es un
rectángulo, de 32 por 24, porque todos los archivos de imagen son rectángulos.
Entonces, ¿qué ha pasado con las esquinas?
Son *transparentes*. Un PNG puede decir, para cada píxel, lo sólido que es, y
las imágenes del kit dicen «aquí no hay nada» para cada píxel que queda fuera
de la forma. Singe lo respeta, así que el fondo negro se ve a través de las
esquinas y la nave tiene un contorno en lugar de una caja alrededor.
Esta es la razón de que el kit sea PNG y no JPEG. Un JPEG no puede guardar
transparencia en absoluto -- te daría la nave dentro de una caja gris -- y
además emborrona los bordes nítidos, que es lo último que necesita el arte
pequeño. Para el arte de un juego, PNG. Para una fotografía que llena toda la
pantalla y no tiene partes transparentes, JPEG está bien y ocupa mucho menos.
La entrada de `spriteLoad` en la referencia enumera todos los formatos que Singe
sabe leer, y son muchos.
Pruébalo tú mismo: cambia el negro por otra cosa dibujando una caja detrás de
la nave antes de dibujar la nave. Las esquinas mostrarán lo que haya detrás,
que es de lo que se trata.
[#_do_not_load_while_you_draw]
=== No cargues mientras dibujas
Este es el error que esta sección existe para evitar. Parece completamente
razonable:
[source,lua]
----
function onOverlayUpdate()
overlayClear()
local ship = spriteLoad(DIR .. "art/spaceship.png")
spriteDraw(ship, 160, 100)
return OVERLAY_UPDATED
end
----
Ejecútalo y funciona. Dibuja la nave. Nada se queja. No tendrías ningún motivo
para pensar que algo va mal.
`onOverlayUpdate` se ejecuta unas sesenta veces por segundo. Eso son sesenta
copias nuevas de la nave cargadas en memoria cada segundo, tres mil seiscientas
por minuto, y ni una sola se descarga nunca. El juego funciona de maravilla
durante dos minutos, se vuelve más lento, y luego la máquina se queda sin
memoria y el juego se muere. Peor aún: se muere durante la demostración y no
mientras lo estabas probando.
La regla: *carga una vez, dibuja muchas*. Cargar es lento y ocupa memoria.
Dibujar es rápido y no ocupa nada. Así que `spriteLoad` va al principio de tu
archivo, fuera de todas las funciones, donde se ejecuta una sola vez al empezar
el script.
Esto no va solo de sprites. Todos los `somethingLoad` del motor funcionan así
-- los sonidos en la lección diez, las fuentes en la lección diecinueve, los
modelos en la lección veintitrés. Si te descubres escribiendo `Load` dentro de
una función que se ejecuta en cada fotograma, para y sácalo de ahí.
[#_how_big_is_it]
=== ¿Cuánto mide?
Sabes que la nave mide 32 por 24 porque te lo ha dicho este libro. Tu programa
no lo sabe, y no debería tener que saberlo. Pregunta:
[source,lua]
----
local shipWidth = spriteGetWidth(ship)
local shipHeight = spriteGetHeight(ship)
----
Ahora puedes mantener la nave dentro de la pantalla, porque la posición más a
la derecha en la que puede estar es `screenWidth - shipWidth`, y ponerla en el
centro, porque el centro es `(screenWidth - shipWidth) / 2`. Ninguna de las dos
líneas tiene un número que hubiera que cambiar si alguien volviera a dibujar la
nave un poco más grande.
Las dos dan el tamaño *tal como se dibujaría el sprite ahora mismo*. Si alguna
vez escalas o giras un sprite, con `spriteScale` o `spriteRotate`, la respuesta
cambia para coincidir. Normalmente es lo que quieres, y te sorprenderá
exactamente una vez.
[#_drawing_order]
=== El orden de dibujo
Los sprites no se mezclan. Lo de después tapa lo de antes, píxel a píxel,
exactamente igual que pegar recortes de papel en una pared. Así que el orden de
tus llamadas a `spriteDraw` es el orden de atrás hacia delante:
[source,lua]
----
drawStars()
drawRocks()
drawShots()
spriteDraw(shipSprite, shipX, shipY)
drawHud()
----
Las estrellas son lo que está más al fondo, así que van primero. La nave está
delante de todo lo que hay en la zona de juego. La puntuación va encima de
todo. Hazlo al revés y tu nave tan cuidadosamente dibujada se pasará la partida
escondida detrás del campo de estrellas, y no habrá ningún error que te lo
diga -- solo una nave que no puedes ver.
`overlayClear` sigue yendo lo primero, antes de todo eso. Dibujar nunca
sustituye, solo tapa, así que el fotograma empieza vacío cada vez.
[#_frames]
=== Fotogramas
Abre `art/walk.png` en cualquier visor de imágenes. No es la imagen de una
persona. Es la imagen de cuatro personas, una junto a otra: la misma figura en
cuatro momentos de un paso, cada una de 24 píxeles de ancho en una tira de 96.
Una imagen que guarda varios fotogramas de una animación es una *hoja de
sprites*, y es como se guarda casi toda la animación 2D. Un archivo, una carga,
un handle, y el juego elige qué trozo dibujar.
Crea un segundo archivo, `walk.singe`, junto al primero:
[source,lua]
----
dofile("Singe/Framework.singe")
local walk = spriteLoadFrames(4, DIR .. "art/walk.png")
local walkFrame = 1
local walkTick = 0
local walkX = 0
function onOverlayUpdate()
overlayClear()
walkTick = walkTick + 1
if walkTick >= 8 then
walkTick = 0
walkFrame = walkFrame + 1
if walkFrame > 4 then
walkFrame = 1
end
end
walkX = walkX + 1
if walkX > overlayGetWidth() then
walkX = -spriteFrameWidth(walk)
end
spriteDrawFrame(walk, walkX, 100, walkFrame)
return OVERLAY_UPDATED
end
function onShutdown()
spriteUnload(walk)
end
----
Ejecútalo con `Singe rocks/walk.singe`. Una figurita cruza la pantalla caminando a paso
constante, desaparece por el borde derecho y vuelve a entrar por la izquierda.
`spriteLoadFrames` es `spriteLoad` con una cosa más que decir: cuántos
fotogramas hay en la tira. *La cuenta va primero*, antes del nombre del
archivo, que es lo contrario de lo que casi todo el mundo supone. Divide el
ancho de la imagen entre la cuenta y recuerda los trozos. Cuatro fotogramas en
una tira de 96 píxeles son cuatro fotogramas de 24 píxeles de ancho. Equivócate
con la cuenta -- pon 3, por ejemplo -- y tendrás tres trozos de 32 píxeles,
cada uno con partes de dos figuras, que es una forma memorablemente tonta de
descubrir que has escrito el número equivocado.
`spriteDrawFrame` dibuja un trozo. Recibe el handle, dónde ponerlo y qué
fotograma, *en ese orden*, y los fotogramas se numeran *desde 1*: el fotograma
1 es la figura de más a la izquierda y el fotograma 4 la de más a la derecha.
Cuidado con ese 1. En otras partes de la familia de los sprites --
`spriteSetFrame` y `spriteGetFrame` -- los fotogramas se numeran desde 0. Es un
accidente histórico, las dos están en la referencia, y este libro usa
`spriteDrawFrame` y cuenta desde 1.
`spriteFrameWidth` da el ancho de un fotograma, 24 aquí, en lugar de los 96 de
la tira entera. `spriteFrameHeight` es su pareja. Cuando quieras saber cuánto
mide de ancho un trozo de una hoja, esas son las dos a las que preguntar, y no
`spriteGetWidth`.
El número de fotograma es un número en una variable como cualquier otro, así
que caminar es aritmética:
[source,lua]
----
walkTick = walkTick + 1
if walkTick >= 8 then
walkTick = 0
walkFrame = walkFrame + 1
if walkFrame > 4 then
walkFrame = 1
end
end
----
Cuenta los fotogramas que van pasando. Cada ocho, pasa a la imagen siguiente, y
después de la cuarta imagen vuelve a la primera. Sesenta fotogramas por segundo
entre ocho son entre siete y ocho pasos por segundo, lo que para un ciclo de
cuatro fotogramas es un paso ligero pero creíble. Cambia el 8 y cambias la
velocidad: cuanto más grande, más lento.
Puede que te preguntes por qué cuentas tú, cuando el motor tiene `spritePlay`
para reproducir una animación por ti. Lo tiene, y para un GIF animado es la
respuesta correcta, porque un GIF lleva dentro del archivo la duración de cada
uno de sus fotogramas. Una tira normal como `walk.png` no lleva ninguna
duración, así que el motor la reproduce tan deprisa como se le permite -- cien
fotogramas por segundo, veinticinco ciclos completos de paso cada segundo, un
borrón. Cuando los fotogramas vienen de una tira, cuenta tú.
[#_the_game_gets_its_artwork]
=== El juego recibe su arte
Ahora júntalo todo. El juego de la lección siete movía una caja por la parte de
abajo de la pantalla y le dejaba caer otras cajas encima. Cada caja se
convierte en una imagen, y ya que estás, la nave recibe algo con lo que
disparar.
Los cambios van todos juntos, así que aquí está el script entero. Es más largo
que cualquier cosa que hayas escrito, y no tiene nada que no conozcas ya.
[source,lua]
----
dofile("Singe/Framework.singe")
local SHIP_SPEED = 3
local SHOT_SPEED = 6
local ROCK_COUNT = 6
local STAR_COUNT = 40
local START_LIVES = 3
local HIT_SCORE = 10
local shipSprite = spriteLoad(DIR .. "art/spaceship.png")
local rockSprite = spriteLoad(DIR .. "art/asteroid.png")
local shotSprite = spriteLoad(DIR .. "art/bullet.png")
local starSprite = spriteLoad(DIR .. "art/star.png")
local screenWidth = overlayGetWidth()
local screenHeight = overlayGetHeight()
local shipWidth = spriteGetWidth(shipSprite)
local shipHeight = spriteGetHeight(shipSprite)
local rockWidth = spriteGetWidth(rockSprite)
local rockHeight = spriteGetHeight(rockSprite)
local shotWidth = spriteGetWidth(shotSprite)
local shotHeight = spriteGetHeight(shotSprite)
local shipX = (screenWidth - shipWidth) / 2
local shipY = screenHeight - shipHeight - 4
local goLeft = false
local goRight = false
local rocks = {}
local shots = {}
local stars = {}
local score = 0
local lives = START_LIVES
local over = false
function drawHud()
overlayPrint(1, 1, "SCORE " .. score)
overlayPrint(50, 1, "LIVES " .. lives)
if over then
overlayPrint(25, 8, "GAME OVER")
overlayPrint(17, 10, "PRESS 1 TO PLAY AGAIN")
end
end
function drawRocks()
for _, rock in ipairs(rocks) do
spriteDraw(rockSprite, rock.x, rock.y)
end
end
function drawShots()
for _, shot in ipairs(shots) do
spriteDraw(shotSprite, shot.x, shot.y)
end
end
function drawStars()
for _, star in ipairs(stars) do
spriteDraw(starSprite, star.x, star.y)
end
end
function newRock(rock)
rock.x = math.random(0, screenWidth - rockWidth)
rock.y = -rockHeight - math.random(0, 160)
rock.speed = math.random(8, 20) / 10
end
function onInputPressed(what)
if over then
if what == SWITCH_START1 then
startGame()
end
return
end
if what == SWITCH_LEFT then
goLeft = true
elseif what == SWITCH_RIGHT then
goRight = true
elseif what == SWITCH_BUTTON1 then
shots[#shots + 1] = { x = shipX + shipWidth / 2 - shotWidth / 2, y = shipY }
end
end
function onInputReleased(what)
if what == SWITCH_LEFT then
goLeft = false
elseif what == SWITCH_RIGHT then
goRight = false
end
end
function onOverlayUpdate()
overlayClear()
updateStars()
if not over then
updateShip()
updateShots()
updateRocks()
end
drawStars()
drawRocks()
drawShots()
spriteDraw(shipSprite, shipX, shipY)
drawHud()
return OVERLAY_UPDATED
end
function onShutdown()
spriteUnload(shipSprite)
spriteUnload(rockSprite)
spriteUnload(shotSprite)
spriteUnload(starSprite)
end
function overlapping(ax, ay, aw, ah, bx, by, bw, bh)
if ax + aw <= bx then
return false
end
if bx + bw <= ax then
return false
end
if ay + ah <= by then
return false
end
if by + bh <= ay then
return false
end
return true
end
function startGame()
shipX = (screenWidth - shipWidth) / 2
score = 0
lives = START_LIVES
over = false
shots = {}
for i = 1, ROCK_COUNT do
rocks[i] = {}
newRock(rocks[i])
end
end
function updateRocks()
for _, rock in ipairs(rocks) do
rock.y = rock.y + rock.speed
if rock.y > screenHeight then
newRock(rock)
end
if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then
newRock(rock)
lives = lives - 1
if lives <= 0 then
over = true
end
end
end
end
function updateShip()
if goLeft then
shipX = shipX - SHIP_SPEED
end
if goRight then
shipX = shipX + SHIP_SPEED
end
if shipX < 0 then
shipX = 0
end
if shipX > screenWidth - shipWidth then
shipX = screenWidth - shipWidth
end
end
function updateShots()
for i = #shots, 1, -1 do
local shot = shots[i]
local gone = false
shot.y = shot.y - SHOT_SPEED
if shot.y + shotHeight < 0 then
gone = true
end
for _, rock in ipairs(rocks) do
if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then
newRock(rock)
score = score + HIT_SCORE
gone = true
end
end
if gone then
table.remove(shots, i)
end
end
end
function updateStars()
for _, star in ipairs(stars) do
star.y = star.y + star.speed
if star.y > screenHeight then
star.y = 0
star.x = math.random(0, screenWidth - 1)
end
end
end
for i = 1, STAR_COUNT do
stars[i] = { x = math.random(0, screenWidth - 1), y = math.random(0, screenHeight - 1), speed = math.random(1, 3) / 4 }
end
startGame()
----
Las flechas izquierda y derecha dirigen la nave. La barra espaciadora dispara.
Cuando las rocas se han llevado tu última vida, `1` empieza una partida nueva.
Vale la pena señalar algunas cosas de ahí dentro.
Las cuatro llamadas a `spriteLoad` y las ocho medidas están arriba, fuera de
todas las funciones, así que ocurren una vez. Todo lo que viene después está
escrito en términos de `screenWidth`, `shipWidth` y compañía, y no hay ni una
cantidad de píxeles a pelo en todo el código del juego.
Las funciones están en orden alfabético, que es una costumbre que vale la pena
coger ya. A Lua no le importa lo más mínimo -- es para ti, dentro de seis
meses, buscando `updateShots` en un archivo con cuarenta funciones.
Las dos últimas cosas del archivo no son funciones. Son instrucciones normales
al final del script: construir el campo de estrellas y luego llamar a
`startGame` para preparar las rocas y poner a cero la puntuación. Se ejecutan
una vez, en orden, al cargarse el script, y tienen que ir después de las
funciones que usan.
`overlapping` son las cuatro comparaciones que la lección siete decía que podías
escribir tú en lugar de llamar a `collideRects`, escritas como una función con
los mismos ocho argumentos. Dos rectángulos no se tocan
si uno está completamente a la izquierda, a la derecha, por encima o por debajo
del otro; si ninguna de esas cuatro cosas es cierta, se solapan. La lección
once la sustituye por algo que pone el motor, y explica por qué el rectángulo
es la forma equivocada para una nave.
Una última nota para cuando leas código de Singe de otras personas. Todas las
llamadas de sprites de este libro reciben el handle *primero*. Singe 2.10 lo
ponía al final, y un juego puede pedirle al motor que siga haciéndolo así, de
modo que el código que encuentres en internet puede decir
`spriteDraw(x, y, ship)`. No lo copies. El handle primero es el orden actual y
el que documenta la referencia.
[#_try_it_9]
=== Pruébalo
. *Centra la nave en su posición.* Añade `true` como cuarto argumento al
`spriteDraw` de la nave. Fíjate en adónde salta la nave, y deduce por la
distancia que se ha movido qué ha hecho de verdad el cuarto argumento.
. *Más rocas.* Cambia `ROCK_COUNT` a 20. Luego a 200. En algún punto entre esos
dos el juego deja de ser jugable, y en algún punto mucho más allá el juego
empieza a ir más lento. Encuentra los dos.
. *Agranda la nave.* Añade `spriteScale(shipSprite, 2)` al final del todo del
archivo, en la línea de encima de `startGame()`. Ejecútalo y lleva la nave
contra el borde derecho: la nave mide el doble pero sigue parándose donde se
paraba la pequeña, y se sale por el borde derecho y por abajo. Averigua por
qué, y luego sube esa línea por encima de las llamadas a `spriteGetWidth` y
mira cómo se arregla. La entrada de `spriteScale` en la referencia explica lo que
hace el escalado con las medidas.
. *Mete al caminante.* Carga `walk.png` en el juego, y dibuja la figura que
camina por la parte de arriba de la pantalla como decorado. Necesitarás el
conteo de fotogramas del script del paseo, y tendrás que decidir en qué parte
de `onOverlayUpdate` va el dibujo -- delante de las estrellas, detrás de las
rocas.
. *Descarga algo que todavía estás usando.* Añade `spriteUnload(starSprite)` al
final del archivo, después de `startGame()`, y ejecútalo. Lee lo que te sale.
[#_break_it_on_purpose_8]
=== Rómpelo a propósito
Las mayúsculas importan, y el arte es donde más muerden. Cambia la carga de la
nave para que use una S mayúscula:
[source,lua]
----
local shipSprite = spriteLoad(DIR .. "art/Spaceship.png")
----
El juego se niega a arrancar:
----
10:spriteLoad: Couldn't open rocks/art/Spaceship.png: No such file or directory
----
Léelo igual que el error de la lección uno, pero fíjate en que la forma es un
poco distinta. `10` es la línea. `spriteLoad` es la función del motor que se ha
rendido, en lugar de un nombre de archivo -- cuando la queja viene de Singe y
no de Lua, esto es lo que te sale. Luego la razón, que aquí son las palabras
del propio sistema operativo y cambiará un poco de una máquina a otra.
`No such file or directory` es el ordenador siendo preciso, no antipático. De
verdad no hay ningún archivo llamado `Spaceship.png`; hay uno llamado
`spaceship.png`, y para una máquina Linux son dos nombres distintos. En Windows,
y en la mayoría de los Mac, habría funcionado, lo cual es peor, porque significa que el fallo viaja
al ordenador de otra persona y aparece allí por primera vez.
La otra mitad de este error con la que te encontrarás es la carpeta. Si pusiste
el kit de arte en el sitio equivocado, o arrancaste Singe desde la carpeta
equivocada, el mensaje es exactamente el mismo y el arreglo no. La ruta del
mensaje ayuda: es el nombre completo que probó Singe, contado desde la carpeta
en la que lo arrancaste, y el `rocks/` del principio es la parte que añadió
`DIR`. Comprueba tres cosas, en este orden: si el archivo se llama exactamente como lo escribiste, si
está en la carpeta `art`, y si esa carpeta `art` está junto a tu script.
[#_what_you_learned_9]
=== Lo que has aprendido
* Una imagen dibujada en el overlay es un sprite, y se carga con `spriteLoad`.
* `spriteLoad` te devuelve un handle: un número que para el motor significa
«esa imagen», y que guardas en una variable.
* `DIR` es la carpeta en la que está tu script, y cada archivo que cargue tu
juego debería nombrarse con él.
* `spriteDraw(handle, x, y)` lo dibuja, desde su esquina superior izquierda, y
solo dentro de `onOverlayUpdate`. Un cuarto argumento `true` hace que la
posición sea el centro.
* Carga una vez, al principio del archivo. Cargar dentro de `onOverlayUpdate`
se come la memoria hasta que el juego se muere.
* `spriteUnload` devuelve una imagen, normalmente desde `onShutdown`, y después
el handle está muerto.
* El PNG lleva transparencia, y por eso las esquinas de la nave no son cajas
negras.
* `spriteGetWidth` y `spriteGetHeight` miden un sprite tal como se dibujaría
ahora, así que nunca tienes que escribir su tamaño en tu programa.
* Los sprites se tapan unos a otros. El orden en que dibujas va de atrás hacia
delante.
* `spriteLoadFrames(count, file)` corta una tira en fotogramas, y
`spriteDrawFrame(handle, x, y, frame)` dibuja uno de ellos, contando desde 1.
[#_next_time_9]
=== La próxima vez
El juego se ve bien y no suena a nada. En la lección diez los disparos tienen
un ruido, las rocas una explosión, y algo suena por debajo de todo -- y
conocerás tu quinto callback, el que usa el motor para decirte que un sonido ha
terminado.