Cocos2d-x v3.8でAndroid用ゲームを作ってみる

tamuraです。 cocos2d-xで自分にとってHelloWorld的なゲームを作りました。 いくつかはまったポイントがありました。

(今回作ったやつは以前にiアプリ版やJavaScript版として作ったものなので、自分的にHelloWorldプロジェクトです)

AppDelegate.cpp

初期設定等を行っているやつです。 今回は単純に拡大してほしかったので、簡単な設定になっています。

bool AppDelegate::applicationDidFinishLaunching() {
    // 初期化
    auto director = Director::getInstance();
    auto glview = director->getOpenGLView();
    if(!glview) {
        glview = GLViewImpl::create("PostKun");
        director->setOpenGLView(glview);
    }

    // デモ画面用に20FPSにしているけど、実際のゲームでは10FPSで動いています
    director->setAnimationInterval(1.0 / 20);

    // 画面サイズは 120 x 160
    // 良い感じに拡大してもらいます
    glview->setDesignResolutionSize(120, 160, ResolutionPolicy::SHOW_ALL);
    Size frameSize = glview->getFrameSize();

    register_all_packages();

    // デモ表示
    auto scene = DemoScene::createScene();
    director->runWithScene(scene);

    return true;
}

タイトル(デモンストレーション)

画面はエミュレータで動かしたやつです。 実際のゲームの倍のスピードでなわとびをし続けます。

デモ画面

タップが有効にならない

終了ボタンを押したら終了、それ以外を押したらゲームスタートとしています。 イベントリスナーで画面タッチを得ていますが、このやり方がわからなくてはまりました。

当初はタップが終わったら(クリックで言うとボタンがリリースされたら)押されたと判断しようと思い、こんな感じで書いていました。 当然ながら動きません。

auto exitLabel = Label::createWithTTF("exit", "fonts/arial.ttf", 16);
...
this->addChild(exitLabel);

auto exitListener = EventListenerTouchOneByOne::create();
exitListener->onTouchEnded = [](Touch* touch, Event* event) {
    Director::getInstance()->end();
};
this->getEventDispatcher()->addEventListenerWithSceneGraphPriority(exitListener, exitLabel);

onTouchBegantrueが返っていないとonTouchEndedが反応しないっぽいということに気付き、修正しました。

で、今見たらちゃんと書いてました http://www.cocos2d-x.org/reference/native-cpp/V3.8/df/de4/classcocos2d_1_1_layer.html#adc596c208246932a23848702cfda7502

あと、関数の中でちゃんとLabelが押されたのかどうかを判定する必要があります。 この判定をしないとどこをタップしてもLabelが押されたと判定されます。

exitListener->onTouchBegan = [](Touch* touch, Event* event) {
    auto target = event->getCurrentTarget();
    auto targetBox = target->getBoundingBBox();
    auto touchPoint = Vec2(touch->getLocation().x, touch->getLocation().y);

    if (targetBox.contains(touchPoint)) {
        // 押された
        return true;
    }

    return false;
}

ここらへんはなんとなくマクロで定型化できそうです。

V-Sync割り込み

scheduleUpdate()を呼び出せば、指定したフレームレートでvoid update(float f)が呼び出されます。 このゲームではアニメーションが7コマあるので、カウンタ変数を使ってそこら辺を制御しています。

void DemoScene::update(float delta)
{
    counter += 1;
    if (counter == 6) {
        counter = 0;
    }
    else if (counter == 5) {
        postkun->jump();
    }
    characterLayer->enterFrame(counter);
}

キャラクターと線を描く

このゲームはポストととび縄があり、とび縄は伝統的に直線で表現していました。 今回もそのやり方を踏襲します。

レイヤーを用意する

デモ画面とプレイ画面でほぼ同じ動きをするため、共通化させるためにレイヤーを一つ用意しました。 そのレイヤーに

  • 前面のとび縄
  • キャラクター
  • 背面のとび縄

となるように各オブジェクトを配置しています。

bool CharLayer::init()
{
    backLayer = DrawNode::create();
    frontLayer = DrawNode::create();
    sprite = Sprite::create("post.png");
    sprite->setAnchorPoint(Vec2(0.5f, 0.5f));

    this->addChild(backLayer, 0);
    this->addChild(sprite, 1);
    this->addChild(frontLayer, 2);

    this->setVisible(false);

    return true;
}

描画

countの値に応じてとび縄を前面に描くか背面に描くのかを判断しています。 描画する数が少ないのでRenderTextureとか使わなくても大丈夫です。

void CharLayer::enterFrame(int count)
{
    backLayer->clear();
    frontLayer->clear();
    if (count < 3) {
        draw(count, backLayer);
    }
    else {
        draw(count, frontLayer);
    }
    postkun->enterFrame();
    sprite->setPositionX(postkun->x);
    sprite->setPositionY(postkun->y);
}

static const な配列の初期化

あとはとび縄の描画です。 とび縄の各頂点は事前に分かっているので構造体を定義して、その配列を持つようにしています。 なにげに static const な配列を初期化する書き方にはまりました。書き慣れたJavaと違います。

// とび縄の頂点の数とその位置
struct NAWAPOS
{
    int size;
    int x[9];
    int y[9];
};

class CharLayer : public cocos2d::Layer
{
public:  
    ....
private:  
    const static struct NAWAPOS nawapos[7];
}

const struct NAWAPOS CharLayer::nawapos[7] = {
    // 0
    {
        7,
        { 19, 25, 38, 46, 42, 41,  4},
        { 46, 54, 58, 58, 54, 53, 46}
    },
    ....
};

描画するときはcountに応じてループをまわして線を描くだけです。

void CharLayer::draw(int count, DrawNode* node)
{
    const struct NAWAPOS *pos = &CharLayer::nawapos[count];
    Vec2 p1 = Vec2(PostKunConstants::NAWA_CENTER_X + pos->x[0], PostKunConstants::NAWA_CENTER_Y - pos->y[0]);
    for (int ix = 1; ix < pos->size; ix++) {
        Vec2 p2 = Vec2(PostKunConstants::NAWA_CENTER_X + pos->x[ix], PostKunConstants::NAWA_CENTER_Y - pos->y[ix]);
        node->drawSegment(p1, p2, 0.5f, Color4F(0, 0, 0, 1));
        p1 = p2;
    }
}

カウントダウン

一応タイミングゲームなので、カウントダウンをやっておいたほうが親切です。 ここではワンショットのタイマーを使ってカウントダウンしています。

this->scheduleOnce(schedule_selector(PlayScene::readyThree), 0.8f);
this->scheduleOnce(schedule_selector(PlayScene::readyTwo),   1.6f);
this->scheduleOnce(schedule_selector(PlayScene::readyOne),   2.4f);
this->scheduleOnce(schedule_selector(PlayScene::readyStart), 3.2f);
this->scheduleOnce(schedule_selector(PlayScene::start),      4.0f);

void PlayScene::readyThree(float frame) { scoreLabel->setString("3..."); }
void PlayScene::readyTwo(float frame)   { scoreLabel->setString("2..."); }
void PlayScene::readyOne(float frame)   { scoreLabel->setString("1..."); }
void PlayScene::readyStart(float frame) { scoreLabel->setString("start!"); }

void PlayScene::start(float frame)
{
    scoreLabel->setString("score:0");
    jumped = false;
    this->scheduleUpdate();
    listener->setEnabled(true);
}

スコア表示

Labelでやっています。intからstringへの変換はStringUtils::formatで行っています。

auto str = StringUtils::format("score:%d", score);
scoreLabel->setString(str);

ゲームオーバー

うまく飛べないと転びます。約1秒後にもう一度やるかどうか聞かれます。 updateにあまりいろいろ詰め込みたくなかったので別Sceneにしています。

// 転んでいる絵
auto sprite = Sprite::create("post.png");
sprite->setPosition(PostKunConstants::CENTER_X, PostKunConstants::CENTER_Y);
sprite->setRotation(-20);
this->addChild(sprite);

// リプレイを問う
this->scheduleOnce(schedule_selector(GameOverScene::doQuit), 1.5f);

感想

以前にenchant.jsを使ってブラウザで遊べるバージョンを作ったのですが、そのときのよりもiアプリ版に近い動きになりました。

https://github.com/tamurashingo/postkun_android

関連記事

comments powered by Disqus