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

Cocos2d-x
クリック(タップ)でパネルを置く

(2015. 2. 3)


 パネルを指定の位置に置く事ができるPanelクラスを作成しました。今度はその位置をクリック(タップ)で指定する所を作ってみます。



@ 画面内のタップ情報はラムダ式で受ける

 画面内のタッチ情報は次のようにすると取得できます:

// 画面タップ認識
auto listener = EventListenerTouchOneByOne::create();

listener->onTouchBegan = [&]( Touch *touch, Event *unused_event ) {
    // 画面をタッチした瞬間ここに飛んでくる!
    return true;
};

Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority( listener, this );
return true;

 まずEventListenerTouchOneByOneという画面内のシングルタッチを司るオブジェクトを作ります。このオブジェクトは画面をタップした時にコールされるコールバックを幾つか持っています。こういう何かイベントが起きた時に呼ばれるメソッドを持ったクラスを「リスナークラス」と言います。

 EventListenerTouchOneByOneのメンバのコールバックの一つonTouchBeganには上のような引数を持つ「ラムダ式」を代入出来ます。ラムダ式はC++11から追加された、超超強烈な機能です(^-^)/。ラムダ式は簡単に言えば「コールバック関数」のようなものです。実際Cocos2d-xも昔(v2)の頃はここにコールバック関数を引き渡していました。でも、今はラムダ式を使う事を推奨しています。ラムダ式とコールバック関数の一番の違いは、ラムダ式は上のようにメソッドの中に直接コールバックされる部分を書けてしまいます。コールバック関数方式の場合は、そういう関数を都度作らなければならなかったのですが、これが無くなったんです(^-^)。お陰で非常にすっきり記述できるようになりました。ラムダ式の細かな実装方法はここでは割愛しますが、これはC++でのゲーム制作の概念をひっくり返す程の強烈な代物なのです。

 onTouchBeginは画面にタッチした瞬間に呼ばれるコールバックです。ここにラムダ式を代入したら、EventDispathcerというオブジェクトにリスナーを登録します。これはゲーム全体を管理しているDirectorさんが持っているのでgetEventDispatcherメソッドでそれを取得します。そのaddEventListenerWidthScreenGraphPriorityというやけに長いメソッドは画面にある物のプライオリティに従ってコールしてくれる登録メソッドです。例えば画面にボタンがあって前面に配置されている場合はボタンのコールが優先されて上のラムダ式は呼ばれない事になります。第2引数にはラムダ式を持っている自分自身(this)を渡します。



A タップした位置にパネルを置く

 では、タップされた場所にパネルを置いてみます。Touchクラスはタッチされた位置の情報を保持する小さなクラスです。ゲッターとしてはこんなのがあります:

Vec2 getLocation() const;
Vec2 getPreviousLocation() const;
Vec2 getStartLocation() const;
Vec2 getDelta() const;
Vec2 getLocationInView() const;
Vec2 getPreviousLocationInView() const;
Vec2 getStartLocationInView() const;

タッチした位置はgetLocationメソッドで得られます。ラムダ式の中でこのメソッドからタッチ位置を得て、そこにパネルを置いてみます:

auto listener = EventListenerTouchOneByOne::create();

listener->onTouchBegan = [&]( Touch *touch, Event *unused_event ) {

    // タップ位置にパネルを配置
    Panel *panel_ = Panel::create();
    panel_->setPosition( touch->getLocation() );
    panel_->setMotion( 0, 0.0f );
    panel_->selectType( rand() % 2 ? Panel::Type_Maru : Panel::Type_Peke );
    this->addChild( panel_ );

    return true;
};
Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority( listener, this );

パネルを生成して、setPositionメソッドにタップした位置を直接渡しています。後はモーションやらパネルの絵やらを適当に決めてレイヤーの子として登録しているだけです。ラムダ式のおかげで本当にすっきり書けます(^-^)

 この実装で起動して画面をクリックしまくると、こうなりました:

置けまくりです(^-^)v



B 盤のマス上にピッタリ置く

 Aでタッチした位置にダイレクトに置く事はあっさりと出来ました。これまでの準備の賜物です(^-^)。では、これを盤のマス目の上にピッタリ収まるように置くようにしてみます。

 盤の一マスはパネルと同じサイズとします。縦横のマス数は不定ですが、そのマスの座標は以下の図からわかるように一意に決められます:

Cocos2d-xは左下が原点なので、ボードの設置位置を左下基点で決めたとすると、ボード内の(X,Y)位置にあるマスの中心点の座標は、

で計算できます。これでマスの(X,Y)位置がわかればOKです。ではその位置はどう判定するか?

☆マークをクリック位置(Cx,Cy)とすると、(Cx-Left、Cy-Bottom)は左下からの幅と高さになります。今1マスの幅と高さは(tw,th)と分かっているので、幅高さをこれで割れば整数部分にマスの縦横番号が出てきます。すなわち、

上の例の場合だと、(fx, fy ) = (2.8, 1.75)位の値が出てきます。あとはその整数部分が欲しい(X,Y)位置になりますのでint型にキャストして終わりです。

 上記の理屈をまんま実装したテストコードを書きます:

auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = [&]( Touch *touch, Event *unused_event ) {

    int boardWidth = 6;              // 盤のマスの横数
    int boardHeight = 6;             // 盤のマスの縦数
    Vec2 lb(80.0f, 30.0f);           // 盤の左上座標
    Vec2 twh( 64.0f, 64.0f);         // 1マスの縦横幅
    Vec2 cp = touch->getLocation();  // タップ位置

    Vec2 fxy(
         (cp.x - lt.x) / twh.x,
         (cp.y - lt.y) / twh.y
    );
    int x = (int)fxy.x;   // X位置
    int y = (int)fxy.y;   // Y位置

    // ボード内であると判定された場合にパネルを置く
    if ( (fxy.x >= 0 && x < boardWidth) && (fxy.y >= 0 && y < boardHeight) ) {

        // マスの中心座標を算出
        Vec2 p(
            lt.x + (x + 0.5f) * twh.x,
            lt.y + (y + 0.5f) * twh.y
        );

        // マスの中心座標にパネルを配置
        Panel *panel_ = Panel::create();
        panel_->setPosition( p );
        panel_->setMotion( 0, 0.0f );
        panel_->selectType( rand() % 2 ? Panel::Type_Maru : Panel::Type_Peke );
        this->addChild( panel_ );
    }

    return true;
};

Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority( listener, this );

これでタップされた位置にあるマスの中にパネルが置かれるようになりました:

テストが済んだので、この部分をまとめて「Boardクラス」を作ります。



C Boardクラス

 盤を表すBoardクラスの仕事は盤に関する情報の保持と提供です。盤には盤を表す絵(スプライト)があります。また盤上にはPanelが置かれます。そのPanelそのものとその位置(マス位置)は盤が知っていてい良いと思います。BoardはあくまでPanelとBoardスプライトの親という立ち位置に徹し、ゲームのルール自体は他の人のお仕事としたい所です。ただPanelが置かれたタイミングでルールと照らし合わせる必要があるので、「盤上にPanelが置かれた」時に呼び出されるコールバックをラムダ式で設定できるようにしましょう:

#include "cocos2d.h"
#include <functional>

class Panel;

class Board : public cocos2d::Node {

    int cellXNum_;
    int cellYNum_;
    float cellWidth_;
    float cellHeight_;

    // 指定の位置にパネルを置ける?
    bool isAdd( float x, float y, int &px, int &py );

public:

    // パネルが置かれたら呼び出されるコールバック
    std::function < void( int x, int y, Panel* panel) > onPanel;

    Board();
    virtual ~Board();

    CREATE_FUNC( Board );

    // 初期化
    virtual bool init();

    // 盤面スプライトを設定
    void setSprite( cocos2d::Sprite *sprite );

    // 盤面のマス目の情報を設定
    void setCell( int cellXNum, int cellYNum, float cellWidth, float cellHeight );

    // 指定の位置にパネルを置ける?
    bool isAdd( float x, float y );

    // パネルを盤に追加
    bool addPanel( Panel *panel, float x, float y );
};

.cppの実装はこういう感じ:

#include "board.h"
#include "panel.h"


USING_NS_CC;


Board::Board() : cellXNum_(), cellYNum_(), cellWidth_(), cellHeight_(), onPanel() {

}

Board::~Board() {

}

// 初期化
bool Board::init() {

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

    return true;
}

// 指定の位置にパネルを置ける?
bool Board::isAdd( float x, float y, int &px, int &py ) {

    Vec2 pos = getPosition();
    Vec2 twh( cellXNum_, cellYNum_ );
    Vec2 fxy = (Vec2( x, y ) - pos);
    fxy.x /= cellWidth_;
    fxy.y /= cellHeight_;

    if (
        fxy.x < 0.0f || (int)fxy.x >= cellXNum_ ||
        fxy.y < 0.0f || (int)fxy.y >= cellYNum_
    ) {
        return false;
    }

    px = (int)fxy.x;
    py = (int)fxy.y;

    return true;
}

// 盤面スプライトを設定
void Board::setSprite( Sprite *sprite ) {

    cocos2d::Size sz = sprite->getTextureRect().size;
    sprite->setPosition( sz.width / 2.0f, sz.height / 2.0f );

    addChild( sprite );
}

// 盤面のマス目の情報を設定
void Board::setCell( int cellXNum, int cellYNum, float cellWidth, float cellHeight ) {
    cellXNum_ = cellXNum;
    cellYNum_ = cellYNum;
    cellWidth_ = cellWidth;
    cellHeight_ = cellHeight;
}

// 指定の位置にパネルを置ける?
bool Board::isAdd( float x, float y ) {

    int px, py;
    return isAdd( x, y, px, py );
}

// パネルを盤に追加
bool Board::addPanel( Panel *panel, float x, float y ) {

    int px, py;
    if ( isAdd( x, y, px, py ) == false )
        return false;

    panel->setPosition( cellWidth_ * ( px + 0.5f ), cellHeight_ * ( py + 0.5f ) );
    this->addChild( panel );

    if ( onPanel )
        onPanel( px, py, panel );

    return true;
}

addPanelメソッドは先の理屈を使って指定の座標に該当するマス位置にPanelを置いています。置けた場合にonPanelコールバックを呼んでいるので、コールバックを登録した人がアクションを起こせます。ではこのBoardクラスのインスタンスを実際に作って、「画面をタッチ(クリック)する」→「指定の場所にパネルが置かれる」→「置けた知らせを受ける」という一連の流れをレイヤー内に実装してみましょう:

bool GameLayer::init() {

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

    this->scheduleUpdate();

    // ボードを作成
    board_ = Board::create();
    board_->setCell( 8, 8, 64.0f, 64.0f );
    board_->setPosition( 50.0f, (540 - 512) / 2.0f );
    this->addChild( board_ );

    board_->setSprite( Sprite::createWithTexture( Director::getInstance()->getTextureCache()->addImage( "board_bg.png" ) ) );

   // パネルが置かれたらコールされる
    board_->onPanel = [&]( int x, int y, Panel* panel ) {
        // 何かしよう
    };

    // 画面をタップしたらパネルを置く
    auto listener = EventListenerTouchOneByOne::create();
    listener->onTouchBegan = [&]( Touch *touch, Event *unused_event ) {

        Vec2 cp = touch->getLocation();

        if ( board_->isAdd(cp.x, cp.y) == true ) {

            // タップ位置にパネルを配置
            Panel *panel_ = Panel::create();
            panel_->setMotion( 0, 0.0f );
            panel_->selectType( rand() % 2 ? Panel::Type_Maru : Panel::Type_Peke );
            board_->addPanel( panel_, cp.x, cp.y );
        }

        return true;
    };

    // リスナーを登録
    Director::getInstance()->getEventDispatcher()->addEventListenerWithSceneGraphPriority( listener, this );

    return true;
}

これまで解説してきた事を実装しているだけです。これで盤面にパネルを「バチーン!」と置く事ができるようになりました:

さて次は、ここに「○×Evolutionルール」を適用してみます。