Weiter mit dem kleinen, grünen Flieger auf Trinket: Jetzt mit Pizzas
Mit der Portierung meines kleinen, grünen Fliegers von Pygame nach Trinket (in der Processing.py-Variante) bin ich ein gutes Stück vorangekommen. Ich habe dem Flieger Pizzas als Gegner spendiert (denn was ist ein Pizzaflieger ohne Pizzas? Nur ein simples Flugzeug) und Waffen, um diese Pizzen abzuschießen. Doch der Reihe nach:
Getreu dem Motto »Don’t repeat Yourself« habe ich eine Oberklasse Sprite()
implementiert, denn jede (Unter-) Klasse hatte ihre eigene, identische display()
-Methode:
## Oberklasse für alles, was sich bewegt
class Sprite():
def __init__(self):
pass
def display(self):
image(self.img, self.x, self.y)
Und weil ich gerade dabei war, bekam diese Klasse auch noch eine Methode (collide_rect()
) zur Kollisionskerkennung verpaßt. Zwar wird diese momentan nur von der Klasse Missile()
(siehe weiter unten) aufgerufen, aber in einer späteren Version soll auch der Flieger erkennen, ob er mit einer Pizza (oder anderen Gegnern) kollidiert, und dann benötigt er diese Methode ebenfalls:
def collide_rect(self, other):
distance_x = (self.x + self.w/2) - (other.x + other.w/2)
distance_y = (self.y + self.h/2) - (other.y + other.h/2)
half_w = self.w/2 + other.w/2
half_h = self.h/2 + other.h/2
if (abs(distance_x) < half_w):
if (abs(distance_y) < half_h):
return True
return False
Diese Methode und ihre Implementierung hatte ich schon im November 2019 für Processing.py und P5.js ausführlich vorgestellt (im Anschluß an einen Beitrag zur Kollisionserkennung zweier Kreise).
Ein Flieger, der Pizzas abschießen will, braucht natürlich Geschosse. Diese werden durch die Klasse Missile()
repräsentiert:
class Missile(Sprite):
def __init__(self, _x, _y):
Sprite.__init__(self)
self.img = loadImage("missile.png")
self.x = _x
self.y = _y
self.w = 19
self.h = 7
self.speed = 10
def update(self):
self.x += self.speed
for enemy in enemies:
if self.collide_rect(enemy):
missiles.remove(self)
# Enemy Explosion
e_x, e_y = enemy.x, enemy.y - 5
enemy.reset()
hit = Explosion(e_x, e_y)
hits.append(hit)
if self.x >= width + 20:
missiles.remove(self)
Diese Geschosse leben entweder so lange, bis sie rechts den Bildschirm verlassen oder auf eine Pizza treffen. Wenn solch ein Geschoß auf eine Pizza trifft, löst es eine Explosion aus. Diese besitzt natürlich eine eigene Klasse:
class Explosion(Sprite):
def __init__(self, _x, _y):
Sprite.__init__(self)
self.img = loadImage("explosion.png")
self.x = _x
self.y = _y
self.w = 38
self.h = 38
self.timer = 10
def update(self):
self.timer -= 1
if self.timer <= 0:
hits.remove(self)
Auch eine Explosion hat natürlich nur eine begrenzte Lebensdauer. Diese wird durch die Variable timer
gesteuert, die von einem Startwert aus zurückgezählt wird. Wird der timer
kleiner oder gleich Null, ist das Leben der Explosion beendet.
Die Instanzen der Klassen Missile()
und Explosion()
besitzen noch eine weitere Besonderheit. Sie werden nicht in der Hauptschleife des Programms initialisiert, sondern ein Geschoß wird von der Instanz der Klasse Plane()
instanziert, wenn der Spieler die mittlere Maustaste drückt:
def fire(self):
if self.firecount < 0:
missile = Missile(self.x + 20, self.y + 20)
missiles.append(missile)
self.firecount = MAX_FIRECOUNT
Um den Spieler am Dauerfeuer zu hindern, kann ein neues Geschoß erst dann wieder abgefeuert werden, wenn die Variable firecount
, die nach jedem Schuß auf MAX_FIRECOUNT
gesetzt und in jeder Programmschleife um Eins zurückgezählt wird, wieder kleiner oder gleich Null ist.
Und eine Explosion wird in dieser Version von der Klasse Missile()
ausgelöst, wenn diese einen Gegner trifft1.
Last but not least braucht ein Spiel natürlich Gegner. Diese werden durch die Klasse Enemy()
repräsentiert, die beim derzeitigen Stand des Spiels nur pöse Pizzen erzeugt:
class Enemy(Sprite):
def __init__(self, _x, _y):
Sprite.__init__(self)
self.img = loadImage("pizza.png")
self.x = _x
self.y = _y
self.w = 36
self.h = 36
self.speed = randint(3, 6)
def reset(self):
self.x = width + randint(30, 100)
self.y = randint(30, height - 30)
self.speed = randint(3, 6)
def update(self):
self.x -= self.speed
if self.x < -30:
self.reset()
Die Klasse ist relativ einfach gehalten. Neu ist eigentlich nur die Methode reset()
, die die Pizzas nach einem Abschuß oder nachdem sie den linken Bildschirmrand erreicht haben, wieder rechts außen auf eine zufällige Anfangsposition setzt.
Jetzt wie bei jeder neuen Lieferung der vollständige Programmcode, falls Ihr das Programm nachprogrammieren oder remixen wollt:
from processing import *
from random import randint
WIDTH, HEIGHT = 720, 520
BG_WIDTH = 1664
FPS = 60
ANIM = 4 # Animation Cycle
UPDOWN = 3
MAX_FIRECOUNT = 15
NO_ENEMIES = 10
## Oberklasse für alles, was sich bewegt
class Sprite():
def __init__(self):
pass
def display(self):
image(self.img, self.x, self.y)
def collide_rect(self, other):
distance_x = (self.x + self.w/2) - (other.x + other.w/2)
distance_y = (self.y + self.h/2) - (other.y + other.h/2)
half_w = self.w/2 + other.w/2
half_h = self.h/2 + other.h/2
if (abs(distance_x) < half_w):
if (abs(distance_y) < half_h):
return True
return False
class Background(Sprite):
def __init__(self, _x, _y):
Sprite.__init__(self)
self.x = _x
self.y = _y
self.start_x = _x
self.img = loadImage("desert.png")
def update(self):
self.x -= 1
if self.x <= -BG_WIDTH:
self.x = BG_WIDTH
class Missile(Sprite):
def __init__(self, _x, _y):
Sprite.__init__(self)
self.img = loadImage("missile.png")
self.x = _x
self.y = _y
self.w = 19
self.h = 7
self.speed = 10
def update(self):
self.x += self.speed
for enemy in enemies:
if self.collide_rect(enemy):
missiles.remove(self)
# Enemy Explosion
e_x, e_y = enemy.x, enemy.y - 5
enemy.reset()
hit = Explosion(e_x, e_y)
hits.append(hit)
if self.x >= width + 20:
missiles.remove(self)
class Explosion(Sprite):
def __init__(self, _x, _y):
Sprite.__init__(self)
self.img = loadImage("explosion.png")
self.x = _x
self.y = _y
self.w = 38
self.h = 38
self.timer = 10
def update(self):
self.timer -= 1
if self.timer <= 0:
hits.remove(self)
class Plane(Sprite):
def __init__(self):
Sprite.__init__(self)
self.images = []
for i in range(2):
img = loadImage("planegreen_" + str(i) + ".png")
self.images.append(img)
self.img = self.images[0]
self.x = 75
self.y = 240
self.w = 44
self.h = 30
self.dir = "NONE"
self.frame = 0
self.ani = 20
self.firecount = 0
def update(self):
if self.dir == "NONE":
self.y += 0
elif self.dir == "UP":
if self.y > 20:
self.y -= UPDOWN
elif self.dir == "DOWN":
if self.y < height - 20:
self.y += UPDOWN
self.ani += 1
if self.ani >= ANIM:
self.ani = 0
self.frame += 1
if self.frame > 1:
self.frame = 0
self.firecount -= 1
self.img = self.images[self.frame]
def fire(self):
if self.firecount < 0:
missile = Missile(self.x + 20, self.y + 20)
missiles.append(missile)
self.firecount = MAX_FIRECOUNT
class Enemy(Sprite):
def __init__(self, _x, _y):
Sprite.__init__(self)
self.img = loadImage("pizza.png")
self.x = _x
self.y = _y
self.w = 36
self.h = 36
self.speed = randint(3, 6)
def reset(self):
self.x = width + randint(30, 100)
self.y = randint(30, height - 30)
self.speed = randint(3, 6)
def update(self):
self.x -= self.speed
if self.x < -30:
self.reset()
# Listen
backs = []
missiles = []
hits = []
enemies = []
def setup():
global plane
size(WIDTH, HEIGHT)
frameRate(FPS)
print("🍕 Pizza Plane Stage 3 🍕")
print("Linke Maustaste: Flieger fliegt nach oben.")
print("Rechte Maustaste: Flieger fliegt nach unten.")
print("Mittlere Maustaste: Feuern!")
# Hintergrund
back1 = Background(0, 0)
back2 = Background(BG_WIDTH, 0)
backs.append(back1)
backs.append(back2)
# Den Flieger initialisieren
plane = Plane()
# Die Gegner (Pizzas)
for _ in range(NO_ENEMIES):
pizza = Enemy(width + randint(30, 100), randint(30, height - 30))
enemies.append(pizza)
def draw():
background(231, 229, 226) # Wüstenhimmel
for back in backs:
back.update()
back.display()
plane.update()
plane.display()
for missile in missiles:
missile.update()
missile.display()
for enemy in enemies:
enemy.update()
enemy.display()
for hit in hits:
hit.update()
hit.display()
def mousePressed():
if mouseButton == LEFT:
plane.dir = "UP"
elif mouseButton == RIGHT:
plane.dir = "DOWN"
elif mouseButton == CENTER:
plane.fire()
def mouseReleased():
plane.dir = "NONE"
run()
Mit etwa zweihundert Zeilen ist das Programm, das nun in einer spielbaren Version vorliegt, immer noch recht kompakt. Den Quellcode und die Assets gibt es einmal auch in meinem GitHub-Repositorium und dann natürlich auch als Trinket – das macht einen Remix noch einfacher.
Und natürlich darf und will ich die Credits nicht vergessen: Das Hintergrundbild stammt vom User »PWL«, der es auf OpenGameArt.org zur freien Vewendung (CC0) hochgeladen hat. Den grünen Flieger von »pzUH« gibt es als Public Domain ebenfalls auf OpenGameArt.org. Und das Bild der Pizzas, der Geschosse und der Explosion habe ich den freien (CC-BY 4.0) Twemojis von Twitter entnommen und mit der Bildverarbeitung meines Vertrauens ein wenig auf Vordermann gebracht.
Das Programm ist noch nicht komplett. Auf jeden Fall will ich noch eine Kollision der Pizzas mit dem Spieler implementieren, bei der der Spieler Lebenspunkte verliert. Dann möchte ich noch eine Anzeige des Punktestands implementieren. Hierfür muß ich aber erst noch herausbekommen, wie man in Trinket Fonts als Assets installiert. Still digging!
Die bisherigen Beiträge zu dem kleinen, grünen Flieger im Schockwellenreiter:
1. Als Pygame-Projekt:
- Auf ein neues: Pizzaplane in Pygame (Stage 1)
- Jetzt mit Killer-Pizzas: Pizzaplane in Pygame (Stage 2)
- Pizzaplane Stage 3: Jetzt mit Punktestand!
- Pizzaplane Stage 4 – jetzt mit grünem Spieler
2. Exkurse (immer noch Pygame)
- Exkurs 1: Pygame objektorientiert
- Exkurs 2: Pizza Plane Trailer – ebenfalls objektorientiert
3. Und nun als Trinket (mit einer Processing (Python) Variante)
- Pizza Plane OOP (Jetzt in Trinket)
- Weiter mit dem kleinen, grünen Flieger auf Trinket: Jetzt mit Pizzas
Wird fortgesetzt …
Fußnoten
In einer späteren Variante können Explosionen zum Beispiel auch von den Gegnern (Pizzas) ausgelöst werden, wenn diese mit dem Spieler kollidieren.↩︎