[#_lesson_11_hitting_things] == Lección 11: Chocar con cosas image::learn/11-hitting-things.png[La lección terminada, 480] Tu juego de la lección diez funciona. Los disparos rompen rocas, las rocas cuestan vidas y las explosiones suenan donde tienen que sonar. Pero míralo de cerca y verás que hace trampa, en las dos direcciones. Una roca que pasa rozando la punta de un ala te quita una vida. Un disparo que atraviesa el cielo vacío junto a una roca la rompe igualmente. En un juego, dos cosas se tocan cuando los números dicen que se tocan. Nadie mira las imágenes. Esta lección hace que los números estén de acuerdo con lo que ve el jugador: cambia tu comprobación escrita a mano por la del propio motor, encoge las cajas que se comprueban para que se ajusten a las cosas que hay dentro y te da un interruptor que te enseña esas cajas. La imagen de arriba es el juego con ese interruptor encendido. [#_where_you_are] === Dónde estás El juego pregunta «¿se tocan estas dos cosas?» exactamente en dos sitios, y los dos usan `overlapping`, la función que escribiste en la lección nueve. Uno está en `updateRocks`, donde una roca se encuentra con la nave: [source,lua] ---- if overlapping(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then ---- El otro está en `updateShots`, donde un disparo se encuentra con una roca: [source,lua] ---- if not gone and overlapping(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then ---- Todo lo de esta lección cambia esas dos líneas y añade unas cuantas funciones pequeñas que las alimentan. Nada más del juego se mueve. El script terminado entero está en `learn/11-hitting-things.singe`, por si te pierdes. [#_read_it_once_more] === Léela una vez más La lección nueve ya hizo esta parte: `overlapping` es la colisión de rectángulos escrita a mano. Léela una vez más antes de tirarla, porque es la idea entera: [source,lua] ---- 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 ---- La pregunta «¿se están tocando estas dos cosas?» es difícil de responder directamente y fácil de responder al revés. Dos rectángulos *no* se tocan cuando uno de ellos está entero a la izquierda del otro, o entero a la derecha, o entero encima, o entero debajo. No hay una quinta forma de fallar. Si no se cumple ninguna de esas cuatro, se tocan. Cada `if` es una de las cuatro formas de fallar. `ax + aw` es el borde derecho del primer rectángulo, así que `ax + aw <= bx` se lee «el primero termina antes de que empiece el segundo», que es lo que significa «entero a la izquierda» en números. La misma forma tres veces más, para la derecha, encima y debajo, con `y` y las alturas en lugar de `x` y las anchuras, y lo único que queda es `true`. Esos son los ocho números que necesita cualquier comprobación de colisión: una esquina y un tamaño para cada una de dos cosas. Guarda la forma en la cabeza, porque estás a punto de borrarla. [#_the_engine_already_has_it] === El motor ya lo tiene Conociste `collideRects` en la lección siete, donde atrapaba los bloques que caían en Dodge. La lección nueve escribió la misma comprobación a mano para que pudieras verla por dentro. Ya la has visto por dentro. Borra `overlapping` y cambia las dos líneas de *Dónde estás* para que llamen al motor: [source,lua] ---- if collideRects(shipX, shipY, shipWidth, shipHeight, rock.x, rock.y, rockWidth, rockHeight) then ---- [source,lua] ---- if not gone and collideRects(shot.x, shot.y, shotWidth, shotHeight, rock.x, rock.y, rockWidth, rockHeight) then ---- Los mismos ocho números, en el mismo orden: la esquina y el tamaño del primer rectángulo, y luego los del segundo. Ejecútalo, y el juego funciona exactamente igual que antes, que es de lo que se trata. La única diferencia está justo en el borde. Tu `<=` decía que dos rectángulos que se encuentran a lo largo de un borde no se tocan. `collideRects` dice que sí, y un rectángulo sin anchura o sin altura no toca nada en absoluto. Esos casos incómodos son donde las versiones escritas a mano fallan sin avisar, y son una de las razones para usar la del motor. `collideRects` es una de una familia de comprobaciones de colisión, y la familia es una parte de lo que este libro llama el *kit de juego*: un puñado de funciones sencillas para las cosas que casi todos los juegos tenían que escribirse ellos mismos. Comprobar si dos cosas se tocan es una. Guardar un número para la próxima vez es otra, y es la lección doce. Esperar un rato, llevar un número suavemente de un valor a otro y mostrar cuánto ha tardado cada fotograma son el resto. La referencia le da a cada parte una sección propia: Colisión, Partida guardada, Temporizador, Interpolación y Estadísticas. Las comprobaciones de colisión son las más sencillas de todas: preguntas sencillas, sin handles que guardar ni nada que preparar o liberar. Y ninguna mueve nada. `collideRects` te dice que la nave y la roca están en el mismo sitio; qué hacer al respecto lo decides tú, y todo lo interesante de un juego ocurre en esa decisión. [#_the_picture_is_not_the_hit_box] === La imagen no es la zona de impacto La lección diez prometió explicar por qué el rectángulo que rodea una roca redonda tiene la forma equivocada. El rectángulo que rodea la nave también está mal, y por la misma razón. Dibuja en papel el rectángulo de 32 por 24 de la nave y sombrea la nave. Es un triángulo con dos aletas, así que hay cielo vacío en las dos esquinas de arriba y entre las aletas. La roca es redonda, así que hay cielo vacío en las cuatro esquinas de su cuadrado. Una roca que roza la esquina de arriba del rectángulo de la nave no ha chocado con la nave. Ha pasado volando junto al ala, y un jugador que pierde una vida por eso dirá que el juego ha hecho trampa -- y tendrá razón. Así que la caja que compruebas no es la imagen que dibujas. Es más pequeña, y está dentro de la imagen. Añade tres números a las constantes de arriba del todo de tu archivo: [source,lua] ---- local SHIP_PAD_X = 6 local SHIP_PAD_Y = 5 local ROCK_PAD = 4 ---- El margen es lo lejos de cada borde que empieza la cosa de verdad. Seis píxeles menos por cada lado de la nave y cinco menos por arriba y por abajo dejan una caja de 20 por 14 en el centro de una imagen de 32 por 24, que es más o menos donde está el casco. Cuatro menos por cada lado de la roca dejan 16 por 16, y eso es casi todo roca. Las dos cajas son un poco más pequeñas que las cosas que representan, y es a propósito. Una caja un poco demasiado pequeña le da al jugador escapadas por los pelos que parecen habilidad. Una caja un poco demasiado grande le da golpes que parecen trampa. Los jugadores perdonan lo primero y nunca lo segundo. En lugar de escribir la aritmética en cada comprobación, escríbela una vez para cada tipo de cosa: [source,lua] ---- function rockBox(rock) return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2 end function shipBox() return shipX + SHIP_PAD_X, shipY + SHIP_PAD_Y, shipWidth - SHIP_PAD_X * 2, shipHeight - SHIP_PAD_Y * 2 end ---- Los tamaños salen de `rockWidth`, `shipWidth` y compañía, las medidas que la lección nueve tomó de las imágenes, así que sigue sin haber ni un tamaño en píxeles a pelo en el código del juego. Vuelve a dibujar la nave más grande y la caja crece con ella. Cada una de esas funciones devuelve cuatro números a la vez, separados por comas. Una función puede devolver tantos valores como quiera, y la forma de recogerlos es una lista de nombres a la izquierda del `=`: [source,lua] ---- function shipHitsRock(rock) local sx, sy, sw, sh = shipBox() local rx, ry, rw, rh = rockBox(rock) return collideRects(sx, sy, sw, sh, rx, ry, rw, rh) end ---- Ahora la comprobación de `updateRocks` se lee como la pregunta que hace: [source,lua] ---- if shipHitsRock(rock) then ---- La aritmética de los márgenes está exactamente en dos sitios, y los números que usa están arriba del todo del archivo, así que puedes ajustar lo justo que se siente el juego cambiando un número en lugar de ir buscando por tus comprobaciones. Pon las funciones nuevas en su sitio del orden alfabético, como pedía la lección nueve: `rockBox` después de `onSoundCompleted`, y luego `shipBox` y `shipHitsRock`. [#_firing_back] === Devolver el fuego Con eso está hecha la parte de la nave. La otra colisión del juego es con la que devuelves el fuego: cada disparo contra cada roca, en `updateShots`. Tiene el mismo problema que tenía la nave, en pequeño. Un disparo es un rectángulo de 4 por 10 que se comprueba contra el cuadrado entero de 24 por 24 de la roca, así que un disparo que solo pasa por la esquina vacía junto a una roca la rompe igualmente. Podrías darle al disparo una caja con márgenes propia. Pero mira antes los números. Quítale márgenes a un disparo de cuatro píxeles de ancho y no queda casi nada, y comprobar un rectángulo entero para saber si una astilla de luz ha tocado algo es más maquinaria de la que merece la pregunta. [#_a_shot_is_almost_a_point] === Un disparo es casi un punto Comprueba la punta en su lugar. `collidePointRect` pregunta si un punto cae dentro de un rectángulo -- dos números para el punto, y luego la esquina y el tamaño del rectángulo: [source,lua] ---- function shotHitsRock(shot, rock) local rx, ry, rw, rh = rockBox(rock) return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh) end ---- `shot.y` es la parte de arriba del disparo y `shot.x + shotWidth / 2` es la mitad de su anchura, así que el punto que se comprueba es el centro de la punta del disparo. Ahí es donde un jugador cree que está un disparo. El lado de la roca en esta pregunta usa el mismo `rockBox` que la nave, así que una roca mide lo mismo le dé lo que le dé. Va después de `shipHitsRock`, y la comprobación de `updateShots` queda así: [source,lua] ---- if not gone and shotHitsRock(shot, rock) then ---- [#_deciding_what_happened] === Decidir qué ha pasado Ninguna de las comprobaciones de colisión hace nada. Responden, y tu código decide. Aquí están los dos sitios que deciden, con sus preguntas nuevas dentro. Primero `updateRocks`: [source,lua] ---- if shipHitsRock(rock) then local channel = soundPlay(boomClip) newRock(rock) lives = lives - 1 if lives <= 0 then dying = true deathChannel = channel if channel < 0 then endGame() end end end ---- Y el bucle de dentro de `updateShots`: [source,lua] ---- for _, rock in ipairs(rocks) do if not gone and shotHitsRock(shot, rock) then newRock(rock) score = score + HIT_SCORE gone = true soundPlay(boomClip, 0, 45) end end ---- Todo lo que hay debajo de las preguntas es exactamente lo que dejó ahí la lección diez. Una roca que choca con la nave estalla, vuelve arriba y cuesta una vida. Una roca alcanzada por un disparo estalla, vuelve arriba y suma puntos. El disparo también desaparece, y el `not gone` impide que un disparo rompa dos rocas. Cambia cualquiera de esas cosas y cambias el juego, sin tocar ni una sola comprobación de colisión, que es la razón de que las preguntas y las respuestas se mantengan separadas. Ejecútalo. Pasa volando cerca de una roca y deja que te roce una aleta: conservas la vida. Dispara justo al lado de una roca y el disparo pasa de largo. Dispara al centro de una y se rompe, igual que siempre. [#_seeing_the_boxes] === Ver las cajas Las zonas de impacto son invisibles, y eso hace que un error en ellas también lo sea. Dibújalas mientras trabajas. Una constante más, arriba del todo: [source,lua] ---- local SHOW_BOXES = false ---- Dos funciones más, que van al principio del todo del orden alfabético: [source,lua] ---- function drawBox(x, y, width, height) overlayBox(x, y, x + width - 1, y + height - 1) end function drawBoxes() colorForeground(0, 255, 0) drawBox(shipBox()) for _, rock in ipairs(rocks) do drawBox(rockBox(rock)) end end ---- Y en `onOverlayUpdate`, después de dibujar la nave y antes de que la puntuación vaya encima: [source,lua] ---- spriteDraw(shipSprite, shipX, shipY) if SHOW_BOXES then drawBoxes() end drawHud() ---- Cambia `SHOW_BOXES` a `true` y aparecen contornos verdes sobre el arte, como en la imagen del principio de esta lección. Vuelve a cambiarlo cuando acabes. Un interruptor como este, una palabra arriba del todo de un archivo que enciende una imagen de lo que cree el programa, vale más que cualquier cantidad de mirar fijamente. [#_what_just_happened_7] === Qué acaba de pasar [source,lua] ---- function rockBox(rock) return rock.x + ROCK_PAD, rock.y + ROCK_PAD, rockWidth - ROCK_PAD * 2, rockHeight - ROCK_PAD * 2 end ---- Cuatro valores de un solo `return`, separados por comas. Lua devuelve encantado tantos como quieras, y quien llama decide cuántos se queda. Cuando una llamada así es lo *último* dentro de los paréntesis de otra llamada, se pasan los cuatro, y por eso `drawBox(rockBox(rock))` funciona y le da a `drawBox` sus cuatro argumentos. En cualquier otro sitio de una lista de argumentos, solo sobrevive el primer valor. Esa regla muerde exactamente una vez, y muerde al final de esta lección. [source,lua] ---- return collideRects(sx, sy, sw, sh, rx, ry, rw, rh) ---- `collideRects` quiere ocho números: esquina y tamaño, esquina y tamaño. Devuelve `true` o `false` y no cambia nada. Como la respuesta sale directamente de la función con `return`, `shipHitsRock(rock)` se lee como una pregunta allí donde se use. [source,lua] ---- return collidePointRect(shot.x + shotWidth / 2, shot.y, rx, ry, rw, rh) ---- `collidePointRect` quiere seis: un punto, y luego un rectángulo. El borde cuenta como dentro, así que un disparo justo en el límite acierta. Úsala siempre que uno de los lados de la pregunta sea lo bastante pequeño como para que su tamaño no importe. [source,lua] ---- colorForeground(0, 255, 0) overlayBox(x, y, x + width - 1, y + height - 1) ---- Las dos vienen de la lección siete. `overlayBox` recibe dos *esquinas*, no una esquina y un tamaño -- el único sitio de esta lección donde los números cambian de forma -- y el `- 1` está porque las dos esquinas se incluyen. `colorForeground` es un interruptor que se queda puesto, así que todo lo que dibuja `drawBoxes` sale verde. Los sprites y `overlayPrint` no le hacen caso, así que el resto del juego se ve igual. [#_the_rest_of_the_family] === El resto de la familia `collideRects` y `collidePointRect` bastan para la mayoría de los juegos 2D. Las demás están para las formas que los rectángulos describen mal, y cada una es el mismo tipo de pregunta sencilla: * `collideCircles` y `collidePointCircle`, para cualquier cosa redonda. Un círculo es un centro y un radio. * `collideRectCircle`, para una cosa redonda contra una cuadrada. * `collidePointPolygon`, para una forma con esquinas propias -- un dragón, un continente, un botón con forma rara. Le das una lista plana de números `x`, `y`, `x`, `y`. * `collideSegments`, para dos segmentos de línea que se cruzan. Si eres sincero, tu roca es redonda, y las comprobaciones de círculos le van mejor que cualquier caja. Los márgenes son la respuesta barata y bastan para la mayoría de los juegos, y por eso van primero. La respuesta exacta es una de las cosas que probar más abajo. `collideSegments` importa más de lo que parece. Tu disparo se mueve seis píxeles por fotograma y las rocas tienen dieciséis píxeles de zona de impacto, así que un disparo no puede atravesar una sin que se le compruebe dentro de ella. Haz el disparo lo bastante rápido -- cuarenta píxeles por fotograma -- y salta de debajo de una roca a encima de ella, sin estar ni una sola vez en el mismo sitio que la roca, y nunca le da a nada. Trazar la línea desde donde estaba el disparo hasta donde está ahora, y preguntarle a `collideSegments` si esa línea cruza un borde, es la cura. Las cosas lentas no la necesitan. Hay un callback de colisión en Singe, `onCollision`, y no es para esto. Pertenece al motor de física 3D: cuerpos con masa que caen y rebotan unos contra otros en tres dimensiones y te dicen dónde se han tocado y con qué fuerza. Lo conocerás en la lección veinticuatro. Nada del overlay avisa por sí solo, y nada lo necesita. Tus rocas están en una lista, sabes cuántas hay, y preguntar es barato. Y cuando quieras saber no solo *si* dos cosas se han encontrado, sino dónde debería acabar la que se mueve -- deslizándose a lo largo de una pared en lugar de quedarse clavada dentro de ella -- las llamadas de colisión no responden a eso, a propósito. Singe incluye una biblioteca llamada `bump` justo para ese trabajo, y la sección Bibliotecas incluidas de la referencia dice cómo usarla. [#_try_it_11] === Pruébalo . *Quita los márgenes.* Pon `ROCK_PAD` a `0` y juega un minuto. Luego ponlo a `8` y vuelve a jugar. Con uno de los dos parece que el juego te miente. . *Mira lo que estás comprobando.* Pon `SHOW_BOXES` a `true` y observa los contornos verdes mientras una roca pasa junto a la nave. Ahora pon `SHIP_PAD_X` a `16`, juega, y fíjate en que ya nada puede darte. Averigua por qué con la aritmética de `shipBox`. . *Haz el disparo demasiado rápido.* Pon `SHOT_SPEED` a `40`. Cuenta a cuántas rocas de cada diez les das. Averigua con los números por qué pasa eso antes de volver a cambiarlo. . *Usa el disparo entero.* Reescribe `shotHitsRock` para que use `collideRects` con el rectángulo completo del disparo en lugar de `collidePointRect` con su punta. Juega con las dos y decide cuál prefieres; no hay respuesta correcta, y vale la pena saberlo. . *Haz la roca redonda.* Dale a la roca un círculo en lugar de una caja: su centro está a `rock.x + rockWidth / 2` en horizontal y a `rock.y + rockHeight / 2` en vertical, y un radio de `10` es más o menos lo justo. Usa `collideRectCircle` para la nave y `collidePointCircle` para el disparo, y dibuja el círculo en `drawBoxes` con `overlayCircle`, que recibe un centro y un radio. [#_break_it_on_purpose_10] === Rómpelo a propósito Las dos cajas ya son cuatro valores cada una, así que dan ganas de pasarlas directamente y ahorrarse los ocho nombres locales: [source,lua] ---- function shipHitsRock(rock) return collideRects(shipBox(), rockBox(rock)) end ---- Parece correcto y está mal. Ejecútalo y el juego se para antes de dibujar nada: ---- 186:collideRects: Expected 8 argument(s), got 5. ---- La línea es la que acabas de escribir, así que puede que la tuya no sea la 186. El cinco, en cambio, sí. `rockBox(rock)` es lo último dentro de los paréntesis, así que entraron sus cuatro valores. `shipBox()` no lo es, así que se recortó a uno. Uno más cuatro son cinco, y `collideRects` no va a adivinar los otros tres. Este error viene del motor y no de Lua, así que tiene la misma forma que los de las lecciones nueve y diez: el número del principio es la línea, luego viene el nombre de la función que se ha negado, y luego la queja. Vuelve a poner los ocho nombres locales. [#_what_you_learned_11] === Lo que has aprendido * Las colisiones son aritmética con rectángulos, y puedes escribirla tú mismo. * Dos rectángulos pueden no tocarse exactamente de cuatro formas; si no se cumple ninguna, chocan. * `collideRects` recibe una esquina y un tamaño para cada uno de dos rectángulos y responde `true` o `false`. Cuenta como que se tocan los bordes que solo se rozan. * Las comprobaciones de colisión son parte del kit de juego, funciones sencillas sin nada que preparar. Partida guardada, Temporizador, Interpolación y Estadísticas son el resto. * `collidePointRect` pregunta si un punto está dentro de un rectángulo, que es la pregunta adecuada para cualquier cosa pequeña. * La caja que compruebas es más pequeña que la imagen que dibujas, o el juego parecerá injusto. * Una función puede devolver varios valores, y solo la última llamada de una lista de argumentos los pasa todos. * Las comprobaciones de colisión solo responden. Lo que pasa después es cosa de tu código, y mantener las dos partes separadas te deja cambiar cualquiera de ellas por su cuenta. * Dibujar tus zonas de impacto en pantalla convierte un fallo invisible en uno visible. * `onCollision` es para cuerpos de física 3D, no para sprites en el overlay. [#_next_time_11] === La próxima vez El juego cuenta tu puntuación y te quita vidas, y en cuanto sales, la mejor puntuación que nadie haya conseguido desaparece. Empieza en plena acción en el instante en que se carga. Y lleva la cuenta de lo que está haciendo con dos variables de verdadero o falso, `over` y `dying`, que es exactamente la trampa de la que te avisó la lección siete. La lección doce le da una pantalla de título, una sola variable en lugar de esas dos y un récord que mañana seguirá ahí.