ホーム < ゲームつくろー! < IKD備忘録

Cocos2d-x
HelloWorldを眺めてみよう

(2015. 1. 18)


 自動で作成されたコードにはHelloWorldというクラスが定義されています。アプリにはすでに絵も出ているしボタンも出ている。これらがどう作られているかを知ればCocos2d-xをどう扱えば良いのかも見えてきそうです。まずは作成してくれたプロジェクトの中身を見てみましょう。



@ アプリケーションの初期化と呼び出し

 Cocos2d-xがデフォルトで作成してくれたコードは次の6ファイルです:

これらが何なのかを調べる為、F10でコードの最初から実行してみましょう。

 最初に来るのはmain.cppのエントリ関数内です:

main.cpp
#include "main.h"
#include "AppDelegate.h"
#include "cocos2d.h"

USING_NS_CC;

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {

    UNREFERENCED_PARAMETER(hPrevInstance);
    UNREFERENCED_PARAMETER(lpCmdLine);

    // create the application instance
    AppDelegate app;
    return Application::getInstance()->run();
}

はい、理想的にシンプルですね。AppDelegate(app)というオブジェクトを一つ作って、Applicationクラスが持っているgetInstanceメソッドを通してアプリケーションのオブジェクトを動的に作って、runメソッドで実行開始しているだけです。まだ個々のクラスについては見ていませんが、やっている事がコードから大体わかりますよね。

 AppDelegateオブジェクトはローカルで作っているだけですが、これが何をしているのかをちょっと調べてみます。

AppDelegate.h
class AppDelegate : private cocos2d::Application
{
public:
    AppDelegate();
    virtual ~AppDelegate();

    virtual void initGLContextAttrs();
    virtual bool applicationDidFinishLaunching();
    virtual void applicationDidEnterBackground();
    virtual void applicationWillEnterForeground();
};

これはAppDelegate.hの中身です(コメント等は省略しています)。これを見るとAppDelegateクラスはcocos2d::Applicationクラスから派生されています。つまり先の「AppDelegate app」は親クラスであるcoco2d::Applicationの初期化を行っているんですね。Applicationクラスのコンストラクタで自分自身(this)をsm_pSharedApplicationという変数に代入しています。で、Application::getInstanceメソッドでそれを返しています。あまり深入りはしませんが、main関数内でしている事はお約束という事です。

 Cocos2d-xのソースコードはプロジェクトの中でずっと追って行く事が出来ます。OpenGLを使っているのもそこからわかります。ゲームループの仕組みも割とシンプルで分かりやすいです。その辺りは一度追ってみると勉強になると思います。で、肝心の2D描画が行われているのはHelloWorldScene.cppの中です。



A HelloWorldScene.h

 Cocos2d-xが自動的に作成してくれたHelloWorldScene.hにはHelloWorldクラスが定義されています:

HelloWorldScene.h
#include "cocos2d.h"

class HelloWorld : public cocos2d::Layer
{
public:
    // there's no 'id' in cpp, so we recommend returning the class instance pointer
    static cocos2d::Scene* createScene();

    // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone
    virtual bool init();

    // a selector callback
    void menuCloseCallback(cocos2d::Ref* pSender);

    // implement the "static create()" method manually
    CREATE_FUNC(HelloWorld);
};

cocos2d::Layerというクラスから派生されているのがわかります。cocos2d::Layerクラスの宣言を見てみると、onTouchBegan()とかkeyPressed()などタッチパネルやキー入力などをキャッチするようなメソッド(コールバック)が並んでいます。どうやらLayerクラスは画面内に起こるアクションを捉えてくれるクラスのようです。それを継承しているHellowWorldクラスは同じ機能を持っているわけです。

 staticで定義されているHelloWorld::createSceneメソッドは、名前の通りシーンを作ってくれるメソッドなんだろうなと思います。その見てみます:

HelloWorld::createSceneメソッド
Scene* HelloWorld::createScene()
{
    // 'scene' is an autorelease object
    auto scene = Scene::create();

    // 'layer' is an autorelease object
    auto layer = HelloWorld::create();

    // add layer as a child to scene
    scene->addChild(layer);

    // return the scene
    return scene;
}

 目的は「Sceneオブジェクトを返す事」のようです。オブジェクトはScene::createメソッドで取得、次にHelloWorld::createメソッドでレイヤーを作成、Sceneオブジェクトに登録して返す、という事をしているようです。んーと、きっと「シーン(Scene)」という大きなくくりがあって、そこに「レイヤー(Layer)」を複数登録出来るんだろうなぁと想像します。シーンは複数のレイヤーを管理してくれていて、多分各レイヤーの更新や描画コールなどもしてくれるんだろうなと。

 HelloWorld::createメソッド…そんなのHelloWorldクラスに無かったなぁと思ったら、あぁ「CREATE_FUNC(HelloWorld);」というマクロで定義するようです。これも多分レイヤーを作る時のお約束ですね。HelloWorldScene.cppにその実装が無いので、これはマクロ内で実装も担ってくれているようです。中身を見ると、グローバルなレイヤー生成関数を作るようです。簡単に言えば内部でHelloWorldクラスのオブジェクトをnewしているだけでした。

 あれ?でもこのstaticなメソッドはHelloWorldクラスを知らないと呼べないはず…。誰がどこで呼ぶんだろう。調べてみると、おおっと意外な場所でコールされていました:

Appdelegate::applicationDidFinishLaunchingメソッド
bool AppDelegate::applicationDidFinishLaunching() {
    // initialize director
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();
    if(!glview) {
        glview = GLViewImpl::create("My Game");
        director->setOpenGLView(glview);
    }

    // turn on display FPS
    director->setDisplayStats(true);

    // set FPS. the default value is 1.0/60 if you don't call this
    director->setAnimationInterval(1.0 / 60);

    // create a scene. it's an autorelease object
    auto scene = HelloWorld::createScene();

    // run
    director->runWithScene(scene);

    return true;
}

AppDelegateクラスのapplicationDidFinishLaunchingメソッド内で名指しで呼ばれていました。多分このメソッドは名前からすると、アプリケーションの起動準備が整った後に呼ばれるようです。おお…という事は自分のオリジナルなシーンを作る時にはここを書き変えないといけなさそうです。んーー、まぁいいか(^-^;。

シーンを作ってレイヤーを登録すれば、レイヤーの初期化が走るようです。



B HelloWorldレイヤーの初期化

 HelloWorldクラスと似たように自分のレイヤーを作った時にinitメソッドが呼ばれます。HelloWorld::initメソッド内に初期化の仕方がコメントで書かれていました:

HelloWorld::initメソッド(初期化1段階目)
bool HelloWorld::init()
{
    //////////////////////////////
    // 1. super init first
    if ( !Layer::init() )
    {
        return false;
    }

    Size visibleSize = Director::getInstance()->getVisibleSize();
    Vec2 origin = Director::getInstance()->getVisibleOrigin();

    ...

微妙に長いので小出しで(^-^;。まず最初に親クラスであるLayerのinitメソッドは必ず呼ばないといけないようです。次にDirectorが持っているVisibleSizeとVisibleOriginというのを取得しているようです。実行して値を調べてみるとvisibleSize.width = 960、visibleSize.height = 640とあるのでこれはスクリーンサイズを取る方法のようです。originには(x, y)=(0, 0)とありました。んー、まぁ原点の座標なのでしょう。

HelloWorld::initメソッド(初期化2段階目)
    /////////////////////////////
    // 2. add a menu item with "X" image, which is clicked to quit the program
    // you may modify it.
    (訳:"X"画像でメニューアイテムを追加、これはプログラムを終了する時にクリックされる。改良できるよ)

    // add a "close" icon to exit the progress. it's an autorelease object
    (訳:"終了"アイコンを追加。自動リリースオブジェクトだよ)
    auto closeItem = MenuItemImage::create(
        "CloseNormal.png",
        "CloseSelected.png",
        CC_CALLBACK_1(HelloWorld::menuCloseCallback, this));

    closeItem->setPosition(Vec2(origin.x + visibleSize.width - closeItem->getContentSize().width/2 ,
    origin.y + closeItem->getContentSize().height/2));

    // create menu, it's an autorelease object
    (訳:メニューを作成。自動リリースオブジェクトだよ)
    auto menu = Menu::create(closeItem, NULL);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);

    ...

ここは必須項目では無くて終了アイコンを作っている所のようです。MenuItemImage::createメソッドを通してcloseItemというオブジェクト(auto型になっています)を作っています。引数を見ると2枚のPNGをファイル名指定し、第3引数に「CC_CALLBACL_1」というマクロを使ってコールバックを登録しているようです。絵の名前を見る感じでは、最初の絵が平常時の絵、次の絵が選択された時の絵のようです。マクロの使い方は第1引数にコールバックメソッド、第2引数にコールバックの第1引数に該当する物(ここではthis)を渡すようです。この辺りはもう少し詳しく使い方を調べる必要がありそうです。

 こうして出来たcloseItem(MenuItemImageオブジェクト)の表示位置はsetPositionメソッドで指定するようです。上の例では「右下にピッタリ合うように配置」しています。MenuItemImage::getContentSizeメソッドはテクスチャサイズを返してくれるんでしょう。これは重宝しそうです。

 次にメニュー(Menu)を作っています。これなんだろうなぁ…。MenuクラスはCCMenu.h及び.cppに内容があるようです。これを見ると、どうやらメニューは複数のMenuItemオブジェクトを一括で管理して、タッチ情報などを伝える役目をしているようです。なるほど。上の例ではメニューにcloseItemを登録して作成しています。作成したメニューの位置をsetPositionメソッドで決めて、レイヤーにaddChildメソッドで登録しています。addChildメソッドの第2引数は「zOrder」とあります。メニューの描画順番になるんでしょう。

HelloWorld::initメソッド(初期化2段階目)
    /////////////////////////////
    // 3. add your codes below...

    // add a label shows "Hello World"
    // create and initialize a label
    // (訳:"Hello World"ラベルを追加するよ)

    auto label = Label::createWithTTF("Hello World", "fonts/Marker Felt.ttf", 24);

    // position the label on the center of the screen
    label->setPosition(Vec2(origin.x + visibleSize.width/2,
    origin.y + visibleSize.height - label->getContentSize().height));

    // add the label as a child to this layer
    this->addChild(label, 1);

    // add "HelloWorld" splash screen"
    auto sprite = Sprite::create("HelloWorld.png");

    // position the sprite on the center of the screen
    sprite->setPosition(Vec2(visibleSize.width/2 + origin.x, visibleSize.height/2 + origin.y));

    // add the sprite as a child to this layer
    this->addChild(sprite, 0);

return true;

初期化の最後の方は"HelloWorld"をフォントと絵で表示する所の初期化を行っているようです。まずLabel::createWithTTFメソッドでフォントからラベルを作っています。第2引数にTTF(True Type Font)を指定してそこからフォントラベルを作成しています。TTFは有料/無料様々な物が世の中にありますので、それを使えばオリジナルな文字を自由に作れそうです。作ったラベルは位置を決めて(画面上部)、やはりLayer::addChildメソッドでzOrder付きで登録しています。

 後半は"HelloWorld"という「スプライト」をSprite::createメソッドで作っています。引数にファイル名を直接していするだけでスプライトが出来るのは楽で良いですね。これも位置を決めて(画面中央)、それを登録しています。「ん?でもHelloWorldって中央には出て無かったなぁ」」と思ったら、HelloWorld.pngってこれでした↓

なるほど、出てました(^-^;。Layer::addChildメソッドにzOrder=0で登録しています。上の画は背景ロゴ、という事はzOrderは若い程先に描画されるんだろうと思います。



C シーンの終了

 HelloWorldサンプルは右下にあるクローズボタンをクリックすると全体が終了します。Bの初期化の所で、このクローズボタンにはHelloWorld::menuCloseCallbackメソッドが対応しているのでした。中を見るとこうなっています:

HelloWorld::initメソッド(初期化2段階目)
void HelloWorld::menuCloseCallback(Ref* pSender)
{
#if (CC_TARGET_PLATFORM == CC_PLATFORM_WP8) || (CC_TARGET_PLATFORM == CC_PLATFORM_WINRT)
    MessageBox("You pressed the close button. Windows Store Apps do not implement a close button.","Alert");
return;
#endif

    Director::getInstance()->end();

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
    exit(0);
#endif
}

#ifでプラットフォーム別の処理をしていますが、肝は太文字の部分。「Directorのendメソッドを呼ぶ」です。ここをコメントアウトすると終了しませんでした。なるほど、終了はこうするのか。



D アプリ → シーン → レイヤー → メニュー → メニューアイテム

 ここまで見てきた所によると、どうやらCocos2d-xは「シーン」という非常に大きなくくりを一つ作る事からスタートするようです。HelloWorldサンプルではHelloWorldレイヤーのメソッド中で作っていましたが、多分これはそうしない方が良さそうです。なぜなら、レイヤーはシーンの子供だからです。子供の中で親を作るのはどうよ?って話です(^-^;。アプリの初期化終了時にAppDelegate::applicationDidFinishLaunchingメソッドが呼ばれます。そこでシーンを一つ作り、HelloWorldレイヤーを登録した方が素直に感じます。

 レイヤーには複数のメニューを登録できます(Layer::addChildメソッド)。そのメニューには複数のメニューアイテムを登録できます。メニューアイテムにはこのサンプル内ではMenuItemImage、Label、Spriteを登録しましたが、Layer::addChildメソッドに登録できるのはNodeクラスのオブジェクトです。上記はこれらの派生クラスだったわけです。んじゃNodeクラスを継承しているのには他に何があるのかなとソルーションエクスプローラに問い合わせるとこうでした:

いっぱい!Cameraとか描画環境に影響を与えそうな物もあるし、Boneはアニメーションの骨ですよね、これ。そういうのもLayerに登録出来るようです。つまり、Layerは単に描画する物だけではなくあらゆるものをぶら下げる事ができるようです。あ、ParticleSystemとかある(^-^)。楽しそうだな〜。

 どうやら、Cocos2d-xを攻略するカギはこの「Node」にありそうです。

 さてと、ではゲームループはどうするのかな?その辺りを次に調べてみます。