image image


Tutorial: »Shoot ‘em up« in Pygame Zero (Stage 2)

Nachdem ich im ersten Teil dieses Tutorials ein Raumschiff mit flackerndem Schweif vor einem majestätisch dahingleitenden Sternenhimmel implementiert hatte, möchte ich nun in diesem Teil feindliche Ufos erscheinen lassen und das Raumschiff bewaffnen, damit es sich dieser Feinde entledigen kann. Dazu mußte ich zuerst einmal zwei neue Klassen implementieren. Einmal die Klasse Bullet für die Torpedos, die der Spieler auf die Ufos abfeuern kann:

class Bullet(Actor):
    
    def __init__(self, image):
        Actor.__init__(self, image)
        self.state = "ready"
        self.pos = NIRWANA
        self.speed = 20

    def update(self):
        if keyboard.space:
            if self.state == "ready" and player.state == "ready":
                self.pos = player.pos
                self.state = "fire"
        if self.state == "fire":
            self.x += self.speed
        if self.x >= WIDTH:
            self.state = "ready"
            self.pos = NIRWANA

Die Klasse Bullet hat zwei Stati, ready und fire. Damit soll verhindert werden, daß der Spieler ganze Salven abfeuert, denn nur im Status ready kann der Spieler feuern und das Torpedo gelangt nur in diesem Status, wenn es entweder ein Ufo trifft (dazu weiter unten mehr) oder es über den rechten Bildschirmrand heraus verschwunden ist.

NIRWANA ist übrigens eine konstantes Tupel, dessen Position weit außerhalb des sichtbaren Bildschirmfensters liegt. Und abgefeuert wird das Torpedo mit der Leertaste.

Gleichzeitig wollte ich auch verhindern, daß der Spieler feuern kann, wenn er sich bewegt. Denn es sieht in meinen Augen sehr unrealistische aus, wenn sich die Kugel nicht direkt vom Raumschiff entfernt, sondern daß Raumschiff sich schon wieder weiter nach oben oder nach unten bewegt hat. Dazu mußte ich auch der Klasse Ship zwei Stati verpassen, ready und moving. Die Klasse Ship bekommt initial den Status ready zugewiesen, doch das kann sich in der update()-Methode ändern:

    def update(self):
        if keyboard.up:
            self.y -= 1
            self.state = "moving"
        elif keyboard.down:
            self.y += 1
            self.state = "moving"
        else:
            self.state = "ready"

Nun zu den feindlichen Ufos. Ihre Bildchen stammen ebenfalls von Jacob Zinman-Jeanes aus dem gleichen freien (CC-BY-3.0) Sprite-Satz auf Gamedevtuts+, dem ich auch schon die Bildchen der Rakete und den Sternenhimmel entnommen habe. Sie besitzen sechs Animationsphasen und dementsprechen ist die Animation ein wenig aufwendiger als bei der Rakete des Spielers:

class Enemy(Actor):
    
    def __init__(self, image, pos):
        Actor.__init__(self, image)
        self.pos = pos
        self.start_pos = pos
        self.frame = 0
        self.speed = 1
    
    def update(self):
        self.y += self.speed
        if self.y >= HEIGHT - 20 or self.y <= 20:
            self.x -= 60
            self.speed *= -1

    def make_animation(self):
        if self.frame <= 5:
            self.image = "enemy1"
        elif self.frame <= 10:
            self.image = "enemy2"
        elif self.frame <= 15:
            self.image = "enemy3"
        elif self.frame <= 20:
            self.image = "enemy4"
        elif self.frame <= 25:
            self.image = "enemy5"
        elif self.frame <= 30:
            self.image = "enemy6"
        if self.frame >= 30:
            self.frame = 0
        self.frame += 1
    
    def hit(self, bullet):
        a = self.x - bullet.x
        b = self.y - bullet.y
        d = math.sqrt((a**2) + (b**2))
        if d < 20:
            bullet.state = "ready"
            bullet.pos = NIRWANA
            self.pos = NIRWANA
            player.hitpoints += 1
            print("Score " + str(player.hitpoints))

Die hit()-Methode habe ich in diese Klasse gelegt, weil ich es weniger aufwendig zu implmentieren fand, wenn die Ufos selber registrieren, wenn sie getroffen sind und sich dann auch selber ins NIRWANA katapultieren. 🤪

image

Das hat natürlich zur Folge, daß ich im dritten Teil dieser kleinen Reihe, wenn auch das Schiff des Spielers von den Ufos getroffen werden kann, die Klasse Ship ebenfalls eine eigene hit()-Methode benötigt. Doch darüber mache ich mir Gedanken, wenn ich die dritte Version dieses Spiels implementiere.

Wie man an der hit()-Methode erkennt, habe ich auch eine rudimentäre Punktvergabe implementiert, die zur Zeit jedoch nur den Punktestand auf der Konsole und nicht im Pygame Zero-Fenster ausgibt. Auch dies habe ich auf Stage 3 verschoben.

Bei der von mir gewählten Größe des Bildschirmfensters passen neun feindliche Ufos in einer Reihe auf dem Bildschirm, wenn ich ihnen oben und unten noch etwas Luft lasse:

NO_ENEMYS = 9

enemy_pos = []
for i in range(NO_ENEMYS):
    enemy_pos.append((WIDTH + 20, (i+1)*40))

enemys = []
for i in range(len(enemy_pos)):
    enemys.append(Enemy("enemy1", enemy_pos[i]))

Die Initialisierung erfolgt in zwei Schleifen. Die erste Schleife füllt eine Liste mit den Positionen der Ufos (als Tupel) und in der zweiten Schleife werden die feindlichen Ufos auf ihren Positionen erzeugt.

So, und nun wie gewohnt der komplette Quellcode:

import pgzrun
import math
import sys

WIDTH = 680
HEIGHT = 400
TITLE = "Shmup 2"
TILEWIDTH = 640
NIRWANA = -2000, -2000
NO_ENEMYS = 9

class Layer(Actor):
    
    def __init__(self, image, xpos):
        Actor.__init__(self, image)
        self.left = xpos
        self.scroll_speed = 0.3

    def update(self):
        self.left -= self.scroll_speed
        if self.right < 0:
            self.left = TILEWIDTH

class Ship(Actor):
    
    def __init__(self, image, pos):
        Actor.__init__(self, image, pos)
        self.state = "ready"
        self.pos = pos
        self.frame = 0
        self.hitpoints = 0
        
    def make_animation(self):
        if self.frame <= 5:
            self.image = "ship1"
        elif self.frame <= 10:
            self.image = "ship2"
        elif self.frame <= 15:
            self.image = "ship3"
        elif self.frame <= 20:
            self.image = "ship4"
        if self.frame >= 20:
            self.frame = 0
        self.frame += 1
        
    def update(self):
        if keyboard.up:
            self.y -= 1
            self.state = "moving"
        elif keyboard.down:
            self.y += 1
            self.state = "moving"
        else:
            self.state = "ready"
        if self.hitpoints == NO_ENEMYS:
            print("You Won!")
            sys.exit()
    
    def check_edges(self):
        if self.y >= HEIGHT - 16:
            self.y = HEIGHT - 16
        elif self.y <= 16:
            self.y = 16

class Bullet(Actor):
    
    def __init__(self, image):
        Actor.__init__(self, image)
        self.state = "ready"
        self.pos = NIRWANA
        self.speed = 20

    def update(self):
        if keyboard.space:
            if self.state == "ready" and player.state == "ready":
                self.pos = player.pos
                self.state = "fire"
        if self.state == "fire":
            self.x += self.speed
        if self.x >= WIDTH:
            self.state = "ready"
            self.pos = NIRWANA

class Enemy(Actor):
    
    def __init__(self, image, pos):
        Actor.__init__(self, image)
        self.pos = pos
        self.start_pos = pos
        self.frame = 0
        self.speed = 1
    
    def update(self):
        self.y += self.speed
        if self.y >= HEIGHT - 20 or self.y <= 20:
            self.x -= 60
            self.speed *= -1

    def make_animation(self):
        if self.frame <= 5:
            self.image = "enemy1"
        elif self.frame <= 10:
            self.image = "enemy2"
        elif self.frame <= 15:
            self.image = "enemy3"
        elif self.frame <= 20:
            self.image = "enemy4"
        elif self.frame <= 25:
            self.image = "enemy5"
        elif self.frame <= 30:
            self.image = "enemy6"
        if self.frame >= 30:
            self.frame = 0
        self.frame += 1
    
    def hit(self, bullet):
        a = self.x - bullet.x
        b = self.y - bullet.y
        d = math.sqrt((a**2) + (b**2))
        if d < 20:
            bullet.state = "ready"
            bullet.pos = NIRWANA
            self.pos = NIRWANA
            player.hitpoints += 1
            print("Score " + str(player.hitpoints))
            
bg1 = Layer("bg", 0)
bg2 = Layer("bg", 1188)
layers = [bg1, bg2]

enemy_pos = []
for i in range(NO_ENEMYS):
    enemy_pos.append((WIDTH + 20, (i+1)*40))

enemys = []
for i in range(len(enemy_pos)):
    enemys.append(Enemy("enemy1", enemy_pos[i]))
    
player = Ship("ship1", (60, HEIGHT/2))
bullet = Bullet("laserred")

def draw():
    for layer in layers:
        layer.draw()
    for enemy in enemys:
        enemy.draw()
    bullet.draw()
    player.draw()

def update():
    for layer in layers:
        layer.update()
    for enemy in enemys:
        enemy.make_animation()
        enemy.update()
        enemy.hit(bullet)
    bullet.update() 
    player.make_animation()
    player.update()
    player.check_edges()

pgzrun.go()

Ihr findet den Quellcode und sämtliche Assets natürlich auch wieder in meinem GitHub-Repositorium. Die Datei dieser Fassung heißt shmup2.py. Wenn Ihr diese Datei und den images-Ordner herunterladet und Pygame Zero (größer/gleich Version 1.2) auf Eurem Rechner installiert habt, dürfte das Programm sofort lauffähig sein.

In dieser Fassung kann der Spieler das Spiel nicht verlieren. Dazu müßte man die Anzahl der feindlichen Ufos erhöhen, die dann auch in mehreren Reihen auftreten könnten und/oder die Geschwindigkeit der Ufos steigern. Oder die Ufos schießen zurück und der Spieler muß ihren Torpedos ausweichen. Das programmiere ich vielleicht in einer letzten, endgültigen Fassung des Spieles. Still digging!


(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

Diese Spalte wurde absichtlich leergelassen!


Werbung


image  image  image
image  image  image


image