[#_lesson_25_particles] == Lección 25: Partículas image::learn/25-particles.png[La lección terminada, 480] Al final de esta lección, una nave cruzará la parte de abajo de la pantalla dejando un rastro de fuego detrás de su motor, y un botón la hará estallar en ciento cuarenta chispas que giran y rebotan en el suelo. No escribirás ni una sola línea que dibuje una chispa. Esa es toda la idea de esta lección. Tú describes el fuego; el motor lo hace. [#_why_you_do_not_draw_the_sparks] === Por qué no dibujas tú las chispas Podrías hacerlo al revés. Sabes lo suficiente: una tabla de chispas de la lección seis, cada una con una x, una y, y una velocidad; un bucle en `onOverlayUpdate` que las mueve todas, las desvanece, quita cada una cuando muere y la dibuja. Funciona. También son doscientas líneas de contabilidad que cada juego vuelve a escribir, y se ejecuta en Lua, una chispa cada vez, sesenta veces por segundo. En lugar de eso, Singe lo hace por ti. Le das una *receta* -- cuántas chispas por segundo, cuánto vive cada una, a qué velocidad y hacia dónde salen, qué tira de ellas, qué aspecto tienen y a qué color se desvanecen -- y él se encarga de las chispas. Lo que guarda la receta y la ejecuta se llama *emisor*. Un emisor no es una imagen ni un objeto de tu juego. Es la descripción de un tipo de fuego, guardada en algún sitio que puedes señalar, y puede hacer ese fuego en cualquier sitio que le pidas. [#_start_with_something_that_moves] === Empieza con algo que se mueve Las partículas resultan más convincentes cuando salen de algo, así que empieza con una nave. Crea una carpeta, copia dentro la carpeta `art` del libro y pon esto en `particles.singe`: [source,lua] ---- dofile("Singe/Framework.singe") local SHIP_WIDTH = 32 local SHIP_HEIGHT = 24 local SHIP_SPEED = 120 local shipSprite = spriteLoad(DIR .. "art/spaceship.png") local shipX = (overlayGetWidth() - SHIP_WIDTH) / 2 local shipY = overlayGetHeight() - 56 local movingLeft = false local movingRight = false local lastTicks = singeGetTicks() function onInputPressed(what) if what == SWITCH_LEFT then movingLeft = true elseif what == SWITCH_RIGHT then movingRight = true end end function onInputReleased(what) if what == SWITCH_LEFT then movingLeft = false elseif what == SWITCH_RIGHT then movingRight = false end end function onOverlayUpdate() local now = singeGetTicks() local seconds = (now - lastTicks) / 1000 lastTicks = now if movingLeft then shipX = shipX - SHIP_SPEED * seconds end if movingRight then shipX = shipX + SHIP_SPEED * seconds end overlayClear() spriteDraw(shipSprite, shipX, shipY) return OVERLAY_UPDATED end ---- Ejecútalo con `Singe -R particles` y maneja la nave con las teclas de flecha izquierda y derecha. Aquí no hay nada nuevo: un sprite de la lección nueve, los interruptores de la lección tres y `singeGetTicks` para que la nave se mueva a la misma velocidad sea cual sea la tasa de fotogramas. La única línea que merece la pena señalar es `dofile("Singe/Framework.singe")`. Es la que define `DIR`, la carpeta donde está tu script, así que `DIR .. "art/spaceship.png"` encuentra la imagen sea cual sea la carpeta desde la que arrancaste Singe. [#_give_it_an_exhaust] === Ponle un escape Ahora el fuego. Añade esto encima de los callbacks, después de las variables de la nave: [source,lua] ---- local exhaust = emitterNew() emitterSetBlend(exhaust, PARTICLE_ADD) emitterSetRate(exhaust, 90) emitterSetLife(exhaust, 0.15, 0.40) emitterSetDirection(exhaust, 0, 1) emitterSetSpread(exhaust, 12) emitterSetSpeed(exhaust, 40, 90) emitterSetSize(exhaust, 7, 1) emitterSetColor(exhaust, 255, 240, 180, 255, 255, 60, 0, 0) emitterSetMax(exhaust, 120) emitterStart(exhaust) ---- Y dos líneas dentro de `onOverlayUpdate`. La primera va justo antes de `overlayClear` y la segunda justo después de `spriteDraw`: [source,lua] ---- emitterSetPosition(exhaust, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT - 2) ---- [source,lua] ---- emitterDraw(exhaust) ---- Guarda. Ahora la nave va montada sobre una lengua corta de llama amarilla, y la llama la acompaña cuando la manejas. [#_blow_it_up] === Hazla estallar Un escape funciona para siempre. Una explosión ocurre una sola vez, y para el motor eso es otro tipo de petición al mismo emisor. Antes, dos archivos más y un número más. Estos van junto al sprite de la nave, arriba del todo: [source,lua] ---- local sparkSprite = spriteLoad(DIR .. "art/star.png") local boomSound = soundLoad(DIR .. "art/boom.wav") ---- y esto va junto a `shipX` y `shipY`, veinte unidades por encima de la parte de abajo de la pantalla: [source,lua] ---- local ground = overlayGetHeight() - 20 ---- Ahora la segunda receta, debajo de la primera: [source,lua] ---- local boom = emitterNew() emitterSetTexture(boom, sparkSprite) emitterSetBlend(boom, PARTICLE_ADD) emitterSetLife(boom, 0.4, 1.1) emitterSetSpeed(boom, 40, 220) emitterSetSpread(boom, 180) emitterSetGravity(boom, 0, 160) emitterSetDrag(boom, 0.9) emitterSetSize(boom, 10, 2, 0.4) emitterSetSpin(boom, -240, 240) emitterSetColor(boom, 255, 255, 220, 255, 255, 70, 20, 0) emitterSetCollide(boom, COLLIDE_FLOOR, 0.35, 0.3, ground) emitterSetTrail(boom, 4, 2) emitterSetMax(boom, 400) ---- Fíjate en que falta `emitterStart`. Este no va a soltar nunca un chorro. Añade una función que lo dispare, encima de `onInputPressed`: [source,lua] ---- function explode() if not flying then return end flying = false emitterStop(exhaust) emitterSetPosition(boom, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT / 2) emitterBurst(boom, 140) soundPlay(boomSound) timerAfter(RESPAWN_MS, function() flying = true emitterStart(exhaust) end) end ---- Para eso hacen falta tres cosas más: `local RESPAWN_MS = 1400` con las demás constantes, `local flying = true` con las demás variables y una rama en `onInputPressed`: [source,lua] ---- elseif what == SWITCH_BUTTON1 then explode() ---- Por último, la nave no debería estar ahí mientras está hecha pedazos. Envuelve el manejo y el dibujo de `onOverlayUpdate` en `if flying then ... end`, y añade `emitterDraw(boom)` junto a la otra llamada de dibujo. El script terminado es `learn/25-particles.singe`. Pulsa la *barra espaciadora* -- el botón uno por defecto -- y la nave se deshace en una nube de estrellas que dan vueltas, caen en arco, chocan contra el suelo y rebotan. [#_what_just_happened_21] === Qué acaba de pasar En esta lección hay muchísimas llamadas nuevas, y todas tienen la misma forma: primero el emisor y luego lo que estás ajustando. Míralas por grupos. [source,lua] ---- local exhaust = emitterNew() ---- Crea un emisor y te devuelve un *handle*: un número que el motor usa para volver a encontrarlo, exactamente igual que el número que te da `spriteLoad`. Sin argumento obtienes un emisor bidimensional, que vive en coordenadas del overlay y se dibuja desde `onOverlayUpdate`. Dale un nodo a `emitterNew` y obtienes uno tridimensional, que vive en la escena y se dibuja solo; todo lo demás de esta lección es igual para los dos. Un emisor nuevo ya tiene una receta completa -- cincuenta partículas por segundo, blancas, un segundo o dos de vida, subiendo despacio por la pantalla -- así que con cada llamada posterior le llevas la contraria a un valor por defecto. La entrada de la referencia para `emitterNew` los enumera todos. [source,lua] ---- emitterSetRate(exhaust, 90) emitterSetLife(exhaust, 0.15, 0.40) ---- Noventa partículas por segundo, cada una viviendo entre quince centésimas y cuatro décimas de segundo. Esos dos números juntos deciden cuánto fuego hay: noventa por segundo que viven un tercio de segundo son unas treinta en pantalla en cada momento. Siempre que una llamada de emisor recibe un mínimo y un máximo, cada partícula recibe su propio valor, elegido al azar entre los dos. De ahí viene la irregularidad. Un fuego en el que cada llama viviera exactamente lo mismo que todas las demás parecería una máquina. [source,lua] ---- emitterSetDirection(exhaust, 0, 1) emitterSetSpread(exhaust, 12) emitterSetSpeed(exhaust, 40, 90) ---- Hacia dónde salen, lo ancho que es el abanico y a qué velocidad. La Y del overlay crece *hacia abajo*, como desde la lección uno, así que `(0, 1)` es hacia abajo en la pantalla -- por la parte de atrás de una nave que apunta hacia arriba. `emitterSetSpread` es la mitad del ángulo del abanico: doce grados es un haz estrecho, noventa lo abre a medio círculo y ciento ochenta es todas las direcciones a la vez. [source,lua] ---- emitterSetSize(exhaust, 7, 1) emitterSetColor(exhaust, 255, 240, 180, 255, 255, 60, 0, 0) ---- Estas dos son la razón de que la llama parezca una llama. Una partícula no tiene un tamaño y un color; tiene un tamaño y un color *al nacer* y otros *al morir*, y pasa de manera uniforme de unos a otros a lo largo de la vida que le haya tocado. Así que cada chispa empieza con siete unidades del overlay de ancho y de color amarillo pálido, y acaba con una unidad de ancho y de color rojo anaranjado con un alfa de cero, que es invisible. Encoge y se desvanece porque tú dijiste dónde acaba, no porque algo la desvaneciera. `emitterSetColor` recibe ocho números, que es mucho que leer: rojo, verde, azul y alfa al principio, y luego rojo, verde, azul y alfa al final. [source,lua] ---- emitterSetBlend(exhaust, PARTICLE_ADD) ---- Cómo se combina una partícula con lo que tiene detrás. `PARTICLE_ALPHA` es pintar normal y es el valor por defecto: úsalo para humo, polvo, lluvia y escombros. `PARTICLE_ADD`, en cambio, suma su luz a la imagen, así que dos chispas una encima de otra brillan más que una y nada se oscurece nunca. El fuego, las chispas, los láseres y la magia son aditivos. El humo no, y el humo dibujado de forma aditiva parece vapor iluminado desde dentro. [source,lua] ---- emitterSetMax(exhaust, 120) emitterStart(exhaust) ---- `emitterSetMax` es el tamaño de la reserva: cuántas partículas de este emisor pueden existir a la vez. Cuando la reserva está llena no nace ninguna nueva hasta que muere alguna. Noventa por segundo que viven hasta cuatro décimas de segundo necesitan unas treinta y seis, así que ciento veinte va holgado. Ajústalo antes de arrancar, porque cambiarlo después tira todas las partículas vivas. `emitterStart` abre el grifo. A partir de ahí el emisor crea partículas en cada fotograma, lo dibujes o no, hasta que `emitterStop` lo vuelve a cerrar. Fíjate en lo que `emitterStop` *no* hace: detiene las partículas nuevas, y las que ya están vivas terminan su vida y se desvanecen con normalidad. Por eso el escape se va apagando en lugar de desaparecer de golpe cuando la nave explota. Si las quieres fuera en este mismo instante, `emitterClear` las mata. [source,lua] ---- emitterSetPosition(exhaust, shipX + SHIP_WIDTH / 2, shipY + SHIP_HEIGHT - 2) ---- Así es como un emisor sigue a algo. Un emisor bidimensional crea sus partículas en un punto, y ese punto es donde lo pusiste la última vez, así que lo ajustas en cada fotograma a partir de lo que se esté moviendo: la posición de un sprite, el nodo de un cuerpo físico, el ratón. Las partículas nuevas aparecen en el sitio nuevo; las que ya están en el aire se quedan donde nacieron, que es justo lo correcto para un escape, porque el escape se queda atrás. Un emisor tridimensional se crea sobre un nodo y sigue a ese nodo por su cuenta, sin ninguna llamada en tu actualización. Y cuando *sí* quieres que toda la nube viaje con la cosa -- un escudo, un aura, la llama de un propulsor vista desde fuera -- `emitterSetLocal` lo activa. [source,lua] ---- emitterDraw(exhaust) ---- Los emisores bidimensionales se dibujan cuando lo pides, una vez por fotograma, desde `onOverlayUpdate`. El orden de tus llamadas a `emitterDraw` es el orden en que se apilan. Olvidar esta llamada es el error que todo el mundo comete una vez: el emisor sigue funcionando, sigue gastando su reserva y no muestra nada. Ahora las llamadas propias de la explosión. [source,lua] ---- emitterSetTexture(boom, sparkSprite) ---- Sin textura, cada partícula es una mancha redonda y suave que dibuja el propio motor, que es lo que usa el escape y lo adecuado para una llama. Con una, cada partícula lleva puesta esa imagen -- aquí `star.png`, de ocho por ocho píxeles. El emisor se queda con su propia copia de los píxeles, así que puedes descargar el sprite después y las partículas siguen funcionando. [source,lua] ---- emitterSetGravity(boom, 0, 160) emitterSetDrag(boom, 0.9) ---- La gravedad es un tirón constante, en unidades del overlay por segundo por segundo, y es positiva hacia abajo por el mismo motivo por el que el escape sale con una Y positiva. El rozamiento es lo contrario: quita velocidad, un poco en cada fotograma, así que las chispas salen disparadas con fuerza y luego se quedan suspendidas. Las chispas con gravedad y sin rozamiento caen en línea recta como un fuego artificial; el rozamiento es lo que hace que parezca que se mueven a través del aire. [source,lua] ---- emitterSetSpin(boom, -240, 240) emitterSetSize(boom, 10, 2, 0.4) ---- El giro va en grados por segundo, y un rango que cruza el cero te da algunas que giran hacia cada lado. Se nota en una textura y no le hace nada en absoluto a la mancha incorporada, que es redonda. El tercer número de `emitterSetSize` es la variación. Escala los dos tamaños de cada partícula por un mismo factor aleatorio -- aquí cualquier valor entre seis décimas y uno con cuatro décimas -- para que las chispas no sean todas la misma estrella del mismo tamaño. [source,lua] ---- emitterSetCollide(boom, COLLIDE_FLOOR, 0.35, 0.3, ground) emitterSetTrail(boom, 4, 2) ---- `COLLIDE_FLOOR` pone un suelo plano e invisible de lado a lado del mundo a la altura que indiques, y las partículas rebotan en él. Los dos números que van antes son cuánta velocidad sobrevive a un rebote y cuánta velocidad lateral se pierde. No cuesta nada, porque es una comparación por partícula, y hace que los restos parezcan haber caído en algún sitio en lugar de atravesar el mundo. En un juego tridimensional, `COLLIDE_SCENE` hace rebotar las partículas en la geometría real, lo que cuesta muchísimo más. Una estela guarda los últimos sitios por los que ha pasado una partícula y dibuja a través de ellos una cinta que se desvanece. Cuatro posiciones y dos unidades de ancho convierten cada estrella en un trazo corto. [source,lua] ---- emitterBurst(boom, 140) ---- Y esta es la otra forma de crear partículas. `emitterStart` es un grifo; `emitterBurst` es un puñado lanzado de golpe. El emisor no tiene que estar soltando un chorro, y este nunca lo está. Todo lo demás -- la receta, la reserva, la posición -- funciona igual. Merece la pena quedarse con esta distinción. Todo lo continuo es un chorro que abres y cierras: escape, humo, lluvia, una antorcha, una cascada. Todo lo que pasa en un momento es una ráfaga: una explosión, una salpicadura, una pisada en el polvo, el impacto de una bala, el fogonazo de un disparo. Muchos efectos son las dos cosas, con un emisor o con dos: un proyectil estalla y luego echa humo. [#_how_many_is_too_many] === Cuántas son demasiadas La respuesta sincera es que las partículas son baratas de mover y caras de mirar. Mover diez mil partículas es aritmética, y un ordenador capaz de ejecutar un juego puede hacerlo sin enterarse. Lo que cuesta es dibujarlas, y lo que cuesta dibujarlas es más o menos el número de píxeles de pantalla que cubren las partículas, contando los solapamientos. Doscientas chispas de ocho píxeles de ancho cubren menos pantalla que una bocanada de humo que la llena entera. Esa es la regla: pequeñas y muchas es casi gratis, y grandes y solapadas no. La mezcla aditiva es la cara, porque nada de lo que dibuja se puede saltar -- cada capa se suma a lo que hay debajo, así que la máquina las pinta todas. Una docena de destellos aditivos a pantalla completa uno encima de otro dejarán una Raspberry Pi 4 a paso de tortuga, mientras que diez mil chispitas que rebotan no la inmutan. Así que unos pocos miles de partículas pequeñas no son motivo de preocupación. Antes de pasar de ahí, pon el recuento en pantalla y vigílalo. [source,lua] ---- overlayPrint(2, 4, "Particles alive: " .. (emitterGetCount(exhaust) + emitterGetCount(boom))) ---- `emitterGetCount` es cómo dimensionas una reserva con honradez en lugar de a ojo. Si el número se queda exactamente en tu `emitterSetMax`, la reserva está llena y el motor se está negando en silencio a crear partículas que le has pedido. Si nunca se acerca, estás reservando memoria para nada. [#_try_it_25] === Pruébalo . *Convierte el escape en humo.* Cambia `emitterSetBlend(exhaust, PARTICLE_ADD)` por `PARTICLE_ALPHA`, pon el color en gris que se desvanece a gris transparente y haz que crezca en lugar de encoger con `emitterSetSize(exhaust, 3, 14)`. Tres líneas han hecho una máquina distinta. . *Quítale el rozamiento a la explosión.* Borra la línea `emitterSetDrag` y compara. Luego vuelve a poner el rozamiento y quita la gravedad. Una de las dos parece fuegos artificiales y la otra parece un accidente. . *Ensancha el escape.* Pon `emitterSetSpread(exhaust, 180)`. La nave deja de parecer que tiene un motor. La apertura del abanico hace más de lo que crees. . *Lanza mil.* Cambia `emitterBurst(boom, 140)` por `emitterBurst(boom, 1000)` y mira el recuento en pantalla. No llegará a mil. Averigua por qué a partir de `emitterSetMax`, luego agranda la reserva, haz las chispas cuatro veces más grandes y mira qué le pasa a la tasa de fotogramas. . *Lanza la explosión donde no está la nave.* Pon `emitterSetPosition(boom, 40, 60)` en `explode` en lugar de la posición de la nave. Un emisor es la descripción de un fuego, no una cosa que viva en algún sitio, y puedes lanzar una ráfaga del mismo emisor en diez sitios en el mismo fotograma. [#_break_it_on_purpose_24] === Rómpelo a propósito Las constantes -- `PARTICLE_ADD`, `COLLIDE_FLOOR`, `OVERLAY_UPDATED` -- son nombres que Singe les ha dado a números. Escribe una un poco mal y Lua no se queja, porque en Lua un nombre al que nunca se le dio un valor no es un error: es `nil`, el valor que significa «aquí no hay nada». Cambia la línea de mezcla del escape por un nombre que parece igual de probable: [source,lua] ---- emitterSetBlend(exhaust, PARTICLE_ADDITIVE) ---- Singe se para antes de que arranque el juego, nombrando la línea de tu archivo en la que está esa llamada: ---- 22:emitterSetBlend: Argument 2 must be a number. ---- Este error tiene una forma distinta de los errores de Lua de la lección ocho, porque viene del motor y no de Lua. Es el número de línea, luego la llamada que protestó y luego la queja. El argumento dos de `emitterSetBlend` es el modo de mezcla, y lo que llegó fue `nil`, porque `PARTICLE_ADDITIVE` no es un nombre que nadie haya definido nunca. «Must be a number» casi siempre significa esto: una constante mal escrita, o una variable que creías haber asignado. El arreglo es comparar el nombre con la referencia, que lo escribe `PARTICLE_ADD`. [#_what_you_learned_25] === Lo que has aprendido * Un emisor guarda la receta de un tipo de partícula y se encarga él mismo de las partículas, así que tu script nunca dibuja ninguna. * `emitterNew` sin argumento crea un emisor bidimensional en coordenadas del overlay; con un nodo crea uno tridimensional en la escena. * Todos los emisores empiezan con una receta completa, y cada llamada `emitterSet...` cambia una parte de ella. * Siempre que una llamada recibe un mínimo y un máximo, cada partícula saca su propio valor al azar entre los dos, y de ahí viene la irregularidad. * El tamaño y el color se fijan al nacer y al morir, y la partícula pasa de uno a otro a lo largo de su vida. * `emitterStart` suelta un chorro hasta `emitterStop`; `emitterBurst` lanza un puñado de golpe. Los efectos continuos son chorros, los momentáneos son ráfagas. * `emitterStop` deja que las partículas vivas terminen; `emitterClear` las mata ya. * Un emisor bidimensional se mueve con `emitterSetPosition` en cada fotograma y se dibuja con `emitterDraw` en cada fotograma. Uno tridimensional sigue a su nodo y se dibuja solo. * Las partículas cuestan lo que cubren en pantalla, no lo que cuesta moverlas, y la mezcla aditiva es la cara. * `emitterSetMax` pone un tope a la reserva, y `emitterGetCount` te dice si acertaste con el tamaño. [#_next_time_25] === La próxima vez La explosión movió ciento cuarenta cosas sin que nadie le dijera adónde tenía que ir ninguna, pero eran chispas, y a las chispas les da igual con qué choquen. La lección veintiséis trata de algo a lo que no le da igual: un personaje que tiene que llegar de aquí a allí, con una pared en medio.