欢迎回来,上篇我们讲到了物理引擎中重力环境模拟以及主角考拉与地面墙壁的碰撞,相信大家已经对2D世界的物理模拟有了一定的了解,现在我们接着讲如何让考拉动起来吧!
让考拉动起来!
这里控制考拉移动变得非常简单,它只有向前和跳两个能力(源码中我加了考拉向后走功能,建议大家自己加几个虚拟按键来实现更非富的功能)如果你按着屏幕左半部考拉会向前走,按住右半部考拉会跳起来(原文设定考拉不会后退-_-)。
我们需要在Player.h里加两个成员变量:
bool _forwardMarch; //是否向前走
bool _mightAsWellJump; //可以跳跃吗?
在player.cpp的init方法或在构造函数里把它们设置为false
在GameLevelLayer类里加上触摸,在.h文件里加上:
virtual void registerWithTouchDispatcher();void ccTouchesBegan(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);void ccTouchesMoved(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);void ccTouchesEnded(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);
在GameLevelLayer.cpp的init里加上(加载地图代码后)
setTouchEnabled(true); //设置可触摸
然后写注册触摸方法
void GameLevelLayer::registerWithTouchDispatcher()
{CCDirector* pDirector = CCDirector::sharedDirector();pDirector->getTouchDispatcher()->addStandardDelegate(this, 0); //注册多点触摸
}
现在,让我们看看那三个触摸事件吧!
void GameLevelLayer::ccTouchesBegan(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent)
{CCSetIterator iter = pTouches->begin();for (; iter!=pTouches->end(); iter++){CCTouch* pTouch = (CCTouch*)(*iter);CCPoint touchLocation = this->convertTouchToNodeSpace(pTouch); //把touch点位置转换为本地坐标if (touchLocation.x > 240) //在屏幕最右边点,就是跳{_player->bMightAsWellJump = true;}else {_player->bForwardMarch = true;}}
}void GameLevelLayer::ccTouchesEnded(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent)
{_player->bForwardMarch = false; //松开按键时,设置为不可跳也不是向前状态_player->bMightAsWellJump = false;
}
代码一目了然,ccTouchesBegan时根据玩家按的位置决定了考拉状态是前进还是跳跃,松开按键时将这两个状态变量重置为false。
真正的角色移动是在player的update方法里进行的,看代码:
void Player::update(float delta)
{CCPoint gravity = ccp(0.f, -450.f); //考拉每秒下降450个单位CCPoint gravityStep = ccpMult(gravity, delta); //计算在重力影响下delta时间内具体下降了多少, 即dt时间后下落速度为多少CCPoint forwardMove = ccp(800.f, 0.f); //前进速度,每秒前进800.fCCPoint forwardStep = ccpMult(forwardMove, delta); // 1this->_velocity = ccpAdd(this->_velocity, gravityStep); //当前速度=当前速度+重力加速度this->_velocity = ccp(this->_velocity.x *0.9f, this->_velocity.y); //2if (this->bForwardMarch){this->_velocity = ccpAdd(this->_velocity, forwardStep); //当前速度要加上向前的速度矢量}// 3CCPoint minMovement = ccp(-120.f, -350.f);CCPoint maxMovement = ccp(120.0f, 250.f);this->_velocity = ccpClamp(this->_velocity, minMovement, maxMovement); //4CCPoint stepVelocity = ccpMult(this->_velocity, delta); //计算下此速度下主角移动了多少this->_desiredPosition = ccpAdd(this->getPosition(), stepVelocity); //当前期望要去的位置=当前位置+当前速度
}
让我们来详细地看一下新增部分:
- 当玩家触摸前进的时候和重力模拟类似,我们加了一个向前的推进力800.f,处理方式与重力相同
- 第2步横向速度乘以0.9是模拟摩擦力效果,考虑下当有向前推力时玩家会向前移动,没有了呢?是不是立即停下来?那样看起来太生硬了,所以这里x轴速度每帧乘以0.9就起到了均匀减速的效果,就像摩擦力一样
- 检查一下玩家是否按了前,按前进时速度上就要加上向前的推动力,就起到了向前进的效果了
- 这一步是调用了clamp是限定速度前后上下不要太大,给它一个最大值。之前的那句设定下落速度最大值可以去掉了。
让考拉跳起来!
跳跃是动作游戏里最明显的一个特征。我们希望角色跳的平滑逼真,现在让我们来实现它。
到player类的update方法里,在if(this->_forwardMarch)语句之前加上下面代码:
CCPoint jumpForce = ccp(0.f, 310.f);if(this->_mightAsWellJump && this->_onGround)
{this->_velocity = ccpAdd(this->_velocity, jumpForce);
}
只要加一个向上的力,角色就可以跳起来了。
如果你止步于此,你会得到一个老式的雅代利式跳跃,即每次跳跃都是相同的高度,每次你都施给玩家一个同样的力,然后等着重力把你拉回地面来。
这么做似乎没什么不妥,如果你要求不高的话,但是仔细观察一下各种流行的平台游戏,如超级马里奥,索尼克,洛克人,水上魂斗罗等,似乎玩家能够通过按键的力度来控制跳跃的高度,达到更灵活的效果。那是怎么做到的呢?
其实实现这个效果也很简单,所谓玩家按键力度不过就是按键时间的长久,按的时间长也就是施加跳跃力的时间就长,跳的当然高了,半路上如果玩家不给力了,当然会跳到一半掉链子,不过玩家有种错觉就是想跳得高就使劲按着跳跃键,不想跳了松开键就是-_-
CCPoint jumpForce = ccp(0.f, 310.f); //向上的跳跃力 玩家一直按着跳跃键时的跳跃力float jumpCutOff = 150.f; //玩家没有按住跳跃键时的跳跃力if(this->bMightAsWellJump && this->onGround) //如果当前玩家按了跳跃键并且在地上{this->_velocity = ccpAdd(this->_velocity, jumpForce); //跳跃就是加上一个向上的速度}else if (!this->bMightAsWellJump && this->_velocity.y > jumpCutOff) //玩家没有按住跳跃键,并且向上的速度已经超过了设定的值,就限定向上跳跃速度{this->_velocity = ccp(this->_velocity.x, jumpCutOff);}
注释解释的很清楚,就不多解释了。
好了,build一下run下我们的游戏吧,看吧,我们的小考拉可以自由欢腾地上下翻飞了。
跳是跳的很欢了,不过悲剧的是,它跳到最右边就跳出屏幕,看不见了。
来修正这个问题,这个问题其实就是视点跟随,在cocos2dx上有解决这一问题的标准算法,贴出代码:
void GameLevelLayer::setViewpointCenter(cocos2d::CCPoint pos)
{CCSize winSize = CCDirector::sharedDirector()->getWinSize();//限定角色不能超过半屏int x = MAX(pos.x, winSize.width/2);int y = MAX(pos.y, winSize.height/2);//限定角色不能跑出屏幕x = MIN(x, (_map->getMapSize().width * _map->getTileSize().width) - winSize.width/2);y = MIN(y, (_map->getMapSize().height * _map->getTileSize().height) - winSize.height/2);CCPoint actualPosition = ccp(x, y);CCPoint centerOfView = ccp(winSize.width/2, winSize.height/2);CCPoint viewPoint = ccpSub(centerOfView, actualPosition);//设定一下地图的位置_map->setPosition(viewPoint);
}
方法参数就是玩家考拉当前位置。这个方法可以不但能左右跟随还能上下跟随主角,非常好用。这个方法原理在很多博客都有提到,原理其实就是地图在跟玩家做返方向运动,大家可以查阅一下其他文章解释它的原理,不过我在这里不想再多说了,不是一两句能说清,我们只要会用这个方法就行了。
我们要做的就是在GameLevelLayer类的update方法里调用就行了,可以放在update方法结尾之前,如下:
this->setViewpointCenter(_player->getPosition());
现在build再run一下,这回我们的小考拉再也不会跑出屏幕外了!
尝一下受伤的滋味!
现在我们可以着手做游戏过关和GameOver的功能了。
地图里有个hazards层,这个层里放置一些考拉碰上就会挂的object。其实本质也上碰撞检测,看代码:
void GameLevelLayer::handleHazardCollisions(Player* player)
{CCArray *tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _hazards);CCDictionary* dic = NULL;CCObject* obj = NULL;float x=0.f; float y = 0.f;int gid = 0;CCARRAY_FOREACH(tiles, obj){dic = (CCDictionary*)obj;x = dic->valueForKey("x")->floatValue();y = dic->valueForKey("y")->floatValue();CCRect tileRect = CCRectMake(x, y, _map->getTileSize().width, _map->getTileSize().height);CCRect pRect= player->collisionBoundingBox();gid = dic->valueForKey("gid")->intValue();if (gid && tileRect.intersectsRect(pRect)) //如果有钉刺并且玩家与它发生碰撞了,gameOver{this->gameOver(false);}}
}
代码看上去是不是很熟悉呀,你没记错,其实就是从checkAndResolveCollisions方法里拷来的,只不过没分那么多情况而且处理方式也简单了只是调用了gameOver方法,gameOver的布尔值参数为true表示游戏胜利,为false就是失败
_hazards在GameLevelLayer类也是个CCTMXLayer* 地图层类型的成员变量,在此类的init方法里初始化它:
_hazards = _map->layerNamed("hazards");
然后在update方法里调用这个方法,现在update方法是这个样子:
void GameLevelLayer::update(float delta)
{_player->update(delta);this->handleHazardCollisions(_player);this->checkForAndResolveCollisions(_player);this->setViewpointCenter(_player->getPosition());
}
现在实现gameOver方法了,当玩家跳到harads层里的刺上时,我们会调用这个方法游戏结束,或者当玩家走到终点时,我也会调用这个方法,这个方法会显示出一个restart按钮,并在屏幕上打印出一些信息,告诉玩家你死了或者你赢了之类的-_-
void GameLevelLayer::gameOver(bool bWon)
{bGameOver = true;CCString* gameText;if (bWon){gameText = CCString::create("You Won!");}elsegameText = CCString::create("You have Died!");CCLabelTTF* diedLabel = CCLabelTTF::create(gameText->getCString(), "Marker Felt", 40);diedLabel->setPosition(ccp(240, 200));CCMoveBy *slideIn = CCMoveBy::create(1.f, ccp(0, 250));CCMenuItemImage* replay = CCMenuItemImage::create("replay.png","replay.png","replay.png", this, menu_selector(GameLevelLayer::restartGame));CCArray *menuItems = CCArray::create();menuItems->addObject(replay);CCMenu *menu = CCMenu::create();menu->addChild(replay);menu->setPosition(ccp(240, -100));this->addChild(menu);this->addChild(diedLabel);menu->runAction(slideIn);
}
方法开头的变量bGameOver也是GameLevelLayer类新定义的一个成员变量,在init里要初始化为false,此变量表示游戏是否结束。这个变量作用是你在update方法里开头判断一下,如果bGameOver==true,则直接返回不要做任何事了。如下:
void GameLevelLayer::update(float delta)
{if(bGameOver)return;_player->update(delta);this->handleHazardCollisions(_player);this->checkForAndResolveCollisions(_player);this->setViewpointCenter(_player->getPosition());
}
现在你再编译运行,碰上钉板试试,会出现杯具性的结局...
不要尝试的太多,小心动物保护组织会找上你家门来(-_-这就是所谓的美式幽默吗?)
修正一个致命BUG
还记得前面出现过当考拉掉出地图出现的事情吗?在游戏地图里有些坑我们本意是考拉掉下去游戏会结束,但事实上程序崩掉了。凡是带有TILED地图的都会出现这样的问题,只要角色走出地图边界游戏就会崩掉,这个问题困扰了好多人,也包括我。其实解决这个BUG很简单,关键在tileGIDAt方法。
此方法你打开源码实现就知道,它有个CCASSERT宏,判断出地图就会抛出异常。本来吗此方法是取地图上某处tile的gid,你都出地图了还取什么当然要抛异常啦。这里我们就在getSurroundingTilesAtPosition方法里加个判断,注意当然要在调用tileGIDAt方法之前,不让考拉出地图:
if (tilePos.y > (_map->getMapSize().height-1))
{this->gameOver(false);return NULL;
}
在checkForAndResolveCollisions方法里有调用
getSurroundingTilesAtPosition方法,如果它返回个空数组可就不妙了,所以在checkForAndResolveCollisions也要改一下,在 CCArray* tiles = this->getSurroundingTilesAtPosition(player->getPosition(), _walls);一句之后,加上if (bGameOver) //可能玩家掉坑里,就不处理了{return;}
编译再运行,把考拉掉进坑里试试,发现程序不再DOWN了!Winner!
在最后,我们处理下考拉走到终点时显示胜利的情况。
这里我们简单点,只是判断下考拉向右走了多远就取胜
void GameLevelLayer::checkForWin()
{if (_player->getPositionX()>3130.0){this->gameOver(true);}
}
真实游戏里我们通常在终点放置一个object,如门呀城堡小旗什么的,主角碰到就胜利了可进入下一关,有兴趣的自己尝试!这个方法也要放到GameLevelLayer的update方法里,如下:
void GameLevelLayer::update(float delta)
{if(bGameOver)return;_player->update(delta);this->handleHazardCollisions(_player);this->checkForWin();this->checkForAndResolveCollisions(_player);this->setViewpointCenter(_player->getPosition());
}
运行一下,走到终点试试:
添加音效
胜利了之后没有音乐怎么行,一个无声的世界可真是会令人绝望呀,来我们加些音效吧!
在GameLevelLayer.cpp和 player.cpp加上头文件如下:
#include "SimpleAudioEngine.h"
using namespace CocosDenshion;
在GamelevelLayer的init方法里加上:
SimpleAudioEngine::shareEngine()->playBackgroundMusic("level1.mp3");
在player.cpp里的update方法里判断玩家跳的地方加上音效:
if(this->bMightAsWellJump && this->onGround) //如果当前玩家按了跳跃键并且在地上{this->_velocity = ccpAdd(this->_velocity, jumpForce); //跳跃就是加上一个向上的速度SimpleAudioEngine::sharedEngine()->playEffect("jump.wav");}
在GameLevelLayer.cpp的gameOver方法里也加上音效:
void GameLevelLayer::gameOver(bool bWon)
{if (bGameOver) //不要反复调用 {return;}bGameOver = true;SimpleAudioEngine::sharedEngine()->playEffect("hurt.wav");
编译运行,试一试你亲手制作的带有音乐感的游戏吧!
源码下载地址: 下载
剩下我们还能做什么?
要做的还有什么,角色动画没有吧,此外还有怪物,AI, 关卡,角色状态机等等。这些都在IOS GAME三件套 Platformer Start Kit里,前面说过,本教程只是这个Start Kit的前奏曲,那真正的Platformer game是什么样的呢?
学完此教程后,你可以学到
怎么样?心动了吧,涉于版权问题不便在此公开,有兴趣的可到此看看: 仿马里奥平台动作
此外大名鼎鼎的三件套之一横版格斗游戏 Beat 'Em Up Game Starter Kit 也是非常精彩哦!网上公开的源码教程还不到里面的二十分之一-_-有兴趣可到此看看: 横版格斗游戏源码