Gibt es eine Möglichkeit, die Effizienz der Kollisionsprüfung eines Systems von n Objekten zu erhöhen?

9

Ich mache ein Spiel, das aus vielen Bildschirmobjekten besteht, von denen eines der Spieler ist. Ich muss wissen, welche Objekte bei jeder Iteration kollidieren.

Ich habe so etwas gemacht:

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

   bla.draw();
}

Dies hat O (n ^ 2), was mir gesagt wurde, ist schlecht. Wie mache ich das effizienter, ist das überhaupt möglich? Ich mache das in Javascript und n ist normalerweise niedriger als 30, wird es ein Problem sein, wenn dies gleich bleibt?

jcora
quelle
3
Haben Sie versucht, den Code auszuführen, um zu sehen, wie er funktioniert?
Thedaian
Nein, ich habe nicht angenommen, dass es wegen des O (n ^ 2) schlecht ist.
JCora
1
Nur 30 Objekte? Ich hätte eine räumliche Unterteilung empfohlen, aber mit nur 30 Objekten wäre sie erfolglos. Es gibt einige kleinere Optimierungen, auf die andere hingewiesen haben, aber es sind alles kleinere Optimierungen auf der Skala, über die Sie sprechen.
John McDonald

Antworten:

16

Mit maximal 30 Objekten sollten Sie nicht viel Optimierung benötigen, außer dass Sie dieselben zwei Paare nicht mehr als einmal pro Frame gegeneinander prüfen. Was das folgende Codebeispiel abdeckt. Wenn Sie jedoch an verschiedenen Optimierungen interessiert sind, die eine Physik-Engine verwenden würde, lesen Sie den Rest dieses Beitrags weiter.

Was Sie benötigen, ist eine Implementierung zur räumlichen Partitionierung , z. B. ein Octree (für 3D-Spiele) oder ein Quadtree (für 2D-Spiele). Diese unterteilen die Welt in Unterabschnitte, und dann wird jeder Unterabschnitt im selben Herrenhaus weiter unterteilt, bis sie auf eine Mindestgröße unterteilt sind. Auf diese Weise können Sie sehr schnell überprüfen, welche anderen Objekte sich in derselben Region der Welt befinden wie andere, wodurch die Anzahl der Kollisionen begrenzt wird, gegen die Sie prüfen müssen.

Zusätzlich zur räumlichen Partitionierung würde ich empfehlen, für jedes Ihrer Physikobjekte einen AABB ( Axis-Aligned Bounding Box ) zu erstellen . Auf diese Weise können Sie den AABB eines Objekts mit einem anderen vergleichen. Dies ist viel schneller als eine detaillierte Prüfung pro Poly zwischen Objekten.

Dies kann für komplizierte oder große Physikobjekte noch einen Schritt weiter gehen, wobei Sie das Physiknetz selbst unterteilen können, wobei jeder Unterform ein eigenes AABB zugewiesen wird, gegen das Sie nur prüfen können, ob sich die AABBs zweier Objekte überlappen.

Die meisten Physik-Engines deaktivieren die aktive Physiksimulation auf Physikkörpern, sobald sie zur Ruhe kommen. Wenn ein Physikkörper deaktiviert ist, muss er nur in jedem Frame auf Kollision mit seinem AABB prüfen. Wenn etwas mit dem AABB kollidiert, wird er reaktiviert und führt eine detailliertere Kollisionsprüfung durch. Dies hält die Simulationszeiten niedrig.

Außerdem verwenden viele Physik-Engines "Simulationsinseln", auf denen eine Gruppe von Physikkörpern, die nahe beieinander liegen, zusammen gruppiert werden. Wenn alles auf der Simulationsinsel in Ruhe ist, wird die Simulationsinsel selbst deaktiviert. Der Vorteil der Simulationsinsel besteht darin, dass alle Körper in ihr aufhören können, nach Kollisionen zu suchen, sobald die Insel inaktiv ist. Die einzige Überprüfung jedes Frames besteht darin, festzustellen, ob etwas in den AABB der Insel gelangt ist. Erst wenn etwas in den AABB der Insel gelangt, muss jeder Körper auf der Insel nach Kollisionen suchen. Die Simulationsinsel wird auch reaktiviert, wenn sich ein Körper in ihr selbstständig wieder bewegt. Wenn sich ein Körper weit genug vom Zentrum der Gruppe entfernt, wird er von der Insel entfernt.

Am Ende bleibt dir so etwas (im Pseudocode):

// 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.
               {
           }
        }
    }
}

Ich würde auch empfehlen, nicht so viele Schleifen in solchen Schleifen zu haben. Das obige Beispiel war nur, damit Sie auf die Idee kamen. Ich würde es in mehrere Funktionen aufteilen, die Ihnen die gleiche Funktionalität bieten wie oben gezeigt.

Stellen Sie außerdem sicher, dass Sie den AABBNodes-Container während des Durchlaufens nicht ändern, da dies zu fehlenden Kollisionsprüfungen führen kann. Das mag nach gesundem Menschenverstand klingen, aber Sie wären überrascht, wie einfach es ist, wenn Dinge auf Kollisionen reagieren und Änderungen verursachen, die Sie nicht erwarten würden. Wenn beispielsweise eine Kollision dazu führte, dass eines der kollidierenden Objekte seine Position so weit änderte, dass es aus dem AABB des von Ihnen überprüften Octree-Knotens entfernt wurde, konnte dieser Container geändert werden. Um dies zu lösen, empfehle ich, eine Liste aller Kollisionsereignisse zu führen, die während der Überprüfungen auftreten. Nachdem alle Überprüfungen abgeschlossen sind, durchlaufen Sie die Liste und senden Sie alle Kollisionsereignisse aus.

Nic Foster
quelle
4
Sehr konsistente Antwort mit netten und nützlichen technischen Präzisionen, um den Leser für bestehende Methoden zu sensibilisieren. +1
Walküre
Was ist, wenn ich das kollidierende Objekt entfernen muss? Kann ich den Container ändern? Ich meine, es aus dem Container zu entfernen, da ich das Objekt nicht mehr brauche, weil es "zerstört" ist. Ich benötige eine weitere Schleife, um die Kollisionsereignisse zu durchlaufen, wenn ich sie während der Kollisionserkennung nicht entferne.
Newguy
Das Entfernen des kollidierenden Objekts ist in Ordnung, aber ich würde empfehlen, darauf zu warten, bis der Kollisionsdurchlauf über die gesamte Simulation durchgeführt wurde. Normalerweise markieren Sie nur Objekte, die entfernt werden müssen, oder generieren eine Liste der zu entfernenden Objekte. Nach Abschluss der Kollisionssimulation wenden Sie diese Änderungen an.
Nic Foster
4

In Ihrem Beispiel wird jedes Objektpaar mehrmals getestet.

Nehmen wir ein sehr einfaches Beispiel mit einem Array, das 0,1,2,3 enthält

Mit Ihrem Code erhalten Sie Folgendes:

  • In Schleife 0 testen Sie gegen 1, 2 und 3
  • In Schleife 1 testen Sie gegen 0, 2 und 3 ===> (0-1 bereits getestet)
  • In Schleife 2 testen Sie gegen 0, 1 und 3 ===> (0-2 / 1-2 bereits getestet)
  • In Schleife 3 testen Sie gegen 0, 1 und 2 ===> (0-3 / 1-3 / 2-3 bereits getestet)

Sehen wir uns nun den folgenden Code an:

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();
}

Wenn wir das Array mit 0,1,2,3 erneut verwenden, haben wir das folgende Verhalten:

  • In Schleife 0 testen Sie gegen 1, 2, 3
  • In Schleife 1 testen Sie gegen 2, 3
  • In Schleife 2 testen Sie gegen 3
  • In Schleife 3 testen Sie gegen nichts

Mit dem zweiten Algorithmus haben wir 6 Kollisionstests, während der vorherige 12 Kollisionstests verlangte.

Walküre
quelle
Dieser Algorithmus führt N(N-1)/2Vergleiche durch, bei denen es sich immer noch um eine O (N ^ 2) -Leistung handelt.
Kai
1
Nun, mit 30 Objekten, wie angefordert, bedeutet dies 465 Kollisionstests gegen 870 ... es ist wahrscheinlich aus Ihrer Sicht ähnlich, aber nicht aus meiner Sicht. Darüber hinaus ist die in der anderen Antwort angebotene Lösung genau der gleiche Algorithmus :)
Valkea
1
@ Walkea: Nun, ein Teil davon ist. :)
Nic Foster
@NicFoster: Ja, Sie haben Recht;) Ich habe streng über den Kollisionstest zwischen den ausgewählten Objekten gesprochen, nicht über den Partitionierungsteil des Algorithmus, der offensichtlich eine sehr wertvolle Ergänzung ist, die ich in meinem Beispiel nicht einmal hinzugefügt habe, als Ich habe es geschrieben.
Walküre
Wird dies als Amortisation bezeichnet? Trotzdem danke!
JCora
3

Entwerfen Sie Ihren Algorithmus entsprechend Ihren Anforderungen, aber halten Sie die Implementierungsdetails gekapselt. Auch in Javascript gelten grundlegende OOP-Konzepte.

Denn das N =~ 30, O(N*N)ist kein Problem, und Ihre lineare Suche ist wahrscheinlich genauso schnell wie jede andere Alternative. Sie möchten jedoch keine Annahmen in Ihren Code fest codieren. Im Pseudocode hätten Sie eine Schnittstelle

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

Das beschreibt, was Ihre Artikelliste tun kann. Anschließend können Sie eine ArrayContainer-Klasse schreiben, die diese Schnittstelle implementiert. In Javascript würde der Code folgendermaßen aussehen:

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

Und hier ist ein Beispielcode, der 300 Begrenzungsrahmen erstellt und alle Schnittpunkte abruft. Wenn Sie ArrayContainer und QuadTreeContainer korrekt implementiert haben, müssen Sie in Ihrem Code nur Änderungen var allMyObjects = new ArrayContainer()an vornehmen 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();

Ich habe die Implementierung für den Standard-ArrayContainer hier erstellt:

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

Jimmy
quelle
Hinweis: Diese Antwort wurde durch Banes Beschwerde motiviert, dass seine Codebasis zu groß, chaotisch und schwer zu verwalten sei. Obwohl es nicht viel zur Diskussion über die Verwendung eines Arrays gegen einen Baum beiträgt, hoffe ich, dass es eine relevante Antwort ist, wie er seinen Code gezielt besser organisieren kann.
Jimmy
2

Sie sollten auch die Arten von Objekten berücksichtigen, die sinnvoll kollidieren können.

Zum Beispiel muss der Spieler wahrscheinlich auf Kollision mit allem außer seinen eigenen Kugeln überprüft werden. Feinde müssen jedoch möglicherweise nur gegen die Kugeln des Spielers geprüft werden. Kugeln müssen mit ziemlicher Sicherheit nicht miteinander kollidieren.

Um dies effizient zu implementieren, möchten Sie wahrscheinlich separate Objektlisten führen, eine pro Objekttyp.

Adam
quelle