Parte anteriorTabla de contenidosiguiente parte
En el parte del tutorial anterior buscamos crear la simulación física más simple posible en LibGDX y Box2D, un solo objeto afectado por la gravedad. En este tutorial vamos a llevar las cosas un paso más allá y aplicaremos fuerza, impulsos y torsión (y no gravedad) a nuestro cuerpo físico. Esta parte consistirá en un solo ejemplo de código grande.
Hablando de eso, ¡aquí está!
package com.gamefromscratch; import com.badlogic.gdx.ApplicationAdapter; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Sprite; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.physics.box2d.*; public class Physics2 extends ApplicationAdapter implements InputProcessor { SpriteBatch batch; Sprite sprite; Texture img; World world; Body body; Box2DDebugRenderer debugRenderer; Matrix4 debugMatrix; OrthographicCamera camera; float torque = 0.0f; boolean drawSprite = true; final float PIXELS_TO_METERS = 100f; @Override public void create() { batch = new SpriteBatch(); img = new Texture("badlogic.jpg"); sprite = new Sprite(img); sprite.setPosition(-sprite.getWidth()/2,-sprite.getHeight()/2); world = new World(new Vector2(0, 0f),true); BodyDef bodyDef = new BodyDef(); bodyDef.type = BodyDef.BodyType.DynamicBody; bodyDef.position.set((sprite.getX() + sprite.getWidth()/2) / PIXELS_TO_METERS, (sprite.getY() + sprite.getHeight()/2) / PIXELS_TO_METERS); body = world.createBody(bodyDef); PolygonShape shape = new PolygonShape(); shape.setAsBox(sprite.getWidth()/2 / PIXELS_TO_METERS, sprite.getHeight() /2 / PIXELS_TO_METERS); FixtureDef fixtureDef = new FixtureDef(); fixtureDef.shape = shape; fixtureDef.density = 0.1f; body.createFixture(fixtureDef); shape.dispose(); Gdx.input.setInputProcessor(this); // Create a Box2DDebugRenderer, this allows us to see the physics simulation controlling the scene debugRenderer = new Box2DDebugRenderer(); camera = new OrthographicCamera(Gdx.graphics.getWidth(),Gdx.graphics. getHeight()); } private float elapsed = 0; @Override public void render() { camera.update(); // Step the physics simulation forward at a rate of 60hz world.step(1f/60f, 6, 2); // Apply torque to the physics body. At start this is 0 and will do nothing. Controlled with [] keys // Torque is applied per frame instead of just once body.applyTorque(torque,true); // Set the sprite's position from the updated physics body location sprite.setPosition((body.getPosition().x * PIXELS_TO_METERS) - sprite. getWidth()/2 , (body.getPosition().y * PIXELS_TO_METERS) -sprite.getHeight()/2 ) ; // Ditto for rotation sprite.setRotation((float)Math.toDegrees(body.getAngle())); Gdx.gl.glClearColor(1, 1, 1, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); batch.setProjectionMatrix(camera.combined); // Scale down the sprite batches projection matrix to box2D size debugMatrix = batch.getProjectionMatrix().cpy().scale(PIXELS_TO_METERS, PIXELS_TO_METERS, 0); batch.begin(); if(drawSprite) batch.draw(sprite, sprite.getX(), sprite.getY(),sprite.getOriginX(), sprite.getOriginY(), sprite.getWidth(),sprite.getHeight(),sprite.getScaleX(),sprite. getScaleY(),sprite.getRotation()); batch.end(); // Now render the physics world using our scaled down matrix // Note, this is strictly optional and is, as the name suggests, just for debugging purposes debugRenderer.render(world, debugMatrix); } @Override public void dispose() { img.dispose(); world.dispose(); } @Override public boolean keyDown(int keycode) { return false; } @Override public boolean keyUp(int keycode) { // On right or left arrow set the velocity at a fixed rate in that direction if(keycode == Input.Keys.RIGHT) body.setLinearVelocity(1f, 0f); if(keycode == Input.Keys.LEFT) body.setLinearVelocity(-1f,0f); if(keycode == Input.Keys.UP) body.applyForceToCenter(0f,10f,true); if(keycode == Input.Keys.DOWN) body.applyForceToCenter(0f, -10f, true); // On brackets ( [ ] ) apply torque, either clock or counterclockwise if(keycode == Input.Keys.RIGHT_BRACKET) torque += 0.1f; if(keycode == Input.Keys.LEFT_BRACKET) torque -= 0.1f; // Remove the torque using backslash / if(keycode == Input.Keys.BACKSLASH) torque = 0.0f; // If user hits spacebar, reset everything back to normal if(keycode == Input.Keys.SPACE) { body.setLinearVelocity(0f, 0f); body.setAngularVelocity(0f); torque = 0f; sprite.setPosition(0f,0f); body.setTransform(0f,0f,0f); } // The ESC key toggles the visibility of the sprite allow user to see physics debug info if(keycode == Input.Keys.ESCAPE) drawSprite = !drawSprite; return true; } @Override public boolean keyTyped(char character) { return false; } // On touch we apply force from the direction of the users touch. // This could result in the object "spinning" @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { body.applyForce(1f,1f,screenX,screenY,true); //body.applyTorque(0.4f,true); return true; } @Override public boolean touchUp(int screenX, int screenY, int pointer, int button) { return false; } @Override public boolean touchDragged(int screenX, int screenY, int pointer) { return false; } @Override public boolean mouseMoved(int screenX, int screenY) { return false; } @Override public boolean scrolled(int amount) { return false; } }
Aquí está ejecutándose en su navegador. Es posible que deba hacer clic en la aplicación para enfocarla.
¡IFRAMES no funciona para ti, lo siento, no hay sopa para ti!
Puedes controlarlo usando los siguientes controles:
- Flecha izquierda y derecha: Aplicar impulso a lo largo del eje X
- Flecha arriba y abajo: aplique fuerza a lo largo del eje Y.
- [ and ]: Aplicar par
- : Ajuste el par a 0
- BARRA ESPACIADORA o ‘2’: Restablecer todo
- Haga clic, aplique fuerza desde el punto del clic
- ESC o ‘1’ activa y desactiva la visualización del gráfico de sprite
EDITAR: Bien, varios combos de teclado no funcionan correctamente en un iframe. Para interactuar completamente con la aplicación anterior haga clic aquí para abrirlo en su propia ventana:
Puede notar que aplicar fuerza hará que su objeto no solo se mueva, sino que también gire, según el ángulo en el que haga clic. También puede compensar esta fuerza haciendo clic en el lado opuesto reflejado. También observe que la aplicación de un impulso mueve el objeto a una velocidad constante, y la aplicación del impulso opuesto hará que el objeto se detenga. Sin embargo, golpear a la izquierda o a la derecha varias veces no aumentará la velocidad. Sin embargo, aplicar fuerza, golpeando hacia arriba o hacia abajo, hará que se vuelva más rápido cada vez que agregue fuerza. Esta es una de las principales diferencias entre impulso y fuerza. El torque funciona de manera muy similar, para evitar que gire, debe aplicar la cantidad adecuada de torque desde la dirección opuesta. Incluso establecer el par en 0 tendrá poco efecto debido al impulso.
Ahora echemos un vistazo a algunos conceptos clave aquí. Lo primero que debes notar es:
final float PIXELS_TO_METERS = 100f;
Bueno, ¿recuerdas cuando dije en la última parte que las unidades que usas realmente no importan mientras seas consistente? Bueno, reclasifiquemos eso como «algo cierto». O, completamente cierto, ¡pero no realmente! ¿Despejado ahora?
La verdad es que Box2d está «sintonizado» para funcionar en Unidades MKS, que para los lectores no canadienses/europeos se traduce en metros, kilogramos y segundos. No me malinterpretes, aún puedes trabajar en libras, millas o parsecs, pero vas a pelear un poco con Box2D. Además, esta propaganda de la documentación de Box2D también es bastante importante:
Box2D está ajustado para unidades MKS. Mantenga el tamaño de los objetos en movimiento aproximadamente entre 0,1 y 10 metros. Deberá usar algún sistema de escala cuando represente su entorno y actores. El banco de pruebas Box2D hace esto mediante el uso de una transformación de ventana gráfica OpenGL. NO USE PÍXELES.
Entonces, ¿qué usé en el ejemplo anterior? Sí… píxeles. Sin embargo, en realidad, cuando se trata solo de la gravedad, realmente no importa tanto. Una vez que la velocidad terminal se activa, todas las cosas caen al mismo ritmo de todos modos.
Pero… ahora que vamos a trabajar con fuerzas y demás, de repente se vuelve muy importante no usar píxeles, y déjame explicarte rápidamente por qué. Considere cuando creamos nuestro objeto físico, definimos su forma y densidad, así:
shape.setAsBox(sprite.getWidth()/2, sprite.getHeight()/2); FixtureDef fixtureDef = new FixtureDef(); fixtureDef.shape = shape; fixtureDef.density = 1f;
Nuestro sprite es una imagen de 256×256 de tamaño. Nuestra densidad, que por cierto es kg/ms2 por defecto es 1. Como resultado directo, en nuestra simulación o sprite tiene una masa por defecto de… 65.536 kg. Hmmm, ¡eso es un poco pesado y requerirá mucha fuerza para moverlo! Si está probando box2d y descubre que la fuerza no funciona como esperaba, eche un vistazo a sus unidades, ese es el culpable más probable. Una solución bastante fácil es usar un conjunto de coordenadas en Box2D y traducir de píxeles a metros y viceversa. Para eso usamos:
Cuando traduzca a coordenadas de caja, divida por este valor, cuando vuelva a traducir, multiplique por él. Básicamente, lo que estamos diciendo es que 1 metro en box2d representa 100 píxeles en nuestro juego. Entonces, para nuestra imagen de 256 × 256, en el recuadro mide aproximadamente 2,5 mx 2,5 m en lugar de 250 × 250. Sin embargo, notará que esta no es la única traducción que hacemos:
Para box2d:
bodyDef.position.set((sprite.getX() + sprite.getWidth()/2) / PIXELS_TO_METERS, (sprite.getY() + sprite.getHeight()/2) / PIXELS_TO_METERS);
De box2d:
sprite.setPosition((body.getPosition().x * PIXELS_TO_METERS) - sprite. getWidth()/2 , (body.getPosition().y * PIXELS_TO_METERS) -sprite.getHeight()/2 ) ;
Hay otro cálculo allí, en cada forma estamos sumando o restando la mitad del ancho y la altura de los sprites. Esto se debe a que box2d y LibGDX usan dos sistemas de coordenadas diferentes. En LibGDX, el origen del sprite es la esquina inferior izquierda, mientras que en box2d es el medio. Puede estar pensando, hey, solo estableceré el origen. Eso no te servirá de nada en realidad, ya que en LibGDX esto solo se usa para escalar y rotar. Por lo tanto, cuando se mueva entre los dos sistemas de coordenadas, tenga en cuenta los diferentes orígenes.
De lo contrario, el código es bastante sencillo. Al jugar con la aplicación, la diferencia entre fuerza e impulso es fácil de ver. Puedes pensar en Force como la acción de empujar a alguien. Requerirá un valor más alto ya que tiene que superar su inercia, sin embargo, empujar a alguien nuevamente hará que acelere cada vez más rápido. El impulso, por otro lado, se puede considerar como la aceleración en un automóvil. Un automóvil con un impulso de 50 km, se mueve constantemente a una velocidad de 50 km. La única forma de cambiar las velocidades sería cambiando el impulso, los valores no serían acumulativos. Sin embargo, si otro automóvil lo chocó por detrás (¡FUERZA!), ¡Ciertamente puede ver un aumento en la velocidad! 😉
El par, por otro lado, es una medida de la fuerza de rotación. A diferencia de la fuerza y el impulso, en Box2D, el par debe aplicarse en cada fotograma. Deje de aplicar torque a cada cuadro y las cosas se detendrán, um, torqueing. Finalmente, en términos de movimiento, el manipulador táctil aplica fuerza desde una dirección determinada. Cuando el usuario presiona hacia arriba o hacia abajo, la fuerza se aplica por igual al centro del objeto. Sin embargo, cuando el usuario toca o hace clic en la pantalla, la fuerza se aplica desde la dirección de contacto. Piensa como en la vida real, si empujas un vaso de cerveza en su punto central, se deslizará por la mesa. Sin embargo, empújelo en la parte superior y probablemente se incline. Tiene la opción de desactivar la rotación de cuerpos si es necesario.
También puede establecer la dirección de los valores, como hice cuando el usuario presiona la barra espaciadora. Configuro la posición, el par, la rotación y la velocidad del objeto de nuevo a cero. Sin embargo, este tipo de cosas REALMENTE jode con Box2D, así que si no es necesario, no manipule los valores directamente, en su lugar, trabaje a través de fuerzas. En una simulación simple como esta, no es gran cosa.
Finalmente, puede notar que habilité Box2DDebugRenderer. En el ejemplo en ejecución, presione la tecla ESC (es posible que deba hacer clic para enfocar el teclado) y verá un rectángulo en lugar del sprite. Esta es una representación interna del cuerpo físico en box2d. Al depurar problemas de física, este renderizador puede ser invaluable. No obstante, tuvimos que modificar su matriz de proyección para que se corresponda con nuestro factor de escala de 100 píxeles por metro.
Ahora que tenemos las cosas en movimiento, en la siguiente parte veremos qué sucede cuando chocan.