夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【cocos2d】教訓:replaceScene後、シングルトンの子ノードを削除する

次回シーン切り替えについて書こうと思いますが、その実装中、前のシーンのスプライトの残骸が画面に残るバグに悩まされました。

結論としては、シングルトンのCCSpriteBatchNodeのインスタンスにaddChildされているスプライト達をシーン切り替えのタイミングで解放していなかったせいでした。
シングルトンはこんな感じに実装されています。

@implementation MySpriteBatch

+ (MySpriteBatch *)sharedMyBatch
{
    static MySpriteBatch* sharedInstance = nil;
    if (sharedInstance == nil) {
        sharedInstance = [self batchNodeWithFile:@"icon-atlas.png"];
    }
    return sharedInstance;
}

@end

インスタンスはスタティック変数に保持されますので、プログラムの終了まで解放される事はありません。従って、ここにaddChildされてぶら下がる子ノード達はシーンの切り替え時に明示的にremoveしてあげる必要があります。
どこかからreplaceSceneが呼ばれ、現在シーンのメインとなるレイヤー(普通にテンプレートから作るとHelloWorldLayer)が別のシーンに置き換えられたあとに処理してあげます。タイミングとしてはインスタンスが消される直前のタイミングであるdeallocが良いと思います。下記のように全てのシングルトンインスタンスが持つ子ノードをremoveAllChildrenWithCleanupで解放してあげる事でスプライトの残骸を次のシーンに持ち越す事がなくなり、一件落着となりました。なお、ARCを有効にしている場合は[super dealloc];は不要というか禁止です。

- (void)dealloc
{
    // シングルトンインスタンスの子ノードを全部削除
    [[MySpriteBatch sharedMyBatch] removeAllChildrenWithCleanup:YES];
    [[MyParentNode sharedParentNode] removeAllChildrenWithCleanup:YES];
    [[MyParticleBatch sharedMyBatch] removeAllChildrenWithCleanup:YES];
}

なお、pushSceneでシーンを切り替える場合はdeallocは呼ばれませんので、その場合はonExitメソッドで次のように処理すれば同様の効果が得られます。ただし、本当にそうすべきかどうかはよく考える必要があると思います。pushSceneで設定画面を開き、popSceneで戻ると必要なスプライトが全部消えていたというようなことにならないように。
なお、onExitでは[super onExit];を必ず入れる必要があります。さもないと謎の不具合に悩まされます。

- (void)onExit
{
    [super onExit]; //忘れずにsuperのonExitを呼ぶ
    // シングルトンインスタンスの子ノードを全部削除
    [[MySpriteBatch sharedMyBatch] removeAllChildrenWithCleanup:YES];
    [[MyParentNode sharedParentNode] removeAllChildrenWithCleanup:YES];
    [[MyParticleBatch sharedMyBatch] removeAllChildrenWithCleanup:YES];
}