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 成员变量: SneakyJoystick * joystick ;

增加一个 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 游戏中加入虚拟摇杆和按钮。它并不是最好的,但对大多数游戏来说已将足够,无论如何,总胜过你自己去写虚拟摇杆的代码。

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

 

 

 

 

 

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐