-
Notifications
You must be signed in to change notification settings - Fork 0
Box2D y las galletas de Frankenstein
- Bienvenidos al laboratorio de Frankenstein del monstruo de las galletas
Si el monstruo de las galletas tuviera un laboratorio para jugar a hacer monstruos de Frankenstein, eso sería Box2D.
Eeeeeeeeeeeeeeeeeh? 😱
Vale, empecemos por el principio. 😂
Box2D es un motor de física para nuestro proyecto de libGDX. Qué significa esto? Que tú le das los objetos de tu juego, y él ya se las apañará para mirar si chocan o rebotan o toman el té.
Si tuviéramos que calcular estas cosas nosotros mismos probablemente no tendríamos ni idea, pero tranquilos 🙇 , alguien sí que tuvo idea y nos preparó un cacharro que lo hace él solito! 🙊
Ese cacharro se llama Box2D.
Gracias, señor que sí supo hacerlo! 😄 😄 😄
Nuestro laboratorio antes de poder funcionar va a necesitar un par de cosas. La primera, que le demos a la corriente.
Vamos a colocar un interruptor para dar corriente al laboratorio (uno de aquellos de palanca, ya sabes, a la vieja usanza). 😁
Lo haremos así:
Box2D.init(); //Init de inicializar o de iniciar o de lo que quieras. Que empiece, vamos.
Queremos que el interruptor esté nada más entrar a nuestro juego, porque vamos a querer tener el laboratorio encendido desde el principio, así que buscaremos un lugar al principio del código y lo pondremos ahí.
Yo lo tengo en
Juego.java
, que es como se llama el primer archivo que se abre en mi juego, dentro del métodocreate()
, que es lo primero que va a leer cuando empiece.
Ya que vamos a jugar a Frankenstein, hagámoslo a lo grande. Vamos a darle a nuestro laboratorio todo un mundo en el que construir! 😄
O varios... 🙊
Un mundo es un lugar donde hay cuerpos y donde pasan cosas. Cuando creemos cuerpos, los crearemos dentro de nuestro mundo, y cuando pidamos al mundo que avance en el tiempo, nuestros cuerpos se moverán.
Y cuando digo cuerpo no quiero decir sólo una persona o un mamut, quiero decir también una pelota, una caja, una pared o una estrella deforme con los pinchos amorfos. 😛
Nuestro mundo va a querer saber dos cosas:
- Hacia dónde va la gravedad? 🙋
- Puedo dejar que las cosas se duerman? 🙋
La primera es un vector en 2D (Vector2
), que en la tierra valdría (0,-10)
, pero que vaya, lo que te apetezca.
La segunda es un boleano (boolean
), y en general es algo bueno, así que si no tienes un buen motivo para ponerlo false
, mejor que lo pongas true
.
las cosas quieren dormir, tío... 💁
Cómo le decimos al mundo las dos cosas que quiere saber? Pues en su inicialización, dentro del archivo de la pantalla (Pantalla.java
, o lo que sea):
World mundo = new World(new Vector2(0,-10),true); //A colocar junto a las declaraciones de variables y eso
O, si nos va más inicializar las cosas en el constructor:
//junto con las declaraciones de variables:
World mundo;
//Y dentro del constructor:
mundo = new World(new Vector2(0,-10),true);
Yo lo tengo hecho de la segunda manera, pero probablemente la primera hace pensar menos al ordenador. Ni idea. 😅 😂
Nuestro mundo es un mundo caprichoso, y no va a dejar que pase el tiempo a menos que se lo pidamos a propósito. Iremos al método render()
de nuestra pantalla, y le diremos al mundo: "Avanza!" (step()
).
mundo.step(1/45f,6,2); //estos números son valores recomendados (1/45f o 1/300f)
El primer argumento es la cantidad de tiempo a simular, y los otros dos cosas sobre iteraciones que nos dan bastante igual (algo sobre cuánto poder de computación queremos que gaste, supongo).
Esta manera sencilla de hacer avanzar el mundo funciona, pero no va a ser muy fiel a la realidad. A poco que queramos un sistema realista tendremos que usar el método "avanzado", que añadiré al final para no volverte loco con tanta información. Si tienes curiosidad o lo necesitas puedes saltar allí a mirarlo. Sí, te dejo. 😂
Los cuerpos de Box2D son como un monstruo de Frankenstein, y se hacen igual: Cogemos un cacho de carne y un molde de galletas. Recortamos. Cogemos otro cacho de carne y otro molde de galletas. Recortamos. Y luego vamos pegando los cachos.
Taraaaaaaaaaaa 😎
Al final tenemos una cosa hecha de muchos cachos de materiales distintos, todos pegados entre ellos.
Pero vayamos paso a paso...
Si queremos construir un cuerpo primero tenemos que saber dos cosas:
- Qué tipo de cuerpo quiero? Se va a mover? Podré empujarlo?
- Dónde voy a ponerle la chincheta? 📌
La chincheta es importante porque nos servirá de GPS (cuando queramos saber dónde está nuestro monstruo de Frankenstein, nos dirán dónde está la chincheta). Además, construiremos al monstruo alrededor de la chincheta, y si en algún momento queremos moverlo a propósito, lo moveremos agarrándolo por la chincheta.
La chincheta tiene el poder! 📌💪
En cuanto a los tipos de cuerpo, tenemos tres para elegir. De más quieto a menos: estático, cinemático y dinámico.
Los cuerpos dinámicos (BodyType.DynamicBody
) se pueden mover, y otros cuerpos pueden empujarlos. Aquí entran pelotas, personas, mamuts, bicicletas, ... casi todo, en realidad.
Los cuerpos cinemáticos (BodyType.KinematicBody
) se pueden mover, pero no les puede empujar nadie.
Son todopoderosos!! 💪
Aquí entran las plataformas que se mueven y otras cosas así. Cualquier parte del escenario que se mueva probablemente será cinemática.
Los cuerpos estáticos son todo lo que no se puede mover: el escenario, vaya.
Nota: Todos los cuerpos serán estáticos si no decimos nada, así que para los otros dos tipos de cuerpos hay que avisar.
Como dejar que todo el mundo haga monstruos de Frankenstein sería peligroso, las chinchetas las guarda el gobierno, y habrá que pedirlas enviando una carta de solicitud al ministro feo de asuntos Frankensteiniosos.
En la solicitud (BodyDef
), apuntaremos dónde queremos hacer el monstruo, y de qué tipo va a ser.
Veamos cómo se pide:
/** En declaración de variables **/
BodyDef solicitud;
/** en el constructor o donde quieras **/
solicitud = new BodyDef(); //empiezo la carta al señor ministro feo
solicitud.type = BodyType.DynamicBody; //aviso a las autoridades de que querré que el monstruo se mueva
solicitud.position.set(1,2); //apunto en la carta dónde voy a querer poner la chincheta
Y una vez tenemos la solicitud preparada, la enviamos al ministerio. Entonces viene el señor ministro feo y pone la chincheta.
<( Señor, aquí tiene su chincheta, puede empezar su monstruo dinámico.)
La solicitud se envia así:
//esto crea un cuerpo que voy a llamar frankie, acorde con la solicitud que he escrito:
frankie = mundo.createBody(solicitud);
Nota: Sí, hay que declarar antes el cuerpo. Arriba, con las variables:
Body frankie;
Ahora ya podemos empezar a construir 😄
Ahora que ya tenemos el cuerpo registrado en la oficina de patentes 😂, y la chincheta en su sitio, vamos a elegir unos cuantos moldes (Shape
) para poder recortar de la masa las formas que nos apetezcan.
Será como hacer galletas. Galletas de Frankenstein. 😱
Un círculo es un molde para hacer una pelota. Qué listo soy. 😁 😂
Los círculos se hacen así:
/**En declaración de variables**/
CircleShape molde_redondo;
/**en el constructor, o donde quieras**/
molde_redondo = new CircleShape(); //hago un nuevo molde redondo.
molde_redondo.setRadius(0.3f); //elijo la medida del molde redondo.
Box2D trabaja con las medidas en metros, así que 0.3f debería equivaler a 30cm.
Como puedo cambiar el radio sobre la marcha, si quiero hacer varias galletas de medidas distintas puedo recortar la primera, hacer
setRadius()
, recortar la segunda, hacersetRadius()
, ...
Esto de las medidas es importante, porque no es lo mismo ver caer una bola de hierro de 300 metros desde 10.000 metros de altitud que ver caer una bola de hierro de 30 centímetros desde 10 metros de altitud.
Con la misma gravedad, si miramos un paisaje mucho más grande, parecerá que todo se mueve más lento, porque las cosas acceleran igual de rápido, pero tienen más cacho a recorrer.
Para hacer cajas usaremos un molde que hace polígonos (PolygonShape
), pidiéndole que haga un rectángulo.
Aquí va:
/** declaración, arriba **/
PolygonShape molde_caja;
/** donde sea **/
molde_caja = new PolygonShape(); //hago un nuevo molde poligonal
molde_caja.setAsBox(0.5f,0.2f); //Pido una caja: los números son la mitad del ancho y la mitad del alto
Si quisiéramos la caja torcida, podemos añadir el centro (Vector2
) y el ángulo cuando pedimos .setAsBox()
. Tal que así:
molde_caja.setAsBox(0.5f,0.2f,new Vector2(1,2),3.14f); //creo que el ángulo va en radianes, pero no lo he probado
Si en lugar de una caja queremos un polígono amorfo chungo, podemos pasarle una lista de vértices, en plan x1,y1,x2,y2,x3,y3, cambiando setAsBox()
por set()
. De este plan:
molde_chungo.set(new float[]{0,0,5,0,6,2,4,6,1,0}); //vértices en (0,0) (5,0) (6,2) (4,6) y (1,0)
Ah, pero, qué pasa si yo quiero un recinto cerrado? Qué pasa si quiero estar dentro de mi polígono chungo?
Entonces, amigo mío, necesitas una cadena.
Las cadenas (ChainShape
) son una serie de línias enganchadas entre ellas. Sirven para hacer recipientes, y todo lo que sea que quieras hacer que no esté relleno por ningún lado.
Si queremos hacer un recinto cerrado, haremos la cadena y le peidremos que haga un bucle, así:
/** declarando, arriba **/
ChainShape molde_hueco;
/** donde sea **/
molde_hueco = new ChainShape(); //hago un molde para recortar cadenas
molde_hueco.createLoop(new float[]{0,0,5,0,6,2,4,6,1,0}); //el polígono chungo de antes, pero hueco.
Si, en cambio, queremos dejar el principio y el final abiertos, cambiamos createLoop()
(crear bucle) por createChain()
(crear cadena). Tal que así:
//Los vértices (0,0) y (1,0) no estarán enganchados entre ellos.
molde_cadena.createChain(new float[]{0,0,5,0,6,2,4,6,1,0});
Vale, tenemos nuestra chincheta preparada, y hemos comprado los moldes... Vamos a hacer galletas!! 😄 😄 😄
Para hacer galletas seguiremos dos sencillos pasos:
- Preparar la receta.
- Cocinar.
Quééééééé fáááááácil es. 😁
La receta (FixtureDef
) la escribiremos más o menos como hemos escrito la solicitud al ministro feo.
Qué cosas quiere saber el cocinero?
- Qué molde tengo que usar? 🙋
- Cuánto quieres que pese la galleta? 🙋
- Cuánto quieres que resbale la galleta? 🙋
- Cuánto quieres que rebote la galleta? 🙋
El molde lo elegiremos de entre los que hemos comprado antes, y las otras tres cosas (sobre el tipo de masa que va a llevar la galleta), son valores que generalmente van entre 0f
y 1f
, y que representan la densidad, la fricción y la restitución.
La densidad (density
) es el modo que tenemos de darle un peso a nuestra galleta sin que dependa de si la galleta es grande o pequeña. Una galleta de plomo tendrá una densidad de 1f
, mientras que una galleta globo la tendrá de 0f
.
La fricción (friction
) será lo que frenará los objetos cuando resbalen con otras cosas, y lo que mantendrá a las cosas quietas si las ponemos sobre una rampa. Una galleta con ganchos de hierro en los zapatos tendrá una fricción de 1f
, mientras que un bloque de hielo tendrá 0f
.
La restitución (restitution
) representa la rebotosidad de las galletas. Una bola de papel mojado tendrá una restitución de 0f
, mientras que la pelota elástica perfecta tendría 1f
. Como las pelotas no son perfectas, un número interesante es 0.6f
.
Además, si queremos que la galleta rebote más fuerte de como la hemos tirado (porque somos unos pirados macabras o algo) podemos poner números mayores que 1f
, como 1.2f
.
No os lo recomiendo para un juego serio, pero para probar qué pasa es gracioso. 😁
Las cuatro cosas que pide el cocinero se escriben en la receta tal que así:
/** declaración, arriba **/
FixtureDef receta;
/** donde sea **/
receta = new FixtureDef(); //empiezo la receta
receta.shape = molde_redondo; //apunto con qué molde hay que cortar
receta.density = 0.5f; //apunto cuánto tiene que pesar
receta.friction = 0.4f; //apunto cuánto tiene que resbalar (rozar, en realidad)
receta.restitution = 0.6f; //apunto cuánto tiene que rebotar
Y cuando tengamos la receta preparada, la mandamos a cocinar así:
galleta_1 = frankie.createFixture(receta);
Y esto ya engancha la galleta en el cuerpo de frankie. Porque no, no se las va a comer, el monstruo va a estar hecho de galletas.
... 😕 Yo qué sé. 😂 Podéis considerarlos cachos en lugar de galletas si eso os convence más. Yo las llamo galletas porque me apetece.
Aquí, si queremos añadir más cachos a frankie podemos seguir cocinando:
receta.shape = molde_caja; //cambio el molde de la receta
galleta_2 = frankie.createFixture(receta); //añado un cacho cuadrado a frankie
//y puedo añadir cachos hechos con recetas distintas, que pesen distinto y tal. Lo que te apetezca.
galleta_3 = frankie.createFixture(receta_2); //habría que escribir la receta_2 antes, evidentemente.
Y ya está. Ya hemos creado nuestro monstruo de Frankenstein hecho con cachos de galletas.
Hemos visto que escribiendo una sola receta (FixtureDef
) podemos cocinar tantas galletas como queramos (Fixture
), e incluso podemos cambiar el molde (.shape
) y aprovechar el resto de la receta, para hacer galletas con formas distintas pero con las mismas propiedades.
Del mismo modo, podemos aprovechar la solicitud (BodyDef
) que hemos escrito para frankie y enviarla más veces para hacer más cuerpos (Body
) en el mismo sitio y del mismo tipo. O podemos cambiar la posición de la chincheta (.position.set(x,y)
) 📌, para hacer cuerpos del mismo tipo en sitios distintos.
Y luego, la otra cosa que me gusta de haberlo contado todo así, es que pensando en galletas es evidente que uno no puede aprovechar una galleta para ponerla en dos cuerpos. Aprovechar una receta o una solicitud y enviarla dos veces tiene sentido, pero poner la misma galleta en dos cuerpos no. Las cosas no están en dos sitios a la vez.
En realidad el programa te deja, pero luego cuando intentas borrar cuerpos o galletas o lo que sea, empiezan a pasar cosas paranormales. Lo digo por experiencia... 🙊
Así que eso... NO APROVECHÉIS LAS GALLETAS.
Ah, una cosa más. Al cocinero le gusta tener la cocina limpia, y le molesta mogollón tener por ahí tirados moldes que no va a usar, así que cuando sepas que puede olvidarse de ellos tienes que avisarle:
molde_redondo.dispose();
molde_caja.dispose();
💬 Chachi, tío. Ahora los guardo. 👍
- La abuela chismosa que todo lo sabe.
Box2DDebugRenderer
- Los boludos y dibujar a frankie.
.userData
- Destruyamos monstruos.
Lugares donde puede que las encontréis mientras tanto:
- Mi ejemplo de Box2D en libGDX
- La wiki de libGDX sobre Box2D
- El lugar donde yo entendí cómo destruir cuerpos, creo.
Cuando hablaba de hacer avanzar al mundo he dejado una cosa colgada. Un método "avanzado" para que el avance del mundo en tu juego se parezca al de la vida real. Funciona así:
En la parte del render donde teníamos world.step()
, vamos a crear un método inventado que voy a llamar miMundoAvanza()
, a quien enviaremos la variable delta
.
delta
es un número que se calcula solo, y que dice cuántos segundos han pasado desde el último fotograma, para que sepas cómo de rápido está corriendo la aplicación.
/** en el Render() **/
miMundoAvanza(delta); //este delta es el que llega desde el "render()"
Esto nos servirá para no tener un montón de cosas escritas en el render. Estamos sacando información a fuera para que sea fácil de leer, eso es todo.
Entonces, miMundoAvanza()
será lo siguiente (keep calm, os lo explico detallado abajo):
/**en declaración de variables**/
float acumulador;
/**fuera de todo (aunque en Pantalla.java)**/
private void miMundoAvanza(float delta){
float tiempo_fotograma = Math.min(delta,0.25f);
acumulador += tiempo_fotograma;
while(acumulador>=1/45f){
mundo.step(1/45,6,2);
acumulador -= 1/45f;
}
}
La variable tiempo_fotograma será la que decida cuánto rato queremos simular, así que la primera línia,
float tiempo_fotograma = Math.min(delta,0.25f);
lo que hace es evitar que simulemos siglos si hemos tardado siglos en cargar el fotograma (como máximo, simularemos 0.25 segundos. Si corre en un móvil rápido serán menos, tenemos 0.25 de tope por si acaso).
Luego, el bucle while
lo que hace es separar tiempo_fotograma en cachos de 1/45 segundos, para simularlos de uno en uno: Mientras hayamos acumulado más de 1/45, simula 1/45 y quita 1/45 de lo que teníamos acumulado. Cuando ya queda menos de 1/45, pasamos al siguiente fotograma.
Esto es mejor que lo que hacíamos antes, porque antes sólo simulábamos 1/45 en cada fotograma, y si los fotogramas duraban más de 1/45, el juego iba lento (no avanzaba suficiente en cada fotograma).