Quadtree

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

Hi, ich bastel derzeit in C++ an einem 2D-Spiel. Zur Kollisionserkennung verwende ich Polygone und umschließe diese Polygone zur Approximation (und schnelleren Erkennung von nicht-kollisionsrelevanten Objekten) Kreise um die Polygone. Und diese Kreise werden von Quadraten eng umschlungen (siehe 1. Phase)
Folgendes Schema beabsichtige ich:
  • 1. Phase: via Quadtree werden alle benachbarten Objekte gesucht. Im Quadtree sollen die Objekte enthalten sein, so dass ich anhand der zugehörigen Quadrate (btw achsenparallel) entscheiden kann, welche Objekte kollisionsverdächtig sind und welche nicht. Quasi ein "Aussieben".
  • 2. Phase: Alle Objekte, deren Quadrat sich mit dem Quadrat des primären Objektes (das, für das ich Kollisionen suche) schneiden, werden bezüglich der Kreise überprüft. Dabei fallen ggf. wieder ein paar Objekte raus.
  • 3. Phase: Schließlich wird polygonbasierte gesuch, d.h. ich gehe z.B. das Polygon Strecke für Strecke durch und suche Schnitte oder Berührungen. Das ist sehr rechenintensiv, so dass es in der dritten und letzten Phase erst soweit kommen soll.
So nun, warum ich das mit Python-Forum poste :D Für "Programmprototypen" finde ich die Verwendung von Python absolut unschlagbar :) Ich habe ein kleines Skript geschrieben, was so einen Quadtree implementiert und die Suche mit einer linearen Liste vergleicht. Nur habe ich keine Ahnung, ob meine Quadtree-Implementierung so "richtig" bzw. vollständig ist, oder worauf ich noch achten sollte. :K

Hier ist das Skript - ich hoffe es ist nicht zu verworren:

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: utf8 -*-

import math


# gewöhnlicher Kreis
class Circle:

    def __init__(self, x, y, r):
        self.x = x # x Koordinate Mittelpunkt
        self.y = y # y Koordinate Mittelpunkt
        self.r = r # Radius

    def __str__(self):
        return '<{0},{1}:{2}>'.format(self.x, self.y, self.r)

    # Eukldischer Abstand zweier Punkte
    def distance(self, x, y):
        return math.sqrt((self.x - x)**2 + (self.y - y)**2)

    # Kollision Kreis-Punkt
    def collide(self, x, y):
        return (self.distance(x, y) <= self.r)

    # Kollision Kreis-Kreis
    def intersect(self, circ):
        if (self.distance(circ.x, circ.y) <= self.r + circ.r):
            # hier sollte dann die polygonbasierte Kollisionsprüfung folgen
            #print 'Abstand: {0}, Radiensumme: {1}'.format(self.distance(circ.x, circ.y), self.r+circ.r)
            return True
        else:
            return False

    # Quadrat (achsenparallel) erzeugen, dessen Inkreis dieser Kreis ist
    def getRect(self):
        return Rect(self.x - self.r, self.y - self.r, 2 * self.r, 2 * self.r)


# Achsenparalleles Rechteck
class Rect:

    def __init__(self, x, y, w, h):
        self.x = x # x Koordinate "oben links"
        self.y = y # y Koordinate "oben links"
        self.w = w # Breite
        self.h = h # Höhe

    # Kollision Rechteck-Punkt
    def collide(self, x, y):
        return ((self.x <= x <= self.x + self.w) and
                 (self.y <= y <= self.y + self.h))

    # Kollision Rechteck-Rechteck
    def intersect(self, rect):
        return ((self.x < rect.x + rect.w) and (rect.x < self.x + self.w) and
                (self.y < rect.y + rect.h) and (rect.y < self.y + self.h))

    def __str__(self):
        return '<{0},{1};{2},{3}>'.format(self.x, self.y, self.w, self.h)

class Quadtree:

    def __init__(self, depth, rect):
        self.rect  = rect
        self.depth = depth
        self.ne = None
        self.se = None
        self.sw = None
        self.nw = None
        self.objs = list()
        if (depth > 1):
            w = self.rect.w / 2
            h = self.rect.h / 2
            x = self.rect.x + w
            y = self.rect.y
            self.ne = Quadtree(depth-1, Rect(x, y, w, h))
            w = self.rect.w / 2
            h = self.rect.h / 2
            x = self.rect.x + w
            y = self.rect.y + h
            self.se = Quadtree(depth-1, Rect(x, y, w, h))
            w = self.rect.w / 2
            h = self.rect.h / 2
            x = self.rect.x
            y = self.rect.y + h
            self.sw = Quadtree(depth-1, Rect(x, y, w, h))
            w = self.rect.w / 2
            h = self.rect.h / 2
            x = self.rect.x
            y = self.rect.y
            self.nw = Quadtree(depth-1, Rect(x, y, w, h))

    def insert(self, obj):
        if (not self.rect.intersect(obj.getRect())):
            return
        if (self.depth == 1):
            self.objs.append(obj)
        else:
            self.ne.insert(obj)
            self.se.insert(obj)
            self.sw.insert(obj)
            self.nw.insert(obj)

    def query(self, obj):
        inRange = list()
        if (not self.rect.intersect(obj.getRect())):
            return inRange
        if (self.depth == 1):
            for o in self.objs:
                if (obj.intersect(o)):
                    inRange.append(o)
        else:
            inRange.extend(self.ne.query(obj))
            inRange.extend(self.se.query(obj))
            inRange.extend(self.sw.query(obj))
            inRange.extend(self.nw.query(obj))
        return inRange


if __name__ == '__main__':
    import time, random

    size = 600
    depth = 5
    qtree = Quadtree(depth, Rect(0, 0, size, size))
    linear = list()
    for i in range(size):
        x = random.randint(0, size * 10) / 10.0
        y = random.randint(0, size * 10) / 10.0
        r = random.randint(0, size * 10) / 20.0
        c = Circle(x, y, r)
        qtree.insert(c)
        linear.append(c)
    print 'Strukturen (Liste und Quadtree der Tiefe {0}) mit {1} Objekten wurden erstellt.\n'.format(depth, size)

    prim = Circle(1.5, 1.5, 0.1)
    print 'Kreis {0} (mit Quadrat {1}) Kollisionen:'.format(prim, prim.getRect())
    s = time.time()
    objs = qtree.query(prim)
    e = time.time()
    #for o in objs:
    #    print o
    print '\t\t--> {0} Objekte gefunden'.format(len(objs))
    print 'Zeit Quadtree:\t\t{0}\n'.format(e-s)

    print 'Kreis {0} (mit Quadrat {1}) Kollisionen:'.format(prim, prim.getRect())
    s = time.time()
    objs = list()
    for o in linear:
        if prim.intersect(o):
            objs.append(o)
    e = time.time()
    #for o in objs:
    #    print o
    print '\t\t--> {0} Objekte gefunden'.format(len(objs))
    print 'Zeit lineare Liste:\t{0}'.format(e-s)
Mit der Geschwindigkeit sieht es schonmal gut aus - und in C++ kann ich vllt. mit noch etwas mehr Speed rechnen:

Code: Alles auswählen

Strukturen (Liste und Quadtree der Tiefe 4) mit 5000 Objekten wurden binnen 2.72023701668s erstellt.

Kreis <1.5,1.5:0.1> (mit Quadrat <1.4,1.4;0.2,0.2>) Kollisionen:
		--> 321 Objekte gefunden
Zeit Quadtree:		0.00606107711792

Kreis <1.5,1.5:0.1> (mit Quadrat <1.4,1.4;0.2,0.2>) Kollisionen:
		--> 321 Objekte gefunden
Zeit lineare Liste:	0.0397760868073
Vielen Dank im Vorraus!

LG Glocke

PS: falls ich im falschen Unterforum gelandet bin, dann verschiebt mich einfach ^^
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Hallo.

Ich gehe einfach mal von oben nach unten durch:

- Für Koordinaten würde ich Tupel verwenden (oder noch besser Matrizen) verwenden und keine einzelnen Attribute. Wenn du trotzdem über x und y auf die Werte zugreifen möchtest, dann kannst du das wunderbar über Properties erledigen.

- Kommentare würde ich nie hinter den Code schreiben, dass wird sehr schnell unübersichtlich. Vor der entsprechende Zeile ist ein Kommentar am besten untergebracht.

- Wenn du Methoden dokumentierst, dann solltest du Docstrings verwenden, darin kannst du jede Menge unterbringen: was macht die Methode, was liefert sie als Ergebnis, was bedeuten die Parameter, Tests, etc.

- Den Abstand zwischen zwei Punkten solltest du nicht über die Distanz messen, sondern über das Quadrat der Distanzen. Damit spart man sich das Wurzelziehen.

- Um den Rückgabewert beim return gehören KEINE Klammern.

- Und auch um die Bedingung bei if-Statements gehören keine Klammern. Python ist nicht C ;-) Schau dir mal den PEP 8 an.

- Wenn du ein Konstrukt der Form

Code: Alles auswählen

if pred:
    return True
else:
    return False
hast, dann willst du eigentlich das schreiben:

Code: Alles auswählen

return pred
- getRect sollte get_rect heißen. Wie gesagt, PEP 8.

- Ich habe jetzt nicht in den Code geschaut, aber hast du daran gedacht, dass Rechtecke auch kollidieren können, wenn kein Eckpunkt im anderen Rechteck liegt?

Zum Quadtree:

- Erstmal fällt natürlich auf, dass du da sehr viel doppelten Code in der __init__-Methode hast, dass kann man zusammenfassen.

- Am problematischten ist allerdings, dass du gar keinen Quadtree hast. Also prinzipiell natürlich schon, aber du erzeugst immer alle Knoten. Im Prinzip hast du also ein großes Giter, auf welches du noch einen Baum setzt. Das ist ganz schön ineffizient, da du bei Kollisionsabfragen dann auch direkt das Gitter befragen könntest. Was ich damit sagen will: der Baum macht erst dann Sinn, wenn du Knoten nur dann erzeugst, wenn sie auch tatsächlich benötigt werden.


Im __main__-Abschnitt kann man auch noch das eine oder andere verbessern, dafür fehlt mir jetzt aber ein wenig die Zeit. Vielleicht komme ich nachher noch dazu. Ansonsten findet sich sicher auch jemand anderes.
Das Leben ist wie ein Tennisball.
glocke
User
Beiträge: 66
Registriert: Mittwoch 23. Februar 2011, 21:18

EyDu hat geschrieben:- Für Koordinaten würde ich Tupel verwenden (oder noch besser Matrizen) verwenden und keine einzelnen Attribute. Wenn du trotzdem über x und y auf die Werte zugreifen möchtest, dann kannst du das wunderbar über Properties erledigen.
EyDu hat geschrieben:- Kommentare würde ich nie hinter den Code schreiben, dass wird sehr schnell unübersichtlich. Vor der entsprechende Zeile ist ein Kommentar am besten untergebracht.
EyDu hat geschrieben:- Wenn du Methoden dokumentierst, dann solltest du Docstrings verwenden, darin kannst du jede Menge unterbringen: was macht die Methode, was liefert sie als Ergebnis, was bedeuten die Parameter, Tests, etc.
Wie gesagt: Das ganze ist nur ein Funktionstest. Ich werde das ganze später eh in C++ implementieren. Deshalb habe ich mal auf sowas nicht geachtet :oops:
EyDu hat geschrieben:- Den Abstand zwischen zwei Punkten solltest du nicht über die Distanz messen, sondern über das Quadrat der Distanzen. Damit spart man sich das Wurzelziehen.
Okay, danke :)
EyDu hat geschrieben:- Um den Rückgabewert beim return gehören KEINE Klammern.
Ist eine komische Angewohnheit. Merk ich mir :)
EyDu hat geschrieben:- Und auch um die Bedingung bei if-Statements gehören keine Klammern. Python ist nicht C ;-) Schau dir mal den PEP 8 an.
Ja wie gesagt ^^
EyDu hat geschrieben:- Wenn du ein Konstrukt der Form

Code: Alles auswählen

if pred:
    return True
else:
    return False
hast, dann willst du eigentlich das schreiben:

Code: Alles auswählen

return pred
Das habe ich absichtlich gemacht, um für mich die Stelle freizulassen, an der die polygonbasierte Kollision kommt. Da würde "return pred" nicht reichen. Aber du hast selbstverständlich mit allem recht, was du geschrieben hast :)
EyDu hat geschrieben: - Ich habe jetzt nicht in den Code geschaut, aber hast du daran gedacht, dass Rechtecke auch kollidieren können, wenn kein Eckpunkt im anderen Rechteck liegt?
Irgendwie ist mir das im Eifer des Gefechtes entfallen. Hast du da einen Vorschlag?
EyDu hat geschrieben:- Am problematischten ist allerdings, dass du gar keinen Quadtree hast. Also prinzipiell natürlich schon, aber du erzeugst immer alle Knoten. Im Prinzip hast du also ein großes Giter, auf welches du noch einen Baum setzt. Das ist ganz schön ineffizient, da du bei Kollisionsabfragen dann auch direkt das Gitter befragen könntest. Was ich damit sagen will: der Baum macht erst dann Sinn, wenn du Knoten nur dann erzeugst, wenn sie auch tatsächlich benötigt werden.
Arg stimmt :cry: Quasi dynamisches Erweitern und Kürzen. Wird nachgeholt :)
EyDu hat geschrieben:Im __main__-Abschnitt kann man auch noch das eine oder andere verbessern, dafür fehlt mir jetzt aber ein wenig die Zeit. Vielleicht komme ich nachher noch dazu. Ansonsten findet sich sicher auch jemand anderes.
Naja wie gesagt: die Python-Guidelines sind mir hier erstmal egal, weil ich die Sprache gerade nur als "Mittel zum Test-Zweck" verwende :D


Danke für deine Mühe :)

Viel Text .. ich fasse zu sammen:
  • TODO 1: Quadtree dynamisch Erweitern und Kürzen.
  • TODO 2: Rechteckkollision überarbeiten.
  • TODO 3: Abstand zweier Punkte als Quadrat wegen auf-Wurzelziehen-verzichten.
LG Glocke
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

glocke hat geschrieben:Wie gesagt: Das ganze ist nur ein Funktionstest. Ich werde das ganze später eh in C++ implementieren. Deshalb habe ich mal auf sowas nicht geachtet :oops:
Auch bei Prototypen bietet es sich an an Standards zu halten. Im Prinzip ist das bei dir auch kein zusätzlicher Aufwand, das meiste ist nur eine Sache der Form. Allerdings solltest du dir die Sache mit numpy wirklich überlegen, da dies einiges verkürzt. Da du unter C++ sicher auch mit einer entsprechenden Mathematik-Library arbeitest, lässt sich der Code dann noch leichter übetragen. Gerade bei Vektor-/Matrix-Berechnungen kann man den Code dann fast 1 zu 1 übernehmen.

glocke hat geschrieben:
EyDu hat geschrieben:- Ich habe jetzt nicht in den Code geschaut, aber hast du daran gedacht, dass Rechtecke auch kollidieren können, wenn kein Eckpunkt im anderen Rechteck liegt?
Irgendwie ist mir das im Eifer des Gefechtes entfallen. Hast du da einen Vorschlag?
Dein Code sieht gut aus. Da nur der Schnitt berechnet wird ist der Spezialfall (Kreuz aus zwei Rechtecken) abgedeckt. Zu Kollisionserkennungen gibt es übrigens eine ganze Menge an Artikeln und jede Menge fertigen C++-Code.

glocke hat geschrieben:Naja wie gesagt: die Python-Guidelines sind mir hier erstmal egal, weil ich die Sprache gerade nur als "Mittel zum Test-Zweck" verwende :D
Hehe, so gesehen schon. Allerdings fragst du hier natürlich nach Hilfe und da ist es dann nicht mehr nur dein Testcode, dann lesen ihn auch andere. Aber wie gesagt: da ist jetzt nichts bei, was unleserlich wäre.
Das Leben ist wie ein Tennisball.
Antworten