?
Learn IPhoneand 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;
intnumStripes;
CCArray* speedFactors; // 速度系數數組
floatscrollSpeed;
}
@end
~~~
我把CCSpriteBatchNode引用保存在成員變量里,因為它在后面會用得比較頻繁。采用成員變量訪問節點比通過getNodeByTag方式訪問要快一點,每1幀都會節約幾個時鐘周期(但保留幾百個成員變量就太夸張了)。
?
~~~
#import "ParallaxBackground.h"
@implementation ParallaxBackground
-(id) init
{
if ((self = [superinit]))
{
CGSize screenSize = [[CCDirectorsharedDirector] winSize];
// ?把game_art.png加載到貼圖緩存
CCTexture2D* gameArtTexture = [[CCTextureCachesharedTextureCache] addImage:@"game-art.png"];
// 初始化CCSpriteBatchNodespritebatch
spriteBatch = [CCSpriteBatchNodebatchNodeWithTexture:gameArtTexture];
[selfaddChild:spriteBatch];
numStripes = 7;
// 從貼圖集中加載7張圖片并進行定位
for (int i = 0; i < numStripes; i++)
{
NSString* frameName = [NSStringstringWithFormat:@"bg%i.png", i];
CCSprite* sprite = [CCSpritespriteWithSpriteFrameName:frameName];
sprite.position = CGPointMake(screenSize.width / 2, screenSize.height / 2);
[spriteBatchaddChild:sprite z:i tag:i];
}
// 再加7個背景層, 將其翻轉并放到下一個屏幕位置的中心for (int i = 0; i < numStripes; i++)
{
NSString* frameName = [NSStringstringWithFormat:@"bg%i.png", i];
CCSprite* sprite = [CCSpritespriteWithSpriteFrameName:frameName];
//放到下一屏的中心
sprite.position = CGPointMake(screenSize.width / 2 + screenSize.width, screenSize.height / 2);
//水平翻轉
sprite.flipX =YES;
[spriteBatchaddChild:sprite z:i tag:i + numStripes];
}
// 初始化速度系數數組,分別定義每一層的滾動速度speedFactors = [[CCArrayalloc] initWithCapacity:numStripes];
[speedFactorsaddObject:[NSNumbernumberWithFloat:0.3f]];
[speedFactorsaddObject:[NSNumbernumberWithFloat:0.5f]];
[speedFactorsaddObject:[NSNumbernumberWithFloat:0.5f]];
[speedFactorsaddObject:[NSNumbernumberWithFloat:0.8f]];
[speedFactorsaddObject:[NSNumbernumberWithFloat:0.8f]];
[speedFactorsaddObject:[NSNumbernumberWithFloat:1.2f]];
[speedFactorsaddObject:[NSNumbernumberWithFloat:1.2f]];
NSAssert([speedFactorscount] == numStripes, @"speedFactors count does notmatch numStripes!");
scrollSpeed = 1.0f;
[selfscheduleUpdate];
}
returnself;
}
-(void) dealloc
{
[speedFactorsrelease];
[superdealloc];
}
-(void) update:(ccTime)delta
{
CCSprite* sprite;
CCARRAY_FOREACH([spriteBatchchildren], sprite)
{
NSNumber* factor = [speedFactorsobjectAtIndex:sprite.zOrder];
CGPoint pos = sprite.position;
pos.x -= scrollSpeed * [factor floatValue];
sprite.position = pos;
}
}
@end
~~~
在GameScene中,我們曾經加載了貼圖集game-art.plist:
~~~
CCSpriteFrameCache* frameCache = [CCSpriteFrameCachesharedSpriteFrameCache];
[frameCacheaddSpriteFramesWithFile:@"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([spriteBatchchildren], sprite)
{
NSNumber* factor = [speedFactorsobjectAtIndex: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 = [CCSpritespriteWithFile:@”square.png” rect:repeatRect];
ccTexParams params ={
GL_LINEAR,
GL_LINEAR,
GL_REPEAT,
?GL_REPEAT
};
[sprite.texture setTexParameters:¶ms];
~~~
這里,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](http://pledgie.com/campaigns/9124)
該項目源碼托管于github庫:
[http://github.com/sneakyness/SneakyInput](http://github.com/sneakyness/SneakyInput).
源碼下載后,解包,打開該項目,編譯運行。你可以在模擬器中看到一個虛擬手柄。
SneakyInput中集成了cocos2d,但可能不是最新版本。如果出現”base SDKmissing”錯誤,你可以修改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 = [InputLayernode];
[scene addChild:inputLayer z:1tag:GameSceneLayerTagInput];
在枚舉GameSceneLayerTags中添加GameSceneLayerTagInput定義,用于InputLayer層的tag:
~~~
typedefenum
{
GameSceneLayerTagGame = 1,
GameSceneLayerTagInput,
} GameSceneLayerTags;
~~~
?
然后新建類InputLayer:
~~~
#import <Foundation/Foundation.h>
#import "cocos2d.h"
// SneakyInputheaders
#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 = [superinit]))
{
[selfaddFireButton];
[selfscheduleUpdate];
}
returnself;
}
-(void) dealloc
{
[superdealloc];
}
-(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
interfaceSneakyButtonSkinnedBase (Extension)
+(id) skinnedButton;
@end
#import "SneakyExtensions.h"
@implementation SneakyButton(Extension)
+(id) button
{
return [[[SneakyButtonalloc] initWithRect:CGRectZero] autorelease];
}
+(id) buttonWithRect:(CGRect)rect
{
return [[[SneakyButtonalloc] initWithRect:rect] autorelease];
}
@end
@implementation SneakyButtonSkinnedBase(Extension)
+(id) skinnedButton
{
return [[[SneakyButtonSkinnedBasealloc] 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 = [[CCDirectorsharedDirector] winSize];
?
fireButton = [SneakyButtonbutton];
fireButton.isHoldable = YES;
SneakyButtonSkinnedBase* skinFireButton = [SneakyButtonSkinnedBaseskinnedButton];
skinFireButton.position = CGPointMake(screenSize.width - buttonRadius, buttonRadius);
skinFireButton.defaultSprite = [CCSpritespriteWithSpriteFrameName:@"button-default.png"];
skinFireButton.pressSprite = [CCSpritespriteWithSpriteFrameName:@"button-pressed.png"];
skinFireButton.button = fireButton;
[selfaddChild: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 = [GameScenesharedGameScene];
[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 = [SneakyJoystickjoystickWithRect:CGRectMake(0, 0, stickRadius,stickRadius)];
joystick.autoCenter = YES;
joystick.hasDeadzone = YES;
joystick.deadRadius = 10;
SneakyJoystickSkinnedBase* skinStick = [SneakyJoystickSkinnedBaseskinnedJoystick];
skinStick.position = CGPointMake(stickRadius * 1.5f, stickRadius * 1.5f);
skinStick.backgroundSprite = [CCSpritespriteWithSpriteFrameName:@"button-disabled.png"];
skinStick.backgroundSprite.color = ccMAGENTA;
skinStick.thumbSprite = [CCSpritespriteWithSpriteFrameName:@"button-disabled.png"];
skinStick.thumbSprite.scale = 0.5f;
skinStick.joystick = joystick;
[selfaddChild:skinStick];
}
~~~
?
同樣的,我們在extension類別中為SneakyJoystickSkinnedBase增加了新的靜態方法skinnedJoystick:
~~~
@implementationSneakyJoystickSkinnedBase (Extension)
+(id) skinnedJoystick
{
return [[[SneakyJoystickSkinnedBasealloc] 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 = [GameScenesharedGameScene];
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 = [[CCDirectorsharedDirector] winSize];
float halfWidth = contentSize_.width * 0.5f;
float halfHeight = contentSize_.height * 0.5f;
?
//防止飛船移出屏幕
if (pos.x < halfWidth)
{
pos.x = halfWidth;
}
elseif (pos.x > (screenSize.width - halfWidth))
{
pos.x = screenSize.width - halfWidth;
}
if (pos.y < halfHeight)
{
pos.y = halfHeight;
}
elseif (pos.y > (screenSize.height - halfHeight))
{
pos.y = screenSize.height - halfHeight;
}
//一定要調用父類的同名方法
[supersetPosition:pos];
}
~~~
?
每當飛船的位置發生改變,上面的代碼會對飛船的位置進行一個邊界檢測。如果飛船x,y坐標移出了屏幕外,它將被保持在屏幕邊沿以內。
由于position是屬性,下面語句會調用setPosition方法:
~~~
ship.position=CGPointMake(200,100);
~~~
點語法比發送getter/setter消息更簡短,當然我們也可以用發送消息的語法:
~~~
[shipsetPosition: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](http://wrensation.com/?p=36)
?
如果你想知道GPJoystick和SneakyInput有什么區別,你可以觀看GP Joystick的YouTube視頻:
[http://www.youtube.com/user/SDKTutor](http://www.youtube.com/user/SDKTutor)
在這里也提供了幾個cocos2d的視頻教程。
?
## 三、結論
這章你學習了背景平行視差滾動效果:背景無限循環滾動(去除抖動),如何將背景拆分成不同的圖層以便Zwoptex能去掉透明區域,同時讓這些圖片保持正確的位置。
?
接下來是指定屏幕分辨率。假設你想創建一個iPad的版本,除了必需創建1024*768的圖片外,你可以使用相同技術。這個工作你可以自己嘗試一下。
后半章介紹了SneakyInput,一個開源項目,可以在cocos2d游戲中加入虛擬搖桿和按鈕。它并不是最好的,但對大多數游戲來說已將足夠,無論如何,總勝過你自己去寫虛擬搖桿的代碼。
現在,飛船已經能控制了并且不再能飛出屏幕邊緣了。通過按下發射按鈕,它也能進行射擊了。但這個游戲仍然還有許多東西要做。如果沒有什么東西給你射擊,那么射擊游戲就不能成為射擊游戲了。下一章繼續。
?
?
?
?
?