読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

Cocos2d-x の画面制御

こんにちは。今回のブログ担当 藤澤です。

Wonderplanet のスローガンに Tablet First というのがありますが、弊社のゲームでは SNS アカウントを使ってタブレットとスマートフォンを行き来したり、タブレットでは画面を横向きにすることで より遊びやすくなったりします。先日このあたりの画面制御の処理を見直す機会がありまして、せっかくなので今回はこれをネタにしてみようと思います。
※ 以下、Cocos2d-x 2.2.3 で確認しています。

画面サイズ

Cocos2d-x 標準では描画領域のサイズ(CCDirector::sharedDirector()->getWinSize() で返ってくるサイズ)は端末の解像度に一致します。つまりどういうことかというと、iPhone 向けにレイアウトしたアプリをそのまま iPad で動かすとレイアウトが崩れてしまいます。

上が iPhone 3.5 inch、下が iPad です。同じサイズの画像を表示していますが iPad のほうが解像度が高いので小さく見えてしまってします。
これについては以下のように DesignResolutionSize を固定にすることで解決できます。

bool AppDelegate::applicationDidFinishLaunching() {  
    // initialize director  
    CCDirector* pDirector = CCDirector::sharedDirector();  
    CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();  
  
    pDirector->setOpenGLView(pEGLView);  
      
    CCEGLView::sharedOpenGLView()->setDesignResolutionSize(960, 640, kResolutionShowAll);  
      
    // (略)  
}  

これで iPad でも iPhone と同じ見た目になりました。
しかし、今度は iPad や iPhone の 4 inch 端末で画面の縦横比の違いにより余白が生じてしまっています。

上記の画像は CCLayerColor で画面全体を白く塗りつぶしているのですが、画面の上下、または左右に黒い部分が残ってしまっています。iPhone 4 inch でいえば 1136 x 640 の画面のうち 960 x 640 だけを描画領域として指定しているため、残りの部分が黒く表示されているわけです。
setDesignResolutionSize の第3引数で引き伸ばしを指定することもできますが、それだと間延びした感じになってしまってよろしくないです。

さすがにこの違いを共通化して吸収することは難しそうなので、縦横比ごとにレイアウトを切り替えるしかなさそうです。CCEGLView::sharedOpenGLView()->getFrameSize() で端末の解像度が取得できますので、縦横比を計算して iPad 用、3.5 inch 用、4 inch 用でレイアウトを切り替えましょう(※)。
※ Android を考慮するともっと色々な画面サイズがありますが、さすがにすべては網羅できないので近いサイズのレイアウトを適用するのが現実的といえそうです。

また、ここでは DesignResolutionSize を 960 x 640 に設定しましたが、これを iPad で表示すると画像が引き伸ばされて粗くなってしまいます。リソースの容量に余裕があれば高解像度用の画像を用意して iPad ではそちらを表示するようにしてあげるとよいです。
このあたりは Cocos2d-x のサンプルプロジェクト(TestCpp)が参考になります。

    CCSize screenSize = CCEGLView::sharedOpenGLView()->getFrameSize();  
  
    CCSize designSize = CCSizeMake(480, 320);  
  
    CCFileUtils* pFileUtils = CCFileUtils::sharedFileUtils();  
      
    if (screenSize.height > 320)  
    {  
        CCSize resourceSize = CCSizeMake(960, 640);  
        std::vector<std::string> searchPaths;  
        searchPaths.push_back("hd");  
        pFileUtils->setSearchPaths(searchPaths);  
        pDirector->setContentScaleFactor(resourceSize.height/designSize.height);  
    }  
  
    CCEGLView::sharedOpenGLView()->setDesignResolutionSize(designSize.width, designSize.height, kResolutionNoBorder);  

このサンプルでは CCFileUtils::sharedFileUtils()->setSearchPaths() でリソースの参照元を変更して解像度が大きい場合は hd フォルダ内の画像を表示するようにしています。

画面の縦横切り替え

端末を横に傾けると表示も切り替わるというのは今ではすっかり当たり前になりましたが(ゲームで対応しているものは まだ多くはないようですが)、これを Cocos2d-x で実現する方法は iOS と Android でそれぞれ異なります。

iOS

iOS の場合はプロジェクトの Device Orientation の設定で Portrait にチェックをつけてあげれば端末の向きに追従するようになります。

しかし、それだけでは横のレイアウトのまま縦画面に表示されてしまい表示が見切れています。

端末の向きにあわせてきちんとレイアウトを変更するには、端末の回転を検知してレイアウトを再編集してやる必要があります。
iOS の場合は RootViewController の willRotateToInterfaceOrientation で端末の回転を検知できますので、以下のように実装してやります。

- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration  
{  
    if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation) == UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication.statusBarOrientation))  
        return;  
      
    if (UIInterfaceOrientationIsPortrait(toInterfaceOrientation) == UIInterfaceOrientationIsPortrait(UIApplication.sharedApplication.statusBarOrientation))  
        return;  
      
    cocos2d::CCSize fsize = cocos2d::CCDirector::sharedDirector()->getOpenGLView()->getFrameSize();  
    cocos2d::CCSize winSize = cocos2d::CCDirector::sharedDirector()->getWinSize();  
      
    CGSize s = CGSizeMake(fsize.height, fsize.width);  
      
    cocos2d::CCDirector* director = cocos2d::CCDirector::sharedDirector();  
    director->getOpenGLView()->setFrameSize(s.width, s.height);  
    director->getOpenGLView()->setDesignResolutionSize(winSize.height, winSize.width, kResolutionShowAll);  
      
    cocos2d::CCNotificationCenter::sharedNotificationCenter()->postNotification("ROTATED", NULL);  
}  

最後に CCNotificationCenter で通知を飛ばしていますが、各画面ではこの通知を受け取ってレイアウトの変更処理を行ないます。

Android

Android の場合は AndroidManifest.xml で android:screenOrientation を unspecified に設定すれば画面の回転に追従するようになりますが、これだけでは iOS のときと同様にレイアウトが適切に変更されません。Android で端末の回転を検出してレイアウトの変更を行なうためには、JNI を利用して Java から C++ に変更を知らせる必要があります。
まず、回転の検出は Cocos2dxRenderer の onSurfaceChanged でできますので、ここに C++ の呼び出しを書いてあげます。

   private static native void nativeSurfaceChanged(final int w, final int h);  
      
    @Override  
    public void onSurfaceChanged(final GL10 pGL10, final int pWidth, final int pHeight) {  
        nativeSurfaceChanged(pWidth, pHeight);  
    }  

次に C++ 側で呼び出される側のコードです。記述はどこでもよいですが、ここでは proj.android/jni/hellocpp/main.cpp に書いています。

    void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeSurfaceChanged(JNIEnv* env, jobject thiz, jint w, jint h)  
    {  
        CCEGLView* view = CCDirector::sharedDirector()->getOpenGLView();  
        if (!view)  
            return;  
          
        if (view->getFrameSize().width == w && view->getFrameSize().height == h)  
            return;  
          
        view->setFrameSize(w, h);  
        if (w < h)  
            view->setDesignResolutionSize(640, 960, kResolutionShowAll);  
        else  
            view->setDesignResolutionSize(960, 640, kResolutionShowAll);  
          
        CCNotificationCenter::sharedNotificationCenter()->postNotification("ROTATED", NULL);  
    }  

あとは iOS のときと同様に CCNotificationCenter の通知を受け取って各画面でレイアウトの変更を行なえば OK です。

画面の向きの強制

ところで、iOS でも Android でもコードで画面の向きを指定することができます。
iOS の場合は RootViewController の supportedInterfaceOrientations でサポートする画面の向きを返し(※)、Android では任意のタイミングで Activity の setRequestedOrientation を呼びます。
※ iOS6 以上の場合。iOS5 以下は shouldAutorotateToInterfaceOrientation を使用します。

- (NSUInteger) supportedInterfaceOrientations{  
#ifdef __IPHONE_6_0  
    return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight;  
#endif  
}  
   public static void resetOrientation() {  
        ((Activity)Cocos2dxActivity.getContext()).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);  
    }  

これを使えばアプリ内で画面によって縦横を切り替えたり、オプションで回転の有効無効を切り替えたりすることも可能です。
なお、Android の場合は setRequestedOrientation を呼んだ時点で強制的に画面が切り替わりますが、iOS のほうは実際に端末を傾けないと変更が有効になりません。無理やりな方法ではありますが、以下のようにすることで いちおう向きを変更させることができました。presentModalViewController で View を切り替えることで画面の回転イベントを発生させ、すぐに dismissModalViewControllerAnimated で元に戻しています。

            UIViewController *root = [[[UIApplication sharedApplication] keyWindow] rootViewController];  
            UIViewController *viewController = [[RootViewController alloc] init];  
            [viewController setModalPresentationStyle:UIModalPresentationCurrentContext];  
            viewController.view.frame = CGRectZero;  
            [root presentModalViewController:viewController animated:NO];  
            [root dismissModalViewControllerAnimated:NO];  
            [viewController release];  

今回は以上です。レイアウト関係はなかなか面倒な作業ですが、今回の内容が少しでもお役に立てば幸いです。