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

Cocos2d-x
「パネルをバチン!」をクラス化する

(2015. 1. 31)


 前回パネルを盤に置く時のモーションをテストコードで作りました。ただそのままだとあちこちに置く事ができません。ので、クラス化作業をします。



@ どういう機能が必要か?

 クラス化の基本は「インターフェイス(メソッド)」です。外部の人がどうそのクラスを扱うかなぁ〜っとイメージするのがコツです。

 パネルを扱う時にまず欲しいのが「位置の指定」です。これはスクリーン座標指定にします。実際は盤の升目に配置されますが、それは盤(Board)の役目です。パネルは○と×がありますので、それはフラグで指定すると扱いやすいですね。置く時には前回の「バチーン!」というモーションを勝手に起こして欲しいので、それはクラス中に隠ぺいしてしまいましょう。ただ、パネルは「バチーン!」と置くだけでなく、別のモーションがあるかもしれません。例えば何かコンボ的な物が起こった時に「キラーン」と光るかもしれません。吹っ飛ぶかもしれません。あらゆるモーションを考えると切りが無いので、「モーションパータンID」でそれを指定する方式にします。それなら追加もできますものね。

 という事で、クラスのインターフェイスはざっくりこんな感じです:

Panelクラス
class Panel : public Node {
public:
    enum Type {
        Type_Maru = 0,
        Type_Peke = 1,
        TypeNum
    };

protected:
    cocos2d::Texture2D *tex_[TypeNum];
    cocos2d::Sprite *sprite_;

public:
    // 生成メソッド
    CREATE_FUNC( Panel );

    // 初期化
    virtual bool init();

    // パネルの種類を選択
    void selectType( Type type );

    // モーションをセット
    void setMotion( int motionId, float startTime );
};

 パネルはレイヤーとか何か別の物の子オブジェクトになれるのが理想です。Nodeクラスを継承すれば他のNodeの子になれます。ただし、Nodeクラスを継承する時にはNodeクラスが要求する物を実装する必要があります。その一つが「CREATE_FUNCマクロによる生成メソッド群の作成」です。Nodeクラスを継承したオブジェクトは必ずこのマクロが提供するcreateメソッドで作成する決まりになっています。これにより「自動リリース」機能が提供されます。次にinit仮想メソッドを実装します。Panelオブジェクトをcreateメソッドで作った時に呼ばれます。この辺りの実装は後述です。

 作成したら操作系のメソッドが呼び出せます。位置の設定はNodeクラスが持っているsetPositionメソッドがそのまま使えますので特にPanelクラスには定義しません。selectTypeメソッドは○や×のイメージタイプを列挙型で指定します。setMotionメソッドは第1引数にモーションID、第2引数にモーションが起こるまでの時間を指定できるようにしました。例えばコンボみたいなものが起こる時に、時間差で「キラーン、キラーン、キラーン!」と光ると動きがあって見栄えが良くなります。

 これらを少しずつ実装していきましょう。 


A Nodeクラスを継承したクラスのお約束

 Nodeクラスはゲーム内の「物」の基本で、位置等の情報を保持し、お互いに親子関係を取る事が出来る基本クラスです。ゲーム中のほぼ全てのオブジェクトはこのクラスを継承する事になると思います。Nodeクラスの派生クラスでは、Nodeクラスが規定する「お約束」に従う必要があります。

 Nodeオブジェクトはnew等で生成してはいけません。Cocos2d-xでは「CREATE_FUNCマクロ」を宣言部に埋め込む事を推奨しているようです。このマクロはcreateメソッドを提供してくれます。createメソッドの中ではオブジェクトがnewされるだけでなく、オブジェクトの寿命を管理する仕組みが整えられます。また、createメソッドが呼ばれた後に自身のinitメソッドも呼んでくれます。

 そのinitメソッドは次のような実装を行います:

Panel::initメソッド
// 初期化
bool Panel::init() {

    if ( Node::init() == false )
        return false;

    // 初期化作業
    ....
}

createメソッドは自分自身のinitメソッドを呼びますが、その親のinitメソッドは呼んでくれません。そこで自分のinitメソッド内で親のinitメソッドを呼ぶのがお約束になります。親のinitメソッドがtureならば、後は自分の初期化をするだけです。


B テクスチャキャッシュ

 パネルは沢山作られます。この時毎回スプライトをPNGファイルから作ると時間もかかりますしメモリもガンガン圧迫します。通常テクスチャは複製するのでは無くて「参照」するようにします。そのためには、一度作ったテクスチャをストックする管理人が必要です。それを担うのが「テクスチャキャッシュ」です。こういうのは自前で作る事もありますが、Cocos2d-xにはTextureCacheというそのままのクラスが用意されています。そして、このクラスのオブジェクトをDirectorが保持しています。

 パネルクラスの初期化では「○」と「×」のテクスチャを両方保持しておきます。作成したテクスチャを指定時にSpriteにアタッチすればスプライトとして使えるようになります:

Panel::initメソッド
// 初期化
bool Panel::init() {

    if ( Node::init() == false )
        return false;

    // 初期化作業
    // パネルテクスチャを作成
    tex_[Type_Maru] = Director::getInstance()->getTextureCache()->addImage( "maru_panel.png" );
    tex_[Type_Peke] = Director::getInstance()->getTextureCache()->addImage( "peke_panel.png" );

    // スプライトを初期化してノードに登録
    sprite_ = Sprite::createWithTexture( tex_[Type_Maru] );
    this->addChild( sprite_ );

    sprite_->setOpacity( 0.0f ); // 初期状態では非表示に

    ....

    return true;
}

TextureCache::addImageメソッドを呼ぶと指定のファイルからテクスチャ(Texture2Dオブジェクト)を作ります。この時、すでに作成されている(ストックされている)テクスチャだった場合は作成せずにすでにあるテクスチャを返してくれます。

 SpriteにはそのテクスチャをcreateWithTextureメソッドで引き渡します。そのスプライトをPanel自身に登録する事でPanelが世界に置かれるとスプライトも表示されます。。



C モーション生成

 続いて「バチーン!」というモーションを組み込みます。前章ではシーケンスとパーティクルを組み合わせてパネルにくっつけましたが、今回はsetMotionメソッドで指定された時にそれを再生するので、一連のシーケンスをスプライトにアタッチしてくれるサブメソッドに実装をまとめましょう。実装の中身は前章と大体同じです:

Panel::setMotionメソッド
// モーションをセット
void Panel::setMotion( int motionId, float startTime ) {
    if ( motionId == 0 )
        createPlaceMotion( startTime );
}

// 配置モーション生成
void Panel::createPlaceMotion( float startTime ) {

    sprite_->setScale( 1.5f );

    float rotTm = 0.015f, rotAng = 3.0f;
    CCSequence *rotAct = CCSequence::create(
        CCRotateTo::create( rotTm, rotAng ),
        CCRotateTo::create( rotTm, 0.0f ),
        CCRotateTo::create( rotTm, -rotAng ),
        CCRotateTo::create( rotTm, 0.0f ),
        NULL
    );

    CCSequence *motionSeq = CCSequence::create(
        CCDelayTime::create( startTime ),
        CCScaleTo::create( 0.075f, 1.0f ),
        CCCallFuncN::create( this, callfuncN_selector( Panel::flashPanelCallback ) ),
        CCRepeat::create( rotAct, 3 ), // 回転リピート
        NULL
    );

    CCSequence *fadeInSeq = CCSequence::create(
        CCDelayTime::create( startTime ),
        CCFadeIn::create( 0.075f ),
        NULL
    );

    sprite_->runAction( fadeInSeq );
    sprite_->runAction( motionSeq );
}

void Panel::flashPanelCallback( Node* pSender ) {

    auto flash_ = Sprite::createWithTexture(
                      Director::getInstance()->getTextureCache()->addImage( "flash_panel.png" )
                  );

    Sprite *parent = (Sprite*)pSender;
    const Rect &rect = parent->getTextureRect();
    flash_->setPosition( rect.size / 2.0f );

    CCSequence *seq = CCSequence::create(
        CCFadeIn::create( 0.2f ),
        CCFadeOut::create( 0.1f ),
    NULL
    );

    flash_->runAction( seq );

    BlendFunc blendAdd;
    blendAdd.src = GL_ONE;
    blendAdd.dst = GL_ONE;
    flash_->setBlendFunc( blendAdd );

    parent->addChild( flash_, 5 );

    // パーティクル
    class SparkParticle {
    public:
        static ParticleSystemQuad* create( float x, float y, float ang, float lenX, float lenY ) {

            auto em = ParticleSystemQuad::createWithTotalParticles( 500 );
            em->setTexture( Director::getInstance()->getTextureCache()->addImage( "fire.png" ) );

            em->setDuration( 0.05f );

            em->setGravity( Point( 0, 0 ) );
            em->setAngle( ang );
            em->setAngleVar( 45 );
            em->setPosition( Point( x, y ) );
            em->setPosVar( Vec2( lenX, lenY ) );
            em->setLife( 0.15f );
            em->setLifeVar( 0.05f );
            em->setStartColor( Color4F( 255, 255, 255, 1 ) );
            em->setEndColor( Color4F( 255, 255, 255, 0.0f ) );
            em->setStartSize( 15 );
            em->setStartSizeVar( 4 );
            em->setEndSize( 8.0f );
            em->setEndSizeVar( 2 );
            em->setEmissionRate( 1000 );
            em->setSpeed( 261 );
            em->setSpeedVar( 100 );

            em->setBlendAdditive( true );

            return em;
        }
    };

    Size sz = rect.size;
    ParticleSystemQuad *p1 = SparkParticle::create( sz.width, sz.height / 2, 0, 0, sz.height / 2 );
    ParticleSystemQuad *p2 = SparkParticle::create( sz.width / 2, sz.height, 90, sz.width / 2, 0 );
    ParticleSystemQuad *p3 = SparkParticle::create( 0, sz.height / 2, 180, 0, sz.height / 2 );
    ParticleSystemQuad *p4 = SparkParticle::create( sz.width / 2, 0, 270, sz.width / 2, 0 );

    parent->addChild( p1, 10 );
    parent->addChild( p2, 10 );
    parent->addChild( p3, 10 );
    parent->addChild( p4, 10 );
}

一連のシーケンスをプログラム化するとどうしても長くなってしまいますね(^-^;。でもこれで、パネルクラスはめでたく独立、好きな所に配置できるようになりました。レイヤーの中でパネルオブジェクトを作ってみます:

bool GameLayer::init() {

    if ( Layer::init() == false )
        return false;

    this->scheduleUpdate();

    for ( int y = 0; y < 8; y++ ) {
    for ( int x = 0; x < 8; x++ ) {

        Panel *panel_ = Panel::create();
        panel_->setPosition( (960 - 64 * 7) / 2 + x * 64, (540 - 64 * 7) / 2 + y * 64 );
        panel_->setMotion( 0, (y * 8.0f + x) / 16.0f );
        panel_->selectType( rand() % 2 ? Panel::Type_Maru : Panel::Type_Peke );
        this->addChild( panel_ );

    }}

    return true;
}

パネルの生成が凄くすっきりしました(^-^)。上のコードは縦横8マスに左下から順番にパネルを時間差で表示させています。また、表示するパネルの種類をランダムで○か×にしています。実際に動かした時のスクリーンショットはこんな感じです:

ズドドドドド!こりゃ楽し〜〜(^▽^)

 これでパネルの配置やモーションについては一段落。次はゲームルールのお話です。