cocos2dでの画面遷移(またはiPhoneで、コードでスクリーンショットをとる方法)
cocos2dを使ったゲーム開発で、紙芝居のように次画面をバックにおきつつ、現画面をスライドアウト(SlideOut)させることができないかと思い、試してみました。
cocos2dで画面遷移を行うには、replaceScene、pushScene、popSceneを利用します。その遷移の際には通常、CCTransitionファミリーのクラスを利用し、
[CCDirector sharedDirector] replaceScene: [CCTransitionFlipX transitionWithDuration:0.8 scene:[MainMenuScene node]]];
のように記述することで、FadeoutやSlide In/outの効果を得ることができます。 多くの利用シーンではそれで問題ないのですが、次画面が後ろに存在していながら、現在の画面を紙芝居のようにスライドさせて遷移させるには、少し工夫が必要でした。状況に応じて2つの方法がとれたので、その方法をメモしておこうと思います。
popSceneをTransition対応にする
replaceSceneやpushSceneでは、上記のTransitionを利用することができますが、popSceneでは、現在のcocos2dのバージョン(1.0.1)では利用ができません。ただ、cocos2dのフォーラムでは議論がなされており、将来的には正式リリースにくみこまれるかもしれません。それを待っている訳にもいかないので、議論のあった記事を参考にさせてもらい、popSceneをTransition対応にします。 まず、CCDirector.hに以下のメソッド定義を追加します。
- (void) popSceneWithTransition: (Class)c duration:(ccTime)t;
次に、その実装をCCDirector.mに行いましょう。
-(void) popSceneWithTransition: (Class)transitionClass duration:(ccTime)t; { NSAssert( runningScene_ != nil, @"A running Scene is needed"); [scenesStack_ removeLastObject]; NSUInteger c = [scenesStack_ count]; if( c == 0 ) { [self end]; } else { CCScene* scene = [transitionClass transitionWithDuration:t scene:[scenesStack_ objectAtIndex:c-1]]; [scenesStack_ replaceObjectAtIndex:c-1 withObject:scene]; nextScene_ = scene; } }
これで、popSceneでもTransitionが利用できるようになりました。実際には以下のようにして使います。
[CCDirector sharedDirector] popSceneWithTransition:[CCTransitionFlipX class] duration:0.8];
比較的軽い修正により、popSceneでもトランジション効果が得られるため、非常にスマートな方法だと思います。しかし、この方法には1つの制限があります。popSceneはその名の通り、シーンをPOPするので、pushされているシーンに対してしか利用できないということです。 つまり、アプリの設定画面や入力補助画面など、一時的に利用する画面をpushし、入力が終わったらそれをpopする場合には多いに利用できるでしょうが、メインメニューから各機能へ遷移するボタンでの利用の時などは、そもそも、後ろ(次の)画面にどのシーンが来るかは前もってわかりません。次に遷移する可能性のあるシーンをすべてpushしておくのも、メモリに余裕の無いスマートフォンでは、あまり良い方法とは言えないでしょう。
Layerを利用して、画面遷移効果を得る
そこで、すこしコーディングして、同じような効果を得ることにしました。 処理の流れは以下のような感じです。 ①遷移ボタンが押されたタイミングで、現在のシーンのキャプチャをとる。 ②このキャプチャしたイメージを引数にして、次のシーンを生成し、キャプチャから作ったLayerを画面のレイヤー階層の一番上に配置する。 ③現画面から次画面に切り替える。(同じ見た目なので、ユーザには切り替わったように見えない) ④完全に次画面に切り替わった後で、キャプチャLayerを自由に動かして、画面遷移の効果とする。 ①で現在の画面のキャプチャをとるため、画面がどのような状態であるか気にしないで、次の画面を用意できることが、この方法のメリットだと思います。 さて、私が実装した内容を以下に記述したいと思います。 まず、現在のシーンのキャプチャをとります。シーンを生成するクラスのどこに書いても良いと思いますが、私は、AppDelegateに記述しました。キャプチャするコードは、フォーラムのこの記事を参考にしました。
- (UIImage *)takeAsUIImageInRect:(CGRect)rect { CCDirector* director = [CCDirector sharedDirector]; if(rect.origin.x < 0) rect.origin = CGPointMake(0, rect.origin.y); if(rect.origin.y [director winSize].width) rect.size = CGSizeMake([director winSize].width, rect.size.height); if(rect.size.height > [director winSize].height) rect.size = CGSizeMake(rect.size.width, [director winSize].height); if(rect.origin.x+rect.size.width > [director winSize].width) rect.size = CGSizeMake(floor([director winSize].width - rect.origin.x), rect.size.height); if(rect.origin.y+rect.size.height > [director winSize].height) rect.size = CGSizeMake(rect.size.width, floor([director winSize].height - rect.origin.y)); CGSize size = rect.size; //Create buffer for pixels GLuint bufferLength = size.width * size.height * 4; GLubyte* buffer = (GLubyte*)malloc(bufferLength); //Read Pixels from OpenGL glReadPixels(rect.origin.x, rect.origin.y, size.width, size.height, GL_RGBA, GL_UNSIGNED_BYTE, buffer); //Make data provider with data. CGDataProviderRef provider = CGDataProviderCreateWithData(NULL, buffer, bufferLength, NULL); //Configure image int bitsPerComponent = 8; int bitsPerPixel = 32; int bytesPerRow = 4 * size.width; CGColorSpaceRef colorSpaceRef = CGColorSpaceCreateDeviceRGB(); CGBitmapInfo bitmapInfo = kCGBitmapByteOrderDefault; CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; CGImageRef iref = CGImageCreate(size.width, size.height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); uint32_t* pixels = (uint32_t*)malloc(bufferLength); CGContextRef context = CGBitmapContextCreate(pixels, size.width, size.height, 8, size.width * 4, CGImageGetColorSpace(iref), kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); CGContextTranslateCTM(context, 0, size.height); CGContextScaleCTM(context, 1.0f, -1.0f); switch (director.deviceOrientation) { case CCDeviceOrientationPortrait: break; case CCDeviceOrientationPortraitUpsideDown: CGContextRotateCTM(context, CC_DEGREES_TO_RADIANS(180)); CGContextTranslateCTM(context, -size.width, -size.height); break; case CCDeviceOrientationLandscapeLeft: CGContextRotateCTM(context, CC_DEGREES_TO_RADIANS(-90)); CGContextTranslateCTM(context, -size.height, 0); break; case CCDeviceOrientationLandscapeRight: CGContextRotateCTM(context, CC_DEGREES_TO_RADIANS(90)); CGContextTranslateCTM(context, size.width * 0.5f, -size.height); break; } CGContextDrawImage(context, CGRectMake(0.0f, 0.0f, size.width, size.height), iref); UIImage *outputImage = [UIImage imageWithCGImage:CGBitmapContextCreateImage(context)]; //Dealloc CGDataProviderRelease(provider); CGImageRelease(iref); CGContextRelease(context); free(buffer); free(pixels); return outputImage; }
次に、いろいろな場面で使えるように、キャプチャのレイヤー用のクラスを作成しておきます。各シーンは、このキャプチャした画像を引数に生成し、クラス内部に保持しておきます。
CaptureLayer.h
@interface CaptureLayer : CCLayer { } +(id) sceneWithScreenShot:(UIImage *)screenImage; -(id) initWithScreenShot:(UIImage *)screenImage; @end
CaptureLayer.m
@implementation CaptureLayer +(id) sceneWithScreenShot:(UIImage *)screenImage { CCScene *scene = [CCScene node]; CaptureLayer *layer = [[self alloc] initWithScreenShot:screenImage]; [scene addChild: layer]; return scene; } -(id) init { if( (self=[super init])) { } return self; } - (id) initWithScreenShot:(UIImage *)screenImage { if ((self = [super init])){ CCTexture2D *tex = [[CCTexture2D alloc] initWithImage: screenImage]; CCSprite *background = [[CCSprite spriteWithTexture:tex] retain]; [background setPosition:ccp(160,206)]; [self addChild:background]; [background release]; } return self; } - (void) dealloc { [super dealloc]; } @end
最後に、このCaptureLayerを保持したシーンが完全に生成されたタイミングで、Layerが動き始めるよう、Sceneクラスの中で、以下の実装を加えます。
Scene.m
-(void) onEnterTransitionDidFinish { [super onEnterTransitionDidFinish]; [captureLayer runAction:[CCMoveTo actionWithDuration:0.5 position:ccp(0,-1000)]]; }
以上で、紙芝居のようなFlideOut効果を得ることができました。実機で動かしてみましたが、うまく動いてくれているようです。
コラム
cocos2dでゲームを作る際に参考にした本です。初心者用、中級者用とありますので、参考にしてください。私には非常に役立ちましたよ!

これは非常にいい本です。画面遷移の基本から応用まで、体系だって解説がなされており、ゲーム作成時に何度も読み返しました。パフォーマンスを考慮したコーディングや物理エンジンについても言及があります。
cocos2dを使ってゲームを作るのが初めての人に最適です。いくつかのサンプルのプロジェクトとその解説があり、それをもとに、簡単なゲームは作れるようになるでしょう。初心者がつまづきやすい、AppStoreへの申請方法も例示されています。初めてゲームをリリースするときに参考にしました。

cocos2dで物理エンジンを利用したゲームを作る際に参考になります。英語ですが、物理エンジンを解説した本は少ないため、貴重です。