Swept AABB Collision Detection Conflict with Tile-sized Gaps

Sep 09 2020

พื้นหลัง

วิธีการกวาดเพื่อตรวจจับและแก้ไขการชนได้พิสูจน์แล้วว่าใช้งานได้หลากหลายมากในการใช้การชนกันในเกมของเราและฉันมีบทความ GameDev.net นี้เพื่อขอบคุณสำหรับสิ่งนั้น: https://www.gamedev.net/tutorials/programming/general-and-gameplay-programming/swept-aabb-collision-detection-and-response-r3084/

บทความนี้มีประโยชน์มากและฉันได้เรียนรู้มากมายจากมัน มันมีข้อบกพร่องบางอย่างในโค้ดตัวอย่าง (โชคดีที่เราไม่มีพวกเขาเอง) แต่แนวคิดเป็นส่วนสำคัญ ฉันขอแนะนำให้คุณอ่านเนื่องจากเป็นข้อมูลอ้างอิงที่สำคัญมากสำหรับโพสต์นี้

ความขัดแย้ง

ปัญหาที่ฉันนำเสนอคือพฤติกรรมที่ไม่ต้องการ แต่เป็นที่คาดหวัง โดยทั่วไปผู้เล่นจะไม่สามารถตกลงไปในช่องว่างขนาดกระเบื้องได้หากผู้เล่นมีขนาดกระเบื้องด้วย (เว้นแต่จะมีบางอย่างที่ปลายหลุมเพื่อปิดกั้นผู้เล่น)

ในขณะที่รหัสทำงานตามบทความ GameDev (ฉันมีการสาธิตเพิ่มเติมในโพสต์นี้) มีปัญหาเล็กน้อยเกี่ยวกับวิธีการ "เลื่อน"

การเลื่อนเป็นวิธีการแก้ปัญหาการชนเพื่อให้เอนทิตีไม่ยึดติดกับพื้นผิวของร่างกายฟิสิกส์อื่น ๆ โดยทั่วไปจะช่วยให้ผู้เล่นสามารถเดินบนพื้นและเลื่อนไปตามกำแพงเมื่อกระโดดขึ้นไปโดยไม่ติดอยู่บนพื้นผิว ภาพนี้จากบทความอธิบายได้ดีกว่าที่ฉันทำ:

นี่เป็นทางออกที่ดี แต่ จำกัด ผู้เล่นที่มีขนาดกระเบื้องไม่ให้ตกลงไปในรูขนาดกระเบื้องและให้ผู้เล่นเดินข้ามมันไปแทน

อย่างที่คุณเห็นคุณคาดว่าผู้เล่นจะตกลงไปในหลุมเหล่านั้น แต่ดูเถิดเขาเดินผ่านพวกเขาไป ฉันได้ทดสอบด้วยความเร็วในการเดินที่หลากหลายแม้จะต่ำกว่าหนึ่งพิกเซลต่อเฟรมและผู้เล่นก็ยังเดินผ่านพวกเขา

นอกจากนี้ฉันต้องการชี้แจงว่านี่ไม่ใช่ปัญหากับแกนเดียวเนื่องจากผู้เล่นไม่สามารถกระโดดลงไปในรูขนาดเท่ากระเบื้องได้

โปรดทราบว่าผู้เล่นยังคงสามารถตกลงไปในหลุมเหล่านั้นได้เพียงแค่ต้องมีการปิดกั้นเพื่อให้สามารถทำได้

... และเช่นเดียวกันกับการเดินเป็นหนึ่งเดียว

ต้นตอของปัญหา

ฉันขอแนะนำให้คุณอ่านโค้ดบางส่วนในการสาธิตที่ฉันให้ไว้เนื่องจากการตัดโค้ดต่อไปนี้เกี่ยวข้องกับส่วนนี้:

resolveCollisionOrMove(deltaTime, movement = this.vel, remainingTime = 1, collisionStack = this.collisionStack) {
    // Sort collisions in the correct order: First by distance, then by
    // time of impact to correct the order a bit.
    collisionStack.sort((a, b) => a.distance - b.distance);
    collisionStack.sort((a, b) => a.collisionTime - b.collisionTime);
    let hit = collisionStack.shift();

    // If a collision is detected at all, resolve for the closest AABB. Otherwise add velocity to position.
    if (hit) {
        this.center.x += movement.x * hit.collisionTime * deltaTime;
        this.center.y += movement.y * hit.collisionTime * deltaTime;

        // Calculate the correct time of impact for the remaining
        // collisions or to apply movement
        let time = remainingTime - hit.collisionTime;

        // Calculate the collision normal (vector used to slide the object that collided)
        let dotProduct = p5.Vector.dot(movement, hit.normal) * time;
        hit.normal.mult(dotProduct);

        // Handle events after each respective side that collided
        this.onCollision(hit.side);

        if (time > 0) {
            // If the time of impact happened to be above zero, then recalculate
            // collision for all other potential collisions with respect to the normal vector
            let stack = [];
            for (let sweep of collisionStack) {
                // Pass the normal vector as the movement vector for the next possible collision
                if (this.createMovementBoundary(deltaTime, hit.normal).overlaps(sweep.aabb)) {
                    let newSweep = this.calculateSweepData(sweep.aabb, hit.normal, deltaTime);
                    stack.push(newSweep);
                }
            }

            // Keep resolving collisions for the other potential collisions
            this.resolveCollisionOrMove(deltaTime, hit.normal, time, stack);
        }
    } else {
        this.center.add(p5.Vector.mult(movement, deltaTime));
    }
}

ฉันคิดว่านี่เป็นปัญหาที่เกิดจากการชนกันตามปกติที่คำนวณสำหรับเอฟเฟกต์การเลื่อน สมมติว่าผู้เล่นเดินไปยังรูขนาดเท่ากระเบื้องจากด้านซ้าย (ตอนนี้รูอยู่ทางขวาของผู้เล่น) อัลกอริธึมการแก้ปัญหาการชนของฉันคำนึงถึงการชนอื่น ๆ ที่อาจเกิดขึ้นตามเวกเตอร์การเลื่อนที่ใช้กับผู้เล่นเมื่อพบการชนที่ใกล้เคียงที่สุด ซึ่งหมายความว่ากระเบื้องก่อนหลุมคือสิ่งที่ชนกับผู้เล่นก่อนซึ่งจะใช้เวกเตอร์การเคลื่อนไหว (สไลด์) ที่มีองค์ประกอบ y เป็นศูนย์ สิ่งนี้ทำให้อัลกอริทึมการชนกันไม่สนใจกระเบื้องทางด้านซ้ายของหลุมเนื่องจากอาจเกิดการชนกันเนื่องจากตามตรรกะแล้วผู้เล่นจะไม่ชนกับด้านซ้าย

เส้นสีเขียวเป็นเวกเตอร์ปกติที่ใช้สำหรับการเลื่อน (ซึ่งใช้ในการคำนวณการชนครั้งต่อไป) ในขณะที่เส้นสีแดงคือความเร็วจริงที่ใช้กับผู้เล่นก่อนที่จะแก้ไขการชน ...

ปัญหาคือฉันไม่รู้วิธีแก้ปัญหานี้โดยไม่ทำลายรหัส ฉันรู้ว่าปัญหานี้สามารถแก้ไขได้เพียงแค่ลดขนาดเครื่องเล่น นั่นไม่ใช่สิ่งที่ฉันยินดีที่จะยอมรับว่าเป็นวิธีแก้ปัญหาเนื่องจากมัน จำกัด กลไกการเล่นเกมจำนวนมากซึ่งฉันจะไม่เข้าที่นี่

รหัส

นี่คือลิงค์ไปยัง JSFiddle ที่ฉันสร้างขึ้นเมื่อเวทมนตร์ทั้งหมดเกิดขึ้น: https://jsfiddle.net/uwg0m9zp/

ฉันได้เขียนมันใน JavaScript ในไฟล์ HTML ไฟล์เดียวที่ฉันใช้ไลบรารี p5.js เพื่อลดความซับซ้อนของการป้อนข้อมูลกราฟิกและการคำนวณเวกเตอร์ การเรียกใช้ฟังก์ชัน p5.js ส่วนใหญ่ไม่สำคัญสำหรับปัญหานี้ยกเว้นคณิตศาสตร์เวกเตอร์ แต่ถ้าโครงสร้างของโค้ดทำให้สับสนคุณเพียงแค่ค้นหาเอกสาร (หรือถามในความคิดเห็น):https://p5js.org/

คำแนะนำการสาธิต

การสาธิตนี้มีรูปสี่เหลี่ยมผืนผ้าสีน้ำเงินซึ่งเป็นผู้เล่นของเรา เขาสามารถเดินบินและชนกับกระเบื้องสีม่วงในแผนที่

การควบคุม ได้แก่ :

  • A: เดินไปทางซ้าย
  • D: เดินไปทางขวา
  • อวกาศ: บิน

ใช้ตัวควบคุมเหล่านี้เพื่อเดิน / บินเข้าไปในช่องว่างของกระเบื้อง

คำสุดท้าย

ฉันหวังว่าฉันจะอธิบายปัญหาของฉันได้ดี โปรดอย่าลังเลที่จะถามคำถามหากฉันรู้สึกสับสนเล็กน้อย โปรดทราบว่าฉันได้ทดสอบด้วยเพื่อดูว่านี่เป็นปัญหาความไม่แม่นยำของการลอย (โดยใช้ epsilon เป็นต้น) และไม่มีโชค

ขอขอบคุณที่อ่านสิ่งนี้และขอความช่วยเหลือล่วงหน้า!

คำตอบ

2 KevinReid Sep 11 2020 at 09:36

เนื่องจากคุณมีโลกที่เรียงต่อกันและตัวอักษรที่มีขนาดพอดีขั้นตอนแรกในการแยกแยะปัญหานี้จึงสามารถรับรู้ได้เมื่ออักขระอยู่ในแนวเดียวกับกระเบื้อง ในกรณีที่ตกลงไปในหลุมจะเกิดขึ้นเมื่อ

  • พิกัด Y เป็นจำนวนเต็ม (สมมติว่าไทล์ของคุณมีความยาวด้าน 1) ซึ่งควรเป็นจริงเมื่อใดก็ตามที่เกิดการเลื่อนบนสิ่งกีดขวางแนวกระเบื้อง
  • พิกัด X คำนวณโดยการเลื่อนปัดเศษลงเป็นจำนวนเต็มเปลี่ยนเป็นจำนวนเต็มต่างจากที่เป็นจุดเริ่มต้นของส่วนเลื่อนของการเคลื่อนที่

การกระโดดจะเหมือนกันกับพิกัดที่กลับด้าน

เมื่อคุณทราบว่าการคำนวณการชนของคุณเกิดขึ้นจากการจัดแนวกระเบื้องให้แบ่งการเคลื่อนไหวออกเป็นสองส่วน:

ตอนนี้คุณสามารถแสร้งทำเป็นว่าคุณกำลังทำขั้นตอนทางฟิสิกส์ที่เริ่มต้นที่ตำแหน่งกล่องประในแผนภาพนี้จัดการสถานการณ์ตามที่คุณต้องการและหากคำตอบไม่ใช่ "ตกลงไปในหลุม" (เช่นในกรณีที่ ไม่มีรู!) ให้ใช้ส่วนที่สองของการเคลื่อนไหวต่อไป (กล่องกรอบทึบในแผนภาพ) เพื่อรักษาความเร็ว


ส่วนที่สองของปัญหาน่าสนใจ: เราจะตัดสินใจอย่างไรว่าตัวละครควรตกลงไปในหลุม? ถ้าเรามองในขณะนี้ก็สอดคล้องกับกระเบื้องมันจะไปทางขวาหรืออาจลงไปและเวกเตอร์ความเร็วสอดคล้องส่วนใหญ่ไปทางขวามีเล็กมีส่วนร่วมของแรงโน้มถ่วงชี้ลง (น้อยเพราะมันได้รับในเหวสำหรับศูนย์เวลา ; หรือในการจำลองความเร็วหนึ่งเฟรม) ในทำนองเดียวกันในกรณีการกระโดดความเร็วจะขึ้นและมีแรงไปด้านข้าง แต่ไม่มีความเร็วด้านข้าง ความละเอียดตามการจัดแนวของเวกเตอร์ความเร็วจะให้คำตอบที่คุณไม่ต้องการอย่างสม่ำเสมอ!

ดังนั้นสำหรับการเล่นเกมที่คุณต้องการคุณควรตรวจจับกรณีที่ตรงตามเงื่อนไขเหล่านี้ทั้งหมด:

  • ตัวอักษรสอดคล้องกับกระเบื้อง (ตามที่กล่าวไว้ข้างต้น)
  • ความเร็วกำลังชี้ไปในทิศทางที่แน่นอนและแรงโน้มถ่วงหรือแรงป้อนข้อมูลของผู้เล่นจะชี้ในแนวตั้งฉาก
  • และมีพื้นที่เปิดโล่งในทิศทางตั้งฉากนั้น

และเลือกใช้เส้นทางตั้งฉากเสมอ! คุณสามารถใช้สิ่งนี้ได้โดยดำเนินการต่อราวกับว่ามีไทล์ปิดกั้นที่หยุดตัวละครให้เคลื่อนที่ไปในทิศทางปกติ (อาจจะยุ่งยากในกรณีกระโดด) หรือคุณสามารถทำให้การใช้งานง่ายขึ้นและยังเพิ่มความมีไหวพริบโดยให้ตัวละครเตะพิเศษเข้าไปใน รู (หนึ่งเฟรมของการเลื่อนตำแหน่ง + ความเร็วบางส่วนในทิศทางนั้น) อาจจะเป็นภาพเคลื่อนไหวพิเศษ

1 AlanRostem Sep 12 2020 at 06:09

หลังจากใช้เวลานับไม่ถ้วนในการค้นหาปัญหาผ่านการลองผิดลองถูกและถามรอบ ๆ บนเซิร์ฟเวอร์ Discord และที่นี่เพื่อนของฉันและฉันพบวิธีแก้ปัญหาที่หลากหลายมากสำหรับปัญหาที่น่ารำคาญโดยเฉพาะ

เครดิตเกิดจาก Kevin Reid ที่สละเวลาอันมีค่าของเขามาตอบกระทู้นี้ เขาเขียนคำตอบที่เป็นลายลักษณ์อักษรซึ่งอธิบายแนวคิดที่สำคัญในโซลูชันนี้ ฉันอยากจะขอบคุณเพื่อนที่ฉันคุยด้วยบนเซิร์ฟเวอร์ SFML Discord ที่เป็นประโยชน์มากในการแสดงให้เราเห็นว่าเราควรแก้ไขปัญหานี้อย่างไร

เราเรียนรู้อะไร

มีสิ่งง่ายๆอย่างหนึ่งที่ฉันกลัวซึ่งตลกพอแก้ไขได้ทั้งหมดนั่นคือการเพิ่มกรณีพิเศษ

คดีพิเศษคดีพิเศษคดีพิเศษ ! ฉันกลัวมากที่จะทำลายการไหลของอัลกอริทึมที่ฉันไม่เคยคิดมาตั้งแต่แรก (การกวาด) แต่สิ่งที่จำเป็นก็คือการเพิ่มสิ่งพิเศษเพื่อตรวจสอบสถานการณ์รูขนาดกระเบื้องเหล่านี้ในขณะที่ยังคงติดต่อกับ อัลกอริทึม "คิด"

โซลูชันนี้ไม่เพียง แต่คำนึงถึงรูที่มีขนาดเท่ากันกับกระเบื้องเท่านั้น แต่ยังมีขนาดของเอนทิตีใด ๆ ที่กำลังจะเดินเข้าไปในหลุมเดียว

การตรวจจับกรณีดังกล่าว

ฉันจะอธิบายการตรวจจับด้วยตัวอย่างง่ายๆที่สามารถนำไปใช้กับการเคลื่อนไหวในแกนใดก็ได้ สมมติว่าผู้เล่นเดินจากซ้ายไปขวาและกำลังเข้าใกล้หลุมที่มีขนาดแนวนอนเท่ากับเขาในเฟรมนี้ อัลกอริทึมส่วน Broadphase จะตรวจจับการชนที่อาจเกิดขึ้นกับสองชิ้นต่อไปนี้เสมอ:

  • ผู้เล่นที่กำลังเดินอยู่
  • อันหนึ่ง (อาจ) ถัดจากหลุม

นี่เป็นข้อมูลที่มีประโยชน์เนื่องจากส่วนความละเอียดการชนกันของอัลกอริทึมจะแก้ไขการชนกันตามลำดับไทล์ที่ใกล้เคียงที่สุดกับผู้เล่น

fixEqualSizedHoleCollision(hit, potentialMovement, time, collisionStack) {
     if (collisionStack.length > 0) {
         // The tile/entity the player currently stands on
         let standingOn = hit.aabb;

         // The tile/entity assumed to be next to the player-sized hole
         let nextToHole = collisionStack[0].aabb;
                ...

อย่างไรก็ตามฉันเรียกฟังก์ชั่นนี้ในส่วนความละเอียดการชนกันในการสาธิตใหม่ของฉัน (เพิ่มเติมคำตอบนี้)

เนื่องจากฉันแสดงการชนครั้งแรกที่อาจเกิดขึ้นฉันจึงส่งมันเป็นพารามิเตอร์สำหรับฟังก์ชันนี้เพื่อให้การชนกันที่อาจเกิดขึ้นต่อไปนี้เป็นครั้งแรกในสแต็ก

ตอนนี้สิ่งที่เราต้องทำคือตรวจสอบว่ากระเบื้องเหล่านี้อยู่ภายใต้เครื่องเล่นหรือไม่ตั้งแต่แรก! สามารถทำได้โดยการตรวจสอบว่าไพ่ทั้งสองนี้มีขนาดครึ่งหนึ่งของขนาดผู้เล่นในแนวตั้งบวกกับขนาดกระเบื้องครึ่งหนึ่งจากผู้เล่นบนแกน y แผนภาพนี้แสดงสิ่งนี้ได้ดีที่สุด

เส้นสีเขียวคือสิ่งที่แสดงถึงระยะทางที่ฉันพูดถึง เส้นสีเขียวทั้งหมดเหล่านั้นจะต้องมีค่าเท่ากันเพื่อตัดสินว่าไทล์เหล่านี้อยู่ใต้เครื่องเล่นบนแกน y หรือไม่

คุณอาจสังเกตเห็นเส้นสีแดงด้วย! สิ่งนี้จะใช้เพื่อตรวจสอบว่ามีหลุมขนาดเท่าผู้เล่นหรือไม่ เพียงตรวจสอบให้แน่ใจว่าระยะห่าง x ระหว่างไพ่สองแผ่นนั้นตรงกับขนาดแนวนอนของผู้เล่น และบิงโก! ตอนนี้เรารู้แล้วว่ามีรูหรือเปล่า

ความละเอียด

การไขคดีมีความซับซ้อนน้อยกว่าการตรวจจับอย่างน่าตกใจ เพียงแค่หยุดเครื่องเล่นเพื่อให้พอดีกับหลุมหยุดการเคลื่อนที่ x ทั้งหมดแล้วเลื่อนลงด้านล่างด้วยขนาดเดียวกับแรงโน้มถ่วง การหยุดเครื่องเล่นสามารถจำลองได้ด้วย "อุปสรรค" ที่อยู่ด้านบนของกระเบื้องถัดจากรู

เส้นสีเขียวตอนนี้เป็นเวกเตอร์การเคลื่อนไหวใหม่ของผู้เล่นและกล่องสีขาวคือสิ่งกีดขวางจำลองซึ่งจะหยุดผู้เล่นในเส้นทางของมัน! นี่คือส่วนหนึ่งของรหัสที่แก้ไขได้:

barrierOffset.x = (this.extents.x - nextToHole.extents.x) * -movementDir;
if (areBothUnderMe && isHoleBetween) {
    // Create a simulated barrier that will block the player and make him fall
    // into the hole.
    let barrier = new AABB(nextToHole.center.copy(), this.extents.copy());
    barrier.center.sub(barrierOffset);
    this.center.x = barrier.center.x + (barrier.extents.x + this.extents.x) * (-movementDir);
    hit.normal.x = 0; // The player should now fall into the hole and no longer walk
    hit.normal.y = potentialMovement.y; // The sliding should now be applied to the hole wall
}

การสาธิตใหม่

นี่คือลิงค์ใหม่ไปยัง JSFiddle พร้อมรหัสใหม่: https://jsfiddle.net/2bnv510x/

มีบางส่วนเพิ่มเติมใหม่จากก่อนหน้านี้ ก่อนอื่นคุณสามารถส่งผ่านไปยังรูขนาดกระเบื้อง (ชัด ๆ ) และประการที่สองฉันได้เพิ่มเอนทิตีขนาดใหญ่สองรายการโดยแยกขนาดกระเบื้องออกจากกัน ฉันยังได้เพิ่มการควบคุมใหม่ที่ช่วยให้คุณเปลี่ยนความเร็วของคุณ:

  • Shift: เคลื่อนที่ช้ามาก
  • ถาม: เคลื่อนที่เร็วมาก

สนุก!

คำพูดสุดท้าย

ฉันขอขอบคุณทุกคนที่ช่วยเราแก้ปัญหานี้ เราได้เรียนรู้มากมายจากปัญหานี้และฉันหวังว่าทุกคนที่อ่านสิ่งนี้ก็ทำได้เช่นกัน

สิ่งที่ทำให้ฉันโพสต์ในตอนแรกคือการขาดข้อมูลเกี่ยวกับการตรวจจับการชนกันของ AABB และแทบไม่มีข้อมูลเกี่ยวกับปัญหานี้เลย ดังนั้นฉันต้องการให้โพสต์นี้ช่วยทุกคนที่ต่อสู้กับปัญหาเดียวกัน