Problems with tiled maps

Hi,
I’m learning about tiled maps. So, i’ve some doubts that i will try tu summarize here.

  1. How to detect collisions using tiled maps and box2d? I tried to implement this classes and logic, but it doesn’t work for me: Create Box2D bodies with TMX Objects layer! Cocos2d-x 3.0
    I’m creating rectancles, circles, etc in the tiled map but i can’t detect a collision in the game. I followed all of the tips mentioned in that topic.

  2. My map is a racetrack and the player is a “car”. How can I follow the car around the map? For example, if i’ve a big map, if the car accelerates i should to follow the car around the map.
    I read a lot of topics here (for example this Help with Make Camera follow player) and i tried to implement cocos2d::Follow.
    It works but the initial position of the car and map is moved when I use follow or other methods… it’s strange. Why?

  3. There are some dimmensions that’s recommended to use in tiled maps? for example 32x32, etc.
    I worry how it would look on different devices. Should I scale each tile? Should I set the screen to always see 10x10 for example in the screen? Any example?

  4. I’m using Tiled tool (last version 1.2.1) for design the map.
    Everything works fine but I always see this msg in the console:
    cocos2d: TMXFormat: Unsupported TMX version: 1.2
    It’s normal? Or maybe cocos2d should update something in the framework? i’m using cocos 3.17.
    Anyway, if there are others tools for recommend, plz share…

Thanks :wink:

I can answer part of the 4th question. Cocos is checking with following code (found in CCTMXXMLParser.cpp):

if ( version != "1.0")
{
  CCLOG("cocos2d: TMXFormat: Unsupported TMX version: %s", version.c_str());
}

Hi ! Thanks!
I would like to know … why? I shouldn’t use Tiled 1.2 with cocos 3.17? :thinking:
@slackmoehrle @drelaptop

  1. I don’t often use Box2D, but I’d suggest creating the physics world separate from the TMX. Tilemap tile layers could be still used to render the background, then get the info from tiles/objects in the .tmx file after parsing and çreate physics bodies and moving object Sprites “on top” of the TileMap. You can pull info either from the tile GIDs themselves, or since it sounds like you’re using objects already you can try to use the circle, box objects as info to create the physics world bodies.

  2. c2d::Follow probably isn’t that great. “Camera” isn’t necessarily “easy” especially in fast moving games since you prob. won’t want it to follow exactly, but rather interpolate (e.g. Lerp). Two options are moving the parent (or root) Node object that contains the entire background (track) and sprites (cars) or trying to use the Camera class.

  3. Usually you’ll want the player to see a similar view on any device, but it’s up to your gameplay design whether you want an iPad to show more of the track or whatever. I’d recommend setting a design resolution and having the resolution policy crop the screen since your play area is the center of the screen. I think you can then use the VisibleOrigin to get the point/size that is the rect on the edge of the device screen where it’s being cropped.

  4. Cocos2d hasn’t had an update to the TMX parse in a long while, and don’t expect any in the future. I think you can just not use the 1.1+ features, or ignore the errors and info that isn’t parsed, but you could also look into using a different parser/renderer (though that’ll be some work). It’d probably be easier to just download Tiled 1.0, but you can definitely use .tmx files created in the latest version (again just making sure to not use the newer features like terrain and such).

Format changes (new features).
https://doc.mapeditor.org/en/stable/reference/tmx-changelog/

Hi @stevetranby
Thanks for your help!

Step by step…

Did you see the classes that i’m trying to use?
I’m trying to create the physics world separate from the TMX, by code, reading the objets / bodys from the tmx.
For example, i would like to create a rectangle in the TMX and by code create the body/fixture in box2d from that rectangle, in that position.

This class should helps me to do it:

However, i don’t detect the collisions, i don’t understand why.
I would like to apply this class.
I know that i could set “collision” property in each tile and detect it by code, but i don’t want to do it, because, conceptually i think it’s better to define all bodys in my box2d world and detect all collisions in box2d.
I think it is wrong to define tiles for collision if it doesn’t exist in my box2d world. Do you understand me?

If you haven’t already, forget the tilemap (tmx) for now and just create Rect/Circles by hand using Box2D (or Cocos2d physics) directly. Once you get that working I would think it’d be easier to debug what’s wrong with that TileBodyCreator class (whether in its code or how you’re using it) whether it’s a position/size units mismatch or something else.

I will try to take a glance at that class, but I probably won’t be able to debug by reading it.

Also, have you tried to step through that classes’ methods in the debugger?

If you have a full project (excluding /cocos/ folder) that you can .zip up and share here I am willing to test it out. Basically /classes/ and resources are only necessary files.

Edit: also, if you haven’t you probably will want to enable “debug mode” on your Box2D physics world. Whether cocos2d supports this? or using [Box2d B2DebugDraw updated file for cocos2dx V3].

It works ok. I can create circles, rectangles, etc. in box2d and it works.
I tested and debugged it. My problem is with TileBodyCreator class.

It’s very easy. Create TMX file using Tiled, and create a rectangle, circle, etc in the TMX.
Then, load the tmx and use TileBodyCreator for define body/fixtures in box2d, and debug it, and tell me if you see the body/fixtures in the scene…

I tried to debug it but i can’t, i don’t see the bodys when i add the tmx map.
if i don’t have background in the scene, my box2d debugger works perfect, but when i add the tmx map i don’t see the bodys :frowning:
Maybe is something related to the z-order in the childs…

Ok, here i developed a testing case for iOS. Plz download here: https://github.com/raz0r007/TileMapTest/tree/master/_TileMapTest
Copy cocos2d folder in the project (i’m using cocos2d 3.17). Make sure to edit cocos2d\cocos\base\ccConfig.h
and set CC_FIX_ARTIFACTS_BY_STRECHING_TEXEL to 1

This is for black lines problem in tiled maps.
After that, run the game.

As you can see, this is a racetrack and the car moves in.
The car should collision with a big circle defined in TMX. If you want, you can open TestMap.tmx in Tiled and you will see the circle defined.

The question is, why doesn’t the car collide with the circle? :thinking:

Thanks for the project. I’ll take a look this week when I have some free time.

This is what I’m using for Tiled -> Box2d-body conversion. I translated a big part of this logic from another game library (HaxeFlixel), where it’s also used for Tiled -> physics-body conversion, but for another physics library (which I don’t think is being maintained today). I have some code for adding sloped (45 degree, upper- and lower parts) tiles in another project, but I haven’t yet brought that over to this one yet, so it’s not here atm.
Btw, I use 16x16 pixels for my tiles.

Here’s the code I ported: https://github.com/HaxeFlixel/flixel-addons/blob/master/flixel/addons/nape/FlxNapeTilemap.hx

First, in your init()

std::vector<unsigned int> wallsAndFloors = {};
std::vector<unsigned int> slopesTL = { 1926 };
std::vector<unsigned int> slopesTR = { 2037 };
std::vector<unsigned int> slopesBL = { 1889 };
std::vector<unsigned int> slopesBR = { 2000 };
std::vector<unsigned int> exludeIndices = {};
getTileIndexes(wallsAndFloors, EntityType::COLLIDABLE_TILES_START, EntityType::COLLIDABLE_TILES_END, exludeIndices, false);
setupTileIndices(wallsAndFloors, map->getLayer(LAYER_NAME_FOREGROUND));
// addSlopes(slopesTL, slopesBL, slopesBR, slopesTR, map->getLayer(LAYER_NAME_FOREGROUND));

Where the magic happens

void PlayState::getTileIndexes(std::vector<unsigned int>& indexes, unsigned int lowerLimit, unsigned int upperLimit, std::vector<unsigned int>& exclude, bool includeUpper) {
	
	if (includeUpper)
		upperLimit++;
	
	if (exclude.size() == 0) {
		for (size_t i = lowerLimit; i < upperLimit; i++) {
			indexes.push_back(i);
		}
	} else {
		for (size_t i = lowerLimit; i < upperLimit; i++) {
			if (std::find(exclude.begin(), exclude.end(), i) == exclude.end()) {
				indexes.push_back(i);
			}
		}
	}
}
void PlayState::setupTileIndices(std::vector<unsigned int> &tileIndices, TMXLayer* tmxLayer) {
	int tileIdx = 0;
	auto layerSize = tmxLayer->getLayerSize();
	std::vector<unsigned int> _binaryData;
	uint32_t* tiles = tmxLayer->getTiles();

	for (size_t y = 0; y < layerSize.height; y++) {
		for (size_t x = 0; x < layerSize.width; x++) {
			tileIdx = x + (y * layerSize.width);
			// make it match the value reported inside Tiled editor
			int curTile = tiles[tileIdx]-1;
			if (std::find(tileIndices.begin(), tileIndices.end(), curTile) != tileIndices.end()) {
				_binaryData.push_back(1);
			} else {
				_binaryData.push_back(0);
			}
		}
	}

	constructCollider(_binaryData, tmxLayer);
}
void PlayState::constructCollider(std::vector<unsigned int> &_binaryData, TMXLayer* tmxLayer) {
	int tileIndex = 0;
	int startRow = -1;
	int endRow = -1;
	std::vector<cocos2d::Rect> rects;

	//Go over every column, then scan along them
	for (size_t x = 0; x < tmxLayer->getLayerSize().width; x++)
	{
		for (size_t y = 0; y < tmxLayer->getLayerSize().height; y++)
		{
			tileIndex = x + (y * tmxLayer->getLayerSize().width);
			//Is that tile solid?
			if (_binaryData[tileIndex] == 1)
			{
				//Mark the beginning of a new rectangle
				if (startRow == -1)
					startRow = y;

				//Mark the tile as already read
				_binaryData[tileIndex] = -1;

			}
			//Is the tile not solid or already read
			else if (_binaryData[tileIndex] == 0 || _binaryData[tileIndex] == -1)
			{
				//If we marked the beginning a rectangle, end it and process it
				if (startRow != -1)
				{
					endRow = y - 1;
					rects.push_back(constructRectangle(x, startRow, endRow, tmxLayer, _binaryData));
					startRow = -1;
					endRow = -1;
				}
			}
		}
		//If we reached the last line and marked the beginning of a rectangle, end it and process it
		if (startRow != -1)
		{
			endRow = tmxLayer->getLayerSize().height - 1;
			rects.push_back(constructRectangle(x, startRow, endRow, tmxLayer, _binaryData));
			startRow = -1;
			endRow = -1;
		}
	}

	//Convert the rectangles to polygons
	for (size_t r = 0; r < rects.size(); r++)
	{
		auto rect = rects.at(r);
		auto w = (rect.size.width - rect.origin.x) + 1;
		auto h = (rect.size.height - rect.origin.y) + 1;

		rect.setRect(
			rect.getMinX() * tmxLayer->getMapTileSize().width,
			rect.getMinY() * tmxLayer->getMapTileSize().height,
			w * tmxLayer->getMapTileSize().width,
			h * tmxLayer->getMapTileSize().height
			);

		unsigned int numPts = 4;
		b2Vec2* vertices = new b2Vec2[numPts];
		auto mapMaxY = map->getBoundingBox().getMaxY();
		vertices[0] = b2Vec2(rect.getMinX() / PTM_RATIO, (mapMaxY - rect.getMaxY()) / PTM_RATIO);
		vertices[1] = b2Vec2(rect.getMaxX() / PTM_RATIO, (mapMaxY - rect.getMaxY()) / PTM_RATIO);
		vertices[2] = b2Vec2(rect.getMaxX() / PTM_RATIO, (mapMaxY - rect.getMinY()) / PTM_RATIO);
		vertices[3] = b2Vec2(rect.getMinX() / PTM_RATIO, (mapMaxY - rect.getMinY()) / PTM_RATIO);

		auto stageMaterial = getFixtureDef(StageComponents::wallsAndFloors);
		makePoly(vertices, numPts, b2BodyType::b2_staticBody, &stageMaterial);

		/*    3 <<<<<<<<< 2
		      v           ^
		      v           ^
		      v           ^
		      v           ^
		start 0 >>>>>>>>> 1    */
	}
}
b2Body* PlayState::makePoly(b2Vec2* vertices, int32 count, b2BodyType type, b2FixtureDef *fixDef) {
	b2BodyDef bDef;
	bDef.type = type;
	b2Body* b = world->CreateBody(&bDef);

	b2PolygonShape polygonShape;
	polygonShape.Set(vertices, count);

	b2FixtureDef fixtureDef;
	fixtureDef.shape = &polygonShape;
	
	if (type == b2BodyType::b2_staticBody)
	{
		fixtureDef.filter.categoryBits = FilterCategory::CT_STAGE;
	}

	b->CreateFixture(&fixtureDef);
	delete[] vertices;

	return b;
}
Rect PlayState::constructRectangle(int startX, int startY, int endY, TMXLayer* tmxLayer, std::vector<unsigned int> &_binaryData)
{
	//Increase StartX by one to skip the first column, we checked that one already
	startX++;
	bool rectFinished = false;
	int tileIndex = 0;
	auto layerSize = tmxLayer->getLayerSize();

	//go along the columns from StartX onwards, then scan along those columns in the range of StartY to EndY
	for (size_t x = startX; x < layerSize.width; x++)
	{
		for (size_t y = startY; y < (endY + 1); y++)
		{
			tileIndex = x + (y * layerSize.width);
			//If the range includes a non-solid tile or a tile already read, the rectangle is finished
			if (_binaryData[tileIndex] == 0 || _binaryData[tileIndex] == -1)
			{
				rectFinished = true;
				break;
			}
		}

		if (rectFinished)
		{
			//If the rectangle is finished, fill the area covered with -1 (tiles have been read)
			for (size_t u = startX; u < x; u++)
			{
				for (size_t v = startY; v < (endY + 1); v++)
				{
					tileIndex = u + (v * layerSize.width);
					_binaryData[tileIndex] = -1;
				}
			}

			//StartX - 1 to counteract the increment in the beginning
			//Slight misuse of Rectangle here, width and height are used as x/y of the bottom right corner
			return Rect(startX - 1, startY, x - 1, endY);
		}
	}

	//We reached the end of the map without finding a non-solid/alread-read tile, finalize the rectangle with the map's right border as the endX
	for (size_t u = startX; u < layerSize.width; u++)
	{
		for (size_t v = startY; v < (endY + 1); v++)
		{
			tileIndex = u + (v * layerSize.width);
			_binaryData[tileIndex] = -1;
		}
	}

	return Rect(startX - 1, startY, layerSize.width - 1, endY);
}

Thanks, i will wait your answer :slight_smile:

@xerosugar thanks for your code. I will take a look.

I’m not an expert in Box2D, nor physics games since I prefer custom collision detection and haven’t ventured into true pure-physics games (where user controls objects with impulses and forces).

First, I’m fairly convinced this is the “wrong” way to handle collisions, but maybe if you look up “top-down car games with box2d physics” or whatever you’ll find this is how they do it as a pseudo-hack? Anyway, I don’t believe you can detect collisions between static/kinematic bodies (I tried changing car to kinematic first). Unity Physics was odd in this matter as well and while I understand how the physics engines behave normally only when collisions are detected through the normal physics simulation (ie: with impulses/forces affecting dynamic objects into static/kinematic objects).

Having look at your code I can get it to work by:

  1. disabling gravity since 2D gravity doesn’t make sense for a top-down car game? I could be wrong.
  2. Setting the player to a dynamic body sensor.
  3. I changed the .tmx Circle object to set “type” instead of “name” (didn’t affect collision, but created rect instead of circle).
  4. I moved the car object further down in the tilemap so that it would start moving from the bottom of the screen.
  5. I also added a rect object in .tmx that was much closer to the new position of car.
  6. … not sure if there was anything else.

Listing 1
(sorry, modified your code a bit for “easier debugging” so I knew what things were)

// disable gravity
b2Vec2 gravity(0, 0);//-9.8);

//... other code ...

            //Box2d car
            b2BodyDef bdCarDef;
            bdCarDef.type = b2_dynamicBody;
            bdCarDef.position = b2Vec2(x/SCALE_RATIO, y/SCALE_RATIO);
            bdCarDef.userData = car;
            auto bCarBody = _world->CreateBody(&bdCarDef);

            // add fixture
            float w = car->getBoundingBox().size.width;
            float h = car->getBoundingBox().size.height;
            float boxWidth = (w/2) / SCALE_RATIO;
            float boxHeight = (h/2) / SCALE_RATIO;
            b2PolygonShape psCarShape;
            psCarShape.SetAsBox(boxWidth, boxHeight);
//            bCarBody->CreateFixture(&psCarShape, 0);

            b2FixtureDef fdCarFixture;
            fdCarFixture.isSensor = true;
            fdCarFixture.shape = &psCarShape;
            bCarBody->CreateFixture(&fdCarFixture);

Let me know if this helps. I could post the full .cpp/.h/.tmx, but this instead is hopefully enough info to avoid that.

Oh, and my research was maybe unnecessary, though at least I learned how Box2D works (compared to Unity).

You can set the global Z-order of the debug layer.

auto debugLayer = B2DebugDrawLayer::create(_world, SCALE_RATIO); //PTM_RATIO);
debugLayer->setGlobalZOrder(99);
addChild(debugLayer);

Hello @stevetranby
Thanks, but it doesn’t help :frowning: . Have you test your changes? I tried it, the result is the same for me.
The problem here is about the detection of collisions between tiled maps and box2d (the simple collisions in box2d works for me, that’s not the problem). It doesn’t matter if the object is static or dynamic or if there is gravity or not. That doesn’t influence in the collision. It also doesn’t influence whether it is a circle or not.

Anyway, i’m agree with you. Maybe it’s better gravity 0 and dynamic object, but it’s independent of this problem.

The car moves into a rect and circle defined in the “Collisions” object layer of the .tmx tile map and triggers the Begin/EndContact() methods. I thought that’s what wasn’t working for you. If that wasn’t the problem, then xerosugar’s advice may help if you’re looking for parsing in the “Background” tile layer and having certain tile types (GIDs) cause collisions as well as the “Collisions” shape objects.

Sorry I can’t be much more help if the car triggering BeginContact/etc when it collides with the shape objects (circle, rect, poly) is not the solution you were looking for.

I’m debugging it again…
You’re right: when i changed static to dynamic (the car), BeginContact is called. But why? I’ve other games that collides with static objects. Anyway, I expected it to collide with the “invisible” circle, however it doesn’t “collide”. Maybe it’s related to categoryBits and maskBits? But i don’t changed that, by default anything should collide with anything. Hmm… :thinking:

I think I know what the problem is.
All the bodys created by TiledBodyCreator are static (I don’t see it defined, but I assume that by default it are static?). So, the car must be dynamic, because static with static there is no collision.

About the categoryBits and maskBits… i don’t know…

You’ll need to read up on Box2D physics, or game physics engines in general.

This brief gist is that dynamic objects are required for collisions, and you’ll also need to fully utilize the physics world such that you must set mass of dynamic objects (static/kinematic are infinite mass) and apply forces and/or impulses to get your desired behavior.

Otherwise you’re currently trying to use it in a collision detection only manner and you are then on the hook for applying behavior to the objects that collided (movement them, changing their velocity, etc).

I don’t understand why TiledBodyCreator doesn’t specify explicitly that the bodys are static. It’s strange. This has confused me. And what about the “collide” between the car and the circle? Why it doesn’t occur? I’m not defining categoryBits and maskBits. As i understand, by default anything should collide with anything… :thinking:

You’re likely wrong on the any/any collisions, though I’m sure there’s a way to hack around that. :smiley:

Default body type is probably static in Box2D, but you could just modify TiledBodyCreator to explicitly set it to static in initCollisionMap just set it bd.type= b2_staticBody;.