616 lines
21 KiB
Text
616 lines
21 KiB
Text
[#_lesson_18_quick_time_events]
|
|
== Lección 18: Quick-time events
|
|
|
|
image::learn/18-qte.png[La lección terminada, 480]
|
|
|
|
Aparece una indicación. Tienes medio segundo. Pulsa lo correcto y la historia
|
|
sigue; pulsa lo que no es, o no pulses nada, y se va a otra parte.
|
|
|
|
Esa es toda la idea, y un número sorprendente de juegos de vídeo no están hechos
|
|
de casi nada más. Ya tienes todas las piezas: la lección catorce te dio los
|
|
fotogramas, la lección seis una lista de registros, la lección tres la entrada,
|
|
y la lección dieciséis las ramificaciones. Esta lección las junta y añade una
|
|
cosa que es nueva de verdad -- una ventana de tiempo, y lo que pasa cuando se
|
|
cierra.
|
|
|
|
[#_the_clock_is_the_film]
|
|
=== El reloj es la película
|
|
|
|
Aquí todo se mide en *fotogramas*, no en segundos, y vale la pena detenerse en
|
|
ello, porque es la única decisión que hace que esto funcione.
|
|
|
|
La lección catorce decía que un fotograma es la dirección de un momento de la
|
|
película. La indicación tiene que aparecer cuando el actor empieza a lanzar el
|
|
golpe, y la ventana tiene que cerrarse cuando llega el puño. Eso no son tiempos,
|
|
son lugares de las imágenes, y los encontraste avanzando por ellas paso a paso.
|
|
Si escribes la ventana en segundos, tienes que poner en marcha un cronómetro
|
|
cuando aparece la indicación, y entonces los dos relojes -- el tuyo y el de la
|
|
película -- pueden desfasarse. Si el vídeo da tirones en un ordenador lento, tu
|
|
cronómetro sigue corriendo y el jugador sale castigado por culpa de su hardware.
|
|
|
|
Los fotogramas no pueden desfasarse, porque solo hay un reloj. `discGetFrame()`
|
|
es la hora, siempre. Una ventana son dos números de fotograma, y «cuánto
|
|
queda» es una resta.
|
|
|
|
Ejecútalo con el mismo vídeo que antes:
|
|
|
|
----
|
|
Singe -R -v Singe/menuBackground.mkv moves
|
|
----
|
|
|
|
Sigue siendo la cuadrícula morada y la puesta de sol. Imagínate una pelea.
|
|
|
|
[#_a_move_is_a_record]
|
|
=== Un movimiento es un registro
|
|
|
|
Esta es la tabla de la que cuelga toda la lección:
|
|
|
|
[source,lua]
|
|
----
|
|
local moves = {
|
|
{ first = 60, last = 105, switch = SWITCH_LEFT, name = "LEFT" },
|
|
{ first = 140, last = 185, switch = SWITCH_UP, name = "UP" },
|
|
{ first = 220, last = 265, switch = SWITCH_RIGHT, name = "RIGHT" },
|
|
{ first = 300, last = 345, switch = SWITCH_DOWN, name = "DOWN" },
|
|
}
|
|
----
|
|
|
|
Cuatro movimientos. Cada uno dice cuándo se abre la ventana, cuándo se cierra,
|
|
qué interruptor la responde y qué poner en pantalla. Cuarenta y cinco fotogramas
|
|
son más o menos un segundo y medio a la velocidad de este vídeo, lo cual es
|
|
generoso; los juegos de verdad suelen darte menos de la mitad.
|
|
|
|
Todo lo que hace que un movimiento sea un movimiento está en esa tabla. No hay
|
|
ningún código más abajo que sepa que hay cuatro movimientos, ni que el segundo es
|
|
hacia arriba, ni que la primera ventana se abre en el fotograma sesenta. Añadir
|
|
un quinto movimiento es añadir una línea. Cambiar el ritmo de todos los
|
|
movimientos es editar números. Nadie tiene que leer una función para cambiar la
|
|
pelea.
|
|
|
|
Eso no es un truco para esta lección. Es la diferencia entre un juego que puedes
|
|
ajustar y uno que no, y no cuesta nada hacerlo desde el principio. Cuando te veas
|
|
a punto de escribir `if frame == 60 then` por cuarta vez, para y haz una tabla.
|
|
|
|
[#_what_happens_when]
|
|
=== Qué pasa y cuándo
|
|
|
|
A un movimiento le pueden pasar tres cosas, y son tres resultados distintos, no
|
|
dos:
|
|
|
|
* El jugador pulsa el interruptor correcto dentro de la ventana. Se puntúa y se
|
|
sigue.
|
|
* El jugador pulsa un interruptor *equivocado* dentro de la ventana. Lo ha
|
|
intentado y ha fallado.
|
|
* La ventana se cierra sin ninguna respuesta. Se ha quedado helado.
|
|
|
|
El código perezoso trata igual los dos últimos. Los juegos casi nunca lo hacen,
|
|
porque al jugar se sienten completamente distintos: equivocarse es un error, y
|
|
quedarse helado es otro error distinto, y normalmente la película va a un sitio
|
|
distinto con cada uno. Aquí la respuesta equivocada manda el disco al fotograma
|
|
`362` y la falta de respuesta lo manda al fotograma `388`, lo que en estas
|
|
imágenes significa dos cantidades ligeramente distintas de puesta de sol. En
|
|
imágenes de verdad serían dos planos distintos de ti perdiendo.
|
|
|
|
Así que cada registro gana un quinto campo mientras dura la partida -- se llama
|
|
`answer` y es `nil` hasta que al movimiento le pasa algo, y luego una de tres
|
|
cadenas:
|
|
|
|
[source,lua]
|
|
----
|
|
move.answer = "right"
|
|
move.answer = "wrong"
|
|
move.answer = "late"
|
|
----
|
|
|
|
`nil` es el valor que te da Lua para un campo al que nadie ha dado valor. Lo
|
|
conociste en la lección tres. Aquí se gana el sueldo: «todavía sin respuesta» es
|
|
de verdad un estado distinto de cualquiera de las tres respuestas, y `nil` lo
|
|
dice sin que tengas que inventarte una palabra para ello.
|
|
|
|
[#_finding_the_move_that_is_being_asked]
|
|
=== Encontrar el movimiento que se está pidiendo
|
|
|
|
Todo lo demás necesita saber una cosa: ¿qué movimiento está abierto ahora mismo,
|
|
si es que hay alguno?
|
|
|
|
[source,lua]
|
|
----
|
|
local function moveAt(frame)
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame >= move.first and frame <= move.last then
|
|
return move
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
----
|
|
|
|
Recorre la lista y responde con el primer movimiento cuya ventana cubra este
|
|
fotograma y que no se haya respondido. Si no coincide ninguno, responde `nil`.
|
|
|
|
Vale la pena fijarse en que se devuelve `nil` a propósito. Una función que a
|
|
veces no tiene respuesta debería decirlo, y «no hay ningún movimiento abierto» es
|
|
una respuesta perfectamente buena. Quien la llama escribe entonces
|
|
`if move ~= nil then`, y el código que dibuja la indicación nunca se ejecuta
|
|
cuando no hay nada que indicar.
|
|
|
|
La comprobación `move.answer == nil` es lo que impide que un movimiento se pida
|
|
dos veces. En cuanto el jugador acierta, la indicación desaparece aunque la
|
|
ventana siga abierta -- que es justo lo que debe hacer, porque ya ha respondido.
|
|
|
|
[#_reading_the_answer]
|
|
=== Leer la respuesta
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if not isDirection(what) then
|
|
return
|
|
end
|
|
|
|
local move = moveAt(discGetFrame())
|
|
if move == nil then
|
|
return
|
|
end
|
|
|
|
if what == move.switch then
|
|
move.answer = "right"
|
|
score = score + RIGHT_SCORE
|
|
else
|
|
move.answer = "wrong"
|
|
endRun("WRONG WAY", WRONG_FRAME)
|
|
end
|
|
end
|
|
----
|
|
|
|
Léelo de arriba abajo. Si el interruptor no es una de las cuatro direcciones, no
|
|
es asunto nuestro: vete. Si no hay ningún movimiento abierto, pulsar una
|
|
dirección no significa nada: vete. Si no, hay exactamente un movimiento con el
|
|
que comparar, y una comparación que hacer.
|
|
|
|
Esas dos líneas `return` del principio son guardas: echan fuera los casos que no
|
|
te interesan para que el código de debajo solo se ejecute en la única situación
|
|
para la que se escribió. Escribirlo al revés, con el trabajo de verdad metido
|
|
tres niveles más adentro, dentro de tres bloques `if`, dice lo mismo y es mucho
|
|
más difícil de leer.
|
|
|
|
`isDirection` es una función diminuta que no hace nada más que mantener corta
|
|
esa primera línea:
|
|
|
|
[source,lua]
|
|
----
|
|
local function isDirection(what)
|
|
return what == SWITCH_UP or what == SWITCH_DOWN or what == SWITCH_LEFT or what == SWITCH_RIGHT
|
|
end
|
|
----
|
|
|
|
Fíjate en que devuelve la propia comparación en lugar de decir
|
|
`if ... then return true else return false end`. Una comparación ya es `true` o
|
|
`false`. Comprobar si algo es verdad para poder decir que es verdad es una
|
|
costumbre que conviene perder pronto.
|
|
|
|
[#_the_window_closing]
|
|
=== La ventana se cierra
|
|
|
|
Nadie pulsa nada, y los fotogramas siguen pasando. Algo tiene que darse cuenta.
|
|
Ese algo es `onOverlayUpdate`, porque se ejecuta en cada fotograma, haga algo el
|
|
jugador o no:
|
|
|
|
[source,lua]
|
|
----
|
|
if not runOver then
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame > move.last then
|
|
move.answer = "late"
|
|
endRun("TOO SLOW", LATE_FRAME)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
----
|
|
|
|
Un movimiento sin responder cuyo último fotograma ya ha pasado se ha perdido.
|
|
`break` sale del bucle inmediatamente, porque una vez terminada la partida no
|
|
tiene sentido mirar el resto.
|
|
|
|
`endRun` es la ramificación, y es la misma ramificación que escribiste en la
|
|
lección dieciséis:
|
|
|
|
[source,lua]
|
|
----
|
|
local function endRun(reason, frame)
|
|
verdict = reason
|
|
runOver = true
|
|
discSkipToFrame(frame)
|
|
end
|
|
----
|
|
|
|
Tres líneas. Recuerda qué decir, recuerda que la partida ha terminado y manda la
|
|
película a otra parte. Un quick-time event perdido no es un mecanismo especial.
|
|
Es una elección que el jugador ha hecho al no hacer ninguna, y la película se
|
|
ramifica con ella exactamente igual que con una puerta que hubiera escogido.
|
|
|
|
[#_a_gauge_that_drains]
|
|
=== Un indicador que se vacía
|
|
|
|
El jugador no ve los números de fotograma. Necesita ver que se acaba el tiempo, y
|
|
necesita verlo sin apartar la vista de la acción, lo que significa una forma que
|
|
cambia en lugar de un número que cuenta.
|
|
|
|
Dos números te lo dan todo:
|
|
|
|
[source,lua]
|
|
----
|
|
local left = move.last - frame
|
|
local span = move.last - move.first
|
|
----
|
|
|
|
`span` es lo que dura la ventana. `left` es lo que queda de ella. La barra ocupa
|
|
`left` partido por `span` del ancho completo:
|
|
|
|
[source,lua]
|
|
----
|
|
fillBar(GAUGE_X, GAUGE_Y, GAUGE_W * left // span, GAUGE_H)
|
|
----
|
|
|
|
`//` es una división que tira los decimales, que es lo que quieres aquí, porque
|
|
una barra de ciento treinta y tres píxeles y un tercio de ancho no existe.
|
|
Multiplica antes de dividir -- `GAUGE_W * left // span` y no
|
|
`GAUGE_W * (left // span)` -- o los decimales se tiran mientras todavía son la
|
|
única información que tienes, y la barra salta de llena a vacía sin nada en
|
|
medio.
|
|
|
|
El overlay no tiene rectángulos rellenos. `overlayBox` dibuja un contorno, que es
|
|
lo que quieres para el marco del indicador, y el relleno lo pones tú:
|
|
|
|
[source,lua]
|
|
----
|
|
local function fillBar(x, y, width, height)
|
|
if width < 1 then
|
|
return
|
|
end
|
|
for row = 0, height - 1 do
|
|
overlayLine(x, y + row, x + width - 1, y + row)
|
|
end
|
|
end
|
|
----
|
|
|
|
Dieciocho líneas horizontales apiladas una encima de otra. La guarda del
|
|
principio importa: en el último fotograma de la ventana `left` es cero, el ancho
|
|
es cero, y sin ella `x + width - 1` queda un píxel a la *izquierda* de `x` y
|
|
dibujas una línea corta hacia atrás en lugar de nada.
|
|
|
|
El último toque es un cambio de color cuando la cosa se pone desesperada:
|
|
|
|
[source,lua]
|
|
----
|
|
if left * 3 < span then
|
|
colorForeground(255, 60, 60, 255)
|
|
else
|
|
colorForeground(255, 200, 0, 255)
|
|
end
|
|
----
|
|
|
|
La barra se pone roja en su último tercio. Escrito así -- multiplicando el tiempo
|
|
que queda en lugar de dividir lo que dura la ventana -- todo sigue siendo números
|
|
enteros y funciona con una ventana de cualquier longitud.
|
|
|
|
[#_the_whole_thing_10]
|
|
=== Todo junto
|
|
|
|
[source,lua]
|
|
----
|
|
-- Aprende a programar con Singe -- Lección 18: Quick-time events
|
|
|
|
dofile("Singe/Framework.singe")
|
|
|
|
overlaySetResolution(discGetWidth(), discGetHeight())
|
|
|
|
local FIRST_FRAME = 20
|
|
local WRONG_FRAME = 362
|
|
local LATE_FRAME = 388
|
|
local LAST_FRAME = 415
|
|
|
|
local PROMPT_COL = 54
|
|
local PROMPT_ROW = 22
|
|
local GAUGE_X = 210
|
|
local GAUGE_Y = 330
|
|
local GAUGE_W = 300
|
|
local GAUGE_H = 18
|
|
|
|
local RIGHT_SCORE = 100
|
|
|
|
local moves = {
|
|
{ first = 60, last = 105, switch = SWITCH_LEFT, name = "LEFT" },
|
|
{ first = 140, last = 185, switch = SWITCH_UP, name = "UP" },
|
|
{ first = 220, last = 265, switch = SWITCH_RIGHT, name = "RIGHT" },
|
|
{ first = 300, last = 345, switch = SWITCH_DOWN, name = "DOWN" },
|
|
}
|
|
|
|
local score = 0
|
|
local runOver = false
|
|
local verdict = ""
|
|
|
|
|
|
local function fillBar(x, y, width, height)
|
|
if width < 1 then
|
|
return
|
|
end
|
|
for row = 0, height - 1 do
|
|
overlayLine(x, y + row, x + width - 1, y + row)
|
|
end
|
|
end
|
|
|
|
|
|
local function isDirection(what)
|
|
return what == SWITCH_UP or what == SWITCH_DOWN or what == SWITCH_LEFT or what == SWITCH_RIGHT
|
|
end
|
|
|
|
|
|
local function moveAt(frame)
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame >= move.first and frame <= move.last then
|
|
return move
|
|
end
|
|
end
|
|
return nil
|
|
end
|
|
|
|
|
|
local function startRun()
|
|
score = 0
|
|
runOver = false
|
|
verdict = ""
|
|
for _, move in ipairs(moves) do
|
|
move.answer = nil
|
|
end
|
|
discSkipToFrame(FIRST_FRAME)
|
|
end
|
|
|
|
|
|
local function endRun(reason, frame)
|
|
verdict = reason
|
|
runOver = true
|
|
discSkipToFrame(frame)
|
|
end
|
|
|
|
|
|
function onInputPressed(what)
|
|
if runOver then
|
|
if what == SWITCH_START1 then
|
|
startRun()
|
|
end
|
|
return
|
|
end
|
|
|
|
if not isDirection(what) then
|
|
return
|
|
end
|
|
|
|
local move = moveAt(discGetFrame())
|
|
if move == nil then
|
|
return
|
|
end
|
|
|
|
if what == move.switch then
|
|
move.answer = "right"
|
|
score = score + RIGHT_SCORE
|
|
else
|
|
move.answer = "wrong"
|
|
endRun("WRONG WAY", WRONG_FRAME)
|
|
end
|
|
end
|
|
|
|
|
|
function onOverlayUpdate()
|
|
local frame = discGetFrame()
|
|
|
|
if not runOver then
|
|
for _, move in ipairs(moves) do
|
|
if move.answer == nil and frame > move.last then
|
|
move.answer = "late"
|
|
endRun("TOO SLOW", LATE_FRAME)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if frame >= LAST_FRAME and discGetState() == DISC_PLAYING then
|
|
if not runOver then
|
|
verdict = "CLEAR"
|
|
runOver = true
|
|
end
|
|
discSearch(LAST_FRAME)
|
|
end
|
|
|
|
overlayClear()
|
|
|
|
local move = nil
|
|
if not runOver then
|
|
move = moveAt(frame)
|
|
end
|
|
|
|
if move ~= nil then
|
|
local left = move.last - frame
|
|
local span = move.last - move.first
|
|
|
|
overlayPrint(PROMPT_COL, PROMPT_ROW, "PRESS " .. move.name)
|
|
|
|
colorForeground(255, 255, 255, 255)
|
|
overlayBox(GAUGE_X - 2, GAUGE_Y - 2, GAUGE_X + GAUGE_W + 1, GAUGE_Y + GAUGE_H + 1)
|
|
|
|
if left * 3 < span then
|
|
colorForeground(255, 60, 60, 255)
|
|
else
|
|
colorForeground(255, 200, 0, 255)
|
|
end
|
|
fillBar(GAUGE_X, GAUGE_Y, GAUGE_W * left // span, GAUGE_H)
|
|
end
|
|
|
|
overlayPrint(2, 1, "SCORE " .. score .. " FRAME " .. frame)
|
|
|
|
if runOver then
|
|
overlayPrint(2, 3, verdict)
|
|
overlayPrint(2, 4, "PRESS 1 TO RUN IT AGAIN")
|
|
end
|
|
|
|
return OVERLAY_UPDATED
|
|
end
|
|
|
|
|
|
startRun()
|
|
----
|
|
|
|
Las teclas de flecha responden a las indicaciones. Acierta las cuatro y la
|
|
película llega hasta el final y se queda ahí con `CLEAR`. Falla una, o quédate de
|
|
brazos cruzados, y corta a otro fotograma con otra palabra en pantalla. `1` la
|
|
vuelve a poner en marcha.
|
|
|
|
[#_what_just_happened_14]
|
|
=== Qué acaba de pasar
|
|
|
|
Casi todo ya lo has leído. Hay cuatro trozos que merecen una segunda mirada.
|
|
|
|
[source,lua]
|
|
----
|
|
if frame >= LAST_FRAME and discGetState() == DISC_PLAYING then
|
|
----
|
|
|
|
Hay que atrapar el final de la película, o se sale del final y no queda nada que
|
|
ver. `discSearch` va a un fotograma y se *pausa* ahí, que es exactamente lo que
|
|
quiere un plano final. La mitad de `discGetState()` impide que eso vuelva a pasar
|
|
en cada uno de los fotogramas siguientes: una vez en pausa, el disco ya no está
|
|
en `DISC_PLAYING`, así que la condición es falsa y el salto no se repite sesenta
|
|
veces por segundo. `discGetState` y las constantes `DISC_*` están en la
|
|
referencia, en `discGetState`.
|
|
|
|
[source,lua]
|
|
----
|
|
local move = nil
|
|
if not runOver then
|
|
move = moveAt(frame)
|
|
end
|
|
----
|
|
|
|
`onOverlayUpdate` vuelve a preguntar cuál es el movimiento abierto, en lugar de
|
|
que se lo diga `onInputPressed`. Es a propósito. Hay exactamente una función que
|
|
sabe qué movimiento está abierto, y todo el mundo se lo pregunta a ella. Si la
|
|
indicación saliera de una variable que el código de entrada mantuviera al día,
|
|
habría dos trozos de código recordando el mismo dato, y tarde o temprano uno de
|
|
ellos se equivocaría. Cuando tengas que elegir entre preguntar y recordar,
|
|
pregunta.
|
|
|
|
[source,lua]
|
|
----
|
|
overlayPrint(PROMPT_COL, PROMPT_ROW, "PRESS " .. move.name)
|
|
----
|
|
|
|
`overlayPrint` cuenta en celdas de carácter, como desde la lección uno, así que
|
|
la indicación se coloca a ojo: la columna cincuenta y cuatro está lo bastante
|
|
cerca del centro para palabras de esta longitud. Está lo bastante cerca, no bien,
|
|
y «PRESS RIGHT» termina tres caracteres más a la derecha que «PRESS UP». La lección
|
|
diecinueve carga una fuente de verdad, y con ella llega la posibilidad de medir
|
|
una cadena en píxeles y centrarla como es debido.
|
|
|
|
[source,lua]
|
|
----
|
|
function onInputPressed(what)
|
|
if runOver then
|
|
if what == SWITCH_START1 then
|
|
startRun()
|
|
end
|
|
return
|
|
end
|
|
----
|
|
|
|
Un callback, dos trabajos completamente distintos, y `runOver` elige cuál.
|
|
Mientras dura la partida, las direcciones son movimientos; una vez terminada, la
|
|
única tecla que significa algo es la de empezar. Una variable que decide qué
|
|
reglas se aplican es un *estado*, que es como lo llamó la lección doce, y un
|
|
juego es sobre todo un puñado de ellos.
|
|
|
|
[#_try_it_18]
|
|
=== Pruébalo
|
|
|
|
. *Hazlo más difícil.* Cambia el `last` del primer movimiento de `105` a `75`.
|
|
Quince fotogramas son medio segundo. Juégalo y averigua si puedes.
|
|
. *Añade un movimiento.* Pon una quinta línea en la tabla con una ventana entre
|
|
los fotogramas `370` y `400`, y sube `LAST_FRAME` a `418`. No deberías tener
|
|
que tocar ninguna otra línea. Si tienes que hacerlo, algo del código sabe
|
|
demasiado de la tabla.
|
|
. *Puntúa la rapidez.* Da más puntos por responder pronto. En `onInputPressed`,
|
|
donde se puntúa la respuesta correcta, calcula `left` y `span` a partir de
|
|
`move` y `discGetFrame()` igual que hace el indicador, y suma
|
|
`RIGHT_SCORE * left // span` en lugar de `RIGHT_SCORE`. Decide si eso hace el
|
|
juego mejor o solo más ajetreado.
|
|
. *Dale a una respuesta equivocada su propia consecuencia.* En lugar de terminar
|
|
la partida, haz que pulsar mal cueste cincuenta puntos y deje la ventana
|
|
abierta para volver a intentarlo. Tendrás que dejar de poner `move.answer`, y
|
|
te encontrarás con la razón por la que se ponía.
|
|
. *Dos indicaciones a la vez.* Solapa las ventanas de dos movimientos y mira qué
|
|
pasa. `moveAt` devuelve la primera coincidencia, así que solo se pide uno.
|
|
Decide qué querrías, y qué haría falta.
|
|
|
|
[#_break_it_on_purpose_17]
|
|
=== Rómpelo a propósito
|
|
|
|
Borra el `break` del bucle que encuentra los movimientos perdidos, y pierde el
|
|
primero a propósito. La partida termina, el disco salta y todo parece correcto.
|
|
|
|
Ahora pon `debugPrint(move.name .. " missed")` junto a `move.answer = "late"` y
|
|
vuelve a hacerlo. La consola muestra una línea, como era de esperar.
|
|
|
|
Luego quita también el `if not runOver then` que rodea el bucle, y vuelve a
|
|
perder el primer movimiento:
|
|
|
|
----
|
|
LEFT missed
|
|
UP missed
|
|
RIGHT missed
|
|
DOWN missed
|
|
----
|
|
|
|
La primera línea llega cuando se cierra la ventana, como antes. Las otras tres
|
|
llegan juntas en el fotograma siguiente, en el instante en que el disco aterriza
|
|
en el 388: todas las ventanas que quedaban están ahora en el pasado, así que
|
|
todos los movimientos sin responder se pierden a la vez, y cada uno llama a
|
|
`endRun` y vuelve a saltar. El juego sigue funcionando más o menos, y eso es lo
|
|
que lo hace peligroso. Tres saltos de más en un solo fotograma, tres veces
|
|
sobrescrito el veredicto, y nada en pantalla lo dice.
|
|
|
|
Aquí tampoco hay mensaje de error. La lección es que ese `break` y esa guarda no
|
|
son por limpieza. Son las dos líneas que dicen «esto ya está decidido», y el
|
|
código que sigue decidiendo algo que ya ha decidido es una de las fuentes de
|
|
fallos más fiables que vas a escribir nunca.
|
|
|
|
[#_what_you_learned_18]
|
|
=== Lo que has aprendido
|
|
|
|
* Un quick-time event es una indicación, una ventana y una consecuencia.
|
|
* Mide la ventana en fotogramas, porque la película es el reloj y solo debe haber
|
|
un reloj.
|
|
* Guarda los movimientos en una tabla de registros, para que añadir un movimiento
|
|
sea añadir una línea.
|
|
* Acertar, fallar y no responder son tres resultados, y normalmente llevan a tres
|
|
sitios distintos.
|
|
* `nil` en un campo de un registro es un estado de verdad: nadie ha respondido
|
|
todavía.
|
|
* Una sola función responde a «qué movimiento está abierto», y todo el mundo se
|
|
lo pregunta en lugar de guardar una copia.
|
|
* Las cláusulas de guarda -- un `return` temprano para los casos que no te
|
|
interesan -- evitan que el trabajo de verdad quede anidado.
|
|
* `//` divide y tira los decimales; multiplica antes de dividir.
|
|
* `overlayBox` dibuja un contorno, así que una barra rellena es una pila de
|
|
llamadas a `overlayLine`.
|
|
* Un movimiento perdido es una ramificación, y `discSkipToFrame` la toma,
|
|
exactamente como en la lección dieciséis.
|
|
|
|
[#_next_time_18]
|
|
=== La próxima vez
|
|
|
|
La tercera parte está terminada. Puedes reproducir película, esperarla,
|
|
ramificarla, apuntarle y medir el tiempo del jugador contra ella. Lo que has
|
|
estado escribiendo encima de todo eso es la fuente de consola del motor, que
|
|
tiene seis píxeles de ancho y está pensada para diagnósticos. La lección
|
|
diecinueve carga un tipo de letra de verdad, lo mide y hace que las palabras de
|
|
la pantalla parezcan parte del juego.
|