[#_lesson_16_branching] == Lección 16: Ramificaciones image::learn/16-branching.png[La lección terminada, 480] Un juego de vídeo con ramificaciones es una película con bifurcaciones. Se reproduce una escena, el jugador elige, y a continuación se reproduce una escena distinta. Haz eso veinte o treinta veces y tendrás un juego al que la gente volverá durante años intentando encontrar el final que no ha visto. En esta lección vas a construir uno. Tiene cuatro escenas, dos elecciones, un temporizador en cada elección, una muerte y tres vidas. El código que lo reproduce tiene unas treinta líneas, y seguiría teniendo unas treinta líneas si el juego tuviera doscientas escenas -- porque el juego en sí no va a estar escrito en código en absoluto. [#_imagine_it_is_a_dragon] === Imagina que es un dragón El vídeo que tiene todo el mundo es `Singe/menuBackground.mkv`, los mismos 420 fotogramas que llevas manejando desde la lección catorce. Es una bola de fuego, un dragón hecho de papel, la palabra `SINGE` y luego una cuadrícula magenta que se aleja rodando bajo un sol naranja, para siempre. No es un drama. No sale nadie que pueda estar en peligro, y al atardecer le da igual lo que decidas. Sígueme el juego de todos modos. El dragón es lo bastante real durante dos minutos, y todo lo que vas a escribir funciona exactamente igual cuando las imágenes son de un especialista cayendo por un acantilado. Aquí tienes el mapa, y los números de fotograma son los que encontraste tú mismo en la lección catorce: [cols="1,1,3",options="header"] |=== | Escena | Fotogramas | Qué pasa | `arrive` | 0 a 119 | Fuego, y el dragón aterriza. *¿Izquierda o derecha?* | `sunset` | 120 a 259 | Echa fuego. *¿Te agachas a la izquierda o corres a la derecha?* | `pit` | 260 a 339 | Te come. Pierdes una vida. | `escape` | 340 a 410 | La cuadrícula se aleja y has escapado. |=== Lo diestro, por así decirlo, es ir siempre a la izquierda: la izquierda desde `arrive` lleva a `sunset`, y la izquierda desde `sunset` te lleva a `escape`. La derecha lleva a `pit` las dos veces, y tardar demasiado también. [#_the_map_is_data_not_code] === El mapa son datos, no código Aquí tienes el juego entero, y es una tabla. [source,lua] ---- local scenes = { arrive = { first = 0, last = 119, ask = 30, prompt = "The dragon lands. Left or Right?", left = "sunset", right = "pit", goesTo = "pit" }, sunset = { first = 120, last = 259, ask = 170, prompt = "It breathes fire. Duck Left or run Right?", left = "escape", right = "pit", goesTo = "pit" }, pit = { first = 260, last = 339, death = true, goesTo = "arrive" }, escape = { first = 340, last = 410 } } ---- Una tabla con nombres a la izquierda en lugar de números, exactamente como en la lección seis, y cada valor de dentro es otra tabla. Cada una de esas tablas interiores es un *registro*: una escena, descrita campo a campo. `scenes.arrive` es la primera escena. `scenes.arrive.first` es `0`. `scenes.arrive.left` es la cadena `"sunset"`, que es el nombre de otra escena de la misma tabla -- y así es como se apuntan las bifurcaciones. Una escena no contiene la escena que va después de ella. Contiene el *nombre* de la escena que va después, y el nombre se busca cuando llega el momento. Los campos significan esto: `first` y `last`:: Los fotogramas entre los que va la escena. `ask`:: El fotograma en el que aparece la pregunta. El jugador tiene de `ask` a `last` para responder, y ese es el temporizador. `prompt`:: Lo que dice la pregunta. `left` y `right`:: La escena a la que lleva cada respuesta. `goesTo`:: Adónde va la escena cuando nadie ha elegido -- porque al jugador se le acabó el tiempo, o porque para empezar no había ninguna pregunta. En `pit`, que es una muerte, es adonde vuelve el reintento. `death`:: Está presente, y vale `true`, solo en una escena que cuesta una vida. Hay tres cosas en las que vale la pena fijarse en esa tabla. La primera es que es el documento de diseño. Puedes dársela a alguien que no ha escrito nunca una línea de Lua y sacará el juego de ella con solo leerla. Cuando quieras que el dragón se bifurque en tres caminos, añades un campo. Cuando quieras otra escena, añades otro registro. No vas a buscar el código que reproduce las escenas, porque no lo cambias. La segunda es que los registros no tienen todos la misma forma. `pit` no tiene `ask`, ni `prompt`, ni `left` -- en él no hay nada que elegir. `escape` no tiene `goesTo`, porque es el final y no va a ninguna parte. Un campo que no está en una tabla se lee como `nil`, que es la palabra que conociste en la lección tres para «ahí no hay nada», y pedir `scenes.escape.goesTo` no es un error. Es `nil`, y `nil` es una respuesta. El código que lee estos registros tiene que estar preparado para eso, y dentro de un momento verás la única línea que lo está. La tercera es lo que la tabla *no* contiene: nada de ella dibuja, reproduce ni espera. Es una descripción. Esa separación es todo el truco de esta lección. [#_one_function_that_plays_any_scene] === Una función que reproduce cualquier escena Esta es la mitad hecha de código, y solo hay una función de cierto tamaño. Escrita en el modelo con hilo de la lección quince, porque un juego con ramificaciones es lo más secuencial que existe. [source,lua] ---- local function playScene(name) local scene = scenes[name] discSkipToFrame(scene.first) while discGetFrame() < scene.last do local frame = discGetFrame() local asking = scene.ask ~= nil and frame >= scene.ask if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end drawScene(scene, frame, asking) singeYield() end discPause() if scene.death then lives = lives - 1 if lives == 0 then ending = "The dragon wins. Game over." return nil end waitForGo("Press the space bar to try again.") end return scene.goesTo end ---- Recibe el nombre de una escena, la reproduce y devuelve el nombre de la escena que va a continuación. Ese es todo su contrato, y entonces el juego son tres líneas: [source,lua] ---- function singeMain() local scene = "arrive" while scene ~= nil do scene = playScene(scene) end waitForGo(ending .. " Space to quit.") end ---- Empieza en `arrive`. Reproduce la escena que tengas en la mano, y quédate con lo que te devuelva. Cuando te devuelva `nil` no hay escena siguiente, el bucle termina y la partida se acaba. [#_the_whole_thing_8] === Todo junto Crea `dragon.singe` en tu carpeta `movie` y escribe esto. Es el script más largo del libro hasta ahora, y cada parte de él ya está explicada salvo las dos pequeñas funciones auxiliares, que vienen después. [source,lua] ---- local FPS = 30 local scenes = { arrive = { first = 0, last = 119, ask = 30, prompt = "The dragon lands. Left or Right?", left = "sunset", right = "pit", goesTo = "pit" }, sunset = { first = 120, last = 259, ask = 170, prompt = "It breathes fire. Duck Left or run Right?", left = "escape", right = "pit", goesTo = "pit" }, pit = { first = 260, last = 339, death = true, goesTo = "arrive" }, escape = { first = 340, last = 410 } } local answer = nil local ending = "You got out alive." local lives = 3 local function drawScene(scene, frame, asking) overlayClear() overlayPrint(2, 2, "Lives: " .. lives) if scene.death then overlayPrint(2, 4, "That did not go well.") elseif asking then overlayPrint(2, 4, scene.prompt) overlayPrint(2, 6, math.ceil((scene.last - frame) / FPS) .. " seconds left.") end end local function waitForGo(text) answer = nil while answer ~= "go" do overlayClear() overlayPrint(2, 2, "Lives: " .. lives) overlayPrint(2, 4, text) singeYield() end end local function playScene(name) local scene = scenes[name] discSkipToFrame(scene.first) while discGetFrame() < scene.last do local frame = discGetFrame() local asking = scene.ask ~= nil and frame >= scene.ask if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end drawScene(scene, frame, asking) singeYield() end discPause() if scene.death then lives = lives - 1 if lives == 0 then ending = "The dragon wins. Game over." return nil end waitForGo("Press the space bar to try again.") end return scene.goesTo end function onInputPressed(what) if what == SWITCH_LEFT then answer = "left" elseif what == SWITCH_RIGHT then answer = "right" elseif what == SWITCH_BUTTON1 then answer = "go" end end function singeMain() local scene = "arrive" while scene ~= nil do scene = playScene(scene) end waitForGo(ending .. " Space to quit.") end dofile("Singe/Framework.singe") ---- Ejecútalo: ---- Singe -R -v Singe/menuBackground.mkv movie/dragon.singe ---- Juega cuatro o cinco veces. Ve a la izquierda dos veces y escapa. Ve a la derecha y deja que te coma. Quédate de brazos cruzados y que te coma igualmente. Pierde las tres vidas. [#_what_just_happened_12] === Qué acaba de pasar [source,lua] ---- discSkipToFrame(scene.first) while discGetFrame() < scene.last do ---- Una escena es un salto seguido de una espera, que es el patrón de la lección quince con los números saliendo del registro en lugar de estar escritos a mano. `discSkipToFrame` va a un fotograma y reproduce desde ahí, que es justo lo que es un cambio de escena. El número de fotograma es correcto en el instante en que vuelve la llamada, así que el `while` nunca cree que sigue en la escena que acabas de dejar. [source,lua] ---- local asking = scene.ask ~= nil and frame >= scene.ask ---- La única línea que está preparada para un campo que falta, y la razón por la que la escena de muerte puede dejar fuera `ask`. `and` significa que las dos mitades tienen que ser verdaderas, como en la lección tres, y Lua comprueba primero la mitad izquierda. Si `scene.ask` es `nil`, la mitad izquierda es falsa, Lua se para ahí y la comparación de la derecha no llega a hacerse. Y menos mal, porque comparar `nil` con un número es un error, y te lo encontrarás al final de esta lección. `asking` es la respuesta a «¿está la pregunta en pantalla ahora mismo?», calculada una vez por fotograma y usada tres veces. [source,lua] ---- if not asking then answer = nil elseif answer == "left" or answer == "right" then return scene[answer] end ---- Mientras la pregunta no está puesta, todo lo que pulse el jugador se tira. Tiene que ser así, o un jugador que se apoye en la flecha izquierda durante la película responderá a una pregunta que no ha leído. Una vez que la pregunta está puesta, una respuesta termina la escena en el acto. `return scene[answer]` merece una segunda mirada: `answer` guarda la cadena `"left"` o `"right"`, y `scene["left"]` es lo mismo que `scene.left`. Poner el nombre entre corchetes en lugar de después de un punto te deja buscar un campo cuyo nombre tienes guardado en una variable. Dos líneas de código para las dos respuestas, y con cinco respuestas seguirían siendo dos líneas. El `elseif` también explica por qué solo se dejan pasar `"left"` y `"right"`. La barra espaciadora pone `answer` a `"go"`, y `scene["go"]` es `nil`, así que pulsar la barra espaciadora sin querer durante una pregunta terminaría el juego en silencio. Di qué aceptas. [source,lua] ---- discPause() ---- Justo después del bucle, y es fácil olvidarlo. La escena ha llegado a su último fotograma, pero el disco no tiene ni idea de que la escena ha terminado -- sigue reproduciendo de corrido lo que haya a continuación en el archivo, que en un juego de verdad es la mitad de una escena de otro sitio completamente distinto. Páralo en cuanto dejes de quererlo. [source,lua] ---- if scene.death then lives = lives - 1 ---- `scene.death` es `nil` en todas las escenas salvo `pit`, y `nil` cuenta como falso en un `if`, así que esto se lee como «si esta escena ha sido una muerte». No tienes que escribir `death = false` en los otros tres registros, y no deberías: que falte el campo lo dice igual de bien, y así hay un único sitio que decide qué escenas son mortales. El reintento son las dos líneas de debajo. Quita una vida y, si queda alguna, quédate en el fotograma y espera a la barra espaciadora. Luego `return scene.goesTo` manda al jugador de vuelta a `arrive`, y quien lo sabe es el registro. Cambia `goesTo` a `"sunset"` y el reintento se convierte en un punto de control en lugar de volver a empezar -- una palabra, y cambia la dificultad del juego. [source,lua] ---- overlayPrint(2, 6, math.ceil((scene.last - frame) / FPS) .. " seconds left.") ---- El temporizador. `scene.last - frame` es cuántos fotogramas quedan, y dividirlo por los fotogramas de un segundo lo convierte en segundos. `math.ceil` redondea un número hacia arriba hasta el siguiente entero, así que la cuenta atrás pone `3`, `2`, `1` y nunca `0.4` ni `2.966666`. `FPS` es `30` porque este vídeo va a algo más de treinta fotogramas por segundo. Es el único número del script que es una suposición, y es una suposición con nombre al principio del archivo en lugar de un `30` enterrado en mitad de una línea, para que cuando cambies de vídeo haya exactamente una cosa que cambiar. Fíjate en contra qué se mide el temporizador: la película. El jugador compite contra el vídeo, no contra un reloj, así que la pregunta nunca puede durar más que el plano al que pertenece. Esa es la razón por la que los juegos con ramificaciones cuentan en fotogramas. [#_why_not_write_it_out_by_hand] === Por qué no escribirlo a mano Podrías escribir este juego como una sola función larga. Reproduce los fotogramas 0 a 119; si izquierda, reproduce 120 a 259; si izquierda otra vez, reproduce 340 a 410; si no, reproduce 260 a 339, quita una vida y vuelve arriba. Con cuatro escenas sería más corto que lo que tienes. Con cuarenta escenas sería un desastre, y vale la pena ser preciso sobre por qué. Los números de fotograma estarían repartidos por todo el código, cada uno escrito una vez y sin nombre, así que cambiar dónde empieza una escena significaría encontrar cada sitio donde se menciona. La forma de la historia estaría enterrada en la forma de las instrucciones `if`, así que nadie podría ver el juego sin leerlo entero. Y añadir una escena significaría editar el código que reproduce, que es el código que ya funciona -- cada escena nueva, otra ocasión de romper una vieja. La versión que has escrito tiene un trozo de código que reproduce escenas y un trozo de datos que dice cuáles son las escenas. Añadir una escena solo toca los datos. Eso vale la media hora de más que te ha costado, y es la misma idea que hay debajo del archivo `games.dat` de la lección trece: describe la cosa, y luego escribe un trozo de código que lea la descripción. [#_try_it_16] === Pruébalo . *Añade un final.* Dale a `escape` una pregunta y una segunda escena final que reutilice fotogramas de algún punto de la película. Solo cambia la tabla. . *Hazlo justo.* Baja `ask` en las dos escenas para que el jugador tenga cinco segundos en lugar de tres, y luego súbelo para que tenga uno. Encuentra el número que publicarías de verdad. . *Mueve el punto de control.* Cambia el `goesTo` de `pit` a `"sunset"` y juega. Luego piensa en qué pasa si un jugador muere en `arrive` -- y en si la escena de muerte debería saber de dónde viene en lugar de que se lo digan. . *Cuenta lo que ha visto el jugador.* Añade una tabla junto a `scenes` que apunte qué nombres de escena se han reproducido, y muestra la cuenta en la esquina. Ahora tienes un juego que puede decir «has encontrado tres de los cinco finales». . *Rompe el mapa.* Cambia el `left` de `arrive` a `"cave"`, una escena que no existe. Ejecútalo, ve a la izquierda y lee lo que dice Lua. Ese error es el más común que tiene un juego dirigido por datos, y conocer su forma te ahorra una hora. [#_break_it_on_purpose_15] === Rómpelo a propósito Quita la guarda de la línea de `asking`, para que quede así: [source,lua] ---- local asking = frame >= scene.ask ---- Ejecútalo y ve a la derecha. La escena `arrive` se reproduce bien, porque `arrive` tiene un `ask`. Luego empieza la escena `pit`, que no lo tiene, y el juego se para con esto: ---- Error executing function 'onOverlayUpdate': movie/dragon.singe:68: attempt to compare nil with number stack traceback: [C]: in function 'error' Singe/Framework.singe:603: in function 'onOverlayUpdate' ---- Léelo como te enseñó la lección uno. El archivo y la línea van primero, y son exactamente correctos: la línea 68 es la que acabas de cambiar. La queja es el resto: le has preguntado a Lua si un número era mayor que `nil`, y Lua no tiene ni idea. `scene.ask` es `nil` porque el registro `pit` no tiene campo `ask`, y un campo que no está es `nil`. Hay una parte de ese mensaje que merece una explicación, porque es una mentira por omisión. La función que nombra es `onOverlayUpdate`, que tú no has escrito. Es la del propio framework, la que según la lección quince se instala para mover tu `singeMain`. El error ha salido a través de ella, así que ese es el nombre que el motor tenía a mano, y la traza de la pila que hay debajo -- la lista de las funciones que se estaban ejecutando, empezando por la más interna -- solo puede mostrar al framework pasando tu error. No hagas caso a ninguno de los dos. El archivo y la línea de arriba son tuyos, y son los que dicen la verdad. [#_what_you_learned_16] === Lo que has aprendido * Un juego con ramificaciones es un conjunto de escenas, cada una de las cuales nombra las escenas que van después de ella. * Guarda las escenas como datos -- una tabla de registros -- y el código que las reproduce seguirá siendo una sola función por mucho que crezca el juego. * Un registro guarda el nombre de la escena siguiente, no la escena, y el nombre se busca cuando hace falta. * Un campo que no está en un registro es `nil`, lo cual no es un error, y el código que lee registros tiene que contar con ello. * `scene[answer]` busca el campo cuyo nombre está en una variable; `scene.left` y `scene["left"]` son el mismo campo. * Pon el temporizador sobre el vídeo: una elección dura de un fotograma a otro, así que nunca puede durar más que su propio plano. * Tira todo lo que el jugador haya pulsado antes de que apareciera la pregunta. * Pausa el disco en cuanto termine una escena, o seguirá reproduciendo las imágenes de la escena siguiente. * Un campo ausente que hace las veces de falso -- `death` en tres de los cuatro registros -- mantiene la descripción corta y honesta. * Comparar `nil` con un número es un error, y `and` es la forma de evitarlo. [#_next_time_16] === La próxima vez Tu juego pregunta izquierda o derecha. Las máquinas recreativas en las que creció este tipo de juego preguntaban algo más difícil: apunta a la cosa de la pantalla y dispárale. La lección diecisiete trata de las pistolas de luz -- adónde apunta el jugador, qué hace el juego con eso y la calibración sin la que no puede vivir una recreativa de verdad.