cocos2d-x 如何制作一个类马里奥的横版平台动作游戏 1 献给所有
本文翻译自国外著名IOS源码教学商业网站raywenderlich的
IOS Game Start Kits三件套之一的Platformer Game/平台动作游戏的前奏曲,另一个是Beat'Em up Game/横版格斗游戏,作者是国外著名游戏开发专家Jake Gundersen,曾参与开发过SFC时代的洛克人X系列。 原文网址: http://www.raywenderlich.com/15230/how-to-make-a-platform-game-like-super-mario-brothers-part-1 开篇之前先怀旧一番吧! 还记得超级马里奥的青青草地蓝天白云吗?还记得曾让人爱恨交加又不屈不挠让人不忍放弃的洛克人ZERO吗,我们燃起小宇宙一招龙炎刃击败最终Boss的场面是曾多么热血澎湃!这些感动一代人的游戏陪伴了我们80后儿时整整一个时代。横版平台动作游戏,作为FC时代最早的游戏类型,以美丽精致的游戏画面,曲折有趣的关卡设计,丰富流畅的动作设定深深地俘获住了众玩家的芳心,让玩家过足了在游戏世界里冒险探索的瘾。这个悬崖怎么跳过去?这个机关怎么破解?这个洞里是不是还有隐藏宝物?下一关是啥样的?最终Boss到底是谁该怎么打?无数的问题让玩家欲罢不能。现在IOS时代这种游戏的光环已经渐渐褪散了,代之以没剧情没关卡只有背景无限滚动的无脑跑酷模式。这样的快餐游戏我已经不想再说什么了,一句话,任何所谓的创新类型都无法超越经典,大家是不是已经燃起想学习的Cosmos了?那么请看下面吧! 创建一个简单易用的2D物理引擎: 假设大家对Tiled map editor工具已经非常熟悉,让我们打开下载的资源目录里的level1.tmx,就会看到: 你会发现地图有三个层,分别是:
class GameLevelLayer : public cocos2d::CCLayer { public: GameLevelLayer(void); ~GameLevelLayer(void); CREATE_FUNC(GameLevelLayer); bool init(); static cocos2d::CCScene* scene(); protected: cocos2d::CCTMXTiledMap *map; };下面我们在init里加入地图,如下: //加载一个蓝色背景当装饰 CCLayerColor *blueSky = CCLayerColor::create(ccc4(100,100,250,255)); this->addChild(blueSky); //加载地图 _map = CCTMXTiledMap::create("level1.tmx"); this->addChild(_map); 我们先加了一个带有蓝色背景的CCLayerColor作为蓝天,下面两行就是大家熟知的加载地图了。 CCScene* GameLevelLayer::scene() { CCScene *scene = CCScene::create(); if(!scene) return NULL; GameLevelLayer *layer = GameLevelLayer::create(); scene->addChild(layer); return scene; }好了现在运行游戏可以看到地图了,接着再加我们的主角考拉,在GameLevelLayer.h里弄一个前向声明class Player;然后加一个成员变量Player* _player; 在GameLevelLayer.cpp里加上主角考拉: //地图上加载主角考拉熊 _player = Player::create("koalio_stand.png"); _player->setPosition(ccp(100,50)); _map->addChild(_player,15);类Player的头文件Player.h class Player : public cocos2d::CCSprite { public: Player(void); ~Player(void); //以图片初始化 virtual bool initWithFile(const char *pszFilename); static Player* create(const char *pszFileName); void update(float delta); }其实就是用一张纹理贴图初始化,给了考拉一个zorder,使它能显示在地图之上,player.cpp的关键代码如下: bool Player::initWithFile(const char *pszFilename) { CCAssert(pszFilename != NULL,"Invalid filename for Player"); //作些自己的初始化 bool bRet = CCSprite::initWithFile(pszFilename); _velocity = ccp(0.f,0.f); //速度初始化 return bRet; } Player* Player::create(const char *pszFileName) { Player *pobPlayer = new Player(); if (pobPlayer && pobPlayer->initWithFile(pszFileName)) { pobPlayer->autorelease(); return pobPlayer; } CC_SAFE_DELETE(pobPlayer); return NULL; }运行游戏,可以看到考拉已经出现在我们眼前: 考拉看上去悬在空中,下一步就是要给它添加重力支持了! 考拉的重力环境模拟
this->scheduleUpdate(); GameLevelLayer类的update方法目前就只是调用Player的update,如下: void GameLevelLayer::update(float delta) { _player->update(delta); }好了,看下Player类的update方法: void Player::update(float dt) {//2 CCPoint gravity = ccp(0.f,-450.f); //3 CCPoint gravityStep = ccpMult(gravity,dt); //4 this->_velocity = ccpAdd(this->_velocity,gravityStep); //5 this->setPosition(ccpAdd(this->getPosition(),stepVelocity)); } 我们来仔细解释下: 1.在init方法里我们将Player的_velocity初始化为(0,0) 2.我们声明了一个代表重力的向量gravity (0,-450.f)y 值为负表示方向是垂直向下指向地的,这个力使考拉每帧加速度移动450像素,假设一帧时间是1秒,则第一帧考拉由0下落了450个像素,第二帧内会下落900个像素,这里不是物理学上的9.8因为计算机上像素和物理学的米单位差别是很大的 3. 我们用ccpMult来计算重力加速度,因为每帧时间是update参数里的dt,所以重力加速度值就是gravity*此帧时间dt 不过....可悲的是,考拉的确能做自由落体下落运动了,不过也不出意外的掉出了屏幕,程序Down掉! 不要在黑夜里瞎摸 --- 碰撞检测 看来只有重力模拟是远远不能代表游戏里的物理世界,在任何物理引擎里,碰撞检测与处理都是最核心和基本的部分。检测碰撞首要解决的问题是要计算出游戏角色的包围盒,不过可喜的是cocos2d-x已经提供了这样的函数boundingBox,是根据提供的资源纹理来计算包围盒的,纹理多大包围盒就多大,实际使用中还需要修正。因为美术给的纹理图片不可避免的周围会留下空白透明部分,所以需要适当放缩。 在Player.h里,加入这一个方法: CCRect Player::collisionBoundingBox() { //这里要将包围盒宽度-2个单位,但中心点不变 CCRect collisionBox = Tools::CCRectInset(this->boundingBox(),3,0); return returnBoundingBox; }在IOS版本的cocos2d-iphone里有CGRectInset方法,能使一个矩形在x轴和y轴上放缩指定像素大小,正值为缩小负值为放大,但可惜cocos2d-x版本没有提供此方法,需要自己实现,我写在了一个Tools类的一个静态方法,代码如下: CCRect Tools::CCRectInset(CCRect &rect,float dx,float dy) { rect.origin.x += dx; rect.size.width -= dx * 2; rect.origin.y -= dy; //缩小时y轴应该向下,IOS的坐标系与cocos2d-x不一样,y原点是在左下角而非左上角 rect.size.height -= dy * 2; return rect; }此代码完全是按照原MAC版CGRectInset方法来的,效果跟它一样,可以放心使用 举重开始! 我们的考拉学会了下落,下一步就要教它举重了(什么?你想说我很胖?)这里的举重不是考拉举,当然是地了 我们需要写一些方法来实现地面与考拉的碰撞检测,如下:
你还需要在GameLevelLayer类里创建两个工具函数便于解决问题:
CCPoint GameLevelLayer::tileCoordForPosition(cocos2d::CCPoint position) { float x = floor(position.x / _map->getTileSize().width); //位置x值/地图一块tile的宽度即可得到x坐标 float levelHeightInPixels = _map->getMapSize().height * _map->getTileSize().height; //地图的实际高度 float y = floor((levelHeightInPixels - position.y)/_map->getTileSize().height); //地图的原点在左上角,与cocos2d-x是不同的(2dx原点在左下角) return ccp(x,y); } CCRect GameLevelLayer::tileRectFromTileCoords(cocos2d::CCPoint tileCoords) { float levelHeightInPixels = _map->getMapSize().height * _map->getTileSize().height; //地图的实际高度 //把地图坐标tileCoords转化为实际游戏中的坐标 CCPoint origin = ccp(tileCoords.x * _map->getTileSize().width,levelHeightInPixels - ((tileCoords.y+1)*_map->getTileSize().height)); return CCRectMake(origin.x,origin.y,_map->getTileSize().width,_map->getTileSize().height); }注释写的很清楚,大家很容易理解.方法1里求y时是用地图高度-坐标高度,这个是因为游戏里采用OpenGL坐标系左下角为原点,而tileMap的坐标系的原点在左上角,所以要减一下。第二个方法返回指定tile坐标处tile的Rect,因为每个tile都是有大小的,而这个tile包围盒后面要用到,所以要计算一下,计算方法与第一个大同小异。 我被Tiles包围啦! CCArray* GameLevelLayer::getSurroundingTilesAtPosition(cocos2d::CCPoint position,cocos2d::CCTMXLayer* layer) { CCPoint plPos = this->tileCoordForPosition(position); //1 返回此处的tile坐标 //存gid的数组 CCArray* gids = CCArray::create();//2 gids->retain(); //3 我们的目的是想取出环绕在精灵四周的8个tile,这里就从上至下每行三个取9个tile(中间一个不算)仔细画画图就知代码的意义 for (int i=0; i<9; i++) { int c = i % 3; //相当于当前i所处的列 int r = (int)(i/3); //相当于当前i所处的行 CCPoint tilePos = ccp(plPos.x + (c-1),plPos.y + (r-1)); //4 取出包围tile的gid int tgid = layer->tileGIDAt(tilePos); //5 CCRect tileRect = this->tileRectFromTileCoords(tilePos); //包围盒 float x = tileRect.origin.x; //位置 float y = tileRect.origin.y; //取出这个tile的各个属性,放到CCDictionary里 CCDictionary *tileDict = CCDictionary::create(); CCString* str_tgid = CCString::createWithFormat("%d",tgid); CCString* str_x = CCString::createWithFormat("%f",x); CCString* str_y = CCString::createWithFormat("%f",y); tileDict->setObject(str_tgid,"gid"); tileDict->setObject(str_x,"x"); tileDict->setObject(str_y,"y"); tileDict->setObject((CCObject *)&tilePos,"tilePos"); //6 gids->addObject(tileDict); } //去掉中间(即自身结点tile) gids->removeObjectAtIndex(4); gids->insertObject(gids->objectAtIndex(2),6); gids->removeObjectAtIndex(2); gids->exchangeObjectAtIndex(4,6); gids->exchangeObjectAtIndex(0,4);//7 CCDictionary* d = NULL; CCObject *obj = NULL; CCARRAY_FOREACH(gids,obj) { d = (CCDictionary*)obj; CCLog("%d",d);//8 } return gids; }代码很长也很多,不用担心,我们会一点一点给它解释清楚 在开始之前,注意到这个方法有一个CCTMXLayer*参数,之前提到我们检测冲突时有些是不希望检测到的,如背景,所以在tilemap地图里分好了三个层,一个hazards是放敌人和陷阱的,当考拉碰到会受伤害,一个是walls层,就是现在我们主要讨论的地和墙,考拉碰上了一般是退回原位。还有一个就是backgrounds背景层,只是起个装饰作用,就不需要考虑怎么碰撞了。 开始分析代码: 1. 第一件要做的事就是将传入的position(考拉在游戏层中的位置)转换为地图中的tile坐标 2. 接着,我们create了一个数组用来返回8个tiles所需要的信息 3.然后开始循环9次 包括8个包围考拉的tile还有考拉自己所站的位置。一一计算出这9个tile的地图tile坐标放在tilePos变量里。 4.第4步是调用tileGIDAt方法返回tilePos位置的tile的GID,如果当前位置没有tile,则GID返回0,我们可以据此判断当前位置有没有tile 5.接着用我们定义好的方法计算出tilePos处的tile的包围盒CCRect,以及顶点位置属性,我们会把存入一个CCDictionary里 6.然后在第7步,我们把考拉所在的tile从数组中移除出去,并把这些数组元素按优先级重新排序。我们想先解决考拉身边下,上,左,右这四个是最优先的,也是游戏中最容易发生碰撞的,其他才是对角线的tile 下面这张图先显示了这些tiles原先在数组中的次序,接着排序过后的位置,你会发现排过序后位于下,上,左,右的四个tiles最先被处理,了解这些次序有助于你以后设置什么时候考拉接触到地的标志位。 然后在update方法里,加入下面代码: CCRect Player::collisionBoundingBox() { //这里要将包围盒宽度-2个单位,但中心点不变 CCRect collisionBox = Tools::CCRectInset(this->boundingBox(),0); CCPoint diff = ccpSub(this->_desiredPosition,this->getPosition()); //玩家当前距离与目的地的差距 CCRect returnBoundingBox = Tools::CCRectOffset(collisionBox,diff.x,diff.y); //计算调整后的碰撞盒,即包围盒x,y轴方向上移动diff.x,diff.y个单位 return returnBoundingBox; }跟以前相比碰撞盒是基于_desiredPosition的,游戏层会用它来做碰撞检测. 还有一点要修改的是在Player.cpp里的update方法里最后一句不能直接setPosition了,要修改的是desiredPosition this->_desiredPosition = ccpAdd(this->getPosition(),stepVelocity); //当前期望要去的位置=当前位置+当前速度 让我们解决一些碰撞! void GameLevelLayer::checkForAndResolveCollisions(Player* player) { CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(),_walls); //1 CCObject* obj = NULL; CCDictionary* dic = NULL; CCARRAY_FOREACH(tiles,obj) { dic = (CCDictionary*)obj; CCRect playerRect = player->collisionBoundingBox(); //2 玩家的包围盒 int gid = dic->valueForKey("gid")->intValue(); //3 从CCDictionary中取得玩家附近tile的gid值 if (gid) { float rect_x = dic->valueForKey("x")->floatValue(); float rect_y = dic->valueForKey("y")->floatValue(); float width = _map->getTileSize().width; float height = _map->getTileSize().height; //4 取得这个tile的Rect CCRect tileRect = CCRectMake(rect_x,rect_y,width,height); if (tileRect.intersectsRect(playerRect)) //如果玩家包围盒与tile包围盒相撞 { //5 取得相撞部分 CCRect intersection = Tools::intersectsRect(playerRect,tileRect); int tileIndx = tiles->indexOfObject(dic); //6 取得dic的下标索引 if (tileIndx == 0) { //tile在koala正下方 考拉落到了tile上 player->_desiredPosition = ccp(player->_desiredPosition.x,player->_desiredPosition.y + intersection.size.height); } else if (tileIndx == 1) //考拉头顶到tile { //在koala上面的tile,要让主角向上移移 player->_desiredPosition = ccp(player->_desiredPosition.x,player->_desiredPosition.y - intersection.size.height); } else if (tileIndx == 2) { //左边的tile player->_desiredPosition = ccp(player->_desiredPosition.x+intersection.size.width,player->_desiredPosition.y); } else if (tileIndx == 3) { //右边的tile player->_desiredPosition = ccp(player->_desiredPosition.x-intersection.size.width,player->_desiredPosition.y); } else { //7 如果碰撞的水平面大于竖直面,说明角色是上下碰撞 if (intersection.size.width > intersection.size.height) { //tile is diagonal,but resolving collision vertically float intersectionHeight; if (tileIndx>5) //说明是踩到斜下的砖块,角色应该向上去 { intersectionHeight = intersection.size.height; } else //说明是顶到斜上的砖块,角色应该向下托 { intersectionHeight = -intersection.size.height; } player->_desiredPosition = ccp(player->_desiredPosition.x,player->_desiredPosition.y + intersectionHeight); } else //如果碰撞的水平面小于竖直面,说明角色是左右撞到 { float resolutionWidth; if (tileIndx == 6 || tileIndx == 4) //角色碰到斜左边的tile 角色应该向右去 { resolutionWidth = intersection.size.width; } else //角色碰到斜右边的tile,角色应该向左去 { resolutionWidth = -intersection.size.width; } player->_desiredPosition = ccp(player->_desiredPosition.x + resolutionWidth,player->_desiredPosition.y ); } } } } } player->setPosition(player->_desiredPosition); //7 把主角位置设定到它期望去的地方 }又是洋洋洒洒一大段,让我们好好看看刚才写下的是什么 1.首先我们调用getSurroundingTilesAtPosition方法返回在walls层考拉四周的tiles数组。接着你就针对数组里每个tile进行循环,检测主角包围是否与这些tile相撞,如果发生了碰撞则我们通过改变desiredPosition的办法来解决 2.在每次循环,我们首先计算出考拉的包围盒,就像刚刚我们提到的,这个desiredPosition是计算包围盒的基础,每当碰撞发生时,我们就要改变desiredPosition值直到不再和tile发生碰撞。 3.下一步是我们要从数组元素中取出GID,这是我们之前存在dictionary里的。很可能当前tile坐标下没有任何tile存在,这时GID就为0,如果是这样,当前循环就可以直接转到下一次循环。 4.如果当前位置有tile存在,你就需要计算出那个tile的CCRect(就是tile的包围盒),存入tileRect变量里,现在你有了主角考拉和tile的包围盒,就可以检测它们是否发生碰撞了 5. 为了检测碰撞,我们调用了intersectsRect方法检测是否发生了碰撞,如果有碰撞还要再计算出这个碰撞部分的真实大小CCRect,后面会有用。可惜cocos2d-x版没有提供计算两个矩形相交的方法,而cocos2d-iphone版里却有CGRectIntersection方法能计算出,汗一个!没办法只有自己实现了,我自己实现了一个Tools::intersectsRect(rectA,rectB)可以计算出两矩形相交部分,目测用到现在好几年了没出过问题。 停下来考虑一个比较棘手的问题... 回到代码里... 6.在第6步int tileIndx = tiles->indexOfObject(dic) 里我们取到了当前tile在数组里的索引号,这个索引号就告诉我们当前tile是在考拉上下左右哪个位置,这就好办了,根据位置的不同和碰撞盒的宽度和高度的大小比较我们就将考拉前移后移上移下移碰撞盒宽或高的位置,当碰撞是发生在对角线部分时,同样方法我们可以根据碰撞部分是宽度大还是高度大还决定是前后移还是上下移,这个过程可以看代码注释,可能会更好理解一些 让我们用一下这个方法,在GameLevelLayer的update方法里,在player->update()一句后面加上这一句:
if(_velocity.y<-kVelocityYMax) _velocity = ccp(this->_velocity.x,-kVelocityYMax);这个kVelocityYMax你可以在player.h里#define kVelocityYMax 500 //Y方向的最大速度 再回到那个 checkForAndResolveCollisions方法,加以改动: 在开头处: CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(),_walls); player->_onGround = false; ///这里加上这一句 在if(tileIndx==0)里,设完desiredPosition后,加上下面两句 player->_velocity = ccp(player->_velocity.x,0.f); player->_onGround = true; 注意你要在player类里加个bool _onGround这个成员变量,表示考拉是否在地上,在player的init方法或构造函数里设初始值为false; 接着在if(tileIndx==1)里,设完desiredPosition后,加上 player->_velocity = ccp(player->_velocity.x,0.f); //此时考拉是向上跳顶到砖块,不是在地上 接着在else分支里,第一个判断if (intersection.size.width > intersection.size.height)里面第一行加上 player->_velocity = ccp(player->_velocity.x,0.f); 紧接着下面: if (tileIndx>5) { intersectionHeight = intersection.size.height; player->_onGround = true; ///注意加上这一句 } 好了大功告成,这样每次我们在考拉下落到地面或上顶到tile时都会把y轴速度重置为0,并且设是否在地面标志,这下编译运行下游戏看看吧!
好了第一部分有关物理引擎的部分讲完了,我们写了好多内容,大家先消化消化,在下一节里我会接着讲考拉的移动,跳跃,陷阱还有过关判定给内容!并把完整源码奉上! (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |