《Learn IPhone and iPad Cocos2d Game Delevopment》第6章

firedragonpzy發表於2012-07-28
一、 CCSpriteBatchNode

在螢幕上貼圖時,圖形硬體需要經過準備、渲染、清除等步驟。每次貼圖都會重複這個過程。如果圖形硬體能事先知道有一組擁有相同紋理的Sprite需要渲染,則這個過程會被簡化。比如,一組Sprite的準備和清除動作總共只需要執行一次。

下圖的例子使用了CCSpriteBacthNode。螢幕上同時有幾百顆子彈飛過。如果一次只渲染一顆,那麼幀率馬上降到85%。使用CCSpriteBatchNode,可以避免這種情況:




通常我們這樣建立一個CCSprite:

CCSprite* sprite=[CCSprite spriteWithFile:@”bullet.png”];

[self addChild:sprite];

而使用CCSpriteBatchNode 則需要修改為:

CCSpriteBatchNode* batch=[CCSpriteBatchNode batchNodeWithFile:@”bullet.png”];

[self addChild:batch];

for(int i=0;i<100;i++){

CCSprite* sprite=[CCSprite spriteWithFile:@”bullet.png”];

[batch addChild:bullet];

}

注意,CCSpriteBatchNode需要一個圖片檔名作為引數,哪怕它根本用不著這個圖片(進行顯示)。可以把它看做是一個Layer,你可以用它來加入一些CCSprite節點。由於它使用了一個圖片檔案作為構造引數,所以在後面加入的CCSprite中必須使用相同的檔案作為構造引數,否則會導致如下錯誤:

SpriteBatches[13879:207] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'CCSprite is not using the same texture id'

當採用相同紋理的CCSpite越多,則採用CCSpriteBatchNode的好處越明顯。

但這有一個限制,所有的CCSprite節點都會位於同一個Z座標(深度)上。如果子彈是“擊穿”敵人並向後飛,你得使用兩個Z軸不同的CCSpriteBatchNode。

另外一個限制是,CCSpriteBatchNode和加入其中的CCSprite必須使用相同的貼圖。這一點在使用Texture Atlas時尤其顯得重要。一個Texture Atlas可以畫多個不同的圖片,並且這些圖片使用同一個CCSpriteBatchNode,以提高渲染速度。

Z軸的問題可以通過指定CCSpriteBatchNode中單個CCSprite的Z值來解決。如果你所有的圖片都放到了一個Texture Atlas(紋理集),則你完全可以只使用一個CCSpriteBatchNode。

把CCSpriteBatchNode看成一個簡單的CCLayer,它只接受使用相同圖片的CCSprite,這樣,你就知道怎麼用它了。

在下面程式碼中,隱藏有一個致命的陷阱:

-(id)init{

If ((self = [super initWithFile:@"ship.png"])) {

[self scheduleUpdate];

}

return self;

}

由於-(id)init方法是預設的初始化方法,它會被其他初始化方法比如initWithFile呼叫。 在-(id)init方法中呼叫了[super initWithFile…]方法,[super initWithFile…]會呼叫[super init], 該類覆蓋了-(id)init方法,於是又會呼叫-(id)init方法,無限迴圈。

解決辦法是修改方法名,比如修改為-(id)initWithShipImage。

這個教訓告訴我們,在預設初始化方法-(id)init中,除了[super init]之外,永遠不要呼叫其他東西(其他的初始化方法)。如果你必須在初始化方法中呼叫[super initWith…]方法,你應當把方法名命名為initWith…。

二、示例程式碼

1、ship類

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface Ship : CCSprite
{
}

+( id ) ship;

@end
#import "Ship.h"
#import "Bullet.h"
#import "GameScene.h"

@interface Ship (PrivateMethods)
-( id ) initWithShipImage;
@end


@implementation Ship

+( id ) ship
{
return [[[ self alloc ] initWithShipImage ] autorelease ];
}

-( id ) initWithShipImage
{
if (( self = [ super initWithFile : @"ship.png" ]))
{
[ self scheduleUpdate ];
}
return self ;
}

-( void ) dealloc
{
[ super dealloc ];
}

-( void ) update:( ccTime )delta
{
[[ GameScene sharedGameScene ] shootBulletFromShip : self ];
}

@end

ship類很簡單,除了update方法。該方法呼叫了GameScene的shootBulletFromShip方法外([GameScene shareGameScene]實際上只是獲取GameScene 的單例項)。

2、GameScene類

#import <Foundation/Foundation.h>
#import "cocos2d.h"

#import "Ship.h"

typedef enum
{
GameSceneNodeTagBullet = 1 ,
GameSceneNodeTagBulletSpriteBatch ,
} GameSceneNodeTags;

@interface GameScene : CCLayer
{
int nextInactiveBullet ;
}

+( id ) scene;
+( GameScene *) sharedGameScene;

-( void ) shootBulletFromShip:( Ship *)ship;

@property ( readonly ) CCSpriteBatchNode* bulletSpriteBatch;

@end
#import "GameScene.h"
#import "Ship.h"
#import "Bullet.h"

@interface GameScene (PrivateMethods)
-( void ) countBullets:( ccTime )delta;
@end

@implementation GameScene

static GameScene* instanceOfGameScene;
+( GameScene *) sharedGameScene
{
NSAssert ( instanceOfGameScene != nil , @"GameScene instance not yet initialized!" );
return instanceOfGameScene ;
}

+( id ) scene
{
CCScene *scene = [ CCScene node ];
GameScene *layer = [ GameScene node ];
[scene addChild : layer];
return scene;
}

-( id ) init
{
if (( self = [ super init ]))
{
instanceOfGameScene = self ;
CGSize screenSize = [[ CCDirector sharedDirector ] winSize ];
CCColorLayer * colorLayer = [ CCColorLayer layerWithColor : ccc4 ( 255 , 255 , 255 , 255 )];
[ self addChild :colorLayer z :- 1 ];
CCSprite * background = [ CCSprite spriteWithFile : @"background.png" ];
background. position = CGPointMake (screenSize. width / 2 , screenSize. height / 2 );
[ self addChild :background];
Ship * ship = [ Ship ship ];
ship. position = CGPointMake (ship. texture . contentSize . width / 2 , screenSize. height / 2 );
[ self addChild :ship];
CCSpriteBatchNode * batch = [ CCSpriteBatchNode batchNodeWithFile : @"bullet.png" ];
[ self addChild :batch z : 1 tag : GameSceneNodeTagBulletSpriteBatch ];

for ( int i = 0 ; i < 400 ; i++)
{
Bullet * bullet = [ Bullet bullet ];
bullet. visible = NO ;
[batch addChild :bullet];
}
[ self schedule : @selector ( countBullets :) interval : 3 ];
}
return self ;
}

-( void ) dealloc
{
instanceOfGameScene = nil ;
// don't forget to call "super dealloc"
[ super dealloc ];
}

-( void ) countBullets:( ccTime )delta
{
CCLOG ( @"Number of active Bullets: %i" , [ self . bulletSpriteBatch . children count ]);
}

-( CCSpriteBatchNode *) bulletSpriteBatch
{
CCNode * node = [ self getChildByTag : GameSceneNodeTagBulletSpriteBatch ];
NSAssert ([node isKindOfClass :[ CCSpriteBatchNode class ]], @"not a CCSpriteBatchNode" );
return ( CCSpriteBatchNode *)node;
}

-( void ) shootBulletFromShip:( Ship *)ship
{
CCArray * bullets = [ self . bulletSpriteBatch children ];
CCNode * node = [bullets objectAtIndex : nextInactiveBullet ];
NSAssert ([node isKindOfClass :[ Bullet class ]], @"not a bullet!" );
Bullet * bullet = ( Bullet *)node;
[bullet shootBulletFromShip :ship];
nextInactiveBullet ++;
if ( nextInactiveBullet >= [bullets count ])
{
nextInactiveBullet = 0 ;
}
}

@end
現在你應該看到了,在init方法中使用CCSpriteBatchNode加入了400顆子彈(被設定為不可見了)。然後在接下來的shootBulletFromShip方法(在ship的update方法中呼叫)中,依次呼叫每一顆子彈的shootBulletFromShip方法。

3、Bullet類

#import <Foundation/Foundation.h>
#import "cocos2d.h"

#import "Ship.h"

@interface Bullet : CCSprite
{
CGPoint velocity ;
float outsideScreen ;
}

@property ( readwrite , nonatomic ) CGPoint velocity;

+( id ) bullet;

-( void ) shootBulletFromShip:( Ship *)ship;

@end
#import "Bullet.h"


@interface Bullet (PrivateMethods)
-( id ) initWithBulletImage;
@end


@implementation Bullet

@synthesize velocity;

+( id ) bullet
{
return [[[ self alloc ] initWithBulletImage ] autorelease ];
}

-( id ) initWithBulletImage
{
if (( self = [ super initWithFile : @"bullet.png" ]))
{
}
return self ;
}

-( void ) dealloc
{
[ super dealloc ];
}

// Re-Uses the bullet
-( void ) shootBulletFromShip:( Ship *)ship
{
float spread = ( CCRANDOM_0_1 () - 0.5f ) * 0.5f ;
velocity = CGPointMake ( 1 , spread);
outsideScreen = [[ CCDirector sharedDirector ] winSize ]. width ;
self . position = CGPointMake (ship. position . x + ship. contentSize . width * 0.5f , ship. position . y );
self . visible = YES ;
[ self scheduleUpdate ];
}

-( void ) update:( ccTime )delta
{
self . position = ccpAdd ( self . position , velocity );
if ( self . position . x > outsideScreen )
{
self . visible = NO ;
[ self unscheduleAllSelectors ];
}
}
@end
shootBulletFromShip方法實現了子彈的射擊。Spread變數計算了一個擴散值,使從飛船中射出的子彈有1/2的機率會向上/下兩邊擴散。Velocity是一個每幀移動的位置偏移量。然後設定子彈的初始位置位於飛船右邊。在把子彈可視狀態設定為顯示後,排程執行update方法(每幀呼叫一次)。

在update方法中,讓子彈移動velocity的偏移量。這種方式,比CCMoveXX方法效率更高一些。而且這裡用了一個技巧,當子彈飛出螢幕後,我們並沒有立即將Bullet物件清除(為了節省資源),而是子彈設定為不可視快取起來,方便再次使用以提高程式效能。出於這個原因,我們在GameScene類中設計了一個nextInactiveBullet變數,以此來記錄已經使用掉(射出去)的子彈(設定為可視的子彈)。等所有子彈都射出去以後,nextInactiveBullet重置為0。

三、增加角色動畫

以下程式碼為ship物件增加角色動畫。Ship物件的角色動畫是5張連續的幀影像,以表現飛船尾部不斷噴射並變化的火焰。

-( id ) initWithShipImage
{
if (( self = [ super initWithFile : @"ship.png" ]))
{
// 把5張圖片裝入動畫幀陣列
NSMutableArray * frames = [ NSMutableArray arrayWithCapacity : 5 ];
for ( int i = 0 ; i < 5 ; i++)
{
NSString * file = [ NSString stringWithFormat : @"ship-anim%i.png" , i];
// 使用貼圖快取構造2D貼圖
CCTexture2D * texture = [[ CCTextureCache sharedTextureCache ] addImage :file];
CGSize texSize = texture. contentSize ;
CGRect texRect = CGRectMake ( 0 , 0 , texSize. width , texSize. height );
// 用2D貼圖構造動畫幀
CCSpriteFrame * frame = [ CCSpriteFrame frameWithTexture :texture rect :texRect offset : CGPointZero ];
// 把動畫幀放入陣列
[frames addObject :frame];
}
// 用動畫幀陣列構造動畫物件,幀率:0.08秒/幀,標識名:move
CCAnimation * anim = [ CCAnimation animationWithName : @"move" delay : 0.08f frames :frames];
// 如果你把anim儲存到CCSprite,則可以通過名稱move來訪問CCAnimation
//[self addAnimation:anim];
// 構造Action:無限重複
CCAnimate * animate = [ CCAnimate actionWithAnimation :anim];
CCRepeatForever * repeat = [ CCRepeatForever actionWithAction :animate];
[ self runAction :repeat];
[ self scheduleUpdate ];
}
return self ;
}
為求簡便,上面的程式碼我們也可以封裝為一個新的類別Category。

1、類別Animation Helper

利用OC中的Category,我們可以擴充套件CCAnimation類。Category提供了一種不需要修改類的原始碼即可為類增加新方法的途徑(有點象AOP?),但它不能增加新的成員變數。下面的程式碼為CCAnimation增加了一個Category,名為Helper(新建Class,名為CCAnimationHelper.h):

#import <Foundation/Foundation.h>
#import "cocos2d.h"

@interface CCAnimation (Helper)

+( CCAnimation *) animationWithFile:( NSString *) name frameCount:( int )frameCount delay:( float ) delay ;
+( CCAnimation *) animationWithFrame:( NSString *)frame frameCount:( int )frameCount delay:( float ) delay ;

@end
#import "CCAnimationHelper.h"

@implementation CCAnimation (Helper)

// 通過圖片檔名建立CCAnimation物件 .
+( CCAnimation *) animationWithFile:( NSString *) name frameCount:( int )frameCount delay:( float ) delay
{
// 把前面的程式碼移到這裡來了
NSMutableArray * frames = [ NSMutableArray arrayWithCapacity :frameCount];
for ( int i = 0 ; i < frameCount; i++)
{
NSString * file = [ NSString stringWithFormat : @"%@%i.png" , name , i];
CCTexture2D * texture = [[ CCTextureCache sharedTextureCache ] addImage :file];

CGSize texSize = texture. contentSize ;
CGRect texRect = CGRectMake ( 0 , 0 , texSize. width , texSize. height );
CCSpriteFrame * frame = [ CCSpriteFrame frameWithTexture :texture rect :texRect offset : CGPointZero ];
[ frames addObject :frame];
}
return [ CCAnimation animationWithName : name delay : delay frames : frames ];
}

// 通過 sprite frames 建立CCAnimation .
+( CCAnimation *) animationWithFrame:( NSString *)frame frameCount:( int )frameCount delay:( float ) delay
{
// 未實現
return nil ;
}
@end
現住,在Ship類的初始化方法裡,可以通過CCAnimation的類別Helper這樣簡單地建立動畫物件了:

-( id ) initWithShipImage
{
if (( self = [ super initWithFile : @"ship.png" ]))
{
// 使用類別Helper來建立動畫物件
CCAnimation * anim = [ CCAnimation animationWithFile : @"ship-anim" frameCount : 5 delay : 0.08f ];
// 建立Action:無限迴圈播放動畫
CCAnimate * animate = [ CCAnimate actionWithAnimation :anim];
CCRepeatForever * repeat = [ CCRepeatForever actionWithAction :animate];
[ self runAction :repeat];
[ self scheduleUpdate ];
}
return self ;
}
四、 Texture Atlas 貼圖集(或譯作紋理集)

1、定義

貼圖集Texture Atlas僅僅是一張大的貼圖。通過使用CCSpriteBatchNode,你可以一次性渲染所有的圖片。使用Texture Atlas不但節約了記憶體也提升了效能。

貼圖的大小(寬和高)總是2的n次方——例如1024*128或256*512。由於這個規則,貼圖尺寸有時候是大於圖片的實際尺寸的。例如,圖片大小140*600,當載入到記憶體時,貼圖尺寸是256*1024。這顯然是一種記憶體浪費,尤其是你有幾個這樣的單獨的Texture時。

因此有了Texture Atlas的概念。它是一張包含了多個圖片的圖片,並且它的尺寸已經是對齊的。所謂對齊,即是根據前面提到的那個規則,指它的長和寬都已經是2的n次方。貼圖集每一個角色幀(sprite frame)都定義為貼圖集中的一部分(一個矩形區域)。這些角色幀的CGrect則定義在單獨的一個.plist檔案裡。這樣cocos2d就可以從一張大的貼圖集中單獨渲染某個指定的角色幀。

2、Zwoptex

Zwoptex是一個2D貼圖工具,付費版需要$24.95。有一個7天的試用版,下載地址http://zwoptexapp.com 。但Zwoptex提供了一個flash版,沒有時間限制:http://zwoptexapp.com/flashwersion ,也基本夠用(僅僅有一些限制,比如2048*2048貼圖限制,角色旋轉等)。

如果你不想試用Zwoptex,那麼有一個可以替換的工具是TexturePacker:

http://texturepacker.com/

這裡以Zwoptex 0.3b7版本為例(這個是免費版)。開啟Zwoptex,預設是一個叫做Untitled的空白畫布。選擇選單:Sprite Sheet ——>Import Sprites,會彈出檔案選擇對話方塊,選擇你需要的角色幀圖片檔案,點選import,於是所有的圖片會匯入到Zwoptex。



選擇選單:Sprite Sheet——>Settings,會彈出佈局視窗:




你可以更改設定,包括畫布大小,排序規則、行間距、列間距等,目的是用最小的貼圖集容納全部所需的圖片。然後點選save去應用。

注意,除非單獨為3GS、iPad和iPhone4開發,否則不要使用2048*2048的畫布尺寸,因為老的型號最大隻支援1024*1024。

改變畫布大小時要當心,因為有時候圖片會疊在一起——由於空間不足。

建議不要手動更改圖片(如移動、旋轉),因為這個版本並不支援,它是自動佈局的。

Zwoptex會自動截掉圖片中透明邊沿,所以本來一樣大小的圖片在Zwoptex中會顯得大小不一。




不用擔心,cocos2d會自動計算這些誤差並正確顯示(不用擔心,這些資料都記載在.plist裡)。

點選File——>save選單,編輯結果儲存為.zss檔案格式(Zwoptex格式)。

點選Sprite Sheet——>Export——>Texture,編輯結果儲存為.png格式。

點選Sprite Sheet——>Export——>Cordinates,編輯結果儲存為.plist格式。

後兩者,正是cocos2d所需要的。

3、Cocos2d中使用貼圖集

首先,將Zwoptex生成的.png和.plist檔案加入到專案的Resource組中。然後在程式碼中使用貼圖集:

-( id ) initWithShipImage
{
// 用CCSpriteFrameCache載入貼圖集,用.plist檔案而不是.png檔案做引數
CCSpriteFrameCache * frameCache = [ CCSpriteFrameCache sharedSpriteFrameCache ];
[frameCache addSpriteFramesWithFile : @"ship-and-bullet.plist" ];

// 從貼圖集中載入名為ship.png的sprite,注意ship.png是.plist中定義的key,而不是真正意義的檔名
if (( self = [ super initWithSpriteFrameName : @"ship.png" ]))
{
// 從貼圖集中載入sprite幀,注意用.plist中的key值做引數而非檔名
NSMutableArray* frames = [NSMutableArray arrayWithCapacity : 5 ];
for ( int i = 0 ; i < 5 ; i ++)
{
NSString* file = [NSString stringWithFormat: @"ship-anim%i.png" , i ];
CCSpriteFrame * frame = [frameCache spriteFrameByName :file];
[frames addObject :frame];
}
CCAnimation * anim = [ CCAnimation animationWithName : @"move" delay : 0.08f frames :frames];
CCAnimate * animate = [ CCAnimate actionWithAnimation :anim];
CCRepeatForever * repeat = [ CCRepeatForever actionWithAction :animate];
[ self runAction :repeat];
[ self scheduleUpdate ];
}
return self ;
}
[CCSpriteFrameCache sharedSpriteFrameCache]是一個單例物件,其 addSpriteFramesWithFile 方法用於載入貼圖集(需要以.plist檔名作為引數)。對於大檔案貼圖集(超過512*512),載入過程可能會花費數秒,應當在遊戲開始前就載入。

CCSprite的initWithSpriteFrameName方法可以從貼圖集中獲取貼圖,並設定Sprite的顯示圖片,但它需要以貼圖集(.plist)中的幀名(framename,實際上是<frames>中的一個<key>)為引數。

當然,如果要從貼圖集中得到一個幀,可以用CCSpriteFrameCache的spriteFrameByName方法。這同樣需要用貼圖集中的幀名為引數。

如果你載入了多個貼圖集,但只要名為ship.png的幀只有1個,那麼cocos2d就可以找到正確貼圖的。


其他程式碼沒有任何改變。但出現了一個奇怪的現象:飛船的位置莫名其妙地向螢幕中心靠近了一點,儘管不是很明顯。這個問題很容易解決,之前Ship的初始化程式碼是這樣的:

Ship * ship = [ Ship ship ];
ship. position = CGPointMake (ship. texture . contentSize . width / 2 , screenSize. height / 2 );
[ self addChild :ship];


這個地方需要改變:

ship. position = CGPointMake (ship. contentSize . width / 2 , screenSize. height / 2 );
問題解決了。導致這個現象的原因是,ship物件的texture的contentSize要比ship物件的contentSize大(Ship的Texture現在用的是貼圖集——具體說就是 ship-and-bullet.png 這張圖,尺寸為256*256,而原來的ship.png才128*64)。

4、修改CCAnimation類別Helper

現在是實現類別Helper中的animationWithFrame方法的時候了:

+( CCAnimation *) animationWithFrame:(NSString*)frame frameCount:( int )frameCount delay:( float ) delay
{
// 構造一個frame陣列
NSMutableArray* frames = [NSMutableArray arrayWithCapacity :frameCount];
// 通過CCSpriteFrameCache從貼圖集中載入frame,並將frame加到陣列中
for ( int i = 0 ; i < frameCount; i ++)
{
NSString* file = [NSString stringWithFormat: @"%@%i.png" , frame, i ];
CCSpriteFrameCache * frameCache = [ CCSpriteFrameCache sharedSpriteFrameCache ];
CCSpriteFrame * frame = [frameCache spriteFrameByName :file];
[ frames addObject :frame];
}
// 用frame陣列構建animation物件並返回
return [ CCAnimation animationWithName :frame delay : delay frames : frames ];
}
現在,可以修改ship類中initWithShipImage方法,把CCAnimation的初始化修改為:

CCAnimation * anim = [ CCAnimation animationWithFrame : @"ship-anim" frameCount : 5 delay : 0.08f ];


5、弱水三千,只取一瓢飲

只要你願意,你可以把所有遊戲圖片都加到一個貼圖集裡。用3個1024*1024的貼圖集跟用20個更小的貼圖集效率更高。

對於程式設計師而言,應當把程式碼“分離”成不同的邏輯元件。於此不同,對於貼圖集來說,我們的目標就是儘可能多地把圖片放到一個貼圖集裡,儘可能降低記憶體空間的浪費。

用一個貼圖集放入玩家圖片,用另外的貼圖集放怪物A、B、C的圖片——這好像更符合邏輯些,但這僅僅有助於你有大量的圖片,而且每次你只是有選擇地載入一部分圖片的時候。

當你的圖片只需要3-4個1024*1024貼圖集的時候,你應當把所有圖片只放在這些貼圖集裡進行預載入。這需要12-16MB的記憶體。程式程式碼和其他資源如音訊則不會佔用這麼多記憶體,你可以把這些貼圖集都保留在記憶體,這樣哪怕只有128MB RAM的老IOS裝置也可以承受。

如果超過這個記憶體,就應該採取特別的策略了。比如,可以把遊戲圖片進行分類,並且只在當前地圖中載入必要的貼圖集。這可以減少地圖載入時的延時。

因為cocos2d會自動快取所有圖片,需要一種解除安裝貼圖的機制。絕大部分情況下你可以使用cocos2d提供的:

[[CCSpriteFrameCache sharedSpriteFrameCache]removeUnusedSpriteFrames];

[[CCTextureCache sharedTextureCache] removeUnusedTextures];

顯然應當在某些貼圖不再使用的時候呼叫這些方法。比如轉場景完成後。遊戲正在進行時就不行了。注意,僅僅在新場景初始化結束後,前面的場景才會被 deallocated 。意即在一個 Scenen 的初始化方法中你不能呼叫 removeUnusedxxx 方法——除非你在兩個 scene 轉換中使用了第 5 章的 LoadingScene 類,這樣你要擴充套件 LoadingScene 使它在載入新場景替時 remove 所有未使用的貼圖。

如果要絕對清除所有記憶體中的貼圖以載入新的貼圖,應當使用:

[CCSpriteFrameCache purgeSharedSpriteFrameCache];

[CCTextureCache purgeSharedTextureCache];


摘自:[url]http://blog.csdn.net/kmyhy/article/details/6387190[/url]

相關文章