声明:这个教程来自这里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。看看都有什么
正如你看到的。它是一个很简单的地图,只有三种tile块:水、草地和木头(桥),如果你右键点击水的tile块,然后点击属性,你将看到属性被定义为墙,这个待会在代码中将会被指出。 :
这里只有一个层,命名为背景层,我们没有添加任何东西到这个地图层上,像坦克和退出啥的,我们将在代码中添加。
可以随意修改这个地图,更多使用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地图,并把它添加到层当中,然后从地图中获取背景层,
编译运行,你将看到地图的左下角 :
在这个游戏中我们想让我们的坦克从左上角开始, 这很容易,我们来建立一系列的辅助函数,我在很多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坐标函数到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地图,如果在场景中心剪掉我们的目标位置,我们将得到一个错误,我们能移动地图的多少.
仅仅不好弄得部分是有一些点我们不能设置到中心。 如果我们试图把地图的中心放在一个小于屏幕一半的点上,那么黑色的区域就会被看到,那样就太难看了,因为如果我们把试图把中心位置放在地图的上侧的点的时候,我们就要小心检查下它 .
现在我们试试这些辅助函数,添加下列代码到init函数中
CCPoint spawnTileCoord = ccp(4,4);
CCPoint spawnPos=positionForTileCoord(spawnTileCoord);
setViewpointCenter(spawnPos);
编译运行,现在我们看到地图的左上角,我们的坦克即将在这个位置诞生!
添加坦克
是添加我们的英雄的时候了。
建立一个新的文件,类命名为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是所有动作的地方,这个函数每一帧都会调用一次,让我们一行一行的看这个代码: (关于这部分的理解,请看最后附的学习资料,就能完全明白是怎么回事了!!)
- 如果参数movin的g值为false,当程序刚开始运行的时候。这个参数将会被设置为 false.
- 目标位置和当前位置的差值,就是我们要去的那个方向的向量
- 检测这条线的长度,看是否小于10个点,如果是,我们距离太近了,就直接返回不作任何操作。
- 用ccpNormalize把这个向量弄成一个向量单位, 这使得我们能够很容易下一步在一个线上生成一个长度值.
- 乘以向量值来设置我们的坦克移动的速度(150/秒),这个结果只是一个向量值,单位是像素点/秒,表明坦克移动速度。 .
- 这个方法是一秒钟调用很多次,因为我们乘以这个向量值依靠delta time (大概 1/60 一秒钟) 来指明我们应该真实的移动距离
- 设置坦克的位置,我们也保留了旧的位置,这个将来要用 .
现在让我们开始使用这个坦克类,在 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);
}
在上面的代码中,我们在onEnterTransitionDidFini
然后使用下面的方法来操控坦克的运动。这些内容在之前的系列博文中都有提及,因此这里不再一一赘述
添加音乐
当然,我也没忘了给大家提供一些很酷的音效。只需在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]
这下明白了吧。