Wo ist unser Vektor, Viktor? Teil2: Vektoren in Pygame

Spieleprogrammierung
Python
Pygame
Nature of Code
PyCharm
Pygbag
Autor:in

Jörg Kantel

Veröffentlichungsdatum

17. Februar 2025

Der Gravitar hat mich angefixt. Nachdem ich gestern (mehr oder weniger) erfolgreich die Python Arcade Bibliothek mit Vektoren verheiratet hatte, mußte ich heute auf seine Anregung hin unbedingt noch testen, wie das denn mit Pygame (genauer: Pygame CE) funktioniert.

Nicht, daß ich mit Arcade unzufrieden wäre, aber mir fehlte einfach die Möglichkeit, die Ergebnisse auch im Web und damit auf diesen Seiten zu präsentieren, wie es eben recht einfach mit Pygame CE und Pygbag geht. Also habe ich erst einmal eine Version des sich den Kopf an den Fensterrändern anschlagenden Kükens in Pygame ohne Vektorunterstützung implementiert (bouncingball_no_vector.py):

# Bouncing Chicken with No Vectors
import asyncio
import pygame
import os, sys

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

# Einige nützliche Konstanten
WIDTH = 800
HEIGHT = 450
CHICKEN_SIZE = 48
TITLE = "Bouncing Chicken with No Vectors (Pygame Version)"
FPS = 60  # Framerate

# Farben
BG_COLOR = 59, 122, 87, 255  # Billardtisch-Grün

# Klassen
# ---------------------------------------------------------------------- #
## Class GameWorld
class GameWorld:

    def __init__(self):
        # Pygame und das Fenster initialisieren
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption(TITLE)

        self.clock = pygame.time.Clock()
        self.keep_going = True

    def reset(self):
        # Neustart oder Status zurücksetzen
        # Hier werden alle Elemente der GameWorld initialisiert
        self.all_sprites = pygame.sprite.Group()
        # Load Assets
        self.chicken_im = pygame.image.load(os.path.join(DATAPATH, "chick.png")).convert_alpha()
        self.chicken_im = pygame.transform.scale(self.chicken_im, (CHICKEN_SIZE, CHICKEN_SIZE))
        chicken = Chicken(self)
        self.all_sprites.add(chicken)

    def events(self):
        # Poll for events
        for event in pygame.event.get():
            if ((event.type == pygame.QUIT) or
                (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE)):
                self.keep_going = False

    def update(self):
        self.all_sprites.update()

    def draw(self):
        self.screen.fill(BG_COLOR)
        # Game drawings go here
        self.all_sprites.draw(self.screen)
        # Alle Änderungen auf den Bildschirm
        pygame.display.flip()

# ---------------------------------------------------------------------- #
## Class Chicken
class Chicken(pygame.sprite.Sprite):

    def __init__(self, _world):
        super().__init__()
        self.game_world = _world
        self.image = self.game_world.chicken_im
        self.rect = self.image.get_rect()
        self.rect.x, self.rect.y = WIDTH / 2, HEIGHT / 2
        self.radius = CHICKEN_SIZE
        self.x_speed = 2.5
        self.y_speed = 2

    def update(self):
        self.rect.x += self.x_speed
        self.rect.y += self.y_speed
        if self.rect.x > WIDTH - self.radius  or self.rect.x < 0:
            self.x_speed *= -1
        if (self.rect.y > HEIGHT - self.radius or self.rect.y < 0):
            self.y_speed *= -1

# ---------------------------------------------------------------------- #
# ## Hauptprogramm
world = GameWorld()
world.reset()

# Hauptschleife
async def main():
    while world.keep_going:
        # Framerate festsetzen
        world.clock.tick(FPS)

        world.events()
        world.update()
        world.draw()
        await asyncio.sleep(0)  # Very important, and keep it 0

    pygame.quit()
    sys.exit(0)

asyncio.run(main())

Ich wollte damit mich selber an mein Vorhaben »Pygame objektorientiert erinnern und eine Blaupause schaffen, mit der ich zukünftig objektorientierte Pygame-Programme schreiben kann, die sowohl standalone als auch mit Pygbag im Web funktionieren. Also so eine Art »write once, run everywhere« 🥸

Nachdem mir dies gelungen war, war die Erweiterung um eine Vektorenunterstützung nur noch eine Frage von wenigen Änderungen (bouncingball_vectors.py):

# Bouncing Chicken with Vectors
import asyncio
import pygame
import pygame.math as math
import os, sys

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

# Einige nützliche Konstanten
WIDTH = 800
HEIGHT = 450
CHICKEN_SIZE = 48
TITLE = "Bouncing Chicken with Vectors (Pygame Version)"
FPS = 60  # Framerate

# Farben
BG_COLOR = 59, 122, 87, 255  # Billardtisch-Grün

# Klassen
# ---------------------------------------------------------------------- #
## Class GameWorld
class GameWorld:

    def __init__(self):
        # Pygame und das Fenster initialisieren
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption(TITLE)

        self.clock = pygame.time.Clock()
        self.keep_going = True

    def reset(self):
        # Neustart oder Status zurücksetzen
        # Hier werden alle Elemente der GameWorld initialisiert
        self.all_sprites = pygame.sprite.Group()
        # Load Assets
        self.chicken_im = pygame.image.load(os.path.join(DATAPATH, "chick.png")).convert_alpha()
        self.chicken_im = pygame.transform.scale(self.chicken_im, (CHICKEN_SIZE, CHICKEN_SIZE))
        chicken = Chicken(self)
        self.all_sprites.add(chicken)

    def events(self):
        # Poll for events
        for event in pygame.event.get():
            if ((event.type == pygame.QUIT) or
                (event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE)):
                self.keep_going = False

    def update(self):
        self.all_sprites.update()

    def draw(self):
        self.screen.fill(BG_COLOR)
        # Game drawings go here
        self.all_sprites.draw(self.screen)
        # Alle Änderungen auf den Bildschirm
        pygame.display.flip()

# ---------------------------------------------------------------------- #
## Class Chicken
class Chicken(pygame.sprite.Sprite):

    def __init__(self, _world):
        super().__init__()
        self.game_world = _world
        self.image = self.game_world.chicken_im
        self.rect = self.image.get_rect()
        self.position = math.Vector2(100, 100)
        self.rect.x, self.rect.y = self.position
        self.radius = CHICKEN_SIZE
        self.velocity = math.Vector2(2.5, 2)

    def update(self):
        self.rect.x += self.velocity.x
        self.rect.y += self.velocity.y
        if self.rect.x > WIDTH - self.radius  or self.rect.x < 0:
            self.velocity.x *= -1
        if self.rect.y > HEIGHT - self.radius or self.rect.y < 0:
            self.velocity.y *= -1

# ---------------------------------------------------------------------- #
# ## Hauptprogramm
world = GameWorld()
world.reset()

# Hauptschleife
async def main():
    while world.keep_going:
        # Framerate festsetzen
        world.clock.tick(FPS)

        world.events()
        world.update()
        world.draw()
        await asyncio.sleep(0)  # Very important, and keep it 0

    pygame.quit()
    sys.exit(0)

asyncio.run(main())

Nach dem Import von pygame.math mußten die Änderungen nur noch in der Klasse Chicken() vorgenommen werden (ein Vorteil des strikt objektorientierten Designs).

Natürlich mußte ich dieses Ergebnis meiner heutigen Versuche auch gleich in diese Seiten einbinden. Mittlerweile weiß ich ja, wie dies geht.

Und jetzt stehe ich wieder vor der Frage, ob ich meine derzeitige Lektüre von Daniel Shiffmans neuer, verbesserter und erweiterter Auflage von »The Nature of Code« nicht mit Pygame-Programmen begleiten soll. Denn das schöne an der Online-Version des Buches ist ja, daß Shiffmans P5.js-Skripte auf den Seiten leben. Und das könnte ich mit Pygame und Pygbag ebenfalls erreichen. Pygame im Browser ist einfach geil!

Eine Bemerkung zum Schluß noch: Am Screenshot im Bannerbild oben können aufmerksame Leserinnen und Leser erkennen, daß ich seit heute eine neue Python-IDE benutze: Ich bin zu PyCharm CE, der freien (Apache-Lizenz) Version von JetBrains Python-Entwicklungsumgebung, gewechselt. Ich hatte langsam Angst, daß ich mein Visual Studio Code mit all den Erweiterungen, die ich mittlerweile im Einsatz habe, einfach überfrachte und wollte daher etwas nutzen, was nur für Python im Einsatz ist und VS Code entlastet. Und Thonny ist zwar ein schönes Spielzeug für Anfänger und Gelegenheitscoder, genügt aber meinen Ansprüchen nicht (mehr). Bisher hat sich PyCharm trotz seines Funktionsumfangs als wenig störrisch gezeigt. Schauen wir mal, was daraus noch wird. Still digging!