簡體   English   中英

libgdx中的Circle-Rectangle碰撞側檢測

[英]Circle-Rectangle collision side detection in libgdx

我花了幾個小時尋找解決方案:我正在用libgdx開發一個小型自上而下的游戲(也許這對我使用的引擎很重要)。 現在我必須在我的角色(圓圈)和牆壁(矩形)之間實現碰撞檢測。 如果可以滑動,我希望角色在碰撞時沿着牆壁滑動。 讓我解釋:

  • 如果我向上移動45度,我可以與牆的左下角碰撞。
  • 如果我與左邊碰撞,我想停止x移動並向上移動。 如果我離開牆壁,那么我想繼續前進。 與下側相同(停止y運動)
  • 如果我與角落碰撞,我想停止運動(滑動不可能)。

我實際上在做的是檢查矩形的左邊是否與我的圓相交。 然后我檢查左邊的牆和我的圓圈以及牆的底線和我的圓圈之間的交叉點。 根據哪個交點發生,我設置了我的圓的x / y,並將x / y速度設置為0.問題是,大多數情況下不是碰撞而是發生 重疊 所以底部檢查返回true,即使實際上圓圈只會與右邊碰撞。 在這種情況下,兩個交叉點測試都將返回true,我將重置兩個速度,就像在Corner碰撞中一樣。 我怎么解決這個問題? 是否有更好的方法來檢測碰撞和碰撞的側面或角落? 我不需要在矩形的一側確切的碰撞點。

編輯:我不得不說,rects不是平行於x軸旋轉。

您可以在下面找到圓/矩形碰撞的解釋,但請注意,這種類型的碰撞可能不是您的需要所必需的。 例如,如果您的角色有一個矩形邊界框,則算法會更簡單,更快捷。 即使您使用的是圓形,也可能有一種更簡單的方法可以滿足您的需要。

我雖然為此編寫代碼,但這需要太長時間,所以這里只是一個解釋:

以下是角色圈的示例移動,包括其最后(前一個)和當前位置。 牆上方顯示牆面矩形。


在此輸入圖像描述


這是相同的運動,虛線表示圓圈在此移動中掃過的區域。 掃掠區域是膠囊形狀。


在此輸入圖像描述

計算這兩個對象的碰撞是很困難的,所以我們需要以不同的方式做到這一點。 如果你看一下上一張圖像上的膠囊,你會看到它只是在每個方向上由圓的半徑延伸的運動線。 我們可以將“擴展”從移動線移動到牆矩形。 這樣我們得到一個圓角矩形,如下圖所示。


在此輸入圖像描述

當且僅當膠囊與牆矩形碰撞時,移動線將與此擴展(圓角)矩形碰撞,因此它們在某種程度上是等效且可互換的。

由於此碰撞計算仍然非常重要且相對昂貴,因此您可以先在擴展牆矩形(此時為非舍入)和運動線的邊界矩形之間進行快速碰撞檢查。 您可以在下面的圖像上看到這些矩形 - 它們都是點綴的。 這是一個快速簡便的計算,當您玩游戲時,可能不會與特定的牆矩形重疊> 99%的時間,碰撞計算將在此處停止。


在此輸入圖像描述

然而,如果存在重疊,則可能存在字符圓與牆矩形的碰撞,但是不確定,如稍后將說明的。

現在,您需要計算移動線本身(不是其邊界框)與擴展牆矩形之間的交點。 您可以找到一個算法如何在線執行此操作,搜索線/矩形交點或線/ aabb交點(aabb = Axis Aligned Bounding Box)。 矩形是軸對齊的,這使計算更簡單。 該算法可以為您提供一個或多個交叉點,因為有可能有兩個 - 在這種情況下,您可以選擇最接近該線的起點。 以下是此交叉/碰撞的示例。


在此輸入圖像描述

當你得到一個交叉點時,應該很容易計算這個交叉點所在的擴展矩形的哪個部分。 您可以在上面的圖像上看到這些部分,用紅線分隔並標有一個或兩個字母(l - 左,r - 右,b - 底部,t - 頂部,tl - 頂部和左側等)。
如果交叉點位於部分l,r,b或t(中間的單字母),那么您就完成了。 字符圈和牆矩形之間肯定存在碰撞,你知道在哪一邊。 在上面的例子中,它位於底部。 你應該使用4個變量,如isLeftCollisionisRightCollisionisBottomCollsionisTopCollision 在這種情況下,您將isBottomCollision設置為true,而其他3將保持為false。

但是,如果交叉點位於角上,在兩個字母的部分上,則需要進行其他計算以確定字符圓和牆矩形之間是否存在實際碰撞。 下圖顯示了角落上的3個這樣的交叉點,但只有2個實際的圓形 - 矩形碰撞。


在此輸入圖像描述

要確定是否存在碰撞,您需要在移動線和以原始非擴展牆矩形的最近角為中心的圓之間找到交點。 該圓的半徑等於字符圓的半徑。 再一次,你可以google for line / circle intersection算法(甚至libgdx也有),它並不復雜,不應該很難找到。
bl部分沒有直線/圓形交叉(並且沒有圓/矩形碰撞),並且br和tr部分存在交叉/碰撞。
在這種情況下,你將isRightCollisionisBottomCollsion都設置為true,在tr情況下你將isRightCollisionisTopCollision設置為true。

您還需要注意一個邊緣情況,您可以在下面的圖像中看到它。


在此輸入圖像描述

如果前一步的移動在擴展矩形的角落中結束,但在內部矩形角的半徑之外(沒有碰撞),則會發生這種情況。
要確定是否是這種情況,只需檢查運動起始點是否在擴展矩形內。
如果是,在初始矩形重疊測試之后(在擴展牆矩形和移動線的邊界矩形之間),你應該跳過線/矩形交叉測試(因為在這種情況下可能沒有任何交叉並且仍然是圓/矩形碰撞),並且還簡單地根據移動說明點確定您所在的角落,然后僅檢查與該角落圓圈的線/圓交叉點。 如果有交叉點,則會出現字符圓/牆矩形碰撞,否則不會。

畢竟,碰撞代碼應該很簡單:

// x, y - character coordinates
// r - character circle radius
// speedX, speedY - character speed
// intersectionX, intersectionY - intersection coordinates
// left, right, bottom, top - wall rect positions

// I strongly recomment using a const "EPSILON" value
// set it to something like 1e-5 or 1e-4
// floats can be tricky and you could find yourself on the inside of the wall
// or something similar if you don't use it :)

if (isLeftCollision) {
    x = intersectionX - EPSILON;
    if (speedX > 0) {
        speedX = 0;
    }
} else if (isRightCollision) {
    x = intersectionX + EPSILON;
    if (speedX < 0) {
        speedX = 0;
    }
}

if (isBottomCollision) {
    y = intersectionY - EPSILON;
    if (speedY > 0) {
        speedY = 0;
    }
} else if (isTopCollision) {
    y = intersectionY + EPSILON;
    if (speedY < 0) {
        speedY = 0;
    }
}

[更新]

這是一個簡單的,我認為有效的段 - aabb交叉實現,應該足夠您的目的。 這是一個略微修改的Cohen-Sutherland算法 您也可以查看此答案的第二部分。

public final class SegmentAabbIntersector {

    private static final int INSIDE = 0x0000;
    private static final int LEFT = 0x0001;
    private static final int RIGHT = 0x0010;
    private static final int BOTTOM = 0x0100;
    private static final int TOP = 0x1000;

    // Cohen–Sutherland clipping algorithm (adjusted for our needs)
    public static boolean cohenSutherlandIntersection(float x1, float y1, float x2, float y2, Rectangle r, Vector2 intersection) {

        int regionCode1 = calculateRegionCode(x1, y1, r);
        int regionCode2 = calculateRegionCode(x2, y2, r);

        float xMin = r.x;
        float xMax = r.x + r.width;
        float yMin = r.y;
        float yMax = r.y + r.height;

        while (true) {
            if (regionCode1 == INSIDE) {
                intersection.x = x1;
                intersection.y = y1;
                return true;
            } else if ((regionCode1 & regionCode2) != 0) {
                return false;
            } else {
                float x = 0.0f;
                float y = 0.0f;

                if ((regionCode1 & TOP) != 0) {
                    x = x1 + (x2 - x1) / (y2 - y1) * (yMax - y1);
                    y = yMax;
                } else if ((regionCode1 & BOTTOM) != 0) {
                    x = x1 + (x2 - x1) / (y2 - y1) * (yMin - y1);
                    y = yMin;
                } else if ((regionCode1 & RIGHT) != 0) {
                    y = y1 + (y2 - y1) / (x2 - x1) * (xMax - x1);
                    x = xMax;
                } else if ((regionCode1 & LEFT) != 0) {
                    y = y1 + (y2 - y1) / (x2 - x1) * (xMin - x1);
                    x = xMin;
                }

                x1 = x;
                y1 = y;
                regionCode1 = calculateRegionCode(x1, y1, r);
            }
        }
    }

    private static int calculateRegionCode(double x, double y, Rectangle r) {
        int code = INSIDE;

        if (x < r.x) {
            code |= LEFT;
        } else if (x > r.x + r.width) {
            code |= RIGHT;
        }

        if (y < r.y) {
            code |= BOTTOM;
        } else if (y > r.y + r.height) {
            code |= TOP;
        }

        return code;
    }
}

以下是一些代碼示例用法:

public final class Program {

    public static void main(String[] args) {

        float radius = 5.0f;

        float x1 = -10.0f;
        float y1 = -10.0f;
        float x2 = 31.0f;
        float y2 = 13.0f;

        Rectangle r = new Rectangle(3.0f, 3.0f, 20.0f, 10.0f);
        Rectangle expandedR = new Rectangle(r.x - radius, r.y - radius, r.width + 2.0f * radius, r.height + 2.0f * radius);

        Vector2 intersection = new Vector2();

        boolean isIntersection = SegmentAabbIntersector.cohenSutherlandIntersection(x1, y1, x2, y2, expandedR, intersection);
        if (isIntersection) {
            boolean isLeft = intersection.x < r.x;
            boolean isRight = intersection.x > r.x + r.width;
            boolean isBottom = intersection.y < r.y;
            boolean isTop = intersection.y > r.y + r.height;

            String message = String.format("Intersection point: %s; isLeft: %b; isRight: %b; isBottom: %b, isTop: %b",
                    intersection, isLeft, isRight, isBottom, isTop);
            System.out.println(message);
        }

        long startTime = System.nanoTime();
        int numCalls = 10000000;
        for (int i = 0; i < numCalls; i++) {
            SegmentAabbIntersector.cohenSutherlandIntersection(x1, y1, x2, y2, expandedR, intersection);
        }
        long endTime = System.nanoTime();
        double durationMs = (endTime - startTime) / 1e6;

        System.out.println(String.format("Duration of %d calls: %f ms", numCalls, durationMs));
    }
}

這是我從執行此操作得到的結果:

Intersection point: [4.26087:-2.0]; isLeft: false; isRight: false; isBottom: true, isTop: false
Duration of 10000000 calls: 279,932343 ms

請注意,這是i5-2400 CPU上的桌面性能。 在Android設備上它可能會慢得多,但我相信還是綽綽有余。
我只是在表面上測試過,所以如果你發現任何錯誤,請告訴我。

如果您使用此算法,我相信您不需要特殊處理,因為起始點位於擴展牆矩形的角落,因為在這種情況下,您將獲得行開始處的交叉點,以及碰撞檢測程序將繼續下一步(線圈碰撞)。

我想你通過計算圓心與線的距離來確定碰撞。 如果距離相等且小於半徑,我們可以簡化情況並告知圓與角相撞。 平等當然應該有寬容。

更多 - 可能不是必要的 - 現實的方法是考慮x,y速度並將其考慮在等式檢查中。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM