Previous PartTable Of ContentsNext Part
In the previous tutorial part we look at creating about the simplest physics simulation possible in LibGDX and Box2D, a single object affected by gravity. In this tutorial we are going to take things one step further and apply force, impulses and torque ( and no gravity ) to our physics body. This part is going to consist of a single large code example.
Speaking of which, here it is!
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; } }
Here it is running in your browser. You may have to click the application to give it focus.
IFRAMES no work for you, sorry, no soup for you!
You can control it using the following controls:
- Left and Right Arrow: Apply impulse along X axis
- Up and Down Arrow: Apply force along Y axis.
- [ and ]: Apply torque
- : Set torque to 0
- SPACEBAR or ‘2’: Reset all
- Click, apply force from point of click
- ESC or ‘1’ toggles display of the sprite graphic on and off
EDIT: OK, several keyboard combo’s do not work correctly in an iframe. To fully interact with the above application click here to open it in it’s own window:
You may notice applying force will cause your object to not just move, but also rotate, depending on the angle you click. You can also offset this force by clicking in the mirrored opposite side. Also notice that applying impulse moves the object along at a constant rate, and applying the opposite impulse will bring the object to a standstill. Hitting left or right multiple times however will not increase the speed. However, applying force, by hitting up or down will cause it to get faster for each time you add force. This is one of the major differences between impulse and force. Torque works very similar, to stop it from spinning you need to apply the appropriate amount of torque from the opposite direction. Even setting torque to 0 will have little effect due to momentum.
Now let’s take a look at a few key concepts here. First thing you should notice is:
final float PIXELS_TO_METERS = 100f;
Well, remember when I said in the last part that the units you use don’t really matter as long as you are consistent? Well, let’s reclassify that as “kinda true”. Or, completely true, but not really! Clear now?
Truth of the matter is, Box2d is “tuned” to work in MKS units, which to you non-Canadian/European readers translates to Meters, Kilograms and Seconds. Don’t get me wrong, you can still work in lbs, miles or parsecs, but you are going to fight with Box2D a bit. Additionally, this blurb from Box2D documentation is also pretty important:
Box2D is tuned for MKS units. Keep the size of moving objects roughly between 0.1 and 10 meters. You’ll need to use some scaling system when you render your environment and actors. The Box2D testbed does this by using an OpenGL viewport transform. DO NOT USE PIXELS.
So, what did I use in the prior example? Yeah… pixels. In reality though, when dealing with gravity only it really doesn’t matter all that much. Once terminal velocity kicks in, all things fall at the same rate anyways.
But… now that we are going to working with forces and such, suddenly it becomes very important not to use pixels, and let me quickly explain why. Consider when we create our physics object, we define it’s shape and density, like so:
shape.setAsBox(sprite.getWidth()/2, sprite.getHeight()/2); FixtureDef fixtureDef = new FixtureDef(); fixtureDef.shape = shape; fixtureDef.density = 1f;
Our sprite is an image 256×256 in size. Our density, which by the way is by default kg/ms2 is 1. As a direct result, in our simulation or sprite has a default mass of… 65,536kg. Hmmm, that’s kind of heavy and is going to require a hell of a lot of force to move! If you are trying out box2d and you are finding that force isn’t working as you’d expect, take a look at your units, that’s the most likely culprit. A rather easy work around is to use one set of coordinates in Box2D and translate to and from pixels to meters and back again. For that we use:
When translating to box coordinates, divide by this value, when translating back, multiply by it. Basically what we are saying is 1 meter in box2d represents 100 pixels in our game. So for our 256×256 image, in box it is roughly 2.5m x 2.5m instead of 250×250! You will notice however that this isn’t the only translation we do:
To box2d:
bodyDef.position.set((sprite.getX() + sprite.getWidth()/2) / PIXELS_TO_METERS, (sprite.getY() + sprite.getHeight()/2) / PIXELS_TO_METERS);
From box2d:
sprite.setPosition((body.getPosition().x * PIXELS_TO_METERS) - sprite. getWidth()/2 , (body.getPosition().y * PIXELS_TO_METERS) -sprite.getHeight()/2 ) ;
There is another calculation in there, each way we are either adding or subtracting half the sprites width and height. This is due to box2d and LibGDX using two different coordinate systems. In LibGDX, the sprite’s origin is it’s bottom left corner, while in box2d, it’s the middle. You may be thinking, hey, I’ll just set the origin. That wont do you any good actually, as in LibGDX this is only used for scaling and rotation. So, when moving between the two coordinate systems be mindful of the different origins.
Otherwise the code is fairly straight forward. From playing around with the application the difference between force and impulse is easy to see. You can think of Force like the action of pushing someone. It will require a higher value as you have to overcome their inertia, however pushing someone again will cause them to accelerate faster and faster. Impulse on the other hand can be though of like the acceleration in a car. A car with an impulse of 50km, moves along steadily at a rate of 50km. The only way to change speeds would be to change impulse, the values would not be cumulative. However, if another car rear ended you ( FORCE! ), you can certainly see an increase in velocity! 😉
Torque on the other hand is a measure of rotational force. Unlike force and impulse, in Box2D, torque needs to be applied each frame. Stop applying torque each frame and things stop, um, torqueing. Finally, in terms of motion, the touch handler applies force from a given direction. When the user presses up or down, the force was applied to the center of the object equally. However, when the user touches or clicks the screen, the force is applied from the direction of contact. Think just like real life, if you push a glass of beer at it’s center point, it will slide across the table. However, push it at the top and it will probably tip. You do have the option of turning a bodies rotation off if so required.
You can also set values direction, like I did when the user hits the space bar. I set the position, torque, rotation and speed of the object back to zero. This kind of stuff REALLY screws with Box2D though, so if you don’t have to, don’t directly manipulate values, instead work through forces. In a simple simulation like this, its not really a big deal.
Finally you may notice I enabled the Box2DDebugRenderer. In the running example, hit the ESC key ( you may have to click to give keyboard focus ) and you will see a rectangle in place of the sprite. This is an internal representation of the physics body in box2d. When debugging physics problems, this renderer can be invaluable. Not however that we had to modify it’s projection matrix to correspond with our 100pixel per meter scaling factor.
Now that we’ve got things moving, in the next part we will look at what happens when they collide.