image image


image

Tutorial: Alien Invaders mit Pygame Zero

Vor etwa einem Monat hatte ich mit einem Tutorial begonnen, das das Spiel »Alien Invaders« mit Pygame programmieren wollte. In der Zwischenzeit bin ich jedoch mit Pygame Zero vertrauter geworden und hatte festgestellt, daß einem Pygame Zero tatsächlich sehr viele Zeilen Quellcode erspart. Daher habe ich einen Cornerturn vollzogen und das Spiel in Pygame Zero neu programmiert.

Zuerst einmal die Fassung aus oben verlinktem Tutorial statt in Pygame in Pygame Zero:

import pgzrun

WIDTH = 600
HEIGHT = 600
TITLE = "Alien Invaders 1"

player_ship = Actor("playership")
player_ship.centerx = WIDTH/2
player_ship.bottom = HEIGHT - 20
player_speed = 5

def update():
    if keyboard.left:
        player_ship.centerx -= player_speed
        if player_ship.left <= 0:
            player_ship.left = 0
    elif keyboard.right:
        player_ship.centerx += player_speed
        if player_ship.right >= WIDTH:
            player_ship.right = WIDTH

def draw():
    screen.blit(images.background, (0, 0))
    player_ship.draw()

pgzrun.go()

Diese Version macht immer noch nicht mehr, als daß das Raumschiff des Spielers mit den Pfeiltasten hin und her bewegt werden kann, aber anstatt der 110 Zeilen Quellcode in der Pygame-Version kommt Pygame Zero mit 26 Zeilen Quellcode aus. Das halte ich schon für eine beachtliche Reduktion auf das Wesentliche.

Stage 2: Wir wollen schießen

Bevor die gefürchteten Aliens auftauchen, wollte ich den Spieler mit einer Waffe ausstatten, die es ihm erlaubt, sich gegen diese gefährlichen Gegner zu wehren. Dazu benötigte ich erst einmal einen weiteren Actor und einen Flag:

bullet = Actor("laserred16")
bullet_speed = 25
bullet_fire = False

Der Flag soll sicherstellen, daß imer nur eine Kugel im Spiel ist, denn den Spieler mit der Möglichkeit eines Dauerfeuers auszustatten, hielt ich gegenüber den Aliens für unfair. Das habe ich innerhalb der Update-Methode so realisiert:

    if not bullet_fire:
        bullet.bottom = player_ship.bottom
        bullet.centerx = player_ship.centerx
    if keyboard.SPACE:
        bullet_fire = True
    if bullet_fire:
        bullet.bottom -= bullet_speed
        if bullet.bottom <= 0:
            bullet_fire = False

Zu Beginn der update()-Methode muß der Flag bullet_fire natürlich als global deklariert werden.

In der draw()-Methode muß nur darauf geachtet werden, daß die Kugel vor dem Raumschiff gezeichnet wird, damit sie im passiven Zustand vom Raumschiff verdeckt wird:

def draw():
    screen.blit(images.background, (0, 0))
    bullet.draw()
    player_ship.draw()

Nun ist schon einiges an Funktionalität hinzugekommen, aber die Länge des Quellcodes ist mit 41 Zeilen immer noch überschaubar.

Stage 3: Ein Alien taucht auf

Bevor ich eine Horde von Aliens auf den Spieler loslassen wollte, wollte ich erst einmal mit einem Alien üben. Auch dieses muß natürlich erst initialisiert werden:

def reset_enemy():
    return (randint(25, WIDTH - 25), randint(-200, -50))

enemy = Actor("enemyred")
enemy.pos = reset_enemy()
enemy_speed = 5

score = 0
game_over = False

Da der Spieler nun auch verlieren kann, werden noch ein paar zusätzliche Flags und Variablen deklariert, die auch alle zu Beginn der update()-Methode als global vereinbart werden mußten.

Die Funktion reset_enemy() gibt eine zufällige Position oberhalb des Spielefensters zurück, die als Startposition des Gegners gilt. Dafür muß zu Beginn mit

from random import randint

die entsprechende Zufallsfunktion aus Pythons Standardbibliothek importiert werden.

Die Zeilen des Quellcodes, die das Alien-Schiff betreffen werden an den Schluß der update()-Methode angehängt:

    enemy.top += enemy_speed
    bullet_hit = bullet.colliderect(enemy)
    if bullet_hit:
        enemy.pos = reset_enemy()
        bullet.bottom = player_ship.bottom
        bullet.centerx = player_ship.centerx
        bullet_fire = False
        score += 1
    enemy_hit = enemy.colliderect(player_ship)
    if enemy_hit:
        game_over = True
        enemy.pos = reset_enemy()
        player_speed = enemy_speed = bullet_speed = 0
    if enemy.top >= HEIGHT:
        score -= 1
        enemy.pos = reset_enemy()
        if score <= 0:
            player_speed = enemy_speed = bullet_speed = score = 0
            game_over = True

Das Alien-Schiff bewegt sich auf den unteren Fensterrand zu und dann wird überprüft, ob es mit einer Kugel des Spielers kollidiert. Die Klasse Actor aus Pygame Zero erbt alle Methoden der Rect-Klasse aus Pygame, so auch die Methode colliderect(). Trifft also eine Kugel das feindliche Schiff, wird dieses wieder mit reset_enemy() in den Orbit zurückgeschossen, von wo aus es einen neuen Angriff starten kann. Die Kugel wiederum wird in den Zustand versetzt, der es dem Spieler erlaubt, erneut eine Kugel abzuschießen. Außerdem bekommt der Spieler einen Punkt gutgeschrieben.

Kollidiert dagegen das feindliche Schiff mit dem Raumschiff des Spielers wird alles zurück oder auf Null gesetzt und das Spiel ist zu Ende (game_over = True).

Das Spiel kann jedoch noch auf eine andere Art zu Ende gehen. Denn jedesmal, wenn ein Alien-Raumschiff dem Spieler entkommt – das heißt am unteren Fensterrand verschwindet – verliert der Spieler einen Punkt. Und wenn der Punktestand null oder weniger Punkte aufweist, ist das Spiel ebenfalls zu Ende.

Die draw()-Methode sieht immer noch sehr übersichtlich aus:

def draw():
    screen.blit(images.background, (0, 0))
    enemy.draw()
    bullet.draw()
    player_ship.draw()
    screen.draw.text("Score: " + str(score), (10, 10))
    if game_over:
        screen.draw.textbox("Game Over!", (95, 200, 400, 80), color = "red")

Neu sind eigentlich nur die beiden Aufrufe zum Zeichnen von Text auf den Bildschirm. Pygame Zero besitzt eine reichhaltige Text API, doch auch sie ist mit sinnvollen Default-Werten vorbelegt, so daß man erst einmal wenig dazu basteln muß – in diesem Fall nur die Größe und Farbe des »Game Over«-Textes.

Diese Fassung ist im Quellcode jetzt 75 Zeilen lang, auch das kann man wirklich nicht als übermäßig geschwätzig bezeichnen.

Stage 4: Viele, viele Aliens

Dank der bisherigen Vorarbeit ist eine Erweiterung auf viele Aliens nur noch ein Kinderspiel. Zuerst die Initialisierung als Liste:

num_enemies = 5
enemies = []
for _ in range(num_enemies):
    enemy = Actor("enemyred")
    enemy.pos = reset_enemy()
    enemies.append(enemy)
enemy_speed = 2

Ohne zu üben war das Spiel bei mir immer zu schnell verloren, daher habe ich mit enemy_speed = 2 die Geschwindigkeit der feindlichen Raumschiffe drastisch herabgesetzt. Die Leserinnen und Leser sind aufgefordert, hier mit eigenen Werten zu experimentieren.

Der update()-Methode mußte eigentlich nur ein

    for enemy in enemies:

vorangestellt, werden, alles nachfolgenden (neu eingerückten!) Zeilen funktionieren wie in Stage 3.

Das komplette (und vollständige) Programm sieht daher so aus:

import pgzrun
from random import randint

WIDTH = 600
HEIGHT = 600
TITLE = "Alien Invaders 4"

player_ship = Actor("playership")
player_ship.centerx = WIDTH/2
player_ship.bottom = HEIGHT - 20
player_speed = 5

bullet = Actor("laserred16")
bullet_speed = 25
bullet_fire = False

def reset_enemy():
    return (randint(25, WIDTH - 25), randint(-200, -50))

num_enemies = 5
enemies = []
for _ in range(num_enemies):
    enemy = Actor("enemyred")
    enemy.pos = reset_enemy()
    enemies.append(enemy)
enemy_speed = 2

score = 0
game_over = False

def update():
    global bullet_fire, score, enemy_speed, bullet_speed, player_speed, game_over
    if keyboard.left:
        player_ship.centerx -= player_speed
        if player_ship.left <= 0:
            player_ship.left = 0
    elif keyboard.right:
        player_ship.centerx += player_speed
        if player_ship.right >= WIDTH:
            player_ship.right = WIDTH
    if not bullet_fire:
        bullet.bottom = player_ship.bottom
        bullet.centerx = player_ship.centerx
    if keyboard.SPACE:
        bullet_fire = True
    if bullet_fire:
        bullet.bottom -= bullet_speed
        if bullet.bottom <= 0:
            bullet_fire = False
    for enemy in enemies:
        enemy.top += enemy_speed
        bullet_hit = bullet.colliderect(enemy)
        if bullet_hit:
            enemy.pos = reset_enemy()
            bullet.bottom = player_ship.bottom
            bullet.centerx = player_ship.centerx
            bullet_fire = False
            score += 1
        enemy_hit = enemy.colliderect(player_ship)
        if enemy_hit:
            game_over = True
            enemy.pos = reset_enemy()
            player_speed = enemy_speed = bullet_speed = 0
        if enemy.top >= HEIGHT:
            score -= 1
            enemy.pos = reset_enemy()
            if score <= 0:
                player_speed = enemy_speed = bullet_speed = score = 0
                game_over = True

def draw():
    screen.blit(images.background, (0, 0))
    for enemy in enemies:
        enemy.draw()
    bullet.draw()
    player_ship.draw()
    screen.draw.text("Score: " + str(score), (10, 10))
    if game_over:
        screen.draw.textbox("Game Over!", (95, 200, 400, 80), color = "red")

pgzrun.go()

Das ist ein funktionierendes Spiel in nur 81 Zeilen Python-Code, kürzer geht es wohl kaum. Diese Einfachheit motiviert, mich noch tiefer in Pygame Zero hineinzuknien. Schaun wir einfach mal, was mir noch einfällt …

Credits

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

Die Raumschiffe und die Laserkugel entstammen wieder dem freien (CC0 1.0 Universal), schier unerschöpflichen Fundus von Kenney.nl. Ich habe die Bildchen mit der Bildverarbeitung meines Vertrauens auf die notwenige Größe zurechtgestutzt.

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


(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