Pygame, Spiel Pong, Probleme

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
Jannemann_14
User
Beiträge: 2
Registriert: Mittwoch 2. Februar 2022, 01:31

Ich probiere gerade ein Spiel zu programmieren aber komme einfach seit Tagen nicht mehr weiter. Es geht um das Spiel Pong, wobei ich aber nur alleine gegen den Ball spiele.

Problem 1: Der Code läuft nicht, weil er ein Problem bei if event.type == pygame.KEYDOWN:, hat.

Problem 2: Ich weiß nicht wie ich es schaffe, dass das Spiel endet, sobald der Ball die linke Wand berührt.

Problem 3: Ich weiß nicht wie ich es schaffe, dass der Ball nach jedem Berühren des Bat´s (Spielers/Schlägers) ein wenig schneller wird, um das Spiel spannender zu machen.

Problem 4: Ich möchte noch Klassen einfügen erstellen. Eine Klasse für den Schläger (Bat) und eine für den Ball. Ich habe dies schonmal probiert, mir aber dadurch den ganzen Code kaputt gemacht

Ich weiß, dass es recht viele Probleme sind, aber vielleicht ist hier ja eine begabte Person die Ahnung davon hat und mir weiterhelfen kann. Ich füge den kompletten Code nochmal unten an. Falls jemand auch über Discord leichter Helfen kann, weils dort mit Dateien schicken usw. leichter ist, kann er/sie auch gerne seinen Namen hier lassen, sodass ich ihn/sie adden kann.


Hier einmal mein kompletter bisheriger Code:

# Importieren der Pygame-Bibliothek
import pygame, sys, math



# Initialisierung von Pygame
pygame.init()


# Farben
white = (255, 255, 255)
red = (255, 0, 0)
black = (0, 0, 0)

# Festlegen der Displayeinstellungen
window_height = 600
window_broad = 600
screen = pygame.display.set_mode((window_height, window_broad))

# Spieltitel
pygame.display.set_caption("Pong")

# Bildschirm Aktualisierung
clock = pygame.time.Clock()

# Score
score = 0

# Definieren der Variablen/Konstanten vom Ball
ballpos_x = 300
ballpos_y = 300
ball_d = 20
ballmove_x = 2
ballmove_y = 2

# Definieren der Variablen/Konstanten vom Schläger
bat_x = 10
bat_y = 300
bat_speed = 0
bat_broad = 20
bat_height = 100

# Ab hier beginnt die Hauptschleife
active = True
while active:

# Überprüfen, ob Nutzer eine Aktion durchgeführt hat
for event in pygame.event.get():
if event.type == pygame.QUIT:
active = False



# Tasten für Spieler (Schläger)
for event in pygame.key.get_pressed():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_DOWN:
bat_speed += 8
if event.key == pygame.K_UP:
bat_speed -= 8
if event.type == pygame.KEYUP:
if event.key == pygame.K_DOWN:
bat_speed -= 8
if event.key == pygame.K_UP:
bat_speed += 8


bat_y += bat_speed



# Spielfeld löschen
screen.fill(black)

# Spielfeld/figuren zeichnen
# Ball zeichnen
ball = pygame.draw.ellipse(screen, white, [ballpos_x, ballpos_y, ball_d, ball_d])

# Schläger zeichnen
player1 = pygame.draw.rect(screen, white, [bat_x, bat_y, bat_broad, bat_height])


# Ballbewegungen
# Falls Ball an eine Wand kommt dreht er sich mit * -1 um
ballpos_x += ballmove_x
ballpos_y += ballmove_y

if ballpos_y > window_height - ball_d or ballpos_y < 0:
ballmove_y = ballmove_y * -1

if ballpos_x > window_height - ball_d or ballpos_x < 0:
ballmove_x = ballmove_x * -1

# Kollision vom Spieler (Schläger) und dem Ball
if player1.colliderect(ball):
ballmove_x = ballmove_x * -1
ballpos_x = 40
score += 1
bat_height -= 5


ausgabetext = "Score:" + str(score)
font = pygame.font.SysFont(None, 70)
text = font.render(ausgabetext, True, red)
screen.blit(text, [100, 10])

# Fenster aktualisieren
pygame.display.flip()

# Frame-Rate/Refresh in hz festlegen
clock.tick(120)

pygame.quit()
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Jannemann_14: Anmerkungen zum Quelltext:

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

Also so etwas wie ``# Importieren der Pygame-Bibliothek``, ``# Initialisierung von Pygame``, oder ``# farben`` kann und sollte man sich definitiv sparen, denn wer das ohne den Kommentar nicht am Code sehen/ablesen kann, dem würde auch der Kommentar nicht weiterhelfen, der müsste sowieso noch andere Informationsquellen für Grundlagen hinzuziehen.

Schlimmer als unnötige Kommentare sind falsche oder irreführende Kommentare, denn deren Sinn ist ja gerade Sachen klar zu stellen und Fragen zu beseitigen, nicht neue aufzuwerfen und zu verunsichern. ``# Bildschirm Aktualisierung`` beim erstellen eines `Clock`-Objektes macht keinen Sinn und verwirrt den Leser deshalb.

`math` und `sys` werden importiert, aber nirgends verwendet. Man würde auch eine ``import``-Anweisung pro Modul schreiben. Leseempfehlung: Style Guide for Python Code.

Der ganze Code sollte in (mindestens) einer Funktion stehen. Die Hauptfunktion heisst per Konvention `main()`.

Die Farben und die Fenstergrösse sind Konstanten. Konstanten werden per Konvention ziemlich am Anfang vom Modul definiert, nach She-Bang-Zeile, Moduldocstring, und den Importen. Und Konstantennamen werden per Konvention KOMPLETT_GROSS geschrieben.

`window_broad` sollte wohl eher `window_width` heissen, beziehunsweise `window_height` und `window_height` dann `window_width`, denn Du übergibst die Argumente in der falsche Reihenfolge, was nur deswegen nicht auffällt, weil beide den gleichen Wert haben. Wenn sie das nicht hätten wäre es auch problematisch das später im Code sowohl X- als auch Y-Koordinate gegen `window_height` geprüft werden.

Namen sollten keine kryptischen Abkürzungen enthalten. Es gibt ein paar allgemein bekannte Abkürzungen wie `x` und `y` für Koordinaten die jeder versteht, aber ansonsten sollte man sehr vorsichtig sein, was die Verständlichkeit von Abkürzungen angeht. Das `ball_d` für `ball_diameter` steht, ist nicht so offensichtlich wie man vielleicht denken mag. Es gibt Leser für die diese Abkürzung noch nicht gängig ist, und welche bei denen Geometrie schon seeeeehr lange her ist, und die das nicht mehr wissen. Zu dem spricht ja nicht jeder Deutsch oder eine Sprache in der das zufällig auch mit einem D wie Durchmesser anfängt, und die deshalb dort eine andere Abkürzung eher erwartet hätten.

ZwischenWortegehörteinUnterstrichweildassonstschwererzulesenist. Der erfüllt den Zweck eines Leerzeichens in normalem Fliesstext.

Konsistenz bei Namen ist auch nicht unwichtig. Warum ist da ein `pos` (also eigentlich `position`) bei den Ballkoordinaten, aber nicht bei den Schlägerkoordinaten? Warum heisst das was beim Ball `move` heisst, beim Schläger `speed`? Das sollte einheitlich sein.

Man nummeriert keine Namen. Entweder will man sich dann bessere Namen überlegen, oder gar keine Einzelwerte sondern eine Datenstruktur verwenden. Oft eine Liste. Im Falle von `player1` ist die Nummer aber einfach nur komplett sinnfrei. Zumal es ein bisschen inkonsistent ist den gezeichneten Ball `ball` zu nennen, den Schläger dann aber `player` statt `bat`.

Man sollte mit ”magischen” Zahlen sparsam sein. Wenn sich Werte von Konstanten ableiten lassen, sollte man das im Code auch tatsächlich tun. Beispielsweise die 300 für Ball- und Schlägerkoordinaten scheinen sich ja aus der Grösse der Anzeige abzuleiten. Dann sollte man das auch so in den Code schreiben, als Rechnung, damit durch Anpassung der Konstante für die Anzeigegrösse auch automatisch die Startpositionen von Ball und Schläger stimmen und man da nicht den gesamten Code durchgehen muss um zu schauen was man neben der Anzeigegrösse noch alles anpassen muss.

Und dann wären wir beim Fehler: `pygame.key.get_pressed()` gibt keine Ereignisobjekte zurück. Die behandlung der Tastenereignisse gehört mit in die erste Schleife über die Ereignisse.

Bei den Ereignissen sind ``if``-Bedingungen die sich gegenseitig ausschliessen, das ist also ein Fall für ein paar ``elif``-Anweisungen.

Es ist inkonsistent, dass die Schlägerposition *vor* dem Zeichnen und die Ballposition *nach* dem Zeichnen berechnet wird.

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.

Ob ein Wert (nicht) innerhalb eines bestimmten Bereichs liegt, lässt sich in Python durch verkettete Vergleichsoperatoren wie in der Mathematik üblich einfacher ausdrücken als durch eine ``and``- oder ``or``-Verknüpfung von zwei Teilbedingungen.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import pygame

WINDOW_SIZE = 600
BALL_DIAMETER = 20
BAT_WIDTH = 20
BAT_SPEED = 8

WHITE = (255, 255, 255)
RED = (255, 0, 0)
BLACK = (0, 0, 0)


def main():
    pygame.init()
    screen = pygame.display.set_mode((WINDOW_SIZE, WINDOW_SIZE))
    pygame.display.set_caption("Pong")
    clock = pygame.time.Clock()
    score = 0

    ball_x = ball_y = WINDOW_SIZE // 2
    ball_x_speed = ball_y_speed = 2

    bat_x = BAT_WIDTH // 2
    bat_y = WINDOW_SIZE // 2
    bat_height = WINDOW_SIZE // 3
    bat_speed = 0

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

            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_DOWN:
                    bat_speed += BAT_SPEED
                elif event.key == pygame.K_UP:
                    bat_speed -= BAT_SPEED

            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_DOWN:
                    bat_speed -= BAT_SPEED
                elif event.key == pygame.K_UP:
                    bat_speed += BAT_SPEED

        bat_y += bat_speed

        ball_x += ball_x_speed
        ball_y += ball_y_speed

        if not 0 <= ball_y < WINDOW_SIZE - BALL_DIAMETER:
            ball_y_speed = ball_y_speed * -1

        if not 0 <= ball_x < WINDOW_SIZE - BALL_DIAMETER:
            ball_x_speed = ball_x_speed * -1

        screen.fill(BLACK)
        ball = pygame.draw.ellipse(
            screen, WHITE, (ball_x, ball_y, BALL_DIAMETER, BALL_DIAMETER)
        )
        bat = pygame.draw.rect(
            screen, WHITE, (bat_x, bat_y, BAT_WIDTH, bat_height)
        )

        if bat.colliderect(ball):
            ball_x_speed *= -1
            ball_x = BAT_WIDTH * 2
            score += 1
            bat_height -= 5

        screen.blit(
            pygame.font.SysFont(None, int(WINDOW_SIZE * 0.1167)).render(
                f"Score: {score}", True, RED
            ),
            (WINDOW_SIZE // 3, WINDOW_SIZE // 30),
        )

        pygame.display.flip()
        clock.tick(120)

    pygame.quit()


if __name__ == "__main__":
    main()
Was hier noch unschön ist, ist die Kollisionserkennung in dem Code der den aktuellen Frame zeichnet. Das ist nötig weil die Rechteckdaten der Kollisionserkennung erst durch das Zeichnen entstehen. Die könnte man aber auch selbst als `pygame.Rect`-Objekte verwalten. Dann würde der Code auch etwas kürzer, weil man dann beispielsweise für Ball und Schläger nicht mehr zwei Variablen für die Position benötigt, denn die ist dann ja im `Rect`-Objekt schon enthalten.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Jannemann_14
User
Beiträge: 2
Registriert: Mittwoch 2. Februar 2022, 01:31

Hey, vielen vielen Dank schon einmal für die große Hilfe! Diese Antwort ist sehr ausführlich und hat mir extrem weiter geholfen. Wenn ich mir nun deinen Code anschaue, ist der viel ordentlicher und leichter zu verstehen als meiner. Eine Frage hätte ich dennoch. Wenn ich jetzt OOP einsetzen möchte müsste ich ja im Prinzip Vieles was in "main()" steht, raus reißen und dafür sorgen, dass Klassen ins Spiel kommen. Ich stelle mir das gerade so vor: Ich müsste erst einmal 2 Klassen benennen, zum Beispiel class ball und class bat. Dann müsste ich ja mit "self." arbeiten und die einzelnen Schritte definieren. Beispielsweise hätte ich dann: def __init__(self, x, y, speed, height) und so weiter. Müsste ich dann nicht alle Funktionen die zugehörig zur Klasse sind, also im Prinzip alles was etwas mit dem bat zutun hat, darunter schreiben? Natürlich dann immer mit "self." davor? Aber dann wäre ja fast die ganze main Schleife leer, weil ja dann alles darüber in den Klassen steht, richtig? Oder habe ich wieder einen Denkfehler.
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Genau so waere das - der Code wird sauber so organisiert, dass er in den Klassen steht, statt in einem grossen Klumpatsch in der main-Funktion.
Antworten