我们来实现勇士与怪物战斗的效果。我们希望勇士在遭遇敌人时,可以显示战斗动画,并且怪物身上显示被打击的效果,勇士头上冒出一行文字提示损失的生命值;战斗结束后,怪物从地图上消失。
首先我们准备好了一个战斗动画效果:一个舞动的刀光,即sword.png文件。我们把文件复制到我们项目的Resource目录下,打开图片,它实际起作用的是4,6,8,10,13,15,17,19,20,22帧,并且第17帧和第19帧在y方向上有-8的偏移量。在GameConstants.h中新增一个动画模板键值aFight,然后在动画管理器中新增一个方法createFightAnimation,用于创建战斗动画模板。在.h文件中添加“CCAnimation *createFightAnimation();”然后在.cpp文件中实现此方法:
//创建战斗动画模板 CCAnimation* AnimationManager::createFightAnimation() { //定义每帧的序号 int fightAnim[]= { 4,6,8,10,13,15,17,19,20,22 }; CCArray* animFrames=new CCArray(); CCTexture2D* texture=CCTextureCache::sharedTextureCache()->addImage("sword.png"); CCSpriteFrame* frame; int x,y; for(int i=0;i<10;i++) { //计算每帧在整个纹理中的偏移量 x=fightAnim[i]%5-1; y=fightAnim[i]/5; frame=CCSpriteFrame::createWithTexture(texture,CCRectMake(192*x,192*y,192,192)); //第17帧和第19帧在y方向上有-8的偏移量 if(fightAnim[i]==17||fightAnim[i]==19) { frame->setOffsetInPixels(ccp(0,-8)); } animFrames->addObject(frame); } animation = CCAnimation::createWithSpriteFrames(animFrames, 0.1f); //若不retain()则会出现错误 animation->retain(); return animation; }
先把准备好的刀光动画放在一边,下面我们来实现勇士遇到怪物时的检测碰撞。方法与检测墙壁碰撞基本一致:将勇士移动的目标位置由cocos2d-x坐标系转换为TileMap坐标系,通过CCTMXLayer的tileGIDAt方法获得图块ID,判断碰撞类型。修改Hero的checkCollision方法如下:
//判断碰撞类型 CollisionType Hero::checkCollision(CCPoint heroPosition) { //cocos2d-x坐标转换为Tilemap坐标 targetTileCoord=sGlobal->gameMap->tileCoordForPosition(heroPosition); //如果勇士坐标超过地图边界,返回kWall类型,阻止其移动 if(heroPosition.x<0||targetTileCoord.x>sGlobal->gameMap->getMapSize().width-1 ||targetTileCoord.y<0||targetTileCoord.y>sGlobal->gameMap->getMapSize().height-1) return kWall; //获取当前坐标位置的图块ID int tileGid=sGlobal->gameMap->getWallLayer()->tileGIDAt(targetTileCoord); //如果图块ID不为0,表示有墙 if(tileGid){ return kWall; } //获取怪物层对应坐标的图块ID tileGid=sGlobal->gameMap->getEnemyLayer()->tileGIDAt(targetTileCoord); //如果图块ID不为0,表示有怪物 if(tileGid){ //开始战斗 fight(); return kEnemy; } //可以通行 return kNone; }
其中fight方法封装了勇士和怪物开始战斗后的逻辑,包括显示怪物受打击的效果,播放战斗动画,显示损失的生命值等。
第一步先来实现怪物受打击效果——每隔0.4s显示一次红色的状态,共重复3次。我们在GameMap中新建一个方法showEnemyHitEffect,根据怪物在TileMap上所在的坐标取得对应位置的CCSprite对象,调用一个间隔为0.2s的定时器,将CCSprite对象的颜色在白色和红色之间切换,反复切换5次后取消定时器。首先在GameMap.h文件里添加“void showEnemyHitEffect(CCPoint tileCoord);”和“void updateEnemyHitEffect(float dt);”,然后.cpp文件中具体的代码如下:
//显示怪物打击 void GameMap::showEnemyHitEffect(CCPoint tileCoord) { //更新次数 fightCount=0; //获取怪物对应的CCSprite对象 fightingEnemy=enemyLayer->tileAt(tileCoord); if(fightingEnemy==NULL) return; //设置怪物sprite颜色为红色 fightingEnemy->setColor(ccRED); //启动定时器更新打击状态 this->schedule(schedule_selector(GameMap::updateEnemyHitEffect),0.18f); } //更新怪物战斗时的颜色状态 void GameMap::updateEnemyHitEffect(float dt) { //更新次数加一 fightCount++; if(fightCount%2==1){ //设置怪物精灵颜色为白色 fightingEnemy->setColor(ccWHITE); }else{ //设置怪物精灵颜色为红色 fightingEnemy->setColor(ccRED); } //切换5次后取消定时器 if(fightCount==5) { unschedule(schedule_selector(GameMap::updateEnemyHitEffect)); } }
当然,别忘了在GameMap.h文件里声明用到的变量。“int fightCount;”和“CCSprite *fightingEnemy;”。
第二步我们希望在战斗的同时,勇士头上会飘出一行文字,即诸如“生命值:-100”类的提示。在GameLayer中实现一个公有方法showTip,创建一个CCLabelTTF对象,让它执行向上移动进入、停顿、淡出的动画序列。在动画结束的回调函数中,将CCLabelTTF对象删除。在GameLayer.h中添加用到的方法声明,“void showTip(const char *tip,CCPoint startPosition);”和“void onShowTipDone(CCNode *pSender);”。然后在GameLayer.cpp最后添加下面代码:
//显示提示信息 void GameLayer::showTip(const char* tip,CCPoint startPosition) { //新建一个文本标签 CCLabelTTF* tipLabel=CCLabelTTF::create(tip,"Arial",20); tipLabel->setPosition(ccpAdd(startPosition,ccp(16,16))); this->addChild(tipLabel,kZTip,kZTip); //定义动画效果 CCAction* action=CCSequence::create( CCMoveBy::create(0.5f,ccp(0,32)), CCDelayTime::create(0.5f), CCFadeOut::create(0.2f), CCCallFuncN::create(this,callfuncN_selector(GameLayer::onShowTipDone)), NULL); tipLabel->runAction(action); } //提示消息显示完后的回调 void GameLayer::onShowTipDone(CCNode* pSender) { //删掉文本标签 this->getChildByTag(kZTip)->removeAllChildrenWithCleanup(true); }
我们还要在GameConstants.h文件中新增一个枚举类型,用来存放各个层的zOrder及tag。代码如下:
enum { kZMap = 0,//地图的zOrder kZNPC, //NPC kZTeleport, //传送点 kZHero,//勇士精灵的zOrder kZTip,//提示信息的zOrder };//GameLayer中各部分的显示zOrder及tag
//开始战斗 void Hero::fight() { //已经在战斗中,避免重复战斗 if(isHeroFighting) return; isHeroFighting=true; //显示怪物受到打击的效果 sGlobal->gameMap->showEnemyHitEffect(targetTileCoord); //显示损失的生命值,先用数据替代一下 char temp[30]={0}; sprintf(temp,"lost hp: -%d",100); sGlobal->gameLayer->showTip(temp,getPosition()); //将用于显示战斗动画的精灵设置为可见 fightSprite->setVisible(true); //计算显示战斗动画的位置为勇士和怪物的中间点 CCPoint pos=ccp((targetPosition.x-getPosition().x)/2+16,(targetPosition.y-getPosition().y)/2+16); fightSprite->setPosition(pos); //创建战斗动画 CCAction *action=CCSequence::create( sAnimationMgr->createAnimate(aFight), CCCallFuncN::create(this,callfuncN_selector(Hero::onFightDone)), NULL); fightSprite->runAction(action); } //战斗结束的回调 void Hero::onFightDone(CCNode* pSender) { //删除怪物对应的图块,表示已经被消灭 sGlobal->gameMap->getEnemyLayer()->removeTileAt(targetTileCoord); isHeroFighting=false; fightSprite->setVisible(false); }
注意,在战斗结束后,除了从TileMap地图上删除图块以外,还需要更新GameMap中保存怪物对象的数组enemyArray。最好不要直接删除,因为有一个定时器在不停地更新怪物动画,正确地办法是在updateEnemyAnimation中判断是否有怪物的图块ID为0,有则说明已经被删除了,记录下次元素,在循环外再进行删除操作。修改updateEnemyAnimation方法如下:
//更新怪物的图块 void GameMap::updateEnemyAnimation(float dt) { //遍历保存所有怪物对象的数组 CCObject* pObject; Enemy* enemy; Enemy* needRemove=NULL; CCARRAY_FOREACH(enemyArray,pObject){ enemy=(Enemy*)pObject; if(enemy!=NULL){ //获取怪物当前的图块ID int gid=enemyLayer->tileGIDAt(enemy->position); //如果怪物被删除了,需要把它在enemyArray中也删除 if(gid==0) { needRemove=enemy; continue; } gid++; //如果结束,设置为起始图块ID if(gid-enemy->startGID>3){ gid=enemy->startGID; } //给怪物设置新的图块 enemyLayer->setTileGID(gid,enemy->position); } } //删除消除的怪物 if(NULL!=needRemove) { enemyArray->removeObject(needRemove,true); } }
此外,记得在AnimationManager中加载勇士战斗的动画。编译运行程序,现在我们的勇士终于可以与怪物战斗了。