• 忍者无敌-实例讲解Cocos2d-x瓦片地图


    实例比较简单,如图所示,地图上有一个忍者精灵,玩家点击他周围的上、下、左、右,他能够向这个方向行走。当他遇到障碍物后是无法穿越的,障碍物是除了草地以为部分,包括了:树、山、河流等。



    忍者实例地图(TODO用这个精灵替换图中的)



    设计地图
    我们采用David Gervais提供开源免费瓦片集,下载的文件dg_grounds32.gif,gif文件格式会有一定的问题,我们需要转换为.jpg或.png文件。本实例中我是使用PhotoShop转换为dg_grounds32.jpg。
    David Gervais提供的瓦片集中的瓦片是32 x 32像素,我们创建的地图大小是32 x 32瓦片。我们先为地图添加普通层和对象层,普通层按照上图设计,对象层中添加几个矩形区域对象,这里不再赘述。这个阶段设计完成的结果如图所示。保存文件名为MiddleMap.tmx,保存目录Resourcesmap。


    设计地图



    程序中加载地图
    地图设计完成我们就可以在程序中加载地图了。下面我们再看看具体的程序代码,首先看一下HelloWorldScene.h文件,它的代码如下: 

    [html] view plaincopy
     
    1. #ifndef __HELLOWORLD_SCENE_H__  
    2. #define __HELLOWORLD_SCENE_H__  
    3.   
    4.   
    5. #include "cocos2d.h"  
    6.   
    7.   
    8. class HelloWorld : public cocos2d::Layer  
    9. {  
    10.     cocos2d::TMXTiledMap* _tileMap;                                     ①  
    11.      cocos2d::Sprite *_player;                                          ②  
    12. public:  
    13.     static cocos2d::Scene* createScene();  
    14.   
    15.   
    16.     virtual bool init();   
    17.   
    18.   
    19.     CREATE_FUNC(HelloWorld);  
    20. };  
    21.   
    22.   
    23. #endif // __HELLOWORLD_SCENE_H__  



    上述代码第①行代码是定义成员变量地图成员_tileMap,。第②行代码是定义精灵成员变量_player。
    HelloWorldScene的实现代码HelloWorldScene.ccp文件,它的HelloWorld::init()代码如下: 

    [html] view plaincopy
     
    1. bool HelloWorld::init()  
    2. {  
    3.     if ( !Layer::init() )  
    4.     {  
    5.         return false;  
    6.     }  
    7.   
    8.   
    9.     Size visibleSize = Director::getInstance()->getVisibleSize();  
    10.     Point origin = Director::getInstance()->getVisibleOrigin();  
    11.   
    12.   
    13.     _tileMap = TMXTiledMap::create("map/MiddleMap.tmx");                            ①  
    14.     addChild(tileMap,0,100);                                                ②  
    15.   
    16.   
    17.     TMXObjectGroup* group = _tileMap ->getObjectGroup("objects");                    ③  
    18.     ValueMap spawnPoint = group->getObject("ninja");                             ④  
    19.   
    20.   
    21.     float x = spawnPoint["x"].asFloat();                                        ⑤  
    22.     float y = spawnPoint["y"].asFloat();                                        ⑥  
    23.   
    24.   
    25.     _player = Sprite::create("ninja.png");                                      ⑦  
    26.     _player ->setPosition(Point(x,y));                                           ⑧  
    27.     addChild(_player, 2,200);  
    28.   
    29.   
    30.     return true;  
    31. }  



    上述第①代码是创建TMXTiledMap对象,地图文件是MiddleMap.tmx,map是资源目录Resources下的子目录。TMXTiledMap对象也是Node对象,需要通过第②行代码添加到当前场景中。
    第③行代码是通过对象层名objects获得层中对象组集合。第④行代码是从对象组中,通过对象名获得ninja对象信息,它的返回值类型是ValueMap,ValueMap是一种“键-值”对结构。第⑤行代码float x = spawnPoint["x"].asFloat()中的spawnPoint["x"]就是从按照x键取出它的值,即x轴坐标。spawnPoint["x"]的返回值是Value类型,还需要使用asFloat()函数转换为基本的int类型。类似地,第⑥行代码是获得y轴坐标。
    第⑦行代码是创建精灵_player,第⑧行代码是设置精灵位置,这个位置是从对象层中ninja对象信息获取的。


    加载地图(TODO重新截取,或者换精灵)



    移动精灵
    移动精灵是通过触摸事件实现移动的,需要在层中进行事件处理,我们需要在层中重写如下函数:
    bool onTouchBegan(Touch * touch, Event* unused_event)
    void onTouchEnded(Touch * touch,Event* unused_event)
    void onTouchMoved(Touch * touch,Event* unused_event)


    下面我们再看看具体的程序代码,首先看一下HelloWorldScene.h文件,它的代码如下: 

    [html] view plaincopy
     
    1. #ifndef __HELLOWORLD_SCENE_H__  
    2. #define __HELLOWORLD_SCENE_H__  
    3.   
    4.   
    5. #include "cocos2d.h"  
    6.   
    7.   
    8. class HelloWorld : public cocos2d::Layer  
    9. {  
    10.     cocos2d::TMXTiledMap* _tileMap;                                       
    11.      cocos2d::Sprite *_player;                                            
    12. public:  
    13.     static cocos2d::Scene* createScene();  
    14.   
    15.   
    16.     virtual bool init();   
    17.   
    18.   
    19.     virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);            ①  
    20.     virtual void onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event);  
    21.     virtual void onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event);            ②  
    22.   
    23.   
    24.     CREATE_FUNC(HelloWorld);  
    25. };  
    26.   
    27.   
    28. #endif // __HELLOWORLD_SCENE_H__  



    上述代码第①~②行代码是声明触摸事件函数。HelloWorldScene的实现代码HelloWorldScene.ccp文件,它的HelloWorld::init()代码如下:

    [html] view plaincopy
     
    1. bool HelloWorld::init()  
    2. {  
    3.     … …  
    4.     setTouchEnabled(true);  
    5.     //设置为单点触摸  
    6.    setTouchMode(Touch::DispatchMode::ONE_BY_ONE);  
    7.   
    8.   
    9.     return true;  
    10. }  



    上述代码setTouchEnabled(true)是使层开始触摸事件支持。代码setTouchMode(Touch::DispatchMode::ONE_BY_ONE)是设置触摸模式为单点触摸。
    HelloWorldScene.ccp文件的触摸事件函数代码如下:

    [html] view plaincopy
     
    1. bool HelloWorld::onTouchBegan(Touch* touch, Event* event)  
    2. {  
    3.     log("onTouchBegan");  
    4.     return true;  
    5. }  
    6.   
    7.   
    8. void HelloWorld::onTouchMoved(Touch *touch, Event *event)  
    9. {  
    10.     log("onTouchMoved");  
    11. }  
    12.   
    13.   
    14. void HelloWorld::onTouchEnded(Touch *touch, Event *event)  
    15. {  
    16.     log("onTouchEnded");  
    17.           
    18.     Point touchLocation = touch->getLocation();                                  ①  
    19.   
    20.   
    21.     Point playerPos = _player->getPosition();                                    ②  
    22.     Point diff = touchLocation - playerPos;                                 ③  
    23.       
    24.     if (abs(diff.x) > abs(diff.y)) {                                         ④  
    25.         if (diff.x > 0) {                                                    ⑤  
    26.             playerPos.x += _tileMap->getTileSize().width;  
    27.             _player->runAction(FlipX::create(false));                                ⑥  
    28.         } else {  
    29.             playerPos.x -= _tileMap->getTileSize().width;  
    30.             _player->runAction(FlipX::create(true));                             ⑦  
    31.         }  
    32.     } else {  
    33.         if (diff.y > 0) {                                                    ⑧  
    34.             playerPos.y += _tileMap->getTileSize().height;  
    35.         } else {  
    36.             playerPos.y -= _tileMap->getTileSize().height;  
    37.         }  
    38.     }  
    39.     _player->setPosition(playerPos);                                         ⑨  
    40. }  



    上述第①代码touch->getLocation()是获得在Open GL坐标,Open GL坐标的坐标原点是左下角,touch对象封装了触摸点对象。第②行代码_player->getPosition()是获得精灵的位置。
    第③行代码是获得触摸点与精灵位置之差。第④行代码是比较一下触摸点与精灵位置之差,是y轴之差大还是x轴之差大,那个轴之差大就沿着那个轴移动,(abs(diff.x) > abs(diff.y))情况是x轴之差大,否则是y轴之差大。第⑤行代码,diff.x > 0情况是沿着x轴正方向移动,否则情况是沿着x轴负方向移动。第⑥行代码_player->runAction(FlipX::create(false))是把精灵翻转回原始状态。第⑦行代码_player->runAction(FlipX::create(true))是把精灵是沿着y轴水平翻转。
    第⑧行代码是沿着y轴移动,diff.y > 0是沿着y轴正方向移动,否则是沿着y轴负方向移动。
    第⑨行代码是重新设置精灵坐标。


    检测碰撞
    到目前为止我们游戏中的精灵,可以穿越任何障碍物。为了能够检测到精灵是否碰撞到障碍物,我们需要再添加一个普通层(collidable),它的目的不是现实地图,而是检测碰撞。我们在检测碰撞层中使用瓦片覆盖background层中的障碍物之上,如图所示。


    检测碰撞层

    检测碰撞层中的瓦片集可以是任何的满足格式要求的图片文件。在本例中我们使用一个32 x 32像素单色jpg图片文件collidable_tiles. jpg,它的大小与瓦片大小一样,也就是说这个瓦片集中只有一个瓦片。导入这个瓦片集到地图后,我们需要为瓦片添加一个自定义属性,瓦片本身也有一些属性,例如:坐标属性x和y。
    我们要添加的属性名为“Collidable”,属性值为“true”。添加过程如图所示,首先,选择collidable_tiles瓦片集中的要设置属性的瓦片。然后,点击属性视图中左下角“+”按钮,添加自定义属性,这时候会弹出一个对话框,我们在对话框中输入自定义属性名“Collidable”,点击确定按钮。这时候回到属性视图,Collidable在属性后面是可以输入内容的,这里我们输入“true”。


    添加检测碰撞属性

    地图修改完成后,我们还要修改代码。首选在头文件HelloWorldScene.h中添加一个成员变量和两个函数的声明。

    [html] view plaincopy
     
    1. #ifndef __HELLOWORLD_SCENE_H__  
    2. #define __HELLOWORLD_SCENE_H__  
    3.   
    4.   
    5. #include "cocos2d.h"  
    6. #include "SimpleAudioEngine.h"  
    7.   
    8.   
    9.   
    10.   
    11. class HelloWorld : public cocos2d::Layer  
    12. {  
    13.     cocos2d::TMXTiledMap* _tileMap;  
    14.     cocos2d::TMXLayer* _collidable;                                         ①  
    15.    cocos2d::Sprite *_player;  
    16. public:  
    17.     static cocos2d::Scene* createScene();  
    18.     virtual bool init();   
    19.   
    20.   
    21.     virtual bool onTouchBegan(cocos2d::Touch* touch, cocos2d::Event* event);  
    22.     virtual void onTouchMoved(cocos2d::Touch *touch, cocos2d::Event *event);  
    23.     virtual void onTouchEnded(cocos2d::Touch *touch, cocos2d::Event *event);  
    24.       
    25.     void setPlayerPosition(cocos2d::Point position);                                ②  
    26. cocos2d::Point tileCoordFromPosition(cocos2d::Point position);                  ③  
    27.   
    28.   
    29.     CREATE_FUNC(HelloWorld);  
    30. };  
    31.   
    32.   
    33. #endif // __HELLOWORLD_SCENE_H__  



    上述代码第①行是声明一个TMXLayer类型的成员变量,它是用来保存地图碰撞层对象。第②行代码setPlayerPosition函数是重新设置精灵的位置,在这个函数中可以检测精灵是否与障碍物碰撞。第③行代码tileCoordFromPosition函数是把像素坐标点转换为地图瓦片坐标点。
    修改HelloWorldScene.cpp中的HelloWorld::init()代码如下:

    [html] view plaincopy
     
    1. bool HelloWorld::init()  
    2. {  
    3.     if ( !Layer::init() )  
    4.     {  
    5.         return false;  
    6.     }  
    7.   
    8.   
    9.     Size visibleSize = Director::getInstance()->getVisibleSize();  
    10.     Point origin = Director::getInstance()->getVisibleOrigin();  
    11.   
    12.   
    13.     _tileMap = TMXTiledMap::create("map/MiddleMap.tmx");  
    14.     addChild(_tileMap,0,100);  
    15.   
    16.   
    17.     TMXObjectGroup* group = _tileMap->getObjectGroup("objects");  
    18.     ValueMap spawnPoint = group->getObject("ninja");  
    19.   
    20.   
    21.     float x = spawnPoint["x"].asFloat();  
    22.     float y = spawnPoint["y"].asFloat();  
    23.   
    24.   
    25.     _player = Sprite::create("ninja.png");  
    26.     _player->setPosition(Point(x,y));  
    27.     addChild(_player, 2, 200);  
    28.   
    29.   
    30.     _collidable = _tileMap->getLayer("collidable");                                  ①  
    31.    _collidable->setVisible(false);                                           ②  
    32.   
    33.   
    34.     setTouchEnabled(true);  
    35.     //设置为单点触摸  
    36.     setTouchMode(Touch::DispatchMode::ONE_BY_ONE);  
    37.   
    38.   
    39.     return true;  
    40. }  



    我们需要在HelloWorld::init()函数中创建并初始化碰撞层。第①行代码是_collidable = _tileMap->getLayer("collidable")是通过层名字collidable创建层,第②行代码_collidable->setVisible(false)是设置层隐藏,我们要么在这里隐藏的,要么在地图编辑的时候,将该层透明,如图所示,在层视图中选择层,然后通过滑动上面的透明度滑块来改变层的透明度,在本例中是需要将透明度设置为0,那么_collidable->setVisible(false)语句就不再需要了。
    注意  在地图编辑器中,设置层的透明度为0与设置层隐藏,在地图上看起来一样,但是有着本质的区别,设置层隐藏是无法通过_collidable = _tileMap->getLayer("collidable")语句访问的。




    设置层透明度



    我们在前面也介绍过,collidable层不是用来显示地图内容的,而是用来检测碰撞的。修改HelloWorldScene.cpp中的

    [html] view plaincopy
     
    1. HelloWorld::onTouchEnded代码如下:  
    2. void HelloWorld::onTouchEnded(Touch *touch, Event *event)  
    3. {  
    4.     log("onTouchEnded");  
    5.   
    6.   
    7.     //获得在OpenGL坐标  
    8.     Point touchLocation = touch->getLocation();        
    9.   
    10.   
    11.     Point playerPos = _player->getPosition();      
    12.     Point diff = touchLocation - playerPos;   
    13.   
    14.   
    15.     if (abs(diff.x) > abs(diff.y)) {       
    16.         if (diff.x > 0) {  
    17.             playerPos.x += _tileMap->getTileSize().width;  
    18.             _player->runAction(FlipX::create(false));  
    19.         } else {  
    20.             playerPos.x -= _tileMap->getTileSize().width;  
    21.             _player->runAction(FlipX::create(true));  
    22.         }  
    23.     } else {  
    24.         if (diff.y > 0) {  
    25.             playerPos.y += _tileMap->getTileSize().height;  
    26.         } else {  
    27.             playerPos.y -= _tileMap->getTileSize().height;  
    28.         }  
    29.     }  
    30.     this->setPlayerPosition(playerPos);                                      ①  
    31. }  



    HelloWorld::onTouchEnded有一些变化,第①行代码this->setPlayerPosition(playerPos)替换了_player->setPosition(playerPos),setPlayerPosition是我们自定的函数,这个函数的作用是移动精灵和检测碰撞。
    setPlayerPosition代码如下: 

    [html] view plaincopy
     
    1. void HelloWorld::setPlayerPosition(Point position)                                
    2. {  
    3.     //从像素点坐标转化为瓦片坐标  
    4.     Point tileCoord =  this->tileCoordFromPosition(position);                            ①  
    5.     //获得瓦片的GID  
    6.     int tileGid = _collidable->getTileGIDAt(tileCoord);                              ②  
    7.   
    8.   
    9.     if (tileGid > 0) {                                                       ③  
    10.         Value prop = _tileMap->getPropertiesForGID(tileGid);                         ④  
    11.         ValueMap propValueMap = prop.asValueMap();                              ⑤  
    12.   
    13.   
    14.         std::string collision = propValueMap["Collidable"].asString();                      ⑥  
    15.           
    16.         if (collision == "true") { //碰撞检测成功                                 ⑦  
    17.             CocosDenshion::SimpleAudioEngine::getInstance()->playEffect("empty.wav");        ⑧  
    18.             return ;  
    19.         }  
    20.     }  
    21.     _player->setPosition(position);  
    22. }  



    上述代码第①行this->tileCoordFromPosition(position)是调用函数,实现从像素点坐标转化为瓦片坐标。第②行代码_collidable->getTileGIDAt(tileCoord)是通过瓦片坐标获得GID值。
    第③行代码tileGid > 0可以判断瓦片是否存在,tileGid == 0是瓦片不存在情况。第④行代码_tileMap->getPropertiesForGID(tileGid)是通过地图对象的getPropertiesForGID返回,它的返回值是Value类型。
    由于Value类型可以代表很多类型。因此第⑤行代码prop.asValueMap()是将Value类型转换成为ValueMap,ValueMap类是“键-值”对。第⑥行代码propValueMap["Collidable"].asString()是将propValueMap变量中的Collidable属性取出来,asString()函数可以将Value类型转换成为std::string类型。第⑦行代码collision == "true"是碰撞检测成功情况。第⑧行代码是碰撞检测成功情况下处理,在本例中我们是播放一下音效。
    tileCoordFromPosition代码如下: 

    [html] view plaincopy
     
    1. Point HelloWorld::tileCoordFromPosition(Point pos)                                
    2. {  
    3.     int x = pos.x / _tileMap->getTileSize().width;                                   ①  
    4.     int y = ((_tileMap->getMapSize().height * _tileMap->getTileSize().height) - pos.y) /  
    5.                                                      _tileMap->getTileSize().height;         ②  
    6.     return Point(x,y);  
    7. }  



    在该函数中第①行代码pos.x / _tileMap->getTileSize().width是获得x轴瓦片坐标(单位是瓦片数),pos.x是触摸点x轴坐标(单位是像素),_tileMap->getTileSize().width是每个瓦片的宽度,单位是像素。代码第②行是获得y轴瓦片坐标(单位是瓦片数),这个计算有点麻烦,瓦片坐标的原点在左上角,而触摸点使用的坐标是Open GL坐标,坐标原点在左下角,表达式(_tileMap->getMapSize().height * _tileMap->getTileSize().height) - pos.y)是反转坐标轴,结果除以每个瓦片的高度_tileMap->getTileSize().height,就得到y轴瓦片坐标了。


    滚动地图
    由于地图比屏幕要大,当我们移动精灵到屏幕的边缘时候,那些处于屏幕之外的地图部分,应该滚动到屏幕之内。这些需要我们重新设置视点(屏幕的中心点),使得精灵一直处于屏幕的中心。但是精灵太靠近地图的边界时候,他有可能不在屏幕的中心。精灵与地图的边界距离的规定是,左右边界距离不小于屏幕宽度的一半,否则会出现图所示的左右黑边问题。上下边界距离不小于屏幕高度的一半,否则也会在上下黑边问题。
    重新设置视点实现的方式很多,本章中采用移动地图位置实现这种效果。
    我们在HelloWorldScene.cpp中再添加一个函数setViewpointCenter,添加后代码如下: 

    [html] view plaincopy
     
    1. void HelloWorld::setViewpointCenter(Point position)  
    2. {  
    3.     Size visibleSize = Director::getInstance()->getVisibleSize();  
    4. int x = MAX(position.x, visibleSize.width / 2);                             ①  
    5.     int y = MAX(position.y, visibleSize.height / 2);                                ②    
    6.     x = MIN(x, (_tileMap->getMapSize().width * _tileMap->getTileSize().width)  
    7.             - visibleSize.width / 2);                                           ③  
    8.     y = MIN(y, (_tileMap->getMapSize().height * _tileMap->getTileSize().height)  
    9.             - visibleSize.height/2);                                            ④  
    10.   
    11.   
    12.     //屏幕中心点  
    13.     Point pointA = Point(visibleSize.width/2, visibleSize.height/2);                    ⑤  
    14.     //使精灵处于屏幕中心,移动地图目标位置  
    15.     Point pointB = Point(x, y);                                             ⑥  
    16.         log("目标位置 (%f ,%f) ",pointB.x,pointB.y);  
    17.   
    18.   
    19.     //地图移动偏移量  
    20. Point offset =pointA - pointB;                                          ⑦  
    21.   
    22.   
    23.     log("offset (%f ,%f) ",offset.x, offset.y);  
    24.     this->setPosition(offset);                                               ⑧  
    25. }  



    在上述代码①~④是保障精灵移动到地图边界时候不会再移动,防止屏幕超出地图之外,这一点非常重要。其中第①行代码是防止屏幕左边超出地图之外,MAX(position.x, visibleSize.width / 2)语句表示当position.x < visibleSize.width / 2情况下,x轴坐标始终是visibleSize.width / 2,即精灵不再向左移动。第②行代码与第①行代码类似,不再解释。第③行代码是防止屏幕右边超出地图之外,MIN(x, (_tileMap->getMapSize().width * _tileMap->getTileSize().width) - visibleSize.width / 2)语句表示当x > (_tileMap->getMapSize().width * _tileMap->getTileSize().width) - visibleSize.width / 2时候,x轴坐标始终是(_tileMap->getMapSize().width * _tileMap->getTileSize().width) - visibleSize.width / 2表达式计算的结果。
    提示visibleSize 是表示屏幕的宽度,_tileMap->getMapSize().width * _tileMap->getTileSize().width) - visibleSize.width / 2表达式计算的是地图的宽度减去屏幕宽度的一半。
    第④行代码与第③行代码类似,不再解释。

    屏幕左边超出地图


    屏幕右边超出地图

    代码⑤~⑧行实现了移动地图效果,使得精灵一直处于屏幕的中心。A点是目前屏幕的中心点,也是精灵的位置。玩家触摸B点,精灵会向B点移动。为了让精灵保持在屏幕中心,地图一定要向相反的方向移动。
    第⑤行代码Point pointA = Point(visibleSize.width/2, visibleSize.height/2)是获取屏幕中心点(A点)。第⑥行代码是获取移动地图目标位置(B点)。第⑦行代码是计算A点与B点两者之差,这个差值就是地图要移动的距离。由于精灵的世界坐标就是地图层的模型坐标,即精灵的坐标原点是地图的左下角,因此第⑧行代码this->setPosition(offset)是将地图坐标原点移动offset位置。

    移动地图

    更多内容请关注国内第一本Swift图书《Swift开发指南》
    本书交流讨论网站:http://www.51work6.com/swift.php
    欢迎加入Swift技术讨论群:362298485

    欢迎关注智捷iOS课堂微信公共平台

  • 相关阅读:
    C# 如何定义让PropertyGrid控件显示[...]按钮,并且点击后以下拉框形式显示自定义控件编辑属性值
    “Word自动更改后的内容保存到通用文档模板上。是否加载该模板?“的解决办法
    Web服务器之iis,apache,tomcat三者之间的比较
    [转]C#如何把文件夹压缩打包然后下载
    [转]C#压缩打包文件
    C#——Marshal.StructureToPtr方法简介
    [Android Pro] 内容提供者ContentProvider的基本使用
    [Linux] awk命令详解
    [Linux] AWK命令详解(大全)
    [Android UI] ProgressBar自定义
  • 原文地址:https://www.cnblogs.com/iOS-Blog/p/3980654.html
Copyright © 2020-2023  润新知