Cocos2d-x 3.17.1で円と矩形(長方形・角度あり)の衝突判定

タイトルにCocos2d-x(2D用のゲームエンジン)でと書きましたが考え方自体は普遍的な円と矩形(長方形)の当たり判定の処理についてです。と、言っても条件式式が結構難しい(当方には説明が難しい)ため解説は以下のサイトが分かりやすくまとまっていますのででそちらで確認ください。

ftvoid.com

四角形と円が重なるときの条件の出し方ややトリッキーなのかと思います。

実際に実装するとこんな感じになります。

ちゃんと白い丸の範囲に赤い四角形が入ったときに当たったと判定されています。

実装環境

今回の実装 & 確認環境は以下の通りです。

  • Cocos2d-x 3.17.1
  • VisualStudio2017 15.9.14
  • Windows10 .oO(すいません今回もモバイルで動作確認してません。多分動くの精神で…)

実装コード

解説サイトにあった式と大きく見た目が変わらないように式をコードにひとつづつ当てはめていきます。

CollitionUtilクラス

そんなに長いコードではないのでCollitionUtilクラスを作成してヘッダーに全部処理を記述してしまいます。

角度なし版

Nodeに角度が付いていない場合の処理方法は以下の通りです。

class CollitionUtil
{
public:

    // 引数
    //   [1] circle : 円として扱うノード
    //   [2] radius : [1]の円の半径
    //   [3] rect   : 衝突対象の四角形
    static bool isCollition(const cocos2d::Point& circle_center, float radius, const cocos2d::Rect& rect)
    {
        using namespace cocos2d;

        // 円の中心座標
        Point center = circle_center;
        float xc = center.x;
        float yc = center.y;
        // 円の半径
        float r = radius;
        // 対象の矩形
        Rect rect = rect->getBoundingBox();
        float x1 = rect.origin.x;
        float y1 = rect.origin.y + rect.size.height;
        float x2 = rect.origin.x + rect.size.width;
        float y2 = rect.origin.y;

        float r2 = pow(r, 2);
        float x1c2 = pow(x1 - xc, 2);
        float x2c2 = pow(x2 - xc, 2);
        float y1c2 = pow(y1 - yc, 2);
        float y2c2 = pow(y2 - yc, 2);

        bool a = (xc > x1) && (xc < x2) && (yc > y1 - r) && (yc < y2 + r);
        bool b = (xc > x1 - r) && (xc < x2 + r) && (yc > y1) && (yc < y2);
        bool c = x1c2 + y1c2 < r2;
        bool d = x2c2 + y1c2 < r2;
        bool e = x1c2 + y2c2 < r2;
        bool f = x2c2 + y2c2 < r2;

        bool ret = a || b || c || d || e || f;
        CCLOG("isin = %s", ret ? "true" : "false"); // 衝突してたらtrue
        
        return ret;
    }
};

コンパイル時にある程度最適化されるのでそこまで神経質になる必要はないかと思いますが、一時変数などに値を保存せずにそのまま判定文に書いたほうが効率的が良くなるかもしれません。

使い方

使い方は以下の通り。判定したいところで先ほどのメソッドを呼び出します。

円として扱うNodeには円の半径(radius)が必要です。

static void foo()
{
    // 円として扱う画像
    Sprite* circle = Sprite::create("a.png");
    // 円の半径
    float radius = 100.0f;
    
    // 衝突対象の四角形
    Sprite* targetRect = Sprite::create("b.png");
    
    // 衝突判定
    bool collided = CollitionUtil::isCollidedCircle2Rect(circle->getPosition(), radius, targetRect);
    if(collided)
    {
        CCLOG("衝突した");
    }
    else
    {
        CCLOG("衝突してない");
    }
}

角度あり版

Nodeに角度が付いている場合計算が少し難しくなります。

基本的にNode::getBoundingBox()で衝突対象の矩形を取得したいのですが、回転していると以下の図のように4辺の最大値が取れてしまいます。

f:id:Takachan:20190928045156p:plain

従って以下のサイトにある通り、矩形の回転を取り除き円の位置を補正してやる必要があります。

円形と矩形(角度あり)の衝突判定 | Miga's Hobby Programming

また、Cocos2d-xのNodeはAnchorPointの指定によって回転中心がNodeの中心以外に存在する場合があるのでそれも考慮して計算を行います。

こちらは動かすとこんな感じになります。

先ほどのCollitionUtilクラスに判定用のメソッドを追加します。

/**
 * 衝突判定に関係する汎用処理を記述する
 */
class CollitionUtil
{
public:

    // 先ほどと同じなので省略
    static bool isCollidedCircle2Rect(const cocos2d::Point& circle_center, float radius, const cocos2d::Rect& rect)
    {
        // ...
    }

    // --- ここから追加 ---

    // 円と矩形が衝突しているかどうかを矩形の回転を考慮して判定する
    //   引数:
    //     [1] circle_center  : 円の中心座標
    //     [2] circle_radius  : 円の半径
    //     [3] rotation_node  : 衝突判定を行うノード
    static bool isCollidedCircle2Rect(const cocos2d::Point& circle_center, 
        float circle_radius, cocos2d::Node* rotation_node)
    {
        using namespace cocos2d;

        // 矩形の回転中心の座標
        const Point& rect_center = rotation_node->getAnchorPointInPoints();
        // 矩形の回転角度
        float rect_angle = rotation_node->getRotation();
        // 回転していない矩形
        Rect rect_no_totate = getNoRotateRect(rotation_node);
        
        return isCollidedCircle2Rect(circle_center, circle_radius, 
            rect_center, rect_angle, rect_no_totate);
    }

    // 円と矩形が衝突しているかどうかを矩形の回転を考慮して判定する
    //   引数:
    //     [1] circle_center  : 円の中心座標
    //     [2] circle_radius  : 円の半径
    //     [3] rect_center    : 矩形の回転中心座標
    //     [4] rect_angle     : 矩形の回転角度
    //     [5] rect_no_rotate : 回転していない矩形
    static bool isCollidedCircle2Rect(const cocos2d::Point& circle_center, 
        float circle_radius, const cocos2d::Point& rect_center, 
            float rect_angle, const cocos2d::Rect& rect_no_rotate)
    {
        using namespace cocos2d;

        if (((int)rect_angle % 90) == 0) // 90度の倍数の場合は回転補正せずにそのまま計算できる
        {
            return isCollidedCircle2Rect(circle_center, circle_radius, rect_no_rotate);
        }
        else
        {
            Point new_circle_center = getCircleCenter(circle_center, rect_center, rect_angle);
            return isCollidedCircle2Rect(new_circle_center, circle_radius, rect_no_rotate);
        }
    }

    // 回転前の円の中心位置を取得する
    static cocos2d::Point getCircleCenter(const cocos2d::Point& circle_center, 
        const cocos2d::Point& rect_center, float angle)
    {
        using namespace cocos2d;

        Point p;
        float theta = CC_DEGREES_TO_RADIANS(-angle);
        p.x = cos(theta) * (circle_center.x - rect_center.x) - 
            sin(theta) * (circle_center.y - rect_center.y) + rect_center.x;
        p.y = sin(theta) * (circle_center.x - rect_center.x) + 
            cos(theta) * (circle_center.y - rect_center.y) + rect_center.y;

        return p;
    }

    // 指定したノードが回転していない時のRectを取得する
    static cocos2d::Rect getNoRotateRect(cocos2d::Node* node)
    {
        using namespace cocos2d;

        // 同じ情報を持つノードで rotation=0 のNodeを作る
        Node* _n = Node::create();
        _n->setContentSize(node->getContentSize());
        _n->setPosition(node->getPosition());
        _n->setAnchorPoint(Point::ANCHOR_MIDDLE);
        _n->setScale(node->getScale());
        return _n->getBoundingBox(); // 角度が0の場合の矩形が取れる
    }
};

Nodeが入れ子になってる場合など単純にNodeを渡しても計算ができない場面が稀によくあるため、プリミティブな型を渡して計算できるオーバーロードを実装しています。

使い方

中で色々やっていますが、使い方は先ほどの角度無し版とほぼ同じです。

static void foo()
{
    // 円として扱う画像
    Sprite* circle = Sprite::create("a.png");
    // 円の半径
    float radius = 100.0f;
    
    // 回転したNode
    auto sp = Sprite::create("a.png");
    sp->setPosition(100.0f, 100.0f);
    sp->setAnchorPoint({ 0.3f, 0.0f });
    sp->setRotation(45.0f);
    this->addChild(sp)
    
    // 汎用処理化しているので呼び出し自体は簡単
    bool collided = CollitionUtil::isCollidedCircle2Rect(circle->getPosition(), radius, sp);
    if(collided)
    {
        CCLOG("衝突した");
    }
    else
    {
        CCLOG("衝突してない");
    }
}

以上です。