如何使用cocos2dx 制作一个多向滚屏坦克类射击游戏-第二部分

 原文链接:http://www.raywenderlich.com/6888/how-to-make-a-multi-directional-scrolling-shooter-part-2 这里使用cocos2dx做了移植。

开发环境为vs2012和cocos2dx 2.0

   

文中的代码根据个人习惯稍有修改。

 

在第一部分的内容中,我们创建了一个全新的Cocos2Dx 2.0项目,将瓦片地图添加到游戏中,并添加了一个坦克,可以使用加速计来进行操控。

 

在这部分(也是最后一部分)的内容中,我们将让坦克可以发射炮弹,同时会添加敌军坦克,添加游戏的赢/输机制,等等。

接下来的内容将从第一部分已完成的项目开始,可以从这里(http://download.csdn.net/detail/hany3000/5204725 )直接下载。

 

准备好了,还是来制作游戏吧。

 

炮火连天

 

现在我们的坦克已经可以四处移动了,但还不能开火!坦克要开火跟女生要买新衣服一样天经地义,所以必须得尽快解决这个问题:)

 

当然,由于之前使用加速计来控制坦克的移动,这里可以直接使用触摸的方式让坦克开火。不过,为了让游戏变得更有趣一点,我们不仅要在玩家触摸的时候发货,还可以让坦克连续开火!

 

到Tank.h,在其中做出以下修改:

 //second part
 cocos2d::CCPoint shootVector;//射击的方向
 double timeSinceLastShot;//从上次射击到现在所经过的时间
 cocos2d::CCSprite *turret;//坦克炮塔

 

bool shooting;
 void shootToward(cocos2d::CCPoint stPosition);
 void shootNow();

 

在上面的代码中,我们添加了一个实例变量shootVector用于保存射击的方向,变量timeSinceLastShot用于保存从上次射击到现在所经过的时间。还添加了一个turret变量来保存添加到坦克顶部的新精灵对象-坦克的炮塔!

 

切换到Tank.cpp,并对代码做出以下调整:

在文件的顶部添加:

#include "SimpleAudioEngine.h"

 

在initWithLayer 方法中添加以下代码:

 

do
 {
 CC_BREAK_IF(!CCSprite::initWithSpriteFrameName("tank1.png"));

 CCSpriteFrame *frame = CCSpriteFrameCache::sharedSpriteFrameCache()->spriteFrameByName(CCString::createWithFormat("tank%d.png",type)->getCString());

 _layer=layer;
 _type = type;
 moving=false;
 this->hp=hp;
 this->scheduleUpdateWithPriority(-1);
 
 //创建坦克炮塔
 CCString turretName=CCString::createWithFormat("tank%d_turret.png",type)->getCString();
 turret = CCSprite::createWithSpriteFrameName(turretName.getCString());
 turret->setAnchorPoint(ccp(0.5,0.25));
 turret->setPosition(ccp(this->getContentSize().width/2,this->getContentSize().height/2));
 this->addChild(turret);

 }while(0);
 return;

在以上代码中,我们创建了一个新的精灵对象代表坦克炮塔,并将其添加为坦克的子节点。这样当我们移动坦克精灵的时候,炮塔也会随之移动。

 

请注意放置炮塔的方式:

首先将锚点的位置设置在靠近炮塔的基座。为什么这样做呢?因为锚点的位置就是旋转的中心点,而我们想要让炮塔沿着基座旋转,就必须将锚点设置在靠近基座。

接着我们将炮塔精灵对象的位置设置在坦克的中心。由于炮塔精灵是坦克的子节点,其位置是相对坦克的左下角的。这样我们就把锚点(炮塔的基座)连接在坦克的中心点上。

 

接下来在文件的底部添加一个新的方法:

 


void Tank::shootToward(CCPoint stPosition)
{
 CCPoint offset = ccpSub(stPosition,this->getPosition());
 float MIN_OFFSET = 10;
 if(ccpLength(offset)<MIN_OFFSET) return;

 shootVector = ccpNormalize(offset);

 shooting = true;
}

 

当玩家触摸屏幕的时候,就会调用该方法。这里需要检查触摸点到目标位置的距离不小于10个点(如果太近,则很难判断射击的方向)。接着我们将向量规范化(也即把向量的长度设置为1),从而得到一个射击的方向向量,并将其保存在shootVector变量中,以便后续使用。

 

接下来添加实际射击的方法如下:

 

void Tank::shootNow()
{
 //1
 float angle = ccpToAngle(shootVector);
 this->turret->setRotation(-1*CC_RADIANS_TO_DEGREES(angle));
 
 //2
 float mapMax=MAX(this->_layer->tileMapWidth(),this->_layer->tileMapHeight());
 CCPoint actualVector = ccpMult(shootVector,mapMax);
 //3
 float POINTS_PER_SECOND = 300;
 float duration  = mapMax/POINTS_PER_SECOND;

 //4
 CCString shootSound = CCString::createWithFormat("tank1Shoot.wav",this->_type)->getCString();
 CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect(shootSound.getCString());

 //5
 CCString bulletName=CCString::createWithFormat("tank%d_bullet.png",_type)->getCString();
 CCSprite* bullet=CCSprite::createWithSpriteFrameName(bulletName.getCString());

 bullet->setTag(_type);
 bullet->setPosition(ccpAdd(this->getPosition(),ccpMult(shootVector,turret->getContentSize().height)));

 CCMoveBy *move = CCMoveBy::create(duration,actualVector);
 CCCallFuncN *call=CCCallFuncN::create(this,callfuncN_selector(Tank::shootEnd));

 bullet->runAction(CCSequence::create(move,call,NULL));
 _layer->_batchNode->addChild(bullet);
}

让我们来解释下其中的代码(按照注释顺序):

1.首先我们将炮塔选中到面朝射击的方向。这里使用一个简单的辅助函数ccpToAngle,可以将向量转换成以弧度为单位的向量角度。急着将其转换成Cocos2D中使用的角度,然后乘以——1,因为在Cocos2D中使用顺时针旋转。同时需要加上90,因为炮塔的美术素材是朝上的(而不是朝右)。

2.接着我们计算出炮弹要射击的距离。这里我们获得瓦片地图宽度或高度的最大值,并乘以射击的方向向量;

3.再接下来我们需要计算出炮弹要达到指定地点所需的时间。这一点很简单,只需使用向量长度(瓦片地图宽度或高度中更大的数值)除以每秒的运动速度即可

4.添加音效

5.最后我们创建了一个新的炮弹精灵,并让其执行某个动作(在动作完成后消失),并将其添加到层的精灵表单中。

 

 

接下来对Tank.cpp做出以下修改:

 

添加以下新的方法:


bool Tank::shouldShoot()
{
 if(!this->shooting) return false;

 double SECS_BETWEEN_SHOTS = 0.25;
 if(timeSinceLastShot > SECS_BETWEEN_SHOTS)
 {
  timeSinceLastShot = 0;
  return true;
 }
 else
 {
  return false;
 }
}

 
void Tank::updateShoot(float dt)
{
 timeSinceLastShot += dt;
 if(this->shouldShoot())
 {
  this->shootNow();
 }
}然后修改update方法如下:

 
void Tank::update(float dt)
{
 this->updateMove(dt);
 updateShoot(dt);

 

通过以上代码,可以让坦克连续射击。每一次更新我们都会调用updateShoot方法。如果从上次射击到现在的时间超过了0.25秒,则调用shootNow方法。

 

好了,Tank.cpp已经完成。到HelloWorldLayer.cpp,并使用以下内容替代ccTouchesBegan和ccTouchesMoved:

 

CCSetIterator it = pTouches->begin();

 CCTouch *touch= (CCTouch*)(*it);

 CCPoint mapLocation = _tileMap->convertTouchToNodeSpace(touch);

 this->tank->shooting = true;
 this->tank->shootToward(mapLocation);
  tank->moving=true;
  tank->moveToward(mapLocation);

 

通过以上的方法,我们使用触碰来进行射击,而非移动坦克。

 

编译运行游戏,可以触摸屏幕连续射击了!

 如何使用cocos2d <wbr>2.0开发类似坦克大战的游戏(且支持ARC)2



当然,这里采用的射击方式并非是最佳的,因为我们在连续分配炮弹,而在ios中这样是非常耗费内存的。一个更好的方式是预先分配一个炮弹数组,并在需要发射炮弹时重用之前的旧炮弹。

 

添加敌军坦克

 

任何一个坦克对战游戏都需要有敌军坦克, 打开HelloWorldLayer.h,然后创建一个数组用于保存敌军坦克:

CCArray *enemyTanks;

 

然后打开HelloWorldLayer.cpp,并在init方法的地步添加以下代码,以产生一些敌军坦克:

 

enemyTanks = CCArray::create();
  this->enemyTanks->retain();
  //产生一些敌对坦克
  int  NUM_ENEMY_TANKS = 50;
  for(int i=0;i<NUM_ENEMY_TANKS;i++)
  {
   RandomTank* enemy = new RandomTank();
   enemy->initWithLayer(this,2,2);
   CCPoint randSpot;
   bool inWall = true;
   while(inWall)
   {
    randSpot.x=CCRANDOM_0_1()*this->tileMapWidth();
    randSpot.y=CCRANDOM_0_1()*this->tileMapHeight();
    inWall = this->isWallAtPosition(randSpot);
   }
   enemy->setPosition(randSpot);
   
   this->_batchNode->addChild(enemy);
   enemyTanks->addObject(enemy);
  }
  //
  explosion = CCParticleSystemQuad::create("explosion.plist");
  explosion->stopSystem();
  _tileMap->addChild(explosion,1);

  explosion2 = CCParticleSystemQuad::create("explosion2.plist");
  explosion2->stopSystem();
  _tileMap->addChild(explosion2,1);

  exit = CCSprite::createWithSpriteFrameName("exit.png");
  CCPoint exitTileCoord = ccp(98,98);
  CCPoint exitTilePos = positionForTileCoord(exitTileCoord);
  exit->setPosition(exitTilePos);
  _batchNode->addChild(exit);

  this->setScale(0.5);

以上代码不难理解。我们在一些随机点创建了一批坦克(只要不是在水中)。

 

编译运行,可以看到敌军坦克遍布地图!为了方便坦克英雄识别,这里将敌军坦克都标识为红色!

如何使用cocos2d <wbr>2.0开发类似坦克大战的游戏(且支持ARC)2

 

 

敌军凶猛!

 

如果这些敌军坦克只是静坐修禅,当然最好不过!不过这样游戏也少了很多乐趣!这里将从Tank类派生一个子类RandomTank,并覆盖其中的一些方法。

 

在Xcode中使用iOS\Cocoa Touch\Objective-C class模板创建一个新的文件,将其命名为RandomTank,并将subclass of设置为Tank。打开RandomTank.h,并使用以下的代码替代其中的内容:

 

#pragma once
#include "tank.h"
#include "cocos2d.h"

class HelloWorld;

class RandomTank :
 public Tank
{
public:
 RandomTank(void);
 ~RandomTank(void);
 CREATE_FUNC(RandomTank);

 double timeForNextShot;
 void  initWithLayer(HelloWorld* theLayer,int theType,int theHp);
 void move(float dt);
 bool shouldShoot();
 void calcNextMove();
 bool clearPathFromTileCoord(cocos2d::CCPoint start,cocos2d::CCPoint end);
};

 

这里添加了一个实例变量,用于记录到下一次设计前要等候多少秒。

 

切换到RandomTank.cpp,并使用以下代码替代其中的内容:

 

#import "

#include "RandomTank.h"
#include "HelloWorldScene.h"
 
using namespace cocos2d;

RandomTank::RandomTank(void)
{
}


RandomTank::~RandomTank(void)
{
}

void RandomTank::initWithLayer(HelloWorld* theLayer,int theType,int theHp)
{
 Tank::initWithLayer(theLayer,theType,theHp);
 this->schedule(cocos2d::SEL_SCHEDULE(&RandomTank::move),0.5f);
}

bool RandomTank::shouldShoot()
{
 if(ccpDistance(this->getPosition(),this->_layer->tank->getPosition())> 600)
  return false;
 if(timeSinceLastShot >timeForNextShot)
 {
  timeSinceLastShot=0;
  timeForNextShot = (CCRANDOM_0_1()*3)+1;

  shootToward(_layer->tank->getPosition());
  return true;
 }
 else return false;
}
void RandomTank::calcNextMove()
{
 bool moveOK=false;
 CCPoint start = _layer->tileCoordForPosition(this->getPosition());
 CCPoint end;
 while(!moveOK)
 {
  end = start;
  end.x +=CCRANDOM_MINUS1_1()*((rand()%10)+3);
  end.y +=CCRANDOM_MINUS1_1()*((rand()%10)+3);
  moveOK=clearPathFromTileCoord(start,end);
 }
 CCPoint moveToward = _layer->positionForTileCoord(end);
 this->moving = true;
 this->moveToward(moveToward);

}
void RandomTank::move(float dt)
{
 if(this->moving && rand()%3 !=0) return;

 this->calcNextMove();
}
 

以上定时了一个移动方法,每半秒调用一次。

当进行射击时,我们会首先确保敌军坦克离坦克英雄足够近,否则如果敌军坦克在很远的地方就开炮,会让游戏难度大大提升。

接下来我们计算出下一次射击的随机时间,大概在1-4秒之间。如果达到该时间,会更新坦克的目标,并继续。

 

在HelloWorldLayer.cpp中添加以下代码:

#include"RandomTank.h"

 

然后在init方法中修改创建正常坦克的代码,如下:

 

RandomTank* enemy = new RandomTank();
   enemy->initWithLayer(this,2,2);

 

编译运行游戏,当坦克英雄距离敌军坦克一定距离的时候,敌军就会开炮了!

 如何使用cocos2d <wbr>2.0开发类似坦克大战的游戏(且支持ARC)2

 

让敌军坦克动起来

 

现在虽然敌军坦克已经开始射击了,但还需要让它们四处动一动。

 

为了让游戏尽可能简化,这里采取的策略是:

1.选择一个临近的随机点

2.确保该路径上没有障碍物,如果是,则让坦克朝该点移动

3.如果不是,则返回第一步

 

这里唯一需要考虑的是第2步!指定起始点和终点的坐标,我们如何走过坦克需要移动的瓦片,并确保不会遇上障碍物?

 

幸运的是,这个问题已被解决了,可参考James McNeil的博客(http://playtechs.blogspot.com/2007/03/raytracing-on-grid.html),这里我们直接使用他给出的方。

 

切换到RandomTank.m,并使用以下代码替代calcNextMove方法:

// From http://playtechs.blogspot.com/2007/03/raytracing-on-grid.html


bool RandomTank::clearPathFromTileCoord(cocos2d::CCPoint start,cocos2d::CCPoint end)
{
 int dx = abs(end.x - start.x);

int dy = abs(end.y - start.y);

int x = start.x;

int y = start.y;

int n = 1 + dx +dy;

int x_inc = (end.x>start.x) ? 1: -1;

int y_inc = (end.y>start.y) ? 1: -1;

int error = dx - dy;

  dx *=2;

  dy *=2;

 
for(;n>0; --n){

if (_layer->isWallAtTileCoord(ccp(x,y))) return false;

 
if(error >0){

      x += x_inc;

      error -= dy;

    }

else{

      y += y_inc;

      error += dx;

    }

 
  }

return true;

}


void RandomTank::calcNextMove()
{
 bool moveOK=false;
 CCPoint start = _layer->tileCoordForPosition(this->getPosition());
 CCPoint end;
 while(!moveOK)
 {
  end = start;
  end.x +=CCRANDOM_MINUS1_1()*((rand()%10)+3);
  end.y +=CCRANDOM_MINUS1_1()*((rand()%10)+3);
  moveOK=clearPathFromTileCoord(start,end);
 }
 CCPoint moveToward = _layer->positionForTileCoord(end);
 this->moving = true;
 this->moveToward(moveToward);

}

不要担心上面第一个方法的工作原理(如果感兴趣可以仔细看看那篇博客),只需要知道它可以检查在起点和终点之间是否存在障碍,如果是则返回FALSE。

 

而在calcNextMove方法中,我们使用了上面的算法。

编译运行,可以看到敌军坦克开始动起来!

 

碰撞,爆炸和出口

 

现在我们有敌人可以打,有炮弹可以发射,还需要的就是刺激的爆炸效果,还有就是让坦克英雄取得胜利的出口!

 

在HelloWorldLayer.h中对代码做出以下修改:

 

typedef enum
 {
  kEndReasonWin,
  kEndReasonLose

 }EndReason;

 

 添加以下几个实例变量;

 

cocos2d::CCParticleSystemQuad* explosion;
 cocos2d::CCParticleSystemQuad* explosion2;
 bool gameOver;
 cocos2d::CCSprite* exit;

 void restartTapped(CCObject* pSender);
 void endScene(EndReason endReason);

 

 

接下来切换到HelloWorldLayer.mcpp并在init方法的底部添加以下代码:

 

 explosion = CCParticleSystemQuad::create("explosion.plist");
  explosion->stopSystem();
  _tileMap->addChild(explosion,1);

  explosion2 = CCParticleSystemQuad::create("explosion2.plist");
  explosion2->stopSystem();
  _tileMap->addChild(explosion2,1);

  exit = CCSprite::createWithSpriteFrameName("exit.png");
  CCPoint exitTileCoord = ccp(98,98);
  CCPoint exitTilePos = positionForTileCoord(exitTileCoord);
  exit->setPosition(exitTilePos);
  _batchNode->addChild(exit);

  this->setScale(0.5);

在以上代码中,我们使用Cocos2Dx内置的粒子系统创建了两种不同类型的爆炸效果,并将其添加为瓦片地图的子节点,但首先需要先将其关闭。当需要使用的时候,会把它们移动到需要的地方,并使用resetSystem来启动。

 

接着我们在地图的右下角添加了一个出口。一旦坦克到达这一点,玩家就赢得了战斗!

 

注意到这里把层的比例设置为0.5,因为我们希望可以看到地图的更多内容。

 

现在在update方法的前面添加这些新的方法:

 


void HelloWorld::restartTapped(CCObject* pSender)
{
 CCDirector::sharedDirector()->replaceScene(CCTransitionFlipX::create(0.5,HelloWorld::scene()));
}
void HelloWorld::endScene(EndReason endReason)
{
 if(gameOver) return;

 gameOver = true;
 CCSize winSize=CCDirector::sharedDirector()->getWinSize();

 CCString message;
 if(endReason == kEndReasonWin)
  message = "you win";
 else if(endReason == kEndReasonLose)
  message="you lose";

 CCLabelBMFont *label;//if(UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad
 label = CCLabelBMFont::create(message.getCString(),"TanksFont.fnt");
 label->setScale(0.1);
 label->setPosition(ccp(winSize.width/2,winSize.height*0.7));
 this->addChild(label);

 CCLabelBMFont *restartLabel;
 restartLabel = CCLabelBMFont::create("Restart","Tanksfont.fnt");

 CCMenuItemLabel *restartItem = CCMenuItemLabel::create(restartLabel,this,menu_selector(HelloWorld::restartTapped));
 restartItem->setScale(0.1);
 restartItem->setPosition(ccp(winSize.width/2,winSize.height*0.3));

 CCMenu* menu = CCMenu::createWithItems(restartItem,NULL);
 menu->setPosition(CCPointZero);
 this->addChild(menu);

 restartItem->runAction(CCScaleTo::create(0.5,4.0));
 label->runAction(CCScaleTo::create(0.5,4.0));

}

以上方法我曾在多个原型游戏中使用。如果看过系列的其它博文应该知道,其作用就是重新启动游戏,这里就不再解释这些了。如果觉得看不太明白,可以先从系列的开始看起。

 

接下来在update方法的开始添加以下代码:

 

if(this->gameOver) return;

 //2
 if( this->exit->boundingBox().intersectsRect(this->tank->boundingBox()))
 {
  this->endScene(kEndReasonWin);
 }

 //3
 CCArray*  childrenToRemove   = CCArray::create();
 CCArray* projectilesToDelete = CCArray::create();

 cocos2d::CCArray * childrens=_batchNode->getChildren();

 CCObject*pObject=NULL;

 CCARRAY_FOREACH(childrens,pObject)
 {
  CCSprite* sprite=(CCSprite*)pObject;
  //CCArray* monstersToDelete=CCArray::create();
  if(sprite->getTag() != 0)//bullet
  {
   if(this->isWallAtPosition(sprite->getPosition()))
    {
     projectilesToDelete->addObject(sprite);
           continue;
       }
  }
  if(sprite->getTag() == 1)//hero bullet
  {
   for(int j=enemyTanks->count()-1;j>=0;j--)
   {
    Tank* enemy = (Tank*)enemyTanks->objectAtIndex(j);

    if(sprite->boundingBox().intersectsRect(enemy->boundingBox()))
    {
     childrenToRemove->addObject(sprite);
     enemy->hp --;
     if(enemy->hp <=0)
     {
      CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect("explode3.wav");
      explosion->setPosition(enemy->getPosition());
      explosion->resetSystem();
      enemyTanks->removeObject(enemy);
      childrenToRemove->addObject(enemy);

     }
     else
     {
                       CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect("explode2.wav");
      
     }
    }
   }
  }
  if(sprite->getTag() == 2)//enemy bullet
  {
   if(sprite->boundingBox().intersectsRect(tank->boundingBox()))
   {
    childrenToRemove->addObject(sprite);
    tank->hp --;
    if(tank->hp <=0)
    {
     CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect("explode2.wav");
     explosion->setPosition(tank->getPosition());
     explosion->resetSystem();
     this->endScene(kEndReasonLose);
    }//dead
    else
    {
     explosion2->setPosition(tank->getPosition());
     explosion2->resetSystem();
     CocosDenshion::SimpleAudioEngine::sharedEngine()->playEffect("explode1.wav");
    }
   }
  }//tag==2
 }

 CCARRAY_FOREACH(childrenToRemove,pObject)
 {
  CCSprite* sprite = (CCSprite*)pObject;
  
  sprite->removeFromParentAndCleanup(true);
 }
 setViewpointCenter(tank->getPosition());

以上就是碰撞检查和游戏机制,这里稍微解释下:

 

1.开始记录游戏的状态,游戏是否结束。如果游戏已结束则无需做任何事。

2.如果坦克碰到出口,则玩家赢得胜利!

3.开始碰撞检测,有时候有的精灵在碰撞后需要从屏幕中删除(例如,当炮弹碰到坦克或障碍的时候,会被删除)。

4.对炮弹精灵设置标记,从而可以轻松将其识别

5.如果在炮弹的位置有障碍,则移除炮弹。

6.如果炮弹是由坦克英雄发射的,则检查它是否击中了敌军坦克,如果是,则将敌军坦克的HP减少(如果HP<=0则将其销毁)。同时还播放一个音效,以及激活一个爆炸的粒子系统。

7.与之类似,如果是敌军坦克发射的炮弹,则检查是否击中了坦克英雄,并进行相应的操作。当玩家的HP达到0时游戏以失败告终。

 

最后一步,在accelerometer:didAccelerate,ccTouchesBegan和ccTouchesMoved方法的前面添加以下代码:

if(gameOver) return;

 

编译运行游戏,现在就可以尽情的坦克大战了!

 

 

 

如何使用cocos2d <wbr>2.0开发类似坦克大战的游戏(且支持ARC)2

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值