588 lines
22 KiB
Text
588 lines
22 KiB
Text
[#_lesson_17_light_guns]
|
|
== Lección 17: Pistolas de luz
|
|
|
|
image::learn/17-light-guns.png[La lección terminada, 480]
|
|
|
|
Vas a hacer una galería de tiro. Aparecen dianas sobre el vídeo en los
|
|
fotogramas que tú eliges, apuntas a la pantalla y aprietas un gatillo, y las que
|
|
aciertas puntúan.
|
|
|
|
No necesitas una pistola de luz para escribir esto. Necesitas un ratón, porque
|
|
para Singe una pistola de luz *es* un ratón: informa de una posición en las
|
|
mismas coordenadas, con las mismas llamadas, y su gatillo llega como el mismo
|
|
interruptor que un botón del ratón. Escribe el juego para el ratón de tu mesa y
|
|
funcionará en una recreativa con una pistola atornillada, que es la única forma
|
|
sensata de hacerlo. Una pistola no es otro tipo de entrada. Es un ratón que
|
|
alguien apunta.
|
|
|
|
[#_what_you_are_pointing_at]
|
|
=== A qué apuntas
|
|
|
|
Empieza igual que en la lección catorce, con el vídeo que el motor ha
|
|
desempaquetado para ti:
|
|
|
|
----
|
|
Singe -R -v Singe/menuBackground.mkv gallery
|
|
----
|
|
|
|
Esas imágenes son una cuadrícula morada y una puesta de sol. No son un atraco a
|
|
un banco. Todos los números de fotograma de esta lección se refieren a ellas para
|
|
que el código funcione el mismo día que lo escribes, y el clip entero mide 720
|
|
por 480 y dura unos 420 fotogramas, que a unos treinta fotogramas por segundo
|
|
son catorce segundos. Imagínate algo mejor. Al código le da igual lo que haya en
|
|
la imagen.
|
|
|
|
[#_match_the_overlay_to_the_video]
|
|
=== Ajusta el overlay al vídeo
|
|
|
|
Pon esto al principio de `gallery.singe`, debajo de la línea del `dofile`:
|
|
|
|
[source,lua]
|
|
----
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
----
|
|
|
|
Sin esa llamada, el overlay mide la *mitad* que el vídeo en cada dirección --
|
|
aquí, 360 por 240 -- y cada coordenada con la que dibujas, y cada posición del
|
|
ratón que te llega, está en ese espacio más pequeño. El overlay pequeño no tiene
|
|
nada de malo, pero es una conversión más que tener en la cabeza mientras
|
|
averiguas dónde ha caído un disparo. Ajustarlo al vídeo significa que una diana
|
|
en `x = 80` está a ochenta píxeles del borde de la imagen, y un disparo en
|
|
`x = 80` también. La entrada de la referencia de `overlaySetResolution` explica lo que
|
|
cuesta.
|
|
|
|
[#_where_the_gun_is_pointing]
|
|
=== Adónde apunta la pistola
|
|
|
|
Eso lo responde una sola llamada:
|
|
|
|
[source,lua]
|
|
----
|
|
local x, y = mouseGetPosition(0)
|
|
----
|
|
|
|
`mouseGetPosition` te devuelve dos números, algo que todavía no habías visto
|
|
hacer a una función y con lo que Lua está encantado: una función puede devolver
|
|
tantos valores como quiera, y tú los recoges poniendo a la izquierda del `=`
|
|
otros tantos nombres. El `0` dice *por qué* dispositivo apuntador preguntas,
|
|
contando desde cero. Con un ratón, o con una pistola, la respuesta es siempre el
|
|
dispositivo `0`.
|
|
|
|
Los números están en coordenadas del overlay, no en píxeles del escritorio, y por
|
|
eso valía la pena hacer la llamada de antes. La posición es la última de la que
|
|
informó el dispositivo, así que puedes pedirla cuando quieras y tantas veces
|
|
como quieras. Pídela en `onOverlayUpdate` y dibuja con la respuesta.
|
|
|
|
Hay otra forma de obtener los mismos números. `onMouseMoved` es un callback, como
|
|
`onOverlayUpdate`, y el motor lo llama cada vez que se mueve un puntero, y le
|
|
pasa la posición nueva. Es la elección correcta cuando quieres *reaccionar* al
|
|
movimiento. Para dibujar un punto de mira no quieres eso: quieres saber dónde
|
|
está la cosa ahora mismo, y `mouseGetPosition` te lo dice sin que tengas que
|
|
guardar una copia.
|
|
|
|
[#_drawing_a_crosshair]
|
|
=== Dibujar un punto de mira
|
|
|
|
El motor no te lo va a dibujar. Un punto de mira son cuatro líneas y un círculo:
|
|
|
|
[source,lua]
|
|
----
|
|
local function drawCrosshair(x, y)
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayCircle(x, y, 10)
|
|
overlayLine(x - 18, y, x - 4, y)
|
|
overlayLine(x + 4, y, x + 18, y)
|
|
overlayLine(x, y - 18, x, y - 4)
|
|
overlayLine(x, y + 4, x, y + 18)
|
|
end
|
|
----
|
|
|
|
Los huecos importan más que las líneas. Dejar los cuatro radios sin llegar al
|
|
círculo te da un agujero en el centro por el que ver la diana, que es todo el
|
|
sentido de un punto de mira y lo primero que los principiantes tapan al dibujar.
|
|
|
|
Una cosa que deberías preguntar antes de dibujarlo:
|
|
|
|
[source,lua]
|
|
----
|
|
local drawAim = singeWantsCrosshairs()
|
|
----
|
|
|
|
Eso devuelve `false` cuando el jugador arrancó Singe con `--nocrosshair`. Algunas
|
|
pistolas de luz ponen un punto propio en la pantalla, y dos puntos de mira a unos
|
|
pocos píxeles de distancia son peor que ninguno. La respuesta no puede cambiar
|
|
mientras el juego está en marcha, así que léela una vez al principio y guárdala
|
|
en una variable.
|
|
|
|
No tienes que ocultar el puntero del sistema, porque Singe ya lo ha hecho.
|
|
Captura el ratón al arrancar, lo que lo encierra en la ventana y oculta la flecha
|
|
del escritorio. Si alguna vez lo quieres de vuelta -- para un editor de niveles,
|
|
por ejemplo -- `mouseSetCaptured(false)` lo suelta, y la tecla asignada a
|
|
`INPUT_GRAB`, que es la `G` en los controles que vienen con Singe, activa y
|
|
desactiva lo mismo mientras juegas.
|
|
|
|
[#_the_trigger]
|
|
=== El gatillo
|
|
|
|
Un gatillo es un botón, y los botones llegan a `onInputPressed`, que viste por
|
|
primera vez en la lección tres:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what, device)
|
|
if what ~= SWITCH_BUTTON3 then
|
|
return
|
|
end
|
|
|
|
local x, y = mouseGetPosition(device or 0)
|
|
-- ...
|
|
end
|
|
----
|
|
|
|
`SWITCH_BUTTON3` es el botón izquierdo del ratón, y en una pistola es el gatillo.
|
|
El `controls.cfg` que viene con Singe asigna el botón izquierdo a
|
|
`SWITCH_BUTTON3`, el derecho a `SWITCH_BUTTON1` y el central a `SWITCH_BUTTON2`.
|
|
Esos números parecen barajados porque lo están: los interruptores son los botones
|
|
del panel de la recreativa, y qué control físico va cableado a cuál es asunto de
|
|
la recreativa, no del juego. La Mayús izquierda también está asignada a
|
|
`SWITCH_BUTTON3`, así que puedes disparar sin ratón mientras haces pruebas.
|
|
|
|
El segundo argumento es lo nuevo. Cuando el interruptor viene de un ratón o de
|
|
una pistola de luz, `device` es el índice de ese dispositivo -- `0` para el
|
|
primero, `1` para el segundo, y así sucesivamente. Cuando viene de una tecla o de
|
|
un mando es `nil`, porque no ha intervenido ningún dispositivo apuntador. Por eso
|
|
la llamada de arriba dice `device or 0`: si `device` es `nil`, usa `0` en su
|
|
lugar. Usarás `or` así constantemente, y se lee exactamente como suena --
|
|
«device, o nada, y en ese caso cero».
|
|
|
|
Ese argumento de más es lo que hace posible una recreativa de dos pistolas. La
|
|
pistola del jugador uno es el dispositivo `0` y la del jugador dos es el
|
|
dispositivo `1`, y el mismo `onInputPressed` las distingue sin que tengas que
|
|
adivinar.
|
|
|
|
[#_did_it_hit_anything]
|
|
=== ¿Ha dado en algo?
|
|
|
|
Ya sabes responder a esto. Es la lección once:
|
|
|
|
[source,lua]
|
|
----
|
|
if collidePointRect(x, y, target.x, target.y, TARGET_SIZE, TARGET_SIZE) then
|
|
----
|
|
|
|
Un disparo es un punto. Una diana es un rectángulo. `collidePointRect` pregunta
|
|
si el punto está dentro del rectángulo, y el borde cuenta como dentro. Disparar
|
|
no tiene nada de especial. La comprobación de impacto de un juego de pistolas es
|
|
la misma que usa un juego de plataformas para saber si has caído sobre un
|
|
bloque, y la familia `collide` de la referencia tiene el resto de las formas.
|
|
|
|
[#_targets_that_come_and_go]
|
|
=== Dianas que vienen y van
|
|
|
|
Lo único que cambia el vídeo es *cuándo* existe una diana. En la lección once,
|
|
una roca estaba ahí hasta que le disparabas. Aquí una diana está en pantalla
|
|
mientras la película muestra la parte de la escena a la que pertenece, y después
|
|
desaparece tanto si le has disparado como si no.
|
|
|
|
Eso son dos campos más en el registro, y la lección seis te dio la herramienta:
|
|
|
|
[source,lua]
|
|
----
|
|
local targets = {
|
|
{ first = 40, last = 110, x = 80, y = 110 },
|
|
{ first = 120, last = 190, x = 470, y = 140 },
|
|
{ first = 200, last = 270, x = 290, y = 250 },
|
|
{ first = 280, last = 360, x = 150, y = 300 },
|
|
}
|
|
----
|
|
|
|
Cuatro registros, cada uno con el fotograma en el que aparece la diana, el
|
|
fotograma en el que se va y dónde está. Añadir una quinta diana es añadir una
|
|
línea. Esa es la forma a la que tienes que recurrir cada vez que te veas a punto
|
|
de escribir las mismas cuatro líneas de código con números distintos.
|
|
|
|
Si una diana cuenta o no es entonces una pequeña función propia:
|
|
|
|
[source,lua]
|
|
----
|
|
local function targetIsUp(target, frame)
|
|
return not target.hit and frame >= target.first and frame <= target.last
|
|
end
|
|
----
|
|
|
|
El dibujo se lo pregunta, y el disparo también, y para eso está en un solo sitio:
|
|
una diana que puedes ver y una diana a la que puedes acertar nunca pueden estar
|
|
en desacuerdo.
|
|
|
|
[#_the_whole_thing_9]
|
|
=== Todo junto
|
|
|
|
[source,lua]
|
|
----
|
|
-- Aprende a programar con Singe -- Lección 17: Pistolas de luz
|
|
|
|
dofile("Singe/Framework.singe")
|
|
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
|
|
local FIRST_FRAME = 20
|
|
local LAST_FRAME = 400
|
|
local TARGET_SIZE = 96
|
|
local HIT_SCORE = 100
|
|
local FLASH_TIME = 12
|
|
|
|
local targets = {
|
|
{ first = 40, last = 110, x = 80, y = 110 },
|
|
{ first = 120, last = 190, x = 470, y = 140 },
|
|
{ first = 200, last = 270, x = 290, y = 250 },
|
|
{ first = 280, last = 360, x = 150, y = 300 },
|
|
}
|
|
|
|
local aimX = overlayGetWidth() / 2
|
|
local aimY = overlayGetHeight() / 2
|
|
local score = 0
|
|
local shots = 0
|
|
local hits = 0
|
|
local flash = 0
|
|
local flashX = 0
|
|
local flashY = 0
|
|
local drawAim = singeWantsCrosshairs()
|
|
|
|
|
|
local function drawCrosshair(x, y)
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayCircle(x, y, 10)
|
|
overlayLine(x - 18, y, x - 4, y)
|
|
overlayLine(x + 4, y, x + 18, y)
|
|
overlayLine(x, y - 18, x, y - 4)
|
|
overlayLine(x, y + 4, x, y + 18)
|
|
end
|
|
|
|
|
|
local function targetIsUp(target, frame)
|
|
return not target.hit and frame >= target.first and frame <= target.last
|
|
end
|
|
|
|
|
|
local function startRound()
|
|
score = 0
|
|
shots = 0
|
|
hits = 0
|
|
flash = 0
|
|
for _, target in ipairs(targets) do
|
|
target.hit = false
|
|
end
|
|
discSkipToFrame(FIRST_FRAME)
|
|
end
|
|
|
|
|
|
function onInputPressed(what, device)
|
|
if what == SWITCH_START1 then
|
|
startRound()
|
|
return
|
|
end
|
|
|
|
if what ~= SWITCH_BUTTON3 then
|
|
return
|
|
end
|
|
|
|
local x, y = mouseGetPosition(device or 0)
|
|
local frame = discGetFrame()
|
|
|
|
shots = shots + 1
|
|
|
|
for _, target in ipairs(targets) do
|
|
if targetIsUp(target, frame) and collidePointRect(x, y, target.x, target.y, TARGET_SIZE, TARGET_SIZE) then
|
|
target.hit = true
|
|
hits = hits + 1
|
|
score = score + HIT_SCORE
|
|
flash = FLASH_TIME
|
|
flashX = x
|
|
flashY = y
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
local frame = discGetFrame()
|
|
|
|
if frame >= LAST_FRAME then
|
|
startRound()
|
|
frame = FIRST_FRAME
|
|
end
|
|
|
|
aimX, aimY = mouseGetPosition(0)
|
|
|
|
overlayClear()
|
|
|
|
for _, target in ipairs(targets) do
|
|
if targetIsUp(target, frame) then
|
|
colorForeground(255, 200, 0, 255)
|
|
overlayBox(target.x, target.y, target.x + TARGET_SIZE - 1, target.y + TARGET_SIZE - 1)
|
|
overlayCircle(target.x + TARGET_SIZE / 2, target.y + TARGET_SIZE / 2, TARGET_SIZE / 3)
|
|
end
|
|
end
|
|
|
|
if flash > 0 then
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayCircle(flashX, flashY, 30 - flash * 2)
|
|
flash = flash - 1
|
|
end
|
|
|
|
overlayPrint(2, 1, "SCORE " .. score .. " HITS " .. hits .. "/" .. shots .. " FRAME " .. frame)
|
|
|
|
if drawAim then
|
|
drawCrosshair(aimX, aimY)
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
startRound()
|
|
----
|
|
|
|
Apunta a una caja amarilla y haz clic. El contador de la esquina sube, se abre un
|
|
anillo blanco donde has acertado y la caja desaparece. Si fallas, solo sube la
|
|
cuenta de disparos, que es el marcador más barato que existe y le dice al jugador
|
|
más sobre cómo lo está haciendo que la puntuación. La ronda vuelve a empezar sola
|
|
cuando se acaba la película, y `1` la reinicia cuando quieras.
|
|
|
|
[#_what_just_happened_13]
|
|
=== Qué acaba de pasar
|
|
|
|
[source,lua]
|
|
----
|
|
local frame = discGetFrame()
|
|
|
|
if frame >= LAST_FRAME then
|
|
startRound()
|
|
frame = FIRST_FRAME
|
|
end
|
|
----
|
|
|
|
El vídeo es el reloj, como en la lección catorce, y `discGetFrame` es la forma de
|
|
leerlo. Todo lo demás del fotograma se decide a partir de ese único número.
|
|
Cuando la película pasa del final de la última diana, la ronda vuelve a empezar:
|
|
`startRound` pone a cero la puntuación, marca todas las dianas como no
|
|
disparadas y manda el disco de vuelta al principio con `discSkipToFrame`. La
|
|
línea de debajo cambia también la copia local de `frame`, porque se leyó antes
|
|
del salto y todavía guarda el número viejo. Sin ella, el dibujo de más abajo pasaría un
|
|
fotograma trabajando con un número que ya no es verdad.
|
|
|
|
[source,lua]
|
|
----
|
|
shots = shots + 1
|
|
|
|
for _, target in ipairs(targets) do
|
|
if targetIsUp(target, frame) and collidePointRect(...) then
|
|
----
|
|
|
|
Cuenta el disparo antes de buscar un impacto, para que un fallo también cuente.
|
|
Luego recorre la lista. El `return` de dentro del bucle se para en la primera
|
|
diana acertada, lo que significa que una bala no puede llevarse dos dianas que
|
|
se solapan -- una regla que tienes que decidir en un sentido o en otro, y esta es
|
|
la línea que la decide.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayBox(target.x, target.y, target.x + TARGET_SIZE - 1, target.y + TARGET_SIZE - 1)
|
|
----
|
|
|
|
`overlayBox` quiere dos esquinas opuestas, no una esquina y un tamaño, así que la
|
|
esquina lejana es la cercana más el tamaño. El `- 1` está porque las dos esquinas
|
|
se incluyen: una caja de `80` a `175` mide noventa y seis píxeles de ancho.
|
|
`collidePointRect`, en cambio, quiere una esquina y un tamaño, y por eso
|
|
`TARGET_SIZE` entra solo allí y sumado a la esquina aquí. Confundir esas dos cosas es
|
|
un fallo muy común de verdad, y se nota en dianas a las que puedes acertar un
|
|
poco fuera de donde están dibujadas.
|
|
|
|
[source,lua]
|
|
----
|
|
if flash > 0 then
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayCircle(flashX, flashY, 30 - flash * 2)
|
|
flash = flash - 1
|
|
end
|
|
----
|
|
|
|
`flash` es una cuenta atrás en fotogramas, que se pone a doce cuando un disparo
|
|
acierta y baja en uno cada vez que se dibuja la pantalla. Como el radio se
|
|
calcula *a partir de* la cuenta atrás, el anillo crece a medida que el número
|
|
encoge. Doce fotogramas son menos de medio segundo. Una respuesta que dura más
|
|
que eso deja de sentirse como consecuencia del gatillo y empieza a parecer el
|
|
tiempo que hace.
|
|
|
|
[#_two_guns]
|
|
=== Dos pistolas
|
|
|
|
Estás a dos cambios de una recreativa para dos jugadores, y ninguno de ellos está
|
|
en el bucle del juego.
|
|
|
|
El primero es `mouseSetMode(MOUSE_MANY)`, que le dice a Singe que lea cada
|
|
dispositivo por separado en lugar de juntarlos todos en un solo cursor.
|
|
`mouseHowMany()` dice cuántos encontró al arrancar, así que lo normal es que un
|
|
juego pregunte antes de comprometerse:
|
|
|
|
[source,lua]
|
|
----
|
|
if mouseHowMany() >= 2 then
|
|
mouseSetMode(MOUSE_MANY)
|
|
end
|
|
----
|
|
|
|
A partir de ahí, `mouseGetPosition(1)` es la segunda pistola, y el argumento
|
|
`device` de `onInputPressed` dice qué pistola ha disparado. Ten dos puntos de
|
|
mira, dos puntuaciones y una sola lista de dianas, y tendrás un juego
|
|
cooperativo.
|
|
|
|
El segundo cambio no está en tu script en absoluto. El `controls.cfg` que viene
|
|
con Singe solo asigna los botones del *primer* ratón, así que el gatillo de la
|
|
segunda pistola no llega a ninguna parte. Una recreativa con dos pistolas necesita
|
|
su propia línea en ese archivo que nombre las dos:
|
|
|
|
[source,lua]
|
|
----
|
|
INPUT_ACTION_3 = { SCANCODE.LSHIFT, GAMEPAD_0.BUTTON_X, MOUSE_0.BUTTON_LEFT, MOUSE_1.BUTTON_LEFT }
|
|
----
|
|
|
|
Dilo en tus instrucciones. Un jugador cuya segunda pistola no hace nada le echará
|
|
la culpa a tu juego, y tendrá media razón.
|
|
|
|
[#_the_other_way_to_test_a_hit]
|
|
=== La otra forma de comprobar un impacto
|
|
|
|
Hay una segunda forma de responder a «¿ha dado ese disparo en algo?», y vale la
|
|
pena conocerla aunque hoy no la vayas a usar. `vldpGetPixel(x,
|
|
y)` lee el color de un píxel del fotograma de vídeo que está en pantalla. Los
|
|
juegos lo usan desde la primerísima versión de Singe: pinta cada diana de las
|
|
imágenes de un color que no aparezca en ningún otro sitio, y cuando se apriete el
|
|
gatillo pregunta qué color hay debajo del punto de mira.
|
|
|
|
Te da formas de impacto que siguen la acción con exactitud, gratis, sin
|
|
rectángulos que preparar. Te cuesta unas imágenes que tienes que preparar así, y
|
|
una comprobación de impacto que no puedes ver ni depurar. La entrada de la
|
|
referencia de `vldpGetPixel` tiene los detalles. Los rectángulos son lo correcto por
|
|
defecto; guárdate esto en el bolsillo.
|
|
|
|
[#_what_only_a_cabinet_cares_about]
|
|
=== Lo que solo le importa a una recreativa
|
|
|
|
Todo lo anterior funciona en tu mesa. Varias cosas no existen en absoluto en tu
|
|
mesa, y este es un buen momento para nombrarlas y que no te pillen por sorpresa
|
|
más adelante.
|
|
|
|
*Calibración.* A una pistola de luz de verdad hay que decirle dónde está la
|
|
pantalla, porque es una cámara que mira un monitor desde donde esté el jugador.
|
|
Las herramientas de servicio de Singe tienen una pantalla Light Gun que pone
|
|
dianas en el centro y en cada esquina y marca adónde apuntó de verdad la
|
|
pistola, para que veas hacia qué lado se desvía. Tu juego no hace la calibración
|
|
y no debería intentarlo.
|
|
|
|
*El borde Sinden.* Una familia de pistolas muy popular encuentra la pantalla
|
|
buscando un marco brillante dibujado alrededor de la imagen. `--sindengun` dibuja
|
|
ese marco, y el motor traslada las posiciones a la imagen más pequeña que queda
|
|
dentro, así que tu juego sigue recibiendo coordenadas que significan lo mismo que
|
|
antes.
|
|
|
|
*Pantallas que no tienen la forma de la película.* `ratioGetX()` y `ratioGetY()`
|
|
te dan los números que un jugador ha puesto en la línea de comandos para una
|
|
pantalla cuyas proporciones no coinciden con las imágenes. Singe no los aplica;
|
|
un juego que los quiera hace la cuenta por su cuenta.
|
|
|
|
*Pistolas que se van.* `SWITCH_MOUSE_DISCONNECT` llega a `onInputPressed` cuando
|
|
se desenchufa una pistola. No lo produce ningún botón; lo lanza el motor. Un
|
|
juego que esté pendiente de él puede poner «JUGADOR 2: PISTOLA DESCONECTADA» en
|
|
pantalla en lugar de parecer que se ha muerto.
|
|
|
|
*Ningún ratón.* Una recreativa con un joystick y sin dispositivo apuntador puede
|
|
mover el cursor con el stick, cosa que activa `joyMouseEnable`.
|
|
|
|
Nada de eso cambia el juego que has escrito. Todo ello es la lección treinta y
|
|
uno.
|
|
|
|
[#_try_it_17]
|
|
=== Pruébalo
|
|
|
|
. *Mueve una diana.* Cambia la `x` de la tercera diana a `680` y ejecútalo. La
|
|
mayor parte de ella cuelga ahora por fuera del borde derecho, porque el overlay
|
|
mide 720 de ancho y la diana 96. Todavía puedes disparar a la tira que queda.
|
|
Decide qué quieres que pase en los bordes.
|
|
. *Haz las dianas más fáciles.* Cambia `TARGET_SIZE` a `140`. Fíjate en que
|
|
cambian tanto el dibujo como la comprobación de impacto, y en que solo has
|
|
tocado un número. Para eso sirve una constante con nombre.
|
|
. *Lleva la cuenta de la puntería.* Añade una línea que imprima `hits` como
|
|
porcentaje de `shots`. Protégete contra la división por cero antes del primer
|
|
disparo, y averigua qué imprime Lua si no lo haces.
|
|
. *Haz que una diana te castigue.* Añade un campo `penalty = true` a un registro
|
|
y resta puntos cuando le aciertes. Fíjate en que añades un campo a un registro,
|
|
no a los cuatro, y en que los que no lo tienen tienen `nil` ahí.
|
|
. *Quita el `return`* del bucle de `onInputPressed`, y luego solapa dos dianas
|
|
dándoles los mismos fotogramas y casi la misma posición. Ahora un disparo se
|
|
lleva las dos. Decide qué comportamiento quiere tu juego.
|
|
|
|
[#_break_it_on_purpose_16]
|
|
=== Rómpelo a propósito
|
|
|
|
Cambia la comprobación del disparo para que use `SWITCH_BUTTON1` en lugar de
|
|
`SWITCH_BUTTON3` y ejecútalo. No se rompe nada. No aparece ningún error. El botón
|
|
izquierdo deja de hacer nada, y ahora dispara el *derecho*.
|
|
|
|
Este es el peor tipo de fallo y el más común en el código de entrada: el programa
|
|
no está mal, solo está conectado a lo que no es. No hay ningún mensaje de error
|
|
que leer, porque has hecho una pregunta sensata sobre un interruptor que existe
|
|
y te ha respondido con sinceridad.
|
|
|
|
Hay dos cosas que lo encuentran. La primera es `debugPrint`, de la lección ocho:
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what, device)
|
|
debugPrint("switch " .. what .. " from device " .. tostring(device))
|
|
----
|
|
|
|
Ahora cada pulsación imprime una línea, y puedes ver lo que manda de verdad el
|
|
gatillo. Fíjate en `tostring(device)`: `device` es `nil` cuando se pulsa una
|
|
tecla, y pegar `nil` a una cadena con `..` es un error. `tostring` lo convierte
|
|
en la palabra `nil` y la línea se imprime.
|
|
|
|
La segunda es la propia herramienta Input Test del motor, en las herramientas de
|
|
servicio, que muestra todos los dispositivos que ve y el último interruptor que
|
|
habría recibido el juego. Cuando un botón no hace nada, esa pantalla te dice qué
|
|
mitad del problema tienes: o el interruptor está llegando y tu juego no le hace
|
|
caso, o no está llegando en absoluto.
|
|
|
|
[#_what_you_learned_17]
|
|
=== Lo que has aprendido
|
|
|
|
* Una pistola de luz se presenta como un ratón, así que un juego escrito para el
|
|
ratón funciona con una pistola.
|
|
* `mouseGetPosition(device)` devuelve dos valores, la posición del puntero en
|
|
coordenadas del overlay.
|
|
* Una función de Lua puede devolver más de un valor, y los recoges poniendo otros
|
|
tantos nombres.
|
|
* `singeWantsCrosshairs()` dice si tienes que dibujar tu propio punto de mira.
|
|
Léelo una vez.
|
|
* Singe captura y oculta el puntero del sistema al arrancar; no tienes que
|
|
hacerlo tú.
|
|
* `onInputPressed` recibe un segundo argumento que dice qué ratón o qué pistola
|
|
ha disparado, y `nil` cuando ha sido una tecla o un mando.
|
|
* Una comprobación de impacto contra un rectángulo es `collidePointRect`, la
|
|
misma colisión que aprendiste en la lección once.
|
|
* La ventana de fotogramas de una diana no es más que dos campos más en su
|
|
registro.
|
|
* `a or b` te da `b` cuando `a` es `nil`.
|
|
* La calibración, el borde Sinden y el cableado de dos pistolas viven fuera de tu
|
|
script.
|
|
|
|
[#_next_time_17]
|
|
=== La próxima vez
|
|
|
|
Tu galería pide una sola cosa: dale a eso, ahí. La otra convención de las
|
|
recreativas sobre la que se construyen los juegos de vídeo pide algo más
|
|
difícil -- pulsa *esto*, *ya*, y tienes medio segundo. La lección dieciocho
|
|
construye un quick-time event, con una indicación, una ventana medida en
|
|
fotogramas y tres finales distintos según hayas acertado, fallado o sido
|
|
demasiado lento.
|