Rss

libgdx, box2d, tiled maps: full working example, part 2/3

This is part 2 of a 3 part post demonstrating how to make a simple side scrolling game using libgdx and box2d. See part 1.

Updated 2011-09-19: Heh, now uses libgdx 0.9.2. No longer has its own Box2DDebugRenderer, figured out a better way using the new Matrix4 argument to renderer. Improved the controls by removing linear damping and replacing it with friction. (I am learning as I go with this, for what it is worth.) Discovered that I made a rather embarrassing mistake in leaving out rudimentary on-screen controls for Android devices. They are still pretty cruddy, though. Maybe I’ll come up with something better, or maybe someone else will and will contribute it? :) Either way is fine.

Updated 2011-09-18: Now uses libgdx 0.9.1.

In part 1 you saw how to make what is essentially a tiled map viewer. After you are through with this part you will have a character that you can move around the screen in a “natural” way, or at least a familiar way. As in part 1, download JumperTutorialProjects.zip for part 2 and follow along as the code is explained.

Post index

  1. Creating map tiles and the game map
    1. Where to begin
    2. Creating map tiles
    3. Creating the game map
    4. Saving the map data to the projects
    5. Loading and viewing the map in-game: TiledMapHelper
    6. Enable scrolling through the map by touch
    7. Summary
  2. Integrating box2d
    1. Define the collision boundaries for each type of map tile
    2. Introducing TileCollisionTool
    3. Programmatically configure the map’s collision boundaries
    4. Add a player character sprite
    5. Navigate the map with the player character
    6. Summary
  3. Final touches: Scoring, lives, game over [coming soon]
    1. Add breaking blocks
    2. Add score and score display
    3. Add player death and a game over screen
    4. Hey, you’ve got a game
    5. Summary

Define the collision boundaries for each type of map tile

Adding collision support to a tiled map can be done any number of ways. For example, you could create a map (using Tiled Map Editor or whatever tool) and then use a separate tool to trace over a the map, defining where the collisions should occur. You would then write some code that loads those definitions at run time. If you used this method you would need to redraw your collision boundaries every time you change your map, in effect drawing the map twice (once with the map editor and again with the collision tracer). It would be easy for the map and the collision definitions to become out of sync.

An alternative method, the one used in this example, involves describing where the collisions should occur on each type of tile and then automatically prepare the boundaries at run-time. By setting the collision boundaries per tile, you will ensure that your environmental collisions will always be consistent with your map.

For an example of this per-tile approach consider a “floor” tile, with air above and dirt below. It would have a collision boundary across its top. A ramp would have a diagonal collision boundary from the top of one side to the bottom of the other.

Highlighted collision boundaries on selected tiles, enlarged for detail

The collision boundaries will need to be saved somewhere accessible to the game. In this example, the collision boundaries will be stored in a file, with one entry per tile. Each entry will define the line segments that make up the tile’s boundaries. The boundary file will be read when the map is initialized.

A tile that has no boundaries (such as a sky tile) will have no x and y coordinate pairs. All other tiles should have at least two pairs (minimum for a line segment). Here is the boundaries file used for the tile set that was created for part 1 of this series:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1 
2 
3 0x31,0x16 0x16,16x16 16x16,16x0 16x0,31x0 31x0,31x31 0x31,31x31
4 0x0,27x0 27x0,29x2 29x2,29x31
5 
6 
7 29x0,29x31
8 
9 
10 0x31,0x0 0x0,15x0 15x0,15x16 15x16,31x16 31x16,31x31 31x31,0x31
11 2x0,2x31
12 
13 
14 0x0,31x0
15 
16 0x31,0x0 0x0,31x0 31x0,31x31 31x31,0x31
17 
18 
19 2x31,2x2 2x2,4x0 4x0,31x0
20 
21

Take a look at the tile set and compare it to what you see in this list. Tile 3, the stair-step tile in the top rightcorner of the tile image has collision boundaries all around its outer edge. Tile 7, the right-facing cliff tile, has a collision boundary that follow its edge. Some tiles have no collision boundaries at all, because they’re background pieces or pieces that can never be reached by a player.

You can create these tile boundary files by hand, but it requires a lot of work, and it is easy to make mistakes.

Introducing TileCollisionTool

That’s why I wrote TileCollisionTool. With TileCollisionTool, you can upload your tile image and then draw the specific collision boundaries for each tile. The tool also ensures that the boundaries you create fit within the tile’s width and height, and it handles spacing. It requires JavaScript and has been tested on Chrome, Safari, and Firefox 3.

TileCollisionTool in action

The resulting file will look much like the example boundary file above and should be saved as “collisions.txt” in the same project directory as tiles.png.

Programmatically configure the map’s collision boundaries

Now that each type of tile has its collision boundaries configured, the code can simply loop through the tiles defined in the map file, adding static box2d bodies as it goes. As the name implies, static bodies are unmoving. When the game is running your player character will be a dynamic fixture (see below) and will be unable to pass through these static bodies. This requires no serious effort on the part of the programmer. box2d handles it all.

As you probably suspect, adding a separate body or set of bodies for every tile means the game will have to keep track of a lot of bodies. This may not be a big deal, but why make the device work harder than it has to? The problem can be somewhat mitigated by adding code that detects whether one tile simply “continues” the collision line set up by the previous tile. For example, if you have a four tiles of flat ground next to each other, a “naive” implementation would result in four separate static bodies. If the code can detect that situation, the code can make one large body encompassing all four tiles.

In the spirit of decoupling and compartmentalizing, the map collision boundary code is written as a new method for TiledMapHelper. The following snippet is the code used to populate the world with the collision boundaries (the whole method is too large to reasonably paste here, but you’ll find it in the .zip):

208
209
210
211
212
213
214
215
216
217
218
		BodyDef groundBodyDef = new BodyDef();
		groundBodyDef.type = BodyDef.BodyType.StaticBody;
		Body groundBody = world.createBody(groundBodyDef);
		for (LineSegment lineSegment : collisionLineSegments) {
			PolygonShape environmentShape = new PolygonShape();
			environmentShape.setAsEdge(
					lineSegment.start().mul(1 / pixelsPerMeter), lineSegment
							.end().mul(1 / pixelsPerMeter));
			groundBody.createFixture(environmentShape, 0);
			environmentShape.dispose();
		}

Take special note of the pixelsPerMeter variable there. box2d works well with small numbers, and typically people use the term “meter” for box2d’s distance unit. You’ll need to pick a conversion value to use that will take you from pixels to meters and meters to pixels. In this example project, pixelsPerMeter is 60.0, which results in expected physical behavior.

Add a player character sprite

Draw a small image to use as your player character’s sprite. The sprite can be any size. I suggest making it something like the size of a single tile, at least for this example. The sprite will need to be saved inside of an image with powers-of-2 dimensions, due to the aforementioned OpenGL ES requirements. The sample image included in the project (and displayed below) meets the requirements.

Example player character sprite file

Loading the sprite in the game is simple. First, you need to know the size of your sprite and its position within the overall sprite file. In this example, the jumper sprite is at the top left corner and is 21 pixels wide and 37 pixels tall. This code in JumperTutorial’s create method does the job:

139
140
141
142
		overallTexture = new Texture(Gdx.files.internal("data/sprite.png"));
		overallTexture.setFilter(TextureFilter.Linear, TextureFilter.Linear);
 
		jumperSprite = new Sprite(overallTexture, 0, 0, 21, 37);

Basically, the entire sprite image is loaded in to the system as a Texture, and then a chunk of that Texture is defined as a Sprite. The sprite will be drawn on screen after the TiledMapRenderer.render call.

Navigate the map with the player character

As alluded earlier, the player character will be represented in the box2d world as a dynamic fixture. For simplicity and sanity, this code will assume that the character is rectangular:

152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
		BodyDef jumperBodyDef = new BodyDef();
		jumperBodyDef.type = BodyDef.BodyType.DynamicBody;
		jumperBodyDef.position.set(1.0f, 3.0f);
 
		jumper = world.createBody(jumperBodyDef);
 
		/**
		 * Boxes are defined by their "half width" and "half height", hence the
		 * 2 multiplier.
		 */
		PolygonShape jumperShape = new PolygonShape();
		jumperShape.setAsBox(21f / (2 * PIXELS_PER_METER),
				37f / (2 * PIXELS_PER_METER));
 
		/**
		 * The character should not ever spin around on impact.
		 */
		jumper.setFixedRotation(true);
 
		/**
		 * The density of the jumper, 70, was found experimentally. Play with
		 * the number and watch how the character moves faster or slower.
		 * 
		 * The linear damping was also found the same way.
		 */
		jumper.createFixture(jumperShape, 70);
		jumperShape.dispose();
 
		jumper.setLinearVelocity(new Vector2(0.0f, 0.0f));
		jumper.setLinearDamping(5.0f);

The player character’s sprite will be drawn based on the coordinates maintained in the character’s dynamic body object (jumper). The coordinates must be converted from box2d world units back to pixel units:

331
332
333
334
335
336
337
338
339
		spriteBatch.setProjectionMatrix(tiledMapHelper.getCamera().combined);
		spriteBatch.begin();
 
		jumperSprite.setPosition(
				PIXELS_PER_METER * jumper.getPosition().x
						- jumperSprite.getWidth() / 2,
				PIXELS_PER_METER * jumper.getPosition().y
						- jumperSprite.getHeight() / 2);
		jumperSprite.draw(spriteBatch);

There are a number of possible ways to control a character in a game. In this example I am using the keyboard. Left and right arrows move the character left and right, and the space bar causes the character to jump. Character movement is performed using “impulses”, which, in physics, is a way to apply momentum to a body that results in reasonably smooth action. I’ve pasted the movement code below, however the actual code in the project differs slightly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
		if (Gdx.input.isKeyPressed(Input.Keys.KEYCODE_DPAD_RIGHT)) {
			jumper.applyLinearImpulse(new Vector2(12.0f, 0.0f), new Vector2(
					jumperSprite.getWidth() / (2 * PIXELS_PER_METER),
					jumperSprite.getHeight() / (2 * PIXELS_PER_METER)));
		} else if (Gdx.input.isKeyPressed(Input.Keys.KEYCODE_DPAD_LEFT)) {
			jumper.applyLinearImpulse(new Vector2(-12.0f, 0.0f), new Vector2(
					jumperSprite.getWidth() / (2 * PIXELS_PER_METER),
					jumperSprite.getHeight() / (2 * PIXELS_PER_METER)));
		}
 
		if (Gdx.input.isKeyPressed(Input.Keys.KEYCODE_DPAD_UP)
				&& Math.abs(jumper.getLinearVelocity().y) < 1e-9) {
			jumper.applyLinearImpulse(new Vector2(0.0f, 90.0f), new Vector2(
					jumperSprite.getWidth() / (2 * PIXELS_PER_METER),
					jumperSprite.getHeight() / (2 * PIXELS_PER_METER)));
		}

Summary

Right on. This is looking more and more like a real game. At this point, you’ve seen how to construct a 2 dimensional game world and how to navigate a character around that world. Where to go from here? It might be worth adding some code to reset the game when it detects that the character falls between cliffs, so you don’t have to restart the whole program every time. You could also play with different, more detailed tile sets, making a more complicated and interesting world. Maybe make a few more dynamic objects, attach them to sprites, and see how they interact with your character. In part 3 you’ll see how to add kinematic bodies to the game (such as breakable blocks), how to add superimposed scoring, and a way to detect game over (although that is pretty easy!)

By the way, as Jon pointed out, the code has the debug renderer enabled. You’ll see boxes all over the place, describing the box2d fixtures. You can disable it by commenting out the renderer.render() call.

Comments (92)

  1. akumaGouki

    I did it!

    first:

    bodyDef.type = BodyDef.BodyType.KinematicBody;

    then, when the input happend

    Box.setLinearVelocity(new Vector2(0, 100000f/PIXELS_PER_METER));

  2. Josh13465

    How would I edit the input code so that the jump is still there(you have to press the top right or top left of the screen to jump), but he “walks” across the ground whenever you touch the right bottom or right left of the screen? Sorry, I’m new to Android Development and I’m trying to get the hang of the physics.

  3. Hi!!! im having trouble on resizing the screen, the sprites of my game buttons are resized and positioned ok and the toucheds are recalculated properly but the tilemap seems to get a zoomin and do rare things.. :S
    how can i resize it properly?? :S:S

    • dpk

      I’m not sure, can you post some code (on pastebin or similar)?

      • yeyeyey

        i’ve solved that!! :D but now i have another problem… my tiles are backgrounded transparent but it paints white???!!
        what can be causing that?

        • yeyeyey

          solved!

          Gdx.gl10.glAlphaFunc(GL10.GL_GREATER, 0.5f);
          Gdx.gl10.glEnable(GL10.GL_ALPHA_TEST);

  4. Dewi

    It works when I run it on the desktop, but I get this error when I try and run it on a Android device. Could you please help me. Thanks

    07-25 12:41:19.184: E/dalvikvm(21088): Could not find class ‘com.example.jumpertutorial.JumperTutorial’, referenced from method com.example.jumpertutorial.JumperTutorialAndroid.onCreate
    07-25 12:41:19.189: E/AndroidRuntime(21088): FATAL EXCEPTION: main
    07-25 12:41:19.189: E/AndroidRuntime(21088): java.lang.NoClassDefFoundError: com.example.jumpertutorial.JumperTutorial
    07-25 12:41:19.189: E/AndroidRuntime(21088): at com.example.jumpertutorial.JumperTutorialAndroid.onCreate(JumperTutorialAndroid.java:35)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.app.Activity.performCreate(Activity.java:4465)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1049)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2033)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2104)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.app.ActivityThread.access$600(ActivityThread.java:132)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1157)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.os.Handler.dispatchMessage(Handler.java:99)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.os.Looper.loop(Looper.java:137)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at android.app.ActivityThread.main(ActivityThread.java:4575)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at java.lang.reflect.Method.invokeNative(Native Method)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at java.lang.reflect.Method.invoke(Method.java:511)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:789)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:556)
    07-25 12:41:19.189: E/AndroidRuntime(21088): at dalvik.system.NativeStart.main(Native Method)

    • cgw

      Having the same problem:
      W/dalvikvm(3317): Unable to resolve superclass of Lcom/example/jumpertutorial/android/JumperTutorialAndroid; (520)

    • cgw

      If you haven’t already, open Properties for JumperTutorialAndroid and add the folder where the JumperTutorial class is located to the Libraries as a class folder (Should be JumperTutorial/src).

  5. demo

    No part 3??

  6. Andrew

    Thanks for the great tutorial. I’m just having one small problem. The collision lines of my map are all about 10 pixels too low. Any ideas?

    • Andrew

      Nevermind, answered my own question. I copied your loadCollisions method from the zip and it has 32 hard coded as part of the parameter to addOrExtendCollisionLineSegment. My tiles are 50×50. It’s working perfectly now. :)

  7. Kishor

    Could you please share the Tile Collision Tool

  8. Just one question…
    1.) How do I edit the tiles with the Box2D editor tool? (http://code.google.com/p/box2d-editor/)

  9. Nc

    NEXT PART PLEASE! ;)

  10. [...] http://dpk.net/2011/05/01/libgdx-box2d-tiled-maps-full-working-example-part-1/ a not yet finished tutorial, that will cover a complete example. Unknown to you should be the code from this passage on. [...]

  11. nickel

    No part 3 out? So sad… :(

  12. The tile collision tool only works for me when I set the spacing to zero. However, whenever I do that I get an offset in the tiles. When I set the spacing to two it says the “number of tiles” is zero, and and I’m unable to draw the collision boundary: the collision marker just stays stuck to my mouse wherever I move it.
    Has anyone else had this problem? Can anyone suggest another tool for collision detection?

Leave a Reply

Your email address will not be published. Required fields are marked *