Die rasende Schlange: Vektoren in Pygame
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
undIMKInputSession
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.