Pygame: Performance, Ticklänge, Kollision, Sprites...

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
Benutzeravatar
Don Polettone
User
Beiträge: 115
Registriert: Dienstag 23. November 2010, 20:26
Wohnort: Schweiz

Hallo da allerseits und so seit langem mal wieder... :D

Noch immer geb ich Gas mit PyGame und das sieht doch schon anständiger aus:

Bild

Nun habe ich aber ein neues Projekt begonnen und da geht es weniger taktisch zu - ich mache eine Art Battle City (NES) - Klon (für die die's nicht kennen: 8 BIT Panzergeballere). Nun bin ich ich erstmals auf Performance Probleme gestossen. Es gibt mir schon zu denken, wenn so ein homebrew 8 BIT Klon meinen Quadi zum ächzen bringt, und genau dies ist der Fall. Da die Performance ja von mehreren Faktoren abhängig ist, habe ich mich mal erkundigen wollen, welches "tick" (delta time oder wie auch immer) Ihr da wählen würdet für so ein actionlastiges Game. Bei meinem Tower Defence Zeugs (Screen oben) habe ich einen echt langen Tick von 38 ms. Stört aber nicht weiter und ich habe alle programmiertechnischen Freiheiten - sprich: Ich kann möglichst viel selbst coden und möglichst wenig Pygame Built-In Zeugs benutzen. Genau dies ist auch mein Ziel, denn so hab ich mir meine bescheidenen Kenntnisse soweit beigebracht. Bisher habe ich immer die ganze Szene neu geblittet und den ganzen screen aktualisiert - pygame.display.flip(). In meinem Panzer Game geht das nicht, soweit bin ich schon. Habe auch probiert, auf dem screen nur die relevanten rects mit Hintergrund zu überblitten und diese dann an der neuen Position wieder zu blitten und dann nur die einzelnen Rects upzudaten - pygame.display.update(rectlist). Dies funzt soweit so gut, aber krass Leistung brauchen Kollisionsabfragen...

Das Problem ist Folgendes:

Ich habe 4 Panzer, alle per Pad gesteuert von Spielern. Diese können schiessen, und Tiles sind da auch auf der Map; zum teil zerstörbar, zum Teil nicht. Nun muss ja jeder Panzer in jedem Frame abfragen:

- Bin ich in einem Tile? (Rectkollisionsabfrage mit allen Tiles)
- Bin ich in einem anderen Panzer? (Rectkollisionsabfrage mit allen Gegnern)
- Ist einer meiner Schüsse in einem Tile? (Rectkollisionsabfrage mit allen Tiles)
- Ist einer meiner Schüsse in einem gegn. Panzer? (Rectkollisionsabfrage mit allen Gegnern)

Dies ist ja recht performancelastig... Ich habe das Gefühl, meine eigene Rect Kollisionsabfrage ist nicht sooooooo schlecht; jedoch muss ich, um diese ausführen zu können, jedes Mal die Eckpositionen aller bewegten Objekte updaten. Dafür habe ich eine eigene Methode -> Game_Objekt.blitpos_update(). Game_Objekt ist also quasi meine "Sprite" Klasse. Das fiese ist nur, dass ein pygame.rect.Rect - Objekt im Gegensatz zu meinem Game_Objekt automatisch alle Positionen aktualisiert, wenn man nur eine davon verändert - z.Bsp. wenn man codet: self.rect.centerx += 2 , dann ist auch self.rect.topleft und alles andere angepasst! Das verstehe ich nicht! self.rect.centerx += 2 ist eine Neuzuweisung, wieso kann diese Instanzvariablen verändern? Was geht da ab..? Kann ich selbst sowas coden?

Zum anderen:

Wie genau (im Sinne von zeitlich exakt) arbeitet Pygame? Kann ich damit überhaupt einen Plattformer mit schöner Leistung coden? Welches Tick soll ich nehmen? Ich habe einiges probiert, aber auch wenn ich nicht das ganze Bild jedes Frame update sondern mit Hintergrund überblitte etc und smooth animiere, habe ich das Gefühl, dass es ruckelt, egal welches delta time ich bestimme (auch ohne Kollisionsabfragen etc).

Sorry für den allumfassenden Thread... und bin für jede Antwort dankbar! (Sofern diese denn konstruktiver Natur ist, wohlverstanden :shock: )

Es grüsst:
Ich code, also bin ich.
BlackJack

@Henry Jones Jr.: Also entweder möchtest Du es schnell haben, oder Du möchtest möglichst wenig von PyGame verwenden. Das widerspricht sich ein wenig.

Die Abfragen Panzer und Schuss in Kachel sind eigentlich trivial wenn man davon ausgeht, dass die Kacheln gleichmässig sind. Denn dann kann man doch einfach aus der Position von Panzer oder Schuss die betroffenen Kacheln ermitteln. Und zwar ohne Kollisionsabfrage mit allen Kacheln.

Im Gegensatz zu der wahrscheinlich in C implementierten `Rect`-Kollisionsabfrage von Pygame kann Deine eigene eigentlich nur verlieren. Warum verzichtest Du auf Sprites und Spritegruppen mit deren Möglichkeiten?

Zu den berechneten Attributen: Ob ``spam.parrot = 5`` eine Attributzuweisung oder ein Methodenaufruf ist, kann man an der Syntax nicht ablesen. Umgekehrt kann auch ein scheinbar einfacher Attributzugriff eigentlich ein Methodenaufruf sein. Das Stichwort sind „Properties“ und damit verbunden die `property()`-Funktion, mit der man „berechnete Attribute“ erzeugen kann. Die `Rect`-Klasse wird nicht alle Eckpunkte speichern, sondern nur die Werte, die benötigt werden um diese Punkte alle bei Bedarf, also beim Zugriff, zu berechnen.

Wenn es gar nicht ruckeln darf, dann muss die Hardware mitspielen und es erlauben, dass ein komplettes Bild in einem Durchgang ohne „tearing“ neu „gezeichnet“ werden kann. Dem angepasst müsste dann auch die Bildwiederholrate sein. Da kann man also nicht einfach pauschal eine sagen, die dann auf allen Systemen passt.
deets

@Mr Jones

Das hoert sich fuer mich so an, als ob du da algorithmisch ne Schippe drauflegen solltest. Sowas wie "mit allen Tiles vergleichen" ist auf jeden Fall falsch - dazu gibt es doch gar keinen Grund. Du kennst die Koordinaten des Panzers. Und dadurch kannst du durch eine einfache mathematische Operation bestimmen, auf welchen 4 tiles der stehen kann.

Alternativ kannst du auch noch OcTrees oder BSP zum Einsatz bringen.

Und ich denke, dass da deine Optimierung bezueglich der Bildschirmausgabe ueberzogen ist. Die Zeiten sind vorbei, als man sowas machen musste - das wuerde mich sehr wundern, wenn das wirklich in's Gewicht fiele.
Benutzeravatar
Don Polettone
User
Beiträge: 115
Registriert: Dienstag 23. November 2010, 20:26
Wohnort: Schweiz

Hi zusammen,

vorab vielen Dank für die guten Antworten!

@ BlackJack bzgl. Hitabfrage:

Wenn Du mit "Position" die klassichen x / y Koordinaten des Panzers meinst: Diese reichen doch nicht aus, um zu prüfen, ob er sich in einem anderen Rect befindet? Zumindest was wie in der Antwort von deets muss da doch hin (welche 4 Tiles müssen überhaupt gecheckt werden?) Und um dies zu ermitteln, muss der Panzer doch die ganzen Tiles mit dieser Funktion durchgehen und dann die 4 Tiles noch mit rect Kollision prüfen..? Oder bin ich komplett bekloppt oder begriffsstutzig..? Anders gefragt: Wie würdest Du das machen? Irgendwie muss doch der Panzer prüfen, ob und in welchem Tile er gerade "drin" ist? Hierzu muss man doch über alle Tiles iterieren..?

@ BlackJack Bzgl. RectKollision:

Bei mir funzt das so, dass ich eine pos_in_rect - Funktion gecodet habe. Dann habe ich eine rect_collision - Methode. Dieser gebe ich das Rect mit, das geprüft werden soll. Wenn dann einer der insgesamt 8 zu checkenden Punkte im anderen Rect drin ist (oder umgekehrt): BUMM! Natürlich habe ich returns eingebaut, damit nur jeweils berechnet wird, was nötig ist. Ist doch simpel und müsste recht schnell sein, nicht?

@ BlackJack bzgl. vordefinierter Pygame Syntax:

Hast wohl Recht. Ist eben meine Natur, dass ich's verstehen und dann entsprechend selbst "nachbauen" will - so lerne ich am besten (und stosse natürlich andauernd auf Probleme - da lerne ich nochmal umso besser :mrgreen: ) Noch von wegen `property()`-Funktion: Das muss ich anschauen, klingt heftig, danke!

@ deets:

Bzgl. OcTrees / BSP: Ich bin ein Noob... Hab nur schon googeln müssen, was das ist (bin da stets ganz ehrlich). Im Moment ist mir das noch zu heftig, aber ich lese mich da mal rein bei Gelegenheit.

Und bzgl. Performance: Wenn ich Deine AW richtig interpretiere, sollte es, wenn ich einen 8 Bit Klon code, keinen Unterschied machen, ob ich immer das ganze Bild update oder die einzelnen Rects cleare und neu blitte und nur die veränderten Rects update? Oder was genau meinst Du mit "als man sowas machen musste" ?

Jedenfalls vielen Dank, ich habe das Gefühl, der Sache schon wesentlich näher zu kommen und jedes Mal wenn ich hier wat poste wird meine doch sehr bescheidene Python-Welt um ein paar unbekannte Levels erweitert :shock:
Ich code, also bin ich.
deets

@Mr Jones

Ja, du verstehst richtig - es sollte voellig ok gehen, die paar hundert Kacheln & Sprites neu zu blitten. Das ist auf moderner Hardware kein Problem.

Generell gilt: optimieren da, wo die Zeit wirklich verbraten wird. Dazu gibt es profiling, aber in deinem Fall scheinst du ja schon eh zu wissen, dass die Kollisionsabfragen das Problem sind.

Und zu deiner "optimierten" Kollisions-Funktion: auch ohne die gesehen zu haben kann ich schonmal sagen, dass du da am falschen Ende ansetzt. Die Funktion selbst zu optimieren ist relativ sinnlos. Denn das Problem ist nicht, dass die ein bisschen zu langsam laeuft. Sondern dass sie zu *oft* laeuft. Ein kleines Rechenbeispiel: nehmen wir mal an du hast 100 * 100 Kacheln fuer dein Level. Und 4 Panzer. Dann machst du (so ich deine Worte recht verstehe) momentan 100 * 100 * 4 == 400.000 Kollisionsabfragen. Nehmen wir mal an, die Abfrage dauert 1 micro-Sekunde. Dann sind dass immerhin 0.4 Sekunden. Du optimierst die Funktion, und sie braucht nur noch .5 uS - dann sind das immer noch 0.2 Sekunden.

Aber wenn du die Kollision so checkst wie ich das gesagt habe - indem du aus den Koordinaten der Panzer die moeglicherweise betroffenen Tiles berechnest (einfach durch Division der Koordinaten durch die Tile-Groesse, und dann indizieren in das Level-Array), dann hast du nur noch

4 * 4 == 16

Vergleiche. Und das sogar immer noch, wenn das Level sich verdoppelt oder verzehnfacht in der Groesse. Womit du auch ganz ohne deine Optimierung der Kollisions-Berechnung selbst auskommst.
Benutzeravatar
Don Polettone
User
Beiträge: 115
Registriert: Dienstag 23. November 2010, 20:26
Wohnort: Schweiz

Ciao deets,

habe gerade Pygame auf meinem PC ein bisschen "gebanchmarked" und Du hast vollkommen Recht. Wenn ich tausend Panzer Objekte erstelle und fortlaufend den Hintergrund blitte und all die Panzer darüber und das gesamte display update, komm ich auf ca. 5 Mikrosekunden:

Code: Alles auswählen

    def run(self):

        while True:

            t_before = time.clock()

            self.blit_hintergrund()

            self.check_quit()

            for tank_x in self.tanks:
                self.SCREEN.blit(tank_x.bild, tank_x.rect.topleft)

            t_after = time.clock()

            pygame.display.flip()

            print t_after - t_before
-->

0.00535653260189
0.00545764755423
0.00532240097241
...

Demnach müsste es ausreichen, wenn ich zumindest mit der Sprite Klasse arbeite (wegen den berechneten Attributen), ich kann da sicher meine Kollisionsabfrage beibehalten. Und einfach halt auch schauen, dass ich nicht zu viel sinnloses bzw. unnützes Zeugs rechnen lasse, dann kommt das schon noch.

vielen Besten jedenfalls für die Tipps!
Ich code, also bin ich.
Antworten