image image


image

Tutorial: Alien Invaders mit Python und Pygame (Stage 1)

Nachdem ich Anfang des Monats einen Basic Side Scroller objektorientiert in Pygame Zero programmiert hatte und dabei ein wenig über die Einschränkungen gestolpert bin, die Pygame Zero wegen des Fokusses auf Lernumgebung besitzt, wollte ich daraus ausbrechen und so kam mir die Idee, ein komplettes Spiel als Fingerübung mal direkt in Pygame zu programmieren. Dabei sollte der Schwerpunkt auch hier komplett auf objektorientierter Programmierung liegen und so habe ich vermutlich ein paar Objekte mehr erstellt, als eigentlich nötig. 🤓

Die Idee zu diesem Spiel ist als eine Kombination aus zwei verschiedenen Quellen entstanden: Zum einem wurde es von dem Video-Tutorial zu einem Shooter (Shmup) aus der Pygame-Playlist von KidsCanCode inspiriert, zum anderen war das Projekt Alien Invasion aus dem Buch Python Crash Course von Eric Matthes (Seite 233 bis Seite 317) Ideengeber.

Im ersten Teil dieser Reihe möchte ich einfach nur die Grundlagen schaffen und das Raumschiff des Spielers auf den Bildschirm bringen, so wie es der Screenshot oben zeigt.

Dazu habe ich einen Projektordner angelegt, der zum einen die Dateien main.py, settings.py und sprites.py enthält. Außerdem gibt es noch einen Ordner images für die Bilder und eventuell kommt später noch ein Ordner sounds für Geräusche hinzu (ich habe als Großraumbüro-geschädigter Mensch ein zwiespältiges Verhältnis zu Geräuschen).

Am einfachsten ist die Datei settings.py zu füllen. Sie enthält einfach nur ein paar Konstanten und Variablen für das Spiel, gemäß meinen selbstgewählten Vorgaben natürlich als Klasse:

class Settings(object):
    
    def __init__(self):
        self.WIDTH = 900
        self.HEIGHT = 600
        self.TITLE = "Alien Invaders"
        self.bg_color = (0, 80, 125)
        
        self.ship_speed_factor = 2.5
        
        self.FPS = 60

Sie ist momentan noch sehr übersichtlich, aber im weiteren Verlauf dieser Tutorial-Reihe wird dort noch einiges hinzukommen.

Die Datei main.py ist – wie der Name schon vermuten läßt – das Hauptprogramm. Als erstes werden dort ein paar notwendige Importe vorgenommen:

import pygame as pg
from settings import Settings
from sprites import Player
import os

Der Import von os wird vor allen Dingen benötigt, um betriebssystemunabhängig die Pfadangaben zusammenzubasteln, wie es die nächsten beiden Zeilen zeigen:

file_path = os.path.dirname(os.path.abspath(__file__))
image_path = os.path.join(file_path, "images")

Dieses Konstrukt, speziell os.path.join(), sorgt dafür, daß die Verzeichnistrenner korrekt eingesetzt werden, unter Linux und MacOS der Slash (/) und unter Windows der Backslash (\).

Danach habe ich mit

s = Settings()

eine globale Instanz der Klasse Settings erzeugt, die ihren Inhalt so dem Programm zur Verfügung stellt, ohne den Namensraum zu verschmutzen.

Das komplette Spiel habe ich in der Klasse Game gekapselt. Sie stellt die Pygame-üblichen Funktionen für die Hauptschleife eines Spiels zur Verfügung: watch_for_events(), update() und draw(), die wiederum in der Funktion run() gekapselt sind. Die Methode run() wird nach der Initialisierung von der Methode new() aufgerufen. Das ermöglicht es in einem späteren Stadium, daß das Spiel nach Beendigung von einem Start- oder Game-Over-Bildschirm erneut aufgerufen werden kann. Folgerichtig gibt es zwei Flags, einmal keep_going für den gesamten Spielverlauf und einmal playing für das einzelne Spiel.

Diese Konstruktion macht die Hauptschleife des Spiels zu einem Vierzeiler:

g = Game()

while g.keep_going:
    g.new()
    g.update()

Die komplette Spiellogik wurde in die Klasse Game verlagert, die als erstes initialisiert werden muß:

class Game(object):
    
    def __init__(self):
        pg.init()
        self.screen = pg.display.set_mode((s.WIDTH, s.HEIGHT))
        pg.display.set_caption(s.TITLE)
        self.background = pg.image.load(os.path.join(image_path, "background.gif")).convert()
        self.background_rect = self.background.get_rect()
        self.clock = pg.time.Clock()
        self.keep_going = True

In »normalen« Pygame-Tutorien findet man diese Zeilen in der Regel nicht in einem Konstruktor gekapselt, sondern offen im Hauptprogramm. Pygame wird initialisiert, das Spiele-Fenster bekommt Höhe und Weite sowie einen Titel zugewisen, danach wird das Hintergrundbild vorgeladen, die Uhr initialisiert und der Flag keep_going auf Truegesetzt,

Das Hintergrundbild hatte ich schon einmal verwendet, es entstammt aus einem freien Set (CC-BY-3.0) von Jacob Zinman-Jeanes. Es ist etwas größer als benötigt, Ihr könnt daher mit der Weite des Bildschirms noch ein wenig spielen.

Die Methode new() initialisiert momentan einesprite.Group und fügt das Raumschiff des Spielers dieser Sprite-Group hinzu:

    def new(self):
        self.all_sprites = pg.sprite.Group()
        self.player = Player()
        self.all_sprites.add(self.player)
        self.run()

Die Klasse Player werde ich weiter unten behandeln, wenn ich die Datei sprites.py bespreche.

Sprite-Groups sind ein wichtiger Bestandteil von Pygame. Sie optimieren und vereinfachen die Behandlung einzelner Sprites. Daher sollte man jede Instanz von pygame.sprite.Sprite in mindestens eine Sprite-Group einfügen.

Als letztes wird dann die run()-Methode aufgerufen:

    def run(self):
        self.playing = True
        self.clock.tick(s.FPS)
        while self.playing:
            self.watch_for_events()
            self.update()
            self.draw()

Solange, wie gespielt wird, setzt sie die Framerate, überwacht die Events, erledigt die Updates und zeichnet schließlich das Ergebnis auf den Bildschirm.

Die Methode watch_for_events() macht eigentlich (noch) nicht viel, ist aber dennoch die umfangreichste von allen:

    def watch_for_events(self):
        for event in pg.event.get():
            if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE):
                if self.playing:
                    self.playing = False
                self.keep_going = False
            elif event.type == pg.KEYDOWN:
                if event.key == pg.K_RIGHT:
                    self.player.moving_right = True
                elif event.key == pg.K_LEFT:
                    self.player.moving_left = True
            elif event.type == pg.KEYUP:
                if event.key == pg.K_RIGHT:
                    self.player.moving_right = False
                elif event.key == pg.K_LEFT:
                    self.player.moving_left = False

Zuerst wird überprüft, ob der Spieler den Schließknopf angeklickt oder die Escape-Taste gedrückt hat. Dann wird nicht nur das aktuelle Spiel beendet (playing = False), sondern auch die Spielumgebung komplett verlassen (keep_going = False).

Danach werden die Pfeiltasten rechts und links daraufhin überprüft, ob sie gedrückt oder losgelassen wurden. Im ersten Fall wird entweder player.moving_right oder player.moving_left auf True, im zweiten Falle auf False gesetzt. Wie die Klasse Player mit diesen Flags umgeht, ist dort definiert.

Die update()- und die draw()-Methoden sind einfach und kurz:

    def update(self):
        self.all_sprites.update()
    
    def draw(self):
        self.screen.fill(s.bg_color)
        self.screen.blit(self.background, self.background_rect)
        self.all_sprites.draw(self.screen)
        pg.display.flip()

Beim derzeitigen Stand des Spieles wird die Sprite-Group all_sprites auf Updates überprüft. Und dann wird das Hintergrundbild in den Double Buffer gezeichnet und anschließen alle Sprites der Sprite-Group all_sprites. Last but not least wird der Inhalt des Double Buffers mit pygame.display.flip() in das Bildschirmfenster »geblittet«.

Nun bleibt mir nur noch übrig die Datei sprites.py vorzustellen. Sie sieht so aus:

import pygame as pg
from settings import Settings
vec = pg.math.Vector2
import os

file_path = os.path.dirname(os.path.abspath(__file__))
image_path = os.path.join(file_path, "images")

s = Settings()

class Player(pg.sprite.Sprite):
    
    def __init__(self):
        pg.sprite.Sprite.__init__(self)
        self.image = pg.image.load(os.path.join(image_path, "playerShip1_red.png")).convert_alpha()
        self.image = pg.transform.scale(self.image, (50, 38))
        self.rect = self.image.get_rect()
        self.rect.centerx = s.WIDTH/2
        self.rect.bottom = s.HEIGHT - 20
        self.center = float(self.rect.centerx)
        self.moving_right = False
        self.moving_left = False
    
    def update(self):
        if self.moving_right and self.rect.right < s.WIDTH:
            self.center += s.ship_speed_factor
        if self.moving_left and self.rect.left > 0:
            self.center -= s.ship_speed_factor
        self.rect.centerx = self.center

Die ersten 10 Zeilen vor der Klassendefinition sind nahezu identisch mit den ersten Zeilen des Hauptprogramms. Lediglich die Klasse pygame.math.Vector2 für die Behandlung zweidimensionaler Vektoren habe ich prophylaktisch schon einmal importiert, sie wird aber erst später benötigt.

Wie alle Sprites in Pygame ist der Spieler eine Unterklasse von pygame.sprite.Sprite, dessen Konstruktor daher im Konstruktor von Player als erstes aufgerufen werden muß. Danach wird das Bild des Raumschiffs geladen, Es entstammt wieder dem freien (CC0 1.0 Universal), schier unerschöpflichen Fundus von Kenney.nl, aus dem ich im weiteren Verlauf dieser Tutorialreihe noch einige Bilder entnehmen werde.

Grundsätzlich sollte man in Pygame jedes geladene Bild entweder mit convert() (wie bei dem Hintergrundbild) oder mit convert_alpha() (bei Bildern mit Transparenz) in ein Format überführen, mit dem Pygame schneller umgehen kann. Andernfalls kann sich das Programm merklich verlangsamen.

Das Bild des Raumschiffs war mir etwas zu groß geraten. Natürlich hätte ich es mit der Bildverarbeitung meines Vertrauens verkleinern können, aber warum soll ich mir diese Arbeit machen, wenn ich sie mit pygame.transform.scale() Pygame überlassen kann?

Da das Raumschiff sich nicht unbedingt mit Pixel-Koordinaten (Integer-Werten) bewegt findet die Berechnung der Koordinaten in einer Fließkomma-Varaiblen (center) statt, die erst nach Ende der Berechnungen wieder in Pixel-Koordinaten umgerechnet wird.

Die Zeilen

        if self.moving_right and self.rect.right < s.WIDTH:
            self.center += s.ship_speed_factor
        if self.moving_left and self.rect.left > 0:
            self.center -= s.ship_speed_factor

sind eine recht elegante Methode, den Spielraum des Raumschiffes in jeweils einer einzigen Zeile auf die Fensterkoordinaten zu begrenzen.

Für diejenigen unter Euch da draußen, die das Spiel in seinem derzeitigen Zustand nachprogrammieren wollen, hier noch einmal das vollständige Hauptprogramm (die Dateien settings.py und sprites.py hatte ich oben im Text ja schon jeweils vollständig wiedergegeben):

import pygame as pg
from settings import Settings
from sprites import Player
import os

file_path = os.path.dirname(os.path.abspath(__file__))
image_path = os.path.join(file_path, "images")

s = Settings()

class Game(object):
    
    def __init__(self):
        pg.init()
        self.screen = pg.display.set_mode((s.WIDTH, s.HEIGHT))
        pg.display.set_caption(s.TITLE)
        self.background = pg.image.load(os.path.join(image_path, "background.gif")).convert()
        self.background_rect = self.background.get_rect()
        self.clock = pg.time.Clock()
        self.keep_going = True
    
    def new(self):
        self.all_sprites = pg.sprite.Group()
        self.player = Player()
        self.all_sprites.add(self.player)
        self.run()
    
    def run(self):
        self.playing = True
        self.clock.tick(s.FPS)
        while self.playing:
            self.watch_for_events()
            self.update()
            self.draw()
        
    def watch_for_events(self):
        for event in pg.event.get():
            if event.type == pg.QUIT or (event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE):
                if self.playing:
                    self.playing = False
                self.keep_going = False
            elif event.type == pg.KEYDOWN:
                if event.key == pg.K_RIGHT:
                    self.player.moving_right = True
                elif event.key == pg.K_LEFT:
                    self.player.moving_left = True
            elif event.type == pg.KEYUP:
                if event.key == pg.K_RIGHT:
                    self.player.moving_right = False
                elif event.key == pg.K_LEFT:
                    self.player.moving_left = False
    
    def update(self):
        self.all_sprites.update()
    
    def draw(self):
        self.screen.fill(s.bg_color)
        self.screen.blit(self.background, self.background_rect)
        self.all_sprites.draw(self.screen)
        pg.display.flip()

g = Game()

while g.keep_going:
    g.new()
    g.update()

print("I did it, Babe!")
pg.quit()

Momentan passiert ja noch nicht viel, der Spieler kann gerade einmal das Raumschiff mit den Pfeiltasten hin und her bewegen und dafür scheinen die insgesamt etwa 110 Zeilen Quellcode etwas viel zu sein, aber ich hoffe, hier ein Grundstruktur gelegt zu haben, die das Programm im weiteren Fortschritt übersichtlich und lesbar hält.

Den derzeitigen Stand des Quellcodes wie auch alle Bilder könnt Ihr meinem GitLab-Repositorium entnehmen.

image


(Kommentieren) 

image image



Über …

Der Schockwellenreiter ist seit dem 24. April 2000 das Weblog digitale Kritzelheft von Jörg Kantel (Neuköllner, EDV-Leiter Rentner, Autor, Netzaktivist und Hundesportler — Reihenfolge rein zufällig). Hier steht, was mir gefällt. Wem es nicht gefällt, der braucht ja nicht mitzulesen. Wer aber mitliest, ist herzlich willkommen und eingeladen, mitzudiskutieren!

Alle eigenen Inhalte des Schockwellenreiters stehen unter einer Creative-Commons-Lizenz, jedoch können fremde Inhalte (speziell Videos, Photos und sonstige Bilder) unter einer anderen Lizenz stehen.

Der Besuch dieser Webseite wird aktuell von der Piwik Webanalyse erfaßt. Hier können Sie der Erfassung widersprechen.

Diese Seite verwendet keine Cookies. Warum auch? Was allerdings die iframes von Amazon, YouTube und Co. machen, entzieht sich meiner Kenntnis.


Werbung


Werbung


image  image  image
image  image  image


image