Cocos2d开发系列(六)

来源:互联网 发布:支付宝五福红包算法 编辑:程序博客网 时间:2024/06/05 09:52

Learn IPhone and iPad Cocos2d Game Delevopment》第7章(原文中有部分无关紧要的 内容未 进行翻译)。

对于射击类游戏,使用重力感应进行游戏控制是不可接受的,采用虚拟手柄将会更恰当。出于“不重新发明轮子”的原则,我们将采用开源库SneakyInput。

控制玩家的飞船进行移动只是其中一件事情。我们还需要让背景能够滚动,以造成在某个方向上“前进”的感觉。为此必须自己实现背景滚动。由于CCParallaxNode的限制,它不能无限制地滚动卷轴式背景。

一、高级平行视差滚动

在这个射击游戏中,我们将使用ParallaxBackground节点。同时,我们将使用CCSpriteBatchNode以提高背景图片的渲染速度。

1、创建背景层

下图显示了我用Seashore绘制背景层。

 

 

每个背景层位于Seashore的单独的图层中,每一层可以保存为单独的文件,分别命名为bg0-bg6。

以这种方式创建背景层的原因在于:你既可以把各个层的背景放在一起,也可以分别把每一层存成单独的文件。所有文件大小都是480*320,似乎有点浪费。但不需要把单独把每个文件加到游戏里,只需要把它们融合在一个贴图集里。由于Zwoptex会自动去除每个图片的透明边沿,它会把这些背景层紧紧地放到一起没有丝毫空间的浪费。

把背景分层的原因不仅是便于把每一层放在不同的Z轴。严格讲,bg5.png(位于最下端)和bg6.png(位于最上端)应该是相同的Z坐标,因为它们之间没有交叠,所以我把他们存在分开的文件里。这样Zwoptex会把两者上下之间的空白空间截掉。

此外,把背景分层有利于提高帧率。iOS设备的填充率很低(每1帧能绘制的像素点数量)。由于不同图片之间常存在交叠的部分,iOS设备每1帧经常需要在同1点上绘制多次。比如,最极端的情况,一张全屏图片位于另一张全屏图片之上。你明明只能看到最上面的图片,但设备却不得不两张图片都绘制出来。这种情况叫做overdraw(无效绘制)。把背景分层可以尽量地减少无效绘制。

2、修改背景的绘制

#import <Foundation/Foundation.h>

#import "cocos2d.h"

 

@interface ParallaxBackground : CCNode

{

CCSpriteBatchNode spriteBatch ;

 

int numStripes ;

CCArray speedFactors ; // 速度系数数组

float scrollSpeed ;

}

 

@end

 

我把CCSpriteBatchNode引用保存在成员变量里,因为它在后面会用得比较频繁。采用成员变量访问节点比通过getNodeByTag方式访问要快一点,每1帧都会节约几个时钟周期(但保留几百个成员变量就太夸张了)。

 

#import "ParallaxBackground.h"

 

@implementation ParallaxBackground

 

-( id ) init

{

if (( self = [ super init ]))

{

CGSize screenSize = [[ CCDirector sharedDirector winSize ];

//   把game_art.png加载到贴图缓存

CCTexture2D * gameArtTexture = [[ CCTextureCache sharedTextureCache addImage : @"game-art.png" ];

// 初始化CCSpriteBatchNode spritebatch

spriteBatch = [ CCSpriteBatchNode batchNodeWithTexture :gameArtTexture];

[ self addChild : spriteBatch ];

 

numStripes 7 ;

// 从贴图集中加载7张图片并进行定位

for ( int i = 0 ; i < numStripes ; i++)

{

NSString * frameName = [ NSString stringWithFormat : @"bg%i.png" , i];

CCSprite * sprite = [ CCSprite spriteWithSpriteFrameName :frameName];

sprite. position CGPointMake (screenSize. width 2 , screenSize. height 2 );

[ spriteBatch addChild :sprite z :i tag :i];

}

 

// 再加7个背景层 将其翻转并放到下一个屏幕位置的中心 for ( int i = 0 ; i < numStripes ; i++)

{

NSString * frameName = [ NSString stringWithFormat : @"bg%i.png" , i];

CCSprite * sprite = [ CCSprite spriteWithSpriteFrameName :frameName];

// 放到下一屏的中心

sprite. position CGPointMake (screenSize. width 2 + screenSize. width , screenSize. height 2 );

 

// 水平翻转

sprite. flipX YES ;

[ spriteBatch addChild :sprite z :i tag :i + numStripes];

}

// 初始化速度系数数组,分别定义每一层的滚动速度 speedFactors = [[ CCArray alloc initWithCapacity :numStripes ];

[ speedFactors addObject :[ NSNumber numberWithFloat : 0.3f ]];

[ speedFactors addObject :[ NSNumber numberWithFloat : 0.5f ]];

[ speedFactors addObject :[ NSNumber numberWithFloat : 0.5f ]];

[ speedFactors addObject :[ NSNumber numberWithFloat : 0.8f ]];

[ speedFactors addObject :[ NSNumber numberWithFloat : 0.8f ]];

[ speedFactors addObject :[ NSNumber numberWithFloat : 1.2f ]];

[ speedFactors addObject :[ NSNumber numberWithFloat : 1.2f ]];

NSAssert ([ speedFactors count ] == numStripes @"speedFactors count does not match numStripes!" );

 

scrollSpeed 1.0f ;

[ self scheduleUpdate ];

}

return self ;

}

 

-( void ) dealloc

{

[ speedFactors release ];

[ super dealloc ];

}

 

-( void ) update:( ccTime )delta

{

CCSprite * sprite;

CCARRAY_FOREACH ([ spriteBatch children ], sprite)

{

NSNumber * factor = [ speedFactors objectAtIndex :sprite. zOrder ];

CGPoint pos = sprite. position ;

pos. x -= scrollSpeed * [factor floatValue ];

sprite. position = pos;

}

}

 

@end

 

在GameScene中,我们曾经加载了贴图集game-art.plist:

CCSpriteFrameCache * frameCache = [ CCSpriteFrameCache sharedSpriteFrameCache ];

[frameCache addSpriteFramesWithFile : @"game-art.plist" ];

因此,实际上game-art.png已经加载。当我们在init方法中再次加载game-art.png时(我们需要获得一个CCTexture2D以构造CCSpriteBatchNode),实际上并不会再次加载game-art.png,CCTextureCache会从缓存中返回一个已经加载的CCTexture2D对象。我们没有其他办法,因为cocos2d没有提供一个 getTextureByName 的方法。

接下来,初始化了CCSpriteBatchNode对象,并从贴图集中加载了7张背景图。

在update方法中,每一层背景图的x位置每播放一帧,就减去了一点(从右向左移动)。移动的距离由scrollSpeed*一个速度系数(speedFactors数组中相应的一个数值)来计算。

这样,每1层的背景会有不同的速度系数,从而会以不同的速度移动:

 

由于各层移动速度不同,所以最终背景的右边沿会呈现出不整齐的现象:

 

3、无限滚动

在 ParallaxBackground 类的init方法中,我们再次添加了7张背景图并进行水平翻转。目的是让每一层背景图片的宽度在水平方向上延伸,翻转的目的则是使拼在一起的时候两张图片的对接边沿能够对齐。

同时,把第2幅图片紧挨着放在第1幅图右边,从而把两张相同但互为镜像的图片拼接在一起。

这是第1幅图的位置摆放:

sprite. position CGPointMake (screenSize. width 2 , screenSize. height 2 );

 

这是第2幅图的位置摆放:

// 放到下一屏的中心

sprite. position CGPointMake (screenSize. width 2 + screenSize. width , screenSize. height 2 );

通过比较很容易就得以看出二者的x坐标相差1个屏幕宽度:screenSize(这同时也是图片宽度,我们的图片是严格按照480*320的屏幕尺寸制作的)。

下面我们可以用另外一种方式来摆放图片(更直观),把相应的代码修改为:

第1幅图的摆放:

sprite. anchorPoint CGPointMake ( 0 0.5f );

sprite. position CGPointMake ( 0 screenSize . height 2 );

第2幅图的摆放:

sprite. anchorPoint CGPointMake ( 0 0.5f );

sprite. position CGPointMake ( screenSize . width screenSize . height 2 );

 

 

我们改变了图片的anchorPoint属性。anchorPoint就是一个图形对象“锚点”或“对齐点”,这个属性对于静止不动的对象是没有意义的。但对于可以移动的对象来说,意味着位置移动的参考点。也就是说物体移动后锚点应该和目标点对齐(定点停车?)。如果命令一个物体移动到a点,真实的意思其实是把这个物体的锚点和a点对齐。锚点用一个CGPoint表示,不过这个CGPoint的x和y值都是0-1之间的小数值。 一个物体的锚点,如果不改变它的话, 默认 是(0.5f, 0.5f)。这两个浮点数所代表的含义是:该锚点位于物体宽度1/2和高度1/2的地方。即物体(图形)的正中心 :

 

而代码 sprite. anchorPoint CGPointMake ( 0 0.5f ); 实际上是把图片的锚点移到了图片左中部的位置:

 

这样我们摆放第1张图时候可以从横坐标0开始摆,而不必要计算屏幕宽度。

而摆放第2张图的时候直接从第2屏的起始位置(即1个屏幕宽度)开始摆。

接下来,我们可以修改update的代码,让两幅图交替移动以模拟出背景图无限滚动的效果:

-( void ) update:( ccTime )delta

{

CCSprite * sprite;

CCARRAY_FOREACH ([ spriteBatch children ], sprite)

{

NSNumber * factor = [ speedFactors objectAtIndex :sprite. zOrder ];

CGPoint pos = sprite. position ;

pos. x -= scrollSpeed * [factor floatValue ];

// 当有一副图移出屏幕左边后,把它挪到屏幕右边等待再次滚动—无限滚动

if (pos. x < - screenSize . width )

{

pos. x += screenSize . width 2 1 ;

}

sprite. position = pos;

}

}

实际上,飞船是不动的,动的是背景,以此模拟出飞船在游戏世界中前进的效果。

 

4、防止抖动

仔细观察,你会发现画面上有时会出现一条黑色的竖线。这是由于图片之间拼接位置出现凑整的问题。帧与帧之间,由于小数点上的误差,有时会出现1个像素宽度的缝隙。对于商业品质的游戏,应该解决这个小问题。

最简单的办法,让图片之间微微交叠1个像素。

在摆放第2幅图时:

sprite. position CGPointMake ( screenSize . width-1 screenSize . height 2 );

 

在update方法中:

// 当有一副图移出屏幕左边后,把它挪到屏幕右边等待再次滚动—无限滚动

if (pos. x < - screenSize . width )

{

pos. x += screenSize . width 2 2 ;

}

sprite. position = pos;

为什么是减2个像素?因为1个像素是上次拼接时“用掉”的(一开始我们在init的时候就拼接过一次)。而在update方法中,已经是第2次拼接了。1次拼接需要1个像素,两次拼接自然要2个像素。

 

5、重复贴图

在这一章没有其他值得注意的技巧了。你可以让同一个贴图在任意一个空间里重复。只要这个空间够大,你能让这个贴图没完没了地重复。至少成千上万像素或成打的屏幕上能够用一张贴图贴满,而不会给性能和内存带来不良影响。

这个技巧就是使用OpenGL 的GL_REPEAT参数。只不过,要重复的对象只能是边长为2的n次方的正方形。如32*32,128*128。

CGRect repeatRect = CGRectMake(-5000, -5000, 5000, 5000);

CCSprite* sprite = [CCSprite spriteWithFile:@”square.png” rect:repeatRect];

ccTexParams params ={

GL_LINEAR,

GL_LINEAR,

GL_REPEAT,

  GL_REPEAT

};

[sprite.texture setTexParameters:&params];

这里,CCSprite必须用一个CGRect构造,这个CGRect描述了要重复贴图的矩形范围。ccTexParams参数是一个GL_REPEAT结构,这个参数用于CCTexture2D的setTexParameters方法。

这将使整个指定的矩形区域被square.png图片铺满(横向平铺,纵向平铺)。当你移动CCSprite时,整个贴图局域也被移动。你可以用这个技巧把最底层的背景删除,然后用一张简单的小图片替代。

二、虚拟手柄

由于iOS设备没有按钮(除了Home键),虚拟手柄(或D-pads)在游戏中就显得很有用。

1、SneakyInput介绍

SneakyInput的作者是Nick Pannuto,示例代码由CJ Hanson提供。这是一个免费的开源项目, 它接受自愿捐助:http://pledgie.com/campaigns/9124

该项目源码托管于github库:

http://github.com/sneakyness/SneakyInput .

源码下载后,解包,打开该项目,编译运行。你可以在模拟器中看到一个虚拟手柄。

SneakyInput中集成了cocos2d,但可能不是最新版本。如果出现”base SDK missing”错误,你可以修改Info面板中的base SDK。

2、集成SneakyInput

对于源代码项目,有这样一个问题:当我们需要和其他项目集成时,哪些文件是必须的?每个源码项目都不一样,答案也不尽相同。

但我会告诉你SneakyInput的哪些文件是必须的,包括5个核心的类:

SneakyButton 和 SneakyButtonSkinnedBase

SneakyJoystick 和 SneakyJoystickSkinnedBase

ColoredCircleSprite(可选的)

其他文件不是必须的,但可作为一些参考。

使用Add Existing Files对话框加入上述5个类(5个.m文件,5个.h文件)。

 

3、射击按钮

首先,我们需要在GameScene的scene方法中加入一个InputLayer(继承自CCLayer) :

InputLayer * inputLayer = [ InputLayer node ];

[scene addChild :inputLayer z : 1 tag : GameSceneLayerTagInput ];

在枚举GameSceneLayerTags中添加GameSceneLayerTagInput定义,用于InputLayer层的tag:

typedef enum

{

GameSceneLayerTagGame 1 ,

GameSceneLayerTagInput ,

} GameSceneLayerTags;

 

然后新建类InputLayer:

 

#import <Foundation/Foundation.h>

#import "cocos2d.h"

 

// SneakyInput headers

#import "ColoredCircleSprite.h"

#import "SneakyButton.h"

#import "SneakyButtonSkinnedBase.h"

#import "SneakyJoystick.h"

#import "SneakyJoystickSkinnedBase.h"

 

#import "SneakyExtensions.h"

 

@interface InputLayer : CCLayer

{

SneakyButton fireButton ;

}

@end

 

#import "InputLayer.h"

#import "GameScene.h"

 

@interface InputLayer (PrivateMethods)

-( void ) addFireButton;

@end

 

 

@implementation InputLayer

 

-( id ) init

{

if (( self = [ super init ]))

{

[ self addFireButton ];

[ self scheduleUpdate ];

}

return self ;

}

 

-( void ) dealloc

{

[ super dealloc ];

}

 

-( void ) addFireButton

{

float buttonRadius = 80;

CGSize screenSize = [[CCDirector sharedDirector] winSize];

fireButton = [[[SneakyButton alloc] initWithRect:CGRectZero] autorelease];

fireButton.radius = buttonRadius;

  fireButton.position = CGPointMake(screenSize.width - buttonRadius,buttonRadius);

[self addChild:fireButton];

}

 

-( void ) update:( ccTime )delta

{

if (fireButton.active) {

CCLOG(@"FIRE!!!");

}

}

@end

 

在头文件中,我们定义了一个Sneakbutton成员变量。然后我们通过addFireButton方法创建发射按钮。

因为SneakyButton的initWithRect方法的CGRect参数其实并没有用到,所以我们可以简单地传递一个CGRectZero给它。实际上SneakyButton使用radius属性代表触摸所能响应的圆形半径,我们通过简单计算(屏幕宽度-按钮半径)把射击按钮紧凑地放到屏幕的右下角。

接下来,[self shceduleUpdate]调用了update方法。

在update方法里,我简单地在Log里输出一句话,以代替射击动作。

 

4、订制按钮外观

我用了一个特殊的类别(Category),为SneakyButton增加了一个两个特殊的静态初始化方法,以防止你忘记alloc或者autorelease对象。如SneakyExtensions.h和SneakyExtensions.m所示:

 

#import "ColoredCircleSprite.h"

#import "SneakyButton.h"

#import "SneakyButtonSkinnedBase.h"

#import "SneakyJoystick.h"

#import "SneakyJoystickSkinnedBase.h"

 

 

@interface SneakyButton (Extension)

+( id ) button;

+( id ) buttonWithRect:( CGRect )rect;

@end

 

@interface SneakyButtonSkinnedBase (Extension)

+( id ) skinnedButton;

@end

#import "SneakyExtensions.h"

 

 

@implementation SneakyButton (Extension)

+( id ) button

{

return [[[ SneakyButton alloc initWithRect : CGRectZero autorelease ];

}

 

+( id ) buttonWithRect:( CGRect )rect

{

return [[[ SneakyButton alloc initWithRect :rect] autorelease ];

}

@end

 

 

@implementation SneakyButtonSkinnedBase (Extension)

+( id ) skinnedButton

{

return [[[ SneakyButtonSkinnedBase alloc init autorelease ];

}

@end

我导入了所有 .h 文件,因为在这个类别中,我打算对每个 SneakyInput 都进行扩展。

用于 SneakyButton的initWithRect方法的CGRect参数其实并没有用到,所以我们可以用button方法来替代SneakyButton的初始化方法:

fireButton=[SneakyButton button];

 

现在开始订制 SneakyButton 的外观。首先制作 4 张 100*100 大小的图片,分别表示按钮的 4 个状态:默认,按下,激活,失效。默认状态即按钮未被按下时的外观,于此相反的是按下状态。激活状态仅发生在切换按钮的时候,此时按钮被激活,或获得焦点。失效状态表示按钮此时是无效的。例如,当武器过热时,你会有几秒钟无法射击,此时应该让按钮失效并让按钮显示失效状态的图片。当然,在这里,我们仅需要使用默认图片和按下图片。

修改 InputLayer 的 addFireButton 方法为:

 

-( void ) addFireButton

{

float buttonRadius = 50 ;

CGSize screenSize = [[ CCDirector sharedDirector winSize ];

 

fireButton = [ SneakyButton button ];

fireButton . isHoldable YES ;

SneakyButtonSkinnedBase * skinFireButton = [ SneakyButtonSkinnedBase skinnedButton ];

skinFireButton. position CGPointMake (screenSize. width - buttonRadius, buttonRadius);

skinFireButton. defaultSprite = [ CCSprite spriteWithSpriteFrameName : @"button-default.png" ];

skinFireButton. pressSprite = [ CCSprite spriteWithSpriteFrameName : @"button-pressed.png" ];

skinFireButton. button fireButton ;

[ self addChild :skinFireButton];

}

 

这里设置了 isHoldable 属性,这意味着当你按下按钮不放时会导致子弹不停地发射。现在,不需要设置 radius 属性,因为接下来的 SneakyButtonSkinnedBase 中的图片的大小就决定了 radius 的值。 SneakyButtonSkinedBase 的静态初始化方法 skinnedButton 是我们在 Extension 这个类别中定义过的。

现在,我们用 SneakyButtonSkinnedBase 替代了 SneakyButton ,用设置 SneakyButtonSkinnedBase 的位置替代了设置 SneakyButton 的位置。并且设置了 SneakyButtonSkinnedBase 的状态图片。

注意最后两句代码, SneakyButtonSkinnedBase 的 button 属性持有了 SneakyButton 对象引用,这样 fireButton 对象隐式地被加到了 InputLayer 。

 

update 方法也修改了,这次调用了 GameScene 的射击方法:

-( void ) update:( ccTime )delta

{

totalTime += delta;

if ( fireButton . active && totalTime nextShotTime )

{

nextShotTime totalTime 0.5f ;

GameScene * game = [ GameScene sharedGameScene ];

[game shootBulletFromShip :[game defaultShip ]];

}

// Allow faster shooting by quickly tapping the fire button.

if ( fireButton . active == NO )

{

nextShotTime 0 ;

}

}

变量 totalTime 和 nextShortTime 限制了子弹射击的速度为 2 发 / 秒。如果发射按钮的 active 状态为 NO (意味着它未被按下), nextshortTime 变量被设置为 0 ,从而保证你下一次按下发射键时,子弹不再判断时间,直接发射。快速点击发射键导致子弹的射速会更快(超过连续发射)。

 

5 、动作控制

我们需要使用 SneakyJoystick 来生成一个虚拟摇杆。 首先,增加一个 SneakyJoystick 成员变量: SneakyJoystickjoystick ;

增加一个 addJoystick 方法,这次我们直接使用了 SneakyJoystickSkinnedBase ,以定制其外观,:

-( void ) addJoystick

{

float stickRadius = 50 ;

 

joystick = [ SneakyJoystick joystickWithRect : CGRectMake ( 0 0 , stickRadius, stickRadius)];

joystick . autoCenter YES ;

joystick . hasDeadzone YES ;

joystick . deadRadius 10 ;

SneakyJoystickSkinnedBase * skinStick = [ SneakyJoystickSkinnedBase skinnedJoystick ];

skinStick. position CGPointMake (stickRadius * 1.5f , stickRadius * 1.5f );

skinStick. backgroundSprite = [ CCSprite spriteWithSpriteFrameName : @"button-disabled.png" ];

skinStick. backgroundSprite . color ccMAGENTA ;

skinStick. thumbSprite = [ CCSprite spriteWithSpriteFrameName : @"button-disabled.png" ];

skinStick. thumbSprite . scale 0.5f ;

skinStick. joystick joystick ;

[ self addChild :skinStick];

}

 

同样的,我们在 extension 类别中为 SneakyJoystickSkinnedBase 增加了新的静态方法 skinnedJoystick :

@implementation SneakyJoystickSkinnedBase (Extension)

+( id ) skinnedJoystick

{

return [[[ SneakyJoystickSkinnedBase alloc init autorelease ];

}

@end

SneakyJoystick 的初始化方法需要一个 CGRect 参数,与 SneakyButton 不同,这里 CGRect 的确能决定摇杆的半径。 autoCenter 设置为 YES 可以使摇杆自动回到中心位置。 hasDeadZone 和 deadRadius 属性决定了你能移动的最小半径,在此范围内的移动视作无效。如果 hasDeadZone=NO ,你几乎不可能让摇杆稳定保持在中心位置。

摇杆与屏幕边缘稍微空出了一些距离,对于游戏而言摇杆的位置和尺寸不是最恰当的,但用来演示足够了。

如果摇杆过于靠近屏幕边缘,手指很容易移出屏幕从而失去对飞船的控制。

我决定让摇杆使用 button-disabled.png 作为背景图,同时摇杆大小缩放为原来的一半。这里 backgroundSprite 和thumbSprite 使用的图片都是同一张。二者的区别是:

摇杆的手柄( thumbSprite) 半径仅为按钮背景 (backgroundSprite) 半径的一半。 button-disabled.png 图片是一个灰色的圆形按钮。这样的将导致虚拟摇杆由两个灰色的正圆构成,在一个圆形的中心还有一个一半大小的小圆。

而且,把背景图选取为灰色图片是特意的。因为 backgroundSprite 的 color 属性被设置为品红,于是backgroundSprite 的灰色图片被着色为品红了!通过把 color 属性设置为不同的颜色:红色、绿色、黄色,你可以轻易地为 backgroundSprite 染上不同的颜色!

当然,控制飞船移动的代码是在 update 方法中:

 

GameScene * game = [ GameScene sharedGameScene ];

Ship * ship = [game defaultShip ];

CGPoint velocity = ccpMult ( joystick . velocity 200 );

if (velocity. x != 0 && velocity. y != 0 ) {

ship. position CGPointMake (ship. position . x + velocity. x * delta, ship. position . y + velocity. y * delta);

}

我们在 GameScene 中增加了 defaultShip 方法,以便在这里访问 ship 对象。摇杆的 velocity 属性用于改变飞船的位置,但需要根据比例放大,这使得摇柄能在控制上能够有一个放大效果。放大比例是一个经验值,在游戏中感觉可以就行了。

万一出现 update 方法调用不规律的情况,为确保飞船平滑移动的效果,我们必须利用 update 方法的 delta 参数。Delta 参数传递了从上次 update 调用以来到本次调用之间的时间值。另外,飞船可能被移出屏幕区域外——你肯定不希望这样。你可能想把代码直接加在 InputLayer 中 ship 位置被改变的地方。这会有一个问题:你是为了防止摇柄把飞船移到屏幕外?还是为了让飞船根本就没有移到屏幕外的能力?无疑,后者更为优雅——这样,你就要覆盖 Ship 类的 setPosition 方法了:

-( void ) setPosition:( CGPoint )pos

{

CGSize screenSize = [[ CCDirector sharedDirector winSize ];

float halfWidth = contentSize_ . width 0.5f ;

float halfHeight = contentSize_ . height 0.5f ;

 

// 防止飞船移出屏幕

if (pos. x < halfWidth)

{

pos. x = halfWidth;

}

else if (pos. x > (screenSize. width - halfWidth))

{

pos. x = screenSize. width - halfWidth;

}

if (pos. y < halfHeight)

{

pos. y = halfHeight;

}

else if (pos. y > (screenSize. height - halfHeight))

{

pos. y = screenSize. height - halfHeight;

}

// 一定要调用父类的同名方法

[ super setPosition :pos];

}

 

每当飞船的位置发生改变,上面的代码会对飞船的位置进行一个边界检测。如果飞船 x,y 坐标移出了屏幕外,它将被保持在屏幕边沿以内。

由于 position 是属性,下面语句会调用 setPosition 方法:

ship.position=CGPointMake(200,100);

点语法比发送 getter / setter 消息更简短,当然我们也可以用发送消息的语法:

[ship setPosition:CGPointMake(200,100)];

通过这种方法,你可以重写其他基类的方法,以改变游戏对象的行为。例如,如果要限定一个对象只能旋转 0-180 度,你可以重写 setRotation(float)rotation 方法在其中加入限制旋转的代码。

 

6 、数字控制

如果你的游戏不适合采用模拟控制,你可以把 SneakyJoystick 类转换成数字控制,即 D-pad 。这需要改动的代码很少:

joystick=[SneakyJoystick joystickWithRect:CGRectMake(0,0,stickRadius,stickRadius)];

 

joystick.autoCenter=YES;

 

// 减少控制方向为 8 方向

joystick.isDPad=YES;

joystick.numberOfDirections=8;

 

dead zone 属性被删除了——在数字控制中他们不再需要了。 isDPad 属性设置为 YES ,表明采用数字控制。同时你可以定义方向数。如果你想让 D-pads 在上下左右 4 个方向的同时,增加斜角方向(同时按下两个方向将使角色沿斜角移动),你只需要把 numberOfDirections 设置为 8 。 SneakyJoystick 自动把模拟控制的方向转换成 8 个方向。当然,如果你把方向数设置成 6 ,你会得到一些怪异的结果。

 

7 、 GP Joystick

SneakyInput 不是仅有的解决方案。还有 GP Joystick ,一个付费的商业产品,不过费用很低:

http://wrensation.com/?p=36

 

如果你想知道 GPJoystick 和 SneakyInput 有什么区别,你可以观看 GP Joystick 的 YouTube 视频:

http://www.youtube.com/user/SDKTutor

在这里也提供了几个 cocos2d 的视频教程。

 

三、结论

这章你学习了背景平行视差滚动效果:背景无限循环滚动(去除抖动),如何将背景拆分成不同的图层以便Zwoptex 能去掉透明区域,同时让这些图片保持正确的位置。

 

接下来是指定屏幕分辨率。假设你想创建一个 iPad 的版本,除了必需创建 1024*768 的图片外,你可以使用相同技术。这个工作你可以自己尝试一下。

后半章介绍了 SneakyInput ,一个开源项目,可以在 cocos2d 游戏中加入虚拟摇杆和按钮。它并不是最好的,但对大多数游戏来说已将足够,无论如何,总胜过你自己去写虚拟摇杆的代码。

现在,飞船已经能控制了并且不再能飞出屏幕边缘了。通过按下发射按钮,它也能进行射击了。但这个游戏仍然还有许多东西要做。如果没有什么东西给你射击,那么射击游戏就不能成为射击游戏了。下一章继续

原创粉丝点击