いぬでもわかる

iOSやCocos2dxが好きでときどきAndroid。できるだけ毎日何かしら書く事を目標に頑張る。

cocos2d-xでportraitゲームを作る

cocos2d-xのデフォルトで作ったプロジェクトはlandscape設定されています。
が、気軽にできるゲームとしては片手でできるportraitモードにしたいという人も多いでしょう。
縦横の変更は簡単ですが、cocos2d-xからではなくiOSAndroidそれぞれのネイティブのコードでそれぞれ管理します。

iOS
変更する箇所は大きく2つあります。
1つは、TargetsのDeployment infoでDevice OrientationでPortraitを選択します。
2つ目は、以下のコードを変更します。

  • RootViewController.mm
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
	return UIInterfaceOrientationIsPortrait(interfaceOrientation);
//    return UIInterfaceOrientationIsLandscape( interfaceOrientation );
}

- (NSUInteger) supportedInterfaceOrientations{
#ifdef __IPHONE_6_0
	return UIInterfaceOrientationMaskPortrait;
//    return UIInterfaceOrientationMaskAllButUpsideDown;
#endif
}

Android
Androidの変更箇所は一カ所だけです。
AndroidManifest.xmlファイルのandroid:screenOrientation="landscape"となっているラインを、android:screenOrientation="portrait"に変更します。

  • AndroidManifest.xml
<activity android:name="org.cocos2dx.cpp.Cocos2dxActivity"
  android:label="@string/app_name"
  android:screenOrientation="portrait"
  android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
  android:configChanges="orientation">
</activity>

cocos2d-x 3.0

しばらくcocos2d-xから離れていたら3.0がbetaリリースされていました。
書籍を見ながら久しぶりに触っていると結構異なっていてなかなか想い通りに進まない!
どんな変更があるのかと見てみると色々大きな変更も多いような。


c++11機能

  • callbackのためにstd::functionやlamdaの実装
  • strongly typed enums

kから始まる定数やenumをstrongly typed enumsで置き換えました。

  • override

overrideキーワードをつけられたメソッドがオーバーライドされていない場合エラーになります。

Objective-cパターンの削除

  • CCプリフィックス削除

CCSpriteやCCLabelTTFなどCCプレフィックスがついているクラスやメソッドからCCが削除されました。

  • メソッド名の変更

例えば、singleインスタンスの取得メソッドがshared~()からgetInstance()に変更されていたりします。

  • getterメソッドにgetがついた
//2.2まで
node->boundingBox();
//3.0以降
node->getBoundingBox() ;
  • copy()の代わりにclone()を実装した

clone()で複製されたインスタンスは、autoreleaseされているのでrelease、autoreleaseする必要はありません。
copy()は、compileはできちゃうけどクラッシュするので使わないで下さい。

  • PODタイプ

SizeやPointなどのPODタイプの引数を持つメソッド

// v2.1
void setTexParameters(ccTexParams* texParams);
// v3.0
void setTexParameters(const ccTexParams& texParams);

TouchDispatcher系のイベントデスパッチャーの削除
代わりにEventListenerが追加されました。

auto sprite = Sprite::create("file.png");
...
auto listener = EventListenerTouchOneByOne::create();
listener->setSwallowTouch(true);
listener->onTouchBegan     = [](Touch* touch, Event* event) { do_some_thing();  return true;  };
listener->onTouchMoved     = [](Touch* touch, Event* event) { do_some_thing();  };
listener->onTouchEnded     = [](Touch* touch, Event* event) { do_some_thing();  };
listener->onTouchCancelled = [](Touch* touch, Event* event) { do_some_thing();  };
// The priority of the touch listener is based on the draw order of sprite
EventDispatcher::getInstance()->addEventListenerWithSceneGraphPriority(listener, sprite);
// Or the priority of the touch listener is a fixed value
EventDispatcher::getInstance()->addEventListenerWithFixedPriority(listener, 100); // 100 is a fixed value

物理特性の統合
PhysicsWorld、PhysicsBody、PhysicsShape、PhysicsJoint、PhysicsContactからなります。
これらを利用したい場合には、ccConfigヘッダでCC_USE_PHYSICSを定義する必要があるようですが、デフォルトで定義されていました。

  • PhysicsWorld

PhysicsWorldオブジェクトは、衝突判定や他の物理プロパティをシミュレーションします。そして、直接オブジェクトを生成する必要はなく、sceneオブジェクトからPhysicsWorldオブジェクトを得る事ができます。

Scene* scene = Scene::createWithPhysics();
PhysicsWorld* world = scene->getPhysicsWorld();
PhysicsBody
  • PhysicsBody

PhysicsBodyオブジェクトは、ノードに物理シミュレーションを追加する時に使われます。

PhysicsBody* body = PhysicsBody::createCircle(5.0f);
Node* node = Node::create();
node->setPhysicsBody(body);
scene->addChild(node);

物理オブジェクトとC++11については正直良く分かっていないので、サンプルコードを載せながらまた説明できればと思っています。

https://github.com/cocos2d/cocos2d-x/blob/develop/docs/RELEASE_NOTES.md

cocos2d-xでの複雑なアクション

ジャンプさせたり、移動させたりのアニメーションについて説明しました。
が、単独でのアニメーションを利用することは稀で、例えばジャンプと移動を合わせた複合的なアニメーションを使う事の方が多いと思います。
そんな複雑なアニメーションもcocos2d-xでは簡単に作る事ができます。

まず、アニメーションの変化量を直線的ではなく、カーブを描くように終わりと初めを緩やかに変化させたい場合にはCCEase〜を使います。
イージングの適用場所によってCCEaseInやCCEaseOutといったようにクラスの名前が変わります。
イージングを適用することでよりぽく動くようになると思います。

CCEaseInOut::create(CCActionInterval *pAction, float fRate)
第1引数にはイースイン、アウトを適用させるアクション、第2引数には変化量の割合

//200,200の移動を移動し始めと移動し終わりの変化量を緩やかにする。
CCEaseINOut* ease = CCEaseInOut::create((CCActionInterval*)CCMoveBy::create(1, ccp(200, 200)), 3);
sprite->runAction(ease);


次に、複数のアクションを連続して適用する場合にCCSequenceを使います。
CCSequenceインスタンスの生成時の引数に適用したいアクションを渡します。
CCSequence::create (CCFiniteTimeAction *pAction1,...)

//x,y軸それぞれに200移動した後、さらにx軸に100移動します
CCSequence* sequence = CCSequence::create(CCMoveBy::create(1.0, ccp(200, 200)), CCMoveBy::create(1.0, ccp(100, 0)), NULL)
sprite->runAction(sequence);


また、アニメーション終わりに特定のメソッドを呼び出したりすることもできます。

//x,y軸それぞれに200移動した後、HelloWorld::finishAnimationメソッドを呼び出します
CCSequence* sequence = CCSequence::create(CCMoveBy::create(1.0, ccp(200, 200)), CCCallFuncN::create( this, callfuncN_selector(HelloWorld::finishAnimation)), NULL)
sprite->runAction(sequence);

CCSequenceはアニメーションの連続再生ですが、CCSpawnを使う事で同時再生も行う事ができます。

CCSpawn* spawn = CCSpawn::create(CCMoveBy::create(1.0, ccp(200, 200)), CCRotateBy::create(1, 60), NULL);
sprite->runAction(spawn);

cocos2d-xでの単一アクション

cocos2d-xでは、CCActionクラスを使う事でCCNodeが持っているpositionやscale、rotationといったプロパティを、時間とともに変化させ簡単にアニメーションを作ることができます。
CCActionクラスは、createというstaticなメソッドを持っており、第1引数にはアニメーション時間をとります。
CCActionクラスを継承したアクションクラスは多くあり、リファレンスから確認できます。
以下にいくつかアクションクラスをあげます。

CCMoveTo(float duration, const CCPoint &position)
指定時間をかけて指定したpositionに移動する

//x:100、y:50に1秒かけて移動する。
CCAction* action = CCMoveBy::create(1.0f, ccp(100, 50));
sprite->runAction(action);

CCMoveBy(float duration, const CCPoint &deltaPosition)
CCMoveToとは異なり、指定時間をかけて指定したposition分移動

//現在位置から横方向に100、縦方向に50移動する。
CCAction* action = CCMoveBy::create(1.0f, ccp(100, 50));
sprite->runAction(action);

CCScaleTo(float duration, float s)
指定時間をかけて指定倍率まで拡大/縮小

//1秒掛けて2倍に拡大表示
CCAction* action = CCScaleBy::create(1, 2);
sprite->runAction(action);

CCRotateBy::create(float fDuration, float fDeltaAngle)
指定時間をかけて指定角度傾ける

//1秒かけて現在の角度から90度傾ける
CCAction* action = CCRotateBy::create(1, 90);
sprite->runAction(action);

CCRotateTo::create(float fDuration, float fDeltaAngle)
指定時間をかけて指定角度まで傾ける

//1秒かけて指定角度まで傾ける
CCAction* action = CCRotateTo::create(1, 90);
sprite->runAction(action);

Actionクラスはこの他にもありますので用途に合わせて利用してください。
また、見て分かるように、〜Toクラスは指定の値まで変化させ、〜Byクラスは指定の値分相対的に変化させます。

setDesignResolutionSizeとsetContentScaleの動作

cocos2d-xでマルチディスプレイ対応をしようと思ったら外せないのがsetDesignResolutionSizeとsetContentScale。
いまいち何をしているのかわからず使って来た上、この2つのメソッドがどういった動きをするのか解説しているページもなかったので調べてみた。

リファレンスの情報は少ないので、基本は公式のマルチディスプレイ対応のドキュメント
http://www.cocos2d-x.org/projects/cocos2d-x/wiki/Multi_resolution_support
と自分で動作を確認した結果です。
何か間違えがあれば指摘してもらえると嬉しいです。


まず、non-retina iPhoneでは320*480の座標系ですが、retina iPhoneでは640*960であるようにデバイスごとに異なる座標系の問題を解決します。
cocos2d-xは、デバイスごとに異なる画面の解像度ではなくDesign Resolutionと呼ばれるサイズをもとにした座標系を使います。
Design Resolutionを設定することで、開発者はデバイスごとに異なるデバイスサイズではなくDesign Resolutionのみを意識するだけでマルチディスプレイ対応したアプリの開発をすることができます。
Design Resolutionの設定は、setDesignResolutionSizeで行います。

  • CCEGLView::sharedOpenGLView()->setDesignResolutionSize(width, height, policy)
    • width
      • Design Resolutionの幅を設定
    • height
      • Design Resolutionの高さを設定
    • policy
      • デバイスサイズとDesign Resolutionのアスペクト比が異なる場合のDesign Resolutionの適用方針


policyは以下の3つの値を取ります。

  • policy
    • kResolutionExactFit
      • Design Resolutionのアスペクト比を保たず画面サイズにフィットする形で適用されます。
    • kResolutionNoBorder
      • アスペクト比を保ったまま、余白が出ないような形で適用されます。
    • kResolutionShowAll
      • アスペクト比を保ったまま適用されます。縦もしくは横に余白が発生します。


non-retina、retinaそれぞれのiPhone向けにDesign Resolutionを設定してみます。

//design Resolutionのサイズを320*480に設定します
CCSize designSize = CCSize(320, 480);
//画面サイズの取得
CCSize screenSize = pEGLView->getFrameSize();
CCSprite* backgroundImage;

//Design Resolutionの設定。
//iPhone5の場合は縦方向に余白が発生します。
CCEGLView::sharedOpenGLView()->setDesignResolutionSize(designSize.width, designSize.height, kResolutionShowAll);

if(screenSize.height > 480){
  //retina向けに高解像度の背景を用意
  backgroundImage = CCSprite.create("640_960.png");
}
else{
  backgroundImage = CCSprite.create("320_480.png");
}
backgroundImage->(ccp(designSize.width * 0.5f, designSize.height * 0.5f));
this.addChild(backgroundImage);


non-retinaのiPhoneでは正常に動作します。
しかし、このコードを実行してみるとわかりますが、retina iPhoneでは背景画像がはみ出て設定されます。
というのもDesign Resolutionに320*480を設定しているため高解像度の背景画像を設定するのでは、横方向に320ぶん、縦方向に480ぶんはみ出てしまいます。
そのため、non-retina同様320*480の画像を設定すれば奇麗に収まり表示されます。
しかし、retinaなのに低解像度の背景を設定するわけにはいきません。
そこで、setContentScaleの出番です。

  • CCDirector::sharedDirector()->setContentScaleFactor(scale)
    • scale
      • 設定する背景画像と設定したDesignResolutionとの比


用意した画像をDesignResolutionに合った形で表示を行えるように、画像の解像度とDesignResolutionとの比率を設定します。
一般的に設定しやすいために設定する背景画像の高さもしくは幅と、Design Resolutionの高さもしくは幅との比を設定します。
setContentScaleFactorを設定したコードは以下のようになります。

float ImageHeight;
if(screenSize.height > 480){
  //retina向けに高解像度の背景を用意
  backgroundImage = CCSprite.create("640_960.png");
  imageHeight = 960;
}
else{
  backgroundImage = CCSprite.create("320_480.png");
  imageHeight = 480;
}
//scaleの設定
CCDirector::sharedDirector()->setContentScaleFactor(imageHeight/designSize.height);
backgroundImage->(ccp(designSize.width * 0.5f, designSize.height * 0.5f));
this.addChild(backgroundImage);

cocos2dxで複数画面サイズに対応する

前回の記事で、リソースファイルの読み込み方について説明しましたが もう少しそれを発展させて様々なディスプレイサイズで動作するようにコードを書いていきます。

AppDelegate::applicationDidFinishLaunching

// directorの初期化
CCDirector* pDirector = CCDirector::sharedDirector();
CCEGLView* pEGLView = CCEGLView::sharedOpenGLView();

pDirector->setOpenGLView(pEGLView);

//retina ipad用にサイズを設定
CCSize designSize = CCSize(2048, 1536);

//設定したサイズを縦横比を保たず画面サイズに合うように拡大/縮小表示
CCEGLView::sharedOpenGLView()->setDesignResolutionSize(designSize.width, designSize.height, kResolutionExactFit);

CCSize screenSize = pEGLView->getFrameSize();
//画面サイズに合わせてリソースファイルパスを設定する
if (screenSize.height > 768) {
    searchPaths.push_back("ipadhd");
    fileUtil->setSearchPaths(searchPaths);
} else if (screenSize.height > 320) {
    searchPaths.push_back("ipad");
    fileUtil->setSearchPaths(searchPaths);
} else {
    searchPaths.push_back("iphone");
    fileUtil->setSearchPaths(searchPaths);
}

CCEGLView::setDesignResolutionSize(float width, float height, ResolutionPolicy resolutionPolicy)

第三引数に設定するResolutionPolicyには3つの値を設定できます。 kResolutionExactFit 画面の比率と第一引数と第二引数に渡したサイズの比率が異なっても画面一杯に引き延ばされます。

kResolutionNoBorder アスペクト比を保って余白無しに画面サイズ一杯に表示されます。 画面の比率と第一引数と第二引数に渡したサイズの比率が異なった場合、一部表示されない部分が発生します。

kResolutionShowAll アスペクト比を保って余白有りで画面サイズ一杯に表示されます。 画面の比率と第一引数と第二引数に渡したサイズの比率が異なった場合、何も表示されない部分が発生します。

COCOS2DX Beginer's Guid

COCOS2DX Beginer's Guidを読み進めていますが、書籍のバージョンと最新バージョンでは差異がある模様。 スクリーンサイズなどの違いによって利用するリソースファイルを使い分けたい場合、読み込むリソースディレクトリを指定します。

本だと以下のようになっていますが

CCSize screenSize = pEGLView->getFrameSize();
if(screenSize.height > 768){
  CCFileUtils::sharedFileUtils()->setResourceDirectory("ipadhd");
}
else{
  CCFileUtils::sharedFileUtils()->setResourceDirectory("ipad");
}

cocos2dx-2.1.1からsetResourceDirectoryがdeprecatedされCCFileUtils::setSearchPathを使用するようになっています。 setSearchPathはstring型のベクタークラスを引数にとります。

std::vector<std::string> resDirOrders;
if (screenSize.height > 768)
{
    resDirOrders.push_back("resources-ipadhd");
}

また、ファイルパスの取得方法も変わっています。 オーディオファイルの読み込みは、CCFileUtils::fullPathFromRelativePathを使ってファイルのパスの取得を行っていましたが、

SimpleAudioEngine::sharedEngine()->preloadBackgroundMusic(CCFileUtils::sharedFileUtils()->fullPathFromRelativePath("background.mp3"));
    SimpleAudioEngine::sharedEngine()->preloadEffect( CCFileUtils::sharedFileUtils()->fullPathFromRelativePath("bombFail.wav") );

fullPathFromRelativePathがdeprecatedされCCFileUtils::fullPathForFilenameでのパスの取得に成っています。

SimpleAudioEngine::sharedEngine()->preloadBackgroundMusic((CCFileUtils::sharedFileUtils()->fullPathForFilename("background.mp3")).c_str());
SimpleAudioEngine::sharedEngine()->preloadEffect((CCFileUtils::sharedFileUtils()->fullPathForFilename("bombFail.wav")).c_str());