มีวิธีเพิ่มประสิทธิภาพการตรวจสอบการชนของระบบของวัตถุ n หรือไม่?


9

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

ฉันทำอะไรแบบนี้

for (o in objects)
{
   o.stuff();
   for (other in objects)
      if (collision(o, other))
          doStuff();

   bla.draw();
}

นี่มี O (n ^ 2) ซึ่งฉันบอกว่าไม่ดี ฉันจะทำสิ่งนี้ได้อย่างมีประสิทธิภาพมากขึ้นเป็นไปได้ไหม ฉันกำลังทำสิ่งนี้ใน Javascript และโดยปกติแล้ว n จะต่ำกว่า 30 จะเป็นปัญหาหรือไม่หากสิ่งนี้ยังคงเหมือนเดิม


3
คุณลองใช้รหัสเพื่อดูว่ามันทำงานอย่างไร
thedaian

ไม่ฉันไม่ได้ฉันแค่คิดว่ามันไม่ดีเพราะ O (n ^ 2)
jcora

1
เพียง 30 วัตถุ? ฉันอยากจะแนะนำการแบ่งเชิงพื้นที่ แต่มันจะไร้ผลกับวัตถุเพียง 30 ชิ้น มีการปรับแต่งเล็ก ๆ น้อย ๆ บางอย่างที่คนอื่น ๆ ชี้ให้เห็น แต่ทั้งหมดเป็นการปรับแต่งเล็กน้อยในสเกลที่คุณกำลังพูดถึง
John McDonald

คำตอบ:


16

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

สิ่งที่คุณจะต้องมีคือการใช้การแบ่งพาร์ติชันเชิงพื้นที่เช่นOctree (สำหรับเกม 3D) หรือQuadtree (สำหรับเกม 2D) แบ่งพาร์ติชันเหล่านี้โลกออกเป็นส่วนย่อยจากนั้นแต่ละส่วนย่อยจะแบ่งพาร์ติชันเพิ่มเติมในคฤหาสน์เดียวกันจนกว่าพวกเขาจะแบ่งย่อยให้มีขนาดต่ำสุด สิ่งนี้ช่วยให้คุณตรวจสอบวัตถุอื่น ๆ ที่อยู่ในภูมิภาคเดียวกันของโลกได้อย่างรวดเร็วเป็นอย่างมากซึ่งจะ จำกัด จำนวนการชนที่คุณต้องตรวจสอบ

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

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

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

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

ในที่สุดคุณก็เหลืออะไรแบบนี้ (ในรหัสหลอก):

// Go through each leaf node in the octree. This could be more efficient
// by keeping a list of leaf nodes with objects in it.
for ( node in octreeLeafNodes )
{
    // We only need to check for collision if more than one object
    // or island is in the bounds of this octree node.
    if ( node.numAABBsInBounds > 1)
    {
        for ( int i = 0; i < AABBNodes.size(); ++i )
        {
           // Using i+1 here allows us to skip duplicate checks between AABBS
           // e.g (If there are 5 bodies, and i = 0, we only check i against
           //      indexes 1,2,3,4. Once i = 1, we only check i against indexes
           //      2,3,4)
           for ( int j = i + 1; j < AABBNodes.size(); ++j )
           {
               if ( AABBOverlaps( AABBNodes[i], AABBNodes[j] ) )
               {
                   // If the AABB we checked against was a simulation island
                   // then we now check against the nodes in the simulation island

                   // Once you find overlaps between two actual object AABBs
                   // you can now check sub-nodes with each object, if you went
                   // that far in optimizing physics meshes.
               {
           }
        }
    }
}

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

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


4
คำตอบที่สอดคล้องกันอย่างมากกับความต้องการด้านเทคนิคที่ดีและมีประโยชน์เพื่อเปิดใจผู้อ่านถึงวิธีการที่มีอยู่ +1
Valkea

ถ้าฉันต้องการลบวัตถุที่กำลังชนกันล่ะ ฉันสามารถเปลี่ยนตู้คอนเทนเนอร์ได้หรือไม่? ฉันหมายถึงลบมันออกจากภาชนะเพราะฉันไม่ต้องการวัตถุอีกต่อไปเพราะมันเป็น "ทำลาย" ฉันต้องการการวนซ้ำอีกหนึ่งครั้งเพื่อเรียกใช้เหตุการณ์การชนกันของข้อมูลหากฉันไม่ลบออกระหว่างการตรวจจับการชนกันของข้อมูล
newguy

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

4

ตัวอย่างของคุณทดสอบวัตถุแต่ละคู่หลาย ๆ ครั้ง

ลองมาตัวอย่างที่ง่ายมากกับอาร์เรย์ที่มี 0,1,2,3

ด้วยรหัสของคุณคุณจะได้รับสิ่งนี้:

  • ที่ loop 0 คุณทดสอบกับ 1, 2 และ 3
  • ที่วน 1 คุณทดสอบกับ 0, 2 และ 3 ===> (0-1 ทดสอบแล้ว)
  • ที่วน 2 คุณทดสอบกับ 0, 1 และ 3 ===> (0-2 / 1-2 ทดสอบแล้ว)
  • ที่วง 3 คุณทดสอบกับ 0, 1 และ 2 ===> (0-3 / 1-3 / 2-3 ทดสอบแล้ว)

ตอนนี้ให้ดูรหัสต่อไปนี้:

for(i=0;i<=objects.length;i++)
{
    objects[i].stuff();

    for(j=i+1;j<=objects.length;j++)
    {
        if (collision(objects[i], objects[j]))
        doStuff();
    }

    bla.draw();
}

หากเราใช้อาร์เรย์ที่มี 0,1,2,3 อีกครั้งเราจะมีพฤติกรรมดังต่อไปนี้:

  • ที่ loop 0 คุณทดสอบกับ 1, 2, 3
  • ที่วง 1 คุณทดสอบกับ 2, 3
  • ที่วง 2 คุณทดสอบกับ 3
  • เมื่อวนที่ 3 คุณทดสอบกับสิ่งใด

ด้วยอัลกอริธึมที่สองเรามีการทดสอบการชนกัน 6 ครั้งในขณะที่การทดสอบก่อนหน้านี้ขอการทดสอบการชน 12 ครั้ง


อัลกอริทึมนี้ทำการN(N-1)/2เปรียบเทียบซึ่งยังคงมีประสิทธิภาพ O (N ^ 2)
ไก่

1
ด้วยวัตถุ 30 ชิ้นตามที่ร้องขอนั่นหมายถึงการทดสอบการชน 465 ครั้งเทียบกับ 870 ... มันอาจคล้ายกันจากมุมมองของคุณ แต่ไม่ใช่จากของฉัน นอกจากนี้วิธีแก้ปัญหาที่นำเสนอในคำตอบอื่น ๆ คืออัลกอริธึมที่เหมือนกันทุก
ประการ

1
@Valkea: ส่วนหนึ่งของมันคือ :)
Nic Foster

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

นี่เรียกว่าค่าตัดจำหน่ายหรือไม่ อย่างไรก็ตามขอบคุณ!
jcora

3

ออกแบบอัลกอริทึมของคุณให้ตรงกับความต้องการของคุณ แต่ให้รายละเอียดของการดำเนินการถูกห่อหุ้มไว้ แม้แต่ใน Javascript ก็มีการนำแนวคิด OOP พื้นฐานมาใช้

เพราะN =~ 30, O(N*N)ไม่ใช่สิ่งที่น่ากังวลและการค้นหาเชิงเส้นของคุณน่าจะเร็วเท่ากับการค้นหาทางเลือกอื่น ๆ แต่คุณไม่ต้องการตั้งสมมติฐานรหัสยากในรหัสของคุณ ใน pseudocode คุณจะมีส่วนต่อประสาน

interface itemContainer { 
    add(BoundingBox);
    remove(BoundingBox);
    BoundingBox[] getIntersections();
}

ซึ่งอธิบายสิ่งที่รายการของคุณสามารถทำได้ จากนั้นคุณสามารถเขียนคลาส ArrayContainer ที่ใช้อินเทอร์เฟซนี้ ใน Javascript โค้ดจะมีลักษณะดังนี้:

function ArrayContainer() { ... } // this uses an array to store my objects
ArrayContainer.prototype.add = function(box) { ... };
ArrayContainer.prototype.remove = function(box) { ... };
ArrayContainer.prototype.getIntersections = function() { ... };

function QuadTreeContainer { ... } // this uses a quadtree to store my objects
... and implement in the add/remove/getIntersections for QuadTreeContainer too

และนี่คือตัวอย่างรหัสที่สร้างกล่องที่มีขอบเขต 300 กล่องและรับจุดตัดทั้งหมด หากคุณติด ArrayContainer และ QuadTreeContainer อย่างถูกต้องสิ่งเดียวที่คุณจะต้องมีการเปลี่ยนแปลงในรหัสของคุณคือการเปลี่ยนแปลงไปvar allMyObjects = new ArrayContainer()var allMyObjects = QuadTreeContainer()

var r = Math.random;
var allMyObjects = new ArrayContainer();
for(var i=0; i<300; i++)
    allMyObjects.add(new BoundingBox(r(), r()));
var intersections = allMyObjects.getIntersections();

ฉันไปข้างหน้าและวิปปิ้งการใช้งานสำหรับ ArrayContainer มาตรฐานที่นี่:

http://jsfiddle.net/SKkN5/1/


หมายเหตุ: คำตอบนี้ได้รับแรงบันดาลใจจากคำร้องเรียนของ Bane ว่า codebase ของเขาใหญ่เกินไปยุ่งและจัดการยาก แม้ว่าจะไม่ได้เพิ่มการสนทนาเกี่ยวกับการใช้ Array vs a Tree มากนัก แต่ฉันหวังว่ามันจะเป็นคำตอบที่เกี่ยวข้องว่าจะสามารถจัดการรหัสของเขาได้ดีขึ้นโดยเฉพาะอย่างไร
จิมมี่

2

คุณควรพิจารณาประเภทของวัตถุที่สามารถชนกันได้อย่างสมเหตุสมผล

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

หากต้องการใช้สิ่งนี้อย่างมีประสิทธิภาพคุณอาจต้องการเก็บรายการวัตถุแยกกันหนึ่งรายการต่อประเภทวัตถุ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.