singe/docs/es/lessons/24-physics.adoc

549 lines
22 KiB
Text

[#_lesson_24_physics]
== Lección 24: Física
image::learn/24-physics.png[La lección terminada, 480]
Hasta ahora, todo lo que se movía en tus juegos se movía porque tú hacías las
cuentas. Un número subía de dos en dos en cada fotograma; si se pasaba de un
borde, le dabas la vuelta. Es la forma honrada de hacer que un juego se mueva,
y la mayoría de los juegos están hechos de eso.
Esta lección le pasa las cuentas a otro. Un *motor de física* simula el
mundo -- la gravedad, las caídas, los deslizamientos, las pilas, los rebotes,
el rodar y las cosas que chocan entre sí -- y mueve tus nodos por ti. Al final
tendrás una bandeja de cajas y un cañón que las tira. La última sección trata de
cuándo no hacer nada de esto, y es la parte que hay que leer dos veces.
[#_what_it_does_and_what_it_does_not]
=== Lo que hace y lo que no
Dale un *cuerpo* a un nodo y la simulación se hace cargo de él. A partir de
entonces cae por la gravedad, aterriza sobre lo que tenga debajo, se vuelca
cuando le dan descentrado, descansa en una pila, se desliza hasta pararse, y el
nodo va allí donde lo ponga la simulación. Tú dejas de fijar su posición. Ese es
el trato: entregas la posición y la rotación de ese nodo, y a cambio obtienes un
comportamiento que no has tenido que escribir.
Lo que no hace es nada que tenga que ver con tu juego. No sabe qué es una
puntuación, ni que la caja roja es la que da puntos, ni que caerse por el borde
cuesta una vida. No anima a un personaje que camina, no sabe cómo debería
sentirse un salto y no hará que un objeto se mueva de una forma que puedas
predecir con exactitud. La física es una máquina de producir líos verosímiles.
Las unidades son metros, kilogramos y segundos. La gravedad es `0, -9.81, 0`
salvo que digas otra cosa, que es la de la Tierra, hacia abajo, en metros por
segundo al cuadrado. Una caja de una unidad de ancho mide un metro de ancho, y
cae como tal. Si tu escena está construida a otra escala, todo parecerá caer a
través de almíbar o dentro de unos dibujos animados, y el arreglo es construir
en metros.
[#_a_floor_and_one_crate]
=== Un suelo y una caja
Crea una carpeta, crea dentro `crates.singe` y escribe esto. Es una escena, una
losa, una caja encima de la losa y tres líneas de física.
[source,lua]
----
sceneEnable(true)
sceneSetBackground(16, 18, 26)
sceneSetAmbient(40, 42, 50)
local stone = materialNew()
materialSetColor(stone, 110, 110, 120)
materialSetRoughness(stone, 0.9)
local wood = materialNew()
materialSetColor(wood, 190, 140, 80)
materialSetRoughness(wood, 0.8)
local floor = nodeNew()
nodeSetMesh(floor, meshBox(16, 0.4, 16), stone)
nodeSetPosition(floor, 0, -0.2, 0)
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16)
local crate = nodeNew()
nodeSetMesh(crate, meshBox(0.8, 0.8, 0.8), wood)
nodeSetPosition(crate, 0.3, 4, 0)
nodeSetRotation(crate, 20, 0, 15)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
bodySetMass(crate, 4)
local sun = lightNew(LIGHT_DIRECTIONAL)
nodeSetPosition(sun, -5, 9, 6)
nodeLookAt(sun, 0, 0, 0)
lightSetIntensity(sun, 1.7)
lightSetShadow(sun, true)
local camera = nodeNew()
nodeSetPosition(camera, 0, 4, 11)
nodeLookAt(camera, 0, 1, 0)
cameraSet(camera)
function onOverlayUpdate()
return OVERLAY_UPDATED
end
----
Ejecútalo con `Singe -R crates`. La caja cae desde cuatro metros de altura,
aterriza sobre una esquina porque la inclinaste, se balancea hasta quedar sobre
una cara y se para. Pulsa *F5* para ejecutarlo otra vez y volver a mirarlo.
Nada de lo que hay en `onOverlayUpdate` mueve nada. Las dos llamadas a `bodyNew`
lo han hecho todo.
[#_what_just_happened_20]
=== Qué acaba de pasar
[source,lua]
----
bodyNew(floor, BODY_STATIC, SHAPE_BOX, 16, 0.4, 16)
----
`bodyNew` le da un cuerpo a un nodo. Recibe el nodo, qué tipo de cuerpo es, qué
forma debe usar la simulación para él y las medidas de esa forma.
Hay tres tipos, y elegir el correcto es la mayor parte de la habilidad.
* `BODY_STATIC` no se mueve nunca. Se coloca donde está el nodo cuando creas el
cuerpo, y ahí se queda para siempre. Suelos, paredes, el terreno, el armario,
el nivel. Mover su nodo después no mueve el cuerpo, lo cual es una trampa que
vale la pena recordar: estático significa estático.
* `BODY_DYNAMIC` lo mueve la simulación. Cae, rebota, se desliza, recibe
empujones y arrastra a su nodo consigo. Esto es una caja, un barril, una
pelota, un trozo de escombro.
* `BODY_KINEMATIC` va donde *tú* pones su nodo, y aparta de su camino a los
cuerpos dinámicos sin que nada lo empuje nunca a él. Una plataforma móvil, una
pala, una puerta sobre un raíl, el puño de un personaje que lanza un golpe. Es
el puente entre tus cuentas y la simulación: tú lo manejas a él, y él los
maneja a ellos.
La forma no es la malla. Es un sustituto aproximado que usa la simulación porque
puede responder preguntas sobre una caja muchísimo más rápido que sobre
novecientos triángulos. `SHAPE_BOX` recibe un ancho, un alto y un fondo;
`SHAPE_SPHERE` recibe un radio; `SHAPE_CAPSULE` y `SHAPE_CYLINDER` reciben un
radio y una altura. Está `SHAPE_HULL`, que envuelve como un film retráctil la
malla que haya bajo el nodo -- la respuesta habitual para un modelo -- y
`SHAPE_MESH`, los triángulos exactos, que puede usar el decorado estático y no
puede usar un objeto que cae.
Las medidas te toca acertarlas a ti. Nada comprueba que el cuerpo que has
descrito coincida con la malla que has dibujado, y una caja dibujada a `0.8` con
un cuerpo creado a `2` flotará por encima del suelo y tirará cosas a distancia.
Cuando algo se comporta como si tuviera otro tamaño, es que lo tiene.
[source,lua]
----
bodySetMass(crate, 4)
----
La masa de un cuerpo dinámico sale de su forma con la densidad del agua, lo que
hace que las cosas pesen mucho más de lo que esperas: una caja del tamaño de una
puerta pesa unos ciento cuarenta kilos, y nada que le puedas lanzar la moverá.
Fija la masa de todo lo que deba parecer ligero. Cuatro kilos es una caja que
podrías levantar.
La masa importa porque las fuerzas se reparten según ella. El mismo empujón
mueve más lejos una cosa ligera, y un impulso de masa por velocidad es lo que
hace falta para poner algo en movimiento a esa velocidad. Ajusta primero la masa
y después las fuerzas, o acabarás persiguiéndote la cola.
[#_making_things_move]
=== Hacer que las cosas se muevan
Tres llamadas empujan un cuerpo dinámico, y no son intercambiables.
`bodySetVelocity(node, x, y, z)` dice «ahora vas a esta velocidad», y sustituye
lo que estuviera haciendo. Esto es un saque, un lanzamiento, algo que aparece
ya con algo de velocidad.
`bodyApplyImpulse(node, ix, iy, iz)` es un golpe: una patada, una explosión, un
bate que se encuentra con una pelota. Se suma a lo que el cuerpo ya estaba
haciendo. Dale un punto del mundo como tres números más y golpea *ahí* en lugar
de en el centro, así que una caja golpeada cerca de arriba se vuelca en lugar de
deslizarse.
`bodyApplyForce(node, fx, fy, fz)` es un empujón que dura: un cohete, un
ventilador, una cinta transportadora. Una fuerza aplicada durante un fotograma
casi no hace nada, lo que sorprende a quien esperaba un impulso.
La gravedad en sí es `physicsSetGravity(x, y, z)` y vale para todo el mundo. La
luna es `physicsSetGravity(0, -1.62, 0)`, el espacio es todo ceros, y un juego
con la gravedad de lado está a una línea de distancia.
Dos ajustes más completan cómo se siente un objeto.
`bodySetBounce(node, amount)` va de `0`, que se para en seco, a `1`, que vuelve
con todo lo que traía; por defecto es un `0.1` que casi ni se nota.
`bodySetFriction(node, amount)` es `0` para el hielo y `0.5` por defecto. Los
dos se combinan entre las dos cosas que se tocan, así que un suelo helado
necesita el número bajo en el suelo, no en todo lo que camina por encima.
[#_knowing_when_things_touch]
=== Saber cuándo se tocan las cosas
[source,lua]
----
function onCollision(nodeA, nodeB, x, y, z, speed)
if speed > 4 then
knocks = knocks + 1
end
end
----
Ahí, `knocks` es una variable tuya, que espera al principio del archivo con
`local knocks = 0`, y el juguete de más abajo la imprime.
`onCollision` es un callback, como `onOverlayUpdate`: lo escribes tú y el motor
lo llama. Llega una vez por cada contacto nuevo -- dos cuerpos que siguen
tocándose no vuelven a avisar -- con los dos nodos, el punto del mundo donde se
encontraron y lo rápido que se encontraron. La velocidad es lo que permite que
un golpecito y un choque suenen distinto, y es la razón de que la comprobación
de arriba ignore cómo se asienta suavemente una pila.
La otra mitad de la pareja es `onTrigger`. Convierte un cuerpo en activador con
`bodySetTrigger(node, true)` y deja de empujar cosas: se convierte en una zona
que informa de lo que entra y sale de ella a través de
`onTrigger(trigger, other, entered)`. Una línea de meta, una puerta, un foso, un
objeto que recoger. Es el descendiente en 3D de las comprobaciones de colisión
de la lección once, y para cualquier cosa del tipo «¿ha llegado aquí el
jugador?» es la llamada que quieres en lugar de un cuerpo sólido.
[#_the_toy]
=== El juguete
Ahora todo junto. Parte del script de arriba y conviértelo en esto: una bandeja
con cuatro paredes, seis cajas en pirámide y una bola de acero que puedes
apuntar y disparar. Todas las piezas son cosas que ya conoces.
Todo lo que recuerda el código nuevo va al principio del archivo, encima de la
escena:
[source,lua]
----
local crates = {}
local balls = {}
local aimX = 0
local aimLeft = false
local aimRight = false
local knocks = 0
local showShapes = false
----
Con los materiales van dos handles compartidos más: un metal pulido para la
bola y para el marcador de puntería, y una malla de caja para las seis cajas.
[source,lua]
----
local steel = materialNew()
materialSetColor(steel, 200, 205, 215)
materialSetMetallic(steel, 1)
materialSetRoughness(steel, 0.25)
local crateMesh = meshBox(0.8, 0.8, 0.8)
----
Las paredes de la bandeja son cuatro cajas estáticas, creadas en un bucle a
partir de una tabla de posiciones, que es la lección seis haciendo un trabajo
honrado:
[source,lua]
----
local wallMesh = meshBox(16, 1, 0.4)
for _, side in ipairs({ { 0, -8, 0 }, { 0, 8, 0 }, { -8, 0, 90 }, { 8, 0, 90 } }) do
local wall = nodeNew()
nodeSetMesh(wall, wallMesh, stone)
nodeSetPosition(wall, side[1], 0.5, side[2])
nodeSetRotation(wall, 0, side[3], 0)
bodyNew(wall, BODY_STATIC, SHAPE_BOX, 16, 1, 0.4)
end
----
Cada elemento es una X, una Z y un giro alrededor de Y. Dos paredes van de lado
a lado, al fondo y delante; dos están giradas noventa grados para formar los
laterales. Una malla sirve para las cuatro, y el cuerpo se crea después de
colocar y girar el nodo, porque un cuerpo estático toma la posición y la
rotación del nodo en el momento en que se crea.
Las cajas salen de una función, para que seis cajas sean seis líneas:
[source,lua]
----
local function crateAt(x, y)
local crate = nodeNew()
nodeSetMesh(crate, crateMesh, wood)
nodeSetPosition(crate, x, y, -2)
bodyNew(crate, BODY_DYNAMIC, SHAPE_BOX, 0.8, 0.8, 0.8)
bodySetMass(crate, 4)
crates[#crates + 1] = crate
end
----
Guarda cada caja que crea en la tabla `crates`, que es como el callback de
dibujo puede hacerles una pregunta a todas más tarde. Seis llamadas construyen
la pirámide, tres en el suelo, dos encima de esas y una arriba del todo:
[source,lua]
----
crateAt(-0.9, 0.4)
crateAt(0, 0.4)
crateAt(0.9, 0.4)
crateAt(-0.45, 1.25)
crateAt(0.45, 1.25)
crateAt(0, 2.1)
----
Las filas de arriba quedan a un dedo de las de abajo y caen a su sitio en cuanto
empieza el juego. Dejar un hueco pequeño es más fácil que apilar con exactitud,
y es más amable con la simulación que darle un montón que ya está apretado.
Disparar crea una bola nueva cada vez:
[source,lua]
----
local function fire()
local ball = nodeNew()
nodeSetMesh(ball, meshSphere(0.35, 24), steel)
nodeSetPosition(ball, aimX, 1.2, 6)
bodyNew(ball, BODY_DYNAMIC, SHAPE_SPHERE, 0.35)
bodySetMass(ball, 3)
bodySetBounce(ball, 0.3)
bodySetVelocity(ball, 0, 2, -14)
balls[#balls + 1] = ball
if #balls > 5 then
nodeDelete(balls[1])
table.remove(balls, 1)
end
end
----
Fíjate en el orden. El nodo se crea, recibe una forma que dibujar y se
*coloca*, y solo entonces recibe un cuerpo. Un cuerpo dinámico empieza donde
está su nodo, y a partir de ahí el tráfico va en el otro sentido: el cuerpo
mueve el nodo. Fijar tú después la posición de un nodo dinámico es pelearte con
la simulación, y la simulación gana. Cuando haya que mover algo, dale una
velocidad, golpéalo con un impulso o haz lo que hace esto -- crea uno nuevo y
deja ir el viejo.
Dejar ir el viejo es `nodeDelete`, que libera el nodo, todo lo que cuelga de él
y el cuerpo que iba montado en él. Sin esas tres líneas, cada disparo se
quedaría en el mundo para siempre, y unos cientos de bolas después la simulación
estaría trabajando muchísimo en cosas que nadie puede ver. Limpiar forma parte
de usar la física.
Los controles y lo que se muestra en pantalla son el resto:
[source,lua]
----
function onInputPressed(what)
if what == SWITCH_BUTTON1 then
fire()
elseif what == SWITCH_BUTTON3 then
showShapes = not showShapes
if showShapes then
physicsSetDebug(DEBUG_SHAPES + DEBUG_CONTACTS)
else
physicsSetDebug(DEBUG_NONE)
end
elseif what == SWITCH_LEFT then
aimLeft = true
elseif what == SWITCH_RIGHT then
aimRight = true
end
end
----
La barra espaciadora dispara, Mayús activa y desactiva la vista de la física, y
las flechas, mantenidas pulsadas, desplazan el punto de lanzamiento. Una placa
plana de metal sin ningún cuerpo, que se mueve en cada fotograma a `aimX`,
muestra de dónde saldrá la siguiente bola. El script completo, con el callback
de soltar y el resto de la preparación, es `24-physics.singe` en la carpeta
`learn`.
`physicsSetDebug` es la llamada más útil de esta lección. Dibuja lo que la
simulación tiene de verdad -- las formas reales en alámbrico, y una cruz roja
allí donde dos cuerpos se han tocado en este paso -- por encima de tu escena. La
primera vez que una caja flote a un palmo del suelo, o lo atraviese, o tire algo
desde dos metros de distancia, activa esto y la respuesta estará en la
pantalla. Añade `DEBUG_STATIC` para ver también el suelo y las paredes. No
cuesta nada mientras está desactivado.
Por último, la cuenta:
[source,lua]
----
function onOverlayUpdate()
local over = 0
local settled = true
if aimLeft then
aimX = math.max(aimX - 0.08, -5)
end
if aimRight then
aimX = math.min(aimX + 0.08, 5)
end
nodeSetPosition(marker, aimX, 0.05, 6)
for _, crate in ipairs(crates) do
local pitch, _, roll = nodeGetRotation(crate)
if math.abs(pitch) > 30 or math.abs(roll) > 30 then
over = over + 1
end
if not bodyIsResting(crate) then
settled = false
end
end
overlayClear()
overlayPrint(2, 2, "Arrows aim. Space fires. Shift shows the shapes.")
overlayPrint(2, 4, "knocked over " .. over .. " of " .. #crates .. " hard hits " .. knocks)
if settled then
overlayPrint(2, 6, "everything has settled")
end
return OVERLAY_UPDATED
end
----
Una caja cuenta como derribada cuando se inclina más de treinta grados, que es
una regla del juego escrita en una línea de aritmética por encima de lo que
haya hecho la simulación. `nodeGetRotation` da tres ángulos y aquí se quieren el
primero y el tercero, así que el del medio se recoge en una variable llamada
`_`. Ese guion bajo es un nombre normal y corriente sin nada de especial, que se
usa por convención para decir «este lo ignoro».
`bodyIsResting` dice si la simulación ha dormido un cuerpo porque ha dejado de
moverse. Así es como sabes que un tiro ha terminado: ni un temporizador ni una
suposición, sino todas las cajas dormidas. Un juego de bolos puntúa su tirada
justo ahí.
[#_when_not_to_use_it]
=== Cuándo no usarla
La física es la herramienta más tentadora de cualquier motor y la que más fines
de semana hace perder. Hay tres cosas ciertas sobre ella, y ninguna es culpa del
motor.
*Es cara.* Cada cuerpo dinámico es trabajo en cada paso, sesenta veces por
segundo, lo esté mirando alguien o no. Cuarenta cajas está bien. Cuatro mil no,
y en una máquina pequeña cuatrocientas tampoco.
*Es impredecible.* Ejecuta tu juguete dos veces y las cajas caen de forma
distinta. Eso es lo que hace que parezca vivo, y también es la razón de que un
juego no pueda confiar en ella para nada que tenga que salir igual cada vez. Una
puerta que tiene que abrirse, una plataforma que tiene que llegar, un salto que
el jugador tiene que poder hacer: eso es aritmética, o una interpolación, o un
cuerpo cinemático sobre un recorrido que has escrito tú. Nunca dejes algo que el
jugador tiene que hacer en manos de una simulación que puede dejarlo caer detrás
de una caja.
*Es difícil de ajustar.* La masa, la fricción, el rebote, la forma y el tamaño
tiran unos de otros, y «se siente raro» rara vez tiene una sola causa. Cada hora
que pasas consiguiendo que una caja con física se sienta bien es una hora que no
has pasado con el juego.
Así que recurre a ella cuando el lío *es* lo que buscas: cosas que se
derrumban, escombros que vuelan, un montón que se asienta, una pelota que
rebota por una habitación, un vehículo que rueda por un terreno irregular, un
ragdoll que cae a trompicones por una escalera. Nadie puede escribir eso a mano
y nadie puede decirte exactamente qué aspecto debería tener.
No recurras a ella cuando sabes lo que debe pasar. Una bala que vuela recta son
dos líneas de la lección dos. Una plataforma que sube y baja es `math.sin`. Un
jugador que camina y salta como los jugadores esperan quiere un controlador de
personaje (`playerNew`, en la referencia), porque una persona no es una caja y
nunca se comporta como tal. Un enemigo que patrulla quiere un recorrido. Un menú
que entra deslizándose quiere una interpolación.
La regla que mejor funciona: física para las cosas que el jugador mira, y
aritmética para las cosas que el jugador hace.
[#_try_it_24]
=== Pruébalo
. *Cambia el mundo.* Añade `physicsSetGravity(0, -1.62, 0)` cerca del principio
y vuelve a disparar. Luego prueba `0, -30, 0`. Las mismas cajas, otro planeta,
una línea.
. *Cambia la bola.* Haz la bola diez veces más pesada con
`bodySetMass(ball, 30)`, y luego diez veces más ligera con `0.3`, manteniendo
la misma velocidad. Después devuelve la masa a su valor y cambia en su lugar
el `-14`. Dos formas de golpear más fuerte, y no se sienten igual.
. *Haz que las cajas sean de hielo.* Añade `bodySetFriction(crate, 0.02)` en
`crateAt`, y añade una línea que le dé también al suelo una fricción baja.
Mira cómo la pila se niega a mantenerse en pie desde el principio.
. *Equivoca la forma a propósito.* En `crateAt`, cambia las medidas del cuerpo
a `2, 2, 2` mientras la malla se queda en `0.8`. Ejecútalo, mira cómo las
cajas flotan y se empujan unas a otras desde la nada, y luego pulsa Mayús y
mira cómo la vista de depuración lo explica en una sola imagen.
. *Puntúalo como es debido.* Usa `bodyIsResting` en cada caja para darte cuenta
de cuándo ha terminado el tiro, y solo entonces cuenta cuántas han caído y
súmalo a una puntuación. Esa es la mitad de un juego de bolos que lleva la
puntuación.
[#_break_it_on_purpose_23]
=== Rómpelo a propósito
Añade una línea debajo del `bodyNew` del suelo para que el suelo sea pesado, que
es algo razonable de creer que quieres:
[source,lua]
----
bodySetMass(floor, 200)
----
Singe detiene el juego con algo así:
----
32:bodySetMass: Node 1 is not a dynamic body, or the mass is not positive.
----
El primer número es la línea de tu propio archivo, así que el tuyo será
distinto.
El número que va después de `Node` es el handle del nodo, el número que te
devolvió `nodeNew`. La queja es exacta: la masa es cosa de lo que mueve la
simulación, y a un cuerpo estático no lo mueve nada, así que no tiene masa que
fijar. Pedirlo es señal de que el cuerpo es del tipo equivocado para lo que
tenías en mente -- y si de verdad quieres un suelo pesado que se pueda mover, lo
que quieres es un cuerpo dinámico con mucha masa, o uno cinemático que muevas
tú.
El mismo mensaje aparece con una masa de `0`, que la gente escribe queriendo
decir «sin peso». Sin peso no es una masa. Es un mundo sin gravedad, o
`playerSetGravityScale` en un controlador de personaje.
[#_what_you_learned_24]
=== Lo que has aprendido
* Un cuerpo se une a un nodo, y desde ese momento el nodo y el cuerpo son una
sola cosa.
* El estático no se mueve nunca, al dinámico lo mueve la simulación, y al
cinemático lo mueves tú y aparta todo lo demás.
* La forma de colisión es un sustituto de la malla, y eres tú quien responde de
que sus medidas coincidan con lo que has dibujado.
* La masa por defecto es el volumen de la forma con la densidad del agua, así
que fíjala en todo lo que deba parecer ligero.
* `bodySetVelocity` sustituye el movimiento de un cuerpo, `bodyApplyImpulse` es
un golpe y `bodyApplyForce` es un empujón que dura.
* No fijes la posición de un nodo dinámico; bórralo y crea uno nuevo cuando
tengas que hacerlo empezar de nuevo.
* `onCollision` informa de cada contacto nuevo con su punto y su velocidad, y
un cuerpo activador informa de las llegadas y las salidas a través de
`onTrigger`.
* `bodyIsResting` te dice cuándo se ha parado todo, que suele ser cuando ha
terminado un turno.
* `physicsSetDebug` dibuja lo que ve la propia simulación, y responde a la
mayoría de las preguntas sobre la física más rápido que leer código.
* La física es cara, irrepetible y delicada de ajustar. Úsala para el lío, no
para las cosas de las que depende el juego.
[#_next_time_24]
=== La próxima vez
Las cajas que caen son un tipo de espectáculo. El otro tipo está hecho de mil
cosas diminutas que viven medio segundo: chispas, humo, fuego, lluvia y polvo.
La próxima lección trata de los emisores, y termina con una explosión. Esa es la
lección veinticinco.