Auf ein neues: Pizzaplane in Pygame (Stage 1)

Pygame
Spieleprogrammierung
Autor:in

Jörg Kantel

Veröffentlichungsdatum

25. Dezember 2022

Nachdem ich mir ein paar der am Freitag vorgestellten Video-Tutorien reingezogen hatte, bekam ich unbändige Lust, meine eingerosteten Pygame-Kenntnisse frisch zu ölen und selber etwas mit diesem Spiele-Framework anzustellen. Was lag näher, als das nach meiner Abkehr von TigerJython verwaiste Projekt Pizzaplane wiederzubeleben und es in Pygame zu implementieren?

Ich halte ja Pygames Sprite()-Klasse für das große Alleinstellungsmerkmal, das Pygame von allen anderen Python-Frameworks abhebt. Um so verwunderter war ich, daß die Autoren der Freitag vorgestellten Pygame-Tutorien die Sprite-Klasse entweder ignorierten oder sich in der Implementierung verhedderten (zumindest in den Videos, die ich mir bis heute angeschaut hatte). Daher habe ich in meinem Programm darauf geachtet, die Sprites sauber zu implementieren.

Das beginnt mit dem endlos scrollenden Hintergrund, den ich ebenfalls als Unterklasse von pygame.sprite.Sprite implementiert habe. Das ist zwar ungewöhnlich – in Pygame-Prgrammen wird der Hintergrund normalerweise einfach als surface in das Fenster »geblittet«, aber meine (Pygame-) Philosophie ist: Alles, was sich bewegt, sind Sprites.

Ebenso ungewöhnlich – und vermutlich nicht notwendig – ist, daß ich den Hintergründen eine eigene Sprite-Group spendiert habe. Ich meine mich jedoch zu erinnern, daß man in Sprite-Groups nur sehr schwer die Reihenfolge bestimmen kann, in der die daran enthaltenen Sprites auf den Bildschirm gezeichnet werden, Deshalb sind die beiden Hintergrundbilder in einer eigenen Gruppe zusammengefaßt, die vor den eigentlichen Sprites gezeichnet wird1.

Ein übler Hack hingegen ist die Zeile,

os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (1320, 60)

mit der ich das Spielefenster auf den zweiten Monitor meine Home-Konfiguration beame. Dieser Hack scheint bis heute der einzige Weg zu sein, einem Pygame-Fenster eine Startposition zu verpassen. Wer keinen zweiten Monitor hat, sollte diese Zeile auskommentieren – ich wollte nur, daß Ihr mal seht, wie man so etwas bewerkstelligt2.

Ein wenig übervorsichtig ist auch die try: ... finally:-Klausel bei der Behandlung des Spieleabbruchs, aber Thonny ist in diesem Fall manchmal (aber nicht reproduzierbar) ein wenig zickig, wenn man die Hauptschleife nur mit keep_going = False beendet. So aber wird das Programm in jeder Situation sauber abgeschlossen.

Ansonsten setzt die Event-Behandlung in der Hauptschleife auf entsrechenden Tastendruck nur den Status plane.dir auf "Up" oder "DOWN" respektive – wenn eine Taste losgelassen wird – auf "NONE". Wie mit diesen Stati umzugehen ist, ist Aufgabe der Klasse Plane().

Überhaupt habe ich versucht, das Programm so klar wie möglich zu gestalten. Alle Updates finden in den Klassen statt, in der Hauptschleife werden eigentlich nur die Events abgefragt (und gegenenenfalls an die dafür zuständigen Klassen durchgereicht (im Moment ist das nur die Klasse Plane())) und die Ergebnisse auf den Monitor gezeichnet. Hier ist das komplette Programm, damit Ihr einen Überblick bekommt:

pizzaplane1.py
import pygame
from pygame.locals import *
import os
import sys

# Hier wird der Pfad zum Verzeichnis der Assets gesetzt
DATAPATH = os.path.join(os.getcwd(), "data")

# Konstanten deklarieren
WIDTH, HEIGHT = 720, 520
BG_WIDTH = 1664
TITLE = "Pizza Plane Stage 1"
FPS = 60
ANIM = 4 # Animation cycle
UPDOWN = 3

# Farben
BG_COLOR = (231, 229, 226) # Wüstenhimmel

# Objekte
class Background(pygame.sprite.Sprite):
    
    def __init__(self, _x, _y):
        pygame.sprite.Sprite.__init__(self)
        self.x = _x
        self.y = _y
        self.start_x = _x
        self.image = pygame.image.load(os.path.join(DATAPATH,
                            "desert.png"))
        self.rect = self.image.get_rect()
        
    def update(self):
        self.x -= 1
        # print(self.x)
        self.rect.x = self.x
        if self.x <= -BG_WIDTH:
            self.x = BG_WIDTH

class  Plane(pygame.sprite.Sprite):
    
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        # Load Images
        self.images = []
        for i in range (3):
            img = pygame.image.load(os.path.join(DATAPATH,
                         "planered_" + str(i) + ".png"))
            self.images.append(img)
        self.image = self.images[0]
        self.rect = self.image.get_rect()
        self.x = 75
        self.y = 250
        self.frame = 0
        self.ani = 20
        self.dir = "NONE"
    
    def update(self):
        if self.dir == "NONE":
            self.y += 0
        elif self.dir == "UP":
            if self.y > 20:
                self.y -= UPDOWN
        elif self.dir == "DOWN":
            if self.y < HEIGHT - 20:
                self.y += UPDOWN
        self.rect.center = (self.x, self.y)
        self.ani += 1
        if self.ani >= ANIM:
            self.ani = 0
            self.frame += 1
            if self.frame > 2:
                self.frame = 0
        self.image = self.images[self.frame]
        
# Pygame initialisieren
clock = pygame.time.Clock()
pygame.init()
# Ein übler Hack, um die Position des Fensters auf meinen
# zweiten Bildschirm zu setzen, aber er funktioniert …
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (1320, 60)
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption(TITLE)

# Sprite-Gruppe(n)
backs       = pygame.sprite.Group()
all_sprites = pygame.sprite.Group()

# Hintergrund
back1 = Background(0, 0)
back2 = Background(BG_WIDTH, 0)
backs.add(back1)
backs.add(back2)

# Der rote Flieger
plane = Plane()
all_sprites.add(plane)

keep_going = True
while keep_going:
    
    clock.tick(FPS)
    for event in pygame.event.get():
        if ((event.type == pygame.QUIT)
            or (event.type == pygame.KEYDOWN
                and event.key == pygame.K_ESCAPE)):
            print("Bye, Bye, Baby!")
            pygame.quit()
            try:
                sys.exit()
            finally:
                keep_going = False
                
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_UP:
                plane.dir = "UP"
            elif event.key == pygame.K_DOWN:
                plane.dir = "DOWN"
                
        if event.type == pygame.KEYUP:
            plane.dir = "NONE"

    backs.update()
    backs.draw(screen)
    all_sprites.update()
    all_sprites.draw(screen)
    pygame.display.update()
    pygame.display.flip()

Wie immer findet Ihr das Programm und alle Assets auch in meinem GitHub-Repositorium. Fortsetzung folgt …

Fußnoten

  1. Sollte mich meine Erinnerung trügen oder diese Eigenschaft mittlerweile verschwunden sein – diese wenigen zusätzlichen Zeilen schaden ja nicht.↩︎

  2. Ich muß allerdings zugeben, daß ich diesen Hack auch »nur« auf Stack Overflow gefunden habe.↩︎