[#_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.