[#_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.