Seasons.NET

ちょっとした技術ブログです

cocos2d performance tips2(翻訳)

Cocos2d本家のパフォーマンスチューニングを軽く翻訳してみました。

  • XcodeのthumbオプションをOFFにする
    • thumbオプションは、floatを扱うコードを小さくしてくれる
    • thumbオプションを有効にすると、処理速度は、落ちてしまう。
  • Director
    • DisplayLinkを使うこと( ただし、iPhone SDK3.1から使える OS3.1以上が必要 )
    • DisplayLinkが使えない環境では、NSThreadで動作するThreadMainLoopか、MainLoopを使うこと
    • もしくは、NSTimerのDirectorで1/240.fのフレームレートで動作させて使うこと
  • 可能な限り、テクスチャatlasを使うこと
    • CCSpriteSheetの子供として、CCSpriteを使うこと
    • CCLabelの代わりにCCLabelAtlasかCCBitmapFontAtlasを使うこと
    • CCTMXTileMapかCCTileMapAtlasを使うこと
      • Atlasによる管理は、処理を複雑にするが、処理速度は速い。
      • →(画像を1枚にまとめることができる為。OpenGLESの描画命令回数を減らす事ができる為。
      • Atlasには、グループ分けして画像を登録しておくこと
  • 可能な限り、4bitか16bitのテクスチャを使うこと
    • 16bitのテクスチャには、PNG/GIF/BMP/TIFFが使える。
      • [CCTexture2D setDefaultAlphaPixelFormat:kCCTexture2DPixelFormat_RGBA4444];
    • 4bit,2bitテクスチャをPVRTCフォーマットで利用すること。
      • CCSprite *sprite = [CCSprite spriteWithFile: @"sprite.pvr"];
    • 32bitテクスチャは、最終手段だ。
  • メモリを減らすこと
    • 16bitか4bitのテクスチャを使うこと。パフォーマンスに期待出来る
    • CCTextureCacheを使うこと
      • CCTextureCacheは、全てのイメージをキャッシュする
      • どこでもその画像を使ってないとしてもメモリには画像が残っている
      • メモリから画像を消す必要がある。
// textures with retain count 1 will be removed
// you can add this line in your scene#dealloc method
// テクスチャのretainカウントが1のものを消します
// あなたはこのメソッドをSceneのdeallocに入れると良い。
[[CCTextureCache sharedTextureCache] removeUnusedTextures]; // v0.8から使える
 
// removes a certain texture from the cache
// テクスチャをキャッシュから消します。
CCTexture2D *texture = [sprite texture];
[[CCTextureCache sharedTextureCache] removeTexture: texture]]; // available in v0.7 too
 
// removes all textures... only use when you receive a memory warning signal
// 全てのテクスチャを消します。もし、memory warningが出たら消すこと。
[[CCTextureCache sharedTextureCache] removeAllTextures];    // available in v0.7
  • タイマーについて
    • CocoaのNSTimerを使わないこと。cocos2dのスケジューラーを代わりに使ってください。
    • もしあなたがcocos2dのタイマーを使えば、以下のような恩恵がある。
      • オートポーズ、リジュームが出来る
      • CCLayer(CCScene,CCSprite,CCNode)がステージに入った時に自動でアクティブになり、ステージから離れると非アクティブになります。
      • あなたのターゲットセレクターは、delata timeごとにコールされます。=指定したintervalで呼ばれる。
/**********************************************************/
// これはOK。指定したintervalで呼ばれる
/**********************************************************/
-(id) init
{
    if( (self=[super init] ) ) {
        // schedule timer
        [self schedule: @selector(tick:)];
        [self schedule: @selector(tick2:) interval:0.5];
    }
 
    return self;
}
 
-(void) tick: (ccTime) dt
{
    // bla bla bla
}
 
-(void) tick2: (ccTime) dt
{
    // bla bla bla
}
 
/**********************************************************/
// これはダメ!! NSTimerを直接使わないで!!
/**********************************************************/
// Why BAD ?
// You can't pause the game automatically.
-(void) onEnter
{
    [super onEnter];
    timer1 = [NSTimer scheduledTimerWithTimeInterval:1/FPS target:self selector:@selector(tick1) userInfo:nil repeats:YES];
    timer2 = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(tick2) userInfo:nil repeats:YES];
}
-(void) onExit
{
    [timer1 invalidate];
    [timer2 invalidate];
    [super onExit];
}
-(void) tick
{
    // bla bla bla
}
 
-(void) tick2
{
    // bla bla bla
}
  • 描画と更新について
    • 描画セレクター内では、状態変数を更新しないでください。
    • スケジュールセレクタ内では、描画を行わないでください。
    • 描画セレクタ内で描画に関する事を全て行う。
    • スケジュールセレクタ内で状態更新を行う。
    • 描画セレクタ内で状態更新を行うと、一時停止や再開時にそれが処理されないので気を付ける。
    • スケジュールセレクター内で描画を行うと、トランスフォーム(変形)処理ができない。
    • drawは、毎フレーム呼ばれる
    • スケジュールセレクターは、任意のフレームレートで呼ばれる。それはアプリケーションのFPSよりも頻繁に。
/**********************************************************/
// これは良い例
/**********************************************************/
-(void) draw
{
    [item draw];    // 描画内で呼び出しているOK!!
}
-(void) tick:(ccTime) dt
{
    item.position = dt * finalPosition; // スケジュールセレクター内で呼び出しているOK
}
 
/**********************************************************/
// ダメな例1
/**********************************************************/
-(void) draw
{
    dt = [self calculateDelta];         // 描画内でdeltaTimeを更新してはダメ
    item.position = dt * finalPosition; // 一時停止の時に動かない
 
    [item draw];
}
 
/**********************************************************/
// ダメな例2
/**********************************************************/
-(void) tick:(ccTime) dt
{
    item.position = dt * finalPosition;
    [item draw];            // <--- スケジュールセレクター内で呼び出してはいけない。
    // なぜなら、変形処理が働かなくなるから
  • Directorの操作
    • 可能ならば、pushSceneの代わりにreplaceSceneを使ってください。
    • pushSceneは、とても扱い易いが、メモリ上にSceneの情報を残してしまう。
      • iPhoneの貴重なメモリを消費してしまうのでなるべく避けること
// なるべくpushSceneは避けること
-(void) mainMenu()
{
    // etc
    [[CCDirector sharedDirector] pushScene: gameScene];
}
// stack:
//   . game  <-- running scene
//   . mainMenu
 
-(void) game
{
    [[CCDirector sharedDirector] pushScene: gameOverScene];
}
// stack:
//   . gameOver  <-- running scene
//   . game
//   . mainMenu
 
-(void) showGameOver
{
    [[CCDirector sharedDirector] pushScene: hiScoreScene];
}
// stack:
//   . scores  <-- running scene (4 pushed scenes... expensive)
//   . gameOver
//   . game
//   . mainMenu
  • Nodeの生成について
    • 可能な限り、initセレクターの中でオブジェクト、Nodeを生成すること。
    • Nodeを作成する事は、コストがかかる。可能ならば、事前に作成しておくことをオススメする。
    • メモリには注意してください。使っていないオブジェクトのメモリを確保する事は避ける。
/**********************************************************/
// 良い例
/**********************************************************/
-(id) init
{
    // etc...
 
    sprite1 = [CCSprite create];     // initでオブジェクトを生成する事は良い。
 
    // etc...
}
 
-(void) tick: (ccTime) dt
{
    // etc...
    if( someThing ) {
        [sprite1 show];         // あまり表示しないオブジェクトを抱える=無駄なメモリ消費があるということ
    }
}
 
/**********************************************************/
// ダメな例
/**********************************************************/
-(void) tick: (ccTime) dt
{
    // etc...
    if( someThing ) {
        sprite = [CCSprite create];      // スケジューラー内で生成するのはコストがかかる
        [sprite1 show];
 
        //...
 
        [sprite1 release];      // 少なくともここでメモリは開放されます。
    }
 
}
  • レイヤー階層
    • レイヤー階層を深く持つことはやめてください。
  • アクション
    • 特定のアクションを生成する事はコストがかかる。それはたくさんのmallocを必要とするからだ。
      • 例えば、CCSpawnのCCSequenceとCCRotateBy,その他のCCSequenceは、とてもコストがかかる。
    • なるべく、アクションは使いまわすこと。
    • 一度使ったアクションは、保存して、再利用したい時に使ってください。
    • 新しいアクションをアロケートすれば、それを初期化する事ができます。

2011-04-03追記
Objective-Cでは、既に初期化されたオブジェクトが再び、init系メソッドが呼び出されることを
想定していません。そのため、このようなことが行われるとメモリーがリークしてしまいます。
例として、CCSequenceが上げられます。initOne:というメソッドで初期化したCCSequenceアクションは、
前述の通り再利用することはできません。もし再利用したいならば、setActionOne:のような
メソッドを作成し、内部でセットしたアクションに対して、前回のものをrelease、新規のものをretainした
オブジェクトを返すものを用意する必要があるでしょう。
結果として、初期化メソッドを再度呼び出すということはなくなります。


具体的には、init系のメソッドではなく、autorelease付きのオブジェクトを返す
actions,actionOneを利用することで上記問題を回避することが可能になります。


1.0.0では、actionsWithArrayというNSArrayからCCSequenceオブジェクト(autorelease付き)
が生成できるようになりました。


Fix Me!!に関しては翻訳してません。