Pygame Fenster mit Buttons flackert

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
_squid_
User
Beiträge: 2
Registriert: Dienstag 24. Mai 2022, 10:05

Einen schönen guten Tag allen miteinander!

Ich bin relativ neu bei Python und Pygame mit dabei. Dabei wollte ich Snake in Pygame programmieren und habe mich an das Tutorial von The Dev gehalten. Jedoch wollte ich am Ende einen Exit und einen Replay Button mit einbauen. Die Buttons an sich funktionieren, wenn ich diese jedoch in den Mainloop mit einbaue, flackert das Fenster die ganze Zeit auf und die Buttons funktionieren nicht mehr. Ich hab auch schon alles ausprobiert, im Internet gesucht, ich habe auch versucht die Buttons an eine andere Stelle einzusetzen und das Fenster neu definiert. Aber eine konkrete Lösung für mein Problem habe ich nicht gefunden.

Code: Alles auswählen

 go = True
 ende = False
 
 while go:
 	......
 	if not ende:
 		zeichner()
 		textGrund, textKasten = textObjekt("Score: " + str(score), font)
 		textKasten.center = (350, 40)
 		screen.blit(textGrund, textKasten)
 		pygame.display.update()
 	else:
 		pygame.display.set_mode([700, 700])
 		screen.fill((255, 255, 255))
 		if start_button.draw(screen):
 			go = True
 		if exit_button.draw(screen):
 			ende = True
 			print("Du hast " + str(score) + " Punkte erreicht")
 			sys.exit()
 		pygame.diplay.update()
 	clock.tick(7)
 
 pygame.display.update()
Der Startbutton ist in dem Fall der Replaybutton. Ich wäre euch mega dankbar, wenn ihr mir helft. Ich sitze an dem Problem schon seit Tagen.

Viele Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@_squid_: Der Quelltext enthält sehr wenig informationen, da viel fehlt und man beispielsweise nicht weiss was `zeichner()` macht oder was für einen Typ `start_button` und `exit_button` haben, und damit was die `draw()`-Methode macht.

Das sieht auch jeweils komisch aus. `zeichner()` weil die Funktion gar keine Argumente bekommt und das vermuten lässt, dass hier auf globalen Variablen operiert wird. Und die `draw()`-Methoden scheinen gleichzeitig irgendwie zu testen ob ein Mausklick stattgefunden hat. Das ist überraschend, weil das aus dem Methodennamen so überhaupt nicht hervor geht. Und so etwas sollte eigentlich auch in der Schleife passieren wo die Ereignisse behandelt werden.

Was problematisch aussieht ist der `set_mode()`-Aufruf. Den macht man einmal um das Fenster mit einer gegebenen Grösse zu erzeugen, nicht ständig in einer Schleife. Und diese Funktion liefert als Ergebnis das `Surface` das für die Darstellung in dem Fenster verwendet wird. Der Code ignoriert diesen Rückgabewert aber und malt auf einem `screen` das vermutlich von einem anderen Aufruf dieser Funktion stammt.

Letztendlich würde man das eher nicht alles in eine Funktion stecken, das wird zu schnell zu umfangreich und unübersichtlich.

Im ``else``-Zeig steckt ein `AttributeError`: `pygame` hat kein Attribut `diplay`.

Sonstige Anmerkungen: Eingerückt wird per Konvention mit vier Leerzeichen pro Ebene. Nicht mit Tabulatorzeichen.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (PascalCase).

Funktions- und Methodennamen beschreiben üblicherweise die Tätigkeit die von der Funktion/Methode durchgeführt wird. `zeichner` oder `textObjekt` sind keine Tätigkeiten.

Bei `textGrund` verstehe ich den `Grund` nicht so wirklich. Was soll das bedeuten? Das ist ja der Text als `Surface`.

Das zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.

Wenn am Anfange oder am Ende von allen Zweigen eines ``if``/``elif``/``else``-Konstrukts das gleiche gemacht wird, dann schreibt man das nicht in jeden Zweig, sondern einmal vor beziehungsweise hinter das Konstrukt.

``ende = True`` ist sinnfrei wenn das nicht irgendwo verwendet wird, was das `sys.exit()` kurz danach verhindert.

`sys.exit()` sollte man nicht als ”Notausgang” verwenden um sich um einen ordentlichen, sinnvollen Programmablauf zu drücken. Hier will man die Schleife verlassen. Und man sollte ordentlich hinter sich aufräumen und ein `pygame.quit()` sicherstellen.

Ungetestet:

Code: Alles auswählen

# ...

WHITE = (255, 255, 255)

# ...


def main():
    try:
        # ...
        go = True
        ende = False

        while go:
            # ...

            #
            # TODO So unterschiedliche Aufgaben sollten nicht in einer Funktion
            # stecken.
            #
            if not ende:
                #
                # FIXME Hier fehlen Argumente.  Mindestens `screen`.
                #
                # TODO Es wäre nett wenn der Name verrät *was* die Funktion
                # zeichnet.
                #
                zeichnen()
                text_surface, text_rect = create_text(f"Score: {score}", font)
                text_rect.center = (350, 40)
                screen.blit(text_surface, text_rect)
            else:
                #
                # BUG Nicht in Schleife immer wieder aufrufen und Rückgabewert nicht
                # ignorieren, sondern verwenden.
                #
                pygame.display.set_mode((700, 700))
                screen.fill(WHITE)
                if start_button.draw(screen):
                    go = True
                elif exit_button.draw(screen):
                    print(f"Du hast {score} Punkte erreicht")
                    break

            pygame.display.update()
            clock.tick(7)

    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17748
Registriert: Sonntag 21. Oktober 2012, 17:20

Solche Neustart-Buttons würde ich in eine extra Event-Schleife auslagern, Du brauchst eh eine große Schleife um das Spiel neu zu starten.
Also ungefähr so (natürlich alles so exemplarisch, wie Dein ursprüngliches Codefragment, aber das Prinzip sollte klar werden):

Code: Alles auswählen

def play_game():
    ...
    ende = False
    while not ende:
        zeichnen()
        text_surface, text_rect = create_text(f"Score: {score}", font)
        text_rect.center = (350, 40)
        screen.blit(text_surface, text_rect)
        pygame.display.update()
        clock.tick(7)
    return score

def ask_restart_game():
    while True:
        pygame.display.set_mode((700, 700))
        screen.fill(WHITE)
        start_button.draw(screen)
        exit_button.draw(screen)
        if start_button.is_pressed():
            return True
        elif exit_button.is_pressed():
            return False
        pygame.display.update()
        clock.tick(7)

def main():
    try:
        ...
        while True:
            score = play_game()
            if not ask_restart_game():
                break
        print(f"Du hast {score} Punkte erreicht")
    finally:
        pygame.quit()

if __name__ == "__main__":
    main()
_squid_
User
Beiträge: 2
Registriert: Dienstag 24. Mai 2022, 10:05

Vielen Dank für eure Antwort!

Ich bin ehrlich gesagt noch viel am lernen und das war auch mein erstes Projekt mit Pygame. Da habe ich mir ein Tutorial auf YouTube angeschaut und auch dran gehalten. In Zukunft werde ich allerdings die ganzen Tipps berücksichtigen. Vielen Dank übrigens für die ganzen Tipps! Am Ende habe ich nochmal den ganzen Code hingeschrieben, damit man einige Aspekte vielleicht besser nachvoll ziehen kann. Die Funktion- und Methoden sind auch alle definiert. Die Knöpfe habe ich in einem separaten Fenster definiert und eingefügt. Mir war gar nicht bewusst, dass ich die Neustart-Buttons auch als eigene Event-Schleifen schreiben kann. Ich habe eher die ganze Zeit versucht, diese irgendwo einzusetzen.

Aber nochmal vielen Dank für die Hilfe.


Code: Alles auswählen

import pygame
import sys
import numpy as np
from pygame import surface
import button

partikel = 25
kopf_partikel = 25
schlange = [[13, 13], [13, 14]]
apfelCords = []
richtung = 0

pygame.init()
clock = pygame.time.Clock()
font = pygame.font.SysFont('arial black', 35)
screen = pygame.display.set_mode([700, 700])

start_img = pygame.image.load('WiederholenKnopf.png').convert_alpha()
exit_img = pygame.image.load('ExitKnopf.png').convert_alpha()

start_button = button.Button(100, 200, start_img, 1)
exit_button = button.Button(450, 200, exit_img, 1)


def textObjekt(text, font):
    textFlaeche = font.render(text, True, (255, 255, 255))
    return textFlaeche, textFlaeche.get_rect()


def zeichner():
    screen.fill((0, 102, 0))

    for a in apfelCords:
        Cords = [a[0] * partikel, a[1] * partikel]
        pygame.draw.rect(screen, (255, 0, 0), (Cords[0], Cords[1], partikel, partikel), 0)

    kopf = True
    for b in schlange:
        Cords = [b[0] * kopf_partikel, b[1] * kopf_partikel]
        if kopf:
            pygame.draw.rect(screen, (0, 150, 10), (Cords[0], Cords[1], 20, 20), 0)
            kopf = False
        else:
            pygame.draw.rect(screen, (47, 79, 79), (Cords[0], Cords[1], 20, 20), 0)


def apfelCordGen():
    notOk = True
    while notOk:
        Cord = [np.random.randint(0, 28), np.random.randint(0, 28)]
        change = False
        for x in schlange:
            if Cord == x:
                change = True
        for x in apfelCords:
            if Cord == x:
                change = True
        if not change:
            return Cord


apfelCords.append(apfelCordGen())


go = True
anhang = None
apfelInd = -1
ende = False
score = 0
pause = False

while go:

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP and richtung != 2:
                richtung = 0
            if event.key == pygame.K_RIGHT and richtung != 3:
                richtung = 1
            if event.key == pygame.K_DOWN and richtung != 0:
                richtung = 2
            if event.key == pygame.K_LEFT and richtung != 1:
                richtung = 3

    if anhang is not None:
        schlange.append(anhang.copy())
        anhang = None
        apfelCords.pop(apfelInd)

    zahl = len(schlange) - 1
    for i in range(1, len(schlange)):
        schlange[zahl] = schlange[zahl - 1].copy()
        zahl -= 1

    if richtung == 0:
        schlange[0][1] -= 1
    if richtung == 1:
        schlange[0][0] += 1
    if richtung == 2:
        schlange[0][1] += 1
    if richtung == 3:
        schlange[0][0] -= 1

    for x in range(1, len(schlange)):  
        if schlange[0] == schlange[x]:
            ende = True

    if schlange[0][0] < 0 or schlange[0][0] > 27:  
        ende = True

    if schlange[0][1] < 0 or schlange[0][1] > 27:  
        ende = True

    for x in range(0, len(apfelCords)):
        if apfelCords[x] == schlange[0]:
            anhang = schlange[-1].copy()
            apfelInd = x
            score += 1

    zufall = np.random.randint(0, 100)
    if zufall <= 1 and len(apfelCords) < 1 or len(apfelCords) == 0:
        apfelCords.append(apfelCordGen())

    if not ende:
        zeichner()
        textGrund, textKasten = textObjekt("Score: " + str(score), font)
        textKasten.center = (350, 40)
        screen.blit(textGrund, textKasten)
        pygame.display.update()
    else:
        pygame.display.set_mode([700, 700])
        screen.fill((255, 255, 255))
        if start_button.draw(screen):
            go = True
        if exit_button.draw(screen):
            ende = True
            print("Du hast " + str(score) + " Punkte erreicht")
            sys.exit()
        pygame.display.update()
    clock.tick(7)

pygame.display.update()


So habe ich die Buttons definiert.

Code: Alles auswählen

class Button():
    def __init__(self, x, y, image, scale):
        width = image.get_width()
        height = image.get_height()
        self.image = pygame.transform.scale(image, (int(width * scale), int(height * scale)))
        self.rect = self.image.get_rect()
        self.rect.topleft = (x, y)
        self.clicked = False

    def draw(self, surface):

        action = False
        # Maus position
        pos = pygame.mouse.get_pos()

        # check mouse over and clicked conditions
        if self.rect.collidepoint(pos):
            if pygame.mouse.get_pressed()[0] == 1 and self.clicked == False:  
                self.clicked = True
                action = True

        if pygame.mouse.get_pressed()[0] == 0:
            self.clicked = False

        # draw button on screen
        surface.blit(self.image, (self.rect.x, self.rect.y))

        return action
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@_squid_: Wie vermutet passiert da eine Menge über globale Variablen, was nicht sein sollte. Das ist unübersichtlich und fehleranfällig. Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Ein Modul mit dem Namen `button` und einer einzigen Klasse `Button` macht nicht wirklich Sinn. Das ist Python und kein Java, wo man jede Klasse in eine eigene Datei steckt.

Numpy zu importieren nur um daraus dann die `randint()`-Funktion für einzelne Zahlen zu verwende ist sehr übertrieben. Es gibt so eine Funktion auch im `random`-Modul in der Standardbibliothek.

Das `surface`-Modul aus `pygame` wird importiert, aber nirgends verwendet.

Der problematisch aussehende `set_mode()`-Aufruf ist überhaupt nicht notwendig, weil das Fenster ja schon besteht und auch schon die gleiche Grösse hat. Der Aufruf kann/muss also einfach nur entfernt werden.

`go` ist immer `True` und `pause` wird nirgends verwendet.

Namen sollten keine kryptischen Abkürzungen enthalten. `Cord` ist beispielsweise irreführend, weil es das Wort `cord` tatsächlich gibt, und es Schnur oder Kordel bedeutet, was ja aber nichts mit den Koordinaten zu tun hat, für die diese Abkürzung steht.

Es ist auch nicht gut solche Denglischen Namen wie `apfelCords` zu verwenden. Entweder komplett Deutsch oder komplett Englisch. Wonach entscheidest Du denn was Deutsch und was Englisch benannt ist?

`apfelCordGen()` ist sehr umständlich ausgedrückt. `notOk` ist überflüssig als Variable, weil das immer den Wert `True` hat. Und `change` und die beiden Schleifen sind überflüssig, weil man hier ganz einfach eine Bedingung mit ``in`` formulieren kann.

Magische Zahlen für Richtungen zu verwenden erschwert das lesen und damit das verstehen und ist deshalb eine Fehlerquelle beim schreiben oder überarbeiten des Code. Man sollte da wenigstens Konstanten für definieren. Oder einen `enum.Enum`-Typ.

Wenn sich mehrere ``if``-Zweige gegenseitig ausschliessen, kommt ``elif`` zum Einsatz.

`anhang.copy()` ist eine unnötige Kopie denn `anhang` selbst ist ja bereits eine Kopie. Insgesamt ist das auch keine so gute Idee ständig zweielementige Listen zu kopieren. Ja letztlich um zu verhindern dass dann vielleicht irgendwo eine Referenz auf eine Liste verwendet wird, die nicht verändert werden soll. Dafür gibt es aber Tupel, die sich bei Koordinaten sowieso anbieten würden.

Das elementweise von hinten kopieren der Schlangenkoordinaten ist auch irgendwie unsinnig. Denn was da eigentlich passiert, ist da das man das letzte Element weg nimmt und vorne eine neue Kopfkoordinate hinzufüht. Also ein `pop()` und ein `insert()`. Keine eigene Schleife.

Und vor dem Einfügen der neuen Kopfposition kann man mit ``in`` testen ob die im Schlangenkörper gelandet ist. Wieder eine ``for``-Schleife eingespart.

Was man sich auch sparen kann ist `anhang` komplizierter zu machen als notwendig, denn da reicht ein Wahrheitswert. Wenn der wahr ist, spart man sich einfach das entfernen der letzten Schlangenkoordinate.

Und auch `apfel_index` kann man sich sparen, weil man den Apfel auch gleich entfernen kann, wenn man ihn gefunden hat.

Bei dem Test ob der Kopf einen Apfel getroffen hat, kann man auch wieder ohne eigene Schleife auskommen, denn Listen haben eine `index()`-Methode um einen Wert in einer Liste zu suchen.

Mit ``and`` verknüpfte Teilbedingungen die gegen den gleichen Wert prüfen, kann man mit verketteten Vergleichsoperatoren prüfen. Also statt ``a < 0 and a > 27`` kann man ``not 0 <= a <= 27`` schreiben.

Die Bedingung ``if zufall <= 1 and len(aepfel) < 1 or len(aepfel) == 0:`` ist unnötig umständlich. Erst einmal ist ``len(aepfel) < 1`` das gleiche wie ``len(aepfel) == 0``. Und dann wird klar, dass der Zufall hier überhaupt keine Rolle spielt, weil der gesamte Ausdruck immer dann wahr ist, wenn die Äpfel alle sind, und unwahr sonst. Der Wert von `zufall` ist dabei vollkommen egal.

`textObjekt()` würde ich noch beliebige Schlüsselworte mitgeben und die an `get_rect()` weiterreichen, dann kann man so etwas wie `center=…` dort beim Aufruf mitgeben.

Beim Zeichnen von Schlange und Äpfeln sollte man sich die ganzen magischen 0 und 1 Indexzugriffe in die Koordinaten sparen und eifach die beiden Werte an Namen binden.

Statt `kopf` als Flag wäre es simpler die Farbe für den Kopf vor der Schleife als Variable zu setzen und in der Schleife dann zu ändern.

Da das `clicked`-Attribut von `Button`-Objekten nirgends verwendet wird, kann man die `draw()`-Methode deutlich vereinfachen.

`Surface.blit()` kann man als zweites Argument ein `Rect`-Objekt übergeben. Da muss man nicht extra ein Tupel aus `x` und `y` von dem `Rect`-Objekt erstellen.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import random
from enum import Enum, auto

import pygame

PARTIKEL = 25


def create_text(text, font, **kwargs):
    surface = font.render(text, True, (255, 255, 255))
    return surface, surface.get_rect(**kwargs)


class Button:
    def __init__(self, x, y, image, scale=1):
        self.image = pygame.transform.scale(
            image,
            (int(image.get_width() * scale), int(image.get_height() * scale)),
        )
        self.rect = self.image.get_rect(topleft=(x, y))

    def draw(self, surface):
        surface.blit(self.image, self.rect)
        return (
            self.rect.collidepoint(pygame.mouse.get_pos())
            and pygame.mouse.get_pressed()[0]
        )


class Direction(Enum):
    UP = auto()
    RIGHT = auto()
    DOWN = auto()
    LEFT = auto()


def erzeuge_apfel_koordinaten(schlange, aepfel):
    while True:
        koordinaten = (random.randint(0, 28), random.randint(0, 28))
        if not (koordinaten in schlange or koordinaten in aepfel):
            return koordinaten


def zeichne(screen, schlange, aepfel):
    screen.fill((0, 102, 0))

    for x, y in aepfel:
        pygame.draw.rect(
            screen,
            (255, 0, 0),
            (x * PARTIKEL, y * PARTIKEL, PARTIKEL, PARTIKEL),
            0,
        )

    farbe = (0, 150, 10)
    for x, y in schlange:
        pygame.draw.rect(
            screen, farbe, (x * PARTIKEL, y * PARTIKEL, 20, 20), 0
        )
        farbe = (47, 79, 79)


def main():
    try:
        pygame.init()

        clock = pygame.time.Clock()
        font = pygame.font.SysFont("arial black", 35)
        screen = pygame.display.set_mode([700, 700])

        start_button = Button(
            100, 200, pygame.image.load("WiederholenKnopf.png").convert_alpha()
        )
        exit_button = Button(
            450, 200, pygame.image.load("ExitKnopf.png").convert_alpha()
        )

        schlange = [(13, 13), (13, 14)]
        richtung = Direction.UP
        aepfel = []
        aepfel.append(erzeuge_apfel_koordinaten(schlange, aepfel))
        score = 0
        hat_apfel_gefressen = False
        ende = False

        while True:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    return

                if event.type == pygame.KEYDOWN:
                    if (
                        event.key == pygame.K_UP
                        and richtung is not Direction.DOWN
                    ):
                        richtung = Direction.UP
                    elif (
                        event.key == pygame.K_RIGHT
                        and richtung is not Direction.LEFT
                    ):
                        richtung = Direction.RIGHT
                    elif (
                        event.key == pygame.K_DOWN
                        and richtung is not Direction.UP
                    ):
                        richtung = Direction.DOWN
                    elif (
                        event.key == pygame.K_LEFT
                        and richtung is not Direction.RIGHT
                    ):
                        richtung = Direction.LEFT

            x, y = schlange[0]

            if hat_apfel_gefressen:
                hat_apfel_gefressen = False
            else:
                schlange.pop()

            if richtung is Direction.UP:
                head = (x, y - 1)
            elif richtung is Direction.RIGHT:
                head = (x + 1, y)
            elif richtung is Direction.DOWN:
                head = (x, y + 1)
            elif richtung is Direction.LEFT:
                head = (x - 1, y)
            else:
                assert False, f"unknown direction {richtung!r}"

            if head in schlange or not (0 <= x <= 27 and 0 <= y <= 27):
                ende = True

            schlange.insert(0, head)

            try:
                apfel_index = aepfel.index(head)
            except ValueError:
                pass
            else:
                aepfel.pop(apfel_index)
                hat_apfel_gefressen = True
                score += 1

            if not aepfel:
                aepfel.append(erzeuge_apfel_koordinaten(schlange, aepfel))

            if not ende:
                zeichne(screen, schlange, aepfel)
                screen.blit(
                    *create_text(f"Score: {score}", font, center=(350, 40))
                )
            else:
                screen.fill((255, 255, 255))
                if start_button.draw(screen):
                    pass  # TODO Hier sollte wohl irgendwas passieren.

                if exit_button.draw(screen):
                    print(f"Du hast {score} Punkte erreicht")
                    break

            pygame.display.update()
            clock.tick(7)

    finally:
        pygame.quit()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten