TileMap,瓦片地图,应该算是最简单,最原始的地图了。如果你还不知道什么是瓦片地图,请点这里。假设现在你已经对瓦片地图有了最基本的了解,下面通过cocos2d-x自带的testapp了解TileMapEditTest。(ps:我看的是C++的code)
TestApp跑起来之后选择“TileMapTest”,进入“TileMapTest”之后又有N多相关的demo,一直翻到“TileMapEditTest”。
首先我们来看构造函数:
TileMapEditTest::TileMapEditTest()
{
CCTileMapAtlas* map = CCTileMapAtlas::create(s_TilesPng, s_LevelMapTga, 16, 16);
// Create an Aliased Atlas
map->getTexture()->setAliasTexParameters();
CCSize CC_UNUSED s = map->getContentSize();
CCLOG("ContentSize: %f, %f", s.width,s.height);
// If you are not going to use the Map, you can free it now
// [tilemap releaseMap);
// And if you are going to use, it you can access the data with:
schedule(schedule_selector(TileMapEditTest::updateMap), 0.2f);
addChild(map, 0, kTagTileMap);
map->setAnchorPoint( ccp(0, 0) );
map->setPosition( ccp(-20,-200) );
}
我们看到第一句就创建了一个map,然后向map addchild到当前layer,再就是设置一堆属性。
我们继续跟踪 CCTileMapAtlas::create
/** creates a CCTileMap with a tile file (atlas) with a map file and the width and height of each tile in points.
The tile file will be loaded using the TextureMgr.
*/
CCTileMapAtlas * CCTileMapAtlas::create(const char *tile, const char *mapFile, int tileWidth, int tileHeight)
{
CCTileMapAtlas *pRet = new CCTileMapAtlas();
if (pRet->initWithTileFile(tile, mapFile, tileWidth, tileHeight))
{
pRet->autorelease();
return pRet;
}
CC_SAFE_DELETE(pRet);
return NULL;
}
四个参数,tile是瓦片路径,其实是一张png的图片(位于resource下面的TileMaps/tiles.png),之后我们看到map上的东西都是从此图片上分割出来的。
mapFile,是tga格式的图片(位于resource下面的TileMaps/testmap.tga),用于构造地图,也就是说mapFile的像素大小决定了最终地图的基础大小。
tileWidth tileHeight,将tile文件按照从左到右,从上到下的优先级分割成一个一个小tile。
我们继续向下看CCTileMapAtlas的initWithTileFile函数:
bool CCTileMapAtlas::initWithTileFile(const char *tile, const char *mapFile, int tileWidth, int tileHeight)
{
this->loadTGAfile(mapFile);
this->calculateItemsToRender();
if( CCAtlasNode::initWithTileFile(tile, tileWidth, tileHeight, m_nItemsToRender) )
{
m_pPosToAtlasIndex = new CCDictionary();
this->updateAtlasValues();
this->setContentSize(CCSizeMake((float)(m_pTGAInfo->width*m_uItemWidth),
(float)(m_pTGAInfo->height*m_uItemHeight)));
return true;
}
return false;
}
第一句,loadTGAfile,所以此时传进来的mapFile一定个要是TGA格式的文件。
第二句calculateItemsToRender,我们进去这个函数瞄一眼,
void CCTileMapAtlas::calculateItemsToRender()
{
CCAssert( m_pTGAInfo != NULL, "tgaInfo must be non-nil");
m_nItemsToRender = 0;
for(int x=0;x < m_pTGAInfo->width; x++ )
{
for( int y=0; y < m_pTGAInfo->height; y++ )
{
ccColor3B *ptr = (ccColor3B*) m_pTGAInfo->imageData;
ccColor3B value = ptr[x + y * m_pTGAInfo->width];
if( value.r )
{
++m_nItemsToRender;
}
}
}
}
一个遍历,乏善可陈。有意思的时这一句 if( value.r ),如果此像素的r值不为0,则 m_nItemsToRender ++,所以我推断map上的图像只跟tga文件像素值得R(红色)分量有关,最后实时证明确实只跟r相关。退出继续向下走。
if( CCAtlasNode::initWithTileFile(tile, tileWidth, tileHeight, m_nItemsToRender) )
只有最后一个参数变了,是在calculateItemsToRender中计算出来的。
CCAtlasNode::initWithTileFile我们就不继续深入看这个函数了,有兴趣的朋友自己去八一八。这个函数里面做三件事:
1.将tile处理成OpenGL的一张纹理;
2.创建m_nItemsToRender个宽solid模型;
3.将tilewidth和tileHeight分别赋值给m_uItemWidth和m_uItemHeight,这两个东西后面会用到。
如果initWithTileFile没问题,下载就会进入if,
首先通过m_pPosToAtlasIndex = new CCDictionary();创建一个字典,通过hash表对资源的管理,方便资源查找,点击这里深入了解。
下一步会调用CCTileMapAtlas的updateAtlasValues,进去看看。
void CCTileMapAtlas::updateAtlasValues()
{
CCAssert( m_pTGAInfo != NULL, "tgaInfo must be non-nil");
int total = 0;
for(int x=0;x < m_pTGAInfo->width; x++ )
{
for( int y=0; y < m_pTGAInfo->height; y++ )
{
if( total < m_nItemsToRender )
{
ccColor3B *ptr = (ccColor3B*) m_pTGAInfo->imageData;
ccColor3B value = ptr[x + y * m_pTGAInfo->width];
if( value.r != 0 )
{
this->updateAtlasValueAt(ccp(x,y), value, total);
CCString *key = CCString::createWithFormat("%d,%d", x,y);
CCInteger *num = CCInteger::create(total);
m_pPosToAtlasIndex->setObject(num, key->getCString());
total++;
}
}
}
}
}
同样先是遍历,遍历到一个r不为0的value之后会调用
this->updateAtlasValueAt(ccp(x,y), value, total);
先看下面的,一会回过头再看此函数。
CCString *key = CCString::createWithFormat("%d,%d", x,y);
CCInteger *num = CCInteger::create(total);
m_pPosToAtlasIndex->setObject(num, key->getCString());
这三句是将渲染的index和渲染的坐标组成一个hash键值对,存入上一步创建的字典中。
好,我们回头看updateAtlasValueAt
void CCTileMapAtlas::updateAtlasValueAt(const CCPoint& pos, const ccColor3B& value, unsigned int index)
{
ccV3F_C4B_T2F_Quad quad;
int x = pos.x;
int y = pos.y;
float row = (float) (value.r % m_uItemsPerRow);
float col = (float) (value.r / m_uItemsPerRow);
float textureWide = (float) (m_pTextureAtlas->getTexture()->getPixelsWide());
float textureHigh = (float) (m_pTextureAtlas->getTexture()->getPixelsHigh());
float itemWidthInPixels = m_uItemWidth * CC_CONTENT_SCALE_FACTOR();
float itemHeightInPixels = m_uItemHeight * CC_CONTENT_SCALE_FACTOR();
#if CC_FIX_ARTIFACTS_BY_STRECHING_TEXEL
float left = (2 * row * itemWidthInPixels + 1) / (2 * textureWide);
float right = left + (itemWidthInPixels * 2 - 2) / (2 * textureWide);
float top = (2 * col * itemHeightInPixels + 1) / (2 * textureHigh);
float bottom = top + (itemHeightInPixels * 2 - 2) / (2 * textureHigh);
#else
float left = (row * itemWidthInPixels) / textureWide;
float right = left + itemWidthInPixels / textureWide;
float top = (col * itemHeightInPixels) / textureHigh;
float bottom = top + itemHeightInPixels / textureHigh;
#endif
quad.tl.texCoords.u = left;
quad.tl.texCoords.v = top;
quad.tr.texCoords.u = right;
quad.tr.texCoords.v = top;
quad.bl.texCoords.u = left;
quad.bl.texCoords.v = bottom;
quad.br.texCoords.u = right;
quad.br.texCoords.v = bottom;
quad.bl.vertices.x = (float) (x * m_uItemWidth);
quad.bl.vertices.y = (float) (y * m_uItemHeight);
quad.bl.vertices.z = 0.0f;
quad.br.vertices.x = (float)(x * m_uItemWidth + m_uItemWidth);
quad.br.vertices.y = (float)(y * m_uItemHeight);
quad.br.vertices.z = 0.0f;
quad.tl.vertices.x = (float)(x * m_uItemWidth);
quad.tl.vertices.y = (float)(y * m_uItemHeight + m_uItemHeight);
quad.tl.vertices.z = 0.0f;
quad.tr.vertices.x = (float)(x * m_uItemWidth + m_uItemWidth);
quad.tr.vertices.y = (float)(y * m_uItemHeight + m_uItemHeight);
quad.tr.vertices.z = 0.0f;
ccColor4B color = { _displayedColor.r, _displayedColor.g, _displayedColor.b, _displayedOpacity };
quad.tr.colors = color;
quad.tl.colors = color;
quad.br.colors = color;
quad.bl.colors = color;
m_pTextureAtlas->updateQuad(&quad, index);
}
稍微有点长,最关键的是对quad的赋值,
先看下quad这个结构体:
typedef struct _ccV3F_C4B_T2F_Quad
{
//! top left
ccV3F_C4B_T2F tl;
//! bottom left
ccV3F_C4B_T2F bl;
//! top right
ccV3F_C4B_T2F tr;
//! bottom right
ccV3F_C4B_T2F br;
} ccV3F_C4B_T2F_Quad;
原来是一个矩形的四个顶点。回头继续
先是对quad四个顶点纹理坐标texCoords进行赋值;
再对四个顶点的的顶点坐标赋值,可以看到此矩形大小刚好是m_uItemWidth*m_uItemHeight,和CCTileMapAtlas::create最后两个参数保持一致。
最后设置顶点的默认颜色(没有贴图时候的颜色,如果有贴图这个颜色可能起作用,也可能不起,具体看给OpenGL设置的混合方式)。
到此,我们既有了模型,也有了纹理,刚刚又设置了纹理坐标,我们的工作全完成!只等OpenGL给我们渲染了。
向后退,一直到CCTileMapAtlas::initWithTileFile
updateAtlasValues完之后会设置map的大小,
his->setContentSize(CCSizeMake((float)(m_pTGAInfo->width*m_uItemWidth),
(float)(m_pTGAInfo->height*m_uItemHeight)));
在前面我讲了,mapFile只是决定map的基础像素大小,“基础”二字还特意标红。map实际的大小是tga和tileWidth/tileHeight共同决定的。
好,再继续探栈,一直到TileMapEditTest的构造函数
CCTileMapAtlas* map = CCTileMapAtlas::create(s_TilesPng, s_LevelMapTga, 16, 16);
// Create an Aliased Atlas
map->getTexture()->setAliasTexParameters();
CCSize CC_UNUSED s = map->getContentSize();
CCLOG("ContentSize: %f, %f", s.width,s.height);
// If you are not going to use the Map, you can free it now
// [tilemap releaseMap);
// And if you are going to use, it you can access the data with:
schedule(schedule_selector(TileMapEditTest::updateMap), 0.2f);
create完之后会调用map的setAliasTexParameters,此方法用来消除tile之间的缝隙。
然后创建了一个schedule,进去updataMap看看,
void TileMapEditTest::updateMap(float dt)
{
// IMPORTANT
// The only limitation is that you cannot change an empty, or assign an empty tile to a tile
// The value 0 not rendered so don't assign or change a tile with value 0
CCTileMapAtlas* tilemap = (CCTileMapAtlas*) getChildByTag(kTagTileMap);
// NEW since v0.7
ccColor3B c = tilemap->tileAt(ccp(1,0));
c.r++;
c.r %= 50;
if( c.r==0)
c.r=1;
// NEW since v0.7
tilemap->setTile(c, ccp(1,0) );
}
首先是回去map对象,在通过map的方法tileat取回(1,0)位置的color,再把color的r值进行处理之后通过setTile设置会(1,0)的位置。
这一段就完成了map的(1,0)图像不断变化的一个动画。
至此,我们的初窥之路基本上走完了,总结起来就是:
1.根据tga创建map;
2.将tile文件按照一定大小进行分割
3.将分割好的tile按照map的各个像素的r值贴上去。
注意两点:
1.map pix 的value的r一定大于0才会贴图,所以在制造tile文件的时候第一个单位的tile(index为0)是不会贴到map上的,一般我们留空
2.如果分割的tile不够,比如我们需要第100个tile,但是tile文件按照tileWidth和tileHeight最多只能分割50个,那么,超过50的我们就用index为50的图片。
下一篇我们会写一个基于tilemap的小游戏。