読者です 読者をやめる 読者になる 読者になる

SHIGEBITログ

主に自作ゲームについて書きます

cocos2d-x : TMXファイルから物理ボディーを作る

cocos2d-x

(修正)コード内で、引数がコンテナの部分で、値渡しになっていたところを参照渡しに直しました。

先日公開したアンドロイドゲームアプリ「とニャンぽりん」は、未だにダウンロード数ゼロという状況です。

公開されるアプリの9割はゾンビアプリになる、とかいう記事も読んだことありますけど、自分のアプリもその運命なのかなぁ。

いやいや、バージョンアップでゲーム内容を充実させたりして頑張ってみますよ。

現在は、即死しない Flappy Bird 風(でもないか)のゲームですけど、マップクリア型のゲームモードを、バージョンアップで追加しようと思います。

というわけで、マップを作ります。

作成には cocos2d-x でサポートされている「Tiled Map Editor」と使います。

サポートされているだけに、タイルを敷き詰めたレイヤーを表示するのは簡単です。

さて、ゲームで物理エンジンを使っているなら、マップの床や壁などは物理ボディーにしたいはず。

「とニャンぽりん」も物理エンジンで動かしてますし。

TMXファイルのオブジェクトレイヤーから物理ボディーを作るクラス

ようやく本題です。

TMXファイルは普通のXMLですので、要素や属性を取り出してそれをパラメーターとしてボディーを作るっていうそれだけの事なので、方法について解説っていうのは特に無いです。
なので、Tiled Map Editor のオブジェクトレイヤーから物理ボディーを作るクラスを掲載するだけにします。

cocos2dxのPhysicsBodyを使うことを想定していますが、BOX2D利用時でもシェイプ作成コールバックを書き換えるだけでそのまま使えるはずです。
また、タイルレイヤーのタイル情報からオブジェクトを作る等、何かしたいときの為の関数も付いています。

なお、base64,zlibでの展開部分はcocos2dのソースを流用しています。

TmxBodyCreator.h

#pragma once
#include "cocos2d.h"

//
//
// 「オブジェクト」と「シェイプ」という言葉が混在してややこしいかと思いまが、同じものを意味しています。
// マップエディターのオブジェクトレイヤー上に作成する「オブジェクト」が、
//  プログラム内では「シェイプ(cocos2d::PhysicsShape)」として作成されるということです。
//
//
//


//---------------------------------------------------------------------------------------------
// オブジェクトレイヤーからボディーを作成する時の、シェイプ作成コールバック
//---------------------------------------------------------------------------------------------
class TmxShapeCreateCallBack
{
private:
	cocos2d::PhysicsMaterial m_defaultMaterial;

public:
	TmxShapeCreateCallBack(const cocos2d::PhysicsMaterial& defaultMaterial = cocos2d::PHYSICSBODY_MATERIAL_DEFAULT);
	virtual ~TmxShapeCreateCallBack();

	/**
	* オブジェクトごとに呼ばれるコールバック
	*
	* オブジェクトに設定したプロパティーを元に何かする場合は、このクラスを継承して以下のコールバック関数に処理を実装すると良いです。
	*
	* @param id オブジェクトID (マップエディタで表示されるID)
	* @param points 頂点リスト
	* @param properties プロパティー (プロパティ名 , 内容)
	* @return 作成したシェイプを返す。
	*          nullptrを返せば、このオブジェクトはボディーに追加されません。
	*          このオブジェクトを、独立した物理ボディーとして作成したい場合や、
	*          マップ作成時の何かのマーカーとしてのみ使用したい場合とかに利用できると思います。
	*/
	virtual cocos2d::PhysicsShape* OnCreatePolylineShape(int id, std::vector<cocos2d::Vec2>& points, std::map<std::string, std::string>& properties);
	virtual cocos2d::PhysicsShape* OnCreatePolygonShape(int id, std::vector<cocos2d::Vec2>& points, std::map<std::string, std::string>& properties);
	virtual cocos2d::PhysicsShape* OnCreateCircleShape(int id, float x, float y, float radius, std::map<std::string, std::string>& properties);

	/**
	* (上のコールバック関数内から呼んでいる)
	* 通常、シェイプの PhysicsMaterial には m_defaultMaterial を使用しますが、
	* プロパティに d,f,r があるオブジェクトには PhysicsMaterial の density, friction, restitution をこの値にします。
	*/
	virtual cocos2d::PhysicsMaterial PropertyCheck(std::map<std::string, std::string>& properties);

	/*
	* オブジェクトを独立したボディーとして作成したい場合に、オブジェクトの中心座標と、各頂点のオフセット座標を取得できます。
	*/
	virtual void GetCenter(const std::vector<cocos2d::Vec2>& inPoints, cocos2d::Vec2* outCenter, std::vector<cocos2d::Vec2>* outPoints);
};


//---------------------------------------------------------------------------------------------
// タイルレイヤーにタイルがある場合に呼ばれるコールバック
//---------------------------------------------------------------------------------------------
class TmxObjectCreateCallBack
{
protected:
	cocos2d::Node* m_nodeForAdd;

public:
	/**
	* @param nodeForAddChild 作成したオブジェクトを追加するノード
	*/
	TmxObjectCreateCallBack(cocos2d::Node* nodeForAddChild = nullptr);

	/**
	* @param tileID		タイルID (マップエディタで表示されるID 0~)
	* @param position	ピクセル座標 (タイルのマスの中央座標)
	*/
	virtual void OnCreateObject(int tileID, cocos2d::Vec2 position);
};



//--------------------------------------------------------------------------------------------
// TMXのオブジェクトレイヤーからボディー作成 & タイルレイヤーからオブジェクト作成
//--------------------------------------------------------------------------------------------
class TmxBodyCreator
{
private:
	static TmxShapeCreateCallBack* m_callBack;
	static TmxObjectCreateCallBack* m_objectCreateCallBack;

private:
	TmxBodyCreator(){};
	static void SetCreateShapeCallBack(TmxShapeCreateCallBack* callBack);

public:
	/**
	* TMXのオブジェクトレイヤーからボディー作成
	* @param tmxStr				tmxファイルの中身
	* @param objectgroupName	レイヤー名	
	* @param callBack			オブジェクトごとに呼ばれるコールバック
	*/
	static cocos2d::PhysicsBody* CreateBodyWithXML(const std::string& tmxStr, const std::string& objectLayerName, TmxShapeCreateCallBack* callBack = nullptr);
	static cocos2d::PhysicsBody* CreateBodyWithXML(const std::string& tmxStr, const std::vector<std::string>& objectLayerNameList, TmxShapeCreateCallBack* callBack = nullptr);

	/**
	* TMXファイルのオブジェクトレイヤーからボディー作成
	* @param tmxStr				tmxファイルの中身
	* @param objectgroupName	レイヤー名
	* @param callBack			オブジェクト作成ごとに呼ばれるコールバック
	*/
	static cocos2d::PhysicsBody* CreateBody(const std::string& tmxFile, const std::string& objectLayerName, TmxShapeCreateCallBack* callBack = nullptr);
	static cocos2d::PhysicsBody* CreateBody(const std::string& tmxFile, const std::vector<std::string>& objectLayerNameList, TmxShapeCreateCallBack* callBack = nullptr);

	/**
	* タイルレイヤーからオブジェクト作成
	* @param tmxStr				tmxファイルの中身
	* @param tileLayerName		レイヤー名
	* @param tileSetName		タイルセット名
	* @param callBack			タイルがある場合に呼ばれるコールバック
	*/
	static int CreateObjects(const std::string& tmxStr, const std::string& tileLayerName, const std::string& tileSetName, TmxObjectCreateCallBack* callBack = nullptr);

	~TmxBodyCreator();
};

TmxBodyCreator.cpp

#include "TmxBodyCreator.h"
#include "external/tinyxml2/tinyxml2.h"


#pragma execution_character_set("utf-8")


USING_NS_CC;


TmxShapeCreateCallBack* TmxBodyCreator::m_callBack = nullptr;
TmxObjectCreateCallBack* TmxBodyCreator::m_objectCreateCallBack = nullptr;


std::vector<std::string> StringSplit(const std::string& str, char separator)
{
	std::vector<std::string> v;
	std::stringstream ss(str);
	std::string buffer;
	while (std::getline(ss, buffer, separator)) {
		v.push_back(buffer);
	}
	return v;
}

std::vector<Vec2> PointsSplit(const char* pointsStr)
{
	std::string str(pointsStr);
	auto pointStrList = StringSplit(str, ' ');
	std::vector<Vec2> points;
	for (auto point : pointStrList)
	{
		auto s = StringSplit(point, ',');
		Vec2 v(std::atof(s[0].c_str()), std::atof(s[1].c_str()));
		points.push_back(v);
	}
	return points;
}





/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// オブジェクトレイヤからボディー作成
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
PhysicsBody* TmxBodyCreator::CreateBody(const std::string& tmxFile, const std::string& objectLayerName, TmxShapeCreateCallBack* callBack)
{
	auto tmxStr = FileUtils::getInstance()->getStringFromFile(tmxFile);
	return CreateBodyWithXML(tmxStr, objectLayerName, callBack);
}

PhysicsBody* TmxBodyCreator::CreateBody(const std::string& tmxFile, const std::vector<std::string>& objectLayerNameList, TmxShapeCreateCallBack* callBack)
{
	auto tmxStr = FileUtils::getInstance()->getStringFromFile(tmxFile);
	return CreateBodyWithXML(tmxStr, objectLayerNameList, callBack);
}

PhysicsBody* TmxBodyCreator::CreateBodyWithXML(const std::string& tmxStr, const std::string& objectLayerName, TmxShapeCreateCallBack* callBack)
{
	std::vector<std::string> objectLayerNameList;
	objectLayerNameList.push_back(objectLayerName);
	return CreateBodyWithXML(tmxStr, objectLayerNameList, callBack);
}

PhysicsBody* TmxBodyCreator::CreateBodyWithXML(const std::string& tmxStr, const std::vector<std::string>& objectLayerNameList, TmxShapeCreateCallBack* callBack)
{
	if (callBack)
	{
		SetCreateShapeCallBack(callBack);
	}

	if(!m_callBack)
	{
		m_callBack = new TmxShapeCreateCallBack();
	}

	// 物理ボディー
	PhysicsBody* body = PhysicsBody::create();

	// XML パース
	tinyxml2::XMLDocument doc;
	doc.Parse(tmxStr.c_str(), tmxStr.size());
	auto rootElement = doc.RootElement();			// ルート取得

	int tileWidth = std::atoi(rootElement->Attribute("tilewidth"));
	int tileHeight = std::atoi(rootElement->Attribute("tileheight"));
	int mapWidth = std::atoi(rootElement->Attribute("width"));
	int mapHeight = std::atoi(rootElement->Attribute("height"));
	int mapPixelHeight = mapHeight * tileHeight;
	//CCLOG("tileW = %d , tileH = %d, mapW = %d , mapH = %d", tileWidth, tileHeight, mapWidth, mapHeight);

	// <objectgroup> を取得
	for (auto& objectLayerName : objectLayerNameList)
	{
		auto childElement = rootElement->FirstChildElement("objectgroup");
		while (childElement)
		{
			std::string attribute(childElement->Attribute("name"));
			if (attribute == objectLayerName)
			{
				auto objectElement = childElement->FirstChildElement("object");
				while (objectElement)
				{
					auto id = atoi(objectElement->Attribute("id"));
					float x = std::atof(objectElement->Attribute("x"));
					float y = std::atof(objectElement->Attribute("y"));

					// プロパティー取得
					std::map<std::string, std::string> properties;
					auto element = objectElement->FirstChildElement("properties");
					if (element)
					{
						auto p = element->FirstChildElement("property");
						while (p)
						{
							properties.insert(std::make_pair(p->Attribute("name"), p->Attribute("value")));
							p = p->NextSiblingElement("property");
						}
					}

					// 頂点データを取得して、シェイプ作成
					PhysicsShape* shape = nullptr;;
					// ポリライン
					if (objectElement->FirstChildElement("polyline"))
					{
						element = objectElement->FirstChildElement("polyline");
						auto pointsStr = element->Attribute("points");
						auto points = PointsSplit(pointsStr);
						for (auto& v : points)
						{
							v.x += x;
							v.y += y;
							v.y = mapPixelHeight - v.y;
						}
						shape = m_callBack->OnCreatePolylineShape(id, points, properties);
					}
					// 凸ポリゴン
					else if (objectElement->FirstChildElement("polygon"))
					{
						element = objectElement->FirstChildElement("polygon");
						auto pointsStr = element->Attribute("points");
						auto points = PointsSplit(pointsStr);
						for (auto& v : points)
						{
							v.x += x;
							v.y += y;
							v.y = mapPixelHeight - v.y;
						}
						shape = m_callBack->OnCreatePolygonShape(id, points, properties);
					}
					// 円
					else if (objectElement->FirstChildElement("ellipse"))
					{
						float radius = std::atof(objectElement->Attribute("width")) / 2;
						shape = m_callBack->OnCreateCircleShape(id, x + radius, mapPixelHeight - (y + radius), radius, properties);
					}
					// 四角
					else
					{
						float w = std::atof(objectElement->Attribute("width"));
						float h = std::atof(objectElement->Attribute("height"));
						std::vector<Vec2> points;
						Vec2 v;
						points.push_back(Vec2(x, mapPixelHeight - y));				// left top
						points.push_back(Vec2(x, mapPixelHeight - (y + h)));		// left bottom
						points.push_back(Vec2(x + w, mapPixelHeight - (y + h)));	// right bottom
						points.push_back(Vec2(x + w, mapPixelHeight - y));			// right top
						shape = m_callBack->OnCreatePolygonShape(id, points, properties);
					}

					if (shape)
					{
						body->addShape(shape);
					}

					objectElement = objectElement->NextSiblingElement("object");
				}
			}
			childElement = childElement->NextSiblingElement("objectgroup");
		}
	}

	return body;
}


void TmxBodyCreator::SetCreateShapeCallBack(TmxShapeCreateCallBack* callBack)
{
	if (m_callBack)
	{
		delete m_callBack;
		m_callBack = nullptr;
	}

	if (callBack)
	{
		m_callBack = callBack;
	}

	return;

}


TmxBodyCreator::~TmxBodyCreator()
{
	if (m_callBack) delete m_callBack;
	if (m_objectCreateCallBack) delete m_objectCreateCallBack;

}




TmxShapeCreateCallBack::TmxShapeCreateCallBack(const cocos2d::PhysicsMaterial& defaultMaterial)
	: m_defaultMaterial(defaultMaterial)
{
}

PhysicsShape* TmxShapeCreateCallBack::OnCreatePolylineShape(int id, std::vector<cocos2d::Vec2>& points, std::map<std::string, std::string>& properties)
{
	auto shape = PhysicsShapeEdgeChain::create(points.data(), points.size(), PropertyCheck(properties));
	return shape;
}



PhysicsShape* TmxShapeCreateCallBack::OnCreatePolygonShape(int id, std::vector<cocos2d::Vec2>& points, std::map<std::string, std::string>& properties)
{
	auto shape = PhysicsShapePolygon::create(points.data(), points.size(), PropertyCheck(properties));
	return shape;
}



PhysicsShape* TmxShapeCreateCallBack::OnCreateCircleShape(int id, float x, float y, float radius, std::map<std::string, std::string>& properties)
{
	auto shape = PhysicsShapeCircle::create(radius, PropertyCheck(properties), Vec2(x, y));
	return shape;
}


cocos2d::PhysicsMaterial TmxShapeCreateCallBack::PropertyCheck(std::map<std::string, std::string>& properties)
{
	auto material = m_defaultMaterial;

	if (properties.find("d") != properties.end()){
		material.density = std::atof(properties["d"].c_str());
	}

	if (properties.find("f") != properties.end()){
		material.friction = std::atof(properties["f"].c_str());
	}

	if (properties.find("r") != properties.end()){
		material.restitution = std::atof(properties["r"].c_str());
	}

	return material;
}


void TmxShapeCreateCallBack::GetCenter(const std::vector<cocos2d::Vec2>& inPoints, cocos2d::Vec2* outCenter, std::vector<cocos2d::Vec2>* outOffsetPoints)
{
	float left = inPoints[0].x;
	float right = left;
	float top = inPoints[0].y;
	float bottom = top;

	for (const auto& point : inPoints)
	{
		if (point.x < left) left = point.x;
		else if (right < point.x) right = point.x;
		if (point.y < bottom) bottom = point.y;
		else if (top < point.y) top = point.y;
	}

	outCenter->set((right - left) / 2 + left, (top - bottom) / 2 + bottom);

	for (const auto& point : inPoints)
	{
		outOffsetPoints->push_back(point - *outCenter);
	}
}


TmxShapeCreateCallBack::~TmxShapeCreateCallBack()
{
}









/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// タイルレイヤーからオブジェクト作成
//
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

int TmxBodyCreator::CreateObjects(const std::string& tmxStr, const std::string& tileLayerName, const std::string& tileSetName, TmxObjectCreateCallBack* callBack)
{
	if (callBack)
	{
		if (m_objectCreateCallBack)
		{
			delete m_objectCreateCallBack;
		}
		m_objectCreateCallBack = callBack;
	}

	if (!m_objectCreateCallBack)
	{
		m_objectCreateCallBack = new TmxObjectCreateCallBack(nullptr);
	}

	// XML パース
	tinyxml2::XMLDocument doc;
	doc.Parse(tmxStr.c_str(), tmxStr.size());
	auto rootElement = doc.RootElement();			// ルート取得

	int tileWidth = std::atoi(rootElement->Attribute("tilewidth"));
	int tileHeight = std::atoi(rootElement->Attribute("tileheight"));
	int mapWidth = std::atoi(rootElement->Attribute("width"));
	int mapHeight = std::atoi(rootElement->Attribute("height"));
	int mapPixelHeight = mapHeight * tileHeight;
	//CCLOG("tileW = %d , tileH = %d, mapW = %d , mapH = %d", tileWidth, tileHeight, mapWidth, mapHeight);


	int firstgid;

	// <tileset> を検索
	bool isFind = false;
	auto childElement = rootElement->FirstChildElement("tileset");
	while (childElement)
	{
		std::string attribute(childElement->Attribute("name"));
		if (attribute == tileSetName)
		{
			firstgid = std::atoi(childElement->Attribute("firstgid"));
			isFind = true;
			break;
		}
		childElement = childElement->NextSiblingElement("tileset");
	}

	if (!isFind)
	{
		return 0;
	}

	// <layer> を検索
	childElement = rootElement->FirstChildElement("layer");	// 最初の<layer>
	while (childElement)
	{
		std::string attribute(childElement->Attribute("name"));
		if (attribute == tileLayerName)
		{
			auto element = childElement->FirstChildElement("data");
			if (element)
			{
				std::string currentString(element->GetText());
				unsigned char *buffer = nullptr;
				auto len = base64Decode((unsigned char*)currentString.c_str(), (unsigned int)currentString.length(), &buffer);
				if (!buffer)
				{
					CCLOG("cocos2d: TiledMap: decode data error");
					CCASSERT(true, "");
				}
				{
					unsigned char *deflated = nullptr;
					ssize_t sizeHint = mapWidth * mapHeight * sizeof(unsigned int);
					ssize_t CC_UNUSED inflatedLen = ZipUtils::inflateMemoryWithHint(buffer, len, &deflated, sizeHint);
					CCASSERT(inflatedLen == sizeHint, "inflatedLen should be equal to sizeHint!");

					free(buffer);
					buffer = nullptr;

					if (!deflated)
					{
						CCLOG("cocos2d: TiledMap: inflate data error");
						CCASSERT(true, "");
					}

					auto tiles = reinterpret_cast<uint32_t*>(deflated);

					int i = 0;
					for (size_t y = 0; y < mapHeight; y++)
					{
						for (size_t x = 0; x < mapWidth; x++)
						{
							int id = tiles[i] - firstgid;
							if (id >= 0)
							{
								Vec2 pos(x*tileWidth + tileWidth / 2, mapPixelHeight - (y*tileHeight) - tileHeight / 2);
								m_objectCreateCallBack->OnCreateObject(id, pos);
							}
							i++;
						}
					}

					free(tiles);
					tiles = nullptr;
				}

			}
		}
		childElement = childElement->NextSiblingElement("layer");
	}
	return 0;
}




TmxObjectCreateCallBack::TmxObjectCreateCallBack(Node* nodeForAddChild)
	: m_nodeForAdd(nodeForAddChild)
{

}



void TmxObjectCreateCallBack::OnCreateObject(int tileID, cocos2d::Vec2 position)
{
	CCLOG("タイルID = %d , ポジション = %f , %f", tileID, position.x, position.y);
}

使い方

こんな感じです。

auto callBack = new MapGameShapeCallBack(PHYSICSBODY_MATERIAL_DEFAULT);   // TmxShapeCreateCallBackを継承したクラス
cocos2d::PhysicsBody* mapBody = TmxBodyCreator::CreateBody("test.tmx", "collision", callBack); // 単に物理ボディーを作るだけなら callBackはnullptr で ok。
mapBody->setCategoryBitmask(GROUP_WALL); 
mapBody->setContactTestBitmask(GROUP_PLAYER);                           // 接触判定
mapBody->setCollisionBitmask(GROUP_PLAYER | GROUP_BLOCK | GROUP_COIN);  // 衝突判定
mapBody->setDynamic(false);                      // 静的ボディー

自分仕様なので、分かりにくい部分もあるかと思います。
上にも書いてますが、単にボディーが欲しいだけなら
cocos2d::PhysicsBody* body = TmxBodyCreator::CreateBody("TMXファイル名", "エディタのオブジェクトレイヤー名");
でOKです。

「とニャンぽりん」をよろしくお願いします。

          Google Play で手に入れよう

f:id:shigebit:20161124233053j:plain