Die rasende Schlange: Vektoren in Pygame

Creative Coding
Python
Pygame
Nature of Code
Pygbag
Autor:in

Jörg Kantel

Veröffentlichungsdatum

25. Februar 2025

Vorbemerkung: Leider hatte ich gestern auf meinem Mac Mini ein Betriebssystem-Update gewagt (von Sonoma auf Sequoia). Seitdem gibt Python seltsame Fehlermeldungen heraus (die irgendetwas von IMKClient und IMKInputSession faseln). Laut den allwissenden Gurus von Stack Overflow ist das ein Bug in macOS Sequoia, der nicht nur in Python, sondern auch in anderen Programmiersprachen, die Fenster benutzen, auftritt. Apple war wohl bisher nicht in der Lage, diesen Bug zu fixen, obwohl mein Sequoia schon die beeindruckende Versionsnummer 15.3.1 aufweist. Wie auch immer, meine Python/Pygame- und Arcade-Skripte laufen unbeeindruckt von diesen Fehlermeldungen auf meinem Rechner weiter, nur Pygbag macht Probleme und kann keine im Browser lauffähigen Pygame-Skripte mehr erzeugen. Daher müsst Ihr heute mit trockenem Quellcode und langweiligen Screenshots vorliebnehmen.

Über das Wochenende hatte ich mich hingesetzt und an meinem vor wenigen Tagen angekündigten Vorhaben gearbeitet, wenigstens die Vektorkapitel 1 und 2 aus Daniel Shiffmans »The Nature of Code« nach Python/Pygame (CE) zu portieren. Und zumindest das erste Kapitel habe ich fast abgeschlossen. Als erstes hatte ich mir das »Example 1.7: Motion 101 (Velocity)« vorgeknöpft. Das war recht einfach, da es im Wesentlichen meinem »Bouncing Chicken« entsprach (ich habe das Bouncing) daher auch in diesem Skript (motion101.py) statt des Wrappings von Shiffman beibehalten:

# Motion 101 (Velocity)
import pygame
from random import randint
import sys

# Einige nützliche Konstanten
WIDTH = 800
HEIGHT = 450
TITLE = "Motion 101 (Velocity)"
FPS = 60  # Framerate

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

vec2 = pygame.Vector2

# 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.mover = Mover(self)

    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.mover.update()

    def draw(self):
        self.screen.fill(BG_COLOR)
        # Game drawings go here
        self.mover.draw()

        # Alle Änderungen auf den Bildschirm
        pygame.display.flip()

# ---------------------------------------------------------------------- #
class Mover():

    def __init__(self, _world):
        self.world = _world
        self.position = vec2((randint(20, WIDTH - 20), randint(20, HEIGHT - 20 )))
        self.velocity = vec2((randint(-5, 5), randint(-5, 5)))
        self.radius = 24

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

    def draw(self):
        pygame.draw.aacircle(self.world.screen, (255, 191, 0), self.position, self.radius)
        pygame.draw.aacircle(self.world.screen, (0, 0, 0), self.position, self.radius, 1)

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

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

    world.events()
    world.update()
    world.draw()

pygame.quit()
sys.exit(0)

Lediglich bei der Behandlung des Kreises gab es eine Änderung. Pygames Shapes haben keinen Rand, daher habe ich erst einen gelbgefüllten Kreis zeichnen lassen und danach eine Kreis, der nur aus einem schwarzen Rand besteht. Und ich habe Anti-Aliasing-Version aacircle() gewählt, die sieht einfach besser aus.

Und Shapes sind keine Sprites. Ich habe jedenfalls in der Dokumentation keinen Hinweis gefunden, daß auch Shapes ein umschließendes Rect besitzen. Daher mußte ich beim Bouncen doch wieder den Radius einbauen.

Als nächstes stand dann das »Example 1.8: Motion 101 (Velocity and Constant Acceleration)« an. Da mußte ich erst einmal ein wenig in der Pygame-Dokumentation stöbern, da Pygames Vektorklassen nicht die Methode limit() besitzen. Aber mit Vector2.clamp_magnitude_ip() hatte ich eine Methode gefunden, die – wenn auch nicht numerisch ein exakt gleiches – ein ähnliches Verhalten erzeugt:

# Motion 101 (Velocity and Constant Acceleration)
import pygame
import sys

# Einige nützliche Konstanten
WIDTH = 800
HEIGHT = 450
TITLE = "Motion 101 (Velocity and Constant Acceleration)"
FPS = 60  # Framerate

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

vec2 = pygame.Vector2

# 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.mover = Mover(self)

    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.mover.update()
        self.mover.check_borders()

    def draw(self):
        self.screen.fill(BG_COLOR)
        # Game drawings go here
        self.mover.draw()

        # Alle Änderungen auf den Bildschirm
        pygame.display.flip()

# ---------------------------------------------------------------------- #
class Mover():

    def __init__(self, _world):
        self.world = _world
        self.position = vec2(WIDTH//2, HEIGHT//2)
        self.velocity = vec2(0, 0)
        self.acceleration = vec2(-0.001, 0.01)
        self.radius = 24
        self.limit = 10

    def update(self):
        self.velocity += self.acceleration
        self.velocity.clamp_magnitude_ip(self.limit)
        self.position += self.velocity

    def check_borders(self):
        # Check borders
        if self.position.x > WIDTH:
            self.position.x = 0
        elif self.position.x < 0:
            self.position.x = WIDTH
        if self.position.y > HEIGHT:
            self.position.y = 0
        elif self.position.y < 0:
            self.position.y = HEIGHT

    def draw(self):
        pygame.draw.aacircle(self.world.screen, (255, 191, 0), self.position, self.radius)
        pygame.draw.aacircle(self.world.screen, (0, 0, 0), self.position, self.radius, 1)

        self.font = pygame.font.SysFont("American Typewriter", 16)
        self.vel_txt = self.font.render(str(self.velocity.magnitude()), True,
                                        (255, 255, 255))
        self.world.screen.blit(self.vel_txt, (20, 20))

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

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

    world.events()
    world.update()
    world.draw()

pygame.quit()
sys.exit(0)

Zusätzlich habe ich noch eine Textausgabe implementiert, die die Magnitude des Vektors ausgibt. Auch den Quellcode dieses Skriptes (motion101b.py) könnt Ihr auf GitHub finden.

Als letztes stand dann noch das »Example 1.9: Motion 101 (Velocity and Random Acceleration)« auf meinem Programm. Hier dachte ich eigentlich, daß das Fehlen der Methode random2D() mir etwas Kopfschmerzen verursachen würde, aber Pythons Methode Random.uniform() zusammen mit Vector2.update() und Vector2.normalize_ip() ließen mich schnell eine Lösung finden.

# Motion 101 (Velocity and Constant Acceleration)
import pygame
from random import random, uniform
import sys

# Einige nützliche Konstanten
WIDTH = 800
HEIGHT = 450
TITLE = "Motion 101 (Velocity and Random Acceleration)"
FPS = 60  # Framerate

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

vec2 = pygame.Vector2

# 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.mover = Mover(self)

    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.mover.update()
        self.mover.check_borders()

    def draw(self):
        self.screen.fill(BG_COLOR)
        # Game drawings go here
        self.mover.draw()

        # Alle Änderungen auf den Bildschirm
        pygame.display.flip()

# ---------------------------------------------------------------------- #
class Mover():

    def __init__(self, _world):
        self.world = _world
        self.position = vec2(WIDTH//2, HEIGHT//2)
        self.velocity = vec2(0, 0)
        self.acceleration = vec2(0, 0)
        self.radius = 24
        self.limit = 10

    def update(self):
        x = uniform(-1, 1)
        y = uniform(-1, 1)
        self.acceleration.update(x, y)
        self.acceleration.normalize_ip()
        self.acceleration *= random()*2
        self.velocity += self.acceleration
        self.velocity.clamp_magnitude_ip(self.limit)
        self.position += self.velocity

    def check_borders(self):
        # Check borders
        if self.position.x > WIDTH:
            self.position.x = 0
        elif self.position.x < 0:
            self.position.x = WIDTH
        if self.position.y > HEIGHT:
            self.position.y = 0
        elif self.position.y < 0:
            self.position.y = HEIGHT

    def draw(self):
        pygame.draw.aacircle(self.world.screen, (255, 191, 0), self.position, self.radius)
        pygame.draw.aacircle(self.world.screen, (0, 0, 0), self.position, self.radius, 1)

        self.font = pygame.font.SysFont("American Typewriter", 16)
        self.vel_txt = self.font.render(str(self.velocity.magnitude()), True,
                                        (255, 255, 255))
        self.world.screen.blit(self.vel_txt, (20, 20))

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

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

    world.events()
    world.update()
    world.draw()

pygame.quit()
sys.exit(0)

Das Programm heißt (Überraschung!) motion101c.py und ist natürlich ebenfalls auf meinem GitHub-Account zu finden.

Jetzt fehlen mir »nur« noch Beispiele zur Interaktivität und Ideen für ein paar Skripte, mit denen ich das Gelernte noch ein wenig aufhübschen kann. Dafür hätte ich aber gerne, das Pygbag wieder mit meinem Mac spielt, denn eigentlich machen Beispiele nur Spaß, wenn man sie auch im Browser vorführen kann.

Denn ich habe mich nur auf Pygame (CE) eingelassen, weil ich – inspiriert durch die Online-Präsentation von »The Nature of Code« – meine Ports und Skripte auch online präsentieren wollte. Denn sonst hätte ich für das Projekt »Python-Port von Nature of Code« ja auch bei Arcade (gefällt mir sogar mittlerweile ein wenig besser als Pygame) oder bei Py5 bleiben können. Und wer weiß, vielleicht ist James Schmitz mit seinen Bemühungen, Py5 via PyScript webtauglich zu machen, ja schneller. Das wäre dann ein echter Game Changer. Still digging!


Bild: Die rasende Python, erstellt mit OpenArt.ai. Prompt: »colored french comic style, a python with horn-rimmed glasses sits at the wheel of an open sports car and races at high speed through a dystopian city with crumbling skyscrapers, junk cars standing on the side of the road«. Modell: Flux (Pro), Style: None.