WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

cocos2d-xでテクスチャの簡易ヒットテスト

こんにちは、エンジニアの成田です。

今回はテクスチャの簡易ヒットテストについて書きたいと思います。

画像をふんだんに使ったゲームを製作していると、境界が複雑なボタンを作らなければいけないシチュエーションに出会うことがあります。
例えばキャラクター画像の縁や世界地図での国境線はまっすぐな線でなくぐにゃぐにゃしているため、四角や円のタッチ領域を基本としていると、他のオブジェクトと近かったり重なった時などにタッチ判定が見た目と異なってしまいます。
ゲームの種類によってはそれでも誤魔化せる場合があったりするのですが、厳密にタッチ判定を行う必要があるときはどうすればよいでしょう。

これは正攻法とは言いがたいのですが、Flash界隈などではビットマップ上のタッチされた箇所のピクセルを取り出してα値を調べることで範囲内か範囲外かを判断していたりします。
今回はそれをcocos2d-xで行ってみました。

利用する画像はこのような、周囲が透過、中心部が非透過の画像です。

screenshot

この画像を画面いっぱいに表示し、ユーザが中心部の絵の部分を触っていたらtrue、外側を触っていたらfalseを返すような関数を作ってみます。
アプリのレイアウト構成はHelloWorldシーンの上にスプライトを乗せただけという簡易なものです。

bool HelloWorld::init()  
{  
    if ( !CCLayer::init() )  
    {  
        return false;  
    }  
  
    // タッチを取得できるようにする  
    this->setTouchEnabled(true);  
    setTouchMode(kCCTouchesOneByOne);  
  
    CCSize size = CCDirector::sharedDirector()->getWinSize();  
  
    // 透過画像スプライト  
    pSprite = CCSprite::create("texture.png");  
    pSprite->setPosition( ccp(size.width/2, size.height/2) );  
    this->addChild(pSprite, 0);  
  
    return true;  
}  

では、このシーンクラスがタッチイベントを拾った時に判断を行う関数を作ってみます。

bool HelloWorld::ccTouchBegan(CCTouch *pTouch, CCEvent *pEvent)  
{  
    CCPoint loc = pTouch->getLocation();  
    CCLOG("loc: x=%f y=%f", loc.x, loc.y);  
    pointIsTransparent(pSprite, loc);  
    return true;  
}  
  
bool HelloWorld::pointIsTransparent(CCSprite* sprite, CCPoint loc)  
{  
    // ワールド座標からスプライトのローカル座標へ変換  
    CCPoint localPoint = sprite->convertToNodeSpace(loc);  
    CCLOG("localPoint: x=%f y=%f", localPoint.x, localPoint.y);  
  
    // 空のテクスチャを作成  
    CCSize size = CCDirector::sharedDirector()->getWinSize();  
    CCRenderTexture* renderTexture = CCRenderTexture::create(size.width, size.height, kCCTexture2DPixelFormat_RGBA8888);  
  
    GLubyte *pixel = new GLubyte[4];  
  
    renderTexture->begin();  
  
    // 作成したテクスチャ上にスプライトをレンダリング  
    sprite->draw();  
  
    // テクスチャの特定座標のピクセルを取得  
    glReadPixels((GLint)localPoint.x,(GLint)localPoint.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel);  
  
    renderTexture->end();  
    renderTexture->release();  
  
    CCLOG("(R,G,B,A)=(%d,%d,%d,%d)", pixel[0], pixel[1], pixel[2], pixel[3]);  
    bool ret = (pixel[3] == 0);  
  
    delete [] pixel;  
    return ret;  
}  

ccTouchBegan()ではただタップ地点を取得してpointIsTransparentに渡しているだけです。
pointIsTransparent()が今回のキモの部分です。CCRenderTextureでテクスチャを作成し、begin()〜end()間で対象ノードのdraw()を呼んでやることでテクスチャ上に対象をレンダリングすることができます。
さらにglReadPixels()でフレームバッファを取得できることを利用して、テクスチャの任意のピクセルを取得しています。

この処理ではバッファにテクスチャを書き出すため毎フレーム走らせるのはお勧めできませんが、ユーザのタップ毎に一度走るだけなら十分実用的かと思います。