Cocos2d-xでテクスチャの簡易ヒットテスト(ver3.17.1対応版)

Cocos2d-xでテクスチャの簡易ヒットテスト(ver3.17.1で確認済み)

今回は、四角いSpriteのテクスチャー内において、色がついている場所がタッチされたかそうでないかを判断する方法を紹介したいと思います。

絵にすると以下のような感じになります。テクスチャーの透過度を利用し、少しでも色が付いてる箇所とそうでない箇所の違いによってタッチ判定を行います。

f:id:Takachan:20190518133241p:plain

ご注意

本記事は、フォーラムに回答があったので、その紹介と補足となります。

discuss.cocos2d-x.org

ネットで検索すると、よく以下のサイトが引っかかるのですがCocos2d-x 2.xの頃の記事で、3.x系列で同じように書いても動作しません。

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

また、以下のサイトは掲載コードが正常に動きますがそのままコピペして仕様するとリソースリークが発生するのでこちらを参照すのではなく、フォーラムか本記事を参照したほうが無難です。

cocos2d-x v3 ピクセルパーフェクトなタッチ判定をする(スプライトの透過部分を無視する) - 戦うアプリニート

確認環境

以下環境で確認しました。

  • Cocos2d-x 3.17.1
  • VisualStudio 2017 (15.9.11, SDK 10.0.17763.0)

使い方

まずは使い方から。

あるTestSceneに任意の画像を追加し、その画像の色が付いた部分がタッチされたかどうかを判定しています。

void TestScene::test_HitTest()
{
    // 確認対象の画像をシーンに追加
    Sprite* sp = Sprite::create("hogehoge.png");
    sp->setPosition(this->getContentSize() / 2.0f);
    this->addChild(sp);

    // タッチイベントの登録
    auto touch = EventListenerTouchOneByOne::create();
    touch->onTouchBegan = [=](Touch* pTouch, Event* pEvent)
    {
        if (PixelReader::isColorTouched(sp, pTouch)) // ★★★ここでタッチされた確認をしている
        {
            CCLOG("touched"); // 色がついている場所がタッチされた
        }
        return true;
    };
    this->_eventDispatcher->addEventListenerWithSceneGraphPriority(touch, this);
}

実装コード

フォーラム内で省略されている箇所の補足と、各種名前や宣言位置を変更しています。内容は完全に同じです。

ヘッダー側(PixelReader.hpp)

色を取るのにNodeを継承し、drawの動作を変更した自作のクラスを新規に作成する必要があるのでまずはヘッダーを宣言します。

補足:

フォーラムにあった、色を取得するクラス「KBRPixelReadNode」を「PixelReader」というクラス名に変更しています

// PixelReader.hpp

#pragma once
#pragma execution_character_set("utf-8")

#include "cocos2d.h"

class PixelReader : public cocos2d::Node
{
    cocos2d::Vec2 pos;              // タッチした座標(色を取得する場所)
    cocos2d::CustomCommand command; // drawされたときの動作の指定
    GLbyte color[4];                // 取得した色

public:
    
    bool init(const cocos2d::Point& readPoint);
    static PixelReader* create(const cocos2d::Point& readPoint);
    
    // 読み取った値を取得する
    GLbyte* getPixelColor() { return color; };

    // 基底クラスのdraw動作を上書き
    virtual void draw(cocos2d::Renderer *renderer, const cocos2d::Mat4& transform, uint32_t flags);
    // レンダーキュー内の特定の位置から色を取り出す
    void onDraw();

    // 指定したSpriteの色のついた部分がタッチされたかどうかを確認する大域メソッド
    static bool isColorTouched(cocos2d::Sprite* sp, cocos2d::Touch* pTouch);
};

実装側(PixelReader.cpp)

上記ヘッダーに対応するコードは以下の通りです。

// PixelReader.cpp

#include "PixelReader.hpp"

using namespace std;
using namespace cocos2d;

bool PixelReader::init(const cocos2d::Point& readPoint)
{
    if(!Node::init())
    {
        return false;
    }
    
    this->pos = readPoint;
    return true;
}

PixelReader* PixelReader::create(const cocos2d::Point& readPoint)
{
    auto p = new PixelReader();
    if (p->init(readPoint))
    {
        p->autorelease();
        return p;
    }
    else
    {
        delete p;
        return nullptr;
    }
}

void PixelReader::draw(Renderer* renderer, const Mat4& transform, uint32_t flags)
{
    command.init(_globalZOrder);
    command.func = CC_CALLBACK_0(PixelReader::onDraw, this);
    renderer->addCommand(&command);
}

void PixelReader::onDraw()
{
    glReadPixels(pos.x, pos.y, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &color[0]);
}

bool PixelReader::isColorTouched(cocos2d::Sprite * sp, cocos2d::Touch * pTouch)
{
    Size size = Director::getInstance()->getWinSize();
    PixelReader* r = PixelReader::create(pTouch->getLocation());
    RenderTexture* rt = RenderTexture::create(size.width, size.height, Texture2D::PixelFormat::RGBA8888);

    rt->beginWithClear(0, 0, 0, 0);
    sp->visit();
    r->visit(); // 作成したクラスもvisitする
    rt->end();
    
    Director::getInstance()->getRenderer()->render(); // render()するとonDraw()が呼ばれる
    
    GLbyte* buffer = r->getPixelColor();
    CCLOG("R = %i, G = %i, B = %i, A = %i", (int)buffer[0], (int)buffer[1], (int)buffer[2], (int)buffer[3]);

    return buffer[3] != 0;
}

RenderTextureクラスを使用して対象の画像をオフスクリーンレンダリングしてそのレンダリング結果から対象ピクセルの色を取り出す流れです。

少し長くなりましたが以上です。