548 lines
21 KiB
Text
548 lines
21 KiB
Text
[#_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.
|