789 lines
28 KiB
Text
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.
|