image image


image

Pizza Plane Stage 4: Shooting the Killer Pizzas

So langsam nimmt mein kleines Spiel, in dem sich der Rote Baron in seinem Doppeldecker gegen die angreifenden Killer-Pizzen wehren muß, Form an. War in der letzten Version noch die einzige Verteidigungsmöglichkeit, den pösen Pizzen auszuweichen, kann nun der Doppeldecker zurückschießen.

Dafür muß für das Geschoß erst einmal eine neue Klasse erzeugt werden, die ich Missile() genannt habe:

class Missile(Actor):

    def __init__(self):
        Actor.__init__(self, True, os.path.join(DATAPATH, "missile.png"))
        self.speed = 5
                        
    def act(self):
        self.move(self.speed)
        if self.getX() >= WIDTH + 20:
            win.removeActor(self)

Dieses Geschoß kann erst einmal nur von ihrer Startposition aus mit ziemlich hoher Geschwindigkeit nach rechts bewegt werden, und wenn sie dann am rechten Fensterrand verschwindet, wird das Objekt auch einfach gelöscht. Hierfür stellt GameGrid die zur Spielewelt gehörende Methode removeActor() zur Verfügung.

Die Frage, welches Objekt die Missile erzeugt, ist eine philosophische: Wird sie in ihrer eigenen Klasse erzeugt oder gehört sie gar zur Spielewelt? Für alle Möglichkeiten gibt es vernünftige Begründungen. Ich habe mich entschieden, da die Player-Klasse Plane() sowieso schon die Tastatur abfragt, daß auch diese für die Erzeugung der Geschosse verantwortlich ist. Dafür hat die act()-Methode von Plane() erst einmal ein paar weitere Zeilen hinzugefügt bekommen:

        if isKeyPressed(32):    # SPACE
            self.fire()
        self.firecount -= 1

Die Methode fire() mußte ich dann natürlich in Plane() ebenfalls noch implementieren:

    def fire(self):
        if self.firecount < 0:
            missile = Missile()
            enemyList = win.getActors(Enemy)
            for enemy in enemyList:
                missile.addCollisionActor(enemy)
            win.addActor(missile, Location(self.getX() + 15, self.getY()))
            self.firecount = 15

Damit der Flieger nicht Raketen am laufenden Band verschießt, habe ich mit firecount einen weiteren Timer implementiert, der heweils nach einem Schuß auf 15 zurückgesetzt wird. Und nur, wenn firecount < 0 ist, kann der Rote Baron eine neue Rakete auf den Weg bringen. Jetzt dürfte klar sein, warum in der act()-Methode von Plane() vom Timer firecount immer eins abgezogen wird.

Die Startposition von missile ist die x-Position des Fliegers mit einem Offset von 15 (damit startet sie genau vor dem Propeller) und natürlich seine y-Position.

In diesem Stadium kann der Rote Baron zwar Raketen verschießen, aber die feindlichen Pizzen zeigen sich davon noch recht unbeeindruckt – die Geschosse gehen einfach durch sie hindurch. Es fehlt noch eine Kollisionserkennung und die Behandlung des Killisionsereignisses. Doch auch dafür stellt GameGrid komfortable Methoden zur Verfügung. Mit

            enemyList = win.getActors(Enemy)
            for enemy in enemyList:
                missile.addCollisionActor(enemy)

in der act()-Methode des Fliegers erstellen wir erst einmal eine Lieste der möglichen Kollisionsobjekte für missile. Diese behandelt sie dann in ihrer eigenen Klasse in der Methode collide(actor1, actor2) (die Namen sind vorgegeben, sollten also nicht verändert werden – außer, man hat Seltsames vor und weiß, was man tut):

    def collide(self, actor1, actor2):
        xpos = actor2.getX()
        ypos = actor2.getY()
        win.removeActor(self)
        win.removeActor(actor2)
        hit = Explosion()
        win.addActor(hit, Location(xpos, ypos))
        return 0

Die x- und y-Position des Gegners benötige ich für die Explosion, die ich in der collide()-Methode auslöse. Ansonsten entferne ich in dieser Methode nur beide Objekte. Zu beachten ist, daß die collide()-Methode immer mit return 0 abgeschlossen werden muß.

Jetzt fehlt als Höhepunkt nur noch die Explosion. Auch sie wird über eine eigene Klasse realisiert, die wir in der collide()-Methode von Missile() ja schon instanziert haben. Sie ist recht kurz und knackig:

class Explosion(Actor):
    
    def __init__(self):
        Actor.__init__(self, True, os.path.join(DATAPATH, "explosion.png"))
        self.timer = 5
        
    def act(self):
        self.timer -= 1
        if self.timer <= 0:
            win.removeActor(self)

Sie zeigt das Bild einer Explosion und initialisiert ebenfalls einen Timer. Und wenn dieser abgelaufen ist, wird das Objekt schlicht und einfach entfernt.

Das ist im Großen und Ganzen der aktuelle Stand des Pizza Plane Game. Ich habe noch an ein paar Parametern geschraubt und and einigen Stellen das Konstrukt setX(getX()) und setY(getY()) durch setLocation(Location(x, y)) ersetzt. Macht den Quellcode etwas schlanker, ändert aber nichts an der Funktionalität.

Hier ist der komplette Quellcode in der aktuellen Fassung (Stage 4):

# Pizza Plane Stage 4: Klasse »Missile and Explosion«
# Background Image: »PWL« (https://opengameart.org/content/seamless-desert-background-in-parts)
# Aeroplane: Tappy Plane, Kenney (https://www.kenney.nl/assets/tappy-plane)
# Pizza: Twitter Twemoji (https://twemoji.twitter.com/)
# Missile and Explosion: TigerJython Sprite-Bibliothek 
# (https://www.programmierkonzepte.ch/index.php?inhalt_links=navigation.inc.php&inhalt_mitte=sprites.html)

from gamegrid import *
from random import randint
import os

# -------------------------- Konstanten ---------------------------- #
WIDTH = 720
HEIGHT = 480

DATAPATH = os.path.join(os.getcwd(), "data")
BGWIDTH    = 1067   # Breite des Hintergrundbildes
BGWIDTH2   = 533    # BGWIDTH//2 (abgerundet)
NO_ENEMIES = 20     # Anzahl der Feinde

# -------------------------- Background ---------------------------- #
class Background(Actor):
    
    def __init__(self):
        Actor.__init__(self, True, os.path.join(DATAPATH, "desert.png"))
        self.speed = -1
        
    def act(self):
        self.move(self.speed)
        if self.getX() < -BGWIDTH2:
            self.setX(BGWIDTH + BGWIDTH2)

# -------------------------- Player Class ---------------------------- #
class Plane(Actor):
    
    def __init__(self):
        Actor.__init__(self, True, os.path.join(DATAPATH, "planered.png"), 3)
        self.timer = 5
        self.firecount = 0
        self.updown = 2
    
    def fire(self):
        if self.firecount < 0:
            missile = Missile()
            enemyList = win.getActors(Enemy)
            for enemy in enemyList:
                missile.addCollisionActor(enemy)
            win.addActor(missile, Location(self.getX() + 15, self.getY()))
            self.firecount = 15

    def act(self):
        if isKeyPressed(38):    # UP
            if self.getY() > 20:
                self.setY(self.getY() - self.updown)
        if isKeyPressed(40):    # DOWN
            if self.getY() < HEIGHT - 20:
                self.setY(self.getY() + self.updown)
        if isKeyPressed(32):    # SPACE
            self.fire()
        if self.timer == 0:
            self.showNextSprite()
            self.timer = 5
        self.timer -= 1
        self.firecount -= 1

# -------------------------- Missile Class -------------------------- #                
class Missile(Actor):

    def __init__(self):
        Actor.__init__(self, True, os.path.join(DATAPATH, "missile.png"))
        self.speed = 5
                        
    def act(self):
        self.move(self.speed)
        if self.getX() >= WIDTH + 20:
            win.removeActor(self)
    
    def collide(self, actor1, actor2):
        xpos = actor2.getX()
        ypos = actor2.getY()
        win.removeActor(self)
        win.removeActor(actor2)
        hit = Explosion()
        win.addActor(hit, Location(xpos, ypos))
#        if win.getNumberOfActors(Enemy) == 15:
#            win.doPause()       # für Screenshot
        return 0

# -------------------------- Missile Class -------------------------- #

class Explosion(Actor):
    
    def __init__(self):
        Actor.__init__(self, True, os.path.join(DATAPATH, "explosion.png"))
        self.timer = 5
        
    def act(self):
        self.timer -= 1
        if self.timer <= 0:
            win.removeActor(self)

# -------------------------- Enemy Class ---------------------------- #
class Enemy(Actor):
    
    def __init__(self):
        Actor.__init__(self, True, os.path.join(DATAPATH, "pizza.png"))
        self.speed = randint(3, 6)
        
    def reset(self):
        self.setLocation(Location(WIDTH + randint(30, 100), randint(30, HEIGHT - 30)))
        self.speed = randint(3, 6)
        
    def act(self):
        self.move(-self.speed)
        if self.getX() < -30:
            self.reset()

# -------------------------- Main Loop ---------------------------- #                        
win = makeGameGrid(WIDTH, HEIGHT, 1, Color.GRAY, True)
win.setTitle("Pizza Plane Stage 4: Shooting the Killer Pizzas")
win.setSimulationPeriod(20)

bg1 = Background()
bg2 = Background()
win.addActor(bg1, Location(BGWIDTH2, 260))
win.addActor(bg2, Location(BGWIDTH + BGWIDTH2, 260))
for _ in range(NO_ENEMIES):
    pizza = Enemy()
    win.addActor(pizza, Location(WIDTH + randint(30, 400), randint(30, HEIGHT - 30)))
redBaron = Plane()
win.addActor(redBaron, Location(70, 200))

win.show()
win.doRun()

Die Herkunft der Assets habe ich im Kopf in den Kommentaren nachgewiesen. Und wie immer gibt es den Quellcode und alle Assets in meinem GitLab-Repositorium zu TigerJython.

Die bisher erschienenen Beiträge in dieser Reihe sind:

Fortsetzung folgt …


(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