声明:这个教程来自这里http://www.raywenderlich.com/6804/how-to-make-a-multi-directional-scrolling-shooter-part-1。由于找不到中文的了。只好自己翻译啦,有版权问题请联系我http://blog.csdn.net/hany3000。
一周以前大家投票说希望有一个教程是制作一个多方向滚动射击游戏,你们的希望就是我的动力,在这篇文章里,我们将制作一个tile游戏,游戏中你将使用加速器来指挥一个坦克, 你的目标是活着走出关口!
在这篇文章的开头,你将需要基于cocos2d 2.x一些经验的帮助,移植到arc,使用数学向量功能,使用tile地图等等。
这篇文章假设你已经了解cocos2d的一些基础知识,如果你还是个新手,你最好是去这里一些 Cocos2D教程学学基础知识,至少在学这篇文章之前,你最好是学习过这篇tile游戏制作的文章:如何制作tile游戏教程。
准备好开发环境,我们开始吧。
开始
我们在项目中将要使用Cocos2D 2.X,如果你还没有安装的话,首先要 下载它,双击这个文件来解压缩,然后用下面的命令行来安装模板:
cd ~/Downloads/cocos2d-iphone-2.0-beta
./install-templates.sh -f -u
然后打开xcode,使用cocos2d模板生成一个新的项目,名字为Tanks。
在这个项目里我们想使用arc来We want to use ARC来做一个简单的内存管理,但是默认的模板没有安装使用arc,因为让我们使用以下5个步骤来把这个功能加上,
- 在xcode项目中,按control点击libs目录,然后点击删除,然后再点击删除来永久删除文件,这将把cocos2d的文件从项目中移除,这就可以了,因为我们一会还要分别增加链接,我们做这个是为了配置项目以使得它能使用arc的功能.
- 找到我们下载cocos2d2.0的地方,找出里面的cocos2d-ios.xcodeproj,拉到我们的项目中
- 点击你的项目,选择Tanks target, 到Build Phases tab. 展开Link Binary With Libraries选项, 点击+ 按钮, 在列表中选择libcocos2d.a 和libCocosDenhion.a , 然后点击添加add.
- 点击build setting按钮,滚动到Search Paths 选项. 设置 Always Search User Paths 为YES, 双击User Header Search Paths, 在路径中输入你存储cocos2d2.0的目录地址,确定 Recursive 是被选中的。.
- 从主菜单选择 Edit\Refactor\Convert to Objective-C ARC. 从下拉选项中选择所有文件,按向导进行下去,一直到完成。 .
就是这样了!编译并运行,确定都没有问题。你应该看到正常的helloworld的界面。 .
但是你应该注意到它是一个竖屏模式,我们在游戏中想要横屏模式,因为打开RootViewController.m 文件,却动 shouldAutorotateToInterfaceOrientation 跟下列代码一样:
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
return ( UIInterfaceOrientationIsLandscape( interfaceOrientation ) );
} |
编译运行,现在我们就有了一个是arc编译的最新版cocos2d 2.0的横屏游戏!
添加资源文件
第一件事情就是-下载 这个项目的资源文件然后把两个目录 (Art and Sounds) 拉到项目中,确定“Copy items into destination group’s folder”选项被选中, 还有“Create groups for any added folders”也要被选中,点击完成,.
里面会有:
最重要的事情显然是tile地图。我推荐你下载tiled软件,如果你还没有安装的话,用它打开tanks.tmx。看看都有什么
正如你看到的。它是一个很简单的地图,只有三种tile块:水、草地和木头(桥),如果你右键点击水的tile块,然后点击属性,你将看到属性被定义为墙,这个待会在代码中将会被指出。 :
这里只有一个层,命名为背景层,我们没有添加任何东西到这个地图层上,像坦克和退出啥的,我们将在代码中添加。
可以随意修改这个地图,更多使用tiled软件的信息,请看之前的文章基于tile游戏开发教程.
添加tile地图和助手
现在我们添加tile地图到我们的场景中,你知道,这在cocos2d来说非常容易。打开HelloWorldLayer.h添加两个变量到HelloWorldLayer类中:
CCTMXTiledMap * _tileMap;
CCTMXLayer * _bgLayer; |
我们要保留tile地图到背景层中,因为我们经常需要使用它们。 .
打开类实现文件 HelloWorldLayer.m 在init 函数中放入以下代码:
-(id) init
{
if( (self=[super init])) {
_tileMap = [CCTMXTiledMap tiledMapWithTMXFile:@"tanks.tmx"];
[self addChild:_tileMap];
_bgLayer = [_tileMap layerNamed:@"Background"];
}
return self;
} |
这里我们将生成tile地图,并把它添加到层当中,然后从地图中获取背景层,
编译运行,你将看到地图的左下角 :
在这个游戏中我们想让我们的坦克从左上角开始, 这很容易,我们来建立一系列的辅助函数,我在很多tile游戏程序中使用这些辅助函数,因此你会发现这些函数能在你自己的项目中被非常方便的使用.
首先,我们需要一些方法来获取地图的宽和高,添加以下代码到类实现文件中HelloWorldLayer.m在init函数的上方
- (float)tileMapHeight {
return _tileMap.mapSize.height * _tileMap.tileSize.height;
}
- (float)tileMapWidth {
return _tileMap.mapSize.width * _tileMap.tileSize.width;
} |
在tile地图中的地图尺寸的属性是一些tile块的号码尺寸,不是一个点,因此我们必须把tile块的大小乘以所在点的大小,下一步我们需要一些函数来见检查所给位置是否在地图中,同样还有tile块的坐标 .
如果你忘记tile坐标是什么了的话,每一个tile块在地图中都有一个坐标,从左上角(0,0)开始到右下角(99,99)结束(在我们这是这样定义地)
这是从之前的 tile-based gatile游戏教程中的截屏如下图:
因此添加下列用于确认位置和tile坐标函数到tilemapwidth函数后面
- (BOOL)isValidPosition:(CGPoint)position {
if (position.x < 0 ||
position.y < 0 ||
position.x > [self tileMapWidth] ||
position.y > [self tileMapHeight]) {
return FALSE;
} else {
return TRUE;
}
}
- (BOOL)isValidTileCoord:(CGPoint)tileCoord {
if (tileCoord.x < 0 ||
tileCoord.y < 0 ||
tileCoord.x >= _tileMap.mapSize.width ||
tileCoord.y >= _tileMap.mapSize.height) {
return FALSE;
} else {
return TRUE;
}
} |
这些都比较好解释,很显然不合法的位置和坐标都会在地图的外面, 上边界就是地图的宽和高,也是分别在tile块内 .
下一步,添加一个方法来做位置和tile坐标转换 :
- (CGPoint)tileCoordForPosition:(CGPoint)position {
if (![self isValidPosition:position]) return ccp(-1,-1);
int x = position.x / _tileMap.tileSize.width;
int y = ([self tileMapHeight] - position.y) / _tileMap.tileSize.height;
return ccp(x, y);
}
- (CGPoint)positionForTileCoord:(CGPoint)tileCoord {
int x = (tileCoord.x * _tileMap.tileSize.width) + _tileMap.tileSize.width/2;
int y = [self tileMapHeight] - (tileCoord.y * _tileMap.tileSize.height) - _tileMap.tileSize.height/2;
return ccp(x, y);
} |
第一个函数是把位置点转成tile坐标,转换x坐标非常容易,它只是依靠每个块上的点来把这些点的数字号来划分出来,y坐标也类似
第二个函数正好相反-tile块坐坐标转位置点,要注意,一个tile内可以返回很多个点,这里我们返回tile块的中心点。因为使用cocos2d把精灵放在tile块中心看上去更友好些。 .
现在有了这些方便的函数,我们就能建立一个 routine来允许在场景中滚动地图到中心位置
添加下列代码
-(void)setViewpointCenter:(CGPoint) position {
CGSize winSize = [[CCDirector sharedDirector] winSize];
int x = MAX(position.x, winSize.width / 2 / self.scale);
int y = MAX(position.y, winSize.height / 2 / self.scale);
x = MIN(x, [self tileMapWidth] - winSize.width / 2 / self.scale);
y = MIN(y, [self tileMapHeight] - winSize.height/ 2 / self.scale);
CGPoint actualPosition = ccp(x, y);
CGPoint centerOfView = ccp(winSize.width/2, winSize.height/2);
CGPoint viewPoint = ccpSub(centerOfView, actualPosition);
_tileMap.position = viewPoint;
} |
最容易的解释方法是通过图片 :
给出一个中心点,我们移动tile地图,如果在场景中心剪掉我们的目标位置,我们将得到一个错误,我们能移动地图的多少.
仅仅不好弄得部分是有一些点我们不能设置到中心。 如果我们试图把地图的中心放在一个小于屏幕一半的点上,那么黑色的区域就会被看到,那样就太难看了,因为如果我们把试图把中心位置放在地图的上侧的点的时候,我们就要小心检查下它 .
现在我们试试这些辅助函数,添加下列代码到init函数中
CGPoint spawnTileCoord = ccp(4,4);
CGPoint spawnPos = [self positionForTileCoord:spawnTileCoord];
[self setViewpointCenter:spawnPos]; |
编译运行,现在我们看到地图的左上角,我们的坦克即将在这个位置诞生!
添加坦克
是添加我们的英雄的时候了。
时候iOS\Cocoa Touch\Objective-C class template模板建立一个新的文件,类命名为Tank, 基类为CCSprite.打开Tank.h替换以下代码:
#import "cocos2d.h"
@class HelloWorldLayer;
@interface Tank : CCSprite {
int _type;
HelloWorldLayer * _layer;
CGPoint _targetPosition;
}
@property (assign) BOOL moving;
@property (assign) int hp;
- (id)initWithLayer:(HelloWorldLayer *)layer type:(int)type hp:(int)hp;
- (void)moveToward:(CGPoint)targetPosition;
@end |
我们来看一下这个类中的变量: :
- type: 表示我们有两种坦克,所以这个变量的值或1或2,取决于我们选择的精灵.
- layer: 之后我们将需要在这个层中调用一些坦克类中的函数,因为在这里存一个指针.
- targetPosition: 坦克通常会有一个要移动到一个位置点,这个点的位置存下来
- moving: 保留坦克当前是移动还是停止.
- hp:保留坦克的hp属性,待会我们要用到
现在打开tank类实现文件Tank.m 写入下列代码:
#import "Tank.h"
#import "HelloWorldLayer.h"
@implementation Tank
@synthesize moving = _moving;
@synthesize hp = _hp;
- (id)initWithLayer:(HelloWorldLayer *)layer type:(int)type hp:(int)hp {
NSString *spriteFrameName = [NSString stringWithFormat:@"tank%d_base.png", type];
if ((self = [super initWithSpriteFrameName:spriteFrameName])) {
_layer = layer;
_type = type;
self.hp = hp;
[self scheduleUpdateWithPriority:-1];
}
return self;
}
- (void)moveToward:(CGPoint)targetPosition {
_targetPosition = targetPosition;
}
- (void)updateMove:(ccTime)dt {
// 1
if (!self.moving) return;
// 2
CGPoint offset = ccpSub(_targetPosition, self.position);
// 3
float MIN_OFFSET = 10;
if (ccpLength(offset) < MIN_OFFSET) return;
// 4
CGPoint targetVector = ccpNormalize(offset);
// 5
float POINTS_PER_SECOND = 150;
CGPoint targetPerSecond = ccpMult(targetVector, POINTS_PER_SECOND);
// 6
CGPoint actualTarget = ccpAdd(self.position, ccpMult(targetPerSecond, dt));
// 7
CGPoint oldPosition = self.position;
self.position = actualTarget;
}
- (void)update:(ccTime)dt {
[self updateMove:dt];
}
@end |
这个初始化非常的简单,放初始化变量,调用刷新函数,你可能还不了解任何CCNode类的update刷新函数,但是现在你已经做到了。注意优先权设置为-1,因为我们想让坦克的刷新运行在场景刷新的前面(场景的优先权默认设置为0)
移动到前面那个刷新目标位置点的函数updateMove是所有动作的地方,这个函数每一帧都会调用一次,让我们一行一行的看这个代码: (关于这部分的理解,请看最后附的学习资料,就能完全明白是怎么回事了!!)
- 如果参数movin的g值为false,当程序刚开始运行的时候。这个参数将会被设置为 false.
- 目标位置和当前位置的差值,就是我们要去的那个方向的向量
- 检测这条线的长度,看是否小于10个点,如果是,我们距离太近了,就直接返回不作任何操作。
- 用ccpNormalize把这个向量弄成一个向量单位, 这使得我们能够很容易下一步在一个线上生成一个长度值.
- 乘以向量值来设置我们的坦克移动的速度(150/秒),这个结果只是一个向量值,单位是像素点/秒,表明坦克移动速度。 .
- 这个方法是一秒钟调用很多次,因为我们乘以这个向量值依靠delta time (大概 1/60 一秒钟) 来指明我们应该真实的移动距离
- 设置坦克的位置,我们也保留了旧的位置,这个将来要用 .
现在让我们开始使用这个坦克类,在 HelloWorldLayer.h实现下列修改:
// Before the @interface
@class Tank;
// After the @interface
@property (strong) Tank * tank;
@property (strong) CCSpriteBatchNode * batchNode; |
在HelloWorldLayer.m添加下列代码:
// At the top of the file
#import "Tank.h"
// Right after the @implementation
@synthesize batchNode = _batchNode;
@synthesize tank = _tank;
// Inside init
_batchNode = [CCSpriteBatchNode batchNodeWithFile:@"sprites.png"];
[_tileMap addChild:_batchNode];
[[CCSpriteFrameCache sharedSpriteFrameCache] addSpriteFramesWithFile:@"sprites.plist"];
self.tank = [[Tank alloc] initWithLayer:self type:1 hp:5];
self.tank.position = spawnPos;
[_batchNode addChild:self.tank];
self.isTouchEnabled = YES;
[self scheduleUpdate];
// After init
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch * touch = [touches anyObject];
CGPoint mapLocation = [_tileMap convertTouchToNodeSpace:touch];
self.tank.moving = YES;
[self.tank moveToward:mapLocation];
}
- (void)ccTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch * touch = [touches anyObject];
CGPoint mapLocation = [_tileMap convertTouchToNodeSpace:touch];
self.tank.moving = YES;
[self.tank moveToward:mapLocation];
}
- (void)update:(ccTime)dt {
[self setViewpointCenter:self.tank.position];
} |
这很简单,我们生成了一个batchnode给精灵们,然后添加到地图层,因此我们能滚动地图,batchnode中的精灵也会跟着滚动
我们生成了一个坦克并添加到batchnode上,我们设置了触屏函数,调用moveToward函数,每次刷新使得tank一直处于视图中心位置
运行一下,就会看到我们的坦克可以移动到地图的任何地方。
检查无法穿越的区域
现在的游戏很不错了,但存在一个致命的问题,我们的坦克不是两栖的,因此不能让它从水中穿越。
为此需要添加几个辅助方法。 在HelloWorldLayer.m中,在init方法的上面添加以下几个方法
-(BOOL)isProp:(NSString*)prop atTileCoord:(CGPoint)tileCoord forLayer:(CCTMXLayer *)layer {
if (![self isValidTileCoord:tileCoord]) return NO;
int gid = [layer tileGIDAt:tileCoord];
NSDictionary * properties = [_tileMap propertiesForGID:gid];
if (properties == nil) return NO;
return [properties objectForKey:prop] != nil;
}
-(BOOL)isProp:(NSString*)prop atPosition:(CGPoint)position forLayer:(CCTMXLayer *)layer {
CGPoint tileCoord = [self tileCoordForPosition:position];
return [self isProp:prop atTileCoord:tileCoord forLayer:layer];
}
- (BOOL)isWallAtTileCoord:(CGPoint)tileCoord {
return [self isProp:@"Wall" atTileCoord:tileCoord forLayer:_bgLayer];
}
- (BOOL)isWallAtPosition:(CGPoint)position {
CGPoint tileCoord = [self tileCoordForPosition:position];
if (![self isValidPosition:tileCoord]) return TRUE;
return [self isWallAtTileCoord:tileCoord];
}
- (BOOL)isWallAtRect:(CGRect)rect {
CGPoint lowerLeft = ccp(rect.origin.x, rect.origin.y);
CGPoint upperLeft = ccp(rect.origin.x, rect.origin.y+rect.size.height);
CGPoint lowerRight = ccp(rect.origin.x+rect.size.width, rect.origin.y);
CGPoint upperRight = ccp(rect.origin.x+rect.size.width, rect.origin.y+rect.size.height);
return ([self isWallAtPosition:lowerLeft] || [self isWallAtPosition:upperLeft] ||
[self isWallAtPosition:lowerRight] || [self isWallAtPosition:upperRight]);
} |
以上只是一些辅助性的方法,用于检查指定的瓦片坐标/位置/矩形是否具有”Wall”属性。如果对此有疑问,不妨看看之前的博文
打开HelloWorldLayer.h,重新声明以上的方法,以便从类的外面访问这些方法:
- (float)tileMapHeight;
- (float)tileMapWidth;
- (BOOL)isValidPosition:(CGPoint)position;
- (BOOL)isValidTileCoord:(CGPoint)tileCoord;
- (CGPoint)tileCoordForPosition:(CGPoint)position;
- (CGPoint)positionForTileCoord:(CGPoint)tileCoord;
- (void)setViewpointCenter:(CGPoint) position;
- (BOOL)isProp:(NSString*)prop atTileCoord:(CGPoint)tileCoord forLayer:(CCTMXLayer *)layer;
- (BOOL)isProp:(NSString*)prop atPosition:(CGPoint)position forLayer:(CCTMXLayer *)layer;
- (BOOL)isWallAtTileCoord:(CGPoint)tileCoord;
- (BOOL)isWallAtPosition:(CGPoint)position;
- (BOOL)isWallAtRect:(CGRect)rect; |
接下来对Tank.m做出以下修改:
在updateMove的方法前面添加以下方法
Tank.m:
// Add right before updateMove
- (void)calcNextMove {
}
// Add at bottom of updateMove
if ([_layer isWallAtRect:[self boundingBox]]) {
self.position = oldPosition;
[self calcNextMove];
} |
updateMove中新添加的代码用于检查坦克是否开进水中。如果是,则需要返回原来的位置,并调用calcNextMove。当然,这里这个方法仍然是空的
添加加速计支持
对于这个游戏,我们需要使用加速计来进行游戏操控。在HelloWorldLayerm.m中添加以下方法
- (void)onEnterTransitionDidFinish {
self.isAccelerometerEnabled = YES;
}
- (void)accelerometer:(UIAccelerometer *)accelerometer
didAccelerate:(UIAcceleration *)acceleration {
#define kFilteringFactor 0.75
static UIAccelerationValue 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;
CGPoint moveTo = _tank.position;
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 = YES;
[_tank moveToward:moveTo];
//NSLog(@"accelX: %f, accelY: %f", accelX, accelY);
} |
在上面的代码中,我们在onEnterTransitionDidFinish方法中设置isAccelerometerEnabled。
然后使用下面的方法来操控坦克的运动。这些内容在之前的系列博文中都有提及,因此这里不再一一赘述
添加音乐
当然,我也没忘了给大家提供一些很酷的音效。只需在HelloWorlayer.m中做出以下修改即可:
在文件的顶部添加一行代码
:
// Add to top of file
#import "SimpleAudioEngine.h"
// Add to end of init
[[SimpleAudioEngine sharedEngine] playBackgroundMusic:@"bgMusic.caf"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"explode.wav"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"tank1Shoot.wav"];
[[SimpleAudioEngine sharedEngine] preloadEffect:@"tank2Shoot.wav"]; |
编译运行游戏,一个基本的游戏就成型了。
译注:该教程的第一部分和之前博文cocos2d漫游指南4-5区别不大,因此对其中的部分代码没有详细解释。
该项目的示例代码在此:http://www.raywenderlich.com/downloads/Tanks1.zip
但需要注意的是,直接解压缩是无法直接运行的,需要将其中的cocos2d-ios.xcodeproj替换成本地文件,且最初设置ARC的部分要重新进行设置,否则会大量报错!
附:关于精灵体移动的学习的辅助资料:
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]
这下明白了吧。