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

 

声明:这个教程来自这里http://www.raywenderlich.com/6804/how-to-make-a-multi-directional-scrolling-shooter-part-1。 开发环境是xcode和cocos2d,这里做了移植,开发环境是使用vs2012和cocos2d-x,方便大家学习使用。 

  

开始

 1.新建Cocos2d-win32工程,工程名为"tanks",去除"Box2D"选项,勾选"Simple Audio Engine in Cocos Denshion"选项;
2.下载本游戏所需的资源这个项目的资源文件,将资源放置"Resources"目录下

里面会有:

  • Particle Designer设计的两个粒子特效 –两种不同类型的爆炸
  • 使用Texture Packer制作的两组精灵表 ,一个包含背景tile,应该包含前景精灵.
  • 我用Glyph Designer设计的一个字体,这个字体我们将在hud和游戏结束的时候使用
  • 我用Garage Band制作的一些背景音乐
  • 我用cxfr制作的声音特效.
  •  我用Tiled制作的tile地图.

最重要的事情显然是tile地图。我推荐你下载tiled软件,如果你还没有安装的话,用它打开tanks.tmx。看看都有什么

A tile map made with Tiled

正如你看到的。它是一个很简单的地图,只有三种tile块:水、草地和木头(桥),如果你右键点击水的tile块,然后点击属性,你将看到属性被定义为墙,这个待会在代码中将会被指出。 :

A tile map property defined with Tiled

这里只有一个层,命名为背景层,我们没有添加任何东西到这个地图层上,像坦克和退出啥的,我们将在代码中添加。

可以随意修改这个地图,更多使用tiled软件的信息,请看之前的文章基于tile游戏开发教程.

 

添加tile地图

现在我们添加tile地图到我们的场景中,你知道,这在cocos2dx来说非常容易。打开HelloWorldLayer.h添加两个变量到HelloWorldLayer类中:

cocos2d::CCTMXTiledMap* _tileMap;
 cocos2d::CCTMXLayer*    _bgLayer;

 

我们要保留tile地图到背景层中,因为我们经常需要使用它们。 .

打开类实现文件 HelloWorldLayer.cpp在init 函数中放入以下代码:

 

_tileMap = CCTMXTiledMap::create("tanks.tmx");
  this->addChild(_tileMap);

  _bgLayer = _tileMap->layerNamed("Background");

 

这里我们将生成tile地图,并把它添加到层当中,然后从地图中获取背景层,

编译运行,你将看到地图的左下角 :

Bottom left corner of the map

在这个游戏中我们想让我们的坦克从左上角开始, 这很容易,我们来建立一系列的辅助函数,我在很多tile游戏程序中使用这些辅助函数,因此你会发现这些函数能在你自己的项目中被非常方便的使用.

首先,我们需要一些方法来获取地图的宽和高,添加以下代码到类实现文件中HelloWorldLayer.cpp在init函数的上方

 

float HelloWorld::tileMapHeight()
{
 return _tileMap->getMapSize().height*_tileMap->getTileSize().height;
}
float HelloWorld::tileMapWidth()
{
 return _tileMap->getMapSize().width*_tileMap->getTileSize().width;

}

 

在tile地图中的地图尺寸的属性是一些tile块的号码尺寸,不是一个点,因此我们必须把tile块的大小乘以所在点的大小,下一步我们需要一些函数来见检查所给位置是否在地图中,同样还有tile块的坐标 .

如果你忘记tile坐标是什么了的话,每一个tile块在地图中都有一个坐标,从左上角(0,0)开始到右下角(99,99)结束(在我们这是这样定义地)

这是从之前的 tile-based gatile游戏教程中的截屏如下图:

 

Tile Coordinates in Tiled

因此添加下列用于确认位置和tile坐标函数到tilemapwidth函数后面


bool HelloWorld::isValidPosition(CCPoint position)
{
 if(position.x <0 ||position.y <0||position.x >this->tileMapWidth() ||position.y>this->tileMapHeight())
  return false;
 else
  return true;
}
bool HelloWorld::isValidTileCoord(CCPoint tileCoord)
{
 if(tileCoord.x <0 || tileCoord.y <0||tileCoord.x >= _tileMap->getMapSize().width||tileCoord.y >=_tileMap->getMapSize().height)
  return false;
 else
  return true;
}

 

这些都比较好解释,很显然不合法的位置和坐标都会在地图的外面, 上边界就是地图的宽和高,也是分别在tile块内 .

下一步,添加一个方法来做位置和tile坐标转换 :


CCPoint HelloWorld::tileCoordForPosition(CCPoint position)
{
 if(!isValidPosition(position)) return ccp(-1,-1);
 
 int x=position.x/_tileMap->getTileSize().width;
 int y=(tileMapHeight()-position.y)/_tileMap->getTileSize().height;

 return ccp(x,y);
}
CCPoint HelloWorld::positionForTileCoord(CCPoint tileCoord)
{
 int x=(tileCoord.x*_tileMap->getTileSize().width)+_tileMap->getTileSize().width/2;
 int y=(tileMapHeight()-(tileCoord.y *_tileMap->getTileSize().height)-_tileMap->getTileSize().height/2);
 return ccp(x,y);
}

 

第一个函数是把位置点转成tile坐标,转换x坐标非常容易,它只是依靠每个块上的点来把这些点的数字号来划分出来,y坐标也类似

第二个函数正好相反-tile块坐坐标转位置点,要注意,一个tile内可以返回很多个点,这里我们返回tile块的中心点。因为使用cocos2d把精灵放在tile块中心看上去更友好些。 .

现在有了这些方便的函数,我们就能建立一个 routine来允许在场景中滚动地图到中心位置

添加下列代码

void  HelloWorld::setViewpointCenter(CCPoint position)
{
 CCSize winSize = CCDirector::sharedDirector()->getWinSize();

 int x=MAX(position.x,winSize.width/2/this->getScale());
 int y=MAX(position.y,winSize.height/2/this->getScale());

 x=MIN(x,tileMapWidth()-winSize.width/2/this->getScale());
 y=MIN(y,tileMapHeight()-winSize.height/2/this->getScale());

 CCPoint actualPosition=ccp(x,y);

 CCPoint centerOfView=ccp(winSize.width/2,winSize.height/2);
 CCPoint viewPoint = ccpSub(centerOfView,actualPosition);

 _tileMap->setPosition(viewPoint);
}

 

最容易的解释方法是通过图片 :

Tile Map Scrolling

给出一个中心点,我们移动tile地图,如果在场景中心剪掉我们的目标位置,我们将得到一个错误,我们能移动地图的多少.

仅仅不好弄得部分是有一些点我们不能设置到中心。 如果我们试图把地图的中心放在一个小于屏幕一半的点上,那么黑色的区域就会被看到,那样就太难看了,因为如果我们把试图把中心位置放在地图的上侧的点的时候,我们就要小心检查下它 .

现在我们试试这些辅助函数,添加下列代码到init函数中 

CCPoint spawnTileCoord = ccp(4,4);

  CCPoint spawnPos=positionForTileCoord(spawnTileCoord);

  setViewpointCenter(spawnPos);

 

编译运行,现在我们看到地图的左上角,我们的坦克即将在这个位置诞生!

Upper left of the map

 

添加坦克

是添加我们的英雄的时候了。

 建立一个新的文件,类命名为Tank, 基类为CCSprite.打开Tank.h替换以下代码:

#pragma once
#include "c:\cocos2d-2.1beta3-x-2.1.1\cocos2dx\sprite_nodes\ccsprite.h"
 

class HelloWorld;

class Tank :
 public cocos2d::CCSprite
{
public:
 Tank(void);
 ~Tank(void);
 CREATE_FUNC(Tank);

 int _type;
 HelloWorld* _layer;
 cocos2d::CCPoint _targetPosition;
 bool moving;
 int hp;
 cocos2d::CCPoint actualTargetPerSecond;
 

 void initWithLayer(HelloWorld* layer,int type,int hp);
 void moveToward(cocos2d::CCPoint targetPosition);
 void updateMove(float dt);
 void update(float dt);

 void calcNextMove();
};

我们来看一下这个类中的变量: :

  • type: 表示我们有两种坦克,所以这个变量的值或1或2,取决于我们选择的精灵.
  • layer: 之后我们将需要在这个层中调用一些坦克类中的函数,因为在这里存一个指针.
  • targetPosition: 坦克通常会有一个要移动到一个位置点,这个点的位置存下来
  • moving: 保留坦克当前是移动还是停止.
  • hp:保留坦克的hp属性,待会我们要用到

现在打开tank类实现文件Tank.cpp 写入下列代码:

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

Tank::Tank(void)
{
}


Tank::~Tank(void)
{
}
 
void Tank::initWithLayer(HelloWorld* layer,int type,int hp)
{
 do
 {
 CC_BREAK_IF(!CCSprite::initWithSpriteFrameName("tank1.png"));

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

 _layer=layer;
 _type = type;
 moving=false;
 this->hp=hp;
 this->scheduleUpdateWithPriority(-1);
 
 }while(0);
 return;
}
void Tank::moveToward(CCPoint targetPosition)
{
 //这里应该是设置移动参数的地方。设了目标就开始移动了。点一次设置一次,

 _targetPosition = targetPosition;
 moving = true;
 
 CCPoint cp=this->getPosition();
 
 CCPoint offset = ccpSub(_targetPosition,this->getPosition());

 float MIN_OFFSET = 10;
 if(ccpLength(offset) <= MIN_OFFSET) {this->moving=false;return;}

 CCPoint targetVector=ccpNormalize(offset);
 //
 float POINTS_PER_SECOND=150;
 actualTargetPerSecond = ccpMult(targetVector,POINTS_PER_SECOND);
 
}

void Tank::calcNextMove()
{
}
void Tank::updateMove(float dt)
{
 if(!this->moving) return;
 
 CCPoint offset = ccpSub(_targetPosition,this->getPosition());
 float MIN_OFFSET = 10;
 if(ccpLength(offset) <= MIN_OFFSET) {this->moving=false;return;}


 CCPoint tp=ccpMult(actualTargetPerSecond,dt);
 
 CCPoint oldPosition= this->getPosition();
 CCPoint actualTarget= ccpAdd(oldPosition,tp);

 this->setPosition(actualTarget);

 if(_layer->isWallAtRect(this->boundingBox()))
 {
  this->setPosition(oldPosition);
  this->calcNextMove();
 }
}

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

 

这个初始化非常的简单,放初始化变量,调用刷新函数,你可能还不了解任何CCNode类的update刷新函数,但是现在你已经做到了。注意优先权设置为-1,因为我们想让坦克的刷新运行在场景刷新的前面(场景的优先权默认设置为0)

移动到前面那个刷新目标位置点的函数updateMove是所有动作的地方,这个函数每一帧都会调用一次,让我们一行一行的看这个代码: (关于这部分的理解,请看最后附的学习资料,就能完全明白是怎么回事了!!)

  1. 如果参数movin的g值为false,当程序刚开始运行的时候。这个参数将会被设置为 false.
  2. 目标位置和当前位置的差值,就是我们要去的那个方向的向量
  3. 检测这条线的长度,看是否小于10个点,如果是,我们距离太近了,就直接返回不作任何操作。
  4. 用ccpNormalize把这个向量弄成一个向量单位, 这使得我们能够很容易下一步在一个线上生成一个长度值. 
  5. 乘以向量值来设置我们的坦克移动的速度(150/秒),这个结果只是一个向量值,单位是像素点/秒,表明坦克移动速度。 .
  6. 这个方法是一秒钟调用很多次,因为我们乘以这个向量值依靠delta time (大概 1/60 一秒钟) 来指明我们应该真实的移动距离
  7. 设置坦克的位置,我们也保留了旧的位置,这个将来要用 .

现在让我们开始使用这个坦克类,在 HelloWorldLayer.h实现下列修改:

Tank *tank;
 cocos2d::CCSpriteBatchNode* _batchNode;

在HelloWorldLayer.cpp添加下列代码到init函数中:

 

  CCSpriteFrameCache::sharedSpriteFrameCache()->addSpriteFramesWithFile("sprites2.plist");
     _batchNode=  CCSpriteBatchNode::create("sprites2.pvr.ccz");
        _batchNode->getTexture()->setAliasTexParameters();
  _tileMap->addChild(_batchNode);
  
 
  this->tank =new Tank();
  tank->initWithLayer(this,1,5);
  this->tank->setPosition(spawnPos);

   _batchNode->addChild(this->tank);
  
  
  this->setTouchEnabled(true);
  scheduleUpdate();

 

在添加触屏函数
void HelloWorld::ccTouchesBegan(CCSet *pTouches, CCEvent *pEvent)
{
 CCSetIterator it = pTouches->begin();

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

 CCPoint mapLocation = _tileMap->convertTouchToNodeSpace(touch);

 tank->moving=true;
 tank->moveToward(mapLocation);
}


void HelloWorld::ccTouchesMoved(CCSet *pTouches, CCEvent *pEvent)
{
 CCSetIterator it = pTouches->begin();

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

 CCPoint mapLocation = _tileMap->convertToNodeSpace(touch->getLocationInView());

 //tank->moving=true;
 //tank->moveToward(mapLocation);
}
   
void HelloWorld::ccTouchesEnded(CCSet *pTouches, CCEvent *pEvent)
{
}


void HelloWorld::update(float dt)
{
 setViewpointCenter(tank->getPosition());
}

这很简单,我们生成了一个batchnode给精灵们,然后添加到地图层,因此我们能滚动地图,batchnode中的精灵也会跟着滚动

我们生成了一个坦克并添加到batchnode上,我们设置了触屏函数,调用moveToward函数,每次刷新使得tank一直处于视图中心位置

运行一下,就会看到我们的坦克可以移动到地图的任何地方。

检查无法穿越的区域

现在的游戏很不错了,但存在一个致命的问题,我们的坦克不是两栖的,因此不能让它从水中穿越。

 

为此需要添加几个辅助方法。 在HelloWorldLayer.cpp中,在init方法的上面添加以下几个方法

 


bool HelloWorld::isPropTileCoor(char* prop,CCPoint tileCoord,CCTMXLayer* layer)
{
 bool bRet = false;
 do
 {
  CC_BREAK_IF(!this->isValidTileCoord(tileCoord));

  int gid=layer->tileGIDAt(tileCoord);

  CCDictionary *properties=_tileMap->propertiesForGID(gid);

  if(properties == NULL) break;

  if(properties->objectForKey(prop) !=NULL)
   bRet = true;

 }while(0);
 return bRet;
}

bool HelloWorld::isPropPosition(char* prop,CCPoint position,CCTMXLayer* layer)
{
 CCPoint tileCoord = this->tileCoordForPosition(position);
 return this->isPropTileCoor(prop,tileCoord,layer);
}
bool HelloWorld::isWallAtTileCoord(CCPoint tileCoord)
{
 return isPropTileCoor("Wall",tileCoord,_bgLayer);

}
bool HelloWorld::isWallAtPosition(CCPoint position)
{
 CCPoint tileCoord=tileCoordForPosition(position);
 if(!isValidPosition(tileCoord)) return true;

 return isWallAtTileCoord(tileCoord);

}
bool HelloWorld::isWallAtRect(CCRect rect)
{
 CCPoint lowerLeft=ccp(rect.origin.x,rect.origin.y);
 CCPoint upperLeft=ccp(rect.origin.x,rect.origin.y+rect.size.height);
 CCPoint lowerRight=ccp(rect.origin.x+rect.size.width,rect.origin.y);
 CCPoint upperRight=ccp(rect.origin.x+rect.size.width,rect.origin.y+rect.size.height);

 return (isWallAtPosition(lowerLeft)||isWallAtPosition(upperLeft)||isWallAtPosition(lowerRight)||isWallAtPosition(upperRight));
}

以上只是一些辅助性的方法,用于检查指定的瓦片坐标/位置/矩形是否具有”Wall”属性。如果对此有疑问,不妨看看之前的博文

打开HelloWorldLayer.h,重新声明以上的方法,以便从类的外面访问这些方法:

 // default implements are used to call script callback if exist
    void ccTouchesBegan(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);
    void ccTouchesMoved(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);
    void ccTouchesEnded(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);
    void ccTouchesCancelled(cocos2d::CCSet *pTouches, cocos2d::CCEvent *pEvent);
    void update(float dt);
 bool isPropTileCoor(char* prop,cocos2d::CCPoint tileCoord,cocos2d::CCTMXLayer* layer);
 bool isPropPosition(char* prop,cocos2d::CCPoint position,cocos2d::CCTMXLayer* layer);
 bool isWallAtRect(cocos2d::CCRect rect);
 bool isWallAtTileCoord(cocos2d::CCPoint tileCoord);
 bool isWallAtPosition(cocos2d::CCPoint position);

添加加速计支持

对于这个游戏,我们需要使用加速计来进行游戏操控。在HelloWorldLayerm.cpp中添加以下方法


void HelloWorld::onEnterTransitionDidFinish()
{
 this->setAccelerometerEnabled(true);

}
void HelloWorld::accelerometer(CCAccelerometer* accelerometer,CCAcceleration* acceleration)
{
    #define kFilteringFactor 0.75

 static float rollingX=0,rollingY=0,rollingZ=0;
 rollingX = (acceleration->x * kFilteringFactor) +
    (rollingX * (1.0 - kFilteringFactor));   
    rollingY = (acceleration->y * kFilteringFactor) +
    (rollingY * (1.0 - kFilteringFactor));   
    rollingZ = (acceleration->z * kFilteringFactor) +
    (rollingZ * (1.0 - kFilteringFactor));
 
    float accelX = rollingX;
    float accelY = rollingY;
    float accelZ = rollingZ;

 CCPoint moveTo = tank->getPosition();
 if (accelX > 0.5) {
        moveTo.y -= 300;
    } else if (accelX < 0.4) {
        moveTo.y += 300;
    }
    if (accelY < -0.1) {
        moveTo.x -= 300;
    } else if (accelY > 0.1) {
        moveTo.x += 300;
    }

 tank->moving=true;
 tank->moveToward(moveTo);
}

 

在上面的代码中,我们在onEnterTransitionDidFinish方法中设置isAccelerometerEnabled。

然后使用下面的方法来操控坦克的运动。这些内容在之前的系列博文中都有提及,因此这里不再一一赘述

添加音乐

当然,我也没忘了给大家提供一些很酷的音效。只需在HelloWorlayer.cpp中做出以下修改即可:

 

在文件的顶部添加一行代码

:#include "SimpleAudioEngine.h"

CocosDenshion::SimpleAudioEngine::sharedEngine()->playBackgroundMusic("bgMusic.caf");
  CocosDenshion::SimpleAudioEngine::sharedEngine()->preloadEffect("explode.wav");
  CocosDenshion::SimpleAudioEngine::sharedEngine()->preloadEffect("tank1Shoot.wav");
  CocosDenshion::SimpleAudioEngine::sharedEngine()->preloadEffect("tank2Shoot.wav");

编译运行游戏,一个基本的游戏就成型了。

 该项目的示例代码在此:http://download.csdn.net/detail/hany3000/5204725 

 

 

 

 附:关于精灵体移动的学习的辅助资料:

 

cocos里边以顺时针为正方向,所以所有的度数按以前的习惯求出来后都要求反

cocos只能处理-180~180的度数范围,所以求出来的度数始终是这个范围(cocos里用反正切来求度数可以保证度数在这个范围)

弧度定义是长度等于半径的弧所对应的角度值为1弧度。所以360度=2pi    1弧度=180/pi   度 

制作需要旋转的精灵图片是总是让初始方向朝右,即和0度吻合,这样求旋转角度的时候比较简便。(即假定初始边为0度)

二:质点移动,p1是炮塔,p2是p1发射的子弹,假设在时刻t1子弹在p2点,求1秒后子弹的位置p3

假设子弹速度320,也就是说p2p3=320

已知p1(100,100), p2(270,200),p2p3=320,求p5

1.首先求p1p2长度

2.求角度:cos(a)=0.85,sin(a)=0.5  (<a=30度左右)

3.求p5, p5.x=p2.x+p2p3*cos(a) p5.y=p2.y+p2p3*sin(a)

具体到cocos里边就是这样的:

CGPoint shootVector = ccpSub(self.target.position, self.position);//1距离
    CGPoint normalizedShootVector = ccpNormalize(shootVector);//2角度(cos(a)和sin(a))
    CGPoint overshotVector = ccpMult(normalizedShootVector, 320);//3新距离=角度*射程
    CGPoint offscreenPoint = ccpAdd(self.position, overshotVector);//4新坐标=p1+新距离

[self.nextProjectile runAction:[CCMoveTo actionWithDuration:delta position:offscreenPoint]


这下明白了吧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值