Pong Klon eines Anfängers

Hier werden alle anderen GUI-Toolkits sowie Spezial-Toolkits wie Spiele-Engines behandelt.
Antworten
MGS_Freak
User
Beiträge: 35
Registriert: Donnerstag 13. Januar 2011, 13:50
Wohnort: Schweiz

Hi zusammen

Hab mir aus Spass am Lernen von python und pygame einen kleinen Pong Klon geschrieben :).

Code: Alles auswählen

import sys
import random
import pygame

RESOLUTION = (800, 600)

RIGHT = "RIGHT"
LEFT = "LEFT"
UP = "UP"
DOWN = "DOWN"

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

class Ball(pygame.sprite.Sprite):
    def __init__(self, color):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((15, 15))
        self.image.fill(color)
        self.initial_position = (RESOLUTION[0] / 2, RESOLUTION[1] / 2)
        self.rect = self.image.get_rect()
        self.rect.center = self.initial_position
        self.x_speed = 10
        self.y_speed = 6
        self.direction = None
        self.blit_me = True
        
    def update(self):
        self.move()
        self.check_x_border()
        self.check_y_border()
        if not self.blit_me:
            self.reset()
  
    def set_random_direction(self):
        if random.randint(0, 1):
            self.direction = RIGHT
        else:
            self.direction = LEFT

    def move(self):
        self.rect.x += self.x_speed
        self.rect.y += self.y_speed
        
    def check_x_border(self):
        if self.rect.left <= 0 or self.rect.right >= RESOLUTION[0]:
            self.blit_me = False

    def check_y_border(self):
        if self.rect.top <= 0 or self.rect.bottom >= RESOLUTION[1]:
            self.y_speed *= -1         
            
    def reset(self):

        self.blit_me  = True
        self.rect.center = self.initial_position
        self.direction = self.set_random_direction()

class Paddle(pygame.sprite.Sprite):
    def __init__(self, number, color):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((15, 150))
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.speed = 0
        self.max_speed = 10
        self.number = number
        rand = 10

        if self.number == 1:
            self.initial_position = self.rect.center = (self.rect.width / 2 + rand, RESOLUTION[1] / 2)

        elif self.number == 2:
            self.initial_position = self.rect.center = (RESOLUTION[0] - rand - self.rect.width / 2, RESOLUTION[1] / 2)

    def set_speed(self, direction):
        if direction == UP:
            self.speed = self.max_speed * -1
        elif direction == DOWN:
            self.speed = self.max_speed

        elif direction == None:
            self.speed = 0
     
    def move(self):
        if self.rect.top <= 0:
            self.rect.top = 0
        elif self.rect.bottom >= RESOLUTION[1]:
            self.rect.bottom = RESOLUTION[1]           
        self.rect.y += self.speed
          
    def update(self):
        self.move()

    def reset(self):
        self.rect.center = self.initial_position        

class Game:

    def __init__(self):
        pygame.init()
        pygame.mouse.set_visible(0)
        self.screen = pygame.display.set_mode(RESOLUTION)
        self.screen.fill(BLACK)
        self.timer = pygame.time.Clock()
        self.ball = Ball(GREEN)
        self.ball.set_random_direction()
        self.paddles = (Paddle(1, RED),
                        Paddle(2, BLUE))

    def blit_ball(self):
        self.screen.blit(self.ball.image, self.ball.rect)

    def blit_paddles(self):
        for paddle in self.paddles:
            self.screen.blit(paddle.image, paddle.rect)

    def check_collision(self):
        if self.ball.rect.colliderect(self.paddles[0].rect) or self.ball.rect.colliderect(self.paddles[1].rect):
            self.ball.x_speed *= -1

    def run(self):
        while True:
            self.screen.fill(BLACK)
            for event in pygame.event.get():
                _quit = event.type == pygame.QUIT
                w = event.type == pygame.KEYDOWN and event.key == pygame.K_w
                notw = event.type == pygame.KEYUP and event.key == pygame.K_w
                s = event.type == pygame.KEYDOWN and event.key == pygame.K_s
                nots = event.type == pygame.KEYUP and event.key == pygame.K_s
                up = event.type == pygame.KEYDOWN and event.key == pygame.K_UP
                notup = event.type == pygame.KEYUP and event.key == pygame.K_UP
                down = event.type == pygame.KEYDOWN and event.key == pygame.K_DOWN
                notdown = event.type == pygame.KEYUP and event.key == pygame.K_DOWN
                esc = event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE
                if _quit or esc:
                    pygame.quit()
                    sys.exit()
                if w:
                    self.paddles[0].set_speed(UP)
                elif s:
                    self.paddles[0].set_speed(DOWN)
                elif notw or nots:
                    self.paddles[0].set_speed(None)
                if up:
                    self.paddles[1].set_speed(UP)
                elif down:
                    self.paddles[1].set_speed(DOWN)
                elif notup or notdown:
                    self.paddles[1].set_speed(None)
            self.check_collision()
            for paddle in self.paddles:
                paddle.update()
            self.ball.update()
            self.blit_ball()
            self.blit_paddles()
            pygame.display.update()            
            self.timer.tick(60)

if __name__ == "__main__":
    pong = Game()
    pong.run()
Da ich denke das noch viel Freiraum zur Verbesserung besteht freue mich auf Kritik - denke gerade an das Input-Handling und vor allem bei den Klassen bin ich unsicher...

Grüsse
MGS_Freak
Bolitho
User
Beiträge: 219
Registriert: Donnerstag 21. Juli 2011, 07:01
Wohnort: Stade / Hamburg
Kontaktdaten:

Guten Morgen,

Daumen hoch! Sieht sehr gut aus. Bin auch Anfänger und kann nachvollziehen, wie viel Mühe und Arbeit drin steckt, bis man soweit ist. Klasse!

Weiterentwicklungsmöglichkeiten:
- Score der Spieler berechnen, ausgeben und das Spiel bei x beenden
- bei der aktuellen Geschwindigkeit läuft der Ball sehr unruhig. Da fällt mir aber auch keine andere Lösung ein, als das Spiel langsamer zu machen
- Ein Paddle vom Computer übernehmen lassen, sprich 1 - Spieler - Spiel
- Ball "rund" machen
- Musik und Soundeffekte einbinden

Wie gesagt, gut gemacht!

Viele Grüße,
Bolitho
MGS_Freak
User
Beiträge: 35
Registriert: Donnerstag 13. Januar 2011, 13:50
Wohnort: Schweiz

Guten Morgen zurück,

Whoa, Danke :)!

- Score hab ich mir auch schon gedacht, wär cool dies mit einer visuellen Anzeige gell ;) Hm, pygame fonts...realisierbar denke ich
- Stimmt, habs nochmal laufen lassen, habs auch nur mit Ball langsamer machen geschafft :( vielleicht hat ja jemand noch ne bessere Idee
- Ja das wärs natürlich, glaub da werd ich ein Weilchen dran zu knabbern haben xD -> Klasseidee auf jeden Fall
- Haha, ja nee is klar, das musste einfach kommen ;), werd ich sicher anschauen, ob ich's dann einbaue...steh halt auf den ultra-retro-look ;)
- jep, sehr gute Idee! Wird definitiv auf todo Liste genommen

Nochmals Danke für Deine Inputs und das Durchlesen :)

Grüsse
MGS_Freak
BlackJack

Ein paar Kleinigkeiten: Der Style Guide for Python Code empfiehlt zwei Leerzeilen zwischen Klassen und Funktionen auf Modulebene.

Die beiden Zeilen Hauptprogramm könnte man noch in eine Funktion stecken, dann hat man das `pong` nicht mehr im Modulnamensraum.

`RESOLUTION` wird nur einmal direkt verwendet, sonst immer mit einem Indexzugriff auf eines der beiden Elemente, da hätte ich noch zusätzlich `WIDTH` und `HEIGHT` definiert. Wobei die Konstanten für meinen Geschmack zu oft benutzt werden. Wenn Ball und Schläger die Spielfeldgrenzen kennen müssen, würde ich die als Argument beim erstellen der Objekte übergeben. Am besten in Form eines `rect`-Objektes, also in der `Game.__init__()` mit ``self.screen.get_rect()`` ermitteln. `Rect`-Objekte haben praktische Attribute mit denen man sich einige Rechnungen sparen kann und das Programm damit auch ein bisschen einfacher lesbar bekommt. Den Spielfeldmittelpunkt in `Ball.__init__()` kann man dann zum Beispiel so schreiben: ``self.initial_position = field_rect.center``. Am Ende kann man wahrscheinlich auch auf `WIDTH` und `HEIGHT` verzichten.

Das `Ball.blit_me`-Attribut wird nur innerhalb der Klasse zwischen Methoden verwendet die sich direkt aufrufen, es müsste also kein Attribut sein, weil man es auch als Argument und Rückgabewert herumreichen kann. Wenn `check_x_border()` das als Antwort zurück gibt, dann ist der Name der Methode auch sinnvoller denn die meisten Programmierer erwarten von einer Methode die `check_*` heisst eine Antwort ob die Prüfung erfolgreich war oder nicht. Und sie erwarten auch *nur* eine Prüfung und nicht das so eine Methode etwas am Zustand des Objekts zur Folge hat. Die beiden Methoden könnten etwas spezifischer benannt werden. Statt die Position der Grenzen (x/y) zu beschreiben, könnte man hier die *Bedeutung* in den Namen stecken. Zum Beispiel `is_out_of_bounds()` für x und `is_within_side_lines()` für y.

`Ball.set_random_direction()` kann man mit `random.choice()` einfacher formulieren. Allerdings ist das `self.direction`-Attribut keine gute Idee denn das ist redundante Information. Die Richtung wird ja auch/schon durch das Vorzeichen vom `x_speed`-Attribut bestimmt. Also wenn man das Attribut nicht komplett weg lassen könnte weil es sowieso nicht verwendet wird, sollte man sich da etwas überlegen. So kann das Attribut und die `set_random_direction()` erst einmal komplett weg.

Die Platzierung beim `Paddle.__init__()` sollte nicht indirekt über eine Nummer passieren. Dem Schläger kann seine Position auf dem Spielfeld, also ob Spieler 1 oder 2 eigentlich egal sein. Dieses Wissen braucht er nicht. Auch hier könnte man die Berechnungen mit dem Spielfeld-`Rect` vereinfachen. Auch so wie es jetzt gelöst ist, muss `number` kein Attribut des Objekts sein.

`Paddle.reset()` wird nicht verwendet und damit auch `Paddle.initial_position` nicht.

Die `set_speed()`-Methode müsste dem Argument nach eigentlich `set_direction()` heissen.

Wenn das Python 2 ist sollte man bei Klassen die sonst von nichts erben, explizit von `object` erben, damit Dinge wie Properties richtig funktionieren.

Für `Game.check_collision()` gilt das weiter oben gesagte zu `check_*`-Methoden und was Programmierer üblicherweise erwarten. Mit `Rect.collidelist()` lässt sich die Methode einfacher formulieren. Irgendeine Form von `SpriteGroup` würde das ganze auch vereinfachen. Vom objektorientierten Entwurf her könnte man hier übrigens auch argumentieren das `Game` zu tief in `Ball` und `Paddle` eingreift und man den Test eher in eine dieser Klassen verlegen sollte.

Bei den Namen nach dem Muster `x`/`notx` in `Game.run()` wird *ein* Zustand jeweils in zwei Namen kodiert. Das finde ich etwas unübersichtlich.

Wenn man sich die Verwendung von `Game` anschaut könnte man eventuell auch in Frage stellen ob das in der Form eine Klasse sein muss.

Ball- und Schlägergrösse sollte man aus der Spielfeldgrösse berechnen oder als Konstanten definieren, damit man es einfach(er) anpassen kann.

Einiges davon umgesetzt (und kaum getestet :-)):

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
import pygame
 
RESOLUTION = 800, 600
 
RIGHT, LEFT, UP, DOWN = 'RIGHT', 'LEFT', 'UP', 'DOWN'
 
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)


class Ball(pygame.sprite.Sprite):
    def __init__(self, field_rect, color):
        pygame.sprite.Sprite.__init__(self)
        self.field_rect = field_rect
        self.image = pygame.Surface((15, 15))
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.rect.center = self.field_rect.center
        self.x_speed = 10
        self.y_speed = 6
       
    @property
    def is_out_of_bounds(self):
        return (
            self.rect.left <= self.field_rect.left
            or self.rect.right >= self.field_rect.right
        )
 
    @property
    def is_within_sidelines(self):
        return (
            self.rect.top > self.field_rect.top
            and self.rect.bottom < self.field_rect.bottom
        )

    def move(self):
        self.rect = self.rect.move(self.x_speed, self.y_speed)
           
    def update(self):
        self.move()
        if self.is_out_of_bounds:
            self.reset()
        if not self.is_within_sidelines:
            self.y_speed = -self.y_speed
     
    def reset(self):
        self.rect.center = self.field_rect.center


class Paddle(pygame.sprite.Sprite):
    def __init__(self, field_rect, margin, color):
        pygame.sprite.Sprite.__init__(self)
        self.field_rect = field_rect
        self.image = pygame.Surface((15, 150))
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.speed = 0
        self.max_speed = 10
        self.rect.centery = self.field_rect.centery
        if margin < 0:
            self.rect.right = self.field_rect.right + margin
        else:
            self.rect.left = self.field_rect.left + margin
 
    def set_direction(self, direction):
        if direction == UP:
            self.speed = -self.max_speed
        elif direction == DOWN:
            self.speed = self.max_speed
        elif direction == None:
            self.speed = 0
     
    def move(self):
        self.rect.y += self.speed
        self.rect = self.rect.clamp(self.field_rect)

    update = move         


def main():
    pygame.init()
    pygame.mouse.set_visible(0)
    screen = pygame.display.set_mode(RESOLUTION)
    timer = pygame.time.Clock()
    field_rect = screen.get_rect()
    ball = Ball(field_rect, GREEN)
    paddles = [
        Paddle(field_rect, 10, RED), Paddle(field_rect, -10, BLUE)
    ]
    while True:
        screen.fill(BLACK)
        for event in pygame.event.get():
            if (
                event.type == pygame.QUIT
                or (
                    event.type == pygame.KEYDOWN
                    and event.key == pygame.K_ESCAPE
                )
            ):
                pygame.quit()
                return
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_w:
                    paddles[0].set_direction(UP)
                elif event.key == pygame.K_s:
                    paddles[0].set_direction(DOWN)
                elif event.key == pygame.K_UP:
                    paddles[1].set_direction(UP)
                elif event.key == pygame.K_DOWN:
                    paddles[1].set_direction(DOWN)
            elif event.type == pygame.KEYUP:
                if event.key in [pygame.K_w, pygame.K_s]:
                    paddles[0].set_direction(None)
                elif event.key in [pygame.K_UP, pygame.K_DOWN]:
                    paddles[1].set_direction(None)

        if ball.rect.collidelist([p.rect for p in paddles]) != -1:
            ball.x_speed = -ball.x_speed

        for paddle in paddles:
            paddle.update()
        ball.update()

        screen.blit(ball.image, ball.rect)
        for paddle in paddles:
            screen.blit(paddle.image, paddle.rect)

        pygame.display.update()            
        timer.tick(15)


if __name__ == '__main__':
    main()
MGS_Freak
User
Beiträge: 35
Registriert: Donnerstag 13. Januar 2011, 13:50
Wohnort: Schweiz

Hi BlackJack,

erneut nimmst Du Dir die Mühe, danke dafür. Diese "Kleinigkeiten" mögen für Dich welche sein, mir eröffnen sie immer eine neue Welt ;).
Um auf Dein Feedback einzugehen, Style Guide habe ich schon ein paar Mal angeschaut, leider kann mein Hirn nicht alles behalten und kleine Fehler schleichen sich ein...
Hmm ja klar stimmt, die Klasse ist überflüssig. Der Nutzen war wieder mit Klassen probiert ;).
Phu ich unterschätze rect Attribute offensichtlich immer noch :( Super Gedankeanstoss, merci! RESOLUTION kann man sich sparen, ist deshalb drin damit ich auf unterschiedlichen Geräten den Wert sofort ändern kann...der Mensch ist ein Faulheitstier :D!
Die check-Funktionen waren so eine Sache bei der ich einfach nicht wusste wie man es sonst umsetzen kann.
Zur Platzierung über eine Nummer, glaube da war ich verwirrt weil es 2 "Spieler" geben sollte.
*facepalm* jedesmal vergess ich zu schreiben das es python 2.7 mit pygame 1.9 ist :( wird ab sofort gemacht! solange ich Klassen nicht von sprite erben lasse habe ich mir das erben von object angewohnt, für den Hinweis bin ich allerdings immer dankbar.

Abschliessend ein grosses Dankeschön an Dich!
Habe Deine Inputs aufgenommen und versucht umzusetzen, nun läuft es perfekt :D! Werde noch eine Score-Anzeige einbauen und dann bei Interesse das Ergebnis wieder posten.

Grüsse
MGS_Freak
MGS_Freak
User
Beiträge: 35
Registriert: Donnerstag 13. Januar 2011, 13:50
Wohnort: Schweiz

Hallo allerseits

Hat etwas gedauert doch ich hab das Spiel mit einer Scoreanzeige pro Spieler versehen. Wie sauber das gelöst ist...bitte um Kritik :) Einrückungen werden noch korrigiert, sind zum Teil der Übersichts halber noch falsch

Code: Alles auswählen

import pygame

RESOLUTION = (800, 600)
FPS = 60
UP, DOWN = "UP", "DOWN"

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


class Ball(pygame.sprite.Sprite):
    def __init__(self, field_rect, color):
        pygame.sprite.Sprite.__init__(self)
        self.field_rect = field_rect
        self.image = pygame.Surface((15, 15))
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.rect.center = self.field_rect.center
        self.x_speed = 10
        self.y_speed = 6

    @property
    def is_out_left(self):
        return self.rect.left <= self.field_rect.left

    @property
    def is_out_right(self):
        return self.rect.right >= self.field_rect.right

    @property
    def is_within_sidelines(self):
        return (
            self.rect.top > self.field_rect.top
            and self.rect.bottom < self.field_rect.bottom
            )

    def move(self):
        self.rect = self.rect.move(self.x_speed, self.y_speed)

    def update(self):
        if not self.is_within_sidelines:
            self.y_speed = -self.y_speed

    def reset(self):
        self.rect.center = self.field_rect.center


class Paddle(pygame.sprite.Sprite):
    def __init__(self, field_rect, margin, color):
        pygame.sprite.Sprite.__init__(self)
        self.field_rect = field_rect
        self.image = pygame.Surface((15, 150))
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.speed = 0
        self.max_speed = 10
        self.rect.centery = self.field_rect.centery
        if margin < 0:
            self.rect.right = self.field_rect.right + margin
        else:
            self.rect.left = self.field_rect.left + margin

    def set_direction(self, direction):
        if direction == UP:
            self.speed = -self.max_speed
        elif direction == DOWN:
            self.speed = self.max_speed
        elif direction == None:
            self.speed = 0

    def move(self):
        self.rect.y += self.speed
        self.rect = self.rect.clamp(self.field_rect)

    update = move

class ScoreBoard(pygame.sprite.Sprite):
    def __init__(self, field_rect, side_margin, top_margin=20):
        self.field_rect = field_rect
        self.score = 0
        self.font = pygame.font.Font(None, 50)
        self.label = self.font.render(str(self.score), 1, WHITE)
        self.rect = self.label.get_rect()
        self.rect.top = self.field_rect.top + top_margin
        if side_margin < 0:
            self.rect.right = self.field_rect.right + side_margin
        else:
            self.rect.left = self.field_rect.left + side_margin

    def increase_score(self, step=1):
        self.score += step

    def update(self):
        self.label = self.font.render(str(self.score), 1, WHITE)           
            
            
def main():
    pygame.init()
    pygame.mouse.set_visible(0)
    screen = pygame.display.set_mode(RESOLUTION)
    timer = pygame.time.Clock()
    ball = Ball(screen.get_rect(), GREEN)
    paddles = [
        Paddle(screen.get_rect(), 10, RED), Paddle(screen.get_rect(), -10, BLUE)
        ]
    scores = [
        ScoreBoard(screen.get_rect(), 40), ScoreBoard(screen.get_rect(), -40)
        ]
    while True:
        screen.fill(BLACK)
        for event in pygame.event.get():
            if (
                event.type == pygame.QUIT
                or (
                    event.type == pygame.KEYDOWN
                    and event.key == pygame.K_ESCAPE)
            ):
                pygame.quit()
                return
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_w:
                    paddles[0].set_direction(UP)
                elif event.key == pygame.K_s:
                    paddles[0].set_direction(DOWN)
                elif event.key == pygame.K_UP:
                    paddles[1].set_direction(UP)
                elif event.key == pygame.K_DOWN:
                    paddles[1].set_direction(DOWN)
            elif event.type == pygame.KEYUP:
                if event.key in [pygame.K_w, pygame.K_s]:
                    paddles[0].set_direction(None)
                elif event.key in [pygame.K_UP, pygame.K_DOWN]:
                    paddles[1].set_direction(None)

        if ball.rect.collidelist([p.rect for p in paddles]) != -1:
            ball.x_speed = -ball.x_speed
        
        if ball.is_out_left:
            scores[1].increase_score()
            scores[1].update()
            ball.reset()
        if ball.is_out_right:
            scores[0].increase_score()
            scores[0].update()
            ball.reset()

        ball.move()
        ball.update()

        for paddle in paddles:
            paddle.update()

        screen.blit(ball.image, ball.rect)
        for paddle in paddles:
            screen.blit(paddle.image, paddle.rect)
        for score in scores:
            screen.blit(score.label, score.rect)

        pygame.display.update()
        timer.tick(FPS)


if __name__ == "__main__":
    main()
(python 2.7 mit pygame 1.9)

Danke & Grüsse
MGS_Freak
Antworten